From 446908b759a1d0b9d91f441023d9db92a18dba7d Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Tue, 8 Apr 2025 22:07:59 +0200 Subject: [PATCH] nuke old implementation --- .dockerignore | 2 - .env | 26 - .github/workflows/image.yaml | 142 ----- .gitignore | 4 - Dockerfile | 18 - LICENSE | 21 - README.md | 184 ------ config/wizardlm-13b-1.1.yaml | 45 -- config/wizardlm-13b-superhot.yaml | 47 -- config/wizardlm-13b.yaml | 45 -- config/wizardlm-30b-superhot-supercot.yaml | 47 -- config/wizardlm-30b.yaml | 46 -- config/wizardlm-7b.yaml | 45 -- docker-compose.yaml | 31 - examples/discord/Dockerfile | 8 - examples/discord/agent.py | 371 ------------ examples/discord/config.ini.example | 31 - examples/discord/config.py | 5 - examples/discord/entrypoint.sh | 6 - examples/discord/main.py | 292 ---------- examples/discord/requirements.txt | 11 - examples/slack/.dockerenv.example | 21 - examples/slack/Dockerfile | 17 - examples/slack/LICENSE | 21 - examples/slack/app/__init__.py | 0 examples/slack/app/agent.py | 396 ------------- examples/slack/app/bolt_listeners.py | 403 ------------- examples/slack/app/env.py | 43 -- examples/slack/app/i18n.py | 75 --- examples/slack/app/markdown.py | 53 -- examples/slack/app/openai_ops.py | 234 -------- examples/slack/app/slack_ops.py | 110 ---- examples/slack/entrypoint.sh | 12 - examples/slack/main.py | 69 --- examples/slack/main_prod.py | 306 ---------- examples/slack/manifest-dev.yml | 32 -- examples/slack/manifest-prod.yml | 43 -- examples/slack/requirements.txt | 15 - examples/slack/run.sh | 2 - main.py | 434 -------------- pyproject.toml | 22 - requirements.txt | 9 - src/localagi/__init__.py | 1 - src/localagi/localagi.py | 633 --------------------- 44 files changed, 4378 deletions(-) delete mode 100644 .dockerignore delete mode 100644 .env delete mode 100644 .github/workflows/image.yaml delete mode 100644 .gitignore delete mode 100644 Dockerfile delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 config/wizardlm-13b-1.1.yaml delete mode 100644 config/wizardlm-13b-superhot.yaml delete mode 100644 config/wizardlm-13b.yaml delete mode 100644 config/wizardlm-30b-superhot-supercot.yaml delete mode 100644 config/wizardlm-30b.yaml delete mode 100644 config/wizardlm-7b.yaml delete mode 100644 docker-compose.yaml delete mode 100644 examples/discord/Dockerfile delete mode 100644 examples/discord/agent.py delete mode 100644 examples/discord/config.ini.example delete mode 100644 examples/discord/config.py delete mode 100755 examples/discord/entrypoint.sh delete mode 100644 examples/discord/main.py delete mode 100644 examples/discord/requirements.txt delete mode 100644 examples/slack/.dockerenv.example delete mode 100644 examples/slack/Dockerfile delete mode 100644 examples/slack/LICENSE delete mode 100644 examples/slack/app/__init__.py delete mode 100644 examples/slack/app/agent.py delete mode 100644 examples/slack/app/bolt_listeners.py delete mode 100644 examples/slack/app/env.py delete mode 100644 examples/slack/app/i18n.py delete mode 100644 examples/slack/app/markdown.py delete mode 100644 examples/slack/app/openai_ops.py delete mode 100644 examples/slack/app/slack_ops.py delete mode 100755 examples/slack/entrypoint.sh delete mode 100644 examples/slack/main.py delete mode 100644 examples/slack/main_prod.py delete mode 100644 examples/slack/manifest-dev.yml delete mode 100644 examples/slack/manifest-prod.yml delete mode 100644 examples/slack/requirements.txt delete mode 100644 examples/slack/run.sh delete mode 100644 main.py delete mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 src/localagi/__init__.py delete mode 100644 src/localagi/localagi.py diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 72a039c..0000000 --- a/.dockerignore +++ /dev/null @@ -1,2 +0,0 @@ -models/ -db/ \ No newline at end of file diff --git a/.env b/.env deleted file mode 100644 index 68c9dac..0000000 --- a/.env +++ /dev/null @@ -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 \ No newline at end of file diff --git a/.github/workflows/image.yaml b/.github/workflows/image.yaml deleted file mode 100644 index a93a97c..0000000 --- a/.github/workflows/image.yaml +++ /dev/null @@ -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 }} diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 339b255..0000000 --- a/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -db/ -models/ -config.ini -.dockerenv \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 8411242..0000000 --- a/Dockerfile +++ /dev/null @@ -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" ]; \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index b60c1df..0000000 --- a/LICENSE +++ /dev/null @@ -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. diff --git a/README.md b/README.md deleted file mode 100644 index ce5414d..0000000 --- a/README.md +++ /dev/null @@ -1,184 +0,0 @@ - -

-
-
- LocalAGI -
-

- -[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. diff --git a/config/wizardlm-13b-1.1.yaml b/config/wizardlm-13b-1.1.yaml deleted file mode 100644 index fb1c6d8..0000000 --- a/config/wizardlm-13b-1.1.yaml +++ /dev/null @@ -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 diff --git a/config/wizardlm-13b-superhot.yaml b/config/wizardlm-13b-superhot.yaml deleted file mode 100644 index d092104..0000000 --- a/config/wizardlm-13b-superhot.yaml +++ /dev/null @@ -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 diff --git a/config/wizardlm-13b.yaml b/config/wizardlm-13b.yaml deleted file mode 100644 index cf5d914..0000000 --- a/config/wizardlm-13b.yaml +++ /dev/null @@ -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 diff --git a/config/wizardlm-30b-superhot-supercot.yaml b/config/wizardlm-30b-superhot-supercot.yaml deleted file mode 100644 index 0833d6a..0000000 --- a/config/wizardlm-30b-superhot-supercot.yaml +++ /dev/null @@ -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 diff --git a/config/wizardlm-30b.yaml b/config/wizardlm-30b.yaml deleted file mode 100644 index 1736a2e..0000000 --- a/config/wizardlm-30b.yaml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/config/wizardlm-7b.yaml b/config/wizardlm-7b.yaml deleted file mode 100644 index 85d2308..0000000 --- a/config/wizardlm-7b.yaml +++ /dev/null @@ -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 diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index 31443f6..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/examples/discord/Dockerfile b/examples/discord/Dockerfile deleted file mode 100644 index a18e89e..0000000 --- a/examples/discord/Dockerfile +++ /dev/null @@ -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" ]; diff --git a/examples/discord/agent.py b/examples/discord/agent.py deleted file mode 100644 index 25cbbed..0000000 --- a/examples/discord/agent.py +++ /dev/null @@ -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"] - } - }, - }, -} \ No newline at end of file diff --git a/examples/discord/config.ini.example b/examples/discord/config.ini.example deleted file mode 100644 index 99ef009..0000000 --- a/examples/discord/config.ini.example +++ /dev/null @@ -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 \ No newline at end of file diff --git a/examples/discord/config.py b/examples/discord/config.py deleted file mode 100644 index 8f40928..0000000 --- a/examples/discord/config.py +++ /dev/null @@ -1,5 +0,0 @@ -from configparser import ConfigParser - -config_file = "config.ini" -config = ConfigParser(interpolation=None) -config.read(config_file) \ No newline at end of file diff --git a/examples/discord/entrypoint.sh b/examples/discord/entrypoint.sh deleted file mode 100755 index 008fc22..0000000 --- a/examples/discord/entrypoint.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -pip uninstall hnswlib chromadb-hnswlib -y -pip install hnswlib chromadb-hnswlib -cd /app -python3 /app/main.py \ No newline at end of file diff --git a/examples/discord/main.py b/examples/discord/main.py deleted file mode 100644 index 4e12bc9..0000000 --- a/examples/discord/main.py +++ /dev/null @@ -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) diff --git a/examples/discord/requirements.txt b/examples/discord/requirements.txt deleted file mode 100644 index cb7261f..0000000 --- a/examples/discord/requirements.txt +++ /dev/null @@ -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 diff --git a/examples/slack/.dockerenv.example b/examples/slack/.dockerenv.example deleted file mode 100644 index 4f588c8..0000000 --- a/examples/slack/.dockerenv.example +++ /dev/null @@ -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 \ No newline at end of file diff --git a/examples/slack/Dockerfile b/examples/slack/Dockerfile deleted file mode 100644 index d6da909..0000000 --- a/examples/slack/Dockerfile +++ /dev/null @@ -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 diff --git a/examples/slack/LICENSE b/examples/slack/LICENSE deleted file mode 100644 index a9f1d6d..0000000 --- a/examples/slack/LICENSE +++ /dev/null @@ -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. diff --git a/examples/slack/app/__init__.py b/examples/slack/app/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/examples/slack/app/agent.py b/examples/slack/app/agent.py deleted file mode 100644 index a40a155..0000000 --- a/examples/slack/app/agent.py +++ /dev/null @@ -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"] \ No newline at end of file diff --git a/examples/slack/app/bolt_listeners.py b/examples/slack/app/bolt_listeners.py deleted file mode 100644 index 7085919..0000000 --- a/examples/slack/app/bolt_listeners.py +++ /dev/null @@ -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_() diff --git a/examples/slack/app/env.py b/examples/slack/app/env.py deleted file mode 100644 index 00099d7..0000000 --- a/examples/slack/app/env.py +++ /dev/null @@ -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") \ No newline at end of file diff --git a/examples/slack/app/i18n.py b/examples/slack/app/i18n.py deleted file mode 100644 index 255e70d..0000000 --- a/examples/slack/app/i18n.py +++ /dev/null @@ -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 diff --git a/examples/slack/app/markdown.py b/examples/slack/app/markdown.py deleted file mode 100644 index f38619b..0000000 --- a/examples/slack/app/markdown.py +++ /dev/null @@ -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]+?)(? 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]+?)(? 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("<", "<").replace(">", ">").replace("&", "&") - - # 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 model’s 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 {role/name}\n{content}\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 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 diff --git a/examples/slack/app/slack_ops.py b/examples/slack/app/slack_ops.py deleted file mode 100644 index 8a33837..0000000 --- a/examples/slack/app/slack_ops.py +++ /dev/null @@ -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 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", - }, - } - ], - } diff --git a/examples/slack/entrypoint.sh b/examples/slack/entrypoint.sh deleted file mode 100755 index fb8ca20..0000000 --- a/examples/slack/entrypoint.sh +++ /dev/null @@ -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 \ No newline at end of file diff --git a/examples/slack/main.py b/examples/slack/main.py deleted file mode 100644 index 3c0d71b..0000000 --- a/examples/slack/main.py +++ /dev/null @@ -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() diff --git a/examples/slack/main_prod.py b/examples/slack/main_prod.py deleted file mode 100644 index 45a9631..0000000 --- a/examples/slack/main_prod.py +++ /dev/null @@ -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_) diff --git a/examples/slack/manifest-dev.yml b/examples/slack/manifest-dev.yml deleted file mode 100644 index 24fc849..0000000 --- a/examples/slack/manifest-dev.yml +++ /dev/null @@ -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 diff --git a/examples/slack/manifest-prod.yml b/examples/slack/manifest-prod.yml deleted file mode 100644 index 1734601..0000000 --- a/examples/slack/manifest-prod.yml +++ /dev/null @@ -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 diff --git a/examples/slack/requirements.txt b/examples/slack/requirements.txt deleted file mode 100644 index fdee1c4..0000000 --- a/examples/slack/requirements.txt +++ /dev/null @@ -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 diff --git a/examples/slack/run.sh b/examples/slack/run.sh deleted file mode 100644 index 6fa435c..0000000 --- a/examples/slack/run.sh +++ /dev/null @@ -1,2 +0,0 @@ -docker build -t slack-bot . -docker run -v $PWD/data:/data --rm -ti --env-file .dockerenv slack-bot \ No newline at end of file diff --git a/main.py b/main.py deleted file mode 100644 index 62ca4ba..0000000 --- a/main.py +++ /dev/null @@ -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"]) diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index e6a5854..0000000 --- a/pyproject.toml +++ /dev/null @@ -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" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 925277a..0000000 --- a/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -langchain -langchain-community -openai==0.28 -chromadb -pysqlite3-binary -requests -ascii-magic -loguru -duckduckgo_search==4.1.1 diff --git a/src/localagi/__init__.py b/src/localagi/__init__.py deleted file mode 100644 index 5bb5e2c..0000000 --- a/src/localagi/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .localagi import * \ No newline at end of file diff --git a/src/localagi/localagi.py b/src/localagi/localagi.py deleted file mode 100644 index aaeb5eb..0000000 --- a/src/localagi/localagi.py +++ /dev/null @@ -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 \ No newline at end of file