nuke old implementation

This commit is contained in:
Ettore Di Giacinto
2025-04-08 22:07:59 +02:00
parent b1d90dbedd
commit 446908b759
44 changed files with 0 additions and 4378 deletions

View File

@@ -1,2 +0,0 @@
models/
db/

26
.env
View File

@@ -1,26 +0,0 @@
# Enable debug mode in the LocalAI API
DEBUG=true
# Where models are stored
MODELS_PATH=/models
# Galleries to use
GALLERIES=[{"name":"model-gallery", "url":"github:go-skynet/model-gallery/index.yaml"}, {"url": "github:go-skynet/model-gallery/huggingface.yaml","name":"huggingface"}]
# Select model configuration in the config directory
#PRELOAD_MODELS_CONFIG=/config/wizardlm-13b.yaml
PRELOAD_MODELS_CONFIG=/config/wizardlm-13b.yaml
#PRELOAD_MODELS_CONFIG=/config/wizardlm-13b-superhot.yaml
# You don't need to put a valid OpenAI key, however, the python libraries expect
# the string to be set or panics
OPENAI_API_KEY=sk---
# Set the OpenAI API base URL to point to LocalAI
DEFAULT_API_BASE=http://api:8080
# Set an image path
IMAGE_PATH=/tmp
# Set number of default threads
THREADS=14

View File

@@ -1,142 +0,0 @@
---
name: 'build container images'
on:
pull_request:
push:
branches:
- main
jobs:
localagi:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Prepare
id: prep
run: |
DOCKER_IMAGE=quay.io/go-skynet/localagi
VERSION=main
SHORTREF=${GITHUB_SHA::8}
# If this is git tag, use the tag name as a docker tag
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
fi
TAGS="${DOCKER_IMAGE}:${VERSION},${DOCKER_IMAGE}:${SHORTREF}"
# If the VERSION looks like a version number, assume that
# this is the most recent version of the image and also
# tag it 'latest'.
if [[ $VERSION =~ ^v[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
TAGS="$TAGS,${DOCKER_IMAGE}:latest"
fi
# Set output parameters.
echo ::set-output name=tags::${TAGS}
echo ::set-output name=docker_image::${DOCKER_IMAGE}
- name: Set up QEMU
uses: docker/setup-qemu-action@master
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@master
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }}
- name: Build
if: github.event_name != 'pull_request'
uses: docker/build-push-action@v4
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./Dockerfile
platforms: linux/amd64
push: true
tags: ${{ steps.prep.outputs.tags }}
- name: Build PRs
if: github.event_name == 'pull_request'
uses: docker/build-push-action@v4
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./Dockerfile
platforms: linux/amd64
push: false
tags: ${{ steps.prep.outputs.tags }}
discord-localagi:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Prepare
id: prep
run: |
DOCKER_IMAGE=quay.io/go-skynet/localagi-discord
VERSION=main
SHORTREF=${GITHUB_SHA::8}
# If this is git tag, use the tag name as a docker tag
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
fi
TAGS="${DOCKER_IMAGE}:${VERSION},${DOCKER_IMAGE}:${SHORTREF}"
# If the VERSION looks like a version number, assume that
# this is the most recent version of the image and also
# tag it 'latest'.
if [[ $VERSION =~ ^v[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
TAGS="$TAGS,${DOCKER_IMAGE}:latest"
fi
# Set output parameters.
echo ::set-output name=tags::${TAGS}
echo ::set-output name=docker_image::${DOCKER_IMAGE}
- name: Set up QEMU
uses: docker/setup-qemu-action@master
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@master
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }}
- name: Build
if: github.event_name != 'pull_request'
uses: docker/build-push-action@v4
with:
builder: ${{ steps.buildx.outputs.name }}
context: ./examples/discord
file: ./examples/discord/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ steps.prep.outputs.tags }}
- name: Build PRs
if: github.event_name == 'pull_request'
uses: docker/build-push-action@v4
with:
builder: ${{ steps.buildx.outputs.name }}
context: ./examples/discord
file: ./examples/discord/Dockerfile
platforms: linux/amd64
push: false
tags: ${{ steps.prep.outputs.tags }}

4
.gitignore vendored
View File

@@ -1,4 +0,0 @@
db/
models/
config.ini
.dockerenv

View File

@@ -1,18 +0,0 @@
FROM python:3.10-bullseye
WORKDIR /app
COPY ./requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
ENV DEBIAN_FRONTEND noninteractive
# Install package dependencies
RUN apt-get update -y && \
apt-get install -y --no-install-recommends \
alsa-utils \
libsndfile1-dev && \
apt-get clean
COPY . /app
RUN pip install .
ENTRYPOINT [ "python", "./main.py" ];

21
LICENSE
View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2023 Ettore Di Giacinto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

184
README.md
View File

@@ -1,184 +0,0 @@
<h1 align="center">
<br>
<img height="300" src="https://github.com/mudler/LocalAGI/assets/2420543/b69817ce-2361-4234-a575-8f578e159f33"> <br>
LocalAGI
<br>
</h1>
[AutoGPT](https://github.com/Significant-Gravitas/Auto-GPT), [babyAGI](https://github.com/yoheinakajima/babyagi), ... and now LocalAGI!
LocalAGI is a small 🤖 virtual assistant that you can run locally, made by the [LocalAI](https://github.com/go-skynet/LocalAI) author and powered by it.
The goal is:
- Keep it simple, hackable and easy to understand
- No API keys needed, No cloud services needed, 100% Local. Tailored for Local use, however still compatible with OpenAI.
- Smart-agent/virtual assistant that can do tasks
- Small set of dependencies
- Run with Docker/Podman/Containers
- Rather than trying to do everything, provide a good starting point for other projects
Note: Be warned! It was hacked in a weekend, and it's just an experiment to see what can be done with local LLMs.
![Screenshot from 2023-08-05 22-40-40](https://github.com/mudler/LocalAGI/assets/2420543/144da83d-3879-44f2-985c-efd690e2b136)
## 🚀 Features
- 🧠 LLM for intent detection
- 🧠 Uses functions for actions
- 📝 Write to long-term memory
- 📖 Read from long-term memory
- 🌐 Internet access for search
- :card_file_box: Write files
- 🔌 Plan steps to achieve a goal
- 🤖 Avatar creation with Stable Diffusion
- 🗨️ Conversational
- 🗣️ Voice synthesis with TTS
## Demo
Search on internet (interactive mode)
https://github.com/mudler/LocalAGI/assets/2420543/23199ca3-7380-4efc-9fac-a6bc2b52bdb3
Plan a road trip (batch mode)
https://github.com/mudler/LocalAGI/assets/2420543/9ba43b82-dec5-432a-bdb9-8318e7db59a4
> Note: The demo is with a GPU and `30b` models size
## :book: Quick start
No frills, just run docker-compose and start chatting with your virtual assistant:
```bash
# Modify the configuration
# vim .env
# first run (and pulling the container)
docker-compose up
# next runs
docker-compose run -i --rm localagi
```
## How to use it
By default localagi starts in interactive mode
### Examples
Road trip planner by limiting searching to internet to 3 results only:
```bash
docker-compose run -i --rm localagi \
--skip-avatar \
--subtask-context \
--postprocess \
--search-results 3 \
--prompt "prepare a plan for my roadtrip to san francisco"
```
Limit results of planning to 3 steps:
```bash
docker-compose run -i --rm localagi \
--skip-avatar \
--subtask-context \
--postprocess \
--search-results 1 \
--prompt "do a plan for my roadtrip to san francisco" \
--plan-message "The assistant replies with a plan of 3 steps to answer the request with a list of subtasks with logical steps. The reasoning includes a self-contained, detailed and descriptive instruction to fullfill the task."
```
### Advanced
localagi has several options in the CLI to tweak the experience:
- `--system-prompt` is the system prompt to use. If not specified, it will use none.
- `--prompt` is the prompt to use for batch mode. If not specified, it will default to interactive mode.
- `--interactive` is the interactive mode. When used with `--prompt` will drop you in an interactive session after the first prompt is evaluated.
- `--skip-avatar` will skip avatar creation. Useful if you want to run it in a headless environment.
- `--re-evaluate` will re-evaluate if another action is needed or we have completed the user request.
- `--postprocess` will postprocess the reasoning for analysis.
- `--subtask-context` will include context in subtasks.
- `--search-results` is the number of search results to use.
- `--plan-message` is the message to use during planning. You can override the message for example to force a plan to have a different message.
- `--tts-api-base` is the TTS API base. Defaults to `http://api:8080`.
- `--localai-api-base` is the LocalAI API base. Defaults to `http://api:8080`.
- `--images-api-base` is the Images API base. Defaults to `http://api:8080`.
- `--embeddings-api-base` is the Embeddings API base. Defaults to `http://api:8080`.
- `--functions-model` is the functions model to use. Defaults to `functions`.
- `--embeddings-model` is the embeddings model to use. Defaults to `all-MiniLM-L6-v2`.
- `--llm-model` is the LLM model to use. Defaults to `gpt-4`.
- `--tts-model` is the TTS model to use. Defaults to `en-us-kathleen-low.onnx`.
- `--stablediffusion-model` is the Stable Diffusion model to use. Defaults to `stablediffusion`.
- `--stablediffusion-prompt` is the Stable Diffusion prompt to use. Defaults to `DEFAULT_PROMPT`.
- `--force-action` will force a specific action.
- `--debug` will enable debug mode.
### Customize
To use a different model, you can see the examples in the `config` folder.
To select a model, modify the `.env` file and change the `PRELOAD_MODELS_CONFIG` variable to use a different configuration file.
### Caveats
The "goodness" of a model has a big impact on how LocalAGI works. Currently `13b` models are powerful enough to actually able to perform multi-step tasks or do more actions. However, it is quite slow when running on CPU (no big surprise here).
The context size is a limitation - you can find in the `config` examples to run with superhot 8k context size, but the quality is not good enough to perform complex tasks.
## What is LocalAGI?
It is a dead simple experiment to show how to tie the various LocalAI functionalities to create a virtual assistant that can do tasks. It is simple on purpose, trying to be minimalistic and easy to understand and customize for everyone.
It is different from babyAGI or AutoGPT as it uses [LocalAI functions](https://localai.io/features/openai-functions/) - it is a from scratch attempt built on purpose to run locally with [LocalAI](https://localai.io) (no API keys needed!) instead of expensive, cloud services. It sets apart from other projects as it strives to be small, and easy to fork on.
### How it works?
`LocalAGI` just does the minimal around LocalAI functions to create a virtual assistant that can do generic tasks. It works by an endless loop of `intent detection`, `function invocation`, `self-evaluation` and `reply generation` (if it decides to reply! :)). The agent is capable of planning complex tasks by invoking multiple functions, and remember things from the conversation.
In a nutshell, it goes like this:
- Decide based on the conversation history if it needs to take an action by using functions. It uses the LLM to detect the intent from the conversation.
- if it need to take an action (e.g. "remember something from the conversation" ) or generate complex tasks ( executing a chain of functions to achieve a goal ) it invokes the functions
- it re-evaluates if it needs to do any other action
- return the result back to the LLM to generate a reply for the user
Under the hood LocalAI converts functions to llama.cpp BNF grammars. While OpenAI fine-tuned a model to reply to functions, LocalAI constrains the LLM to follow grammars. This is a much more efficient way to do it, and it is also more flexible as you can define your own functions and grammars. For learning more about this, check out the [LocalAI documentation](https://localai.io/docs/llm) and my tweet that explains how it works under the hoods: https://twitter.com/mudler_it/status/1675524071457533953.
### Agent functions
The intention of this project is to keep the agent minimal, so can be built on top of it or forked. The agent is capable of doing the following functions:
- remember something from the conversation
- recall something from the conversation
- search something from the internet
- plan a complex task by invoking multiple functions
- write files to disk
## Roadmap
- [x] 100% Local, with Local AI. NO API KEYS NEEDED!
- [x] Create a simple virtual assistant
- [x] Make the virtual assistant do functions like store long-term memory and autonomously search between them when needed
- [x] Create the assistant avatar with Stable Diffusion
- [x] Give it a voice
- [ ] Use weaviate instead of Chroma
- [ ] Get voice input (push to talk or wakeword)
- [ ] Make a REST API (OpenAI compliant?) so can be plugged by e.g. a third party service
- [x] Take a system prompt so can act with a "character" (e.g. "answer in rick and morty style")
## Development
Run docker-compose with main.py checked-out:
```bash
docker-compose run -v main.py:/app/main.py -i --rm localagi
```
## Notes
- a 13b model is enough for doing contextualized research and search/retrieve memory
- a 30b model is enough to generate a roadmap trip plan ( so cool! )
- With superhot models looses its magic, but maybe suitable for search
- Context size is your enemy. `--postprocess` some times helps, but not always
- It can be silly!
- It is slow on CPU, don't expect `7b` models to perform good, and `13b` models perform better but on CPU are quite slow.

View File

@@ -1,45 +0,0 @@
- id: huggingface@TheBloke/WizardLM-13B-V1.1-GGML/wizardlm-13b-v1.1.ggmlv3.q5_K_M.bin
name: "gpt-4"
overrides:
context_size: 2048
mmap: true
f16: true
mirostat: 2
mirostat_tau: 5.0
mirostat_eta: 0.1
parameters:
temperature: 0.1
top_k: 40
top_p: 0.95
- id: model-gallery@stablediffusion
- id: model-gallery@voice-en-us-kathleen-low
- url: github:go-skynet/model-gallery/base.yaml
name: all-MiniLM-L6-v2
overrides:
embeddings: true
backend: huggingface-embeddings
parameters:
model: all-MiniLM-L6-v2
- id: huggingface@TheBloke/WizardLM-13B-V1.1-GGML/wizardlm-13b-v1.1.ggmlv3.q5_K_M.bin
name: functions
overrides:
context_size: 2048
mirostat: 2
mirostat_tau: 5.0
mirostat_eta: 0.1
template:
chat: ""
completion: ""
roles:
assistant: "ASSISTANT:"
system: "SYSTEM:"
assistant_function_call: "FUNCTION_CALL:"
function: "FUNCTION CALL RESULT:"
parameters:
temperature: 0.1
top_k: 40
top_p: 0.95
function:
disable_no_action: true
mmap: true
f16: true

View File

@@ -1,47 +0,0 @@
- id: huggingface@TheBloke/WizardLM-13B-V1-0-Uncensored-SuperHOT-8K-GGML/wizardlm-13b-v1.0-superhot-8k.ggmlv3.q4_K_M.bin
name: "gpt-4"
overrides:
context_size: 8192
mmap: true
f16: true
mirostat: 2
mirostat_tau: 5.0
mirostat_eta: 0.1
parameters:
temperature: 0.1
top_k: 40
top_p: 0.95
rope_freq_scale: 0.25
- id: model-gallery@stablediffusion
- id: model-gallery@voice-en-us-kathleen-low
- url: github:go-skynet/model-gallery/base.yaml
name: all-MiniLM-L6-v2
overrides:
embeddings: true
backend: huggingface-embeddings
parameters:
model: all-MiniLM-L6-v2
- id: huggingface@TheBloke/WizardLM-13B-V1-0-Uncensored-SuperHOT-8K-GGML/wizardlm-13b-v1.0-superhot-8k.ggmlv3.q4_K_M.bin
name: functions
overrides:
context_size: 8192
mirostat: 2
mirostat_tau: 5.0
mirostat_eta: 0.1
template:
chat: ""
completion: ""
roles:
assistant: "ASSISTANT:"
system: "SYSTEM:"
assistant_function_call: "FUNCTION_CALL:"
function: "FUNCTION CALL RESULT:"
parameters:
temperature: 0.1
top_k: 40
top_p: 0.95
rope_freq_scale: 0.25
function:
disable_no_action: true
mmap: true
f16: true

View File

@@ -1,45 +0,0 @@
- id: huggingface@thebloke/wizardlm-13b-v1.0-uncensored-ggml/wizardlm-13b-v1.0-uncensored.ggmlv3.q4_k_m.bin
name: "gpt-4"
overrides:
context_size: 2048
mmap: true
f16: true
mirostat: 2
mirostat_tau: 5.0
mirostat_eta: 0.1
parameters:
temperature: 0.1
top_k: 40
top_p: 0.95
- id: model-gallery@stablediffusion
- id: model-gallery@voice-en-us-kathleen-low
- url: github:go-skynet/model-gallery/base.yaml
name: all-MiniLM-L6-v2
overrides:
embeddings: true
backend: huggingface-embeddings
parameters:
model: all-MiniLM-L6-v2
- id: huggingface@thebloke/wizardlm-13b-v1.0-uncensored-ggml/wizardlm-13b-v1.0-uncensored.ggmlv3.q4_0.bin
name: functions
overrides:
context_size: 2048
mirostat: 2
mirostat_tau: 5.0
mirostat_eta: 0.1
template:
chat: ""
completion: ""
roles:
assistant: "ASSISTANT:"
system: "SYSTEM:"
assistant_function_call: "FUNCTION_CALL:"
function: "FUNCTION CALL RESULT:"
parameters:
temperature: 0.1
top_k: 40
top_p: 0.95
function:
disable_no_action: true
mmap: true
f16: true

View File

@@ -1,47 +0,0 @@
- id: huggingface@TheBloke/WizardLM-Uncensored-SuperCOT-StoryTelling-30B-SuperHOT-8K-GGML/WizardLM-Uncensored-SuperCOT-StoryTelling-30b-superhot-8k.ggmlv3.q4_0.bin
name: "gpt-4"
overrides:
context_size: 8192
mmap: true
f16: true
mirostat: 2
mirostat_tau: 5.0
mirostat_eta: 0.1
parameters:
temperature: 0.1
top_k: 40
top_p: 0.95
rope_freq_scale: 0.25
- id: model-gallery@stablediffusion
- id: model-gallery@voice-en-us-kathleen-low
- url: github:go-skynet/model-gallery/base.yaml
name: all-MiniLM-L6-v2
overrides:
embeddings: true
backend: huggingface-embeddings
parameters:
model: all-MiniLM-L6-v2
- id: huggingface@TheBloke/WizardLM-Uncensored-SuperCOT-StoryTelling-30B-SuperHOT-8K-GGML/WizardLM-Uncensored-SuperCOT-StoryTelling-30b-superhot-8k.ggmlv3.q4_0.bin
name: functions
overrides:
context_size: 8192
mirostat: 2
mirostat_tau: 5.0
mirostat_eta: 0.1
template:
chat: ""
completion: ""
roles:
assistant: "ASSISTANT:"
system: "SYSTEM:"
assistant_function_call: "FUNCTION_CALL:"
function: "FUNCTION CALL RESULT:"
parameters:
temperature: 0.1
top_k: 40
top_p: 0.95
rope_freq_scale: 0.25
function:
disable_no_action: true
mmap: true
f16: true

View File

@@ -1,46 +0,0 @@
- id: huggingface@thebloke/wizardlm-30b-uncensored-ggml/wizardlm-30b-uncensored.ggmlv3.q2_k.bin
galleryModel:
name: "gpt-4"
overrides:
context_size: 4096
mmap: true
f16: true
mirostat: 2
mirostat_tau: 5.0
mirostat_eta: 0.1
parameters:
temperature: 0.1
top_k: 40
top_p: 0.95
- id: model-gallery@stablediffusion
- id: model-gallery@voice-en-us-kathleen-low
- url: github:go-skynet/model-gallery/base.yaml
name: all-MiniLM-L6-v2
overrides:
embeddings: true
backend: huggingface-embeddings
parameters:
model: all-MiniLM-L6-v2
- id: huggingface@thebloke/wizardlm-30b-uncensored-ggml/wizardlm-30b-uncensored.ggmlv3.q2_k.bin
name: functions
overrides:
context_size: 4096
mirostat: 2
mirostat_tau: 5.0
mirostat_eta: 0.1
template:
chat: ""
completion: ""
roles:
assistant: "ASSISTANT:"
system: "SYSTEM:"
assistant_function_call: "FUNCTION_CALL:"
function: "FUNCTION CALL RESULT:"
parameters:
temperature: 0.1
top_k: 40
top_p: 0.95
function:
disable_no_action: true
mmap: true
f16: true

View File

@@ -1,45 +0,0 @@
- id: huggingface@thebloke/wizardlm-7b-v1.0-uncensored-ggml/wizardlm-7b-v1.0-uncensored.ggmlv3.q4_k_m.bin
name: "gpt-4"
overrides:
context_size: 2048
mmap: true
f16: true
mirostat: 2
mirostat_tau: 5.0
mirostat_eta: 0.1
parameters:
temperature: 0.1
top_k: 40
top_p: 0.95
- id: model-gallery@stablediffusion
- id: model-gallery@voice-en-us-kathleen-low
- url: github:go-skynet/model-gallery/base.yaml
name: all-MiniLM-L6-v2
overrides:
embeddings: true
backend: huggingface-embeddings
parameters:
model: all-MiniLM-L6-v2
- id: huggingface@thebloke/wizardlm-7b-v1.0-uncensored-ggml/wizardlm-7b-v1.0-uncensored.ggmlv3.q4_0.bin
name: functions
overrides:
context_size: 2048
mirostat: 2
mirostat_tau: 5.0
mirostat_eta: 0.1
template:
chat: ""
completion: ""
roles:
assistant: "ASSISTANT:"
system: "SYSTEM:"
assistant_function_call: "FUNCTION_CALL:"
function: "FUNCTION CALL RESULT:"
parameters:
temperature: 0.1
top_k: 40
top_p: 0.95
function:
disable_no_action: true
mmap: true
f16: true

View File

@@ -1,31 +0,0 @@
version: "3.9"
services:
api:
image: quay.io/go-skynet/local-ai:master
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/readyz"]
interval: 1m
timeout: 120m
retries: 120
ports:
- 8090:8080
env_file:
- .env
volumes:
- ./models:/models:cached
- ./config:/config:cached
command: ["/usr/bin/local-ai" ]
localagi:
build:
context: .
dockerfile: Dockerfile
devices:
- /dev/snd
depends_on:
api:
condition: service_healthy
volumes:
- ./db:/app/db
- ./data:/data
env_file:
- .env

View File

@@ -1,8 +0,0 @@
FROM python:3.10-bullseye
WORKDIR /app
COPY ./requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
COPY . /app
ENTRYPOINT [ "python", "./main.py" ];

View File

@@ -1,371 +0,0 @@
import openai
#from langchain.embeddings import HuggingFaceEmbeddings
from langchain.embeddings import LocalAIEmbeddings
from langchain.document_loaders import (
SitemapLoader,
# GitHubIssuesLoader,
# GitLoader,
)
import uuid
import sys
from config import config
from queue import Queue
import asyncio
import threading
from localagi import LocalAGI
from loguru import logger
from ascii_magic import AsciiArt
from duckduckgo_search import DDGS
from typing import Dict, List
import os
from langchain.text_splitter import RecursiveCharacterTextSplitter
import discord
import openai
import urllib.request
from datetime import datetime
import json
import os
from io import StringIO
FILE_NAME_FORMAT = '%Y_%m_%d_%H_%M_%S'
EMBEDDINGS_MODEL = config["agent"]["embeddings_model"]
EMBEDDINGS_API_BASE = config["agent"]["embeddings_api_base"]
PERSISTENT_DIR = config["agent"]["persistent_dir"]
MILVUS_HOST = config["agent"]["milvus_host"] if "milvus_host" in config["agent"] else ""
MILVUS_PORT = config["agent"]["milvus_port"] if "milvus_port" in config["agent"] else 0
MEMORY_COLLECTION = config["agent"]["memory_collection"]
DB_DIR = config["agent"]["db_dir"]
MEMORY_CHUNK_SIZE = int(config["agent"]["memory_chunk_size"])
MEMORY_CHUNK_OVERLAP = int(config["agent"]["memory_chunk_overlap"])
MEMORY_RESULTS = int(config["agent"]["memory_results"])
MEMORY_SEARCH_TYPE = config["agent"]["memory_search_type"]
if not os.environ.get("PYSQL_HACK", "false") == "false":
# these three lines swap the stdlib sqlite3 lib with the pysqlite3 package for chroma
__import__('pysqlite3')
import sys
sys.modules['sqlite3'] = sys.modules.pop('pysqlite3')
if MILVUS_HOST == "":
from langchain.vectorstores import Chroma
else:
from langchain.vectorstores import Milvus
embeddings = LocalAIEmbeddings(model=EMBEDDINGS_MODEL,openai_api_base=EMBEDDINGS_API_BASE)
loop = None
channel = None
def call(thing):
return asyncio.run_coroutine_threadsafe(thing,loop).result()
def ingest(a, agent_actions={}, localagi=None):
q = json.loads(a)
chunk_size = MEMORY_CHUNK_SIZE
chunk_overlap = MEMORY_CHUNK_OVERLAP
logger.info(">>> ingesting: ")
logger.info(q)
documents = []
sitemap_loader = SitemapLoader(web_path=q["url"])
text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)
documents.extend(sitemap_loader.load())
texts = text_splitter.split_documents(documents)
if MILVUS_HOST == "":
db = Chroma.from_documents(texts,embeddings,collection_name=MEMORY_COLLECTION, persist_directory=DB_DIR)
db.persist()
db = None
else:
Milvus.from_documents(texts,embeddings,collection_name=MEMORY_COLLECTION, connection_args={"host": MILVUS_HOST, "port": MILVUS_PORT})
return f"Documents ingested"
def create_image(a, agent_actions={}, localagi=None):
q = json.loads(a)
logger.info(">>> creating image: ")
logger.info(q["description"])
size=f"{q['width']}x{q['height']}"
response = openai.Image.create(prompt=q["description"], n=1, size=size)
image_url = response["data"][0]["url"]
image_name = download_image(image_url)
image_path = f"{PERSISTENT_DIR}{image_name}"
file = discord.File(image_path, filename=image_name)
embed = discord.Embed(title="Generated image")
embed.set_image(url=f"attachment://{image_name}")
call(channel.send(file=file, content=f"Here is what I have generated", embed=embed))
return f"Image created: {response['data'][0]['url']}"
def download_image(url: str):
file_name = f"{datetime.now().strftime(FILE_NAME_FORMAT)}.jpg"
full_path = f"{PERSISTENT_DIR}{file_name}"
urllib.request.urlretrieve(url, full_path)
return file_name
### Agent capabilities
### These functions are called by the agent to perform actions
###
def save(memory, agent_actions={}, localagi=None):
q = json.loads(memory)
logger.info(">>> saving to memories: ")
logger.info(q["content"])
if MILVUS_HOST == "":
chroma_client = Chroma(collection_name=MEMORY_COLLECTION,embedding_function=embeddings, persist_directory=DB_DIR)
else:
chroma_client = Milvus(collection_name=MEMORY_COLLECTION,embedding_function=embeddings, connection_args={"host": MILVUS_HOST, "port": MILVUS_PORT})
chroma_client.add_texts([q["content"]],[{"id": str(uuid.uuid4())}])
if MILVUS_HOST == "":
chroma_client.persist()
chroma_client = None
return f"The object was saved permanently to memory."
def search_memory(query, agent_actions={}, localagi=None):
q = json.loads(query)
if MILVUS_HOST == "":
chroma_client = Chroma(collection_name=MEMORY_COLLECTION,embedding_function=embeddings, persist_directory=DB_DIR)
else:
chroma_client = Milvus(collection_name=MEMORY_COLLECTION,embedding_function=embeddings, connection_args={"host": MILVUS_HOST, "port": MILVUS_PORT})
#docs = chroma_client.search(q["keywords"], "mmr")
retriever = chroma_client.as_retriever(search_type=MEMORY_SEARCH_TYPE, search_kwargs={"k": MEMORY_RESULTS})
docs = retriever.get_relevant_documents(q["keywords"])
text_res="Memories found in the database:\n"
sources = set() # To store unique sources
# Collect unique sources
for document in docs:
if "source" in document.metadata:
sources.add(document.metadata["source"])
for doc in docs:
# drop newlines from page_content
content = doc.page_content.replace("\n", " ")
content = " ".join(content.split())
text_res+="- "+content+"\n"
# Print the relevant sources used for the answer
for source in sources:
if source.startswith("http"):
text_res += "" + source + "\n"
chroma_client = None
#if args.postprocess:
# return post_process(text_res)
return text_res
#return localagi.post_process(text_res)
# write file to disk with content
def save_file(arg, agent_actions={}, localagi=None):
arg = json.loads(arg)
file = filename = arg["filename"]
content = arg["content"]
# create persistent dir if does not exist
if not os.path.exists(PERSISTENT_DIR):
os.makedirs(PERSISTENT_DIR)
# write the file in the directory specified
file = os.path.join(PERSISTENT_DIR, filename)
# Check if the file already exists
if os.path.exists(file):
mode = 'a' # Append mode
else:
mode = 'w' # Write mode
with open(file, mode) as f:
f.write(content)
file = discord.File(file, filename=filename)
call(channel.send(file=file, content=f"Here is what I have generated"))
return f"File {file} saved successfully."
def ddg(query: str, num_results: int, backend: str = "api") -> List[Dict[str, str]]:
"""Run query through DuckDuckGo and return metadata.
Args:
query: The query to search for.
num_results: The number of results to return.
Returns:
A list of dictionaries with the following keys:
snippet - The description of the result.
title - The title of the result.
link - The link to the result.
"""
ddgs = DDGS()
try:
results = ddgs.text(
query,
backend=backend,
)
if results is None:
return [{"Result": "No good DuckDuckGo Search Result was found"}]
def to_metadata(result: Dict) -> Dict[str, str]:
if backend == "news":
return {
"date": result["date"],
"title": result["title"],
"snippet": result["body"],
"source": result["source"],
"link": result["url"],
}
return {
"snippet": result["body"],
"title": result["title"],
"link": result["href"],
}
formatted_results = []
for i, res in enumerate(results, 1):
if res is not None:
formatted_results.append(to_metadata(res))
if len(formatted_results) == num_results:
break
except Exception as e:
logger.error(e)
return []
return formatted_results
## Search on duckduckgo
def search_duckduckgo(a, agent_actions={}, localagi=None):
a = json.loads(a)
list=ddg(a["query"], 2)
text_res=""
for doc in list:
text_res+=f"""{doc["link"]}: {doc["title"]} {doc["snippet"]}\n"""
#if args.postprocess:
# return post_process(text_res)
return text_res
#l = json.dumps(list)
#return l
### End Agent capabilities
###
### Agent action definitions
agent_actions = {
"generate_picture": {
"function": create_image,
"plannable": True,
"description": 'For creating a picture, the assistant replies with "generate_picture" and a detailed description, enhancing it with as much detail as possible.',
"signature": {
"name": "generate_picture",
"parameters": {
"type": "object",
"properties": {
"description": {
"type": "string",
},
"width": {
"type": "number",
},
"height": {
"type": "number",
},
},
}
},
},
"search_internet": {
"function": search_duckduckgo,
"plannable": True,
"description": 'For searching the internet with a query, the assistant replies with the action "search_internet" and the query to search.',
"signature": {
"name": "search_internet",
"description": """For searching internet.""",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "information to save"
},
},
}
},
},
"save_file": {
"function": save_file,
"plannable": True,
"description": 'The assistant replies with the action "save_file", the filename and content to save for writing a file to disk permanently. This can be used to store the result of complex actions locally.',
"signature": {
"name": "save_file",
"description": """For saving a file to disk with content.""",
"parameters": {
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "information to save"
},
"content": {
"type": "string",
"description": "information to save"
},
},
}
},
},
"ingest": {
"function": ingest,
"plannable": True,
"description": 'The assistant replies with the action "ingest" when there is an url to a sitemap to ingest memories from.',
"signature": {
"name": "ingest",
"description": """Save or store informations into memory.""",
"parameters": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "information to save"
},
},
"required": ["url"]
}
},
},
"save_memory": {
"function": save,
"plannable": True,
"description": 'The assistant replies with the action "save_memory" and the string to remember or store an information that thinks it is relevant permanently.',
"signature": {
"name": "save_memory",
"description": """Save or store informations into memory.""",
"parameters": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "information to save"
},
},
"required": ["content"]
}
},
},
"search_memory": {
"function": search_memory,
"plannable": True,
"description": 'The assistant replies with the action "search_memory" for searching between its memories with a query term.',
"signature": {
"name": "search_memory",
"description": """Search in memory""",
"parameters": {
"type": "object",
"properties": {
"keywords": {
"type": "string",
"description": "reasoning behind the intent"
},
},
"required": ["keywords"]
}
},
},
}

View File

@@ -1,31 +0,0 @@
[discord]
server_id =
api_key =
[openai]
api_key = sl-d-d-d
[settings]
default_size = 1024x1024
file_path = images/
file_name_format = %Y_%m_%d_%H_%M_%S
[agent]
llm_model = gpt-4
tts_model = en-us-kathleen-low.onnx
tts_api_base = http://api:8080
functions_model = functions
api_base = http://api:8080
stablediffusion_api_base = http://api:8080
stablediffusion_model = stablediffusion
embeddings_model = all-MiniLM-L6-v2
embeddings_api_base = http://api:30316/v1
persistent_dir = /tmp/data
db_dir = /tmp/data/db
milvus_host =
milvus_port =
memory_collection = localai
memory_chunk_size = 600
memory_chunk_overlap = 110
memory_results = 3
memory_search_type = mmr

View File

@@ -1,5 +0,0 @@
from configparser import ConfigParser
config_file = "config.ini"
config = ConfigParser(interpolation=None)
config.read(config_file)

View File

@@ -1,6 +0,0 @@
#!/bin/bash
pip uninstall hnswlib chromadb-hnswlib -y
pip install hnswlib chromadb-hnswlib
cd /app
python3 /app/main.py

View File

@@ -1,292 +0,0 @@
"""
This is a discord bot for generating images using OpenAI's DALL-E
Author: Stefan Rial
YouTube: https://youtube.com/@StefanRial
GitHub: https://https://github.com/StefanRial/ClaudeBot
E-Mail: mail.stefanrial@gmail.com
"""
from config import config
import os
OPENAI_API_KEY = config["openai"][str("api_key")]
if OPENAI_API_KEY == "":
OPENAI_API_KEY = "foo"
if "OPENAI_API_BASE" not in os.environ:
os.environ["OPENAI_API_BASE"] = config["agent"]["api_base"]
os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY
import openai
import discord
import urllib.request
from datetime import datetime
from queue import Queue
import agent
from agent import agent_actions
from localagi import LocalAGI
import asyncio
import threading
from discord import app_commands
import functools
import typing
SERVER_ID = config["discord"]["server_id"]
DISCORD_API_KEY = config["discord"][str("api_key")]
OPENAI_ORG = config["openai"][str("organization")]
FILE_PATH = config["settings"][str("file_path")]
FILE_NAME_FORMAT = config["settings"][str("file_name_format")]
CRITIC = config["settings"]["critic"] if "critic" in config["settings"] else False
SIZE_LARGE = "1024x1024"
SIZE_MEDIUM = "512x512"
SIZE_SMALL = "256x256"
SIZE_DEFAULT = config["settings"][str("default_size")]
GUILD = discord.Object(id=SERVER_ID)
if not os.path.isdir(FILE_PATH):
os.mkdir(FILE_PATH)
class Client(discord.Client):
def __init__(self, *, intents: discord.Intents):
super().__init__(intents=intents)
self.tree = app_commands.CommandTree(self)
async def setup_hook(self):
self.tree.copy_global_to(guild=GUILD)
await self.tree.sync(guild=GUILD)
claude_intents = discord.Intents.default()
claude_intents.messages = True
claude_intents.message_content = True
client = Client(intents=claude_intents)
openai.organization = OPENAI_ORG
openai.api_key = OPENAI_API_KEY
openai.Model.list()
async def close_thread(thread: discord.Thread):
await thread.edit(name="closed")
await thread.send(
embed=discord.Embed(
description="**Thread closed** - Context limit reached, closing...",
color=discord.Color.blue(),
)
)
await thread.edit(archived=True, locked=True)
@client.event
async def on_ready():
print(f"We have logged in as {client.user}")
def diff(history, processed):
return [item for item in processed if item not in history]
def analyze_history(history, processed, callback, channel):
diff_list = diff(history, processed)
for item in diff_list:
if item["role"] == "function":
content = item["content"]
# Function result
callback(channel.send(f"⚙️ Processed: {content}"))
if item["role"] == "assistant" and "function_call" in item:
function_name = item["function_call"]["name"]
function_parameters = item["function_call"]["arguments"]
# Function call
callback(channel.send(f"⚙️ Called: {function_name} with {function_parameters}"))
def run_localagi_thread_history(history, message, thread, loop):
agent.channel = message.channel
def call(thing):
return asyncio.run_coroutine_threadsafe(thing,loop).result()
sent_message = call(thread.send(f"⚙️ LocalAGI starts"))
user = message.author
def action_callback(name, parameters):
call(sent_message.edit(content=f"⚙️ Calling function '{name}' with {parameters}"))
def reasoning_callback(name, reasoning):
call(sent_message.edit(content=f"🤔 I'm thinking... '{reasoning}' (calling '{name}'), please wait.."))
localagi = LocalAGI(
agent_actions=agent_actions,
llm_model=config["agent"]["llm_model"],
tts_model=config["agent"]["tts_model"],
action_callback=action_callback,
reasoning_callback=reasoning_callback,
tts_api_base=config["agent"]["tts_api_base"],
functions_model=config["agent"]["functions_model"],
api_base=config["agent"]["api_base"],
stablediffusion_api_base=config["agent"]["stablediffusion_api_base"],
stablediffusion_model=config["agent"]["stablediffusion_model"],
)
# remove bot ID from the message content
message.content = message.content.replace(f"<@{client.user.id}>", "")
conversation_history = localagi.evaluate(
message.content,
history,
subtaskContext=True,
critic=CRITIC,
)
analyze_history(history, conversation_history, call, thread)
call(sent_message.edit(content=f"<@{user.id}> {conversation_history[-1]['content']}"))
def run_localagi_message(message, loop):
agent.channel = message.channel
def call(thing):
return asyncio.run_coroutine_threadsafe(thing,loop).result()
sent_message = call(message.channel.send(f"⚙️ LocalAGI starts"))
user = message.author
def action_callback(name, parameters):
call(sent_message.edit(content=f"⚙️ Calling function '{name}' with {parameters}"))
def reasoning_callback(name, reasoning):
call(sent_message.edit(content=f"🤔 I'm thinking... '{reasoning}' (calling '{name}'), please wait.."))
localagi = LocalAGI(
agent_actions=agent_actions,
llm_model=config["agent"]["llm_model"],
tts_model=config["agent"]["tts_model"],
action_callback=action_callback,
reasoning_callback=reasoning_callback,
tts_api_base=config["agent"]["tts_api_base"],
functions_model=config["agent"]["functions_model"],
api_base=config["agent"]["api_base"],
stablediffusion_api_base=config["agent"]["stablediffusion_api_base"],
stablediffusion_model=config["agent"]["stablediffusion_model"],
)
# remove bot ID from the message content
message.content = message.content.replace(f"<@{client.user.id}>", "")
conversation_history = localagi.evaluate(
message.content,
[],
critic=CRITIC,
subtaskContext=True,
)
analyze_history([], conversation_history, call, message.channel)
call(sent_message.edit(content=f"<@{user.id}> {conversation_history[-1]['content']}"))
def run_localagi(interaction, prompt, loop):
agent.channel = interaction.channel
def call(thing):
return asyncio.run_coroutine_threadsafe(thing,loop).result()
user = interaction.user
embed = discord.Embed(
description=f"<@{user.id}> wants to chat! 🤖💬",
color=discord.Color.green(),
)
embed.add_field(name=user.name, value=prompt)
call(interaction.response.send_message(embed=embed))
response = call(interaction.original_response())
# create the thread
thread = call(response.create_thread(
name=prompt,
slowmode_delay=1,
reason="gpt-bot",
auto_archive_duration=60,
))
thread.typing()
sent_message = call(thread.send(f"⚙️ LocalAGI starts"))
messages = []
def action_callback(name, parameters):
call(sent_message.edit(content=f"⚙️ Calling function '{name}' with {parameters}"))
def reasoning_callback(name, reasoning):
call(sent_message.edit(content=f"🤔 I'm thinking... '{reasoning}' (calling '{name}'), please wait.."))
localagi = LocalAGI(
agent_actions=agent_actions,
llm_model=config["agent"]["llm_model"],
tts_model=config["agent"]["tts_model"],
action_callback=action_callback,
reasoning_callback=reasoning_callback,
tts_api_base=config["agent"]["tts_api_base"],
functions_model=config["agent"]["functions_model"],
api_base=config["agent"]["api_base"],
stablediffusion_api_base=config["agent"]["stablediffusion_api_base"],
stablediffusion_model=config["agent"]["stablediffusion_model"],
)
# remove bot ID from the message content
prompt = prompt.replace(f"<@{client.user.id}>", "")
conversation_history = localagi.evaluate(
prompt,
messages,
subtaskContext=True,
critic=CRITIC,
)
analyze_history(messages, conversation_history, call, interaction.channel)
call(sent_message.edit(content=f"<@{user.id}> {conversation_history[-1]['content']}"))
@client.tree.command()
@app_commands.describe(prompt="Ask me anything!")
async def localai(interaction: discord.Interaction, prompt: str):
loop = asyncio.get_running_loop()
threading.Thread(target=run_localagi, args=[interaction, prompt,loop]).start()
# https://github.com/openai/gpt-discord-bot/blob/1161634a59c6fb642e58edb4f4fa1a46d2883d3b/src/utils.py#L15
def discord_message_to_message(message):
if (
message.type == discord.MessageType.thread_starter_message
and message.reference.cached_message
and len(message.reference.cached_message.embeds) > 0
and len(message.reference.cached_message.embeds[0].fields) > 0
):
field = message.reference.cached_message.embeds[0].fields[0]
if field.value:
return { "role": "user", "content": field.value }
else:
if message.content:
return { "role": "user", "content": message.content }
return None
@client.event
async def on_ready():
loop = asyncio.get_running_loop()
agent.loop = loop
@client.event
async def on_message(message):
# ignore messages from the bot
if message.author == client.user:
return
loop = asyncio.get_running_loop()
# ignore messages not in a thread
channel = message.channel
if not isinstance(channel, discord.Thread) and client.user.mentioned_in(message):
threading.Thread(target=run_localagi_message, args=[message,loop]).start()
return
if not isinstance(channel, discord.Thread):
return
# ignore threads not created by the bot
thread = channel
if thread.owner_id != client.user.id:
return
if thread.message_count > 5:
# too many messages, no longer going to reply
await close_thread(thread=thread)
return
channel_messages = [
discord_message_to_message(message)
async for message in thread.history(limit=5)
]
channel_messages = [x for x in channel_messages if x is not None]
channel_messages.reverse()
threading.Thread(target=run_localagi_thread_history, args=[channel_messages[:-1],message,thread,loop]).start()
client.run(DISCORD_API_KEY)

View File

@@ -1,11 +0,0 @@
discord
openai
git+https://github.com/mudler/LocalAGI
ascii-magic
loguru
duckduckgo_search==4.1.1
chromadb
pysqlite3-binary
langchain
beautifulsoup4
pymilvus

View File

@@ -1,21 +0,0 @@
SLACK_APP_TOKEN=xapp-
SLACK_BOT_TOKEN=xoxb-
OPENAI_API_KEY=fake
OPENAI_SYSTEM_TEXT=Default System Text
OPENAI_TIMEOUT_SECONDS=30
OPENAI_MODEL=gpt-3.5-turbo
USE_SLACK_LANGUAGE=true
SLACK_APP_LOG_LEVEL=DEBUG
TRANSLATE_MARKDOWN=false
OPENAI_API_BASE=http://localhost:8080/v1
EMBEDDINGS_MODEL=all-MiniLM-L6-v2
EMBEDDINGS_API_BASE=http://localhost:8080/v1
LOCALAI_API_BASE=http://localhost:8080/v1
TTS_API_BASE=http://localhost:8080/v1
IMAGES_API_BASE=http://localhost:8080/v1
STABLEDIFFUSION_MODEL=dreamshaper
FUNCTIONS_MODEL=gpt-3.5-turbo
LLM_MODEL=gpt-3.5-turbo
TTS_MODEL=en-us-kathleen-low.onnx
PERSISTENT_DIR=/data

View File

@@ -1,17 +0,0 @@
FROM python:3.11.3-slim-buster
WORKDIR /app/
COPY requirements.txt /app/
RUN apt-get update && apt-get install build-essential git -y
RUN pip install -U pip && pip install -r requirements.txt
COPY *.py /app/
COPY *.sh /app/
RUN mkdir /app/app/
COPY app/*.py /app/app/
ENTRYPOINT /app/entrypoint.sh
# docker build . -t your-repo/chat-gpt-in-slack
# export SLACK_APP_TOKEN=xapp-...
# export SLACK_BOT_TOKEN=xoxb-...
# export OPENAI_API_KEY=sk-...
# docker run -e SLACK_APP_TOKEN=$SLACK_APP_TOKEN -e SLACK_BOT_TOKEN=$SLACK_BOT_TOKEN -e OPENAI_API_KEY=$OPENAI_API_KEY -it your-repo/chat-gpt-in-slack

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) Slack Technologies, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,396 +0,0 @@
import openai
#from langchain.embeddings import HuggingFaceEmbeddings
from langchain.embeddings import LocalAIEmbeddings
from langchain.document_loaders import (
SitemapLoader,
# GitHubIssuesLoader,
# GitLoader,
)
import uuid
import sys
from app.env import *
from queue import Queue
import asyncio
import threading
from localagi import LocalAGI
from ascii_magic import AsciiArt
from duckduckgo_search import DDGS
from typing import Dict, List
import os
from langchain.text_splitter import RecursiveCharacterTextSplitter
import openai
import urllib.request
from datetime import datetime
import json
import os
from io import StringIO
FILE_NAME_FORMAT = '%Y_%m_%d_%H_%M_%S'
if not os.environ.get("PYSQL_HACK", "false") == "false":
# these three lines swap the stdlib sqlite3 lib with the pysqlite3 package for chroma
__import__('pysqlite3')
import sys
sys.modules['sqlite3'] = sys.modules.pop('pysqlite3')
if MILVUS_HOST == "":
from langchain.vectorstores import Chroma
else:
from langchain.vectorstores import Milvus
embeddings = LocalAIEmbeddings(model=EMBEDDINGS_MODEL,openai_api_base=EMBEDDINGS_API_BASE)
loop = None
channel = None
def call(thing):
return asyncio.run_coroutine_threadsafe(thing,loop).result()
def ingest(a, agent_actions={}, localagi=None):
q = json.loads(a)
chunk_size = MEMORY_CHUNK_SIZE
chunk_overlap = MEMORY_CHUNK_OVERLAP
print(">>> ingesting: ")
print(q)
documents = []
sitemap_loader = SitemapLoader(web_path=q["url"])
text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)
documents.extend(sitemap_loader.load())
texts = text_splitter.split_documents(documents)
if MILVUS_HOST == "":
db = Chroma.from_documents(texts,embeddings,collection_name=MEMORY_COLLECTION, persist_directory=PERSISTENT_DIR)
db.persist()
db = None
else:
Milvus.from_documents(texts,embeddings,collection_name=MEMORY_COLLECTION, connection_args={"host": MILVUS_HOST, "port": MILVUS_PORT})
return f"Documents ingested"
# def create_image(a, agent_actions={}, localagi=None):
# """
# Create an image based on a description using OpenAI's API.
# Args:
# a (str): A JSON string containing the description, width, and height for the image to be created.
# agent_actions (dict, optional): A dictionary of agent actions. Defaults to {}.
# localagi (LocalAGI, optional): An instance of the LocalAGI class. Defaults to None.
# Returns:
# str: A string containing the URL of the created image.
# """
# q = json.loads(a)
# print(">>> creating image: ")
# print(q["description"])
# size=f"{q['width']}x{q['height']}"
# response = openai.Image.create(prompt=q["description"], n=1, size=size)
# image_url = response["data"][0]["url"]
# image_name = download_image(image_url)
# image_path = f"{PERSISTENT_DIR}{image_name}"
# file = discord.File(image_path, filename=image_name)
# embed = discord.Embed(title="Generated image")
# embed.set_image(url=f"attachment://{image_name}")
# call(channel.send(file=file, content=f"Here is what I have generated", embed=embed))
# return f"Image created: {response['data'][0]['url']}"
def download_image(url: str):
file_name = f"{datetime.now().strftime(FILE_NAME_FORMAT)}.jpg"
full_path = f"{PERSISTENT_DIR}{file_name}"
urllib.request.urlretrieve(url, full_path)
return file_name
### Agent capabilities
### These functions are called by the agent to perform actions
###
def save(memory, agent_actions={}, localagi=None):
q = json.loads(memory)
print(">>> saving to memories: ")
print(q["content"])
if MILVUS_HOST == "":
chroma_client = Chroma(collection_name=MEMORY_COLLECTION,embedding_function=embeddings, persist_directory=PERSISTENT_DIR)
else:
chroma_client = Milvus(collection_name=MEMORY_COLLECTION,embedding_function=embeddings, connection_args={"host": MILVUS_HOST, "port": MILVUS_PORT})
chroma_client.add_texts([q["content"]],[{"id": str(uuid.uuid4())}])
if MILVUS_HOST == "":
chroma_client.persist()
chroma_client = None
return f"The object was saved permanently to memory."
def search_memory(query, agent_actions={}, localagi=None):
q = json.loads(query)
if MILVUS_HOST == "":
chroma_client = Chroma(collection_name=MEMORY_COLLECTION,embedding_function=embeddings, persist_directory=PERSISTENT_DIR)
else:
chroma_client = Milvus(collection_name=MEMORY_COLLECTION,embedding_function=embeddings, connection_args={"host": MILVUS_HOST, "port": MILVUS_PORT})
#docs = chroma_client.search(q["keywords"], "mmr")
retriever = chroma_client.as_retriever(search_type=MEMORY_SEARCH_TYPE, search_kwargs={"k": MEMORY_RESULTS})
docs = retriever.get_relevant_documents(q["keywords"])
text_res="Memories found in the database:\n"
sources = set() # To store unique sources
# Collect unique sources
for document in docs:
if "source" in document.metadata:
sources.add(document.metadata["source"])
for doc in docs:
# drop newlines from page_content
content = doc.page_content.replace("\n", " ")
content = " ".join(content.split())
text_res+="- "+content+"\n"
# Print the relevant sources used for the answer
for source in sources:
if source.startswith("http"):
text_res += "" + source + "\n"
chroma_client = None
#if args.postprocess:
# return post_process(text_res)
return text_res
#return localagi.post_process(text_res)
# write file to disk with content
def save_file(arg, agent_actions={}, localagi=None):
arg = json.loads(arg)
file = filename = arg["filename"]
content = arg["content"]
# create persistent dir if does not exist
if not os.path.exists(PERSISTENT_DIR):
os.makedirs(PERSISTENT_DIR)
# write the file in the directory specified
file = os.path.join(PERSISTENT_DIR, filename)
# Check if the file already exists
if os.path.exists(file):
mode = 'a' # Append mode
else:
mode = 'w' # Write mode
with open(file, mode) as f:
f.write(content)
file = discord.File(file, filename=filename)
call(channel.send(file=file, content=f"Here is what I have generated"))
return f"File {file} saved successfully."
def ddg(query: str, num_results: int, backend: str = "api") -> List[Dict[str, str]]:
"""Run query through DuckDuckGo and return metadata.
Args:
query: The query to search for.
num_results: The number of results to return.
Returns:
A list of dictionaries with the following keys:
snippet - The description of the result.
title - The title of the result.
link - The link to the result.
"""
ddgs = DDGS()
try:
results = ddgs.text(
query,
backend=backend,
)
if results is None:
return [{"Result": "No good DuckDuckGo Search Result was found"}]
def to_metadata(result: Dict) -> Dict[str, str]:
if backend == "news":
return {
"date": result["date"],
"title": result["title"],
"snippet": result["body"],
"source": result["source"],
"link": result["url"],
}
return {
"snippet": result["body"],
"title": result["title"],
"link": result["href"],
}
formatted_results = []
for i, res in enumerate(results, 1):
if res is not None:
formatted_results.append(to_metadata(res))
if len(formatted_results) == num_results:
break
except Exception as e:
print(e)
return []
return formatted_results
## Search on duckduckgo
def search_duckduckgo(a, agent_actions={}, localagi=None):
a = json.loads(a)
list=ddg(a["query"], 2)
text_res=""
for doc in list:
text_res+=f"""{doc["link"]}: {doc["title"]} {doc["snippet"]}\n"""
print("Found")
print(text_res)
#if args.postprocess:
# return post_process(text_res)
return text_res
#l = json.dumps(list)
#return l
### End Agent capabilities
###
### Agent action definitions
agent_actions = {
# "generate_picture": {
# "function": create_image,
# "plannable": True,
# "description": 'For creating a picture, the assistant replies with "generate_picture" and a detailed description, enhancing it with as much detail as possible.',
# "signature": {
# "name": "generate_picture",
# "parameters": {
# "type": "object",
# "properties": {
# "description": {
# "type": "string",
# },
# "width": {
# "type": "number",
# },
# "height": {
# "type": "number",
# },
# },
# }
# },
# },
"search_internet": {
"function": search_duckduckgo,
"plannable": True,
"description": 'For searching the internet with a query, the assistant replies with the action "search_internet" and the query to search.',
"signature": {
"name": "search_internet",
"description": """For searching internet.""",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "information to save"
},
},
}
},
},
"save_file": {
"function": save_file,
"plannable": True,
"description": 'The assistant replies with the action "save_file", the filename and content to save for writing a file to disk permanently. This can be used to store the result of complex actions locally.',
"signature": {
"name": "save_file",
"description": """For saving a file to disk with content.""",
"parameters": {
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "information to save"
},
"content": {
"type": "string",
"description": "information to save"
},
},
}
},
},
"ingest": {
"function": ingest,
"plannable": True,
"description": 'The assistant replies with the action "ingest" when there is an url to a sitemap to ingest memories from.',
"signature": {
"name": "ingest",
"description": """Save or store informations into memory.""",
"parameters": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "information to save"
},
},
"required": ["url"]
}
},
},
"save_memory": {
"function": save,
"plannable": True,
"description": 'The assistant replies with the action "save_memory" and the string to remember or store an information that thinks it is relevant permanently.',
"signature": {
"name": "save_memory",
"description": """Save or store informations into memory.""",
"parameters": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "information to save"
},
},
"required": ["content"]
}
},
},
"search_memory": {
"function": search_memory,
"plannable": True,
"description": 'The assistant replies with the action "search_memory" for searching between its memories with a query term.',
"signature": {
"name": "search_memory",
"description": """Search in memory""",
"parameters": {
"type": "object",
"properties": {
"keywords": {
"type": "string",
"description": "reasoning behind the intent"
},
},
"required": ["keywords"]
}
},
},
}
def localagi(q):
localagi = LocalAGI(
agent_actions=agent_actions,
llm_model=LLM_MODEL,
tts_model=VOICE_MODEL,
tts_api_base=TTS_API_BASE,
functions_model=FUNCTIONS_MODEL,
api_base=LOCALAI_API_BASE,
stablediffusion_api_base=IMAGE_API_BASE,
stablediffusion_model=STABLEDIFFUSION_MODEL,
)
conversation_history = []
conversation_history=localagi.evaluate(
q,
conversation_history,
critic=False,
re_evaluate=False,
# Enable to lower context usage but increases LLM calls
postprocess=False,
subtaskContext=True,
)
return conversation_history[-1]["content"]

View File

@@ -1,403 +0,0 @@
import logging
import re
import time
from openai.error import Timeout
from slack_bolt import App, Ack, BoltContext, BoltResponse
from slack_bolt.request.payload_utils import is_event
from slack_sdk.web import WebClient
from app.env import (
OPENAI_TIMEOUT_SECONDS,
SYSTEM_TEXT,
TRANSLATE_MARKDOWN,
)
from app.i18n import translate
from app.openai_ops import (
ask_llm,
format_openai_message_content,
build_system_text,
)
from app.slack_ops import find_parent_message, is_no_mention_thread, post_wip_message, update_wip_message
#
# Listener functions
#
def just_ack(ack: Ack):
ack()
TIMEOUT_ERROR_MESSAGE = (
f":warning: Sorry! It looks like OpenAI didn't respond within {OPENAI_TIMEOUT_SECONDS} seconds. "
"Please try again later. :bow:"
)
DEFAULT_LOADING_TEXT = ":hourglass_flowing_sand: Wait a second, please ..."
def respond_to_app_mention(
context: BoltContext,
payload: dict,
client: WebClient,
logger: logging.Logger,
):
if payload.get("thread_ts") is not None:
parent_message = find_parent_message(
client, context.channel_id, payload.get("thread_ts")
)
if parent_message is not None:
if is_no_mention_thread(context, parent_message):
# The message event handler will reply to this
return
wip_reply = None
# Replace placeholder for Slack user ID in the system prompt
system_text = build_system_text(SYSTEM_TEXT, TRANSLATE_MARKDOWN, context)
messages = [{"role": "system", "content": system_text}]
print("system text:"+system_text, flush=True)
openai_api_key = context.get("OPENAI_API_KEY")
try:
if openai_api_key is None:
client.chat_postMessage(
channel=context.channel_id,
text="To use this app, please configure your OpenAI API key first",
)
return
user_id = context.actor_user_id or context.user_id
content = ""
if payload.get("thread_ts") is not None:
# Mentioning the bot user in a thread
replies_in_thread = client.conversations_replies(
channel=context.channel_id,
ts=payload.get("thread_ts"),
include_all_metadata=True,
limit=1000,
).get("messages", [])
reply = replies_in_thread[-1]
for reply in replies_in_thread:
c = reply["text"]+"\n\n"
content += c
role = "assistant" if reply["user"] == context.bot_user_id else "user"
messages.append(
{
"role": role,
"content": (
format_openai_message_content(
content, TRANSLATE_MARKDOWN
)
),
}
)
else:
# Strip bot Slack user ID from initial message
msg_text = re.sub(f"<@{context.bot_user_id}>\\s*", "", payload["text"])
messages.append(
{
"role": "user",
"content": format_openai_message_content(msg_text, TRANSLATE_MARKDOWN),
}
)
loading_text = translate(
openai_api_key=openai_api_key, context=context, text=DEFAULT_LOADING_TEXT
)
wip_reply = post_wip_message(
client=client,
channel=context.channel_id,
thread_ts=payload["ts"],
loading_text=loading_text,
messages=messages,
user=context.user_id,
)
resp = ask_llm(messages=messages)
print("Reply "+resp)
update_wip_message(
client=client,
channel=context.channel_id,
ts=wip_reply["message"]["ts"],
text=resp,
messages=messages,
user=user_id,
)
except Timeout:
if wip_reply is not None:
text = (
(
wip_reply.get("message", {}).get("text", "")
if wip_reply is not None
else ""
)
+ "\n\n"
+ translate(
openai_api_key=openai_api_key,
context=context,
text=TIMEOUT_ERROR_MESSAGE,
)
)
client.chat_update(
channel=context.channel_id,
ts=wip_reply["message"]["ts"],
text=text,
)
except Exception as e:
text = (
(
wip_reply.get("message", {}).get("text", "")
if wip_reply is not None
else ""
)
+ "\n\n"
+ translate(
openai_api_key=openai_api_key,
context=context,
text=f":warning: Failed to start a conversation with ChatGPT: {e}",
)
)
logger.exception(text, e)
if wip_reply is not None:
client.chat_update(
channel=context.channel_id,
ts=wip_reply["message"]["ts"],
text=text,
)
def respond_to_new_message(
context: BoltContext,
payload: dict,
client: WebClient,
logger: logging.Logger,
):
if payload.get("bot_id") is not None and payload.get("bot_id") != context.bot_id:
# Skip a new message by a different app
return
wip_reply = None
try:
is_in_dm_with_bot = payload.get("channel_type") == "im"
is_no_mention_required = False
thread_ts = payload.get("thread_ts")
if is_in_dm_with_bot is False and thread_ts is None:
return
openai_api_key = context.get("OPENAI_API_KEY")
if openai_api_key is None:
return
messages_in_context = []
if is_in_dm_with_bot is True and thread_ts is None:
# In the DM with the bot
past_messages = client.conversations_history(
channel=context.channel_id,
include_all_metadata=True,
limit=100,
).get("messages", [])
past_messages.reverse()
# Remove old messages
for message in past_messages:
seconds = time.time() - float(message.get("ts"))
if seconds < 86400: # less than 1 day
messages_in_context.append(message)
is_no_mention_required = True
else:
# In a thread with the bot in a channel
messages_in_context = client.conversations_replies(
channel=context.channel_id,
ts=thread_ts,
include_all_metadata=True,
limit=1000,
).get("messages", [])
if is_in_dm_with_bot is True:
is_no_mention_required = True
else:
the_parent_message_found = False
for message in messages_in_context:
if message.get("ts") == thread_ts:
the_parent_message_found = True
is_no_mention_required = is_no_mention_thread(context, message)
break
if the_parent_message_found is False:
parent_message = find_parent_message(
client, context.channel_id, thread_ts
)
if parent_message is not None:
is_no_mention_required = is_no_mention_thread(
context, parent_message
)
messages = []
user_id = context.actor_user_id or context.user_id
last_assistant_idx = -1
indices_to_remove = []
for idx, reply in enumerate(messages_in_context):
maybe_event_type = reply.get("metadata", {}).get("event_type")
if maybe_event_type == "chat-gpt-convo":
if context.bot_id != reply.get("bot_id"):
# Remove messages by a different app
indices_to_remove.append(idx)
continue
maybe_new_messages = (
reply.get("metadata", {}).get("event_payload", {}).get("messages")
)
if maybe_new_messages is not None:
if len(messages) == 0 or user_id is None:
new_user_id = (
reply.get("metadata", {})
.get("event_payload", {})
.get("user")
)
if new_user_id is not None:
user_id = new_user_id
messages = maybe_new_messages
last_assistant_idx = idx
if is_no_mention_required is False:
return
if is_in_dm_with_bot is False and last_assistant_idx == -1:
return
if is_in_dm_with_bot is True:
# To know whether this app needs to start a new convo
if not next(filter(lambda msg: msg["role"] == "system", messages), None):
# Replace placeholder for Slack user ID in the system prompt
system_text = build_system_text(
SYSTEM_TEXT, TRANSLATE_MARKDOWN, context
)
messages.insert(0, {"role": "system", "content": system_text})
filtered_messages_in_context = []
for idx, reply in enumerate(messages_in_context):
# Strip bot Slack user ID from initial message
if idx == 0:
reply["text"] = re.sub(
f"<@{context.bot_user_id}>\\s*", "", reply["text"]
)
if idx not in indices_to_remove:
filtered_messages_in_context.append(reply)
if len(filtered_messages_in_context) == 0:
return
for reply in filtered_messages_in_context:
msg_user_id = reply.get("user")
messages.append(
{
"content": format_openai_message_content(
reply.get("text"), TRANSLATE_MARKDOWN
),
"role": "user",
}
)
loading_text = translate(
openai_api_key=openai_api_key, context=context, text=DEFAULT_LOADING_TEXT
)
wip_reply = post_wip_message(
client=client,
channel=context.channel_id,
thread_ts=payload.get("thread_ts") if is_in_dm_with_bot else payload["ts"],
loading_text=loading_text,
messages=messages,
user=user_id,
)
latest_replies = client.conversations_replies(
channel=context.channel_id,
ts=wip_reply.get("ts"),
include_all_metadata=True,
limit=1000,
)
if latest_replies.get("messages", [])[-1]["ts"] != wip_reply["message"]["ts"]:
# Since a new reply will come soon, this app abandons this reply
client.chat_delete(
channel=context.channel_id,
ts=wip_reply["message"]["ts"],
)
return
resp = ask_llm(messages=messages)
print("Reply "+resp)
update_wip_message(
client=client,
channel=context.channel_id,
ts=wip_reply["message"]["ts"],
text=resp,
messages=messages,
user=user_id,
)
except Timeout:
if wip_reply is not None:
text = (
(
wip_reply.get("message", {}).get("text", "")
if wip_reply is not None
else ""
)
+ "\n\n"
+ translate(
openai_api_key=openai_api_key,
context=context,
text=TIMEOUT_ERROR_MESSAGE,
)
)
client.chat_update(
channel=context.channel_id,
ts=wip_reply["message"]["ts"],
text=text,
)
except Exception as e:
text = (
(
wip_reply.get("message", {}).get("text", "")
if wip_reply is not None
else ""
)
+ "\n\n"
+ f":warning: Failed to reply: {e}"
)
logger.exception(text, e)
if wip_reply is not None:
client.chat_update(
channel=context.channel_id,
ts=wip_reply["message"]["ts"],
text=text,
)
def register_listeners(app: App):
app.event("app_mention")(ack=just_ack, lazy=[respond_to_app_mention])
# app.event("message")(ack=just_ack, lazy=[respond_to_new_message])
MESSAGE_SUBTYPES_TO_SKIP = ["message_changed", "message_deleted"]
# To reduce unnecessary workload in this app,
# this before_authorize function skips message changed/deleted events.
# Especially, "message_changed" events can be triggered many times when the app rapidly updates its reply.
def before_authorize(
body: dict,
payload: dict,
logger: logging.Logger,
next_,
):
if (
is_event(body)
and payload.get("type") == "message"
and payload.get("subtype") in MESSAGE_SUBTYPES_TO_SKIP
):
logger.debug(
"Skipped the following middleware and listeners "
f"for this message event (subtype: {payload.get('subtype')})"
)
return BoltResponse(status=200, body="")
next_()

View File

@@ -1,43 +0,0 @@
import os
DEFAULT_SYSTEM_TEXT = """
"""
SYSTEM_TEXT = os.environ.get("OPENAI_SYSTEM_TEXT", DEFAULT_SYSTEM_TEXT)
DEFAULT_OPENAI_TIMEOUT_SECONDS = 30
OPENAI_TIMEOUT_SECONDS = int(
os.environ.get("OPENAI_TIMEOUT_SECONDS", DEFAULT_OPENAI_TIMEOUT_SECONDS)
)
DEFAULT_OPENAI_MODEL = "gpt-3.5-turbo"
OPENAI_MODEL = os.environ.get("OPENAI_MODEL", DEFAULT_OPENAI_MODEL)
USE_SLACK_LANGUAGE = os.environ.get("USE_SLACK_LANGUAGE", "true") == "true"
SLACK_APP_LOG_LEVEL = os.environ.get("SLACK_APP_LOG_LEVEL", "DEBUG")
TRANSLATE_MARKDOWN = os.environ.get("TRANSLATE_MARKDOWN", "false") == "true"
BASE_PATH = os.environ.get('OPENAI_API_BASE', 'http://localhost:8080/v1')
EMBEDDINGS_MODEL = os.environ.get('EMBEDDINGS_MODEL', "all-MiniLM-L6-v2")
EMBEDDINGS_API_BASE = os.environ.get("EMBEDDINGS_API_BASE", BASE_PATH)
LOCALAI_API_BASE = os.environ.get("LOCALAI_API_BASE", BASE_PATH)
TTS_API_BASE = os.environ.get("TTS_API_BASE", BASE_PATH)
IMAGE_API_BASE = os.environ.get("IMAGES_API_BASE", BASE_PATH)
STABLEDIFFUSION_MODEL = os.environ.get("STABLEDIFFUSION_MODEL", "dreamshaper")
FUNCTIONS_MODEL = os.environ.get("FUNCTIONS_MODEL", OPENAI_MODEL)
LLM_MODEL = os.environ.get("LLM_MODEL", OPENAI_MODEL)
VOICE_MODEL= os.environ.get("TTS_MODEL", "en-us-kathleen-low.onnx" )
PERSISTENT_DIR = os.environ.get("PERSISTENT_DIR", "/data")
MILVUS_HOST = os.environ.get("MILVUS_HOST", "")
MILVUS_PORT = os.environ.get("MILVUS_PORT", 0)
MEMORY_COLLECTION = os.environ.get("MEMORY_COLLECTION", "local")
MEMORY_CHUNK_SIZE = os.environ.get("MEMORY_CHUNK_SIZE", 600)
MEMORY_CHUNK_OVERLAP = os.environ.get("MEMORY_RESULTS", 110)
MEMORY_RESULTS = os.environ.get("MEMORY_RESULTS", 3)
MEMORY_SEARCH_TYPE = os.environ.get("MEMORY_SEARCH_TYPE", "mmr")

View File

@@ -1,75 +0,0 @@
from typing import Optional
import openai
from slack_bolt import BoltContext
from .openai_ops import GPT_3_5_TURBO_0301_MODEL
# All the supported languages for Slack app as of March 2023
_locale_to_lang = {
"en-US": "English",
"en-GB": "English",
"de-DE": "German",
"es-ES": "Spanish",
"es-LA": "Spanish",
"fr-FR": "French",
"it-IT": "Italian",
"pt-BR": "Portuguese",
"ru-RU": "Russian",
"ja-JP": "Japanese",
"zh-CN": "Chinese",
"zh-TW": "Chinese",
"ko-KR": "Korean",
}
def from_locale_to_lang(locale: Optional[str]) -> Optional[str]:
if locale is None:
return None
return _locale_to_lang.get(locale)
_translation_result_cache = {}
def translate(*, openai_api_key: str, context: BoltContext, text: str) -> str:
lang = from_locale_to_lang(context.get("locale"))
if lang is None or lang == "English":
return text
cached_result = _translation_result_cache.get(f"{lang}:{text}")
if cached_result is not None:
return cached_result
response = openai.ChatCompletion.create(
api_key=openai_api_key,
model=GPT_3_5_TURBO_0301_MODEL,
messages=[
{
"role": "system",
"content": "You're the AI model that primarily focuses on the quality of language translation. "
"You must not change the meaning of sentences when translating them into a different language. "
"You must provide direct translation result as much as possible. "
"When the given text is a single verb/noun, its translated text must be a norm/verb form too. "
"Slack's emoji (e.g., :hourglass_flowing_sand:) and mention parts must be kept as-is. "
"Your response must not include any additional notes in English. "
"Your response must omit English version / pronunciation guide for the result. ",
},
{
"role": "user",
"content": f"Can you translate {text} into {lang} in a professional tone? "
"Please respond with the only the translated text in a format suitable for Slack user interface. "
"No need to append any English notes and guides.",
},
],
top_p=1,
n=1,
max_tokens=1024,
temperature=1,
presence_penalty=0,
frequency_penalty=0,
logit_bias={},
user="system",
)
translated_text = response["choices"][0]["message"].get("content")
_translation_result_cache[f"{lang}:{text}"] = translated_text
return translated_text

View File

@@ -1,53 +0,0 @@
import re
# Conversion from Slack mrkdwn to OpenAI markdown
# See also: https://api.slack.com/reference/surfaces/formatting#basics
def slack_to_markdown(content: str) -> str:
# Split the input string into parts based on code blocks and inline code
parts = re.split(r"(```.+?```|`[^`\n]+?`)", content)
# Apply the bold, italic, and strikethrough formatting to text not within code
result = ""
for part in parts:
if part.startswith("```") or part.startswith("`"):
result += part
else:
for o, n in [
(r"\*(?!\s)([^\*\n]+?)(?<!\s)\*", r"**\1**"), # *bold* to **bold**
(r"_(?!\s)([^_\n]+?)(?<!\s)_", r"*\1*"), # _italic_ to *italic*
(r"~(?!\s)([^~\n]+?)(?<!\s)~", r"~~\1~~"), # ~strike~ to ~~strike~~
]:
part = re.sub(o, n, part)
result += part
return result
# Conversion from OpenAI markdown to Slack mrkdwn
# See also: https://api.slack.com/reference/surfaces/formatting#basics
def markdown_to_slack(content: str) -> str:
# Split the input string into parts based on code blocks and inline code
parts = re.split(r"(```.+?```|`[^`\n]+?`)", content)
# Apply the bold, italic, and strikethrough formatting to text not within code
result = ""
for part in parts:
if part.startswith("```") or part.startswith("`"):
result += part
else:
for o, n in [
(
r"\*\*\*(?!\s)([^\*\n]+?)(?<!\s)\*\*\*",
r"_*\1*_",
), # ***bold italic*** to *_bold italic_*
(
r"(?<![\*_])\*(?!\s)([^\*\n]+?)(?<!\s)\*(?![\*_])",
r"_\1_",
), # *italic* to _italic_
(r"\*\*(?!\s)([^\*\n]+?)(?<!\s)\*\*", r"*\1*"), # **bold** to *bold*
(r"__(?!\s)([^_\n]+?)(?<!\s)__", r"*\1*"), # __bold__ to *bold*
(r"~~(?!\s)([^~\n]+?)(?<!\s)~~", r"~\1~"), # ~~strike~~ to ~strike~
]:
part = re.sub(o, n, part)
result += part
return result

View File

@@ -1,234 +0,0 @@
import threading
import time
import re
from typing import List, Dict, Any, Generator
import openai
from openai.error import Timeout
from openai.openai_object import OpenAIObject
import tiktoken
from slack_bolt import BoltContext
from slack_sdk.web import WebClient
from app.markdown import slack_to_markdown, markdown_to_slack
from app.slack_ops import update_wip_message
from app.agent import (
localagi
)
# ----------------------------
# Internal functions
# ----------------------------
MAX_TOKENS = 1024
GPT_3_5_TURBO_0301_MODEL = "gpt-3.5-turbo-0301"
# Format message from Slack to send to OpenAI
def format_openai_message_content(content: str, translate_markdown: bool) -> str:
if content is None:
return None
# Unescape &, < and >, since Slack replaces these with their HTML equivalents
# See also: https://api.slack.com/reference/surfaces/formatting#escaping
content = content.replace("&lt;", "<").replace("&gt;", ">").replace("&amp;", "&")
# Convert from Slack mrkdwn to markdown format
if translate_markdown:
content = slack_to_markdown(content)
return content
def ask_llm(
*,
messages: List[Dict[str, str]],
) -> str:
# Remove old messages to make sure we have room for max_tokens
# See also: https://platform.openai.com/docs/guides/chat/introduction
# > total tokens must be below the models maximum limit (4096 tokens for gpt-3.5-turbo-0301)
# TODO: currently we don't pass gpt-4 to this calculation method
while calculate_num_tokens(messages) >= 4096 - MAX_TOKENS:
removed = False
for i, message in enumerate(messages):
if message["role"] in ("user", "assistant"):
del messages[i]
removed = True
break
if not removed:
# Fall through and let the OpenAI error handler deal with it
break
prompt=""
for i, message in enumerate(messages):
prompt += message["content"] + "\n"
return localagi(prompt)
def consume_openai_stream_to_write_reply(
*,
client: WebClient,
wip_reply: dict,
context: BoltContext,
user_id: str,
messages: List[Dict[str, str]],
steam: Generator[OpenAIObject, Any, None],
timeout_seconds: int,
translate_markdown: bool,
):
start_time = time.time()
assistant_reply: Dict[str, str] = {"role": "assistant", "content": ""}
messages.append(assistant_reply)
word_count = 0
threads = []
try:
loading_character = " ... :writing_hand:"
for chunk in steam:
spent_seconds = time.time() - start_time
if timeout_seconds < spent_seconds:
raise Timeout()
item = chunk.choices[0]
if item.get("finish_reason") is not None:
break
delta = item.get("delta")
if delta.get("content") is not None:
word_count += 1
assistant_reply["content"] += delta.get("content")
if word_count >= 20:
def update_message():
assistant_reply_text = format_assistant_reply(
assistant_reply["content"], translate_markdown
)
wip_reply["message"]["text"] = assistant_reply_text
update_wip_message(
client=client,
channel=context.channel_id,
ts=wip_reply["message"]["ts"],
text=assistant_reply_text + loading_character,
messages=messages,
user=user_id,
)
thread = threading.Thread(target=update_message)
thread.daemon = True
thread.start()
threads.append(thread)
word_count = 0
for t in threads:
try:
if t.is_alive():
t.join()
except Exception:
pass
assistant_reply_text = format_assistant_reply(
assistant_reply["content"], translate_markdown
)
wip_reply["message"]["text"] = assistant_reply_text
update_wip_message(
client=client,
channel=context.channel_id,
ts=wip_reply["message"]["ts"],
text=assistant_reply_text,
messages=messages,
user=user_id,
)
finally:
for t in threads:
try:
if t.is_alive():
t.join()
except Exception:
pass
try:
steam.close()
except Exception:
pass
def calculate_num_tokens(
messages: List[Dict[str, str]],
# TODO: adjustment for gpt-4
model: str = GPT_3_5_TURBO_0301_MODEL,
) -> int:
"""Returns the number of tokens used by a list of messages."""
try:
encoding = tiktoken.encoding_for_model(model)
except KeyError:
encoding = tiktoken.get_encoding("cl100k_base")
if model == GPT_3_5_TURBO_0301_MODEL:
# note: future models may deviate from this
num_tokens = 0
for message in messages:
# every message follows <im_start>{role/name}\n{content}<im_end>\n
num_tokens += 4
for key, value in message.items():
num_tokens += len(encoding.encode(value))
if key == "name": # if there's a name, the role is omitted
num_tokens += -1 # role is always required and always 1 token
num_tokens += 2 # every reply is primed with <im_start>assistant
return num_tokens
else:
error = (
f"Calculating the number of tokens for for model {model} is not yet supported. "
"See https://github.com/openai/openai-python/blob/main/chatml.md "
"for information on how messages are converted to tokens."
)
raise NotImplementedError(error)
# Format message from OpenAI to display in Slack
def format_assistant_reply(content: str, translate_markdown: bool) -> str:
for o, n in [
# Remove leading newlines
("^\n+", ""),
# Remove prepended Slack user ID
("^<@U.*?>\\s?:\\s?", ""),
# Remove OpenAI syntax tags since Slack doesn't render them in a message
("```\\s*[Rr]ust\n", "```\n"),
("```\\s*[Rr]uby\n", "```\n"),
("```\\s*[Ss]cala\n", "```\n"),
("```\\s*[Kk]otlin\n", "```\n"),
("```\\s*[Jj]ava\n", "```\n"),
("```\\s*[Gg]o\n", "```\n"),
("```\\s*[Ss]wift\n", "```\n"),
("```\\s*[Oo]objective[Cc]\n", "```\n"),
("```\\s*[Cc]\n", "```\n"),
("```\\s*[Cc][+][+]\n", "```\n"),
("```\\s*[Cc][Pp][Pp]\n", "```\n"),
("```\\s*[Cc]sharp\n", "```\n"),
("```\\s*[Mm]atlab\n", "```\n"),
("```\\s*[Jj][Ss][Oo][Nn]\n", "```\n"),
("```\\s*[Ll]a[Tt]e[Xx]\n", "```\n"),
("```\\s*bash\n", "```\n"),
("```\\s*zsh\n", "```\n"),
("```\\s*sh\n", "```\n"),
("```\\s*[Ss][Qq][Ll]\n", "```\n"),
("```\\s*[Pp][Hh][Pp]\n", "```\n"),
("```\\s*[Pp][Ee][Rr][Ll]\n", "```\n"),
("```\\s*[Jj]ava[Ss]cript", "```\n"),
("```\\s*[Ty]ype[Ss]cript", "```\n"),
("```\\s*[Pp]ython\n", "```\n"),
]:
content = re.sub(o, n, content)
# Convert from OpenAI markdown to Slack mrkdwn format
if translate_markdown:
content = markdown_to_slack(content)
return content
def build_system_text(
system_text_template: str, translate_markdown: bool, context: BoltContext
):
system_text = system_text_template.format(bot_user_id=context.bot_user_id)
# Translate format hint in system prompt
if translate_markdown is True:
system_text = slack_to_markdown(system_text)
return system_text

View File

@@ -1,110 +0,0 @@
from typing import Optional
from typing import List, Dict
from slack_sdk.web import WebClient, SlackResponse
from slack_bolt import BoltContext
# ----------------------------
# General operations in a channel
# ----------------------------
def find_parent_message(
client: WebClient, channel_id: Optional[str], thread_ts: Optional[str]
) -> Optional[dict]:
if channel_id is None or thread_ts is None:
return None
messages = client.conversations_history(
channel=channel_id,
latest=thread_ts,
limit=1,
inclusive=1,
).get("messages", [])
return messages[0] if len(messages) > 0 else None
def is_no_mention_thread(context: BoltContext, parent_message: dict) -> bool:
parent_message_text = parent_message.get("text", "")
return f"<@{context.bot_user_id}>" in parent_message_text
# ----------------------------
# WIP reply message stuff
# ----------------------------
def post_wip_message(
*,
client: WebClient,
channel: str,
thread_ts: str,
loading_text: str,
messages: List[Dict[str, str]],
user: str,
) -> SlackResponse:
system_messages = [msg for msg in messages if msg["role"] == "system"]
return client.chat_postMessage(
channel=channel,
thread_ts=thread_ts,
text=loading_text,
metadata={
"event_type": "chat-gpt-convo",
"event_payload": {"messages": system_messages, "user": user},
},
)
def update_wip_message(
client: WebClient,
channel: str,
ts: str,
text: str,
messages: List[Dict[str, str]],
user: str,
) -> SlackResponse:
system_messages = [msg for msg in messages if msg["role"] == "system"]
return client.chat_update(
channel=channel,
ts=ts,
text=text,
metadata={
"event_type": "chat-gpt-convo",
"event_payload": {"messages": system_messages, "user": user},
},
)
# ----------------------------
# Home tab
# ----------------------------
DEFAULT_HOME_TAB_MESSAGE = (
"To enable this app in this Slack workspace, you need to save your OpenAI API key. "
"Visit <https://platform.openai.com/account/api-keys|your developer page> to grap your key!"
)
DEFAULT_HOME_TAB_CONFIGURE_LABEL = "Configure"
def build_home_tab(message: str, configure_label: str) -> dict:
return {
"type": "home",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": message,
},
"accessory": {
"action_id": "configure",
"type": "button",
"text": {"type": "plain_text", "text": configure_label},
"style": "primary",
"value": "api_key",
},
}
],
}

View File

@@ -1,12 +0,0 @@
#!/bin/bash
cd /app
pip uninstall hnswlib -y
git clone https://github.com/nmslib/hnswlib.git
cd hnswlib
pip install .
cd ..
python main.py

View File

@@ -1,69 +0,0 @@
import logging
import os
from slack_bolt import App, BoltContext
from slack_sdk.web import WebClient
from slack_sdk.http_retry.builtin_handlers import RateLimitErrorRetryHandler
from app.bolt_listeners import before_authorize, register_listeners
from app.env import *
from app.slack_ops import (
build_home_tab,
DEFAULT_HOME_TAB_MESSAGE,
DEFAULT_HOME_TAB_CONFIGURE_LABEL,
)
from app.i18n import translate
if __name__ == "__main__":
from slack_bolt.adapter.socket_mode import SocketModeHandler
logging.basicConfig(level=SLACK_APP_LOG_LEVEL)
app = App(
token=os.environ["SLACK_BOT_TOKEN"],
before_authorize=before_authorize,
process_before_response=True,
)
app.client.retry_handlers.append(RateLimitErrorRetryHandler(max_retry_count=2))
register_listeners(app)
@app.event("app_home_opened")
def render_home_tab(client: WebClient, context: BoltContext):
already_set_api_key = os.environ["OPENAI_API_KEY"]
text = translate(
openai_api_key=already_set_api_key,
context=context,
text=DEFAULT_HOME_TAB_MESSAGE,
)
configure_label = translate(
openai_api_key=already_set_api_key,
context=context,
text=DEFAULT_HOME_TAB_CONFIGURE_LABEL,
)
client.views_publish(
user_id=context.user_id,
view=build_home_tab(text, configure_label),
)
if USE_SLACK_LANGUAGE is True:
@app.middleware
def set_locale(
context: BoltContext,
client: WebClient,
next_,
):
user_id = context.actor_user_id or context.user_id
user_info = client.users_info(user=user_id, include_locale=True)
context["locale"] = user_info.get("user", {}).get("locale")
next_()
@app.middleware
def set_openai_api_key(context: BoltContext, next_):
context["OPENAI_API_KEY"] = os.environ["OPENAI_API_KEY"]
context["OPENAI_MODEL"] = OPENAI_MODEL
next_()
handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
handler.start()

View File

@@ -1,306 +0,0 @@
# Unzip the dependencies managed by serverless-python-requirements
try:
import unzip_requirements # type:ignore
except ImportError:
pass
#
# Imports
#
import json
import logging
import os
import openai
from slack_sdk.web import WebClient
from slack_sdk.errors import SlackApiError
from slack_sdk.http_retry.builtin_handlers import RateLimitErrorRetryHandler
from slack_bolt import App, Ack, BoltContext
from app.bolt_listeners import register_listeners, before_authorize
from app.env import USE_SLACK_LANGUAGE, SLACK_APP_LOG_LEVEL, DEFAULT_OPENAI_MODEL
from app.slack_ops import (
build_home_tab,
DEFAULT_HOME_TAB_MESSAGE,
DEFAULT_HOME_TAB_CONFIGURE_LABEL,
)
from app.i18n import translate
#
# Product deployment (AWS Lambda)
#
# export SLACK_CLIENT_ID=
# export SLACK_CLIENT_SECRET=
# export SLACK_SIGNING_SECRET=
# export SLACK_SCOPES=app_mentions:read,channels:history,groups:history,im:history,mpim:history,chat:write.public,chat:write,users:read
# export SLACK_INSTALLATION_S3_BUCKET_NAME=
# export SLACK_STATE_S3_BUCKET_NAME=
# export OPENAI_S3_BUCKET_NAME=
# npm install -g serverless
# serverless plugin install -n serverless-python-requirements
# serverless deploy
#
import boto3
from slack_bolt.adapter.aws_lambda import SlackRequestHandler
from slack_bolt.adapter.aws_lambda.lambda_s3_oauth_flow import LambdaS3OAuthFlow
SlackRequestHandler.clear_all_log_handlers()
logging.basicConfig(format="%(asctime)s %(message)s", level=SLACK_APP_LOG_LEVEL)
s3_client = boto3.client("s3")
openai_bucket_name = os.environ["OPENAI_S3_BUCKET_NAME"]
client_template = WebClient()
client_template.retry_handlers.append(RateLimitErrorRetryHandler(max_retry_count=2))
def register_revocation_handlers(app: App):
# Handle uninstall events and token revocations
@app.event("tokens_revoked")
def handle_tokens_revoked_events(
event: dict,
context: BoltContext,
logger: logging.Logger,
):
user_ids = event.get("tokens", {}).get("oauth", [])
if len(user_ids) > 0:
for user_id in user_ids:
app.installation_store.delete_installation(
enterprise_id=context.enterprise_id,
team_id=context.team_id,
user_id=user_id,
)
bots = event.get("tokens", {}).get("bot", [])
if len(bots) > 0:
app.installation_store.delete_bot(
enterprise_id=context.enterprise_id,
team_id=context.team_id,
)
try:
s3_client.delete_object(Bucket=openai_bucket_name, Key=context.team_id)
except Exception as e:
logger.error(
f"Failed to delete an OpenAI auth key: (team_id: {context.team_id}, error: {e})"
)
@app.event("app_uninstalled")
def handle_app_uninstalled_events(
context: BoltContext,
logger: logging.Logger,
):
app.installation_store.delete_all(
enterprise_id=context.enterprise_id,
team_id=context.team_id,
)
try:
s3_client.delete_object(Bucket=openai_bucket_name, Key=context.team_id)
except Exception as e:
logger.error(
f"Failed to delete an OpenAI auth key: (team_id: {context.team_id}, error: {e})"
)
def handler(event, context_):
app = App(
process_before_response=True,
before_authorize=before_authorize,
oauth_flow=LambdaS3OAuthFlow(),
client=client_template,
)
app.oauth_flow.settings.install_page_rendering_enabled = False
register_listeners(app)
register_revocation_handlers(app)
if USE_SLACK_LANGUAGE is True:
@app.middleware
def set_locale(
context: BoltContext,
client: WebClient,
logger: logging.Logger,
next_,
):
bot_scopes = context.authorize_result.bot_scopes
if bot_scopes is not None and "users:read" in bot_scopes:
user_id = context.actor_user_id or context.user_id
try:
user_info = client.users_info(user=user_id, include_locale=True)
context["locale"] = user_info.get("user", {}).get("locale")
except SlackApiError as e:
logger.debug(f"Failed to fetch user info due to {e}")
pass
next_()
@app.middleware
def set_s3_openai_api_key(context: BoltContext, next_):
try:
s3_response = s3_client.get_object(
Bucket=openai_bucket_name, Key=context.team_id
)
config_str: str = s3_response["Body"].read().decode("utf-8")
if config_str.startswith("{"):
config = json.loads(config_str)
context["OPENAI_API_KEY"] = config.get("api_key")
context["OPENAI_MODEL"] = config.get("model")
else:
# The legacy data format
context["OPENAI_API_KEY"] = config_str
context["OPENAI_MODEL"] = DEFAULT_OPENAI_MODEL
except: # noqa: E722
context["OPENAI_API_KEY"] = None
next_()
@app.event("app_home_opened")
def render_home_tab(client: WebClient, context: BoltContext):
message = DEFAULT_HOME_TAB_MESSAGE
configure_label = DEFAULT_HOME_TAB_CONFIGURE_LABEL
try:
s3_client.get_object(Bucket=openai_bucket_name, Key=context.team_id)
message = "This app is ready to use in this workspace :raised_hands:"
except: # noqa: E722
pass
openai_api_key = context.get("OPENAI_API_KEY")
if openai_api_key is not None:
message = translate(
openai_api_key=openai_api_key, context=context, text=message
)
configure_label = translate(
openai_api_key=openai_api_key,
context=context,
text=DEFAULT_HOME_TAB_CONFIGURE_LABEL,
)
client.views_publish(
user_id=context.user_id,
view=build_home_tab(message, configure_label),
)
@app.action("configure")
def handle_some_action(ack, body: dict, client: WebClient, context: BoltContext):
ack()
already_set_api_key = context.get("OPENAI_API_KEY")
api_key_text = "Save your OpenAI API key:"
submit = "Submit"
cancel = "Cancel"
if already_set_api_key is not None:
api_key_text = translate(
openai_api_key=already_set_api_key, context=context, text=api_key_text
)
submit = translate(
openai_api_key=already_set_api_key, context=context, text=submit
)
cancel = translate(
openai_api_key=already_set_api_key, context=context, text=cancel
)
client.views_open(
trigger_id=body["trigger_id"],
view={
"type": "modal",
"callback_id": "configure",
"title": {"type": "plain_text", "text": "OpenAI API Key"},
"submit": {"type": "plain_text", "text": submit},
"close": {"type": "plain_text", "text": cancel},
"blocks": [
{
"type": "input",
"block_id": "api_key",
"label": {"type": "plain_text", "text": api_key_text},
"element": {"type": "plain_text_input", "action_id": "input"},
},
{
"type": "input",
"block_id": "model",
"label": {"type": "plain_text", "text": "OpenAI Model"},
"element": {
"type": "static_select",
"action_id": "input",
"options": [
{
"text": {
"type": "plain_text",
"text": "GPT-3.5 Turbo",
},
"value": "gpt-3.5-turbo",
},
{
"text": {"type": "plain_text", "text": "GPT-4"},
"value": "gpt-4",
},
],
"initial_option": {
"text": {
"type": "plain_text",
"text": "GPT-3.5 Turbo",
},
"value": "gpt-3.5-turbo",
},
},
},
],
},
)
def validate_api_key_registration(ack: Ack, view: dict, context: BoltContext):
already_set_api_key = context.get("OPENAI_API_KEY")
inputs = view["state"]["values"]
api_key = inputs["api_key"]["input"]["value"]
model = inputs["model"]["input"]["selected_option"]["value"]
try:
# Verify if the API key is valid
openai.Model.retrieve(api_key=api_key, id="gpt-3.5-turbo")
try:
# Verify if the given model works with the API key
openai.Model.retrieve(api_key=api_key, id=model)
except Exception:
text = "This model is not yet available for this API key"
if already_set_api_key is not None:
text = translate(
openai_api_key=already_set_api_key, context=context, text=text
)
ack(
response_action="errors",
errors={"model": text},
)
return
ack()
except Exception:
text = "This API key seems to be invalid"
if already_set_api_key is not None:
text = translate(
openai_api_key=already_set_api_key, context=context, text=text
)
ack(
response_action="errors",
errors={"api_key": text},
)
def save_api_key_registration(
view: dict,
logger: logging.Logger,
context: BoltContext,
):
inputs = view["state"]["values"]
api_key = inputs["api_key"]["input"]["value"]
model = inputs["model"]["input"]["selected_option"]["value"]
try:
openai.Model.retrieve(api_key=api_key, id=model)
s3_client.put_object(
Bucket=openai_bucket_name,
Key=context.team_id,
Body=json.dumps({"api_key": api_key, "model": model}),
)
except Exception as e:
logger.exception(e)
app.view("configure")(
ack=validate_api_key_registration,
lazy=[save_api_key_registration],
)
slack_handler = SlackRequestHandler(app=app)
return slack_handler.handle(event, context_)

View File

@@ -1,32 +0,0 @@
display_information:
name: ChatGPT (dev)
features:
app_home:
home_tab_enabled: false
messages_tab_enabled: true
messages_tab_read_only_enabled: false
bot_user:
display_name: ChatGPT Bot (dev)
always_online: true
oauth_config:
scopes:
bot:
- app_mentions:read
- channels:history
- groups:history
- im:history
- mpim:history
- chat:write.public
- chat:write
- users:read
settings:
event_subscriptions:
bot_events:
- app_mention
- message.channels
- message.groups
- message.im
- message.mpim
interactivity:
is_enabled: true
socket_mode_enabled: true

View File

@@ -1,43 +0,0 @@
display_information:
name: ChatGPT
description: Interact with ChatGPT in Slack!
background_color: "#195208"
features:
app_home:
home_tab_enabled: true
messages_tab_enabled: true
messages_tab_read_only_enabled: false
bot_user:
display_name: ChatGPT Bot
always_online: true
oauth_config:
redirect_urls:
- https://TODO.amazonaws.com/slack/oauth_redirect
scopes:
bot:
- app_mentions:read
- channels:history
- groups:history
- im:history
- mpim:history
- chat:write.public
- chat:write
- users:read
settings:
event_subscriptions:
request_url: https://TODO.amazonaws.com/slack/events
bot_events:
- app_home_opened
- app_mention
- app_uninstalled
- message.channels
- message.groups
- message.im
- message.mpim
- tokens_revoked
interactivity:
is_enabled: true
request_url: https://TODO.amazonaws.com/slack/events
org_deploy_enabled: false
socket_mode_enabled: false
token_rotation_enabled: false

View File

@@ -1,15 +0,0 @@
slack-bolt>=1.18.0,<2
lxml==4.9.3
bs4==0.0.1
openai>=0.27.4,<0.28
tiktoken>=0.3.3,<0.4
chromadb==0.3.23
langchain==0.0.242
GitPython==3.1.31
InstructorEmbedding
loguru
git+https://github.com/mudler/LocalAGI
pysqlite3-binary
requests
ascii-magic
duckduckgo_search==4.1.1

View File

@@ -1,2 +0,0 @@
docker build -t slack-bot .
docker run -v $PWD/data:/data --rm -ti --env-file .dockerenv slack-bot

434
main.py
View File

@@ -1,434 +0,0 @@
import openai
#from langchain.embeddings import HuggingFaceEmbeddings
from langchain.embeddings import LocalAIEmbeddings
import uuid
import sys
from localagi import LocalAGI
from loguru import logger
from ascii_magic import AsciiArt
from duckduckgo_search import DDGS
from typing import Dict, List
import os
# these three lines swap the stdlib sqlite3 lib with the pysqlite3 package for chroma
__import__('pysqlite3')
import sys
sys.modules['sqlite3'] = sys.modules.pop('pysqlite3')
from langchain.vectorstores import Chroma
from chromadb.config import Settings
import json
import os
from io import StringIO
# Parse arguments such as system prompt and batch mode
import argparse
parser = argparse.ArgumentParser(description='LocalAGI')
# System prompt
parser.add_argument('--system-prompt', dest='system_prompt', action='store',
help='System prompt to use')
# Batch mode
parser.add_argument('--prompt', dest='prompt', action='store', default=False,
help='Prompt mode')
# Interactive mode
parser.add_argument('--interactive', dest='interactive', action='store_true', default=False,
help='Interactive mode. Can be used with --prompt to start an interactive session')
# skip avatar creation
parser.add_argument('--skip-avatar', dest='skip_avatar', action='store_true', default=False,
help='Skip avatar creation')
# Reevaluate
parser.add_argument('--re-evaluate', dest='re_evaluate', action='store_true', default=False,
help='Reevaluate if another action is needed or we have completed the user request')
# Postprocess
parser.add_argument('--postprocess', dest='postprocess', action='store_true', default=False,
help='Postprocess the reasoning')
# Subtask context
parser.add_argument('--subtask-context', dest='subtaskContext', action='store_true', default=False,
help='Include context in subtasks')
# Search results number
parser.add_argument('--search-results', dest='search_results', type=int, action='store', default=2,
help='Number of search results to return')
# Plan message
parser.add_argument('--plan-message', dest='plan_message', action='store',
help="What message to use during planning",
)
DEFAULT_PROMPT="floating hair, portrait, ((loli)), ((one girl)), cute face, hidden hands, asymmetrical bangs, beautiful detailed eyes, eye shadow, hair ornament, ribbons, bowties, buttons, pleated skirt, (((masterpiece))), ((best quality)), colorful|((part of the head)), ((((mutated hands and fingers)))), deformed, blurry, bad anatomy, disfigured, poorly drawn face, mutation, mutated, extra limb, ugly, poorly drawn hands, missing limb, blurry, floating limbs, disconnected limbs, malformed hands, blur, out of focus, long neck, long body, Octane renderer, lowres, bad anatomy, bad hands, text"
DEFAULT_API_BASE = os.environ.get("DEFAULT_API_BASE", "http://api:8080")
# TTS api base
parser.add_argument('--tts-api-base', dest='tts_api_base', action='store', default=DEFAULT_API_BASE,
help='TTS api base')
# LocalAI api base
parser.add_argument('--localai-api-base', dest='localai_api_base', action='store', default=DEFAULT_API_BASE,
help='LocalAI api base')
# Images api base
parser.add_argument('--images-api-base', dest='images_api_base', action='store', default=DEFAULT_API_BASE,
help='Images api base')
# Embeddings api base
parser.add_argument('--embeddings-api-base', dest='embeddings_api_base', action='store', default=DEFAULT_API_BASE,
help='Embeddings api base')
# Functions model
parser.add_argument('--functions-model', dest='functions_model', action='store', default="functions",
help='Functions model')
# Embeddings model
parser.add_argument('--embeddings-model', dest='embeddings_model', action='store', default="all-MiniLM-L6-v2",
help='Embeddings model')
# LLM model
parser.add_argument('--llm-model', dest='llm_model', action='store', default="gpt-4",
help='LLM model')
# Voice model
parser.add_argument('--tts-model', dest='tts_model', action='store', default="en-us-kathleen-low.onnx",
help='TTS model')
# Stable diffusion model
parser.add_argument('--stablediffusion-model', dest='stablediffusion_model', action='store', default="stablediffusion",
help='Stable diffusion model')
# Stable diffusion prompt
parser.add_argument('--stablediffusion-prompt', dest='stablediffusion_prompt', action='store', default=DEFAULT_PROMPT,
help='Stable diffusion prompt')
# Force action
parser.add_argument('--force-action', dest='force_action', action='store', default="",
help='Force an action')
# Debug mode
parser.add_argument('--debug', dest='debug', action='store_true', default=False,
help='Debug mode')
# Critic mode
parser.add_argument('--critic', dest='critic', action='store_true', default=False,
help='Enable critic')
# Parse arguments
args = parser.parse_args()
STABLEDIFFUSION_MODEL = os.environ.get("STABLEDIFFUSION_MODEL", args.stablediffusion_model)
STABLEDIFFUSION_PROMPT = os.environ.get("STABLEDIFFUSION_PROMPT", args.stablediffusion_prompt)
FUNCTIONS_MODEL = os.environ.get("FUNCTIONS_MODEL", args.functions_model)
EMBEDDINGS_MODEL = os.environ.get("EMBEDDINGS_MODEL", args.embeddings_model)
LLM_MODEL = os.environ.get("LLM_MODEL", args.llm_model)
VOICE_MODEL= os.environ.get("TTS_MODEL",args.tts_model)
STABLEDIFFUSION_MODEL = os.environ.get("STABLEDIFFUSION_MODEL",args.stablediffusion_model)
STABLEDIFFUSION_PROMPT = os.environ.get("STABLEDIFFUSION_PROMPT", args.stablediffusion_prompt)
PERSISTENT_DIR = os.environ.get("PERSISTENT_DIR", "/data")
SYSTEM_PROMPT = ""
if os.environ.get("SYSTEM_PROMPT") or args.system_prompt:
SYSTEM_PROMPT = os.environ.get("SYSTEM_PROMPT", args.system_prompt)
LOCALAI_API_BASE = args.localai_api_base
TTS_API_BASE = args.tts_api_base
IMAGE_API_BASE = args.images_api_base
EMBEDDINGS_API_BASE = args.embeddings_api_base
# Set log level
LOG_LEVEL = "INFO"
def my_filter(record):
return record["level"].no >= logger.level(LOG_LEVEL).no
logger.remove()
logger.add(sys.stderr, filter=my_filter)
if args.debug:
LOG_LEVEL = "DEBUG"
logger.debug("Debug mode on")
FUNCTIONS_MODEL = os.environ.get("FUNCTIONS_MODEL", args.functions_model)
EMBEDDINGS_MODEL = os.environ.get("EMBEDDINGS_MODEL", args.embeddings_model)
LLM_MODEL = os.environ.get("LLM_MODEL", args.llm_model)
VOICE_MODEL= os.environ.get("TTS_MODEL",args.tts_model)
STABLEDIFFUSION_MODEL = os.environ.get("STABLEDIFFUSION_MODEL",args.stablediffusion_model)
STABLEDIFFUSION_PROMPT = os.environ.get("STABLEDIFFUSION_PROMPT", args.stablediffusion_prompt)
PERSISTENT_DIR = os.environ.get("PERSISTENT_DIR", "/data")
SYSTEM_PROMPT = ""
if os.environ.get("SYSTEM_PROMPT") or args.system_prompt:
SYSTEM_PROMPT = os.environ.get("SYSTEM_PROMPT", args.system_prompt)
LOCALAI_API_BASE = args.localai_api_base
TTS_API_BASE = args.tts_api_base
IMAGE_API_BASE = args.images_api_base
EMBEDDINGS_API_BASE = args.embeddings_api_base
## Constants
REPLY_ACTION = "reply"
PLAN_ACTION = "plan"
embeddings = LocalAIEmbeddings(model=EMBEDDINGS_MODEL,openai_api_base=EMBEDDINGS_API_BASE)
chroma_client = Chroma(collection_name="memories", persist_directory="db", embedding_function=embeddings)
# Function to create images with LocalAI
def display_avatar(agi, input_text=STABLEDIFFUSION_PROMPT, model=STABLEDIFFUSION_MODEL):
image_url = agi.get_avatar(input_text, model)
# convert the image to ascii art
my_art = AsciiArt.from_url(image_url)
my_art.to_terminal()
## This function is called to ask the user if does agree on the action to take and execute
def ask_user_confirmation(action_name, action_parameters):
logger.info("==> Ask user confirmation")
logger.info("==> action_name: {action_name}", action_name=action_name)
logger.info("==> action_parameters: {action_parameters}", action_parameters=action_parameters)
# Ask via stdin
logger.info("==> Do you want to execute the action? (y/n)")
user_input = input()
if user_input == "y":
logger.info("==> Executing action")
return True
else:
logger.info("==> Skipping action")
return False
### Agent capabilities
### These functions are called by the agent to perform actions
###
def save(memory, agent_actions={}, localagi=None):
q = json.loads(memory)
logger.info(">>> saving to memories: ")
logger.info(q["content"])
chroma_client.add_texts([q["content"]],[{"id": str(uuid.uuid4())}])
chroma_client.persist()
return f"The object was saved permanently to memory."
def search_memory(query, agent_actions={}, localagi=None):
q = json.loads(query)
docs = chroma_client.similarity_search(q["reasoning"])
text_res="Memories found in the database:\n"
for doc in docs:
text_res+="- "+doc.page_content+"\n"
#if args.postprocess:
# return post_process(text_res)
#return text_res
return localagi.post_process(text_res)
# write file to disk with content
def save_file(arg, agent_actions={}, localagi=None):
arg = json.loads(arg)
filename = arg["filename"]
content = arg["content"]
# create persistent dir if does not exist
if not os.path.exists(PERSISTENT_DIR):
os.makedirs(PERSISTENT_DIR)
# write the file in the directory specified
filename = os.path.join(PERSISTENT_DIR, filename)
with open(filename, 'w') as f:
f.write(content)
return f"File {filename} saved successfully."
def ddg(query: str, num_results: int, backend: str = "api") -> List[Dict[str, str]]:
"""Run query through DuckDuckGo and return metadata.
Args:
query: The query to search for.
num_results: The number of results to return.
Returns:
A list of dictionaries with the following keys:
snippet - The description of the result.
title - The title of the result.
link - The link to the result.
"""
with DDGS() as ddgs:
results = ddgs.text(
query,
backend=backend,
)
if results is None:
return [{"Result": "No good DuckDuckGo Search Result was found"}]
def to_metadata(result: Dict) -> Dict[str, str]:
if backend == "news":
return {
"date": result["date"],
"title": result["title"],
"snippet": result["body"],
"source": result["source"],
"link": result["url"],
}
return {
"snippet": result["body"],
"title": result["title"],
"link": result["href"],
}
formatted_results = []
for i, res in enumerate(results, 1):
if res is not None:
formatted_results.append(to_metadata(res))
if len(formatted_results) == num_results:
break
return formatted_results
## Search on duckduckgo
def search_duckduckgo(a, agent_actions={}, localagi=None):
a = json.loads(a)
list=ddg(a["query"], args.search_results)
text_res=""
for doc in list:
text_res+=f"""{doc["link"]}: {doc["title"]} {doc["snippet"]}\n"""
#if args.postprocess:
# return post_process(text_res)
return text_res
#l = json.dumps(list)
#return l
### End Agent capabilities
###
### Agent action definitions
agent_actions = {
"search_internet": {
"function": search_duckduckgo,
"plannable": True,
"description": 'For searching the internet with a query, the assistant replies with the action "search_internet" and the query to search.',
"signature": {
"name": "search_internet",
"description": """For searching internet.""",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "information to save"
},
},
}
},
},
"save_file": {
"function": save_file,
"plannable": True,
"description": 'The assistant replies with the action "save_file", the filename and content to save for writing a file to disk permanently. This can be used to store the result of complex actions locally.',
"signature": {
"name": "save_file",
"description": """For saving a file to disk with content.""",
"parameters": {
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "information to save"
},
"content": {
"type": "string",
"description": "information to save"
},
},
}
},
},
"save_memory": {
"function": save,
"plannable": True,
"description": 'The assistant replies with the action "save_memory" and the string to remember or store an information that thinks it is relevant permanently.',
"signature": {
"name": "save_memory",
"description": """Save or store informations into memory.""",
"parameters": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "information to save"
},
},
"required": ["content"]
}
},
},
"search_memory": {
"function": search_memory,
"plannable": True,
"description": 'The assistant replies with the action "search_memory" for searching between its memories with a query term.',
"signature": {
"name": "search_memory",
"description": """Search in memory""",
"parameters": {
"type": "object",
"properties": {
"reasoning": {
"type": "string",
"description": "reasoning behind the intent"
},
},
"required": ["reasoning"]
}
},
},
}
if __name__ == "__main__":
conversation_history = []
# Create a LocalAGI instance
logger.info("Creating LocalAGI instance")
localagi = LocalAGI(
agent_actions=agent_actions,
llm_model=LLM_MODEL,
tts_model=VOICE_MODEL,
tts_api_base=TTS_API_BASE,
functions_model=FUNCTIONS_MODEL,
api_base=LOCALAI_API_BASE,
stablediffusion_api_base=IMAGE_API_BASE,
stablediffusion_model=STABLEDIFFUSION_MODEL,
force_action=args.force_action,
plan_message=args.plan_message,
)
# Set a system prompt if SYSTEM_PROMPT is set
if SYSTEM_PROMPT != "":
conversation_history.append({
"role": "system",
"content": SYSTEM_PROMPT
})
logger.info("Welcome to LocalAGI")
# Skip avatar creation if --skip-avatar is set
if not args.skip_avatar:
logger.info("Creating avatar, please wait...")
display_avatar(localagi)
actions = ""
for action in agent_actions:
actions+=" '"+action+"'"
logger.info("LocalAGI internally can do the following actions:{actions}", actions=actions)
if not args.prompt:
logger.info(">>> Interactive mode <<<")
else:
logger.info(">>> Prompt mode <<<")
logger.info(args.prompt)
# IF in prompt mode just evaluate, otherwise loop
if args.prompt:
conversation_history=localagi.evaluate(
args.prompt,
conversation_history,
critic=args.critic,
re_evaluate=args.re_evaluate,
# Enable to lower context usage but increases LLM calls
postprocess=args.postprocess,
subtaskContext=args.subtaskContext,
)
localagi.tts_play(conversation_history[-1]["content"])
if not args.prompt or args.interactive:
# TODO: process functions also considering the conversation history? conversation history + input
logger.info(">>> Ready! What can I do for you? ( try with: plan a roadtrip to San Francisco ) <<<")
while True:
user_input = input(">>> ")
# we are going to use the args to change the evaluation behavior
conversation_history=localagi.evaluate(
user_input,
conversation_history,
critic=args.critic,
re_evaluate=args.re_evaluate,
# Enable to lower context usage but increases LLM calls
postprocess=args.postprocess,
subtaskContext=args.subtaskContext,
)
localagi.tts_play(conversation_history[-1]["content"])

View File

@@ -1,22 +0,0 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "localagi"
version = "0.0.1"
authors = [
{ name="Ettore Di Giacinto", email="mudler@localai.io" },
]
description = "LocalAGI"
readme = "README.md"
requires-python = ">=3.9"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
[project.urls]
"Homepage" = "https://github.com/mudler/LocalAGI"
"Bug Tracker" = "https://github.com/mudler/LocalAGI/issues"

View File

@@ -1,9 +0,0 @@
langchain
langchain-community
openai==0.28
chromadb
pysqlite3-binary
requests
ascii-magic
loguru
duckduckgo_search==4.1.1

View File

@@ -1 +0,0 @@
from .localagi import *

View File

@@ -1,633 +0,0 @@
import os
import openai
import requests
from loguru import logger
import json
DEFAULT_API_BASE = "http://api:8080"
VOICE_MODEL = "en-us-kathleen-low.onnx"
STABLEDIFFUSION_MODEL = "stablediffusion"
FUNCTIONS_MODEL = "functions"
LLM_MODEL = "gpt-4"
# LocalAGI class
class LocalAGI:
# Constructor
def __init__(self,
plan_action="plan",
reply_action="reply",
force_action="",
agent_actions={},
plan_message="",
api_base=DEFAULT_API_BASE,
tts_api_base="",
stablediffusion_api_base="",
tts_model=VOICE_MODEL,
stablediffusion_model=STABLEDIFFUSION_MODEL,
functions_model=FUNCTIONS_MODEL,
llm_model=LLM_MODEL,
tts_player="aplay",
action_callback=None,
reasoning_callback=None,
):
self.api_base = api_base
self.agent_actions = agent_actions
self.plan_message = plan_message
self.force_action = force_action
self.tts_player = tts_player
self.action_callback = action_callback
self.reasoning_callback = reasoning_callback
self.agent_actions[plan_action] = {
"function": self.generate_plan,
"plannable": False,
"description": 'The assistant for solving complex tasks that involves calling more functions in sequence, replies with the action "'+plan_action+'".',
"signature": {
"name": plan_action,
"description": """Plan complex tasks.""",
"parameters": {
"type": "object",
"properties": {
"description": {
"type": "string",
"description": "reasoning behind the planning"
},
},
"required": ["description"]
}
},
}
self.agent_actions[reply_action] = {
"function": None,
"plannable": False,
"description": 'For replying to the user, the assistant replies with the action "'+reply_action+'" and the reply to the user directly when there is nothing to do.',
}
self.tts_api_base = tts_api_base if tts_api_base else self.api_base
self.stablediffusion_api_base = stablediffusion_api_base if stablediffusion_api_base else self.api_base
self.tts_model = tts_model
self.stablediffusion_model = stablediffusion_model
self.functions_model = functions_model
self.llm_model = llm_model
self.reply_action = reply_action
# Function to create images with LocalAI
def get_avatar(self, input_text):
response = openai.Image.create(
prompt=input_text,
n=1,
size="128x128",
api_base=self.sta+"/v1"
)
return response['data'][0]['url']
def tts_play(self, input_text):
output_file_path = '/tmp/output.wav'
self.tts(input_text, output_file_path)
try:
# Use aplay to play the audio
os.system(f"{self.tts_player} {output_file_path}")
# remove the audio file
os.remove(output_file_path)
except:
logger.info('Unable to play audio')
# Function to create audio with LocalAI
def tts(self, input_text, output_file_path):
# strip newlines from text
input_text = input_text.replace("\n", ".")
# get from OPENAI_API_BASE env var
url = self.tts_api_base + '/tts'
headers = {'Content-Type': 'application/json'}
data = {
"input": input_text,
"model": self.tts_model,
}
response = requests.post(url, headers=headers, data=json.dumps(data))
if response.status_code == 200:
with open(output_file_path, 'wb') as f:
f.write(response.content)
logger.info('Audio file saved successfully:', output_file_path)
else:
logger.info('Request failed with status code', response.status_code)
# Function to analyze the user input and pick the next action to do
def needs_to_do_action(self, user_input, agent_actions={}):
if len(agent_actions) == 0:
agent_actions = self.agent_actions
# Get the descriptions and the actions name (the keys)
descriptions=self.action_description("", agent_actions)
messages = [
{"role": "user",
"content": f"""Transcript of AI assistant responding to user requests. Replies with the action to perform and the reasoning.
{descriptions}"""},
{"role": "user",
"content": f"""{user_input}
Function call: """
}
]
functions = [
{
"name": "intent",
"description": """Decide to do an action.""",
"parameters": {
"type": "object",
"properties": {
"confidence": {
"type": "number",
"description": "confidence of the action"
},
"detailed_reasoning": {
"type": "string",
"description": "reasoning behind the intent"
},
# "detailed_reasoning": {
# "type": "string",
# "description": "reasoning behind the intent"
# },
"action": {
"type": "string",
"enum": list(agent_actions.keys()),
"description": "user intent"
},
},
"required": ["action"]
}
},
]
response = openai.ChatCompletion.create(
#model="gpt-3.5-turbo",
model=self.functions_model,
messages=messages,
request_timeout=1200,
functions=functions,
api_base=self.api_base+"/v1",
stop=None,
temperature=0.1,
#function_call="auto"
function_call={"name": "intent"},
)
response_message = response["choices"][0]["message"]
if response_message.get("function_call"):
function_name = response.choices[0].message["function_call"].name
function_parameters = response.choices[0].message["function_call"].arguments
# read the json from the string
res = json.loads(function_parameters)
logger.debug(">>> function name: "+function_name)
logger.debug(">>> function parameters: "+function_parameters)
return res
return {"action": self.reply_action}
# This is used to collect the descriptions of the agent actions, used to populate the LLM prompt
def action_description(self, action, agent_actions):
descriptions=""
# generate descriptions of actions that the agent can pick
for a in agent_actions:
if ( action != "" and action == a ) or (action == ""):
descriptions+=agent_actions[a]["description"]+"\n"
return descriptions
### This function is used to process the functions given a user input.
### It picks a function, executes it and returns the list of messages containing the result.
def process_functions(self, user_input, action="",):
descriptions=self.action_description(action, self.agent_actions)
messages = [
# {"role": "system", "content": "You are a helpful assistant."},
{"role": "user",
"content": f"""Transcript of AI assistant responding to user requests. Replies with the action to perform, including reasoning, and the confidence interval from 0 to 100.
{descriptions}"""},
{"role": "user",
"content": f"""{user_input}
Function call: """
}
]
response = self.function_completion(messages, action=action)
response_message = response["choices"][0]["message"]
response_result = ""
function_result = {}
if response_message.get("function_call"):
function_name = response.choices[0].message["function_call"].name
function_parameters = response.choices[0].message["function_call"].arguments
logger.info("==> function parameters: {function_parameters}",function_parameters=function_parameters)
function_to_call = self.agent_actions[function_name]["function"]
if self.action_callback:
self.action_callback(function_name, function_parameters)
function_result = function_to_call(function_parameters, agent_actions=self.agent_actions, localagi=self)
logger.info("==> function result: {function_result}", function_result=function_result)
messages.append(
{
"role": "assistant",
"content": None,
"function_call": {"name": function_name, "arguments": function_parameters,},
}
)
messages.append(
{
"role": "function",
"name": function_name,
"content": str(function_result)
}
)
return messages, function_result
### function_completion is used to autocomplete functions given a list of messages
def function_completion(self, messages, action=""):
function_call = "auto"
if action != "":
function_call={"name": action}
logger.debug("==> function name: {function_call}", function_call=function_call)
# get the functions from the signatures of the agent actions, if exists
functions = []
for action in self.agent_actions:
if self.agent_actions[action].get("signature"):
functions.append(self.agent_actions[action]["signature"])
response = openai.ChatCompletion.create(
#model="gpt-3.5-turbo",
model=self.functions_model,
messages=messages,
functions=functions,
request_timeout=1200,
stop=None,
api_base=self.api_base+"/v1",
temperature=0.1,
function_call=function_call
)
return response
# Rework the content of each message in the history in a way that is understandable by the LLM
# TODO: switch to templates (?)
def process_history(self, conversation_history):
messages = ""
for message in conversation_history:
# if there is content append it
if message.get("content") and message["role"] == "function":
messages+="Function result: \n" + message["content"]+"\n"
elif message.get("function_call"):
# encode message["function_call" to json and appends it
fcall = json.dumps(message["function_call"])
parameters = "calling " + message["function_call"]["name"]+" with arguments:"
args=json.loads(message["function_call"]["arguments"])
for arg in args:
logger.debug(arg)
logger.debug(args)
v=args[arg]
parameters+=f""" {arg}=\"{v}\""""
messages+= parameters+"\n"
elif message.get("content") and message["role"] == "user":
messages+=message["content"]+"\n"
elif message.get("content") and message["role"] == "assistant":
messages+="Assistant message: "+message["content"]+"\n"
return messages
def converse(self, responses):
response = openai.ChatCompletion.create(
model=self.llm_model,
messages=responses,
stop=None,
api_base=self.api_base+"/v1",
request_timeout=1200,
temperature=0.1,
)
responses.append(
{
"role": "assistant",
"content": response.choices[0].message["content"],
}
)
return responses
### Fine tune a string before feeding into the LLM
def analyze(self, responses, prefix="Analyze the following text highlighting the relevant information and identify a list of actions to take if there are any. If there are errors, suggest solutions to fix them", suffix=""):
string = self.process_history(responses)
messages = []
if prefix != "":
messages = [
{
"role": "user",
"content": f"""{prefix}:
```
{string}
```
""",
}
]
else:
messages = [
{
"role": "user",
"content": f"""{string}""",
}
]
if suffix != "":
messages[0]["content"]+=f"""{suffix}"""
response = openai.ChatCompletion.create(
model=self.llm_model,
messages=messages,
stop=None,
api_base=self.api_base+"/v1",
request_timeout=1200,
temperature=0.1,
)
return response.choices[0].message["content"]
def post_process(self, string):
messages = [
{
"role": "user",
"content": f"""Summarize the following text, keeping the relevant information:
```
{string}
```
""",
}
]
logger.info("==> Post processing: {string}", string=string)
# get the response from the model
response = openai.ChatCompletion.create(
model=self.llm_model,
messages=messages,
api_base=self.api_base+"/v1",
stop=None,
temperature=0.1,
request_timeout=1200,
)
result = response["choices"][0]["message"]["content"]
logger.info("==> Processed: {string}", string=result)
return result
def generate_plan(self, user_input, agent_actions={}, localagi=None):
res = json.loads(user_input)
logger.info("--> Calculating plan: {description}", description=res["description"])
descriptions=self.action_description("",agent_actions)
plan_message = "The assistant replies with a plan to answer the request with a list of subtasks with logical steps. The reasoning includes a self-contained, detailed and descriptive instruction to fullfill the task."
if self.plan_message:
plan_message = self.plan_message
# plan_message = "The assistant replies with a plan of 3 steps to answer the request with a list of subtasks with logical steps. The reasoning includes a self-contained, detailed and descriptive instruction to fullfill the task."
messages = [
{"role": "user",
"content": f"""Transcript of AI assistant responding to user requests.
{descriptions}
Request: {plan_message}
Thought: {res["description"]}
Function call: """
}
]
# get list of plannable actions
plannable_actions = []
for action in agent_actions:
if agent_actions[action]["plannable"]:
# append the key of the dict to plannable_actions
plannable_actions.append(action)
functions = [
{
"name": "plan",
"description": """Decide to do an action.""",
"parameters": {
"type": "object",
"properties": {
"subtasks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"detailed_reasoning": {
"type": "string",
"description": "subtask list",
},
"function": {
"type": "string",
"enum": plannable_actions,
},
},
},
},
},
"required": ["subtasks"]
}
},
]
response = openai.ChatCompletion.create(
#model="gpt-3.5-turbo",
model=self.functions_model,
messages=messages,
functions=functions,
api_base=self.api_base+"/v1",
stop=None,
temperature=0.1,
#function_call="auto"
function_call={"name": "plan"},
)
response_message = response["choices"][0]["message"]
if response_message.get("function_call"):
function_name = response.choices[0].message["function_call"].name
function_parameters = response.choices[0].message["function_call"].arguments
# read the json from the string
res = json.loads(function_parameters)
logger.debug("<<< function name: {function_name} >>>> parameters: {parameters}", function_name=function_name,parameters=function_parameters)
return res
return {"action": self.reply_action}
def evaluate(self,user_input, conversation_history = [], critic=True, re_evaluate=False,re_evaluation_in_progress=False, postprocess=False, subtaskContext=False):
messages = [
{
"role": "user",
"content": user_input,
}
]
conversation_history.extend(messages)
# pulling the old history make the context grow exponentially
# and most importantly it repeates the first message with the commands again and again.
# it needs a bit of cleanup and process the messages and piggyback more LocalAI functions templates
# old_history = process_history(conversation_history)
# action_picker_message = "Conversation history:\n"+old_history
# action_picker_message += "\n"
action_picker_message = "Request: "+user_input
picker_actions = self.agent_actions
if self.force_action:
aa = {}
aa[self.force_action] = self.agent_actions[self.force_action]
picker_actions = aa
logger.info("==> Forcing action to '{action}' as requested by the user", action=self.force_action)
#if re_evaluate and not re_evaluation_in_progress:
# observation = analyze(conversation_history, prefix=True)
# action_picker_message+="\n\Thought: "+observation[-1]["content"]
if re_evaluation_in_progress:
observation = self.analyze(conversation_history)
action_picker_message="Decide from the output below if we have to do another action:\n"
action_picker_message+="```\n"+user_input+"\n```"
action_picker_message+="\n\nObservation: "+observation
# if there is no action to do, we can just reply to the user with REPLY_ACTION
try:
critic_msg=""
if critic:
descriptions=self.action_description("", self.agent_actions)
messages = [
{"role": "user",
"content": f"""Transcript of AI assistant responding to user requests. Replies with the action to perform and the reasoning.
{descriptions}"""},
{"role": "user",
"content": f"""
This is the user input: {user_input}
Decide now the function to call and give a detailed explaination"""
}
]
critic_msg=self.analyze(messages, prefix="", suffix=f"")
logger.info("==> Critic: {critic}", critic=critic_msg)
action = self.needs_to_do_action(action_picker_message+"\n"+critic_msg,agent_actions=picker_actions)
except Exception as e:
logger.error("==> error: ")
logger.error(e)
action = {"action": self.reply_action}
if self.reasoning_callback:
self.reasoning_callback(action["action"], action["detailed_reasoning"])
if action["action"] != self.reply_action:
logger.info("==> LocalAGI wants to call '{action}'", action=action["action"])
#logger.info("==> Observation '{reasoning}'", reasoning=action["detailed_reasoning"])
logger.info("==> Reasoning '{reasoning}'", reasoning=action["detailed_reasoning"])
# Force executing a plan instead
reasoning = action["detailed_reasoning"]
if action["action"] == self.reply_action:
logger.info("==> LocalAGI wants to create a plan that involves more actions ")
#if postprocess:
#reasoning = post_process(reasoning)
function_completion_message=""
if len(conversation_history) > 1:
function_completion_message += self.process_history(conversation_history)+"\n"
function_completion_message += "Request: "+user_input+"\nReasoning: "+reasoning
responses, function_results = self.process_functions(function_completion_message, action=action["action"])
# Critic re-evaluates the action
# if critic:
# critic = self.analyze(responses[1:-1], suffix=f"Analyze if the function that was picked is correct and satisfies the user request from the context above. Suggest a different action if necessary. If the function picked was correct, write the picked function.\n")
# logger.info("==> Critic action: {critic}", critic=critic)
# previous_action = action["action"]
# try:
# action = self.needs_to_do_action(critic,agent_actions=picker_actions)
# if action["action"] != previous_action:
# logger.info("==> Critic decided to change action to: {action}", action=action["action"])
# responses, function_results = self.process_functions(function_completion_message, action=action["action"])
# except Exception as e:
# logger.error("==> error: ")
# logger.error(e)
# action = {"action": self.reply_action}
# Critic re-evaluates the plan
if critic and isinstance(function_results, dict) and function_results.get("subtasks") and len(function_results["subtasks"]) > 0:
critic = self.analyze(responses[1:], prefix="", suffix=f"Analyze if the plan is correct and satisfies the user request from the context above. Suggest a revised plan if necessary.\n")
logger.info("==> Critic plan: {critic}", critic=critic)
responses, function_results = self.process_functions(function_completion_message+"\n"+critic, action=action["action"])
# if there are no subtasks, we can just reply,
# otherwise we execute the subtasks
# First we check if it's an object
if isinstance(function_results, dict) and function_results.get("subtasks") and len(function_results["subtasks"]) > 0:
# cycle subtasks and execute functions
subtask_result=""
for subtask in function_results["subtasks"]:
cr="Request: "+user_input+"\nReasoning: "+action["detailed_reasoning"]+ "\n"
#cr="Request: "+user_input+"\n"
#cr=""
if subtask_result != "" and subtaskContext:
# Include cumulative results of previous subtasks
# TODO: this grows context, maybe we should use a different approach or summarize
##if postprocess:
## cr+= "Subtask results: "+post_process(subtask_result)+"\n"
##else:
cr+="\nAdditional context: ```\n"+subtask_result+"\n```\n"
subtask_reasoning = subtask["detailed_reasoning"]
#cr+="Reasoning: "+action["detailed_reasoning"]+ "\n"
cr+="\nFunction to call:" +subtask["function"]+"\n"
logger.info("==> subtask '{subtask}' ({reasoning})", subtask=subtask["function"], reasoning=subtask_reasoning)
if postprocess:
cr+= "Assistant: "+self.post_process(subtask_reasoning)
else:
cr+= "Assistant: "+subtask_reasoning
subtask_response, function_results = self.process_functions(cr, subtask["function"])
subtask_result+=str(function_results)+"\n"
# if postprocess:
# subtask_result=post_process(subtask_result)
responses.append(subtask_response[-1])
if re_evaluate:
## Better output or this infinite loops..
logger.info("-> Re-evaluate if another action is needed")
## ? conversation history should go after the user_input maybe?
re_eval = ""
# This is probably not needed as already in the history:
#re_eval = user_input +"\n"
#re_eval += "Conversation history: \n"
if postprocess:
re_eval+= self.post_process(self.process_history(responses[1:])) +"\n"
else:
re_eval+= self.process_history(responses[1:]) +"\n"
responses = self.evaluate(re_eval,
responses,
re_evaluate,
re_evaluation_in_progress=True)
if re_evaluation_in_progress:
conversation_history.extend(responses)
return conversation_history
# unwrap the list of responses
conversation_history.append(responses[-1])
#responses = converse(responses)
# TODO: this needs to be optimized
responses = self.analyze(responses[1:],
prefix="",
suffix=f"Return an appropriate answer given the context above, including a summary.\n")
# add responses to conversation history by extending the list
conversation_history.append(
{
"role": "assistant",
"content": responses,
}
)
# logger.info the latest response from the conversation history
logger.info(conversation_history[-1]["content"])
#self.tts(conversation_history[-1]["content"])
else:
logger.info("==> no action needed")
if re_evaluation_in_progress:
logger.info("==> LocalAGI has completed the user request")
logger.info("==> LocalAGI will reply to the user")
return conversation_history
# get the response from the model
response = self.converse(conversation_history)
# add the response to the conversation history by extending the list
conversation_history.extend(response)
# logger.info the latest response from the conversation history
logger.info(conversation_history[-1]["content"])
#self.tts(conversation_history[-1]["content"])
return conversation_history