From 6fde5570b356036ab9150a00da52ebc072451d63 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Sun, 14 Apr 2024 12:04:38 -0700 Subject: [PATCH 01/10] remove unneeded answerKey for Anthropic (#1100) resolves #1096 --- server/utils/AiProviders/anthropic/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/utils/AiProviders/anthropic/index.js b/server/utils/AiProviders/anthropic/index.js index 24a07f6e5..5e8e40a30 100644 --- a/server/utils/AiProviders/anthropic/index.js +++ b/server/utils/AiProviders/anthropic/index.js @@ -4,6 +4,7 @@ const { writeResponseChunk, clientAbortedHandler, } = require("../../helpers/chat/responses"); + class AnthropicLLM { constructor(embedder = null, modelPreference = null) { if (!process.env.ANTHROPIC_API_KEY) @@ -28,7 +29,6 @@ class AnthropicLLM { "INVALID ANTHROPIC SETUP. No embedding engine has been set. Go to instance settings and set up an embedding interface to use Anthropic as your LLM." ); this.embedder = embedder; - this.answerKey = v4().split("-")[0]; this.defaultTemp = 0.7; } From 8306098b08397cba101fbf18c5a90f28f19fa267 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Sun, 14 Apr 2024 12:55:21 -0700 Subject: [PATCH 02/10] Bump all static model providers (#1101) --- server/utils/AiProviders/openRouter/models.js | 370 ++++++++++++----- .../AiProviders/openRouter/scripts/parse.mjs | 2 +- server/utils/AiProviders/perplexity/models.js | 30 -- .../perplexity/scripts/chat_models.txt | 24 +- .../AiProviders/perplexity/scripts/parse.mjs | 2 +- server/utils/AiProviders/togetherAi/models.js | 386 +++++++++++------- .../togetherAi/scripts/chat_models.txt | 94 +++-- .../AiProviders/togetherAi/scripts/parse.mjs | 4 +- 8 files changed, 573 insertions(+), 339 deletions(-) diff --git a/server/utils/AiProviders/openRouter/models.js b/server/utils/AiProviders/openRouter/models.js index c920b88a4..4c2d7946c 100644 --- a/server/utils/AiProviders/openRouter/models.js +++ b/server/utils/AiProviders/openRouter/models.js @@ -1,10 +1,4 @@ const MODELS = { - "nousresearch/nous-capybara-34b": { - id: "nousresearch/nous-capybara-34b", - name: "Nous: Capybara 34B", - organization: "Nousresearch", - maxLength: 32768, - }, "openrouter/auto": { id: "openrouter/auto", name: "Auto (best for prompt)", @@ -21,6 +15,12 @@ const MODELS = { id: "mistralai/mistral-7b-instruct:free", name: "Mistral 7B Instruct (free)", organization: "Mistralai", + maxLength: 32768, + }, + "openchat/openchat-7b:free": { + id: "openchat/openchat-7b:free", + name: "OpenChat 3.5 (free)", + organization: "Openchat", maxLength: 8192, }, "gryphe/mythomist-7b:free": { @@ -45,13 +45,7 @@ const MODELS = { id: "google/gemma-7b-it:free", name: "Google: Gemma 7B (free)", organization: "Google", - maxLength: 8000, - }, - "jondurbin/bagel-34b": { - id: "jondurbin/bagel-34b", - name: "Bagel 34B v0.2", - organization: "Jondurbin", - maxLength: 8000, + maxLength: 8192, }, "jebcarter/psyfighter-13b": { id: "jebcarter/psyfighter-13b", @@ -65,54 +59,12 @@ const MODELS = { organization: "Koboldai", maxLength: 4096, }, - "neversleep/noromaid-mixtral-8x7b-instruct": { - id: "neversleep/noromaid-mixtral-8x7b-instruct", - name: "Noromaid Mixtral 8x7B Instruct", - organization: "Neversleep", - maxLength: 8000, - }, - "nousresearch/nous-hermes-llama2-13b": { - id: "nousresearch/nous-hermes-llama2-13b", - name: "Nous: Hermes 13B", - organization: "Nousresearch", - maxLength: 4096, - }, - "meta-llama/codellama-34b-instruct": { - id: "meta-llama/codellama-34b-instruct", - name: "Meta: CodeLlama 34B Instruct", - organization: "Meta-llama", - maxLength: 8192, - }, - "phind/phind-codellama-34b": { - id: "phind/phind-codellama-34b", - name: "Phind: CodeLlama 34B v2", - organization: "Phind", - maxLength: 4096, - }, "intel/neural-chat-7b": { id: "intel/neural-chat-7b", name: "Neural Chat 7B v3.1", organization: "Intel", maxLength: 4096, }, - "mistralai/mixtral-8x7b-instruct": { - id: "mistralai/mixtral-8x7b-instruct", - name: "Mistral: Mixtral 8x7B Instruct", - organization: "Mistralai", - maxLength: 32768, - }, - "nousresearch/nous-hermes-2-mixtral-8x7b-dpo": { - id: "nousresearch/nous-hermes-2-mixtral-8x7b-dpo", - name: "Nous: Hermes 2 Mixtral 8x7B DPO", - organization: "Nousresearch", - maxLength: 32000, - }, - "nousresearch/nous-hermes-2-mixtral-8x7b-sft": { - id: "nousresearch/nous-hermes-2-mixtral-8x7b-sft", - name: "Nous: Hermes 2 Mixtral 8x7B SFT", - organization: "Nousresearch", - maxLength: 32000, - }, "haotian-liu/llava-13b": { id: "haotian-liu/llava-13b", name: "Llava 13B", @@ -143,30 +95,12 @@ const MODELS = { organization: "Pygmalionai", maxLength: 8192, }, - "undi95/remm-slerp-l2-13b-6k": { - id: "undi95/remm-slerp-l2-13b-6k", - name: "ReMM SLERP 13B 6k", - organization: "Undi95", - maxLength: 6144, - }, - "gryphe/mythomax-l2-13b": { - id: "gryphe/mythomax-l2-13b", - name: "MythoMax 13B", - organization: "Gryphe", - maxLength: 4096, - }, "xwin-lm/xwin-lm-70b": { id: "xwin-lm/xwin-lm-70b", name: "Xwin 70B", organization: "Xwin-lm", maxLength: 8192, }, - "gryphe/mythomax-l2-13b-8k": { - id: "gryphe/mythomax-l2-13b-8k", - name: "MythoMax 13B 8k", - organization: "Gryphe", - maxLength: 8192, - }, "alpindale/goliath-120b": { id: "alpindale/goliath-120b", name: "Goliath 120B", @@ -185,15 +119,27 @@ const MODELS = { organization: "Gryphe", maxLength: 32768, }, + "sophosympatheia/midnight-rose-70b": { + id: "sophosympatheia/midnight-rose-70b", + name: "Midnight Rose 70B", + organization: "Sophosympatheia", + maxLength: 4096, + }, + "undi95/remm-slerp-l2-13b:extended": { + id: "undi95/remm-slerp-l2-13b:extended", + name: "ReMM SLERP 13B (extended)", + organization: "Undi95", + maxLength: 6144, + }, "mancer/weaver": { id: "mancer/weaver", name: "Mancer: Weaver (alpha)", organization: "Mancer", maxLength: 8000, }, - "nousresearch/nous-hermes-llama2-70b": { - id: "nousresearch/nous-hermes-llama2-70b", - name: "Nous: Hermes 70B", + "nousresearch/nous-hermes-llama2-13b": { + id: "nousresearch/nous-hermes-llama2-13b", + name: "Nous: Hermes 13B", organization: "Nousresearch", maxLength: 4096, }, @@ -203,12 +149,24 @@ const MODELS = { organization: "Nousresearch", maxLength: 4096, }, + "meta-llama/codellama-34b-instruct": { + id: "meta-llama/codellama-34b-instruct", + name: "Meta: CodeLlama 34B Instruct", + organization: "Meta-llama", + maxLength: 8192, + }, "codellama/codellama-70b-instruct": { id: "codellama/codellama-70b-instruct", name: "Meta: CodeLlama 70B Instruct", organization: "Codellama", maxLength: 2048, }, + "phind/phind-codellama-34b": { + id: "phind/phind-codellama-34b", + name: "Phind: CodeLlama 34B v2", + organization: "Phind", + maxLength: 4096, + }, "teknium/openhermes-2-mistral-7b": { id: "teknium/openhermes-2-mistral-7b", name: "OpenHermes 2 Mistral 7B", @@ -227,12 +185,6 @@ const MODELS = { organization: "Undi95", maxLength: 4096, }, - "undi95/toppy-m-7b": { - id: "undi95/toppy-m-7b", - name: "Toppy M 7B", - organization: "Undi95", - maxLength: 4096, - }, "openrouter/cinematika-7b": { id: "openrouter/cinematika-7b", name: "Cinematika 7B (alpha)", @@ -271,7 +223,7 @@ const MODELS = { }, "mistralai/mixtral-8x7b": { id: "mistralai/mixtral-8x7b", - name: "Mistral: Mixtral 8x7B (base)", + name: "Mixtral 8x7B (base)", organization: "Mistralai", maxLength: 32768, }, @@ -281,6 +233,12 @@ const MODELS = { organization: "Nousresearch", maxLength: 4096, }, + "nousresearch/nous-hermes-2-mixtral-8x7b-sft": { + id: "nousresearch/nous-hermes-2-mixtral-8x7b-sft", + name: "Nous: Hermes 2 Mixtral 8x7B SFT", + organization: "Nousresearch", + maxLength: 32000, + }, "nousresearch/nous-hermes-2-mistral-7b-dpo": { id: "nousresearch/nous-hermes-2-mistral-7b-dpo", name: "Nous: Hermes 2 Mistral 7B DPO", @@ -303,7 +261,7 @@ const MODELS = { id: "openai/gpt-3.5-turbo", name: "OpenAI: GPT-3.5 Turbo", organization: "Openai", - maxLength: 4095, + maxLength: 16385, }, "openai/gpt-3.5-turbo-0125": { id: "openai/gpt-3.5-turbo-0125", @@ -335,9 +293,15 @@ const MODELS = { organization: "Openai", maxLength: 16385, }, + "openai/gpt-4-turbo": { + id: "openai/gpt-4-turbo", + name: "OpenAI: GPT-4 Turbo", + organization: "Openai", + maxLength: 128000, + }, "openai/gpt-4-turbo-preview": { id: "openai/gpt-4-turbo-preview", - name: "OpenAI: GPT-4 Turbo (preview)", + name: "OpenAI: GPT-4 Turbo Preview", organization: "Openai", maxLength: 128000, }, @@ -373,7 +337,7 @@ const MODELS = { }, "openai/gpt-4-vision-preview": { id: "openai/gpt-4-vision-preview", - name: "OpenAI: GPT-4 Vision (preview)", + name: "OpenAI: GPT-4 Vision", organization: "Openai", maxLength: 128000, }, @@ -387,37 +351,43 @@ const MODELS = { id: "google/palm-2-chat-bison", name: "Google: PaLM 2 Chat", organization: "Google", - maxLength: 36864, + maxLength: 25804, }, "google/palm-2-codechat-bison": { id: "google/palm-2-codechat-bison", name: "Google: PaLM 2 Code Chat", organization: "Google", - maxLength: 28672, + maxLength: 20070, }, "google/palm-2-chat-bison-32k": { id: "google/palm-2-chat-bison-32k", name: "Google: PaLM 2 Chat 32k", organization: "Google", - maxLength: 131072, + maxLength: 91750, }, "google/palm-2-codechat-bison-32k": { id: "google/palm-2-codechat-bison-32k", name: "Google: PaLM 2 Code Chat 32k", organization: "Google", - maxLength: 131072, + maxLength: 91750, }, "google/gemini-pro": { id: "google/gemini-pro", - name: "Google: Gemini Pro (preview)", + name: "Google: Gemini Pro 1.0", organization: "Google", - maxLength: 131040, + maxLength: 91728, }, "google/gemini-pro-vision": { id: "google/gemini-pro-vision", - name: "Google: Gemini Pro Vision (preview)", + name: "Google: Gemini Pro Vision 1.0", organization: "Google", - maxLength: 65536, + maxLength: 45875, + }, + "google/gemini-pro-1.5": { + id: "google/gemini-pro-1.5", + name: "Google: Gemini Pro 1.5 (preview)", + organization: "Google", + maxLength: 2800000, }, "perplexity/pplx-70b-online": { id: "perplexity/pplx-70b-online", @@ -443,18 +413,96 @@ const MODELS = { organization: "Perplexity", maxLength: 4096, }, + "perplexity/sonar-small-chat": { + id: "perplexity/sonar-small-chat", + name: "Perplexity: Sonar 7B", + organization: "Perplexity", + maxLength: 16384, + }, + "perplexity/sonar-medium-chat": { + id: "perplexity/sonar-medium-chat", + name: "Perplexity: Sonar 8x7B", + organization: "Perplexity", + maxLength: 16384, + }, + "perplexity/sonar-small-online": { + id: "perplexity/sonar-small-online", + name: "Perplexity: Sonar 7B Online", + organization: "Perplexity", + maxLength: 12000, + }, + "perplexity/sonar-medium-online": { + id: "perplexity/sonar-medium-online", + name: "Perplexity: Sonar 8x7B Online", + organization: "Perplexity", + maxLength: 12000, + }, + "fireworks/mixtral-8x22b-instruct-preview": { + id: "fireworks/mixtral-8x22b-instruct-preview", + name: "Fireworks Mixtral 8x22B Instruct OH (preview)", + organization: "Fireworks", + maxLength: 8192, + }, + "anthropic/claude-3-opus": { + id: "anthropic/claude-3-opus", + name: "Anthropic: Claude 3 Opus", + organization: "Anthropic", + maxLength: 200000, + }, + "anthropic/claude-3-sonnet": { + id: "anthropic/claude-3-sonnet", + name: "Anthropic: Claude 3 Sonnet", + organization: "Anthropic", + maxLength: 200000, + }, + "anthropic/claude-3-haiku": { + id: "anthropic/claude-3-haiku", + name: "Anthropic: Claude 3 Haiku", + organization: "Anthropic", + maxLength: 200000, + }, + "anthropic/claude-3-opus:beta": { + id: "anthropic/claude-3-opus:beta", + name: "Anthropic: Claude 3 Opus (self-moderated)", + organization: "Anthropic", + maxLength: 200000, + }, + "anthropic/claude-3-sonnet:beta": { + id: "anthropic/claude-3-sonnet:beta", + name: "Anthropic: Claude 3 Sonnet (self-moderated)", + organization: "Anthropic", + maxLength: 200000, + }, + "anthropic/claude-3-haiku:beta": { + id: "anthropic/claude-3-haiku:beta", + name: "Anthropic: Claude 3 Haiku (self-moderated)", + organization: "Anthropic", + maxLength: 200000, + }, "meta-llama/llama-2-70b-chat": { id: "meta-llama/llama-2-70b-chat", name: "Meta: Llama v2 70B Chat", organization: "Meta-llama", maxLength: 4096, }, + "nousresearch/nous-capybara-34b": { + id: "nousresearch/nous-capybara-34b", + name: "Nous: Capybara 34B", + organization: "Nousresearch", + maxLength: 32768, + }, "jondurbin/airoboros-l2-70b": { id: "jondurbin/airoboros-l2-70b", name: "Airoboros 70B", organization: "Jondurbin", maxLength: 4096, }, + "jondurbin/bagel-34b": { + id: "jondurbin/bagel-34b", + name: "Bagel 34B v0.2", + organization: "Jondurbin", + maxLength: 8000, + }, "austism/chronos-hermes-13b": { id: "austism/chronos-hermes-13b", name: "Chronos Hermes 13B v2", @@ -465,7 +513,13 @@ const MODELS = { id: "mistralai/mistral-7b-instruct", name: "Mistral 7B Instruct", organization: "Mistralai", - maxLength: 8192, + maxLength: 32768, + }, + "gryphe/mythomax-l2-13b": { + id: "gryphe/mythomax-l2-13b", + name: "MythoMax 13B", + organization: "Gryphe", + maxLength: 4096, }, "openchat/openchat-7b": { id: "openchat/openchat-7b", @@ -473,18 +527,42 @@ const MODELS = { organization: "Openchat", maxLength: 8192, }, + "undi95/toppy-m-7b": { + id: "undi95/toppy-m-7b", + name: "Toppy M 7B", + organization: "Undi95", + maxLength: 4096, + }, "lizpreciatior/lzlv-70b-fp16-hf": { id: "lizpreciatior/lzlv-70b-fp16-hf", name: "lzlv 70B", organization: "Lizpreciatior", maxLength: 4096, }, + "mistralai/mixtral-8x7b-instruct": { + id: "mistralai/mixtral-8x7b-instruct", + name: "Mixtral 8x7B Instruct", + organization: "Mistralai", + maxLength: 32768, + }, "cognitivecomputations/dolphin-mixtral-8x7b": { id: "cognitivecomputations/dolphin-mixtral-8x7b", name: "Dolphin 2.6 Mixtral 8x7B 🐬", organization: "Cognitivecomputations", maxLength: 32000, }, + "neversleep/noromaid-mixtral-8x7b-instruct": { + id: "neversleep/noromaid-mixtral-8x7b-instruct", + name: "Noromaid Mixtral 8x7B Instruct", + organization: "Neversleep", + maxLength: 8000, + }, + "nousresearch/nous-hermes-2-mixtral-8x7b-dpo": { + id: "nousresearch/nous-hermes-2-mixtral-8x7b-dpo", + name: "Nous: Hermes 2 Mixtral 8x7B DPO", + organization: "Nousresearch", + maxLength: 32000, + }, "rwkv/rwkv-5-world-3b": { id: "rwkv/rwkv-5-world-3b", name: "RWKV v5 World 3B", @@ -507,7 +585,19 @@ const MODELS = { id: "google/gemma-7b-it", name: "Google: Gemma 7B", organization: "Google", - maxLength: 8000, + maxLength: 8192, + }, + "databricks/dbrx-instruct": { + id: "databricks/dbrx-instruct", + name: "Databricks: DBRX 132B Instruct", + organization: "Databricks", + maxLength: 32768, + }, + "huggingfaceh4/zephyr-orpo-141b-a35b": { + id: "huggingfaceh4/zephyr-orpo-141b-a35b", + name: "Zephyr 141B-A35B", + organization: "Huggingfaceh4", + maxLength: 65536, }, "anthropic/claude-2": { id: "anthropic/claude-2", @@ -565,58 +655,124 @@ const MODELS = { }, "anthropic/claude-2:beta": { id: "anthropic/claude-2:beta", - name: "Anthropic: Claude v2 (experimental)", + name: "Anthropic: Claude v2 (self-moderated)", organization: "Anthropic", maxLength: 200000, }, "anthropic/claude-2.1:beta": { id: "anthropic/claude-2.1:beta", - name: "Anthropic: Claude v2.1 (experimental)", + name: "Anthropic: Claude v2.1 (self-moderated)", organization: "Anthropic", maxLength: 200000, }, "anthropic/claude-2.0:beta": { id: "anthropic/claude-2.0:beta", - name: "Anthropic: Claude v2.0 (experimental)", + name: "Anthropic: Claude v2.0 (self-moderated)", organization: "Anthropic", maxLength: 100000, }, "anthropic/claude-instant-1:beta": { id: "anthropic/claude-instant-1:beta", - name: "Anthropic: Claude Instant v1 (experimental)", + name: "Anthropic: Claude Instant v1 (self-moderated)", organization: "Anthropic", maxLength: 100000, }, + "mistralai/mixtral-8x22b": { + id: "mistralai/mixtral-8x22b", + name: "Mistral: Mixtral 8x22B (base)", + organization: "Mistralai", + maxLength: 65536, + }, "huggingfaceh4/zephyr-7b-beta:free": { id: "huggingfaceh4/zephyr-7b-beta:free", name: "Hugging Face: Zephyr 7B (free)", organization: "Huggingfaceh4", maxLength: 4096, }, - "openchat/openchat-7b:free": { - id: "openchat/openchat-7b:free", - name: "OpenChat 3.5 (free)", - organization: "Openchat", + "mistralai/mixtral-8x7b-instruct:nitro": { + id: "mistralai/mixtral-8x7b-instruct:nitro", + name: "Mixtral 8x7B Instruct (nitro)", + organization: "Mistralai", + maxLength: 32768, + }, + "meta-llama/llama-2-70b-chat:nitro": { + id: "meta-llama/llama-2-70b-chat:nitro", + name: "Meta: Llama v2 70B Chat (nitro)", + organization: "Meta-llama", + maxLength: 4096, + }, + "gryphe/mythomax-l2-13b:nitro": { + id: "gryphe/mythomax-l2-13b:nitro", + name: "MythoMax 13B (nitro)", + organization: "Gryphe", + maxLength: 4096, + }, + "mistralai/mistral-7b-instruct:nitro": { + id: "mistralai/mistral-7b-instruct:nitro", + name: "Mistral 7B Instruct (nitro)", + organization: "Mistralai", + maxLength: 32768, + }, + "google/gemma-7b-it:nitro": { + id: "google/gemma-7b-it:nitro", + name: "Google: Gemma 7B (nitro)", + organization: "Google", + maxLength: 8192, + }, + "databricks/dbrx-instruct:nitro": { + id: "databricks/dbrx-instruct:nitro", + name: "Databricks: DBRX 132B Instruct (nitro)", + organization: "Databricks", + maxLength: 32768, + }, + "gryphe/mythomax-l2-13b:extended": { + id: "gryphe/mythomax-l2-13b:extended", + name: "MythoMax 13B (extended)", + organization: "Gryphe", maxLength: 8192, }, "mistralai/mistral-tiny": { id: "mistralai/mistral-tiny", - name: "Mistral: Tiny", + name: "Mistral Tiny", organization: "Mistralai", maxLength: 32000, }, "mistralai/mistral-small": { id: "mistralai/mistral-small", - name: "Mistral: Small", + name: "Mistral Small", organization: "Mistralai", maxLength: 32000, }, "mistralai/mistral-medium": { id: "mistralai/mistral-medium", - name: "Mistral: Medium", + name: "Mistral Medium", organization: "Mistralai", maxLength: 32000, }, + "mistralai/mistral-large": { + id: "mistralai/mistral-large", + name: "Mistral Large", + organization: "Mistralai", + maxLength: 32000, + }, + "cohere/command": { + id: "cohere/command", + name: "Cohere: Command", + organization: "Cohere", + maxLength: 4096, + }, + "cohere/command-r": { + id: "cohere/command-r", + name: "Cohere: Command R", + organization: "Cohere", + maxLength: 128000, + }, + "cohere/command-r-plus": { + id: "cohere/command-r-plus", + name: "Cohere: Command R+", + organization: "Cohere", + maxLength: 128000, + }, }; module.exports.MODELS = MODELS; diff --git a/server/utils/AiProviders/openRouter/scripts/parse.mjs b/server/utils/AiProviders/openRouter/scripts/parse.mjs index fb3b562b5..11c67b22c 100644 --- a/server/utils/AiProviders/openRouter/scripts/parse.mjs +++ b/server/utils/AiProviders/openRouter/scripts/parse.mjs @@ -6,7 +6,7 @@ // copy outputs into the export in ../models.js // Update the date below if you run this again because OpenRouter added new models. -// Last Collected: Feb 23, 2024 +// Last Collected: Apr 14, 2024 import fs from "fs"; diff --git a/server/utils/AiProviders/perplexity/models.js b/server/utils/AiProviders/perplexity/models.js index 95cd8eac7..8bed2a5a0 100644 --- a/server/utils/AiProviders/perplexity/models.js +++ b/server/utils/AiProviders/perplexity/models.js @@ -19,21 +19,11 @@ const MODELS = { name: "sonar-medium-online", maxLength: 12000, }, - "codellama-34b-instruct": { - id: "codellama-34b-instruct", - name: "codellama-34b-instruct", - maxLength: 16384, - }, "codellama-70b-instruct": { id: "codellama-70b-instruct", name: "codellama-70b-instruct", maxLength: 16384, }, - "llama-2-70b-chat": { - id: "llama-2-70b-chat", - name: "llama-2-70b-chat", - maxLength: 4096, - }, "mistral-7b-instruct": { id: "mistral-7b-instruct", name: "mistral-7b-instruct", @@ -44,26 +34,6 @@ const MODELS = { name: "mixtral-8x7b-instruct", maxLength: 16384, }, - "pplx-7b-chat": { - id: "pplx-7b-chat", - name: "pplx-7b-chat", - maxLength: 16384, - }, - "pplx-7b-online": { - id: "pplx-7b-online", - name: "pplx-7b-online", - maxLength: 12000, - }, - "pplx-70b-chat": { - id: "pplx-70b-chat", - name: "pplx-70b-chat", - maxLength: 8192, - }, - "pplx-70b-online": { - id: "pplx-70b-online", - name: "pplx-70b-online", - maxLength: 4000, - }, }; module.exports.MODELS = MODELS; diff --git a/server/utils/AiProviders/perplexity/scripts/chat_models.txt b/server/utils/AiProviders/perplexity/scripts/chat_models.txt index 97ba9017a..41fce0f01 100644 --- a/server/utils/AiProviders/perplexity/scripts/chat_models.txt +++ b/server/utils/AiProviders/perplexity/scripts/chat_models.txt @@ -1,15 +1,9 @@ -| Model | Parameter Count | Context Length | Model Type | -| :-------------------------- | :-------------- | :------------- | :-------------- | -| `sonar-small-chat` | 7B | 16384 | Chat Completion | -| `sonar-small-online` | 7B | 12000 | Chat Completion | -| `sonar-medium-chat` | 8x7B | 16384 | Chat Completion | -| `sonar-medium-online` | 8x7B | 12000 | Chat Completion | -| `codellama-34b-instruct`[3] | 34B | 16384 | Chat Completion | -| `codellama-70b-instruct` | 70B | 16384 | Chat Completion | -| `llama-2-70b-chat`[3] | 70B | 4096 | Chat Completion | -| `mistral-7b-instruct` [1] | 7B | 16384 | Chat Completion | -| `mixtral-8x7b-instruct` | 8x7B | 16384 | Chat Completion | -| `pplx-7b-chat`[2] [3] | 7B | 16384 | Chat Completion | -| `pplx-7b-online`[2] [3] | 7B | 12000 | Chat Completion | -| `pplx-70b-chat`[3] | 70B | 8192 | Chat Completion | -| `pplx-70b-online`[3] | 70B | 4000 | Chat Completion | \ No newline at end of file +| Model | Parameter Count | Context Length | Model Type | +| :-------------------- | :-------------- | :------------- | :-------------- | +| `sonar-small-chat` | 7B | 16384 | Chat Completion | +| `sonar-small-online` | 7B | 12000 | Chat Completion | +| `sonar-medium-chat` | 8x7B | 16384 | Chat Completion | +| `sonar-medium-online` | 8x7B | 12000 | Chat Completion | +| `codellama-70b-instruct` | 70B | 16384 | Chat Completion | +| `mistral-7b-instruct` [1] | 7B | 16384 | Chat Completion | +| `mixtral-8x7b-instruct` | 8x7B | 16384 | Chat Completion | \ No newline at end of file diff --git a/server/utils/AiProviders/perplexity/scripts/parse.mjs b/server/utils/AiProviders/perplexity/scripts/parse.mjs index d2064354a..1858eafb8 100644 --- a/server/utils/AiProviders/perplexity/scripts/parse.mjs +++ b/server/utils/AiProviders/perplexity/scripts/parse.mjs @@ -8,7 +8,7 @@ // copy outputs into the export in ../models.js // Update the date below if you run this again because Perplexity added new models. -// Last Collected: Feb 23, 2024 +// Last Collected: Apr 14, 2024 import fs from "fs"; diff --git a/server/utils/AiProviders/togetherAi/models.js b/server/utils/AiProviders/togetherAi/models.js index ad940bc39..6fad3969b 100644 --- a/server/utils/AiProviders/togetherAi/models.js +++ b/server/utils/AiProviders/togetherAi/models.js @@ -1,8 +1,26 @@ const MODELS = { - "togethercomputer/alpaca-7b": { - id: "togethercomputer/alpaca-7b", - organization: "Stanford", - name: "Alpaca (7B)", + "zero-one-ai/Yi-34B-Chat": { + id: "zero-one-ai/Yi-34B-Chat", + organization: "01.AI", + name: "01-ai Yi Chat (34B)", + maxLength: 4096, + }, + "allenai/OLMo-7B-Instruct": { + id: "allenai/OLMo-7B-Instruct", + organization: "Allen AI", + name: "OLMo Instruct (7B)", + maxLength: 2048, + }, + "allenai/OLMo-7B-Twin-2T": { + id: "allenai/OLMo-7B-Twin-2T", + organization: "Allen AI", + name: "OLMo Twin-2T (7B)", + maxLength: 2048, + }, + "allenai/OLMo-7B": { + id: "allenai/OLMo-7B", + organization: "Allen AI", + name: "OLMo (7B)", maxLength: 2048, }, "Austism/chronos-hermes-13b": { @@ -11,96 +29,150 @@ const MODELS = { name: "Chronos Hermes (13B)", maxLength: 2048, }, - "togethercomputer/CodeLlama-13b-Instruct": { - id: "togethercomputer/CodeLlama-13b-Instruct", - organization: "Meta", - name: "Code Llama Instruct (13B)", + "cognitivecomputations/dolphin-2.5-mixtral-8x7b": { + id: "cognitivecomputations/dolphin-2.5-mixtral-8x7b", + organization: "cognitivecomputations", + name: "Dolphin 2.5 Mixtral 8x7b", + maxLength: 32768, + }, + "databricks/dbrx-instruct": { + id: "databricks/dbrx-instruct", + organization: "databricks", + name: "DBRX Instruct", + maxLength: 32000, + }, + "deepseek-ai/deepseek-coder-33b-instruct": { + id: "deepseek-ai/deepseek-coder-33b-instruct", + organization: "DeepSeek", + name: "Deepseek Coder Instruct (33B)", + maxLength: 16384, + }, + "deepseek-ai/deepseek-llm-67b-chat": { + id: "deepseek-ai/deepseek-llm-67b-chat", + organization: "DeepSeek", + name: "DeepSeek LLM Chat (67B)", + maxLength: 4096, + }, + "garage-bAInd/Platypus2-70B-instruct": { + id: "garage-bAInd/Platypus2-70B-instruct", + organization: "garage-bAInd", + name: "Platypus2 Instruct (70B)", + maxLength: 4096, + }, + "google/gemma-2b-it": { + id: "google/gemma-2b-it", + organization: "Google", + name: "Gemma Instruct (2B)", maxLength: 8192, }, - "togethercomputer/CodeLlama-34b-Instruct": { - id: "togethercomputer/CodeLlama-34b-Instruct", - organization: "Meta", - name: "Code Llama Instruct (34B)", + "google/gemma-7b-it": { + id: "google/gemma-7b-it", + organization: "Google", + name: "Gemma Instruct (7B)", maxLength: 8192, }, - "togethercomputer/CodeLlama-7b-Instruct": { - id: "togethercomputer/CodeLlama-7b-Instruct", - organization: "Meta", - name: "Code Llama Instruct (7B)", - maxLength: 8192, - }, - "DiscoResearch/DiscoLM-mixtral-8x7b-v2": { - id: "DiscoResearch/DiscoLM-mixtral-8x7b-v2", - organization: "DiscoResearch", - name: "DiscoLM Mixtral 8x7b", - maxLength: 32768, - }, - "togethercomputer/falcon-40b-instruct": { - id: "togethercomputer/falcon-40b-instruct", - organization: "TII UAE", - name: "Falcon Instruct (40B)", - maxLength: 2048, - }, - "togethercomputer/falcon-7b-instruct": { - id: "togethercomputer/falcon-7b-instruct", - organization: "TII UAE", - name: "Falcon Instruct (7B)", - maxLength: 2048, - }, - "togethercomputer/GPT-NeoXT-Chat-Base-20B": { - id: "togethercomputer/GPT-NeoXT-Chat-Base-20B", - organization: "Together", - name: "GPT-NeoXT-Chat-Base (20B)", - maxLength: 2048, - }, - "togethercomputer/llama-2-13b-chat": { - id: "togethercomputer/llama-2-13b-chat", - organization: "Meta", - name: "LLaMA-2 Chat (13B)", - maxLength: 4096, - }, - "togethercomputer/llama-2-70b-chat": { - id: "togethercomputer/llama-2-70b-chat", - organization: "Meta", - name: "LLaMA-2 Chat (70B)", - maxLength: 4096, - }, - "togethercomputer/llama-2-7b-chat": { - id: "togethercomputer/llama-2-7b-chat", - organization: "Meta", - name: "LLaMA-2 Chat (7B)", - maxLength: 4096, - }, - "togethercomputer/Llama-2-7B-32K-Instruct": { - id: "togethercomputer/Llama-2-7B-32K-Instruct", - organization: "Together", - name: "LLaMA-2-7B-32K-Instruct (7B)", - maxLength: 32768, - }, - "mistralai/Mistral-7B-Instruct-v0.1": { - id: "mistralai/Mistral-7B-Instruct-v0.1", - organization: "MistralAI", - name: "Mistral (7B) Instruct v0.1", - maxLength: 4096, - }, - "mistralai/Mistral-7B-Instruct-v0.2": { - id: "mistralai/Mistral-7B-Instruct-v0.2", - organization: "MistralAI", - name: "Mistral (7B) Instruct v0.2", - maxLength: 32768, - }, - "mistralai/Mixtral-8x7B-Instruct-v0.1": { - id: "mistralai/Mixtral-8x7B-Instruct-v0.1", - organization: "MistralAI", - name: "Mixtral-8x7B Instruct", - maxLength: 32768, - }, "Gryphe/MythoMax-L2-13b": { id: "Gryphe/MythoMax-L2-13b", organization: "Gryphe", name: "MythoMax-L2 (13B)", maxLength: 4096, }, + "lmsys/vicuna-13b-v1.5": { + id: "lmsys/vicuna-13b-v1.5", + organization: "LM Sys", + name: "Vicuna v1.5 (13B)", + maxLength: 4096, + }, + "lmsys/vicuna-7b-v1.5": { + id: "lmsys/vicuna-7b-v1.5", + organization: "LM Sys", + name: "Vicuna v1.5 (7B)", + maxLength: 4096, + }, + "codellama/CodeLlama-13b-Instruct-hf": { + id: "codellama/CodeLlama-13b-Instruct-hf", + organization: "Meta", + name: "Code Llama Instruct (13B)", + maxLength: 16384, + }, + "codellama/CodeLlama-34b-Instruct-hf": { + id: "codellama/CodeLlama-34b-Instruct-hf", + organization: "Meta", + name: "Code Llama Instruct (34B)", + maxLength: 16384, + }, + "codellama/CodeLlama-70b-Instruct-hf": { + id: "codellama/CodeLlama-70b-Instruct-hf", + organization: "Meta", + name: "Code Llama Instruct (70B)", + maxLength: 4096, + }, + "codellama/CodeLlama-7b-Instruct-hf": { + id: "codellama/CodeLlama-7b-Instruct-hf", + organization: "Meta", + name: "Code Llama Instruct (7B)", + maxLength: 16384, + }, + "meta-llama/Llama-2-70b-chat-hf": { + id: "meta-llama/Llama-2-70b-chat-hf", + organization: "Meta", + name: "LLaMA-2 Chat (70B)", + maxLength: 4096, + }, + "meta-llama/Llama-2-13b-chat-hf": { + id: "meta-llama/Llama-2-13b-chat-hf", + organization: "Meta", + name: "LLaMA-2 Chat (13B)", + maxLength: 4096, + }, + "meta-llama/Llama-2-7b-chat-hf": { + id: "meta-llama/Llama-2-7b-chat-hf", + organization: "Meta", + name: "LLaMA-2 Chat (7B)", + maxLength: 4096, + }, + "mistralai/Mistral-7B-Instruct-v0.1": { + id: "mistralai/Mistral-7B-Instruct-v0.1", + organization: "mistralai", + name: "Mistral (7B) Instruct", + maxLength: 8192, + }, + "mistralai/Mistral-7B-Instruct-v0.2": { + id: "mistralai/Mistral-7B-Instruct-v0.2", + organization: "mistralai", + name: "Mistral (7B) Instruct v0.2", + maxLength: 32768, + }, + "mistralai/Mixtral-8x7B-Instruct-v0.1": { + id: "mistralai/Mixtral-8x7B-Instruct-v0.1", + organization: "mistralai", + name: "Mixtral-8x7B Instruct (46.7B)", + maxLength: 32768, + }, + "NousResearch/Nous-Capybara-7B-V1p9": { + id: "NousResearch/Nous-Capybara-7B-V1p9", + organization: "NousResearch", + name: "Nous Capybara v1.9 (7B)", + maxLength: 8192, + }, + "NousResearch/Nous-Hermes-2-Mistral-7B-DPO": { + id: "NousResearch/Nous-Hermes-2-Mistral-7B-DPO", + organization: "NousResearch", + name: "Nous Hermes 2 - Mistral DPO (7B)", + maxLength: 32768, + }, + "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO": { + id: "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO", + organization: "NousResearch", + name: "Nous Hermes 2 - Mixtral 8x7B-DPO (46.7B)", + maxLength: 32768, + }, + "NousResearch/Nous-Hermes-2-Mixtral-8x7B-SFT": { + id: "NousResearch/Nous-Hermes-2-Mixtral-8x7B-SFT", + organization: "NousResearch", + name: "Nous Hermes 2 - Mixtral 8x7B-SFT (46.7B)", + maxLength: 32768, + }, "NousResearch/Nous-Hermes-llama-2-7b": { id: "NousResearch/Nous-Hermes-llama-2-7b", organization: "NousResearch", @@ -113,66 +185,96 @@ const MODELS = { name: "Nous Hermes Llama-2 (13B)", maxLength: 4096, }, - "NousResearch/Nous-Hermes-Llama2-70b": { - id: "NousResearch/Nous-Hermes-Llama2-70b", - organization: "NousResearch", - name: "Nous Hermes Llama-2 (70B)", - maxLength: 4096, - }, "NousResearch/Nous-Hermes-2-Yi-34B": { id: "NousResearch/Nous-Hermes-2-Yi-34B", organization: "NousResearch", name: "Nous Hermes-2 Yi (34B)", maxLength: 4096, }, - "NousResearch/Nous-Capybara-7B-V1p9": { - id: "NousResearch/Nous-Capybara-7B-V1p9", - organization: "NousResearch", - name: "Nous Capybara v1.9 (7B)", - maxLength: 8192, - }, "openchat/openchat-3.5-1210": { id: "openchat/openchat-3.5-1210", organization: "OpenChat", - name: "OpenChat 3.5 1210 (7B)", + name: "OpenChat 3.5 (7B)", maxLength: 8192, }, - "teknium/OpenHermes-2-Mistral-7B": { - id: "teknium/OpenHermes-2-Mistral-7B", - organization: "teknium", - name: "OpenHermes-2-Mistral (7B)", - maxLength: 4096, - }, - "teknium/OpenHermes-2p5-Mistral-7B": { - id: "teknium/OpenHermes-2p5-Mistral-7B", - organization: "teknium", - name: "OpenHermes-2.5-Mistral (7B)", - maxLength: 4096, - }, "Open-Orca/Mistral-7B-OpenOrca": { id: "Open-Orca/Mistral-7B-OpenOrca", organization: "OpenOrca", name: "OpenOrca Mistral (7B) 8K", maxLength: 8192, }, - "garage-bAInd/Platypus2-70B-instruct": { - id: "garage-bAInd/Platypus2-70B-instruct", - organization: "garage-bAInd", - name: "Platypus2 Instruct (70B)", - maxLength: 4096, + "Qwen/Qwen1.5-0.5B-Chat": { + id: "Qwen/Qwen1.5-0.5B-Chat", + organization: "Qwen", + name: "Qwen 1.5 Chat (0.5B)", + maxLength: 32768, }, - "togethercomputer/Pythia-Chat-Base-7B-v0.16": { - id: "togethercomputer/Pythia-Chat-Base-7B-v0.16", - organization: "Together", - name: "Pythia-Chat-Base (7B)", + "Qwen/Qwen1.5-1.8B-Chat": { + id: "Qwen/Qwen1.5-1.8B-Chat", + organization: "Qwen", + name: "Qwen 1.5 Chat (1.8B)", + maxLength: 32768, + }, + "Qwen/Qwen1.5-4B-Chat": { + id: "Qwen/Qwen1.5-4B-Chat", + organization: "Qwen", + name: "Qwen 1.5 Chat (4B)", + maxLength: 32768, + }, + "Qwen/Qwen1.5-7B-Chat": { + id: "Qwen/Qwen1.5-7B-Chat", + organization: "Qwen", + name: "Qwen 1.5 Chat (7B)", + maxLength: 32768, + }, + "Qwen/Qwen1.5-14B-Chat": { + id: "Qwen/Qwen1.5-14B-Chat", + organization: "Qwen", + name: "Qwen 1.5 Chat (14B)", + maxLength: 32768, + }, + "Qwen/Qwen1.5-32B-Chat": { + id: "Qwen/Qwen1.5-32B-Chat", + organization: "Qwen", + name: "Qwen 1.5 Chat (32B)", + maxLength: 32768, + }, + "Qwen/Qwen1.5-72B-Chat": { + id: "Qwen/Qwen1.5-72B-Chat", + organization: "Qwen", + name: "Qwen 1.5 Chat (72B)", + maxLength: 32768, + }, + "snorkelai/Snorkel-Mistral-PairRM-DPO": { + id: "snorkelai/Snorkel-Mistral-PairRM-DPO", + organization: "Snorkel AI", + name: "Snorkel Mistral PairRM DPO (7B)", + maxLength: 32768, + }, + "togethercomputer/alpaca-7b": { + id: "togethercomputer/alpaca-7b", + organization: "Stanford", + name: "Alpaca (7B)", maxLength: 2048, }, - "togethercomputer/Qwen-7B-Chat": { - id: "togethercomputer/Qwen-7B-Chat", - organization: "Qwen", - name: "Qwen-Chat (7B)", + "teknium/OpenHermes-2-Mistral-7B": { + id: "teknium/OpenHermes-2-Mistral-7B", + organization: "Teknium", + name: "OpenHermes-2-Mistral (7B)", maxLength: 8192, }, + "teknium/OpenHermes-2p5-Mistral-7B": { + id: "teknium/OpenHermes-2p5-Mistral-7B", + organization: "Teknium", + name: "OpenHermes-2.5-Mistral (7B)", + maxLength: 8192, + }, + "togethercomputer/Llama-2-7B-32K-Instruct": { + id: "togethercomputer/Llama-2-7B-32K-Instruct", + organization: "Together", + name: "LLaMA-2-7B-32K-Instruct (7B)", + maxLength: 32768, + }, "togethercomputer/RedPajama-INCITE-Chat-3B-v1": { id: "togethercomputer/RedPajama-INCITE-Chat-3B-v1", organization: "Together", @@ -185,40 +287,34 @@ const MODELS = { name: "RedPajama-INCITE Chat (7B)", maxLength: 2048, }, - "upstage/SOLAR-0-70b-16bit": { - id: "upstage/SOLAR-0-70b-16bit", - organization: "Upstage", - name: "SOLAR v0 (70B)", - maxLength: 4096, - }, "togethercomputer/StripedHyena-Nous-7B": { id: "togethercomputer/StripedHyena-Nous-7B", organization: "Together", name: "StripedHyena Nous (7B)", maxLength: 32768, }, - "lmsys/vicuna-7b-v1.5": { - id: "lmsys/vicuna-7b-v1.5", - organization: "LM Sys", - name: "Vicuna v1.5 (7B)", + "Undi95/ReMM-SLERP-L2-13B": { + id: "Undi95/ReMM-SLERP-L2-13B", + organization: "Undi95", + name: "ReMM SLERP L2 (13B)", maxLength: 4096, }, - "lmsys/vicuna-13b-v1.5": { - id: "lmsys/vicuna-13b-v1.5", - organization: "LM Sys", - name: "Vicuna v1.5 (13B)", + "Undi95/Toppy-M-7B": { + id: "Undi95/Toppy-M-7B", + organization: "Undi95", + name: "Toppy M (7B)", maxLength: 4096, }, - "lmsys/vicuna-13b-v1.5-16k": { - id: "lmsys/vicuna-13b-v1.5-16k", - organization: "LM Sys", - name: "Vicuna v1.5 16K (13B)", - maxLength: 16384, + "WizardLM/WizardLM-13B-V1.2": { + id: "WizardLM/WizardLM-13B-V1.2", + organization: "WizardLM", + name: "WizardLM v1.2 (13B)", + maxLength: 4096, }, - "zero-one-ai/Yi-34B-Chat": { - id: "zero-one-ai/Yi-34B-Chat", - organization: "01.AI", - name: "01-ai Yi Chat (34B)", + "upstage/SOLAR-10.7B-Instruct-v1.0": { + id: "upstage/SOLAR-10.7B-Instruct-v1.0", + organization: "upstage", + name: "Upstage SOLAR Instruct v1 (11B)", maxLength: 4096, }, }; diff --git a/server/utils/AiProviders/togetherAi/scripts/chat_models.txt b/server/utils/AiProviders/togetherAi/scripts/chat_models.txt index 81c23bf4a..03f0414cb 100644 --- a/server/utils/AiProviders/togetherAi/scripts/chat_models.txt +++ b/server/utils/AiProviders/togetherAi/scripts/chat_models.txt @@ -1,39 +1,55 @@ -| Organization | Model Name | Model String for API | Max Seq Length | -| ------------- | ---------------------------- | -------------------------------------------- | -------------- | -| Stanford | Alpaca (7B) | togethercomputer/alpaca-7b | 2048 | -| Austism | Chronos Hermes (13B) | Austism/chronos-hermes-13b | 2048 | -| Meta | Code Llama Instruct (13B) | togethercomputer/CodeLlama-13b-Instruct | 8192 | -| Meta | Code Llama Instruct (34B) | togethercomputer/CodeLlama-34b-Instruct | 8192 | -| Meta | Code Llama Instruct (7B) | togethercomputer/CodeLlama-7b-Instruct | 8192 | -| DiscoResearch | DiscoLM Mixtral 8x7b | DiscoResearch/DiscoLM-mixtral-8x7b-v2 | 32768 | -| TII UAE | Falcon Instruct (40B) | togethercomputer/falcon-40b-instruct | 2048 | -| TII UAE | Falcon Instruct (7B) | togethercomputer/falcon-7b-instruct | 2048 | -| Together | GPT-NeoXT-Chat-Base (20B) | togethercomputer/GPT-NeoXT-Chat-Base-20B | 2048 | -| Meta | LLaMA-2 Chat (13B) | togethercomputer/llama-2-13b-chat | 4096 | -| Meta | LLaMA-2 Chat (70B) | togethercomputer/llama-2-70b-chat | 4096 | -| Meta | LLaMA-2 Chat (7B) | togethercomputer/llama-2-7b-chat | 4096 | -| Together | LLaMA-2-7B-32K-Instruct (7B) | togethercomputer/Llama-2-7B-32K-Instruct | 32768 | -| MistralAI | Mistral (7B) Instruct v0.1 | mistralai/Mistral-7B-Instruct-v0.1 | 4096 | -| MistralAI | Mistral (7B) Instruct v0.2 | mistralai/Mistral-7B-Instruct-v0.2 | 32768 | -| MistralAI | Mixtral-8x7B Instruct | mistralai/Mixtral-8x7B-Instruct-v0.1 | 32768 | -| Gryphe | MythoMax-L2 (13B) | Gryphe/MythoMax-L2-13b | 4096 | -| NousResearch | Nous Hermes LLaMA-2 (7B) | NousResearch/Nous-Hermes-llama-2-7b | 4096 | -| NousResearch | Nous Hermes Llama-2 (13B) | NousResearch/Nous-Hermes-Llama2-13b | 4096 | -| NousResearch | Nous Hermes Llama-2 (70B) | NousResearch/Nous-Hermes-Llama2-70b | 4096 | -| NousResearch | Nous Hermes-2 Yi (34B) | NousResearch/Nous-Hermes-2-Yi-34B | 4096 | -| NousResearch | Nous Capybara v1.9 (7B) | NousResearch/Nous-Capybara-7B-V1p9 | 8192 | -| OpenChat | OpenChat 3.5 1210 (7B) | openchat/openchat-3.5-1210 | 8192 | -| teknium | OpenHermes-2-Mistral (7B) | teknium/OpenHermes-2-Mistral-7B | 4096 | -| teknium | OpenHermes-2.5-Mistral (7B) | teknium/OpenHermes-2p5-Mistral-7B | 4096 | -| OpenOrca | OpenOrca Mistral (7B) 8K | Open-Orca/Mistral-7B-OpenOrca | 8192 | -| garage-bAInd | Platypus2 Instruct (70B) | garage-bAInd/Platypus2-70B-instruct | 4096 | -| Together | Pythia-Chat-Base (7B) | togethercomputer/Pythia-Chat-Base-7B-v0.16 | 2048 | -| Qwen | Qwen-Chat (7B) | togethercomputer/Qwen-7B-Chat | 8192 | -| Together | RedPajama-INCITE Chat (3B) | togethercomputer/RedPajama-INCITE-Chat-3B-v1 | 2048 | -| Together | RedPajama-INCITE Chat (7B) | togethercomputer/RedPajama-INCITE-7B-Chat | 2048 | -| Upstage | SOLAR v0 (70B) | upstage/SOLAR-0-70b-16bit | 4096 | -| Together | StripedHyena Nous (7B) | togethercomputer/StripedHyena-Nous-7B | 32768 | -| LM Sys | Vicuna v1.5 (7B) | lmsys/vicuna-7b-v1.5 | 4096 | -| LM Sys | Vicuna v1.5 (13B) | lmsys/vicuna-13b-v1.5 | 4096 | -| LM Sys | Vicuna v1.5 16K (13B) | lmsys/vicuna-13b-v1.5-16k | 16384 | -| 01.AI | 01-ai Yi Chat (34B) | zero-one-ai/Yi-34B-Chat | 4096 | \ No newline at end of file +| Organization | Model Name | Model String for API | Context length | +| --- | --- | --- | --- | +| 01.AI | 01-ai Yi Chat (34B) | zero-one-ai/Yi-34B-Chat | 4096 | +| Allen AI | OLMo Instruct (7B) | allenai/OLMo-7B-Instruct | 2048 | +| Allen AI | OLMo Twin-2T (7B) | allenai/OLMo-7B-Twin-2T | 2048 | +| Allen AI | OLMo (7B) | allenai/OLMo-7B | 2048 | +| Austism | Chronos Hermes (13B) | Austism/chronos-hermes-13b | 2048 | +| cognitivecomputations | Dolphin 2.5 Mixtral 8x7b | cognitivecomputations/dolphin-2.5-mixtral-8x7b | 32768 | +| databricks | DBRX Instruct | databricks/dbrx-instruct | 32000 | +| DeepSeek | Deepseek Coder Instruct (33B) | deepseek-ai/deepseek-coder-33b-instruct | 16384 | +| DeepSeek | DeepSeek LLM Chat (67B) | deepseek-ai/deepseek-llm-67b-chat | 4096 | +| garage-bAInd | Platypus2 Instruct (70B) | garage-bAInd/Platypus2-70B-instruct | 4096 | +| Google | Gemma Instruct (2B) | google/gemma-2b-it | 8192 | +| Google | Gemma Instruct (7B) | google/gemma-7b-it | 8192 | +| Gryphe | MythoMax-L2 (13B) | Gryphe/MythoMax-L2-13b | 4096 | +| LM Sys | Vicuna v1.5 (13B) | lmsys/vicuna-13b-v1.5 | 4096 | +| LM Sys | Vicuna v1.5 (7B) | lmsys/vicuna-7b-v1.5 | 4096 | +| Meta | Code Llama Instruct (13B) | codellama/CodeLlama-13b-Instruct-hf | 16384 | +| Meta | Code Llama Instruct (34B) | codellama/CodeLlama-34b-Instruct-hf | 16384 | +| Meta | Code Llama Instruct (70B) | codellama/CodeLlama-70b-Instruct-hf | 4096 | +| Meta | Code Llama Instruct (7B) | codellama/CodeLlama-7b-Instruct-hf | 16384 | +| Meta | LLaMA-2 Chat (70B) | meta-llama/Llama-2-70b-chat-hf | 4096 | +| Meta | LLaMA-2 Chat (13B) | meta-llama/Llama-2-13b-chat-hf | 4096 | +| Meta | LLaMA-2 Chat (7B) | meta-llama/Llama-2-7b-chat-hf | 4096 | +| mistralai | Mistral (7B) Instruct | mistralai/Mistral-7B-Instruct-v0.1 | 8192 | +| mistralai | Mistral (7B) Instruct v0.2 | mistralai/Mistral-7B-Instruct-v0.2 | 32768 | +| mistralai | Mixtral-8x7B Instruct (46.7B) | mistralai/Mixtral-8x7B-Instruct-v0.1 | 32768 | +| NousResearch | Nous Capybara v1.9 (7B) | NousResearch/Nous-Capybara-7B-V1p9 | 8192 | +| NousResearch | Nous Hermes 2 - Mistral DPO (7B) | NousResearch/Nous-Hermes-2-Mistral-7B-DPO | 32768 | +| NousResearch | Nous Hermes 2 - Mixtral 8x7B-DPO (46.7B) | NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO | 32768 | +| NousResearch | Nous Hermes 2 - Mixtral 8x7B-SFT (46.7B) | NousResearch/Nous-Hermes-2-Mixtral-8x7B-SFT | 32768 | +| NousResearch | Nous Hermes LLaMA-2 (7B) | NousResearch/Nous-Hermes-llama-2-7b | 4096 | +| NousResearch | Nous Hermes Llama-2 (13B) | NousResearch/Nous-Hermes-Llama2-13b | 4096 | +| NousResearch | Nous Hermes-2 Yi (34B) | NousResearch/Nous-Hermes-2-Yi-34B | 4096 | +| OpenChat | OpenChat 3.5 (7B) | openchat/openchat-3.5-1210 | 8192 | +| OpenOrca | OpenOrca Mistral (7B) 8K | Open-Orca/Mistral-7B-OpenOrca | 8192 | +| Qwen | Qwen 1.5 Chat (0.5B) | Qwen/Qwen1.5-0.5B-Chat | 32768 | +| Qwen | Qwen 1.5 Chat (1.8B) | Qwen/Qwen1.5-1.8B-Chat | 32768 | +| Qwen | Qwen 1.5 Chat (4B) | Qwen/Qwen1.5-4B-Chat | 32768 | +| Qwen | Qwen 1.5 Chat (7B) | Qwen/Qwen1.5-7B-Chat | 32768 | +| Qwen | Qwen 1.5 Chat (14B) | Qwen/Qwen1.5-14B-Chat | 32768 | +| Qwen | Qwen 1.5 Chat (32B) | Qwen/Qwen1.5-32B-Chat | 32768 | +| Qwen | Qwen 1.5 Chat (72B) | Qwen/Qwen1.5-72B-Chat | 32768 | +| Snorkel AI | Snorkel Mistral PairRM DPO (7B) | snorkelai/Snorkel-Mistral-PairRM-DPO | 32768 | +| Stanford | Alpaca (7B) | togethercomputer/alpaca-7b | 2048 | +| Teknium | OpenHermes-2-Mistral (7B) | teknium/OpenHermes-2-Mistral-7B | 8192 | +| Teknium | OpenHermes-2.5-Mistral (7B) | teknium/OpenHermes-2p5-Mistral-7B | 8192 | +| Together | LLaMA-2-7B-32K-Instruct (7B) | togethercomputer/Llama-2-7B-32K-Instruct | 32768 | +| Together | RedPajama-INCITE Chat (3B) | togethercomputer/RedPajama-INCITE-Chat-3B-v1 | 2048 | +| Together | RedPajama-INCITE Chat (7B) | togethercomputer/RedPajama-INCITE-7B-Chat | 2048 | +| Together | StripedHyena Nous (7B) | togethercomputer/StripedHyena-Nous-7B | 32768 | +| Undi95 | ReMM SLERP L2 (13B) | Undi95/ReMM-SLERP-L2-13B | 4096 | +| Undi95 | Toppy M (7B) | Undi95/Toppy-M-7B | 4096 | +| WizardLM | WizardLM v1.2 (13B) | WizardLM/WizardLM-13B-V1.2 | 4096 | +| upstage | Upstage SOLAR Instruct v1 (11B) | upstage/SOLAR-10.7B-Instruct-v1.0 | 4096 | \ No newline at end of file diff --git a/server/utils/AiProviders/togetherAi/scripts/parse.mjs b/server/utils/AiProviders/togetherAi/scripts/parse.mjs index b96d40ab1..b78404012 100644 --- a/server/utils/AiProviders/togetherAi/scripts/parse.mjs +++ b/server/utils/AiProviders/togetherAi/scripts/parse.mjs @@ -8,7 +8,9 @@ // copy outputs into the export in ../models.js // Update the date below if you run this again because TogetherAI added new models. -// Last Collected: Jan 10, 2023 +// Last Collected: Apr 14, 2024 +// Since last collection Together's docs are broken. I just copied the HTML table +// and had claude3 convert to markdown and it works well enough. import fs from "fs"; From 86c01aeb429f6c68338633b92520e9333897c684 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowAlyxia@users.noreply.github.com> Date: Mon, 15 Apr 2024 21:12:41 +0530 Subject: [PATCH 03/10] =?UTF-8?q?=F0=9F=93=9D=20Docs=20Update=20(Readme)?= =?UTF-8?q?=20(#1106)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📝 Updated Readme --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 69e8f867f..88a4f489a 100644 --- a/README.md +++ b/README.md @@ -58,15 +58,16 @@ Some cool features of AnythingLLM - Extremely efficient cost-saving measures for managing very large documents. You'll never pay to embed a massive document or transcript more than once. 90% more cost effective than other document chatbot solutions. - Full Developer API for custom integrations! -### Supported LLMs, Embedders, and Vector Databases +### Supported LLMs, Embedders, Transcriptions models, and Vector Databases **Supported LLMs:** - [Any open-source llama.cpp compatible model](/server/storage/models/README.md#text-generation-llm-selection) - [OpenAI](https://openai.com) - [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service) -- [Anthropic ClaudeV2](https://www.anthropic.com/) +- [Anthropic](https://www.anthropic.com/) - [Google Gemini Pro](https://ai.google.dev/) +- [Hugging Face (chat models)](https://huggingface.co/) - [Ollama (chat models)](https://ollama.ai/) - [LM Studio (all models)](https://lmstudio.ai) - [LocalAi (all models)](https://localai.io/) @@ -74,6 +75,7 @@ Some cool features of AnythingLLM - [Perplexity (chat models)](https://www.perplexity.ai/) - [OpenRouter (chat models)](https://openrouter.ai/) - [Mistral](https://mistral.ai/) +- [Groq](https://groq.com/) **Supported Embedding models:** @@ -84,6 +86,11 @@ Some cool features of AnythingLLM - [LocalAi (all)](https://localai.io/) - [Ollama (all)](https://ollama.ai/) +**Supported Transcription models:** + +- [AnythingLLM Built-in](https://github.com/Mintplex-Labs/anything-llm/tree/master/server/storage/models#audiovideo-transcription) (default) +- [OpenAI](https://openai.com/) + **Supported Vector Databases:** - [LanceDB](https://github.com/lancedb/lancedb) (default) From a5bb77f97aed3ed6d0ec114f49d62c88e291cc6d Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Tue, 16 Apr 2024 10:50:10 -0700 Subject: [PATCH 04/10] Agent support for `@agent` default agent inside workspace chat (#1093) V1 of agent support via built-in `@agent` that can be invoked alongside normal workspace RAG chat. --- .dockerignore | 1 + .github/workflows/build-and-push-image.yaml | 1 + .vscode/settings.json | 4 + .../cloudformation/aws_https_instructions.md | 18 + collector/index.js | 22 +- collector/processLink/convert/generic.js | 29 +- collector/processLink/index.js | 6 + docker/.env.example | 13 + .../ChatContainer/ChatHistory/index.jsx | 20 + .../PromptInput/AgentMenu/index.jsx | 189 +++++ .../SlashCommands/endAgentSession.jsx | 23 + .../PromptInput/SlashCommands/index.jsx | 29 +- .../PromptInput/SlashCommands/reset.jsx | 23 + .../StopGenerationButton/index.jsx | 2 +- .../ChatContainer/PromptInput/index.jsx | 23 + .../WorkspaceChat/ChatContainer/index.jsx | 100 ++- frontend/src/index.css | 13 + .../AgentLLMSelection/AgentLLMItem/index.jsx | 151 ++++ .../AgentConfig/AgentLLMSelection/index.jsx | 163 ++++ .../AgentConfig/AgentModelSelection/index.jsx | 112 +++ .../AgentConfig/GenericSkill/index.jsx | 39 + .../SearchProviderItem/index.jsx | 27 + .../SearchProviderOptions/index.jsx | 84 ++ .../WebSearchSelection/icons/google.png | Bin 0 -> 18000 bytes .../WebSearchSelection/icons/serper.png | Bin 0 -> 31321 bytes .../AgentConfig/WebSearchSelection/index.jsx | 194 +++++ .../WorkspaceSettings/AgentConfig/index.jsx | 203 +++++ .../src/pages/WorkspaceSettings/index.jsx | 8 + frontend/src/utils/chat/agent.js | 103 +++ frontend/src/utils/chat/index.js | 9 +- frontend/src/utils/paths.js | 3 + server/.env.example | 13 + server/endpoints/admin.js | 11 +- server/endpoints/agentWebsocket.js | 61 ++ server/index.js | 3 + server/models/documents.js | 9 + server/models/systemSettings.js | 31 + server/models/telemetry.js | 19 +- server/models/workspace.js | 2 + server/models/workspaceAgentInvocation.js | 95 +++ server/package.json | 8 +- .../20240412183346_init/migration.sql | 24 + server/prisma/schema.prisma | 46 +- server/utils/agents/aibitat/error.js | 18 + .../utils/agents/aibitat/example/.gitignore | 1 + .../agents/aibitat/example/beginner-chat.js | 56 ++ .../aibitat/example/blog-post-coding.js | 55 ++ .../aibitat/example/websocket/index.html | 67 ++ .../websocket/websock-branding-collab.js | 100 +++ .../websocket/websock-multi-turn-chat.js | 91 +++ server/utils/agents/aibitat/index.js | 747 ++++++++++++++++++ .../agents/aibitat/plugins/chat-history.js | 49 ++ server/utils/agents/aibitat/plugins/cli.js | 140 ++++ .../agents/aibitat/plugins/file-history.js | 37 + server/utils/agents/aibitat/plugins/index.js | 26 + server/utils/agents/aibitat/plugins/memory.js | 134 ++++ .../aibitat/plugins/save-file-browser.js | 70 ++ .../utils/agents/aibitat/plugins/summarize.js | 130 +++ .../agents/aibitat/plugins/web-browsing.js | 169 ++++ .../agents/aibitat/plugins/web-scraping.js | 87 ++ .../utils/agents/aibitat/plugins/websocket.js | 150 ++++ .../agents/aibitat/providers/ai-provider.js | 19 + .../agents/aibitat/providers/anthropic.js | 151 ++++ .../utils/agents/aibitat/providers/index.js | 7 + .../utils/agents/aibitat/providers/openai.js | 144 ++++ server/utils/agents/aibitat/utils/dedupe.js | 35 + .../utils/agents/aibitat/utils/summarize.js | 52 ++ server/utils/agents/defaults.js | 42 + server/utils/agents/index.js | 201 +++++ server/utils/chats/agents.js | 71 ++ server/utils/chats/stream.js | 12 + server/utils/collectorApi/index.js | 23 + server/utils/helpers/updateENV.js | 20 + server/yarn.lock | 321 +++++++- 74 files changed, 5107 insertions(+), 52 deletions(-) create mode 100644 frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/AgentMenu/index.jsx create mode 100644 frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/SlashCommands/endAgentSession.jsx create mode 100644 frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/SlashCommands/reset.jsx create mode 100644 frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/AgentLLMItem/index.jsx create mode 100644 frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx create mode 100644 frontend/src/pages/WorkspaceSettings/AgentConfig/AgentModelSelection/index.jsx create mode 100644 frontend/src/pages/WorkspaceSettings/AgentConfig/GenericSkill/index.jsx create mode 100644 frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/SearchProviderItem/index.jsx create mode 100644 frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/SearchProviderOptions/index.jsx create mode 100644 frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/icons/google.png create mode 100644 frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/icons/serper.png create mode 100644 frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/index.jsx create mode 100644 frontend/src/pages/WorkspaceSettings/AgentConfig/index.jsx create mode 100644 frontend/src/utils/chat/agent.js create mode 100644 server/endpoints/agentWebsocket.js create mode 100644 server/models/workspaceAgentInvocation.js create mode 100644 server/prisma/migrations/20240412183346_init/migration.sql create mode 100644 server/utils/agents/aibitat/error.js create mode 100644 server/utils/agents/aibitat/example/.gitignore create mode 100644 server/utils/agents/aibitat/example/beginner-chat.js create mode 100644 server/utils/agents/aibitat/example/blog-post-coding.js create mode 100644 server/utils/agents/aibitat/example/websocket/index.html create mode 100644 server/utils/agents/aibitat/example/websocket/websock-branding-collab.js create mode 100644 server/utils/agents/aibitat/example/websocket/websock-multi-turn-chat.js create mode 100644 server/utils/agents/aibitat/index.js create mode 100644 server/utils/agents/aibitat/plugins/chat-history.js create mode 100644 server/utils/agents/aibitat/plugins/cli.js create mode 100644 server/utils/agents/aibitat/plugins/file-history.js create mode 100644 server/utils/agents/aibitat/plugins/index.js create mode 100644 server/utils/agents/aibitat/plugins/memory.js create mode 100644 server/utils/agents/aibitat/plugins/save-file-browser.js create mode 100644 server/utils/agents/aibitat/plugins/summarize.js create mode 100644 server/utils/agents/aibitat/plugins/web-browsing.js create mode 100644 server/utils/agents/aibitat/plugins/web-scraping.js create mode 100644 server/utils/agents/aibitat/plugins/websocket.js create mode 100644 server/utils/agents/aibitat/providers/ai-provider.js create mode 100644 server/utils/agents/aibitat/providers/anthropic.js create mode 100644 server/utils/agents/aibitat/providers/index.js create mode 100644 server/utils/agents/aibitat/providers/openai.js create mode 100644 server/utils/agents/aibitat/utils/dedupe.js create mode 100644 server/utils/agents/aibitat/utils/summarize.js create mode 100644 server/utils/agents/defaults.js create mode 100644 server/utils/agents/index.js create mode 100644 server/utils/chats/agents.js diff --git a/.dockerignore b/.dockerignore index 32582ced3..fcfafb0e7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ +**/server/utils/agents/aibitat/example/** **/server/storage/documents/** **/server/storage/vector-cache/** **/server/storage/*.db diff --git a/.github/workflows/build-and-push-image.yaml b/.github/workflows/build-and-push-image.yaml index 2044c66ba..bbc2d0064 100644 --- a/.github/workflows/build-and-push-image.yaml +++ b/.github/workflows/build-and-push-image.yaml @@ -21,6 +21,7 @@ on: - '**/.env.example' - '.github/ISSUE_TEMPLATE/**/*' - 'embed/**/*' # Embed should be published to frontend (yarn build:publish) if any changes are introduced + - 'server/utils/agents/aibitat/example/**/*' # Do not push new image for local dev testing of new aibitat images. jobs: push_multi_platform_to_registries: diff --git a/.vscode/settings.json b/.vscode/settings.json index 724023459..7d17c99ee 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,12 @@ { "cSpell.words": [ + "AIbitat", "adoc", + "aibitat", "anythingllm", "Astra", "comkey", + "Deduplicator", "Dockerized", "Embeddable", "epub", @@ -19,6 +22,7 @@ "opendocument", "openrouter", "Qdrant", + "Serper", "vectordbs", "Weaviate", "Zilliz" diff --git a/cloud-deployments/aws/cloudformation/aws_https_instructions.md b/cloud-deployments/aws/cloudformation/aws_https_instructions.md index 39591820b..26b0a6ba5 100644 --- a/cloud-deployments/aws/cloudformation/aws_https_instructions.md +++ b/cloud-deployments/aws/cloudformation/aws_https_instructions.md @@ -36,6 +36,7 @@ These instructions are for CLI configuration and assume you are logged in to EC2 These instructions are for CLI configuration and assume you are logged in to EC2 instance as the ec2-user. 1. $sudo vi /etc/nginx/nginx.conf 2. In the nginx.conf file, comment out the default server block configuration for http/port 80. It should look something like the following: +``` # server { # listen 80; # listen [::]:80; @@ -53,13 +54,23 @@ These instructions are for CLI configuration and assume you are logged in to EC2 # location = /50x.html { # } # } +``` 3. Enter ':wq' to save the changes to the nginx default config ## Step 7: Create simple http proxy configuration for AnythingLLM These instructions are for CLI configuration and assume you are logged in to EC2 instance as the ec2-user. 1. $sudo vi /etc/nginx/conf.d/anything.conf 2. Add the following configuration ensuring that you add your FQDN:. + +``` server { + # Enable websocket connections for agent protocol. + location ~* ^/api/agent-invocation/(.*) { + proxy_pass http://0.0.0.0:3001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + } listen 80; server_name [insert FQDN here]; @@ -70,9 +81,16 @@ server { proxy_read_timeout 605; send_timeout 605; keepalive_timeout 605; + + # Enable readable HTTP Streaming for LLM streamed responses + proxy_buffering off; + proxy_cache off; + + # Proxy your locally running service proxy_pass http://0.0.0.0:3001; } } +``` 3. Enter ':wq' to save the changes to the anything config file ## Step 8: Test nginx http proxy config and restart nginx service diff --git a/collector/index.js b/collector/index.js index f574730c6..fe6df58a8 100644 --- a/collector/index.js +++ b/collector/index.js @@ -9,7 +9,7 @@ const path = require("path"); const { ACCEPTED_MIMES } = require("./utils/constants"); const { reqBody } = require("./utils/http"); const { processSingleFile } = require("./processSingleFile"); -const { processLink } = require("./processLink"); +const { processLink, getLinkText } = require("./processLink"); const { wipeCollectorStorage } = require("./utils/files"); const extensions = require("./extensions"); const { processRawText } = require("./processRawText"); @@ -76,6 +76,26 @@ app.post( } ); +app.post( + "/util/get-link", + [verifyPayloadIntegrity], + async function (request, response) { + const { link } = reqBody(request); + try { + const { success, content = null } = await getLinkText(link); + response.status(200).json({ url: link, success, content }); + } catch (e) { + console.error(e); + response.status(200).json({ + url: link, + success: false, + content: null, + }); + } + return; + } +); + app.post( "/process-raw-text", [verifyPayloadIntegrity], diff --git a/collector/processLink/convert/generic.js b/collector/processLink/convert/generic.js index 1292b850c..a05463abf 100644 --- a/collector/processLink/convert/generic.js +++ b/collector/processLink/convert/generic.js @@ -6,7 +6,7 @@ const { writeToServerDocuments } = require("../../utils/files"); const { tokenizeString } = require("../../utils/tokenizer"); const { default: slugify } = require("slugify"); -async function scrapeGenericUrl(link) { +async function scrapeGenericUrl(link, textOnly = false) { console.log(`-- Working URL ${link} --`); const content = await getPageContent(link); @@ -19,6 +19,13 @@ async function scrapeGenericUrl(link) { }; } + if (textOnly) { + return { + success: true, + content, + }; + } + const url = new URL(link); const filename = (url.host + "-" + url.pathname).replace(".", "_"); @@ -69,8 +76,26 @@ async function getPageContent(link) { return pageContents.join(" "); } catch (error) { - console.error("getPageContent failed!", error); + console.error( + "getPageContent failed to be fetched by puppeteer - falling back to fetch!", + error + ); } + + try { + const pageText = await fetch(link, { + method: "GET", + headers: { + "Content-Type": "text/plain", + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36,gzip(gfe)", + }, + }).then((res) => res.text()); + return pageText; + } catch (error) { + console.error("getPageContent failed to be fetched by any method.", error); + } + return null; } diff --git a/collector/processLink/index.js b/collector/processLink/index.js index bd3ced19f..afa517cae 100644 --- a/collector/processLink/index.js +++ b/collector/processLink/index.js @@ -6,6 +6,12 @@ async function processLink(link) { return await scrapeGenericUrl(link); } +async function getLinkText(link) { + if (!validURL(link)) return { success: false, reason: "Not a valid URL." }; + return await scrapeGenericUrl(link, true); +} + module.exports = { processLink, + getLinkText, }; diff --git a/docker/.env.example b/docker/.env.example index 5efb2c049..32f2a55d4 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -168,3 +168,16 @@ GID='1000' #ENABLE_HTTPS="true" #HTTPS_CERT_PATH="sslcert/cert.pem" #HTTPS_KEY_PATH="sslcert/key.pem" + +########################################### +######## AGENT SERVICE KEYS ############### +########################################### + +#------ SEARCH ENGINES ------- +#============================= +#------ Google Search -------- https://programmablesearchengine.google.com/controlpanel/create +# AGENT_GSE_KEY= +# AGENT_GSE_CTX= + +#------ Serper.dev ----------- https://serper.dev/ +# AGENT_SERPER_DEV_KEY= \ No newline at end of file diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx index 87c90c80b..dc7331476 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx @@ -99,6 +99,10 @@ export default function ChatHistory({ history = [], workspace, sendCommand }) { const isLastBotReply = index === history.length - 1 && props.role === "assistant"; + if (props?.type === "statusResponse" && !!props.content) { + return ; + } + if (isLastBotReply && props.animate) { return ( +
+
+ + {props.content} + +
+
+ + ); +} + function WorkspaceChatSuggestions({ suggestions = [], sendSuggestion }) { if (suggestions.length === 0) return null; return ( diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/AgentMenu/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/AgentMenu/index.jsx new file mode 100644 index 000000000..ef73cb656 --- /dev/null +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/AgentMenu/index.jsx @@ -0,0 +1,189 @@ +import { useEffect, useRef, useState } from "react"; +import { Tooltip } from "react-tooltip"; +import { At, Flask, X } from "@phosphor-icons/react"; +import ModalWrapper from "@/components/ModalWrapper"; +import { useModal } from "@/hooks/useModal"; +import { useIsAgentSessionActive } from "@/utils/chat/agent"; + +export default function AvailableAgentsButton({ showing, setShowAgents }) { + const agentSessionActive = useIsAgentSessionActive(); + if (agentSessionActive) return null; + return ( +
setShowAgents(!showing)} + className={`flex justify-center items-center opacity-60 hover:opacity-100 cursor-pointer ${ + showing ? "!opacity-100" : "" + }`} + > + + +
+ ); +} + +function AbilityTag({ text }) { + return ( +
+

{text}

+
+ ); +} + +export function AvailableAgents({ + showing, + setShowing, + sendCommand, + promptRef, +}) { + const formRef = useRef(null); + const agentSessionActive = useIsAgentSessionActive(); + useEffect(() => { + function listenForOutsideClick() { + if (!showing || !formRef.current) return false; + document.addEventListener("click", closeIfOutside); + } + listenForOutsideClick(); + }, [showing, formRef.current]); + + const closeIfOutside = ({ target }) => { + if (target.id === "agent-list-btn") return; + const isOutside = !formRef?.current?.contains(target); + if (!isOutside) return; + setShowing(false); + }; + + if (agentSessionActive) return null; + return ( + <> + + {showing && } + + ); +} + +export function useAvailableAgents() { + const [showAgents, setShowAgents] = useState(false); + return { showAgents, setShowAgents }; +} + +const SEEN_FT_AGENT_MODAL = "anythingllm_seen_first_time_agent_modal"; +function FirstTimeAgentUser() { + const { isOpen, openModal, closeModal } = useModal(); + useEffect(() => { + function firstTimeShow() { + if (!window) return; + if (!window.localStorage.getItem(SEEN_FT_AGENT_MODAL)) openModal(); + } + firstTimeShow(); + }, []); + + const dismiss = () => { + closeModal(); + window.localStorage.setItem(SEEN_FT_AGENT_MODAL, 1); + }; + + return ( + +
+
+
+ +

+ You just discovered Agents! +

+ +
+
+
+

+ Agents are your LLM, but with special abilities that{" "} + do something beyond chatting with your documents. +
+
+ Now you can use agents for real-time web search and scraping, + saving documents to your browser, summarizing documents, and + more. +
+
+ Currently, agents only work with OpenAI and Anthropic as your + agent LLM. All providers will be supported in the future. +

+

+ This feature is currently early access and fully custom agents + with custom integrations & code execution will be in a future + update. +

+
+
+
+
+ +
+
+
+ + ); +} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/SlashCommands/endAgentSession.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/SlashCommands/endAgentSession.jsx new file mode 100644 index 000000000..093fd5a10 --- /dev/null +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/SlashCommands/endAgentSession.jsx @@ -0,0 +1,23 @@ +import { useIsAgentSessionActive } from "@/utils/chat/agent"; + +export default function EndAgentSession({ setShowing, sendCommand }) { + const isActiveAgentSession = useIsAgentSessionActive(); + if (!isActiveAgentSession) return null; + + return ( + + ); +} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/SlashCommands/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/SlashCommands/index.jsx index 1e85d372d..5a606af6d 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/SlashCommands/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/SlashCommands/index.jsx @@ -1,6 +1,8 @@ import { useEffect, useRef, useState } from "react"; import SlashCommandIcon from "./icons/slash-commands-icon.svg"; import { Tooltip } from "react-tooltip"; +import ResetCommand from "./reset"; +import EndAgentSession from "./endAgentSession"; export default function SlashCommandsButton({ showing, setShowSlashCommand }) { return ( @@ -38,7 +40,6 @@ export function SlashCommands({ showing, setShowing, sendCommand }) { listenForOutsideClick(); }, [showing, cmdRef.current]); - if (!showing) return null; const closeIfOutside = ({ target }) => { if (target.id === "slash-cmd-btn") return; const isOutside = !cmdRef?.current?.contains(target); @@ -47,25 +48,15 @@ export function SlashCommands({ showing, setShowing, sendCommand }) { }; return ( -
-
- + + +
); diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/SlashCommands/reset.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/SlashCommands/reset.jsx new file mode 100644 index 000000000..e53ec26d0 --- /dev/null +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/SlashCommands/reset.jsx @@ -0,0 +1,23 @@ +import { useIsAgentSessionActive } from "@/utils/chat/agent"; + +export default function ResetCommand({ setShowing, sendCommand }) { + const isActiveAgentSession = useIsAgentSessionActive(); + if (isActiveAgentSession) return null; // cannot reset during active agent chat + + return ( + + ); +} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/StopGenerationButton/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/StopGenerationButton/index.jsx index f9872a0d8..6c3d16dbf 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/StopGenerationButton/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/StopGenerationButton/index.jsx @@ -28,7 +28,7 @@ export default function StopGenerationButton() { cx="10" cy="10.562" r="9" - stroke-width="2" + strokeWidth="2" /> { + const input = e.target.value; + if (input === "@") return setShowAgents(true); + if (showAgents) return setShowAgents(false); + }; + const captureEnter = (event) => { if (event.keyCode == 13) { if (!event.shiftKey) { @@ -61,6 +72,7 @@ export default function PromptInput({ }; const watchForSlash = debounce(checkForSlash, 300); + const watchForAt = debounce(checkForAt, 300); return (
@@ -69,6 +81,12 @@ export default function PromptInput({ setShowing={setShowSlashCommand} sendCommand={sendCommand} /> +
{ onChange(e); watchForSlash(e); + watchForAt(e); adjustTextArea(e); }} onKeyDown={captureEnter} @@ -114,6 +133,10 @@ export default function PromptInput({ showing={showSlashCommand} setShowSlashCommand={setShowSlashCommand} /> +
diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx index f215d18b9..7d2850bdc 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx @@ -2,16 +2,24 @@ import { useState, useEffect } from "react"; import ChatHistory from "./ChatHistory"; import PromptInput from "./PromptInput"; import Workspace from "@/models/workspace"; -import handleChat from "@/utils/chat"; +import handleChat, { ABORT_STREAM_EVENT } from "@/utils/chat"; import { isMobile } from "react-device-detect"; import { SidebarMobileHeader } from "../../Sidebar"; import { useParams } from "react-router-dom"; +import { v4 } from "uuid"; +import handleSocketResponse, { + websocketURI, + AGENT_SESSION_END, + AGENT_SESSION_START, +} from "@/utils/chat/agent"; export default function ChatContainer({ workspace, knownHistory = [] }) { const { threadSlug = null } = useParams(); const [message, setMessage] = useState(""); const [loadingResponse, setLoadingResponse] = useState(false); const [chatHistory, setChatHistory] = useState(knownHistory); + const [socketId, setSocketId] = useState(null); + const [websocket, setWebsocket] = useState(null); const handleMessageChange = (event) => { setMessage(event.target.value); }; @@ -68,6 +76,19 @@ export default function ChatContainer({ workspace, knownHistory = [] }) { const remHistory = chatHistory.length > 0 ? chatHistory.slice(0, -1) : []; var _chatHistory = [...remHistory]; + // Override hook for new messages to now go to agents until the connection closes + if (!!websocket) { + if (!promptMessage || !promptMessage?.userMessage) return false; + websocket.send( + JSON.stringify({ + type: "awaitingFeedback", + feedback: promptMessage?.userMessage, + }) + ); + return; + } + + // TODO: Simplify this if (!promptMessage || !promptMessage?.userMessage) return false; if (!!threadSlug) { await Workspace.threads.streamChat( @@ -79,7 +100,8 @@ export default function ChatContainer({ workspace, knownHistory = [] }) { setLoadingResponse, setChatHistory, remHistory, - _chatHistory + _chatHistory, + setSocketId ) ); } else { @@ -92,7 +114,8 @@ export default function ChatContainer({ workspace, knownHistory = [] }) { setLoadingResponse, setChatHistory, remHistory, - _chatHistory + _chatHistory, + setSocketId ) ); } @@ -101,6 +124,77 @@ export default function ChatContainer({ workspace, knownHistory = [] }) { loadingResponse === true && fetchReply(); }, [loadingResponse, chatHistory, workspace]); + // TODO: Simplify this WSS stuff + useEffect(() => { + function handleWSS() { + try { + if (!socketId || !!websocket) return; + const socket = new WebSocket( + `${websocketURI()}/api/agent-invocation/${socketId}` + ); + + window.addEventListener(ABORT_STREAM_EVENT, () => { + window.dispatchEvent(new CustomEvent(AGENT_SESSION_END)); + websocket.close(); + }); + + socket.addEventListener("message", (event) => { + setLoadingResponse(true); + try { + handleSocketResponse(event, setChatHistory); + } catch (e) { + console.error("Failed to parse data"); + window.dispatchEvent(new CustomEvent(AGENT_SESSION_END)); + socket.close(); + } + setLoadingResponse(false); + }); + + socket.addEventListener("close", (_event) => { + window.dispatchEvent(new CustomEvent(AGENT_SESSION_END)); + setChatHistory((prev) => [ + ...prev.filter((msg) => !!msg.content), + { + uuid: v4(), + type: "statusResponse", + content: "Agent session complete.", + role: "assistant", + sources: [], + closed: true, + error: null, + animate: false, + pending: false, + }, + ]); + setLoadingResponse(false); + setWebsocket(null); + setSocketId(null); + }); + setWebsocket(socket); + window.dispatchEvent(new CustomEvent(AGENT_SESSION_START)); + } catch (e) { + setChatHistory((prev) => [ + ...prev.filter((msg) => !!msg.content), + { + uuid: v4(), + type: "abort", + content: e.message, + role: "assistant", + sources: [], + closed: true, + error: e.message, + animate: false, + pending: false, + }, + ]); + setLoadingResponse(false); + setWebsocket(null); + setSocketId(null); + } + } + handleWSS(); + }, [socketId]); + return (
ul { padding-left: 20px; margin: 0px; @@ -585,6 +593,11 @@ dialog::backdrop { margin: 0.35rem; } +.markdown > p > a, +.markdown p a { + text-decoration: underline; +} + .markdown { text-wrap: wrap; } diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/AgentLLMItem/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/AgentLLMItem/index.jsx new file mode 100644 index 000000000..804fe8aef --- /dev/null +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/AgentLLMItem/index.jsx @@ -0,0 +1,151 @@ +// This component differs from the main LLMItem in that it shows if a provider is +// "ready for use" and if not - will then highjack the click handler to show a modal +// of the provider options that must be saved to continue. +import { createPortal } from "react-dom"; +import ModalWrapper from "@/components/ModalWrapper"; +import { useModal } from "@/hooks/useModal"; +import { X } from "@phosphor-icons/react"; +import System from "@/models/system"; +import showToast from "@/utils/toast"; + +export default function WorkspaceLLM({ + llm, + availableLLMs, + settings, + checked, + onClick, +}) { + const { isOpen, openModal, closeModal } = useModal(); + const { name, value, logo, description } = llm; + + function handleProviderSelection() { + // Determine if provider needs additional setup because its minimum required keys are + // not yet set in settings. + const requiresAdditionalSetup = (llm.requiredConfig || []).some( + (key) => !settings[key] + ); + if (requiresAdditionalSetup) { + openModal(); + return; + } + onClick(value); + } + + return ( + <> +
+ +
+ {`${name} +
+
{name}
+
{description}
+
+
+
+ + + ); +} + +function SetupProvider({ + availableLLMs, + isOpen, + provider, + closeModal, + postSubmit, +}) { + if (!isOpen) return null; + const LLMOption = availableLLMs.find((llm) => llm.value === provider); + if (!LLMOption) return null; + + async function handleUpdate(e) { + e.preventDefault(); + e.stopPropagation(); + const data = {}; + const form = new FormData(e.target); + for (var [key, value] of form.entries()) data[key] = value; + const { error } = await System.updateSystem(data); + if (error) { + showToast(`Failed to save ${LLMOption.name} settings: ${error}`, "error"); + return; + } + + closeModal(); + postSubmit(); + return false; + } + + // Cannot do nested forms, it will cause all sorts of issues, so we portal this out + // to the parent container form so we don't have nested forms. + return createPortal( + +
+
+
+

+ Setup {LLMOption.name} +

+ +
+ + +
+

+ To use {LLMOption.name} as this workspace's LLM you need to set + it up first. +

+
{LLMOption.options({ credentialsOnly: true })}
+
+
+ + +
+ +
+
+
, + document.getElementById("workspace-agent-settings-container") + ); +} diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx new file mode 100644 index 000000000..f1b997470 --- /dev/null +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx @@ -0,0 +1,163 @@ +import React, { useEffect, useRef, useState } from "react"; +import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png"; +import AgentLLMItem from "./AgentLLMItem"; +import { AVAILABLE_LLM_PROVIDERS } from "@/pages/GeneralSettings/LLMPreference"; +import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react"; +import AgentModelSelection from "../AgentModelSelection"; + +const ENABLED_PROVIDERS = ["openai", "anthropic"]; + +const LLM_DEFAULT = { + name: "Please make a selection", + value: "none", + logo: AnythingLLMIcon, + options: () => , + description: "Agents will not work until a valid selection is made.", + requiredConfig: [], +}; + +const LLMS = [ + LLM_DEFAULT, + ...AVAILABLE_LLM_PROVIDERS.filter((llm) => + ENABLED_PROVIDERS.includes(llm.value) + ), +]; + +export default function AgentLLMSelection({ + settings, + workspace, + setHasChanges, +}) { + const [filteredLLMs, setFilteredLLMs] = useState([]); + const [selectedLLM, setSelectedLLM] = useState( + workspace?.agentProvider ?? "none" + ); + const [searchQuery, setSearchQuery] = useState(""); + const [searchMenuOpen, setSearchMenuOpen] = useState(false); + const searchInputRef = useRef(null); + + function updateLLMChoice(selection) { + setSearchQuery(""); + setSelectedLLM(selection); + setSearchMenuOpen(false); + setHasChanges(true); + } + + function handleXButton() { + if (searchQuery.length > 0) { + setSearchQuery(""); + if (searchInputRef.current) searchInputRef.current.value = ""; + } else { + setSearchMenuOpen(!searchMenuOpen); + } + } + + useEffect(() => { + const filtered = LLMS.filter((llm) => + llm.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + setFilteredLLMs(filtered); + }, [searchQuery, selectedLLM]); + + const selectedLLMObject = LLMS.find((llm) => llm.value === selectedLLM); + return ( +
+
+ +

+ The specific LLM provider & model that will be used for this + workspace's @agent agent. +

+
+ +
+ + {searchMenuOpen && ( +
setSearchMenuOpen(false)} + /> + )} + {searchMenuOpen ? ( +
+
+
+ + setSearchQuery(e.target.value)} + ref={searchInputRef} + onKeyDown={(e) => { + if (e.key === "Enter") e.preventDefault(); + }} + /> + +
+
+ {filteredLLMs.map((llm) => { + return ( + updateLLMChoice(llm.value)} + /> + ); + })} +
+
+
+ ) : ( + + )} +
+ {selectedLLM !== "none" && ( +
+ +
+ )} +
+ ); +} diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentModelSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentModelSelection/index.jsx new file mode 100644 index 000000000..60a6e940a --- /dev/null +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentModelSelection/index.jsx @@ -0,0 +1,112 @@ +import useGetProviderModels, { + DISABLED_PROVIDERS, +} from "@/hooks/useGetProvidersModels"; + +export default function AgentModelSelection({ + provider, + workspace, + setHasChanges, +}) { + const { defaultModels, customModels, loading } = + useGetProviderModels(provider); + if (DISABLED_PROVIDERS.includes(provider)) return null; + + if (loading) { + return ( +
+
+ +

+ The specific chat model that will be used for this workspace's + @agent agent. +

+
+ +
+ ); + } + + return ( +
+
+ +

+ The specific LLM model that will be used for this workspace's @agent + agent. +

+
+ + +
+ ); +} diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/GenericSkill/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/GenericSkill/index.jsx new file mode 100644 index 000000000..6b100bdd2 --- /dev/null +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/GenericSkill/index.jsx @@ -0,0 +1,39 @@ +import React from "react"; +export default function GenericSkill({ + title, + description, + skill, + toggleSkill, + enabled = false, + disabled = false, +}) { + return ( +
+
+
+ + +
+

+ {description} +

+
+
+ ); +} diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/SearchProviderItem/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/SearchProviderItem/index.jsx new file mode 100644 index 000000000..42e7b04be --- /dev/null +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/SearchProviderItem/index.jsx @@ -0,0 +1,27 @@ +export default function SearchProviderItem({ provider, checked, onClick }) { + const { name, value, logo, description } = provider; + return ( +
+ +
+ {`${name} +
+
{name}
+
{description}
+
+
+
+ ); +} diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/SearchProviderOptions/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/SearchProviderOptions/index.jsx new file mode 100644 index 000000000..15d18178f --- /dev/null +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/SearchProviderOptions/index.jsx @@ -0,0 +1,84 @@ +export function GoogleSearchOptions({ settings }) { + return ( + <> +

+ You can get a free search engine & API key{" "} + + from Google here. + +

+
+
+ + +
+
+ + +
+
+ + ); +} + +export function SerperDotDevOptions({ settings }) { + return ( + <> +

+ You can get a free API key{" "} + + from Serper.dev. + +

+
+
+ + +
+
+ + ); +} diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/icons/google.png b/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/icons/google.png new file mode 100644 index 0000000000000000000000000000000000000000..58a831856c3d832114f37307b6bfcea4273202ee GIT binary patch literal 18000 zcmdSAWmMe3mIsJC1b26b#+~5q1SbKSZrt622e(Fp1PhSh1b24`5G>HRgy8OBJ8x#* z%zLxDA9nZ4a!wz*t8QJdU)}rft{81iC2S0G3^+JAY!zjBT{t*+)xR$^6xc`r8!n>(3CNHDt=Hw>`aC3em#wW;W zFD4`;#K$izAi&EjA}Gu!%Ev1r!09Q*C%`KJ-~$LzN2W8t!6BX6=^J?(X{d_^|W(wrv1y>(#pllQ<8<{pH%;$k~Q$3VqCr49seQ38ps231pRf42M>T7 z@Gn|g1I0Y;JRL#*rnrow=f90O+Woae%+b=>Mv}#s%Nhi>^m6oMk$U6&kM-8H)*uf% z8)sTBS|t}38%K}?&tEMa?fwUwe~kPqXP*BP`TrpMALjl)^n{G3r@NiiKfU4X!EO1E z5wE{4NcseeXN;y))8d_7gHrG!L5U=T=z zm&=luSCmUY6ePeU3g!iIiSqLC3JHTnK-Rog|K#-_Oa2!pMJpI1K|Vn~Awf}q06h5SbX{aCBCpRF&QsUTNigJeLE*wIZJm-Co4-&iND1EHSur8{l8B|7;5}W9{y89 zUrX2jm4Pr~v43QXi@Uyyi=&jBrL(uC1kb+@|KBP3Ul{anG%z&!A1Tc9Pc)O793j7k zgL~brA}^!wn|s`8lWe`{wSV7!n?1p~f^y9D-q{zI5FVdaL06z?k(E`>&Z^N&;fU3n zF`)!KX!X15S7;10W|lEq} zDi|XXsVe9|6tw?;`Qhxb6At7HC_N?Vbm9(lBx_xoP7TVO4RVhe<`~m0l&vr$j>^YP zcR~Dtt!~s`KHAM$dCBP@*&h+Al1RXEA+JnZu z*Xzfd9j`eBF$J+gM+a)}%!dPoDfraR8{Zzz9tsdSvD{#D;#DATOdp^8@m1oJ0ZN6O0u-f<=O$;}v#uB=v2Cl+Ou6JT4 z5T*)*r3gY&3^6_kTJz__W?_fODe9Do*ju!py7^3QueA+3uWyU-sC7*QI2LThg!jnC zH3YtsH%PovJ@RcyJ2MjBb^V>?ap@qQzWF-RU76P!Fr1LT`?===h+QDOAhBQ~&ck6Y zza2H7_g1q$?DnW3O;?tcNMt*LMI8OrH~joKN)|n(c#OT;PqLytgw7stt7QHi4VxZ@ zliN`}vsqFIzfj%A7C&ZN?p-EgSt$dK7JiSR=UzLtZuo()T zSTseCCNLap;CQ1LS>H#9p5v6i3uTZxbwYAH=fa-jaGUt1i(DUT_WhZ4I#-x!#p6%X zWQ5B%{KS$(Wqr=C@faXvHnStDuH8KBg4;x-Rmvsj<(NsDQscwkB3#wQDv9?bz~Se~ zEL0PKl5TH>g#kX%7>3T8$8(orPqboC^{wPn2T?X;Ku6G>_u%@+#FrmTE=z8--hr*z zgMewV8Iy_FLTEU982kdv74}jzWSA|Zx1@E(o%xf`j>YIXp~H|rY#re}JsO3>H$4~C zQPb}x6(klk+ruiZXkenW`CVZd(_c*IQek6!M2~EN-KzXW9*00-Ipg&o;V2!TZ_woS z=TB4N#}J;$K?yvjOOtN4XBqQTDa#SDyQ3>EX@26rT>Ymvyk6d^1sYI5>Ahp2Qi-T0 z=3ul$yhmZo+OFL2_Ikci{TDL1ZCX3J)HO`rH&x9!mY zc-2HORUy7cqR(nbirG=Uca8~!zny>b3!>3jvo%m~_hKleh~%7#IaB7?_3*&c^48V3 zspKUuHlxF;o#F2YVccL*LiF}0cJL=kHh0rcF}wCOZOWEFV&Jvfpkj~`j>>>pvAP~F zn*PPBRkvKB6(It}8C@dSO=8Chn?}vf@vG1mZDN_W43Gix=KBykra*t@t$PusS$^MT zoj7+gfOSTXRuW2}HK%o)&83wak;?7`oA82b5yl^yiLGHRyDbr6@M*4t+KxL$-zKpp;^8nCPl6>ralB=K@g$mhGLpu-LaGgQ|%5C%P`#DSwco@m42P z-ypS`fjb!?nqb(*hM`M>WNxt5Y(ut|#8uWGh6YKLQvsScxY(Pyv*{FeV8u?{P#jYKCG}t5GaZ#T zEn*8QUhSTR(^6n7$ohR|#4_*Jq85*_3&JJ!Rf6stvN*sFMgyBg)}WHbC@zQO8NAII z?=c!JA72>0fv3aE56=lX_k#YS(q>@!2xPt`0p_SEqhj9sR3in|`= z9v&@1`fTG=u^lyIyRmAa_lP)*iCP+7cVG@W#qE^w1dLsU^6ZJ!r}-H2!zU|=aBx%l zADth{%ssEf(EnOvy`xWSh(06qn1(AK07*QlfoG9Wl!gx1KW8KiLut}M%S`-@6Ee*7nvQ7&|^2>p~ROPv2!RI)8f$)Zx(8f z(P(;!d{Wp6vYE%+g{9Kz;tyZ*NpF_bit(ySM$B7>;hy!0!9XzcRts^8Vb~iE2#S#l z_6xXZC`gcoh6`TTApauRNlEB-uYn)$o%2xc9R0;uEqml_G7;qE$fze;DSS0mK!GT# zs1j`#-V*K+I9#CV9gGh3&%Eu(bV8)0`H$+IF$~=g)_4Jd9cdTm6*yXo zc0M%Cx>N>v+WtL<4%FSe)V!9L;P&6{931k(b_`*pwx9c&f1-zoQMs|YT~Zh8N55f$ zpe!Am27~H{yr&s33%)9|bI6HOqx0rrgz%GG9vplio1wp8Z?KP;g-csFQzM|O!R&M< zq4jFbETG3)#D@QPgpmHMbbdh6Jid;3QLTX@@1I0gqN&+A;eF%9hxf;~t$)!J;H1M* z|GSdBu#)U^GR?NYSHQK@L58 z(;&w7JkW>B$7Dz8V9gomC}-fL)a=(3Li~+;R)_k>PVX;yAt(`V=pfXtE|dE$4V@}YRpL|=qpJb8b4qzQ7^=2j}C^qmTpB1a)x|=mfs=Mzw`D=7Zu~maMtj5&EyYhMA{vNiTjHxwf9Vj3Rk5 zq4aruJldmQCcc|Bn|Ki>aAPJS8ifb`igcgoQ5v>iCG>?pqR@SQ!&HBcI9TP#((&Ot zzQ`%8in0vZ@}<-C<94T1qvRT0oe&`T5jiu$;U-L#4L5APP%&Dd07oFsxv|?PVuk%t za=a=OE>s z;XT#;QymrkE~mSyL#R{McCIl#HtQW@BCz7j6LJx***9M2%IKEsk$q_PnKOR(bn#m? z^0eLuXZG*#d%BzYIT!knExd*W@}~QY&d}Ya@7p)ufzd8#3B5bnV75M9`b9H#gC=@p zSQRfN2;DS2j%c#q>EZh(i(bo8`}E{HQZ8sT>H3Zsx@2`wmO-E?x_RzB)}I&g9J8$t zD?&ThTeDCqA>3xay>vI?38oaYUDwv*mp6=WkTQf90^h#7T-q`wZ@T4V){-|r7)iR| z{wUi*lZ5}rmVN7hbv{oBaq2t9Zi!%^C#SXHnvezuGmpmf8wfp}MwqKmB&_Nb&H*GT zu?^!Z^`*B@J(qMW7>BkFz;@P?8i!??i!X3;-wC?_W|-JCK4CY@{ki)xiQz$)fzrmx zafEd4Z&xGnlvl`dY!K?R8b!plpJxrEkduy9RuBg($HZu`1gZF+R-(&J&9S&T;Mld< zsZ1Ge1DZ%KS`9Pcoy*WFq5rDsrEdVkucj3bw5D>mzyzSOYV}PnS^*64p3El5j-U7i zu=2D`HWqp=(iq^cbv5#4@$M&$lY#@&~^MBh(D{NnL6OYGos7%XfLo$&+d z7JWXRu_F)KLF42wQqZP{6CXbUUI`Fr?q3>34Tg1?Q5vI97Y>|{IS#-|QlF%|KwCx& zmkm5!!e)xt4lU8vD>KMN%z@CWrpAgvUOs(UwjNUoDQT&#%r=AyNpzAEev~DW(m*@z zytp|Qp6cz4d?n@MF1Z$HfW zO&4Rml4F9b1dUH8A;)L+NzX1>-|kGiFw#?MM%KX4x{Any4-cy7gBKe&6r+Fl*qWsz zj(+Nb)Da?J(Qb2&H6gP32i#X?BbNJv{-O}f8Zn!Ey!&Uwc8@vcJykLoeSB1wVhoxcdi8RFw{xeo|ULmgP`_9ne zcBQ~RXG_0#nRAB3#PP!j(}QdU*)hJ~()dhsFsoSZwjk19gWSf3r^Kr-v6S%3((nw? z<>(+VFy;rse{=-Mxp}p%N(ze;s6@L?p})(IyFYCGu&vS*pg4W}RcXZR#4gXK;R`87 zVF#?*I%!J+D15BCnA)0`sUtE~*CEMv;(t0)AMp9PtZpmWn2GXCX-I>}1v|8W*wTW; zd{!F5{hc|-DtNYWXN};#f@wv{cqno}Es;ViABqX9(?q)wW%JAU9U8B>FblZ zco$_AnxC={;n=v#)#;#%uuIDl*Jdi?vYZ-WpcDYQ`HfhTW2~OHR`$M&CVO+Qipr68 zshMi1+6C`r+}rlSB96MUA7rk2TX1Bm7tx&+b04?-!Gk@yjt})NiaeRGg7C;Dv}%0Ln_GW9uuiaOL088hHWXjg zA+p`Y(S5PnU{Oc_++{@Gqb?tDDyDu`tW=nu)D@ypw}*S~AX{=hRe_-!6) z03iZezzig<5|=uQBKs|Pux9utgf&J7C_9u zv)iHgcUxu?Bw&Ujc%$|rEDA&hNFh()*@@jkJabK68e8ku5S(JIxDA@LFI>Q-=-H)A z<*v!c*ctQm(V_f5di;CSV0!Qmn8sbaxUzOwN6?>}2>~X)O)DLNGwW=oi8pORn~yk~ z-J6We6JVTJi1|4NU21|Fk#A+%**aLP&BavbL3tNeT^Xmj{1hJ^5tfd(4KXo&oWH0z_z;vb=M)n=Yl9-o>aw=5sZ{MUl_dF= z4jSAAr+DTprghhg=)0&DnnmBcDr{(${M6WhH1%#u0d`#IsOjpkzlP~~8{xAqs#eU3 z-WIi=!%~|{Qr7)~`^UVDH}vq5xBh65AoMR@HIjX0s$eB5oQ1i>(U{~iiXz9H;nZZ% zar5-!D9&cUtIc&EtiC{QkHva3G7N`V^bW$ZzwM^_7dI7!3XF7ORlF7#m$U?j)WGSR zS&38R>22|_$3%jzQ(Q3pn3)?Qhfdbp;2SbLDM=irgNMvw?_uc9V;`Lb23Hxlo!suS$O=dvqt;6>X{mvDYy!yoz@{OvL%PI53`X z4cMi?IeLIa`PCNG10n1}liG;Aj?+a);xuAv$FGo03$RC*lPuU0VGTd0T4X zhi1Ds6vS{7h9X?VRqqn}v+7znKC9(zyCEcfBj|!A&hHvPVs%vt2}7}ASmGY!pzyBQ z^|alDwB*i3MCEQxE(@D$fatJYNZ8Du=p@%Ou{$dv%lnzz$B=MWS6`PE-g#vWJl-~u zYI<-n>|=NIzE*!OhNT;S(tG?4 z-(foCbsBKIb#)}!NiN>C9Lsa@5?iF7qKaOMR!O6^>}JkPKi){OZ_@IK?CgPfGXMv# z;~eR8#@@D@#wk41G>kMHv1>t9q)>y05M)sf(YA_kI<#u4H2(t?SsduYkuL`Hiocy4 z+D;sJzoSCUT9i3xklP=#y;5ZaOpE1vt*^?8sI}C{NiF!G8 z(E7&shbe|}&FHC*E{=nvt`179Dmq9K_he}~EYN3y_QUQIj(k|NO{~d`DD-Bl5%)W=(9#U?izS>y#CO)qhQ^BNEju`$~;mvIb zfiKpTZGCRb!cIG|sIZ>u0}z1e>3yd;kX%Hg(DigfQCHCUIhI7tbs(jM__En91>nV@ z4~kh{(dBI`K>%)2ejT;5Q%RY#d_xibg}pk~gXum(g;wULU>>*&U&g)@Y`g_5?`3r# z1K)Gzj!Kc|3W5v<)J2PUw)9tQH8FKY-HBxKByz3x1MgdqRK%X&s%EO9F}-FWbY7*j zbgG$OrY)cz+Oe{u)!dj#%K%%oExUr;?9;@T1E*^?78kDq*v2sE;809h3O4Uu**f?s08n@+i|aOp`p&ED-&EtrPNBaQ ziMjnzkjB^kVQ(?TK$5IxU zn2-I?NvS}_scztM6j@BF#WltaJ9QKt5WGdqPOWxsd(-IVxN6qNbs(YrJ4~`~meb#$ z7n%v@&!U+(iw@PEja4LjTSxttgh@&h_MeQMTD%Pqe60}Ser&7y>y@FfDV~VbE&mSUI9C!e@ec5MbV@CZn1FcV^1I zH?S6w+}_R=>eGgN9el;|(24YkUbfX+fZ|e&)5`9>w{^Ciar~QXn5PDlhuIF(p5J1? zGRKTRCmjVlM$-!G8$-X@iv5dCak z5DNMxWfsJ$U{UR4ry!?G`h}H4edb_QukZ+fkCUyP%H$zdrxT~EQw_g;Ks>~qpdB@S zhW~58z=4%$mz_<>jsXqP``{u(>zSNLGu|89s zYBJ#08U4@rvIoXwIYDbExBy0b4fZiXOSrGqKCf!PN!lki*OH8knDQ_d_nZ zF+kD1Vg`_4-BRO|blfq4=B}tx$Pg*rS=7!XtEx0_By&m7TV+MTh^riIIV3~8ehqa|Xth@d zyOyZ5!!nJX9x|DDJe5s`f~Eb6s<8(<;f`Z0Hr1w+$?iIU>2GMO^-ryEEbRLC ziFUu>DT~iNzeR?HOG5Y%`%V0;%AWr#(*g|zW42foJy@uwCjI3=I^>^-(STmU0$Vk# z95FjD%*Pc{HH^jhvVOr+6ggHNX(YNrvcsPR0r|q#okBz)WY($dZ$Vu(hpgoh+Ed>; zkA{+2V*%QikxqvMvRYAlL2P5HIwDqf0&|OAg!;0q)06)Ff< zQ@?z?TFYhyH4+rM<{105xy2ka#2O%9XIX6?Q_6|10E~}Gfj}vWgL#x-2Lb(do}!1c zNy23r{_h{Ljv4n&SP5^~5J+1<0j6OLev)?5OtAt z{y$m>i|J>b~*@(NFehF-?|)M?{^p~bn&dV6;HOETeGfZ z7I8|4#eJ0LS1KQCYJwdEJ)>*|2N}_TeoLK<J!0Ko!FML=#=0(!dp z7br1!7wbFx#pGAd4n}k8Ano4G<;X=UR0@9BSYaPRt}! zaCsbkWmb5cJT#TuLNGE}m=S1^Psnw@S}35|Q>0YZZy7DDg*0>M4f>lmD8N9hU@gvQ z6vVAx>YcNvkrTyyHdSzsT0gSvjO_{RgdHTxIk9L2`rrSoqLWxGxW=NSA7~gBxenI? zKBI&O32*|oi~=sTzAw2_^k21FpQ|Vr+llSJ(j{u9U}uEDv6M+@=DuoiwyLF2fhg#{ z&jiIwE4%E>P%r}yr|50IZtLo2mv|+5@H`>*Goq-;h;&6ZHV0=FKA9mEWdjF1${spX z{a^)%X$olltDy7xGWCeu>$aDx;9#?bi@F`=;4Dpx1(692ubr9YN^)~!B$-uW^hNZ@@`DGNn?x-qwB(=_r~P8J_cX9VDfksR#o` zT@dHpkT{opw(JRfeXsD&Qk(?Upd%usD~=cZo>#+U>*vIdB7MBZ5c*W6ax>M#s|I#R zjlyp%RLzLoyip}NtPk3%Zk+qM>B-U>KIZ`k8kD6eK63Dty+=3z({M|%p>(2>u0PC* z!ksv^;RVN>mT-7Vvtnuj*BHu;5;3aiOg-n9Qp3~8P$AG8o5J~kc(Lw+lg;hX1a+0u zAWyC-w^so+h+*))9;&6aGU1~X>*4E0udod5wB5)DJy<^MY3eklb-TlM>%#WND6Y~q z^wi1;K?F-{9I2ZKI<@}U)a{<8O(Zda?Y9V0z`ip04f_}fSY89|ADC*MgYJtn(r4%W z!ogG8e<~bPf6J-IR!C@34fNZa7dsxoiC0!M>e>`KYacxh)LPh7H)f0r-jCEICv99> zOnTpZacF=-01R3<36c&5Y8-qZ=Qf-D>}CIM&7{hNC8RV0|MkGx6^9pir74wUqCVZ> zJu!8GR`VrIs;S>pf=wN6`(EzuYa&P^&@%ygga zU*ku(-W1(P-|eyJ2P_=j!}b39e3ZXAJ9^XsUv$v_ZK^|gzEY+9lQzOclVsP6d-497 z!G;G%4c*{bef&iTbMu*Kb6Un|;IHwf_m@~LolZy%T$EqJ-x~XqAG(0u7n3Y2GY8wE z(Nj)%>-NrRXzd!u!Z6s`AaF^Y60$5pQ1<#@-9#mmjez=$brt9Bf>maG0CdTJh#u8F zA6__W;!B4MRv(@CT{=@g3PWZ0u?_kVp97zVUi5+QXv8cN)t=^`+V!|r#&n{w%%sR8 zX`ONT@+4Jb?$emsCyloB7u@wRJ!igv*TVO{_kWT{gIExkJ{~TuP}sj=C$ZurXkQz; zyLoIw-O{#WRKTwPE_KV7*YH^JJ4PGUF)50yU->@0ruxK_Miy71q4VOKpFCY6ey?>f z+0m!mG_@96^MtJar;D8UaSvcr%;s7ePe%!zcrcX?GyDh#yijuxjNCEF7Jmpz#mGP= zg7s|*SRPK}a?=TXB+E}-M1jweRsryz>K}Y$@z4DsLK) z)dn0c(NHZ?iGL;CzOQUQ-X~po7<$4XlS(PsuPdG5oU4&C%D#tY7R}1Wkit?lT#lt5 zggM9A@gc8pXgH1HzGjqKQ>U0Uy3-o(zESsQy#qqH&omlXtMhjV8A!C2y4du@5;`dz zdffn=hP+C)l;vA&_xM9Q3Dmj@5RB73nA3UR&_KN&6kHN8tZTdvG8BJYcGXtAOFEwn zWQM8`@YEzhcIz0&ciR01;2eTFZZ)lh^uLtV*B_DoPHSKCynT@sX(Eb^>X2hiHO5Ef zjmkEpp+8X9n`hLNDUHPMiP;o?9IB^I-;)h(Cnp{f*TYI`YkV>YM15iW=;|kNQ6D#w zi2!Wd)N0@u=n9f;*!UX7IUWSf>+bpWS-c%}P5b2-&q=&!nGs`{rEq#4)az@)iVYr4Rf~H>DTs4-fnw0X5LM^BH)IE+ zKiu1YW)|7zSR(P;pRWl;wN@35<`oWFIQ9%wH@m^~d{3eYF+2HF?bynSg-0TsE&7CO zCm=TubtZ)?jBu16xV;tj-Szw~9$bCaU@k&-UM_GqJ*x4f~hL`-t7Wbd+DdRlawIbN)=Mv!U)a^j~7^5B2i!*lKFVf?K$6l zAau5J?j*6Z0J4&&F+X1XL^0Kj($Elf+pA*qRqD>w0&PgB;eKu1!g@QewcR`+?#i9M zHIiUD&%*vinxB;(9gz_hH($P;-hIV@!Tq?vsomob4R=p|ZQ(ECt=Z4<*L)L1gesp* z`5=RJJ*)cDoN7+_fHS0GiX~VV%~yB)HhzEK)HqteZ?5#07~2GO@Q0}h3r05#6h&5K z+u^7hY0;_sR&`G9^d(~v$dQ1cRJPgl@a#tsx@+Wa!%b;}=C0ZIHnlYrgB?D)&7wHG z!9c2yC-)#`m%vv;m;mUXwSEm9@$9B)ey1Y4#- z<@owuh}Qec=FB}yZoMZzd6Kbmsv-JrbXp>v$~owBqT+VfB~H3Lcr6_HNiW=dcEvGY zyg*hyD9IIlLaUmaMx3w7S-BTYs*#d{bs(p*arKO;Y*9+9Cczx7;Vxq+seLKt zMN`ros-l`36h}$%8$qtv>i&p@*Rgb@%OiWj&nC>NZ>fL=;q6d4dF7n4?4K{!9gMcM zq#}j#SV>(XV_k-a%bHZ#r}miNd$<31@^`1LP7ydyhLNp=`&od$W*qHYVBehJbrhH( zIy}n+seMHRq7OCI?oM`({_Ldb^B0TFJ zhdlmbggtAUDHko#e*;kKv6-WKy0vD}LozD4dO@sYMTx&cdM6Ry8HA+QT=L>dElT}L zcf`iag7exdV9EW|g}3QCfpmrAoKFEbSI8pHI#)7zPx2@K8$Wda+KejPMrTjr>Iv;t zH}7no-S+!!-R;}d&OpA_e6}!&k0MC)#>vrd8d8}NrLsB&I||IBm-P@`(AKt-VcYRn z294#4>5Jh|6Ev<)hKIY$#O!1?VsM!ui7(*lG3f30dHz~i(fO;pt^Tx$Z=4mR`CbH1 z-tOWdY0l%ZBcUDJY;ts|Ol<%IIRl$kj)BJ!u_JSajaQ2SR1#Q2nAshFppGGrr@U+h`%rm>>JN!frE%b5XmaW)4Jlc%TmV8)x4Q?C9`lgMrw6sZ`KX_7g`x_&mfx_Sw z^fqrfb8QOuanq|reBbrebYH8!`FZJ(FB0&hQOKcI19%!0eMo5ceB`OyutJ%bE!Xx0 z$)?~DKh^u8qujw}BNG30;!sJ_)K;eh!nwL#!PV~~5r9H{eulcg?^ z*CqCT$FSN0XnlFOeAbr4*L19r{d$kSl^q$MFJ+>cy-i^6sNr3S*WP__jlBx&%R5H= zRRWEpJcg`EIw)D~*DGJaNH4nvZzr$|m5axFiDky;6VBY~n}PN(1e%V(XD1rV$^2WJ zAjZ1>b+rrk(e>c{)$!br`(QJ^y9Gw9IBkSnO_+IGoi6zNR>sFx*}{z_woP`-5)-CF zyzR2H(fi3Zro3g@=i@_g#Th@>4wv2gUHF>zoGVF))3@1EXAc`U#O88?ZsO$U!slGv z0Z~eDInX=Om6krpmRR(As64UAhWz+r{f>f;*XzYDGf6^pO1!N-J59-JV>5}x(>GjfYd^0SpPjo&>PiHtm`6BS zBvz{U&76i$kax_})T!0@hhEzPD1weAy(Xe)3C1!^p<#CK@#2=LwmLm1Pt6Ik^5{N0 zk6NWHdkN!c&e%)DWoqINnF0opT24M!ym9aGYGw=bz$**6GyeQa>U~Q~&A5quloqRA zas#@z64xP0zA%l^q~b1e)|$5%qIN@;6WvV{%3fOsg^n6+SKp?5cs%kDCs>Q>lxo}e zG=ISCU3kthmGqEj++%O@-M*b*{*ZEl$u|OqPU^JfM!$4Q&(wqhC+dJOg7p`I*&A3A z*^ZJ)6(apYoAu6;^isFgXXTM?hLr#K)Ej})z%;Sd(eA(tPD#9KlEWFtA<%oc=&ii1 zbD``HSQNSG0vuJ7BQX_@BiFCY6+KF$*oc24pHBAY{~<1HWe!|B#A-JpUYscj${MrJdnu=)fSz-G;7|YvgkoIxgvi2~9u{Hmr^EFi-YkRvlTr zkV%?}Mq%!p@DDshx{)Nff4l+|9tX2RwFmn!I)q3BT}xwlq=RrmXutDQ(l4K2cua>n2_cI73& zg}Q%DGL|waC7fzUJ3^eY_nvV@O$rSq0=4oSup)PKg4@6`Vec&G(d%42&V4=l6{DVE z9Kip;8sn3JZ6h`IYn5U|UOsBN{z%cf9%*W|jz#qGXq}e(_hnzktG|~Hu=*^6y^MH; z7tcfdotg<+n7ymtci=Sqx?j!TX-9<_0CUpE=S= z41AR4^Vq?24L`9r%h@DWg~PltC-6PZ`DOA-&ZDQA5DK8c#39Ozvl^zenVZ;>z-M5g z&+=AYkr@W56hcY~rYIAodnj{6ehyE3 zy;Hxm@30A97}++e>Z8aB{xR-cb6$VeQyz)_ivK`{!9`uCJ|fQXy8ieRo5Y<}bgYw` z|MNS%=;3A82NJX`4MKZ%MHD^GIN{9Gy)3_7@!Wtvr>~>BGuO{BnR+VwC3(Y*_km4O zBu2zzyK|QYY9!>NnAMmd^3kexY?;q0F|XO7*2yJdF-S3&kHArPxC)C{kEvAj5*Pn> zp`%|&0}C5mNON~C>OjPsu1{DL2miCNAgT0fiAP{k^!dNfb;R(hh7`knGGC_c5)PNjO{o4$I35mh*b?Qu{-esKqg(t*e;b*+Ov-u-F~_BE5iY-k&nb)un-0)gO$A#a*zJ$@v>!Xhi?~8g`pG zy6E;E)PePY%_B2t4ctABXYHlz_QvevQyuwR};NY^uPWezEo+AdYL+9IsQjF62Rl@6%`=knCjfQhh(sCtnz?Pm- z<~Ych*w5RItwD);cd7p+(su9Hmu0^OQg#R^2Pcs^tICo&L)Sauponsi zk1T zGFy`!csQrnJS0ZbH^P74Kg<&wO1q`|0aO7Ze|>l*l^A?0JtuZ4VPYf!yQUBaNQ2$L znIM)XQTu^yXb?p|%Fr3kg_AV%MOq8jg;486tip(_bofImQSu6q!%|?} z*qB-qiLpC~Yr(Fd$-Lxbxq!7m7{IRGtDjMTtdh?tBImJV_vDFf`7ObLJjry-8S$Hz z^z_W>ADbMO;MYPca>gGpb{zoe;N>$eI&hH)?`6L5Pd8;_XJSuU_a+sO9ku~)+i$lw zL+WNRMj7=hVc*5Bv6N_@LTvMcO@OI2p|I6-*5++V16kPc-~{P)s7{rB28G@L7d#M` zVCGBrYs%chwN~os1TUJ*pn!z%`X3bfW@D@8Ec`x?Kcu{xNn|430T^`>k#&m6N<6Aa`dMjG(!O8yLFr`Q1<}Qcdayqk>9yPT zODR4+$r$vLMwh;hyim0j+j-4G;oJyVp~&}ASxhdim*uow&hY0Kpm^l<{ZW*6xs{d)HGI*bPvVX?_OP!O5A;uaw*bnVX8Af_p#c`Waj2Gz;KeTPBhPXCqIPBQvI_V@; zPshzQgt|m_=PNekF5epF`abC~RyC9}`GLx{-(uI%)+m1WHon)kZ46X4Hg0qFoe3S8 zsqR)Mq%K@ojw#UGBXOm(%ikQ>F4&pi4+%Y-_(bow3tP#>HZnsxRhGpC|Km8YSsAul zow*l6StVrK|Io{P%cgy+B;b{`z5a+e)qO|AXm{(?8b0xx%&^_e*TQ@LY)v?{wM(-G z$LRJ02giqn5x(C1PE9k6`~ z_7}DgV+!&mzTI$Kz*aNHhw)AQ+#lqDB8m?T2}Ee;m|$5>nZNRGFXI>!q{l>>wa$B( ztI=TKX`<6na)aGbFtfLk$#>YCVmhmKJL1B&HG-a(bF%5S(Q!pftPe@@CQv4M6;JYg z0*vjJIM?1LSgwm@91}$Q*|%0!J*O8Iw`13N4vj4n-fm!>q=?#nmE|U}*WqSy>J17& z5v8_@jOp+lEgsrUiK3@vB*COWB7{l&fWPT*H4vD_nA={Y* zE(It7fKpbMoJr-N3Qk#3>TC?Yv@L3#P z-uuGR?z~4b-H~iI^%+*#xFrc&vd^1;;DFu6P-W=3C%iqLV6T-HN?g|KkN1aCqd>ox z{@(pvR7)R+__}AZbR>>ywHaK`)nKo1gFNYf`!Q;8)nK^Y2dja9q75(fr z^qsZy4F}$VT*F9%ldTYrO-o?2gW2KBdlSf;KK?9?V1ocZHN=HoDxnMR%AdACFd9C4E=#5k{R0Y44g~YNKm-2m04}w&ZZt{4BuGPF}D&Sh0!n1XW{o zd$MxcuCmN}UVE`UmN9kyEp>vMR%5MC5_ng>n3~o?Cikj-`%5;WjLr#zCR}JM;jJvi ztr3A=DIyHLy%lr{E7@gzZ)t| zrmhBBLjqlN5R{p2qF2x=$$Ko3-?($#eh3&sT$MK0r6xl{n1zW^6~H)a}~FVw#}Cz;V-+!cSYUh`VZHU5mh z(<*e*D&#H_W(Rc^!GhgLM7&3X^J`Lg(4}DZ;JDlqbHuQiu#)h{7K!~$Pc%7hBXe9l zDvX$n;k-aM4BROda(+iK#bO+$$Td<3yAx*#9x13E z)+4~VH5X@-K^{=0Izwog&q&NINE<`zggQ)M9fEr6P9d7tlRHlSrm|_;KD|&@bb|0p zCYpNbh`y@-Cq%)x-1$Ni!*}qf%dnV!``38OhA(gQdr5&!KP;152t{Y9@Qi;U+4-o# zM>qMfiUslYUb)y=L5<7z{%ox+zTp^_`yqt@aN>%^0^IS$a-&(SzM>*hEoJcz4Q(5Y zU#=`XYr<`3i14BY573A9-Z39eO!b*1kD4z|JPz@dx_7z`;NOiQ=fKhJ8tqwO>@s+yMnQS z*v70zPwT$(FuQQaHeUCjuGW^k9)-#Vf5$NuY7_b$JX)A?B%Vfn(+0;ZInj~{6;4I3 z_%SxUtmdygigbitZ2U>5{v1G?RI%yg^qxDxha2?~+TbeC-sqBK_lE+1bXZCp;#|K+)=L_DX61M1T&#A48 zfhJq`<^2C-6ReZ^(6-BH#f_u#i!{16?HUH#>mi2%wEUM(Iyd{Hd(;>3VJ)7nelF{r G5}E){M3AWf literal 0 HcmV?d00001 diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/icons/serper.png b/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/icons/serper.png new file mode 100644 index 0000000000000000000000000000000000000000..9659e1d6407b6672f22f7acda65363b594d5e2d5 GIT binary patch literal 31321 zcmdqIWmH_zvIayL%u=a7b_x2+{;6xI+jQJh($}hu{vg zx$nLE-o0;Te$33T>9u;%9NA}|+EulyzWOR$NkI|=jSvk10Rcl=N=yX-;gQV47v%{! z;zR%PG5CY(D5dR!fbhKI;rl3&@i`GVDBvotRW|Id$r zv#B|sikQTIUJCvbq_uQ)b>xFWJv=-h9_$bYXA3ARFE1~Yg$>Ha#tg1tcJZ=zHS%P( zccB6!{p(O-W-cbqR*tS#4)){^LmL@8xVZ|_(*EfufJY>n(K1Zh2)P0h@W+-zNGh1Bi; zala|KshNwFg*`blxvH77qnWb+^kJ5^R{vv|e;oPOk)i)5&;Q3||KoN4A4)>R)z#U` z_@C0ScYzrF^8!8aca| zsd+h?3DT;Vxj48vo0$DiHTcf~MV-xzT+K{{*jU&&m{~ZOS=j`j|90x1D=F}wC&WEn zrA>ucIXKLXIE~nu*-crvm^oNkxtMuajm?<3IZavE%z-+y@^JiftpB*>e~lzz3uw&Vfp7h|2X--Zc%e^GqHSlEusIO>3<*l@5}!$mib@B^iP% z?*F|i0(e*E|=UW(V6 z<74Q@u6@1!xzFYv4hQyBko{S|x(|_&v(0sNU!t}5JWJd|slsz!Inl%?(1m9|mcm*< ziau*;Z0Kw3KNm7hvU*32;>s1L|3$bU9G0_PQe4b^f7vv2O~6`ZlHAb=K5IzI`)7~p zz=wxKlTKexPtQJxF#!TT+--O=`1ttA>3`7Rk&%&o@{S=R4>!E7gKaG@FFU``%W`#c za;iryRcZjY&msT+;&%61%pY)8Y?#O}2k!XQFo)W}`$DUHQnJEzab>*V>@;B*d2kRG zBC-^Hd?+$KYi5eihGJ|aP?)%rECM@<3;RRupVOzmoBRcYgwvoOxUqijU}k%AsExnRwn7mX z#YTfwzvNXh-sDpuB=DZb>BND zFXtY!-p-q_0HI@yJwBuVn8QIV7P$x=T7+>#^7f&&@kTd3+UOj+n9G5NF^M2DrPECo z!NOaDfgRCPPbCh_!tDa0%Db#fe5>Z3iM{AR!m`SDwJ zbB#@+r-~!8{#l6YN4?k5?q2a7Cn)Gt$x_e(7063Ed^B8_S1w{eR7Nqk6x_(k1Cc-+ z2?D*n$lTaU}8WxJ_m)|H9 zaa!40YRVC;@;3LShr{p5;O!FFr-Xa zl)n-BlpUvIm713LM;2DJs01Cew__W>X~S*P^{7DEBEPNAF@aq=X5vCB~XH~ zJa0r0<9QBC{TtlpM6dtYFVEj1=L}dHkiiO=!gucTlUa>!4JV3~DeOHPPjd}&;R);4 zvR>`k4c=-$abZ>jtE18?=XU>m=nFf3$uJR zrNFWfG>OlWJOo3C*kYY~m+fiuYg-V3iE->GDQ!dUVU2P>F%3kq`d^`_;>UbSX_6?2 zYyVwBa#m1Osjvp~1RF$4Fe?C&~hB-U|X$r)OC`_BI{m!LQ z+&#|9g`KBZX>6t=dsY}hh8Y=oP?!H zT;vgX*w)rL)eijSF$Ow5wZnW>pFAzd3cGdj-UPmh*x^>@O6Kt=sPIpPAggh?Qxk+L zxO)X&4)^tAWYX5N4E)@rQ8`wfuy2s1T0%1uRjNb~l`lCV^GvbB6q3G9n!gYoF1(Po zPA5KXoGay6_#9y=VR3qd$Gf0oxr1fpTdd<&NwTzDbarw?-GrIRAGEb65-9A% zlEC>2;>8z;FRZ`^SU5P>FZrIJ`&jnJ1i zpA)L$D~xHU?89r6okE4BS_~u;*xRm4wBz++NYLoB5ZLAUOj6VcUQmCT zJknjXiYEY5gcQ-kz>HGa8w?TX zwbaRfO~CUPMV}uN>wpj#9D@3BDcV=Ad&ka0duP8tJ~WK<)hz8JtDNhSP*(3{dGoT2 z&fk5dTJ{LQ%`=Q&NCpbynDsTsR_+WBf%S)ywg#wUx!t-)F@79;o2XT)llGp2y03CY zPw)yYdGU%q@RTKJbCW_tV~#I z{)dxaEK&M+i`;R!GNh8lJKO~7n_fOD*)LDVEk2?QHYxI(c!I;hM>ApW<+n?UqpVd# zoeZiP4x9~?_2u<#VOB!q)jaKRZbRb{nNR)H>o!&&As#^C54d1o>Cgnhd>3CBX2S!B zme!whlnv?W$SMysx+hOXi_5dv^f|MyScI?M;3hBL(c>Fjvv&G+RYMNg>;-!pKVTs` zu=M|Fi*>q4UHn#H^HnD$TS!Xp@144j9v9bmR6%bS501DxDrYs0wKdJev}{#Lt+pSJ(dzlpif-#Nk{uWLlM==yw5ir?Bjs43p z%~yOkJaqnmjD~_(WxRTl*6`QRpka z5WOTh;}(_boHL!=)DYa5oczM+&f)FZ9B)6%H2m?Eaz0*$)2GPqC@}i`l<{qJ20T3O zytvR)ly0Tgj5AXEzVY6U9a#7814sWHXX!<1@hCrO$=(NE2&{dWM}6yq)kfFEyz^mgh|Oh&GIC=vwRVs^t|bg z{3p(o&P_pagay1QMAh#{QHhIHQZI*V1*2?KhIgbY)GxMUj5Er)$4)WdoSV}_+34%&&zC3!-F?J+@zYIB$3{9lO|&NS^X+bY9dXNMVEH!m$zQt z2_0i}^(Sm9ldP`uIO2XJ9(EyHv+A6x0(g%V$;Fy-k}bpPU*Q}OlTQqbvJiITM`&~M z+NjTkDCKhrx%@s?9@=Jec)Mx7yVirBX|mcugv(aY=;etK%b2he!t&)%^xW^GkMQ|y z)rmvNjp&zCO2;4y)kYMfYL8(Z3?jaI)1cj6oGmyn&S(x(w7${yk#t30WJ2WQKEC2( zN{z`V4@>Xy%wIbTs^d);Kb2>mdW^J~;Sa|cER!?`r_G>r=_&~dF+fJP2eR$^uW;Nv zK8+tH=jl=;8?5gR`Ty9;^viVhQzXOJ1x{6;1zIdf77+Th8G9#$Rq*F({fgWdVF_!iBt7R;U+Y)5ev!O9i-xa*e-NQyf8cxpb{)Sy zd+&4NJU2($Hp|NCc0E=+RgGe~`m1Z_4iAg@F={2qD$o5kCht{jfHb7qS2MN*c=Zv5 z;5X`=lqLGMK7Ucqe3v7|W*U@&Rg!}|Juscd2diihLbG!42U;cz>gkg#Xr=n2SpBwe z3qq`%8N<~(Er{C4G_<{KNXhP`qLhk5&a&pKF!rhE=0seE$QP852X-WttH-k>6E*5Q zj{Azo+K}U(bm}EBN*XTRKn>sepWwh%9?dl**DRsI?NAVhd2|o%seLjpaOCxGEe-Aq z(YNijDLHqrclfwPt$I5tQ#%C03$R}gl9(8sP;OmXIj2&4*9RLgQNo9(yt>^onLc}oWpI6%ry+7i)eu z^SBp2Q|dy)o^RB7`2PFP54pwK#xUg%HRmj@7b~_te?D=zVJ>4x^aoTLsQO%f!S62& zcoiPUnR)cL-0*OH@t(e?zDGT?d+p3I#CZTk;10n@=_OaqK4f1l&YQ(__)F3w z@J!X9X^xV~>qWOoIHHsF7c;S*8&3noot?~&=TB_A@Oo#vnoxquU!Msc6761BLtjL3 zre?kNpx^s@r4R_ClM?-=_KV%`68D?o9cko>W)BlII~gsyIoANcpXjSFG3~@~dyJ&e zKunl$G?YL*pFzC`*Zl7_NhVUo`uiu+^>G%f9Xn0O?^}(njtEEchYXzS@$|Bq zp;J-yMOup`!nkmkZb2Yh>9mF?D4exRn39K{x8tUWx!-@XAKVneWRP<7%-4^s7H*do zS-5>;V6WHw?`s1Y6KbR0oO(`OmVc|?C|Jy`4)9?f+=qNHg1X{A|&z`e7{=u#hJh8cGAMUNxFw>t8uQwu96(1Vt<4@OG z{kbE&bP?Ms3Q2YBJbLL=^mLvQM(27U)X_e4hdMC-o;*QEB&_kRv__IosDZiD4TFWj zrDV+gCcoTgzhoHS7Nq0=0fy4sDbUAmE_|@MOAXdEqB=$U&E(_avU6f>!4|6->lTC( zq*;3N(S8HL703?=VeWJ)gsY>Ba)x~3?p|4G@BSP<9gvYq^i+>LO_QC3b~fLU?}_^SmK9e) zA9d5#gpdU{rXFXZ2n3Q}XWU>s_r2mq<*XftQ02&heIzubchg=Cp3Ip2;?=ZcaXUx)?B=#k!-nVR)0)<=>Vy$A=^TDAtz%G%0Er(6q%jt}_l4PR7Ru<>EBBA`MOaSQ7%N}MefGhp7SrK7L=MU`^U`-6 z^0voCdqi562{J+ux`Ulmsy40*qYmapQrIm!R4OWPlD?ShXPG=5yY~EpDW@Z`%hBtN zZ#;)9tt&udMS)7-{dxjV7-ID~eHOl@3>(-3uI(M)q=K8m6#seQiu5BiPv1u~Pdt=+ zEwnR8MWJ|hey!0lj0x(2HO%7kOL(}o{G9$dxcJ)y}2=Wv-4yh9)n^+DmRW)*gh zcc!q|xwbRa%POV>`mnlWaUYzN(bn+x=oG)F7h>w|l(ZszS4Mojvi)UvNb1*R*fs|gSat;seiJr18&|!1Ai}C@4qKh0MqkmKPmBs-Oh9;W$j^mtnMBWp z?BO(Mxya(>hSZGJq>Lm57@`)yaX19r>+1R5ibvE}@r!WNOVoGt?($T&YJL1MQ1GEt0gq=nyH8>p$AG3V$s15% zg##go3=6y|u~uFy*H`mP(tvpv=inQ%^!F#SCq#J7E?7`ic!j8>2--i$;i>R3Mo_u= zC>gb8ciOZ#r;v(Nu6>;L=cU!Y#kDES`_THO<&Z|Z2<5I?eW^`UMVw2S=f>B$+bU8G1i& zB>J|r;mxps*sRY3I25Hn&9O(#6&|bdC$@w4BDHLEq}EQ~m^upwQEjKFiNh2)aM(Dz$GeJ=V5jQTif=OcK)+tn!>f;AkKG)|elQ|$>hk6Cp%e(f>VWSOtj~&1M{JS=J8;-P5h&4fj zHsu~|z);Af|LPUB{yxf92XX4!{RE_U<&J(>Lb_~(u)SIKfR@<=2*@#S4D zf!6_NC1ZAE-xo2M&=+b}ujr%<3d)i%MKeiL2g%9El=7CO&`%qSHk({H7&|$*Fsy@J zi-Q-2lvv>C(~=&F5A#_o$2e2vKKp>c2oL)L-o8F3%e$+g?pMa z@0p8?J~bz5FeJX~C)TUn+#lY!XLo7em`B8i#DI`hdO#k>ev5i$BHH;&wZ$ykTm?k* zP=g)GZWm|GxGkfcL`)RT?Edbl*MmjWRlg9=H(!CIBlZ>b5y&mBE?=k{Ttn|O!k?TE z&tVsTFOiY|eMNTct=G!c7A+;z{B!OvuW#G;SHi_~HCK#`Zmf?Hvlu|)`Yl10-D3~d z{y3XE=?hc;kE3SAn+Em2umUxO%7f<&1xa$U%mdE5VF3dtYpMih9>?oRbqXiJ*&psZHe!l2g1d)jtCk3MInWm1B)WVw&f@JrU z^7Fd3uV$q!J+2XK#btnMaGcwVql82>b_~*Zn0ez>&?_|G=mX5KK{xg0wc6)(H!Z_V4m;kHDqbp=k;f@OhXbIh#3oe^98%VF6wR_=+YoHcD zAz2?MO85jur*5;@*^jO$7wxzk;_w1g77kvCiInd>QLSbOc|*vTm8a3*L27DrlFhur z9}*!q45E{Ucn;TK;h9BU*D)9sg7nG-zr=8JI^3 zPM`VUeljxA`b8-+l@;d$XO@>AN(Td~E=_o%^}Q!2%~o+2X{r&(^9e z{S{Wgd8|GJk`#?zCDN725n+}#_()I5)-%3kto^oV>H5*2&>=3d`Q9g3G9u(SD-WK0K=A!I`T$pa zE(F(dnZe<<{Q+tmG}muIRi_!55x*D2JS;D}mp!|k(X=F+3Ku389j+=2bZ-8Onl)t* zmAjvBy@FO=sI0ffucnsxwkmx8OBjK`3G2^+J%Wb>iMu1@MmauI!P+R)QkPqP9;w=; zbEdOG2$MJq%WCiK)lbxs@Olgfb*^_xKL=w4J4?HT!n!w}roZnipELC?_*IU7mt=F$ z+?9vZ)4Qsv22d0bAFvZ&?K+fS9}bO%Z@(Qs;CY{OM3 zgT9&l@!b1-L6K!f%-Sgw_S))Fj7WGcU+q`CzIL|9ImJzU-IhkhnNTNT^71J&y>A-} zc;f1%2?3D+wuuQk91E;-{e5J>o2Evxi8kwk1ck+POIy1Mx0&8HSEK(KEt;l2pb>K9JzGUU(0txuX9G`QFraP=PV8KE*`eV_4 z?ENK7kAeU~<$3ccxazur6}3x8D~-V1%e%j({NM?Z4jrDl$u*T?`A+#zsr`4;bcd0f zS3+kDl9~4|U~41tM?F-=f_=TbslwP@{V_V^3}WB6JGY9!24Kfmg|b@_$1N>ii#}<) zy{INFDwLpGfNj+HJtOuFt_dVD#W@DB29XZK!1Ajc@+_N;N4T8}iCSjFvBDp^@&C*m zPI9}7mqa)2*qJRoXi@jZHyml>TJcY^%r0Be_}UrMQyv|$pzr_|+Lrf*=c&+bvG(R0 z<+zHM{Us=!HQzrrimcTY$=cBa>+(dRSSIlk(`obqm`~eS=ZggN$AsezOs1-OzQ^LB z`#zs*m>Y|h9NaG$idu}sv)~Sl09;6I_VsSJ&#;@ql?Wuqb;zst`AX}ouP0awZHye$9Ko&URNKf^?00!{!OO!q#f7c!LA-9lt5m4yrY>fPje(2}e*`R-KK zr>nF&jT3AOQJH>WCnL7+q4kd=3WqCq3`j3$T224{HUViL1AjNHU^b=5ldu*x|+|t>I}2@iB&1y)TOjs5_+!1VoJmt!aF${t0y_2XN`LQIDbb$k_u# z6AM${g?a%V71nxjEjHU&BvetY!+UyL7ymJp@tfCq=6$FhTlpE1I~~YabK&nn4Xy{t z@KSW9b{{xir39JZA86u;TtgQTCl6AJ(!xA95K7+j1Bg9N-(c=GcpZDIY{N0AfdtEoL=x;Ogntv3#2WEzxDfbIFp|{P|~k( zwC)mMZT^xYKUQW~;2DYk=dhNW?p?3t$l!DxWrld92^T`6a6iVm?@Kv!V!HfW1T&%( z>BeU6*+wHr44Z3|eX3MdCm7gv2c`AeZ{;E1w!IP%70H2@25}sH1QCn22?$DN|F}OF zKK22=s;GiYM7@QmtUd+T-oq+ zXUL*Qt8Ym`?!4Sl9g403HC4r}u<~{%>)OE#G9F@(M^UaOZy&H@l7Cjo!P>OM<;L}S zR2-%4Pn#qvcde$$#qQU;NL}K_pT7<#`$>i{&O-dDgTK1Hr9Brs220V&XxJ7f z3c;XFSW0?+bv7T{jsJb`XDOTk?L;MzQ8GZm*t>W_swGfKExM-di@;5KfLwP?^SE(c zVtydM+hIC>Rv^c-XYE?2=a@I~6UPi?tfr?PJ3;yQa>w0d5zY@zCnHw)JvV_t=6N%t zR7CwV{KX5@`ov%>=~*89V2waseaI)VV`?U2Y>|`$hL4CN`*HS6r6k7__yobNgF4Wp=?7KIn z_MKTfj+iK+R8EuugbJgX@CM2#C|Jx|q9ZoGPQ1QR?EiMQCUe^_X`~{GFVy{$ET|s3 zzp~=!#ZXs~D*I|lI#avL#eHg9=QZ(-lYtbIJXPQta`ois)IQ~%3kC909#@UvU+Nae z{sx2HraGESxn48~S*??v1*jV8J{KMoHM}(e5uf?o*J`UuQVG+N1&Vjd%w^_107&m) z`kpTRQIQs)C-J)4)S1%CkX4pv^R{JKbomhx*7 z8DHVyyF`8YM#%daoT=g}MvQr1D5?q}E;=ucDpkLp(7^Hq9>_pf*%=eiykzQ`;IQY- zYQBpY92B^?1Mk;QmoShIFWz6ku#NYcL6T?jCnGG}?IbH7?JIe3u`kRgax@JIG00Jt z_MvV7VC%RPLMyEDgxnYC{jmmYAvo8AnOlDofrULI^-c(P>F%HTO7!R94UpBwMXQ>h z8qy(79VmVRJGOrs0E39uAm_@;9GHIBq`7d9B5j(v!Ox!51n=@|$>ocbp^3kkpe+2p zNz;rLDMT%AKsCsiOTIp?`*&Ybt_ITp<{(=9$PYSv`h-Z_%B}6jAp^ng>U00jR84=7 zUTXl0P@IfG5}Wn@dxd14Ns25n-`SEhq<^mi1iauChm?6IA9MEy+Ec=0UpGH@o|7W` zDTP=B`f^~-e*%)UF5l%^Pgf`b5E};;IWIGls+xHM7(Gqeq@vAYO`&3Mi}xp-z+A0g z?A}*2fyyLEs@uoEY(-HwtNcz zpQSvFY!X4BJ9AhX7a(qAe_Uv?Lkbv8>VZ|V)HF){c|5mA=TgiAGpvOsSTG|{(Ko(+ zuQJ@|)ix`z(xlRp@Hb^Km`DtMpf?>*n2D8QQdPAPGxU4+`s3sv@Khm4u z&Bx@rWEa$hlQ!~4v*$Y#i>L4eg3!UE6M_fdsTq6sdF{452^Y^+ zr&~J)ldtxGmcXG}2ffT|%FPT3`Lk>h^A;(CpXTnX@f#qBE(J1*#-_SMKUp_RAu@i_ zOAf$>m|&~7nSRA7t3t?k+`i#;T6PCjxVbpcN_|zUaz*TY$7Ezk-1>88@iM^cKys0S zSJ><-XGdt!j`>m1vGP~1<|>D2ak_;$ti;e~`W66?T4kpnUFO>D87r9kp5FM>YHwB} zdeW7RXnA%Q6l@$-`PKHXxGtuGFW&*12elY3t^Lxkc8>UfQv&E}MY4U>^QOUx6glkf zS11mG=VYl(j>OGPPJFLq=z*g>d;s|O4(@0Hs;|MkSFIzH=rui88Io5j^3Cv^41*rf zf7txjNh)a~^jFZ1leRHJH)p)C23A~};+H+MPA=Y`EFQBgydfyefLBw&bM+Y5{IBQ$ znT#uX`&m;Uxh|%#LLK~t@*@8A%X$L@O>HvN_q}hQB;oXYgG_W_XS%=nY4fE7~ElruNlu#mrO74^ALP6Vy z@btd#|epHT2VC#|vQck392g|l4SfuJyN2|Cdt z_EBT-yAc5hUf2WnvIV=hadSI}qOd-2Y5KSAeXfjfP?3@Lwu(77xZqm(w#e$hKr|TP z8xZB3x+P2N3Bty-#LFIQh08sqopz*!x4{{L=i1RzpZ&t;qaqrOx0mUAJDa@rrp|6< z)dobnCvNEqE6)>2u9?w{xO}ZirUJRKI`&1!BfYwYNI6yW37-UJdxck>f4xLX+Sof2 z8}WX};re=r%w+Y#hWs=Xy9X__)aCSxq2txJ#cwG?rR5HCYkIOE2p)Lj!F9J^%i#)= z(0J{%Z#5X!PZ6{70jNqbGO@OaLDG#z^G=vJr`{s6de&~S4v`KIjVT&)mOCGb#Rt{v zL#;=SS5vLvmm;H8SeycrEK>wS8>N4ZGi7M8gqjl+8ZOyK4x~%DC;Rbn5zN1O_*$#^(OV;`iGSG*6m;Xr3yrrtdzB@jjgF}b{fC8Q%E6tdnZ+_VtbS%nXmU3Fn^L>mvZTKx21!?ELFfK z$`}%DG*2#cgfk~&`itg8RHj#VRR@uozjO~U4v?mfhomxVsgB`CU|a1KzO;8Dd@jHc zNQs-)_!LzGrf1vOld@qG$@a?OE4KbY!}5N5(J!&Hyw82Tf+q;(9da2jOQ2GFT8T>W+gB*f=Yca!y!G5A zLkVrv0pN0=u5UYQ#aVnIZM7+<&m$sKKj%nEj+GCHtI88fm7@ZSyhZZu>!NJJ?%@wq zGh^?$;u?d=31fAqxNRi%yAir%?K=02Ydur+M(bZYSjNJk_Wc#Ie0st7u?Hr$Te=-R;oesa17~ZEY zxiHDcC>@^8jt)-T9EN1mrTKNmf8lW!HaNZll_JE%CMDg9MiH31S^U_~;vAEj2??{E z#M#Oehw^RD9OF&1JP0vuHOs)DWTyGKNha;gf~sbkyoL$HWeX1=u6z~&cSz=?pRJmR z9P!xCeVlI4NOaFN{LR|)%_;WZR=qN0k*PlW^4>8|=?XE}n7KOAZ$Ka2RGe0tdGMqB zYeEf_lM3y9Ii1Ih_TRcF=>RLE@@D+Cgv@(D2Bh^rA|6dvsN8F#wOXto3g8J&hIYvIBQccYDl&khfYBpMuw+qr@{r8O{jQ<>8DoyULN z)_FWm3J@wlSdr~*?|{5xDn8Tj!R;i@;Ip09&NUSkP3Rc?`UtLz$MxHN2#z!#tE96( zVF0zgfu2pXM2Y`FT6dT*P9>9iyK-05F_+7`iIx(LH9ViL7aB`5dTebc@J}&UhyVu% z@d}&l@x)x?t+P@5x_iySIjV2g2dZ{Xz5VBizo=h=_pvbi!<$mdN@2A%Z~ZdeeTs8i zD$91C1a^91Yx7CF=Q|*l$7>r@S;&Q{{L1@9yxE#_u8CbucEkDFeL}37-oGF5S!IZ&D=lE zmKCh{<|YupaeC?tbR70(zfyiM_mhRrlHPEFWYkoDLd+yXI5{>H0ZRhtC$OW6juOUdR~oXe5W$ zo<%BaHKgckQzz?~v+MlC})ZrAGsds;%2Bl&n9P>6aPqdwJS4bRY?#bZcHhGn#vsko(di zwZ#(@x5g`OYZtY#Y^>m4tqmGHNR~eZ{B3pqvsH#2Rb9uwoQ~sSu&UZeur}*)IHp>% zMvn}(v`!-uCKAp*JarlZJJ_heTTu)dWpn}ZN)DUHY24ctjS}>kX1o2$6@BP7cU3SH zrQlbQ)^}lVi$flWj{;Y-_n{y&8^MvQz7x0c)x@_jY3Ll0J+8rPEgfJFdD}F-%e_iY zoS2$pNf-#q1TW3>#g7x~3)E>U3187;01{jui0XP5Xj@F-Ax^WY3ggk$4Zr0a$bX%M z1w=2*hk-JpiP0_^yQTuW+Y-;T>CXy|;^Js|$3bqgkE?UYV4JNmZv1I|i4#==Xgl<# zsx?tIPX|E_r*j7#!TfK5V4`&HtMh7>_gUQ5+sn}j&aV_&PV{GLcC#%eDX4qXm`-Hc zT(!@M0>M%wzEx!;nLR6CF;R?9Y3k~@JSPx#Nk~j}LQBQ_Ww6S4vB9smpA;w`=|h$K&A7jqeKPpG)<2Z-4RkOB%2Z!)Dr`qg zO?xG{Ad;*7;!vQXUt;P~tu>)Wv7=s86!12%gYca9kv-t0Ai0gJ zcr9a7!Lnn(C~o$cyof)!#W6VW7V`oAR5>06l&;_q$_py`I~FQ>rX}?rZsvGV~jpdv{R(F{YWD zYMfR%WY!K~O&qOwsUDl`sVRK8|LE$#>w}7rthraboN>n57Hg74w={Gx{&Ob47eA{A zS&WY$$4RYp)lJGQ#mEfECpQKsn74RTm%hVL56&$>A`J`4l zzUuulO1#r#9Obb+a_j_(EJypf+o%j@Hsc6pJ6Fb*jBNmYOI1~hQ zwHw}&S?(`d@2RD!R^sT`Llfh&XI6SgdW#Wge+!VR$&2Dp6WF|-7;Y>?DCKyTo26P# z+7|*Cdn-)x`c&y!v`n4$}Q1sA)>6ACDl`I9&?j;R_Zu~_Xd*@Z@yX*w!EDR z9o}tMh855iz9KYbUvtK`vK@qbJf_Exh-r<_QeQK0pLL1>^2ev z+>?2(PCJb7)xCHOw;q_DV%&aN@M0icaI{cgiyh16Q3g!;@QJ7=l8Eu1pcMVq;ypQN z6x%&QbeV*J&Jx^66HQR#1Px;)YdK4haR;PM7G}5E{W6%=OZY8gU-ch>EH(8}sI6`| zXQitj75;9sdX#fG0FD24xdiks4c$XPkn{&57#mHE`}1Qi?+81wF;cag%%T9pOAGc@ zDeNadcYr|xF0SQ1iE%eo=IcxYqy3r3>tRq9zqMzVYC*PaBKBrhs@IhcsHoGc+3H8* zkaCW8#6&kO?b64RB+=h*RDWtzc2(mnU|JE#efA0irsOVs#(O4oNJCjdGVVgdh`xl8 zQmfyZo2*ix{WXQ#MEwxk-kNr9Z17K(0c%0nhkYhc27i)? za4MwzIDjPxNK|pEkFM~g5NhW51o*>iMtWQ zpBySU+@jwr`bCqF`Jg$ls0_q>3@tC*zLbCqu`K9SN6yHPT@|N6-<%I3ahyLd-^-${ zznK^!5IlSW2=uw7jU_$@SPJRp&tJ%dCxqIKOEK{5ErBWe#X} zoZxj&P?E9AD@IM{$KcsX;D05}Ro_t_aA(t{&?0hh_tc7HN<{YjWt#Vsf$?uYWEgWP ziu=)unEGX!aBv1TzB!y5QOfV>`5%j$L%x_^ZF`~BS2Yqp6fRf!T+%y>kOg$5*mlDkd>~_x_Jw_d3U-4kcenm;58OwGN$$s4`inkro5FUkkveU-(eK9GK>H(F0d;r}_5&;J>Rk8@AQxHfEyi?{i7Df<6ccnb z2*yfa_j_x;SK|B$`mk=MMgcuzY4bRs?PA)axVUVES%^q&6*^2ZS&^ll@ZI-0EvkA7XVu7Qp zWDPm_A1u~zo?vZ6ctw=QHioam`6baHl8eb93AKc!qF)En-L1#Mpz`5=@?*1EQ0y;G;_ZB6{XQI0C za|@c2m2{s1ws*^0lvrWV%2^ztU?$N1Q98t1ME`b=U;GUt|2;WYgiQB@puk<@eUhs1 zEot(CXS}cXnmySvi6}~7_eQO~`W+}Aqz zCr2CphdvU!c|d)AC|c_3jWz35(>ac#nF3~=(Wy_k$7$tx4>bO2KNAofDsKHg2N!zD z-?x_Jq_HNzLC-nahpgn1&}~HeRg9JbT?s^$uXGGqd0e>a(`wDl+>i=yVfJ3L2=7o8 zh$z54vzsEY%s&8|?%2Tu3?(a~Iq5n$$-oNj$b>i`Rv;#=733G$9l}wtb${BM*906>h&<3DBMyI7wDDZpaiIqzqAo@Dwp+=LD;4^VO>%>O<2L z2kp?_;zhuUo1bP`n>$ved)Ji56}h})2odOoQ-ajK=aT+q7esd-FR;)HMJ(NNizM4( z$cV3I_x0RNxG;|4xK>HmjII$Vz952ru%{W`Nh%aWJ;@oOT$=;D3)2?%>r;4k_I^Y4 zV{GuC#+}33f{VCEGZ<*l*}Y_5i%wU)AA7s;}^0ydOzus<5W}-kGAkY zO-_&Z$_20mUF}=LvaW>`&SQK~Cg4T}Lo@5T%$)RC;pOKqhFF~T3DBwVCs^`Sv4hk3 zfdW(87~V?nG$w>nH&&S1Z}XvmfDdG<%sI&smIj&3FX3*P+{p{ii<~x$$;j5PDMLW zKji{(lFMN<*=XgpfxF-tZ<iofgqgfre)s$2BVW(FVz!$5Q9L?8)h6yT0>&_fYnnUGUG z|2ob&o)^tp6lq>DLO&|PW6zu>%>@Zg1MtStgd;r0E`E2aq7(Q{GgC%doTPO5g=KvnpEq z&m{dH)m`OV)NKF)0C4rxI;r5kCagau*gmR`D+Mx?tN-rGOp z{lFJ4_Tm?F&zy7S%5=GoXw3!th>5Ev+axE>H@&pg1;17i(L@C@s%wIYYMBlC_6^YX&W6Z#$Ef$do&brtsh<8qJ zT91pR4v=JM{jT}dqi+E(($)^HSP9<43brJhH5GN@K90NTMI#3u|XgKG}e4QOL_)+9wQLpjICTHii0>P{nXAUr9T3N{p>kz zXgAmG5F7=HumwoYa9S+-m(wwpF5na9g~WWj{5(o5{l5cJDXmxgaMAD((q*MfE>L>{ zSY?t}xK$#zG9I2`P4^7L#F8#uQK_QF$u)QC?>bUwB56Pe+;Plze@;Gl zIs$rCzz%SDkCcUTa4PhhzF%jHE^GK($1U|miA8>hzTFfIU>N>~(*u&PNcww|F|36_ zJ+$g4aIkM;YE=9tNf*>_=6-6hC<3VEmE0^g3*Vl@)z5AWHci)m>b`5{mR7!K+v5o+ zqi78;T*0=t$wBd7KN=l_<4+pfh0h|rVc!N6dd*092s7U&)H8szccT3R52jJ7@#Gm! zWDQWtR0ckCP(T$ALstKpnlj0}()Ke^4}**+l?-sdLTM17b-OzF;X%U4smsx{oWvi8 zim*_U2{Ags;oyZ|UREUvMH|SSuPeBx!i^CU1_P1!*kx}U7xBp)%sl*cSXb&yxww_+ z5Me43>97QRuIt&>7ydYL22F|4j9w|tPE+%m?#szeH)w$+z(ADwR9;{J*k=yK|DfSH z^BC*qLFrI(Wp}T-2ragjx-Yl#{>`a_RSJVG+YlOHd@_|}lmSu{-b(#in1-w+-V^(G zzhYf+snn>i-_9U^7C>3jyXlhE3jBHh6ThY_NT4D#V|Hn7$T9A*ES+H4UGUQX#64v% zf*ab!a~rf|+CwS762m@u-AIQ4r9%@f=lqIG=VF6I4TBi2tE6=y?E2dqyJ?@nV-` zGOi#A|1BOPGEH-iO^z=MiF9z=*hOCW%=Kh0R2z28RTW&&Or?o!%9FY%*?1W2Zx%Nry0y zL&qpE6zHbuNkPY+Q_Fn%SN!oM{U>%ew>PT1r4o2nT6|lAwjwgbdK}wm9W!uhPhgXuuQcY~e!KUm9COOI^7GqtrAl*l{2e+4B zTIofsF9eOfE$-u57NoUDYHfx58wyo7TXjk94H)rru0?6LH)uaeWqvk^b5fN;th}3< z3GQ5j@stk{$@B3zM5V%V;sg$))aYCVdPE-ZDWiIZx||t0jL%A%%C5fM<3sr-NiEq`xaVm(5P|dgL&=Yw&5kXn z9HdHxolbb1ig$4PNe@C;&Qhrg`W~mNh&J^3g^>sAzRQj6gf}K7*`WHz${a+ndc(1V zFgJ;X){2v!jd1tYaplqS*I)KRDekissu^}nXFG6+prTH{clg}y1b49$z7+cXHt%iw zZ0@_|%;DbA0uc-eu_EGwMwPKzw?2%51m5D$kw-10tXm0;zb_2ln)`z|vYqo+RlHzi zZdlwzGO!-tYRkM5U44Q!!CrNjV?M~ZVRg6yc|!gJZE}378GFTnL5d_iMwp#nsl`|V zWujtECcCixbbZEEPGZEhF!&nW$lR~>F@-UDa%W8%)St0wnPL<+$5Ckcj}R;QhO3+< zueEG7oK0heSviddU&pe3U-stvD=JJhWjS~L)rHJ=K4q?HirG3X9ll%G;*9CAk@b5E zM(&gpTqn;L*gxg@G4Jq zJ(n$tz-(agXe9M{QPdN{{Gk2U$^^GMQjbTPGyWITq-`qm?XNc$dyB~x+VE1%3Y^z{ z7J=<-;s@T%)P?y$xCU;X0shrwag0{ER)nPb|GmNHm#BnAImy2oJ0lS|Xl=iAH4gqY znsQTHP$hwb(qa&LC+{2e7F)=AUS_J7lV4%H*WbKFG!yBp2&o#6qYhX8)Es!jls+l2 zMwK9jG@Z@~3162*s?S9wJlEyzn{YUITTwre(4;^?%>~I|Za`0cDzl3tLusf+InfXu zjI|&pXqXxwDeHXom8-WqQG$-WihSb@a$_&eKd9Cp^M{t}jU#ni4Hhd2liwR8@@mC=jVdR(9{aBRoiL_~ z+tv;B!c7I}5Mv z!c5G(Lt#6Fy{2r`d*1`AfVrI}(O|Pf(nTOd7tW!GM`E?LDDG;kO}uzyPa`NGxm0|C zDN91&ale*6l(rUyNOyO9n7*fc+j!8vc80tNkVYx$k-mv;tlNlrz?x)ydu3JmJ0c%B z1mTpmxefN%PkJSWvCyz^K|%6jmA&ff32~!m)=Za%rjC2Gg6)Ez-WDMYM@h;F`F!VM zK4MxwY_jGiy0V}6F}xx`hX&EBT79NPoVUShGA=n=`?gJ8Qcc79<7|=WE`^u;7qT z+2~|jZ0Vvo>5Y~fo?BYK-|O-M;jLD7rsGECk^{Lih#`mVNix>Y`x<@rj2p@e6YUGb zchr-zdwA~I4a#K~G`uVwS4$5Wo1eF)+zFMRY1<1E%^koNBRp(>F=ZRuw1WZHHwT_S z25%+cH0g3qTbb&_4KBFF=C(g9=tVmI6H|`uj+*1XEqaJ;j`2(EpJk>0kK=(rzcLsc zmVpJo)8q+P8z>rOvCjJUS~NvEs|@;rq29G>ox4MKQ`$IOi^ZxC1!vTm33uEw7=-8~ zw6LR#Mr?ovqQHqBSZ@=Ydi2JG?R^F$hG(p;FauJ3Bj@UjdT$Ul8r_l+8#mKMR|JHp zoOM@~DR{~Y7lMVB)=P7gCpqX)#vSCv<@d3v=I@w5Luy0jQJK*Yrl$SsN+F0X%8i@a zzjIl7Kmq0L4;D)5N%F!+k1PhI{=b8G8@?CLai$uzn@(QnH9vdS*ut?0yoT`NTQ#Lu zX>ij?YJ9#dD=CR-+vnNUTAo1YOLmT3=iHu?KCOJV8hpd_ikP@=TwVK#MDD9R?gi4< zoHoB#Toy)?aLX{eHRk$#9P4zzwT?8{3Qfw|4vKY{#5v~wq%3GLlKm1WQ%K9n2V*f;V)DRu3Uj^DAz9=z zBSwpOdvl+q-b;2D@?6Y)XK_2ru-$=htWhSQIi)7BoS3b026noNyFXk>P^EQBa%LLU zZMYyam*LQD9*Rz%y1T~#vU}VMS$qW21Nx?d{ak2n4gBEc<$9cu(t~bvNr|Ip6G%U) z6XoV!HHl zE{U-)&7UM5U$JhmmB(vY>}%#6-vy00)Hl1l^-PJ)Qa){Zus+4ztM%riGArc1u0HtV zz`J#SIxhBfmQTV5R_Uuofl&GIiMb@5Z)SW#jwbOP$Tc=rk+B2jXcy?Y&!Q!+sMj(ZyhPj zaLsGB6%u`*9l7?RvDewyj98+6O$Ui z8_xROgwJzwkTglm>Mmsl(Xo{;Ucf`X%zb*NA}u@jjMt5XB3>>oTwGFwlFFQ-%be!l zT}6xc6)JDmGk8KmI#;?@j+b5PK|EfTS{MJY1Z(w{%b))#|Ix8^{KuiukvpUJ_Cw|M z{QT9_wHh98@|*LxpBqH{ei_pW-Lk4!lh**^sj)i#4GquA%w=X?WV>5pb z6XPI)(NNOcPspOve7kz(v5asyHG<1&4U}vSLxWvA%k$X3#O5<|{O6-d7 zwftN1WJzT?3UpKa!S~+=Oe92#EEcM!HwA6uThWEn>{7$iJTpkiIt7aQ3I~KLpNeU9 ze?Fi#>jt}mrc=N6?w(8|X!RH3^yg$+M}IZxk7HyHcnGQAYHG=6RC8a=8k4iL=Panm zR=ZGGYLqE$Q{|0k_^i^B++4Y*HaTVj?jH`8w6A@W7+aYdDOj*2^7#xeV6v}Y7C^=mmOUZoU zZJsz7X!RfS)9PQ*niX|RJCUAspjpjwI&4ZW6;`!SFNsPUsKeh;&TzQxJ)!kG<3HMx z(67|prJ}t$yj`KfRjn|I1-lZMiJPiO>DBNi@j2cktq9Ldw@h(7J}LBjK9b4 z%BN8L8OEGC+qWv6#|(x(fBzf4y_Ti0@F^QHq}%VJqE;G?Mda7J{b7`Qxc*P+$im~< zUEs?$%e_T;**USYS>MLo1t?m+U1Zk2(A{IU)wNXgN2VVVp0TYM(x6tPK2&Wy`EIO= z0r*3r!oW439(-ctjAT0cK5Mo0k`zFz{C8w%nsP7(*YG9G(qmA1M=wSjy-$xXBx(C; z2@h%V0lLnw@zQUh{Zd{&G3ZjE!qC52<7_(j>QTQ2UBS=zlrZMS>8}%*iA%NN1@(7} zl@GrBEp2Zrv^^y}7rheUEf3SDKBqsCY_QEnYaNDikK-g^DP3A#!HzzCfnh$OOZ#r& zGFIQ1!1e3`FD6^Q7kDjZAre{#DXF5knIzQn?)5Udub!E1=dpteueV8CXKXzBFW~A& zk*+T%IZE0H8i)J0KB`orx(52Vk%-s^Id=ZRk>4?B7y8}W#8fyXOg3J>ThF;a*Y! zqZ=oKJ3{MSOnz;Xd95E@=9$z34es{6>Taa{KAP$(FUs2j{XdI)>vQCI#U0yM!sxlG zeJRB$Xc$fx<*Umllk6^bzgL>7lZ>2`AvW!qcGxw0Z&EclT$i0=m1y6_uyD)|ceUXM zVA-muOs}8diYn@Ox?&dQxFl5TO0Yxk#vns(uPwWQdZ z(@5t)xqI|jn@c_WVeq9~)2V=QzY($WA~ehqN?X|Z%XThCNQ0{=yi*QPy~S|)XTxu) z<7H+V-1^>;lcKYEZ{a<{uQ}_k)B5lYn$zv2nL9Wj;#lt1auSsfi;bzVa4Wdx?QcxIDiVw0v^aTodu ztcL@HyD!06#Y+&1V`H(Z>~~vHtlfH5h~mRH1ur|gbV)I8y%g{85h(q`tH}XPmFt$2 zksky2IHnc1a;pR1S*WpD@Cip$q^^MZRM}6vsy>wXuhW}+XQV6l1Nla5-1DS5W|$li z1!cjouYVX_9o@eG3NX0&uDNJ&^|V5N`BM5p0`QSAdD#R*O z4OXt2vo$SCyP9#cCtrWMXkFUz9kWz|t^g7y8@+OlYO&a~6D<}sWG;6WO7o-4mP(MV zZ{{pw6FCW^zTpOeCgKwA@L+l^_tvm(gFV%R#Ltz{uENhQB#UQn6vG#>CPu*ZxHtLj zdh@rxW0qz#Vs+Om2i(W;t*agBU@O7hlcTLL)lsxHHJ9C{^gDUKPJXlVaFcc)a^hyU ztP9^uA;}>Tx@nY?tFqTQC?nOx>);_G>;YYszcTGAj@VD?2Vo0I?XwC0ku&_{?C{2Kmq2jB5Imqi zbFD^ZHoYVVjl7U;ZN-JA=ui6BoH>07Vw*{Dfk)1JlApPD>4HDJf`954IPzirP43I2;skBxKm$W+Gj}+cA5<{X_$> z0)YLm18$Lx`FFi9e=B|0J#DMlK#~tP$%|ci&2LxMU}qFZ zq1;bawM0>LpawbA&9j#+Sq+N7Tq6`6G%z0D_v74edv^cK;*vs6MQRX?IP)mMh$$$a zKGzpSw54LF5Y<6hc}+DeJJ6nQ)$6o33~<#wGSu?#*#4S{U5Ur=>Ah=pM4iZ>^?C&R zHOV$uWhfI4Fcv_gECRnJhAlnin-IvQ+l2TEe)*-oix!1p<&63QUliA z9xa~5A#OnA|AEiU<*Q)%HV%z3(c$?S%5c=`Bp zFi^`Y&eHV0gA=l8!;6^2_8WkGe|Q=j>tFTLyav^OePkp9;+;rNzmw*0^F(E00vHo| z-k|*{F9X{uvv-AD84pF$Qa-dl*f>1j9lpP3+fO9{@L;2o70*$w?-iB>f5F2Z z67TMJINe3DqUs(uPl_v0l((TIqTIQ@n*z%CipvV-)G{W@k&4Pn@sF;(^0u|3b<{X? z+0GT0HNMP^D$A<(oklVuMG}aiZy8Z>;d1feJ@=I{upiHz8OcdrGzi_Hptc;ALr1^- zdd-5QizBkPH!=B=2S&P-P4yn`Lo^bhmVX^(9k$QN6z4`oD`ANF0wmvg_6!rU{($+8 zc4>9&rXqLex`ba-kcaF9E-eX(ry(FE=VVE%ID<7scF24gQdeKoZruu2r3=TT*Sz%6 zi|F}?X2r)@b+tIWa-35Al_5C@K@p~T?|HPR&${U4IJy6$zt(F6x>-+RJPmPJDNpV# zjK9L{{+T_HkrjrPLCQ2o=96@NrhUA{8vY+0Zo7A&*0zK|$e`(HHSwv=)Fr&e6|#+4 zPb2E^IB*0o669I0bKq+}3?IQg83s7(Yng{bhha+)NZTc;C=MUg$`ziND5FDKAD(q4 z%MtsQP~%9SJbr|=(^$WQ6e?;c1}a45ntK_Ci5~xkFCBrH#p}iXcXMQ)(|&%6S$1ed zIFBzbqW(@i8a+fS(tluq^|8!NF^Q__PIw6xBZ*o2xXTxA9B$}sD&EzrvU*m+GA{?I zs$-d*ute1H3qIQJJahBgk3k+CvIQZzX3|^f%T*YZ8Rc8=jUg>}1FDT{wHjwobey+j zISr<7L50WUSng;^i*3@N7|D21@@CEgS(V=FAjD?OHSt0b_6zN!>nFYopR=Xvtt@dz z51&dZN^`{7;49DDYidF7Bfc*4e&-iMdzB^dMFONu_;|&A=+UCI*?!jDJ)?D7BCJGA z_0tDF(tC=meyb^y8C4j6iY5w}eG-$t&~wi391AsZOyDC$cmA_>z{18`N8|n5x+53m zqZOwVFKJ$l7nxj)y&P86-JHX!yPC>01$)#{i}YVLSe`ZvnnLj}YP?8qSb>%i`_)Q~ z7pc{wd&-?G=Lp`t(>)?NN_gwFK@=MGJJ;M-#ujpfSee?R8Ruu^423w$kBi1_<>1cD zqkgG2p027N=+tnOsiqH_V;aBTs0XG^tqnkfYEjzXC^MYtB;`bq9j!zx8M--T)KqC} z@j=5zl|Hb}M~tr)6uN#@#0t1Gl8k-Cr$f5U+knPd8y6}2X0)B7QdNoM=sFDWpYSoI9Lt9-Qc!P@U6qVMaQ_p+revXxO*;;7-%keqw-*CMNv6{EOz# zL}b+SN&g=!iwetIM@-p$&tHofg!3lxi2ckkUEW3g%UaA~F5B^X-cx&1$=q!BnSa`| zlJqjAijL<*UQhXh1e2i73JVi*x;J8R)odcP;!`5YF2DbbvZL1Q>F)X+@7VYqVt%Hl z-}oh2?bm24z|7EJoTC@ra_L93(_BZ>$lU&#Kq{XqGn57J8c0k`T9WG(347amY+2B( zy}vN9VK<1SaqUq}R7I!4$m3vJ93Dk#w}#-{tEyH5CCPD`?ICW;_9n{2i9Cr0P13O% zGi<^c@LVt{_@JKmoI4$M=AQV$!WD%DjdJopR!bD9GWJyE-aYNiP1kqu>$LH^o~f7BqIlZzfJ7}mtUUbbu3z1}-p-;ul~jOQg2-j# z`*KxkToW7!UhjMEw9^S#zrA<0kNa~y(39J5|~|88x}moXaV7Z zg(eDNb$D4%_fSD5!8%^z276&5smZLw@|s4F*0RH{#w-#9F#X&$g@pnuu3j{!rhLA z6|yc;Q&VsziSbAhlD>ZIRdE8c6gi7PpBDD~JO*`w-=pG<(8$wx*&>Vm*|2q&q0*Jw z2NVc_jP+F5_!bQab09CFA=)QKT#e0+;|hZ5lAiC(SiP9qYe<*(xmv0^`WXT^%wU@y zd5C+TlHd9jm8*dGLop@E)zK18I+1G9Uo!BRda}){MLIhHF9hPa-B2dM)oEq{Zz|@_ z>l+_gi_cG0C*0jAzW_X`G@P^HB*irF#5j4`rSV8LXmiE~+g<2_klk)RczmsYI5PzL zbd$RMSklOQ>WpNyIr(xKY+>tKPUO(+ypvRfUcvqen%y^^BY|R-F#P1Xh!GSei0#;c zY`qh7+35yI;ps``UPr0A#$!iB$jEJH&3>RDgH|x|^Yo`(?^B+0?|XfKnQb(_<>HiS zHlR2??Tlm3XHAx4aaeyU2o`g(cyHzRFX%pk8J?(}yenpi7fo$fvsfY6uHy0^!IN?mN__YN%qQ$7(_iCd!+^gJ~7}!w8gduP-kw$0Rcl# zfuLVDoxIYviL#{UqF?5!&_nBBU`XQ9&8uvNe)-Tr(|_tv*C5r|4%h1W^@+eELrZx# zMH5#deP+S>Cg1JZ#vhhT!ZgIRf^0!q+Kp#*1%cyxIkm>G{^3H91CKd-*Q#W&&f=s7$Lbv4K;X$d9!ZrZACt5v zFRapHzp86a%k0$NP5g3;lkAcdE9x^%Fo1A%kPIt~H~7EB7U;Y$YW>)A{`K>1qBFhq z@!W2~goI|(1szrVMU!>@qu)ie6K*%03Q&)9qS3?0^iV97NjQ<m1 z`@Eh|&08Q{H@pcMkoEQq(1DbqIHx+)37}ZL*_!bP?ex1=w`tn>NFPvxhF~#oId{_d zrhtb`0&`iTA2zs;JC<;Vu;ycpH4lxsl9iy znyQc%@z>Q6Ggdbb%`j05Y^6ivs3_VQ=`36FVrmao3+#*~6swPXxqF4TM8bFxG2sjX z@u>0Oo=}YGJJ{;aTj(B&E7gb-Fk6eYdv&WmQ+aToD;Q9z^D#?X(T0&={V{ROE4G)x zOg>$QYt@mujctRMSh%B>Ra^OIPY3Qj)d^NVTfRUA;?ZsUYhcO9@i>8K!S@Rq>Yol8 zI*OH||=z@fR6&0w)DKmi;@4fx-owmW%4L zIUj7fis4@qgTKDvd;4_%?vgF0M7qRXs-%dpcXTat6x!+hJN2Lq`7Kgx#ML^Zg4rkA z)VDvy?&XdE4wUVfbgmJ2At)?f_?@8cpiNAG?;(i%v{fisueHo6rRZ$C(|&DYsZ<|?RaD)zGc6xqBb;hFoYio7hW#vUmr%fo`NWw? zkuJr`$01kufbn!A8t6j|TS&TknKT=kpoB3SLCS#+J=mf%IyZ?Ni5<3G#dn1PPp!s_ z(V3d~?QXUC`$sR5QHut4QQxCGs zog|qdx#0?L0kxCmT)3la5ylslvh<}7iYADIW~;0}WD*h-$IH?HN#7K>e}L}VeFJ%w zNkk)vPZKYd#->^p+6Y^h5}zn9dH>!uun-u5j?{w=)u$B~=0EE8&;Z?8X&Q!ZpH06s zx_G>1qQR`9 zh-8;p3hBPNn^lGgL&vU3O6no==n*;YsKnxItWv9o^`)cvN(POT8b_RB>|cilw7}Ot zY%FrBYGXko0%Sn|Z93J7JJwFXiHh`?IT7f(8oPq-xxQzeCbU$U54@>xxFvk(ZA+!3 zGFDV+GqY5L*2|!gABJpOF8O}T#P*AAI7@fnUbu>Ax%8k8W5+{;=&Z$5NEwV%lHyB-(LNSs5k%17MZ3CYRBOp=S?An( zow|9w3_UVAIAPIT6nK90dn$F;k~Pg z9BwAj%2;EC)79N{D(PF1VbcU5Ct?L?eMX9!f~zuWqtFe>&c&j5cH!e3OXu5w0^i5O z=J&M<5@Ek|Fwt$+c>U`c&KW)0ffJsiiwxtFa9e^ea zT8Q1X@t|WC*DZa<}stvEGlUv#c@x|p%!q(2u z`q&L6lX{8?qM8^fSo)>u)1y_U*n500fM)wjpE;0OdOaMTD#bpKo(>-h#!r>%N+oLQ z2bPq1Zof#t#`FKMBob;qq03?T2pU_UXV=!f}4avJIy=qRhY0IvBTH*|o#PSr(boGsZzbDmXnPu8~h06uWdiOHwn{-j)fAtt3RITWAY9STh0WQ{P z-4HBEpFL8aR(e^|qpYZDqB>;DSUaZMC>;w$HsHgO{&upB{m2Gg@RMe4)Zw=U4}++n znU26$tM!0&2qq}})ItGmaPU$A@&c_OcH2eClS17Ou>*)$j8^-OF z%x~Yi>9F7TtyKYT0Nhc%Q71hBk;%Cj1*-6i(XePwEPsNff$N)P$t!{WW5xH|TA&z= zG`N!O@nI`z&INX^K`nxYcCHL9uBg@E7DL5xED!4Q4C=$Cl46wqOktoIM%3u*Khs4N zW`QD*%&75)-m}>XWWYk^+PSp#wykr8mnB6&NBUumn6f$5e&`1{!S$BK= z{o-IuOsnR*^ZF3p&D#k6I-nC4e@+0+lwJuTaA1M!hX^!3dh>Cwwu1XF>fFHf5~Ln9I|+vhe>TSc$Te}IJ37)0tXiwYIzf` zti>q6{itD*;leM*k81|1vU2Cst`16>7%YNk`NXY*!Rlpn<_QY)c2UIhjvjY)&G;4& zLQ=m1Mc%EGRV)<$5wXzny-YUMC75F5XO8<*&V<0C(#&%{ z>HQ<`yic^Ob6?0S2CsMo0h!KoZT7*wlMjUvf4D}!}*r2PESQCZvW)=X2e~LON z)&FcG=kG|;Sl$h8b*(t$@MH#mvsxD)o%JxBj0&xWP%BMG#popIKq9g@o~3Mg5(uPI zzD%beO{L;t6ZJL=2a-*WLQ%j1>R3<@%O3| , + description: + "Web search will be disabled until a provider and keys are provided.", + }, + { + name: "Google Search Engine", + value: "google-search-engine", + logo: GoogleSearchIcon, + options: (settings) => , + description: + "Web search powered by a custom Google Search Engine. Free for 100 queries per day.", + }, + { + name: "Serper.dev", + value: "serper-dot-dev", + logo: SerperDotDevIcon, + options: (settings) => , + description: + "Serper.dev web-search. Free account with a 2,500 calls, but then paid.", + }, +]; + +export default function AgentWebSearchSelection({ + skill, + settings, + toggleSkill, + enabled = false, +}) { + const searchInputRef = useRef(null); + const [filteredResults, setFilteredResults] = useState([]); + const [selectedProvider, setSelectedProvider] = useState("none"); + const [searchQuery, setSearchQuery] = useState(""); + const [searchMenuOpen, setSearchMenuOpen] = useState(false); + + function updateChoice(selection) { + setSearchQuery(""); + setSelectedProvider(selection); + setSearchMenuOpen(false); + } + + function handleXButton() { + if (searchQuery.length > 0) { + setSearchQuery(""); + if (searchInputRef.current) searchInputRef.current.value = ""; + } else { + setSearchMenuOpen(!searchMenuOpen); + } + } + + useEffect(() => { + const filtered = SEARCH_PROVIDERS.filter((provider) => + provider.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + setFilteredResults(filtered); + }, [searchQuery, selectedProvider]); + + useEffect(() => { + setSelectedProvider(settings?.preferences?.agent_search_provider ?? "none"); + }, [settings?.preferences?.agent_search_provider]); + + const selectedSearchProviderObject = SEARCH_PROVIDERS.find( + (provider) => provider.value === selectedProvider + ); + + return ( +
+
+
+ + +
+

+ Enable your agent to search the web to answer your questions by + connecting to a web-search (SERP) provider. +
+ Web search during agent sessions will not work until this is set up. +

+
+ + ); +} diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/index.jsx new file mode 100644 index 000000000..562c40fc1 --- /dev/null +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/index.jsx @@ -0,0 +1,203 @@ +import System from "@/models/system"; +import Workspace from "@/models/workspace"; +import showToast from "@/utils/toast"; +import { castToType } from "@/utils/types"; +import { useEffect, useRef, useState } from "react"; +import AgentLLMSelection from "./AgentLLMSelection"; +import AgentWebSearchSelection from "./WebSearchSelection"; +import GenericSkill from "./GenericSkill"; +import Admin from "@/models/admin"; +import * as Skeleton from "react-loading-skeleton"; +import "react-loading-skeleton/dist/skeleton.css"; + +export default function WorkspaceAgentConfiguration({ workspace }) { + const [settings, setSettings] = useState({}); + const [hasChanges, setHasChanges] = useState(false); + const [saving, setSaving] = useState(false); + const [loading, setLoading] = useState(true); + const [agentSkills, setAgentSkills] = useState([]); + + const formEl = useRef(null); + useEffect(() => { + async function fetchSettings() { + const _settings = await System.keys(); + const _preferences = await Admin.systemPreferences(); + setSettings({ ..._settings, preferences: _preferences.settings } ?? {}); + setAgentSkills(_preferences.settings?.default_agent_skills ?? []); + setLoading(false); + } + fetchSettings(); + }, []); + + const handleUpdate = async (e) => { + setSaving(true); + e.preventDefault(); + const data = { + workspace: {}, + system: {}, + env: {}, + }; + + const form = new FormData(formEl.current); + for (var [key, value] of form.entries()) { + if (key.startsWith("system::")) { + const [_, label] = key.split("system::"); + data.system[label] = String(value); + continue; + } + + if (key.startsWith("env::")) { + const [_, label] = key.split("env::"); + data.env[label] = String(value); + continue; + } + + data.workspace[key] = castToType(key, value); + } + + const { workspace: updatedWorkspace, message } = await Workspace.update( + workspace.slug, + data.workspace + ); + await Admin.updateSystemPreferences(data.system); + await System.updateSystem(data.env); + + if (!!updatedWorkspace) { + showToast("Workspace updated!", "success", { clear: true }); + } else { + showToast(`Error: ${message}`, "error", { clear: true }); + } + + setSaving(false); + setHasChanges(false); + }; + + function toggleAgentSkill(skillName = "") { + setAgentSkills((prev) => { + return prev.includes(skillName) + ? prev.filter((name) => name !== skillName) + : [...prev, skillName]; + }); + } + + if (!workspace || loading) return ; + return ( +
+
setHasChanges(true)} + id="agent-settings-form" + className="w-1/2 flex flex-col gap-y-6" + > + + + {hasChanges && ( + + )} + +
+ ); + + function LoadingSkeleton() { + return ( +
+
+ +
+ +
+
+ ); + } + + function AvailableAgentSkills({ skills, settings, toggleAgentSkill }) { + return ( +
+
+
+ +
+

+ Improve the natural abilities of the default agent with these + pre-built skills. This set up applies to all workspaces. +

+
+ +
+ + + + + +
+
+ ); + } +} diff --git a/frontend/src/pages/WorkspaceSettings/index.jsx b/frontend/src/pages/WorkspaceSettings/index.jsx index d43530018..7c53dc40f 100644 --- a/frontend/src/pages/WorkspaceSettings/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/index.jsx @@ -9,6 +9,7 @@ import { ArrowUUpLeft, ChatText, Database, + Robot, User, Wrench, } from "@phosphor-icons/react"; @@ -19,6 +20,7 @@ import GeneralAppearance from "./GeneralAppearance"; import ChatSettings from "./ChatSettings"; import VectorDatabase from "./VectorDatabase"; import Members from "./Members"; +import WorkspaceAgentConfiguration from "./AgentConfig"; import useUser from "@/hooks/useUser"; const TABS = { @@ -26,6 +28,7 @@ const TABS = { "chat-settings": ChatSettings, "vector-database": VectorDatabase, members: Members, + "agent-config": WorkspaceAgentConfiguration, }; export default function WorkspaceSettings() { @@ -102,6 +105,11 @@ function ShowWorkspaceChat() { to={paths.workspace.settings.members(slug)} visible={["admin", "manager"].includes(user?.role)} /> + } + to={paths.workspace.settings.agentConfig(slug)} + />
diff --git a/frontend/src/utils/chat/agent.js b/frontend/src/utils/chat/agent.js new file mode 100644 index 000000000..9488360cc --- /dev/null +++ b/frontend/src/utils/chat/agent.js @@ -0,0 +1,103 @@ +import { v4 } from "uuid"; +import { safeJsonParse } from "../request"; +import { saveAs } from "file-saver"; +import { API_BASE } from "../constants"; +import { useEffect, useState } from "react"; + +export const AGENT_SESSION_START = "agentSessionStart"; +export const AGENT_SESSION_END = "agentSessionEnd"; +const handledEvents = [ + "statusResponse", + "fileDownload", + "awaitingFeedback", + "wssFailure", +]; + +export function websocketURI() { + const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + if (API_BASE === "/api") return `${wsProtocol}//${window.location.host}`; + return `${wsProtocol}//${new URL(import.meta.env.VITE_API_BASE).host}`; +} + +export default function handleSocketResponse(event, setChatHistory) { + const data = safeJsonParse(event.data, null); + if (data === null) return; + + // No message type is defined then this is a generic message + // that we need to print to the user as a system response + if (!data.hasOwnProperty("type")) { + return setChatHistory((prev) => { + return [ + ...prev.filter((msg) => !!msg.content), + { + uuid: v4(), + content: data.content, + role: "assistant", + sources: [], + closed: true, + error: null, + animate: false, + pending: false, + }, + ]; + }); + } + + if (!handledEvents.includes(data.type) || !data.content) return; + + if (data.type === "fileDownload") { + saveAs(data.content.b64Content, data.content.filename ?? "unknown.txt"); + return; + } + + if (data.type === "wssFailure") { + return setChatHistory((prev) => { + return [ + ...prev.filter((msg) => !!msg.content), + { + uuid: v4(), + content: data.content, + role: "assistant", + sources: [], + closed: true, + error: data.content, + animate: false, + pending: false, + }, + ]; + }); + } + + return setChatHistory((prev) => { + return [ + ...prev.filter((msg) => !!msg.content), + { + uuid: v4(), + type: data.type, + content: data.content, + role: "assistant", + sources: [], + closed: true, + error: null, + animate: false, + pending: false, + }, + ]; + }); +} + +export function useIsAgentSessionActive() { + const [activeSession, setActiveSession] = useState(false); + useEffect(() => { + function listenForAgentSession() { + if (!window) return; + window.addEventListener(AGENT_SESSION_START, () => + setActiveSession(true) + ); + window.addEventListener(AGENT_SESSION_END, () => setActiveSession(false)); + } + listenForAgentSession(); + }, []); + + return activeSession; +} diff --git a/frontend/src/utils/chat/index.js b/frontend/src/utils/chat/index.js index 37237c9ec..90ed7d7e5 100644 --- a/frontend/src/utils/chat/index.js +++ b/frontend/src/utils/chat/index.js @@ -6,7 +6,8 @@ export default function handleChat( setLoadingResponse, setChatHistory, remHistory, - _chatHistory + _chatHistory, + setWebsocket ) { const { uuid, @@ -18,11 +19,12 @@ export default function handleChat( chatId = null, } = chatResult; - if (type === "abort") { + if (type === "abort" || type === "statusResponse") { setLoadingResponse(false); setChatHistory([ ...remHistory, { + type, uuid, content: textResponse, role: "assistant", @@ -34,6 +36,7 @@ export default function handleChat( }, ]); _chatHistory.push({ + type, uuid, content: textResponse, role: "assistant", @@ -99,6 +102,8 @@ export default function handleChat( }); } setChatHistory([..._chatHistory]); + } else if (type === "agentInitWebsocketConnection") { + setWebsocket(chatResult.websocketUUID); } else if (type === "finalizeResponseStream") { const chatIdx = _chatHistory.findIndex((chat) => chat.uuid === uuid); if (chatIdx !== -1) { diff --git a/frontend/src/utils/paths.js b/frontend/src/utils/paths.js index ffbf04c0c..339ecf439 100644 --- a/frontend/src/utils/paths.js +++ b/frontend/src/utils/paths.js @@ -68,6 +68,9 @@ export default { members: (slug) => { return `/workspace/${slug}/settings/members`; }, + agentConfig: (slug) => { + return `/workspace/${slug}/settings/agent-config`; + }, }, thread: (wsSlug, threadSlug) => { return `/workspace/${wsSlug}/t/${threadSlug}`; diff --git a/server/.env.example b/server/.env.example index 21887d09c..47cda159e 100644 --- a/server/.env.example +++ b/server/.env.example @@ -164,3 +164,16 @@ WHISPER_PROVIDER="local" #ENABLE_HTTPS="true" #HTTPS_CERT_PATH="sslcert/cert.pem" #HTTPS_KEY_PATH="sslcert/key.pem" + +########################################### +######## AGENT SERVICE KEYS ############### +########################################### + +#------ SEARCH ENGINES ------- +#============================= +#------ Google Search -------- https://programmablesearchengine.google.com/controlpanel/create +# AGENT_GSE_KEY= +# AGENT_GSE_CTX= + +#------ Serper.dev ----------- https://serper.dev/ +# AGENT_SERPER_DEV_KEY= \ No newline at end of file diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js index 4bf816a04..2ef611f6a 100644 --- a/server/endpoints/admin.js +++ b/server/endpoints/admin.js @@ -17,7 +17,7 @@ const { canModifyAdmin, validCanModify, } = require("../utils/helpers/admin"); -const { reqBody, userFromSession } = require("../utils/http"); +const { reqBody, userFromSession, safeJsonParse } = require("../utils/http"); const { strictMultiUserRoleValid, flexUserRoleValid, @@ -347,6 +347,15 @@ function adminEndpoints(app) { ?.value || null, max_embed_chunk_size: getEmbeddingEngineSelection()?.embeddingMaxChunkLength || 1000, + agent_search_provider: + (await SystemSettings.get({ label: "agent_search_provider" })) + ?.value || null, + default_agent_skills: + safeJsonParse( + (await SystemSettings.get({ label: "default_agent_skills" })) + ?.value, + [] + ) || [], }; response.status(200).json({ settings }); } catch (e) { diff --git a/server/endpoints/agentWebsocket.js b/server/endpoints/agentWebsocket.js new file mode 100644 index 000000000..9809c6073 --- /dev/null +++ b/server/endpoints/agentWebsocket.js @@ -0,0 +1,61 @@ +const { Telemetry } = require("../models/telemetry"); +const { + WorkspaceAgentInvocation, +} = require("../models/workspaceAgentInvocation"); +const { AgentHandler } = require("../utils/agents"); +const { + WEBSOCKET_BAIL_COMMANDS, +} = require("../utils/agents/aibitat/plugins/websocket"); +const { safeJsonParse } = require("../utils/http"); + +// Setup listener for incoming messages to relay to socket so it can be handled by agent plugin. +function relayToSocket(message) { + if (this.handleFeedback) return this?.handleFeedback?.(message); + this.checkBailCommand(message); +} + +function agentWebsocket(app) { + if (!app) return; + + app.ws("/agent-invocation/:uuid", async function (socket, request) { + try { + const agentHandler = await new AgentHandler({ + uuid: String(request.params.uuid), + }).init(); + + if (!agentHandler.invocation) { + socket.close(); + return; + } + + socket.on("message", relayToSocket); + socket.on("close", () => { + agentHandler.closeAlert(); + WorkspaceAgentInvocation.close(String(request.params.uuid)); + return; + }); + + socket.checkBailCommand = (data) => { + const content = safeJsonParse(data)?.feedback; + if (WEBSOCKET_BAIL_COMMANDS.includes(content)) { + agentHandler.log( + `User invoked bail command while processing. Closing session now.` + ); + agentHandler.aibitat.abort(); + socket.close(); + return; + } + }; + + await Telemetry.sendTelemetry("agent_chat_started"); + await agentHandler.createAIbitat({ socket }); + await agentHandler.startAgentCluster(); + } catch (e) { + console.error(e.message); + socket?.send(JSON.stringify({ type: "wssFailure", content: e.message })); + socket?.close(); + } + }); +} + +module.exports = { agentWebsocket }; diff --git a/server/index.js b/server/index.js index 5b63cafa2..158b80af8 100644 --- a/server/index.js +++ b/server/index.js @@ -21,6 +21,7 @@ const { extensionEndpoints } = require("./endpoints/extensions"); const { bootHTTP, bootSSL } = require("./utils/boot"); const { workspaceThreadEndpoints } = require("./endpoints/workspaceThreads"); const { documentEndpoints } = require("./endpoints/document"); +const { agentWebsocket } = require("./endpoints/agentWebsocket"); const app = express(); const apiRouter = express.Router(); const FILE_LIMIT = "3GB"; @@ -35,6 +36,7 @@ app.use( }) ); +require("express-ws")(app); app.use("/api", apiRouter); systemEndpoints(apiRouter); extensionEndpoints(apiRouter); @@ -46,6 +48,7 @@ inviteEndpoints(apiRouter); embedManagementEndpoints(apiRouter); utilEndpoints(apiRouter); documentEndpoints(apiRouter); +agentWebsocket(apiRouter); developerEndpoints(app, apiRouter); // Externally facing embedder endpoints diff --git a/server/models/documents.js b/server/models/documents.js index aa62ccf66..1c2d17113 100644 --- a/server/models/documents.js +++ b/server/models/documents.js @@ -202,6 +202,15 @@ const Document = { return { document: null, message: error.message }; } }, + content: async function (docId) { + if (!docId) throw new Error("No workspace docId provided!"); + const document = await this.get({ docId: String(docId) }); + if (!document) throw new Error(`Could not find a document by id ${docId}`); + + const { fileData } = require("../utils/files"); + const data = await fileData(document.docpath); + return { title: data.title, content: data.pageContent }; + }, }; module.exports = { Document }; diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index 604e43073..a6a7e50f0 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -22,6 +22,8 @@ const SystemSettings = { "support_email", "text_splitter_chunk_size", "text_splitter_chunk_overlap", + "agent_search_provider", + "default_agent_skills", ], validations: { footer_data: (updates) => { @@ -61,6 +63,28 @@ const SystemSettings = { return 20; } }, + agent_search_provider: (update) => { + try { + if (!["google-search-engine", "serper-dot-dev"].includes(update)) + throw new Error("Invalid SERP provider."); + return String(update); + } catch (e) { + console.error( + `Failed to run validation function on agent_search_provider`, + e.message + ); + return null; + } + }, + default_agent_skills: (updates) => { + try { + const skills = updates.split(",").filter((skill) => !!skill); + return JSON.stringify(skills); + } catch (e) { + console.error(`Could not validate agent skills.`); + return JSON.stringify([]); + } + }, }, currentSettings: async function () { const llmProvider = process.env.LLM_PROVIDER; @@ -104,6 +128,13 @@ const SystemSettings = { // - then it can be shared. // -------------------------------------------------------- WhisperProvider: process.env.WHISPER_PROVIDER || "local", + + // -------------------------------------------------------- + // Agent Settings & Configs + // -------------------------------------------------------- + AgentGoogleSearchEngineId: process.env.AGENT_GSE_CTX || null, + AgentGoogleSearchEngineKey: process.env.AGENT_GSE_KEY || null, + AgentSerperApiKey: process.env.AGENT_SERPER_DEV_KEY || null, }; }, diff --git a/server/models/telemetry.js b/server/models/telemetry.js index ac82d56f4..011da05e9 100644 --- a/server/models/telemetry.js +++ b/server/models/telemetry.js @@ -37,18 +37,25 @@ const Telemetry = { sendTelemetry: async function ( event, eventProperties = {}, - subUserId = null + subUserId = null, + silent = false ) { try { const { client, distinctId: systemId } = await this.connect(); if (!client) return; const distinctId = !!subUserId ? `${systemId}::${subUserId}` : systemId; const properties = { ...eventProperties, runtime: this.runtime() }; - console.log(`\x1b[32m[TELEMETRY SENT]\x1b[0m`, { - event, - distinctId, - properties, - }); + + // Silence some events to keep logs from being too messy in production + // eg: Tool calls from agents spamming the logs. + if (!silent) { + console.log(`\x1b[32m[TELEMETRY SENT]\x1b[0m`, { + event, + distinctId, + properties, + }); + } + client.capture({ event, distinctId, diff --git a/server/models/workspace.js b/server/models/workspace.js index b905c199c..28aebdf26 100644 --- a/server/models/workspace.js +++ b/server/models/workspace.js @@ -24,6 +24,8 @@ const Workspace = { "topN", "chatMode", "pfpFilename", + "agentProvider", + "agentModel", ], new: async function (name = null, creatorId = null) { diff --git a/server/models/workspaceAgentInvocation.js b/server/models/workspaceAgentInvocation.js new file mode 100644 index 000000000..376d62731 --- /dev/null +++ b/server/models/workspaceAgentInvocation.js @@ -0,0 +1,95 @@ +const prisma = require("../utils/prisma"); +const { v4: uuidv4 } = require("uuid"); + +const WorkspaceAgentInvocation = { + // returns array of strings with their @ handle. + parseAgents: function (promptString) { + return promptString.split(/\s+/).filter((v) => v.startsWith("@")); + }, + + close: async function (uuid) { + if (!uuid) return; + try { + await prisma.workspace_agent_invocations.update({ + where: { uuid: String(uuid) }, + data: { closed: true }, + }); + } catch {} + }, + + new: async function ({ prompt, workspace, user = null, thread = null }) { + try { + const invocation = await prisma.workspace_agent_invocations.create({ + data: { + uuid: uuidv4(), + workspace_id: workspace.id, + prompt: String(prompt), + user_id: user?.id, + thread_id: thread?.id, + }, + }); + + return { invocation, message: null }; + } catch (error) { + console.error(error.message); + return { invocation: null, message: error.message }; + } + }, + + get: async function (clause = {}) { + try { + const invocation = await prisma.workspace_agent_invocations.findFirst({ + where: clause, + }); + + return invocation || null; + } catch (error) { + console.error(error.message); + return null; + } + }, + + getWithWorkspace: async function (clause = {}) { + try { + const invocation = await prisma.workspace_agent_invocations.findFirst({ + where: clause, + include: { + workspace: true, + }, + }); + + return invocation || null; + } catch (error) { + console.error(error.message); + return null; + } + }, + + delete: async function (clause = {}) { + try { + await prisma.workspace_agent_invocations.delete({ + where: clause, + }); + return true; + } catch (error) { + console.error(error.message); + return false; + } + }, + + where: async function (clause = {}, limit = null, orderBy = null) { + try { + const results = await prisma.workspace_agent_invocations.findMany({ + where: clause, + ...(limit !== null ? { take: limit } : {}), + ...(orderBy !== null ? { orderBy } : {}), + }); + return results; + } catch (error) { + console.error(error.message); + return []; + } + }, +}; + +module.exports = { WorkspaceAgentInvocation }; diff --git a/server/package.json b/server/package.json index c5654c36a..a4a847509 100644 --- a/server/package.json +++ b/server/package.json @@ -33,11 +33,13 @@ "archiver": "^5.3.1", "bcrypt": "^5.1.0", "body-parser": "^1.20.2", + "chalk": "^4", "check-disk-space": "^3.4.0", "chromadb": "^1.5.2", "cors": "^2.8.5", "dotenv": "^16.0.3", "express": "^4.18.2", + "express-ws": "^5.0.2", "extract-zip": "^2.0.1", "graphql": "^16.7.1", "joi": "^17.11.0", @@ -48,9 +50,12 @@ "mime": "^3.0.0", "moment": "^2.29.4", "multer": "^1.4.5-lts.1", + "node-html-markdown": "^1.3.0", "node-llama-cpp": "^2.8.0", "openai": "^3.2.1", + "openai:latest": "npm:openai@latest", "pinecone-client": "^1.1.0", + "pluralize": "^8.0.0", "posthog-node": "^3.1.1", "prisma": "5.3.1", "slugify": "^1.6.6", @@ -64,6 +69,7 @@ "weaviate-ts-client": "^1.4.0" }, "devDependencies": { + "@inquirer/prompts": "^4.3.1", "eslint": "^8.50.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-ft-flow": "^3.0.0", @@ -78,4 +84,4 @@ "nodemon": "^2.0.22", "prettier": "^3.0.3" } -} \ No newline at end of file +} diff --git a/server/prisma/migrations/20240412183346_init/migration.sql b/server/prisma/migrations/20240412183346_init/migration.sql new file mode 100644 index 000000000..8014c9c3d --- /dev/null +++ b/server/prisma/migrations/20240412183346_init/migration.sql @@ -0,0 +1,24 @@ +-- AlterTable +ALTER TABLE "workspaces" ADD COLUMN "agentModel" TEXT; +ALTER TABLE "workspaces" ADD COLUMN "agentProvider" TEXT; + +-- CreateTable +CREATE TABLE "workspace_agent_invocations" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "uuid" TEXT NOT NULL, + "prompt" TEXT NOT NULL, + "closed" BOOLEAN NOT NULL DEFAULT false, + "user_id" INTEGER, + "thread_id" INTEGER, + "workspace_id" INTEGER NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "workspace_agent_invocations_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "workspace_agent_invocations_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "workspace_agent_invocations_uuid_key" ON "workspace_agent_invocations"("uuid"); + +-- CreateIndex +CREATE INDEX "workspace_agent_invocations_uuid_idx" ON "workspace_agent_invocations"("uuid"); diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 1e589b0f1..a07eb0058 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -56,19 +56,20 @@ model system_settings { } model users { - id Int @id @default(autoincrement()) - username String? @unique - password String - pfpFilename String? - role String @default("default") - suspended Int @default(0) - createdAt DateTime @default(now()) - lastUpdatedAt DateTime @default(now()) - workspace_chats workspace_chats[] - workspace_users workspace_users[] - embed_configs embed_configs[] - embed_chats embed_chats[] - threads workspace_threads[] + id Int @id @default(autoincrement()) + username String? @unique + password String + pfpFilename String? + role String @default("default") + suspended Int @default(0) + createdAt DateTime @default(now()) + lastUpdatedAt DateTime @default(now()) + workspace_chats workspace_chats[] + workspace_users workspace_users[] + embed_configs embed_configs[] + embed_chats embed_chats[] + threads workspace_threads[] + workspace_agent_invocations workspace_agent_invocations[] } model document_vectors { @@ -103,11 +104,14 @@ model workspaces { topN Int? @default(4) chatMode String? @default("chat") pfpFilename String? + agentProvider String? + agentModel String? workspace_users workspace_users[] documents workspace_documents[] workspace_suggested_messages workspace_suggested_messages[] embed_configs embed_configs[] threads workspace_threads[] + workspace_agent_invocations workspace_agent_invocations[] } model workspace_threads { @@ -151,6 +155,22 @@ model workspace_chats { users users? @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: Cascade) } +model workspace_agent_invocations { + id Int @id @default(autoincrement()) + uuid String @unique + prompt String // Contains agent invocation to parse + option additional text for seed. + closed Boolean @default(false) + user_id Int? + thread_id Int? // No relation to prevent whole table migration + workspace_id Int + createdAt DateTime @default(now()) + lastUpdatedAt DateTime @default(now()) + user users? @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: Cascade) + workspace workspaces @relation(fields: [workspace_id], references: [id], onDelete: Cascade, onUpdate: Cascade) + + @@index([uuid]) +} + model workspace_users { id Int @id @default(autoincrement()) user_id Int diff --git a/server/utils/agents/aibitat/error.js b/server/utils/agents/aibitat/error.js new file mode 100644 index 000000000..223f3351e --- /dev/null +++ b/server/utils/agents/aibitat/error.js @@ -0,0 +1,18 @@ +class AIbitatError extends Error {} + +class APIError extends AIbitatError { + constructor(message) { + super(message); + } +} + +/** + * The error when the AI provider returns an error that should be treated as something + * that should be retried. + */ +class RetryError extends APIError {} + +module.exports = { + APIError, + RetryError, +}; diff --git a/server/utils/agents/aibitat/example/.gitignore b/server/utils/agents/aibitat/example/.gitignore new file mode 100644 index 000000000..4b0412c79 --- /dev/null +++ b/server/utils/agents/aibitat/example/.gitignore @@ -0,0 +1 @@ +history/ \ No newline at end of file diff --git a/server/utils/agents/aibitat/example/beginner-chat.js b/server/utils/agents/aibitat/example/beginner-chat.js new file mode 100644 index 000000000..865a8c2a6 --- /dev/null +++ b/server/utils/agents/aibitat/example/beginner-chat.js @@ -0,0 +1,56 @@ +// You must execute this example from within the example folder. +const AIbitat = require("../index.js"); +const { cli } = require("../plugins/cli.js"); +const { NodeHtmlMarkdown } = require("node-html-markdown"); +require("dotenv").config({ path: `../../../../.env.development` }); + +const Agent = { + HUMAN: "🧑", + AI: "🤖", +}; + +const aibitat = new AIbitat({ + provider: "openai", + model: "gpt-3.5-turbo", +}) + .use(cli.plugin()) + .function({ + name: "aibitat-documentations", + description: "The documentation about aibitat AI project.", + parameters: { + type: "object", + properties: {}, + }, + handler: async () => { + return await fetch( + "https://raw.githubusercontent.com/wladiston/aibitat/main/README.md" + ) + .then((res) => res.text()) + .then((html) => NodeHtmlMarkdown.translate(html)) + .catch((e) => { + console.error(e.message); + return "FAILED TO FETCH"; + }); + }, + }) + .agent(Agent.HUMAN, { + interrupt: "ALWAYS", + role: "You are a human assistant.", + }) + .agent(Agent.AI, { + functions: ["aibitat-documentations"], + }); + +async function main() { + if (!process.env.OPEN_AI_KEY) + throw new Error( + "This example requires a valid OPEN_AI_KEY in the env.development file" + ); + await aibitat.start({ + from: Agent.HUMAN, + to: Agent.AI, + content: `Please, talk about the documentation of AIbitat.`, + }); +} + +main(); diff --git a/server/utils/agents/aibitat/example/blog-post-coding.js b/server/utils/agents/aibitat/example/blog-post-coding.js new file mode 100644 index 000000000..50f8d179a --- /dev/null +++ b/server/utils/agents/aibitat/example/blog-post-coding.js @@ -0,0 +1,55 @@ +const AIbitat = require("../index.js"); +const { + cli, + webBrowsing, + fileHistory, + webScraping, +} = require("../plugins/index.js"); +require("dotenv").config({ path: `../../../../.env.development` }); + +const aibitat = new AIbitat({ + model: "gpt-3.5-turbo", +}) + .use(cli.plugin()) + .use(fileHistory.plugin()) + .use(webBrowsing.plugin()) // Does not have introspect so will fail. + .use(webScraping.plugin()) + .agent("researcher", { + role: `You are a Researcher. Conduct thorough research to gather all necessary information about the topic + you are writing about. Collect data, facts, and statistics. Analyze competitor blogs for insights. + Provide accurate and up-to-date information that supports the blog post's content to @copywriter.`, + functions: ["web-browsing"], + }) + .agent("copywriter", { + role: `You are a Copywriter. Interpret the draft as general idea and write the full blog post using markdown, + ensuring it is tailored to the target audience's preferences, interests, and demographics. Apply genre-specific + writing techniques relevant to the author's genre. Add code examples when needed. Code must be written in + Typescript. Always mention references. Revisit and edit the post for clarity, coherence, and + correctness based on the feedback provided. Ask for feedbacks to the channel when you are done`, + }) + .agent("pm", { + role: `You are a Project Manager. Coordinate the project, ensure tasks are completed on time and within budget. + Communicate with team members and stakeholders.`, + interrupt: "ALWAYS", + }) + .channel("content-team", ["researcher", "copywriter", "pm"]); + +async function main() { + if (!process.env.OPEN_AI_KEY) + throw new Error( + "This example requires a valid OPEN_AI_KEY in the env.development file" + ); + await aibitat.start({ + from: "pm", + to: "content-team", + content: `We have got this draft of the new blog post, let us start working on it. + --- BEGIN DRAFT OF POST --- + + Maui is a beautiful island in the state of Hawaii and is world-reknown for its whale watching season. Here are 2 other additional things to do in Maui, HI: + + --- END DRAFT OF POST --- + `, + }); +} + +main(); diff --git a/server/utils/agents/aibitat/example/websocket/index.html b/server/utils/agents/aibitat/example/websocket/index.html new file mode 100644 index 000000000..2fbb56c93 --- /dev/null +++ b/server/utils/agents/aibitat/example/websocket/index.html @@ -0,0 +1,67 @@ + + + + + + + + +
+ + diff --git a/server/utils/agents/aibitat/example/websocket/websock-branding-collab.js b/server/utils/agents/aibitat/example/websocket/websock-branding-collab.js new file mode 100644 index 000000000..7229bd882 --- /dev/null +++ b/server/utils/agents/aibitat/example/websocket/websock-branding-collab.js @@ -0,0 +1,100 @@ +// You can only run this example from within the websocket/ directory. +// NODE_ENV=development node websock-branding-collab.js +// Scraping is enabled, but search requires AGENT_GSE_* keys. + +const express = require("express"); +const chalk = require("chalk"); +const AIbitat = require("../../index.js"); +const { + websocket, + webBrowsing, + webScraping, +} = require("../../plugins/index.js"); +const path = require("path"); +const port = 3000; +const app = express(); +require("express-ws")(app); +require("dotenv").config({ path: `../../../../../.env.development` }); + +// Debugging echo function if this is working for you. +// app.ws('/echo', function (ws, req) { +// ws.on('message', function (msg) { +// ws.send(msg); +// }); +// }); + +// Set up WSS sockets for listening. +app.ws("/ws", function (ws, _response) { + try { + ws.on("message", function (msg) { + if (ws?.handleFeedback) ws.handleFeedback(msg); + }); + + ws.on("close", function () { + console.log("Socket killed"); + return; + }); + + console.log("Socket online and waiting..."); + runAIbitat(ws).catch((error) => { + ws.send( + JSON.stringify({ + from: "AI", + to: "HUMAN", + content: error.message, + }) + ); + }); + } catch (error) {} +}); + +app.all("*", function (_, response) { + response.sendFile(path.join(__dirname, "index.html")); +}); + +app.listen(port, () => { + console.log(`Testing HTTP/WSS server listening at http://localhost:${port}`); +}); + +async function runAIbitat(socket) { + console.log(chalk.blue("Booting AIbitat class & starting agent(s)")); + + const aibitat = new AIbitat({ + provider: "openai", + model: "gpt-4", + }) + .use(websocket.plugin({ socket })) + .use(webBrowsing.plugin()) + .use(webScraping.plugin()) + .agent("creativeDirector", { + role: `You are a Creative Director. Your role is overseeing the entire branding project, ensuring + the client's brief is met, and maintaining consistency across all brand elements, developing the + brand strategy, guiding the visual and conceptual direction, and providing overall creative leadership.`, + }) + .agent("marketResearcher", { + role: `You do competitive market analysis via searching on the internet and learning about + comparative products and services. You can search by using keywords and phrases that you think will lead + to competitor research that can help find the unique angle and market of the idea.`, + functions: ["web-browsing"], + }) + .agent("PM", { + role: `You are the Project Coordinator. Your role is overseeing the project's progress, timeline, + and budget. Ensure effective communication and coordination among team members, client, and stakeholders. + Your tasks include planning and scheduling project milestones, tracking tasks, and managing any + risks or issues that arise.`, + interrupt: "ALWAYS", + }) + .channel("#branding", [ + "creativeDirector", + "marketResearcher", + "PM", + ]); + + await aibitat.start({ + from: "PM", + to: "#branding", + content: `I have an idea for a muslim focused meetup called Chai & Vibes. + I want to focus on professionals that are muslim and are in their 18-30 year old range who live in big cities. + Does anything like this exist? How can we differentiate?`, + }); +} diff --git a/server/utils/agents/aibitat/example/websocket/websock-multi-turn-chat.js b/server/utils/agents/aibitat/example/websocket/websock-multi-turn-chat.js new file mode 100644 index 000000000..3407ef32e --- /dev/null +++ b/server/utils/agents/aibitat/example/websocket/websock-multi-turn-chat.js @@ -0,0 +1,91 @@ +// You can only run this example from within the websocket/ directory. +// NODE_ENV=development node websock-multi-turn-chat.js +// Scraping is enabled, but search requires AGENT_GSE_* keys. + +const express = require("express"); +const chalk = require("chalk"); +const AIbitat = require("../../index.js"); +const { + websocket, + webBrowsing, + webScraping, +} = require("../../plugins/index.js"); +const path = require("path"); +const port = 3000; +const app = express(); +require("express-ws")(app); +require("dotenv").config({ path: `../../../../../.env.development` }); + +// Debugging echo function if this is working for you. +// app.ws('/echo', function (ws, req) { +// ws.on('message', function (msg) { +// ws.send(msg); +// }); +// }); + +// Set up WSS sockets for listening. +app.ws("/ws", function (ws, _response) { + try { + ws.on("message", function (msg) { + if (ws?.handleFeedback) ws.handleFeedback(msg); + }); + + ws.on("close", function () { + console.log("Socket killed"); + return; + }); + + console.log("Socket online and waiting..."); + runAIbitat(ws).catch((error) => { + ws.send( + JSON.stringify({ + from: Agent.AI, + to: Agent.HUMAN, + content: error.message, + }) + ); + }); + } catch (error) {} +}); + +app.all("*", function (_, response) { + response.sendFile(path.join(__dirname, "index.html")); +}); + +app.listen(port, () => { + console.log(`Testing HTTP/WSS server listening at http://localhost:${port}`); +}); + +const Agent = { + HUMAN: "🧑", + AI: "🤖", +}; + +async function runAIbitat(socket) { + if (!process.env.OPEN_AI_KEY) + throw new Error( + "This example requires a valid OPEN_AI_KEY in the env.development file" + ); + console.log(chalk.blue("Booting AIbitat class & starting agent(s)")); + const aibitat = new AIbitat({ + provider: "openai", + model: "gpt-3.5-turbo", + }) + .use(websocket.plugin({ socket })) + .use(webBrowsing.plugin()) + .use(webScraping.plugin()) + .agent(Agent.HUMAN, { + interrupt: "ALWAYS", + role: "You are a human assistant.", + }) + .agent(Agent.AI, { + role: "You are a helpful ai assistant who likes to chat with the user who an also browse the web for questions it does not know or have real-time access to.", + functions: ["web-browsing"], + }); + + await aibitat.start({ + from: Agent.HUMAN, + to: Agent.AI, + content: `How are you doing today?`, + }); +} diff --git a/server/utils/agents/aibitat/index.js b/server/utils/agents/aibitat/index.js new file mode 100644 index 000000000..852baa65b --- /dev/null +++ b/server/utils/agents/aibitat/index.js @@ -0,0 +1,747 @@ +const { EventEmitter } = require("events"); +const { APIError } = require("./error.js"); +const Providers = require("./providers/index.js"); +const { Telemetry } = require("../../../models/telemetry.js"); + +/** + * AIbitat is a class that manages the conversation between agents. + * It is designed to solve a task with LLM. + * + * Guiding the chat through a graph of agents. + */ +class AIbitat { + emitter = new EventEmitter(); + + defaultProvider = null; + defaultInterrupt; + maxRounds; + _chats; + + agents = new Map(); + channels = new Map(); + functions = new Map(); + + constructor(props = {}) { + const { + chats = [], + interrupt = "NEVER", + maxRounds = 100, + provider = "openai", + handlerProps = {}, // Inherited props we can spread so aibitat can access. + ...rest + } = props; + this._chats = chats; + this.defaultInterrupt = interrupt; + this.maxRounds = maxRounds; + this.handlerProps = handlerProps; + + this.defaultProvider = { + provider, + ...rest, + }; + } + + /** + * Get the chat history between agents and channels. + */ + get chats() { + return this._chats; + } + + /** + * Install a plugin. + */ + use(plugin) { + plugin.setup(this); + return this; + } + + /** + * Add a new agent to the AIbitat. + * + * @param name + * @param config + * @returns + */ + agent(name = "", config = {}) { + this.agents.set(name, config); + return this; + } + + /** + * Add a new channel to the AIbitat. + * + * @param name + * @param members + * @param config + * @returns + */ + channel(name = "", members = [""], config = {}) { + this.channels.set(name, { + members, + ...config, + }); + return this; + } + + /** + * Get the specific agent configuration. + * + * @param agent The name of the agent. + * @throws When the agent configuration is not found. + * @returns The agent configuration. + */ + getAgentConfig(agent = "") { + const config = this.agents.get(agent); + if (!config) { + throw new Error(`Agent configuration "${agent}" not found`); + } + return { + role: "You are a helpful AI assistant.", + // role: `You are a helpful AI assistant. + // Solve tasks using your coding and language skills. + // In the following cases, suggest typescript code (in a typescript coding block) or shell script (in a sh coding block) for the user to execute. + // 1. When you need to collect info, use the code to output the info you need, for example, browse or search the web, download/read a file, print the content of a webpage or a file, get the current date/time, check the operating system. After sufficient info is printed and the task is ready to be solved based on your language skill, you can solve the task by yourself. + // 2. When you need to perform some task with code, use the code to perform the task and output the result. Finish the task smartly. + // Solve the task step by step if you need to. If a plan is not provided, explain your plan first. Be clear which step uses code, and which step uses your language skill. + // When using code, you must indicate the script type in the code block. The user cannot provide any other feedback or perform any other action beyond executing the code you suggest. The user can't modify your code. So do not suggest incomplete code which requires users to modify. Don't use a code block if it's not intended to be executed by the user. + // If you want the user to save the code in a file before executing it, put # filename: inside the code block as the first line. Don't include multiple code blocks in one response. Do not ask users to copy and paste the result. Instead, use 'print' function for the output when relevant. Check the execution result returned by the user. + // If the result indicates there is an error, fix the error and output the code again. Suggest the full code instead of partial code or code changes. If the error can't be fixed or if the task is not solved even after the code is executed successfully, analyze the problem, revisit your assumption, collect additional info you need, and think of a different approach to try. + // When you find an answer, verify the answer carefully. Include verifiable evidence in your response if possible. + // Reply "TERMINATE" when everything is done.`, + ...config, + }; + } + + /** + * Get the specific channel configuration. + * + * @param channel The name of the channel. + * @throws When the channel configuration is not found. + * @returns The channel configuration. + */ + getChannelConfig(channel = "") { + const config = this.channels.get(channel); + if (!config) { + throw new Error(`Channel configuration "${channel}" not found`); + } + return { + maxRounds: 10, + role: "", + ...config, + }; + } + + /** + * Get the members of a group. + * @throws When the group is not defined as an array in the connections. + * @param node The name of the group. + * @returns The members of the group. + */ + getGroupMembers(node = "") { + const group = this.getChannelConfig(node); + return group.members; + } + + /** + * Triggered when a plugin, socket, or command is aborted. + * + * @param listener + * @returns + */ + onAbort(listener = () => null) { + this.emitter.on("abort", listener); + return this; + } + + /** + * Abort the running of any plugins that may still be pending (Langchain summarize) + */ + abort() { + this.emitter.emit("abort", null, this); + } + + /** + * Triggered when a chat is terminated. After this, the chat can't be continued. + * + * @param listener + * @returns + */ + onTerminate(listener = () => null) { + this.emitter.on("terminate", listener); + return this; + } + + /** + * Terminate the chat. After this, the chat can't be continued. + * + * @param node Last node to chat with + */ + terminate(node = "") { + this.emitter.emit("terminate", node, this); + } + + /** + * Triggered when a chat is interrupted by a node. + * + * @param listener + * @returns + */ + onInterrupt(listener = () => null) { + this.emitter.on("interrupt", listener); + return this; + } + + /** + * Interruption the chat. + * + * @param route The nodes that participated in the interruption. + * @returns + */ + interrupt(route) { + this._chats.push({ + ...route, + state: "interrupt", + }); + this.emitter.emit("interrupt", route, this); + } + + /** + * Triggered when a message is added to the chat history. + * This can either be the first message or a reply to a message. + * + * @param listener + * @returns + */ + onMessage(listener = (chat) => null) { + this.emitter.on("message", listener); + return this; + } + + /** + * Register a new successful message in the chat history. + * This will trigger the `onMessage` event. + * + * @param message + */ + newMessage(message) { + const chat = { + ...message, + state: "success", + }; + + this._chats.push(chat); + this.emitter.emit("message", chat, this); + } + + /** + * Triggered when an error occurs during the chat. + * + * @param listener + * @returns + */ + onError( + listener = ( + /** + * The error that occurred. + * + * Native errors are: + * - `APIError` + * - `AuthorizationError` + * - `UnknownError` + * - `RateLimitError` + * - `ServerError` + */ + error = null, + /** + * The message when the error occurred. + */ + {} + ) => null + ) { + this.emitter.on("replyError", listener); + return this; + } + + /** + * Register an error in the chat history. + * This will trigger the `onError` event. + * + * @param route + * @param error + */ + newError(route, error) { + const chat = { + ...route, + content: error instanceof Error ? error.message : String(error), + state: "error", + }; + this._chats.push(chat); + this.emitter.emit("replyError", error, chat); + } + + /** + * Triggered when a chat is interrupted by a node. + * + * @param listener + * @returns + */ + onStart(listener = (chat, aibitat) => null) { + this.emitter.on("start", listener); + return this; + } + + /** + * Start a new chat. + * + * @param message The message to start the chat. + */ + async start(message) { + // register the message in the chat history + this.newMessage(message); + this.emitter.emit("start", message, this); + + // ask the node to reply + await this.chat({ + to: message.from, + from: message.to, + }); + + return this; + } + + /** + * Recursively chat between two nodes. + * + * @param route + * @param keepAlive Whether to keep the chat alive. + */ + async chat(route, keepAlive = true) { + // check if the message is for a group + // if it is, select the next node to chat with from the group + // and then ask them to reply. + if (this.channels.get(route.from)) { + // select a node from the group + let nextNode; + try { + nextNode = await this.selectNext(route.from); + } catch (error) { + if (error instanceof APIError) { + return this.newError({ from: route.from, to: route.to }, error); + } + throw error; + } + + if (!nextNode) { + // TODO: should it throw an error or keep the chat alive when there is no node to chat with in the group? + // maybe it should wrap up the chat and reply to the original node + // For now, it will terminate the chat + this.terminate(route.from); + return; + } + + const nextChat = { + from: nextNode, + to: route.from, + }; + + if (this.shouldAgentInterrupt(nextNode)) { + this.interrupt(nextChat); + return; + } + + // get chats only from the group's nodes + const history = this.getHistory({ to: route.from }); + const group = this.getGroupMembers(route.from); + const rounds = history.filter((chat) => group.includes(chat.from)).length; + + const { maxRounds } = this.getChannelConfig(route.from); + if (rounds >= maxRounds) { + this.terminate(route.to); + return; + } + + await this.chat(nextChat); + return; + } + + // If it's a direct message, reply to the message + let reply = ""; + try { + reply = await this.reply(route); + } catch (error) { + if (error instanceof APIError) { + return this.newError({ from: route.from, to: route.to }, error); + } + throw error; + } + + if ( + reply === "TERMINATE" || + this.hasReachedMaximumRounds(route.from, route.to) + ) { + this.terminate(route.to); + return; + } + + const newChat = { to: route.from, from: route.to }; + + if ( + reply === "INTERRUPT" || + (this.agents.get(route.to) && this.shouldAgentInterrupt(route.to)) + ) { + this.interrupt(newChat); + return; + } + + if (keepAlive) { + // keep the chat alive by replying to the other node + await this.chat(newChat, true); + } + } + + /** + * Check if the agent should interrupt the chat based on its configuration. + * + * @param agent + * @returns {boolean} Whether the agent should interrupt the chat. + */ + shouldAgentInterrupt(agent = "") { + const config = this.getAgentConfig(agent); + return this.defaultInterrupt === "ALWAYS" || config.interrupt === "ALWAYS"; + } + + /** + * Select the next node to chat with from a group. The node will be selected based on the history of chats. + * It will select the node that has not reached the maximum number of rounds yet and has not chatted with the channel in the last round. + * If it could not determine the next node, it will return a random node. + * + * @param channel The name of the group. + * @returns The name of the node to chat with. + */ + async selectNext(channel = "") { + // get all members of the group + const nodes = this.getGroupMembers(channel); + const channelConfig = this.getChannelConfig(channel); + + // TODO: move this to when the group is created + // warn if the group is underpopulated + if (nodes.length < 3) { + console.warn( + `- Group (${channel}) is underpopulated with ${nodes.length} agents. Direct communication would be more efficient.` + ); + } + + // get the nodes that have not reached the maximum number of rounds + const availableNodes = nodes.filter( + (node) => !this.hasReachedMaximumRounds(channel, node) + ); + + // remove the last node that chatted with the channel, so it doesn't chat again + const lastChat = this._chats.filter((c) => c.to === channel).at(-1); + if (lastChat) { + const index = availableNodes.indexOf(lastChat.from); + if (index > -1) { + availableNodes.splice(index, 1); + } + } + + // TODO: what should it do when there is no node to chat with? + if (!availableNodes.length) return; + + // get the provider that will be used for the channel + // if the channel has a provider, use that otherwise + // use the GPT-4 because it has a better reasoning + const provider = this.getProviderForConfig({ + // @ts-expect-error + model: "gpt-4", + ...this.defaultProvider, + ...channelConfig, + }); + const history = this.getHistory({ to: channel }); + + // build the messages to send to the provider + const messages = [ + { + role: "system", + content: channelConfig.role, + }, + { + role: "user", + content: `You are in a role play game. The following roles are available: +${availableNodes + .map((node) => `@${node}: ${this.getAgentConfig(node).role}`) + .join("\n")}. + +Read the following conversation. + +CHAT HISTORY +${history.map((c) => `@${c.from}: ${c.content}`).join("\n")} + +Then select the next role from that is going to speak next. +Only return the role. +`, + }, + ]; + + // ask the provider to select the next node to chat with + // and remove the @ from the response + const { result } = await provider.complete(messages); + const name = result?.replace(/^@/g, ""); + if (this.agents.get(name)) { + return name; + } + + // if the name is not in the nodes, return a random node + return availableNodes[Math.floor(Math.random() * availableNodes.length)]; + } + + /** + * Check if the chat has reached the maximum number of rounds. + */ + hasReachedMaximumRounds(from = "", to = "") { + return this.getHistory({ from, to }).length >= this.maxRounds; + } + + /** + * Ask the for the AI provider to generate a reply to the chat. + * + * @param route.to The node that sent the chat. + * @param route.from The node that will reply to the chat. + */ + async reply(route) { + // get the provider for the node that will reply + const fromConfig = this.getAgentConfig(route.from); + + const chatHistory = + // if it is sending message to a group, send the group chat history to the provider + // otherwise, send the chat history between the two nodes + this.channels.get(route.to) + ? [ + { + role: "user", + content: `You are in a whatsapp group. Read the following conversation and then reply. +Do not add introduction or conclusion to your reply because this will be a continuous conversation. Don't introduce yourself. + +CHAT HISTORY +${this.getHistory({ to: route.to }) + .map((c) => `@${c.from}: ${c.content}`) + .join("\n")} + +@${route.from}:`, + }, + ] + : this.getHistory(route).map((c) => ({ + content: c.content, + role: c.from === route.to ? "user" : "assistant", + })); + + // build the messages to send to the provider + const messages = [ + { + content: fromConfig.role, + role: "system", + }, + // get the history of chats between the two nodes + ...chatHistory, + ]; + + // get the functions that the node can call + const functions = fromConfig.functions + ?.map((name) => this.functions.get(name)) + .filter((a) => !!a); + + const provider = this.getProviderForConfig({ + ...this.defaultProvider, + ...fromConfig, + }); + + // get the chat completion + const content = await this.handleExecution( + provider, + messages, + functions, + route.from + ); + this.newMessage({ ...route, content }); + + return content; + } + + async handleExecution( + provider, + messages = [], + functions = [], + byAgent = null + ) { + // get the chat completion + const completion = await provider.complete(messages, functions); + + if (completion.functionCall) { + const { name, arguments: args } = completion.functionCall; + const fn = this.functions.get(name); + + // if provider hallucinated on the function name + // ask the provider to complete again + if (!fn) { + return await this.handleExecution( + provider, + [ + ...messages, + { + name, + role: "function", + content: `Function "${name}" not found. Try again.`, + }, + ], + functions, + byAgent + ); + } + + // Execute the function and return the result to the provider + fn.caller = byAgent || "agent"; + const result = await fn.handler(args); + Telemetry.sendTelemetry("agent_tool_call", { tool: name }, null, true); + return await this.handleExecution( + provider, + [ + ...messages, + { + name, + role: "function", + content: result, + }, + ], + functions, + byAgent + ); + } + + return completion?.result; + } + + /** + * Continue the chat from the last interruption. + * If the last chat was not an interruption, it will throw an error. + * Provide a feedback where it was interrupted if you want to. + * + * @param feedback The feedback to the interruption if any. + * @returns + */ + async continue(feedback) { + const lastChat = this._chats.at(-1); + if (!lastChat || lastChat.state !== "interrupt") { + throw new Error("No chat to continue"); + } + + // remove the last chat's that was interrupted + this._chats.pop(); + + const { from, to } = lastChat; + + if (this.hasReachedMaximumRounds(from, to)) { + throw new Error("Maximum rounds reached"); + } + + if (feedback) { + const message = { + from, + to, + content: feedback, + }; + + // register the message in the chat history + this.newMessage(message); + + // ask the node to reply + await this.chat({ + to: message.from, + from: message.to, + }); + } else { + await this.chat({ from, to }); + } + + return this; + } + + /** + * Retry the last chat that threw an error. + * If the last chat was not an error, it will throw an error. + */ + async retry() { + const lastChat = this._chats.at(-1); + if (!lastChat || lastChat.state !== "error") { + throw new Error("No chat to retry"); + } + + // remove the last chat's that threw an error + const { from, to } = this?._chats?.pop(); + + await this.chat({ from, to }); + return this; + } + + /** + * Get the chat history between two nodes or all chats to/from a node. + */ + getHistory({ from, to }) { + return this._chats.filter((chat) => { + const isSuccess = chat.state === "success"; + + // return all chats to the node + if (!from) { + return isSuccess && chat.to === to; + } + + // get all chats from the node + if (!to) { + return isSuccess && chat.from === from; + } + + // check if the chat is between the two nodes + const hasSent = chat.from === from && chat.to === to; + const hasReceived = chat.from === to && chat.to === from; + const mutual = hasSent || hasReceived; + + return isSuccess && mutual; + }); + } + + /** + * Get provider based on configurations. + * If the provider is a string, it will return the default provider for that string. + * + * @param config The provider configuration. + */ + getProviderForConfig(config) { + if (typeof config.provider === "object") { + return config.provider; + } + + switch (config.provider) { + case "openai": + return new Providers.OpenAIProvider({ model: config.model }); + case "anthropic": + return new Providers.AnthropicProvider({ model: config.model }); + + default: + throw new Error( + `Unknown provider: ${config.provider}. Please use "openai"` + ); + } + } + + /** + * Register a new function to be called by the AIbitat agents. + * You are also required to specify the which node can call the function. + * @param functionConfig The function configuration. + */ + function(functionConfig) { + this.functions.set(functionConfig.name, functionConfig); + return this; + } +} + +module.exports = AIbitat; diff --git a/server/utils/agents/aibitat/plugins/chat-history.js b/server/utils/agents/aibitat/plugins/chat-history.js new file mode 100644 index 000000000..b4b51d0e2 --- /dev/null +++ b/server/utils/agents/aibitat/plugins/chat-history.js @@ -0,0 +1,49 @@ +const { WorkspaceChats } = require("../../../../models/workspaceChats"); + +/** + * Plugin to save chat history to AnythingLLM DB. + */ +const chatHistory = { + name: "chat-history", + startupConfig: { + params: {}, + }, + plugin: function () { + return { + name: this.name, + setup: function (aibitat) { + aibitat.onMessage(async () => { + try { + const lastResponses = aibitat.chats.slice(-2); + if (lastResponses.length !== 2) return; + const [prev, last] = lastResponses; + + // We need a full conversation reply with prev being from + // the USER and the last being from anyone other than the user. + if (prev.from !== "USER" || last.from === "USER") return; + await this._store(aibitat, { + prompt: prev.content, + response: last.content, + }); + } catch {} + }); + }, + _store: async function (aibitat, { prompt, response } = {}) { + const invocation = aibitat.handlerProps.invocation; + await WorkspaceChats.new({ + workspaceId: Number(invocation.workspace_id), + prompt, + response: { + text: response, + sources: [], + type: "chat", + }, + user: { id: invocation?.user_id || null }, + threadId: invocation?.thread_id || null, + }); + }, + }; + }, +}; + +module.exports = { chatHistory }; diff --git a/server/utils/agents/aibitat/plugins/cli.js b/server/utils/agents/aibitat/plugins/cli.js new file mode 100644 index 000000000..06a60e98e --- /dev/null +++ b/server/utils/agents/aibitat/plugins/cli.js @@ -0,0 +1,140 @@ +// Plugin CAN ONLY BE USE IN DEVELOPMENT. +const { input } = require("@inquirer/prompts"); +const chalk = require("chalk"); +const { RetryError } = require("../error"); + +/** + * Command-line Interface plugin. It prints the messages on the console and asks for feedback + * while the conversation is running in the background. + */ +const cli = { + name: "cli", + startupConfig: { + params: {}, + }, + plugin: function ({ simulateStream = true } = {}) { + return { + name: this.name, + setup(aibitat) { + let printing = []; + + aibitat.onError(async (error) => { + console.error(chalk.red(` error: ${error?.message}`)); + if (error instanceof RetryError) { + console.error(chalk.red(` retrying in 60 seconds...`)); + setTimeout(() => { + aibitat.retry(); + }, 60000); + return; + } + }); + + aibitat.onStart(() => { + console.log(); + console.log("🚀 starting chat ...\n"); + printing = [Promise.resolve()]; + }); + + aibitat.onMessage(async (message) => { + const next = new Promise(async (resolve) => { + await Promise.all(printing); + await this.print(message, simulateStream); + resolve(); + }); + printing.push(next); + }); + + aibitat.onTerminate(async () => { + await Promise.all(printing); + console.log("🚀 chat finished"); + }); + + aibitat.onInterrupt(async (node) => { + await Promise.all(printing); + const feedback = await this.askForFeedback(node); + // Add an extra line after the message + console.log(); + + if (feedback === "exit") { + console.log("🚀 chat finished"); + return process.exit(0); + } + + await aibitat.continue(feedback); + }); + }, + + /** + * Print a message on the terminal + * + * @param message + * // message Type { from: string; to: string; content?: string } & { + state: 'loading' | 'error' | 'success' | 'interrupt' + } + * @param simulateStream + */ + print: async function (message = {}, simulateStream = true) { + const replying = chalk.dim(`(to ${message.to})`); + const reference = `${chalk.magenta("✎")} ${chalk.bold( + message.from + )} ${replying}:`; + + if (!simulateStream) { + console.log(reference); + console.log(message.content); + // Add an extra line after the message + console.log(); + return; + } + + process.stdout.write(`${reference}\n`); + + // Emulate streaming by breaking the cached response into chunks + const chunks = message.content?.split(" ") || []; + const stream = new ReadableStream({ + async start(controller) { + for (const chunk of chunks) { + const bytes = new TextEncoder().encode(chunk + " "); + controller.enqueue(bytes); + await new Promise((r) => + setTimeout( + r, + // get a random number between 10ms and 50ms to simulate a random delay + Math.floor(Math.random() * 40) + 10 + ) + ); + } + controller.close(); + }, + }); + + // Stream the response to the chat + for await (const chunk of stream) { + process.stdout.write(new TextDecoder().decode(chunk)); + } + + // Add an extra line after the message + console.log(); + console.log(); + }, + + /** + * Ask for feedback to the user using the terminal + * + * @param node //{ from: string; to: string } + * @returns + */ + askForFeedback: function (node = {}) { + return input({ + message: `Provide feedback to ${chalk.yellow( + node.to + )} as ${chalk.yellow( + node.from + )}. Press enter to skip and use auto-reply, or type 'exit' to end the conversation: `, + }); + }, + }; + }, +}; + +module.exports = { cli }; diff --git a/server/utils/agents/aibitat/plugins/file-history.js b/server/utils/agents/aibitat/plugins/file-history.js new file mode 100644 index 000000000..2cab5e1a5 --- /dev/null +++ b/server/utils/agents/aibitat/plugins/file-history.js @@ -0,0 +1,37 @@ +const fs = require("fs"); +const path = require("path"); + +/** + * Plugin to save chat history to a json file + */ +const fileHistory = { + name: "file-history-plugin", + startupConfig: { + params: {}, + }, + plugin: function ({ + filename = `history/chat-history-${new Date().toISOString()}.json`, + } = {}) { + return { + name: this.name, + setup(aibitat) { + const folderPath = path.dirname(filename); + // get path from filename + if (folderPath) { + fs.mkdirSync(folderPath, { recursive: true }); + } + + aibitat.onMessage(() => { + const content = JSON.stringify(aibitat.chats, null, 2); + fs.writeFile(filename, content, (err) => { + if (err) { + console.error(err); + } + }); + }); + }, + }; + }, +}; + +module.exports = { fileHistory }; diff --git a/server/utils/agents/aibitat/plugins/index.js b/server/utils/agents/aibitat/plugins/index.js new file mode 100644 index 000000000..5892df4a4 --- /dev/null +++ b/server/utils/agents/aibitat/plugins/index.js @@ -0,0 +1,26 @@ +const { webBrowsing } = require("./web-browsing.js"); +const { webScraping } = require("./web-scraping.js"); +const { websocket } = require("./websocket.js"); +const { docSummarizer } = require("./summarize.js"); +const { saveFileInBrowser } = require("./save-file-browser.js"); +const { chatHistory } = require("./chat-history.js"); +const { memory } = require("./memory.js"); + +module.exports = { + webScraping, + webBrowsing, + websocket, + docSummarizer, + saveFileInBrowser, + chatHistory, + memory, + + // Plugin name aliases so they can be pulled by slug as well. + [webScraping.name]: webScraping, + [webBrowsing.name]: webBrowsing, + [websocket.name]: websocket, + [docSummarizer.name]: docSummarizer, + [saveFileInBrowser.name]: saveFileInBrowser, + [chatHistory.name]: chatHistory, + [memory.name]: memory, +}; diff --git a/server/utils/agents/aibitat/plugins/memory.js b/server/utils/agents/aibitat/plugins/memory.js new file mode 100644 index 000000000..c76b687b1 --- /dev/null +++ b/server/utils/agents/aibitat/plugins/memory.js @@ -0,0 +1,134 @@ +const { v4 } = require("uuid"); +const { getVectorDbClass, getLLMProvider } = require("../../../helpers"); +const { Deduplicator } = require("../utils/dedupe"); + +const memory = { + name: "rag-memory", + startupConfig: { + params: {}, + }, + plugin: function () { + return { + name: this.name, + setup(aibitat) { + aibitat.function({ + super: aibitat, + tracker: new Deduplicator(), + name: this.name, + description: + "Search against local documents for context that is relevant to the query or store a snippet of text into memory for retrieval later. Storing information should only be done when the user specifically requests for information to be remembered or saved to long-term memory. You should use this tool before search the internet for information.", + parameters: { + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + properties: { + action: { + type: "string", + enum: ["search", "store"], + description: + "The action we want to take to search for existing similar context or storage of new context.", + }, + content: { + type: "string", + description: + "The plain text to search our local documents with or to store in our vector database.", + }, + }, + additionalProperties: false, + }, + handler: async function ({ action = "", content = "" }) { + try { + if (this.tracker.isDuplicate(this.name, { action, content })) + return `This was a duplicated call and it's output will be ignored.`; + + let response = "There was nothing to do."; + if (action === "search") response = await this.search(content); + if (action === "store") response = await this.store(content); + + this.tracker.trackRun(this.name, { action, content }); + return response; + } catch (error) { + console.log(error); + return `There was an error while calling the function. ${error.message}`; + } + }, + search: async function (query = "") { + try { + const workspace = this.super.handlerProps.invocation.workspace; + const LLMConnector = getLLMProvider({ + provider: workspace?.chatProvider, + model: workspace?.chatModel, + }); + const vectorDB = getVectorDbClass(); + const { contextTexts = [] } = + await vectorDB.performSimilaritySearch({ + namespace: workspace.slug, + input: query, + LLMConnector, + }); + + if (contextTexts.length === 0) { + this.super.introspect( + `${this.caller}: I didn't find anything locally that would help answer this question.` + ); + return "There was no additional context found for that query. We should search the web for this information."; + } + + this.super.introspect( + `${this.caller}: Found ${contextTexts.length} additional piece of context to help answer this question.` + ); + + let combinedText = "Additional context for query:\n"; + for (const text of contextTexts) combinedText += text + "\n\n"; + return combinedText; + } catch (error) { + this.super.handlerProps.log( + `memory.search raised an error. ${error.message}` + ); + return `An error was raised while searching the vector database. ${error.message}`; + } + }, + store: async function (content = "") { + try { + const workspace = this.super.handlerProps.invocation.workspace; + const vectorDB = getVectorDbClass(); + const { error } = await vectorDB.addDocumentToNamespace( + workspace.slug, + { + docId: v4(), + id: v4(), + url: "file://embed-via-agent.txt", + title: "agent-memory.txt", + docAuthor: "@agent", + description: "Unknown", + docSource: "a text file stored by the workspace agent.", + chunkSource: "", + published: new Date().toLocaleString(), + wordCount: content.split(" ").length, + pageContent: content, + token_count_estimate: 0, + }, + null + ); + + if (!!error) + return "The content was failed to be embedded properly."; + this.super.introspect( + `${this.caller}: I saved the content to long-term memory in this workspaces vector database.` + ); + return "The content given was successfully embedded. There is nothing else to do."; + } catch (error) { + this.super.handlerProps.log( + `memory.store raised an error. ${error.message}` + ); + return `Let the user know this action was not successful. An error was raised while storing data in the vector database. ${error.message}`; + } + }, + }); + }, + }; + }, +}; + +module.exports = { + memory, +}; diff --git a/server/utils/agents/aibitat/plugins/save-file-browser.js b/server/utils/agents/aibitat/plugins/save-file-browser.js new file mode 100644 index 000000000..0e5092096 --- /dev/null +++ b/server/utils/agents/aibitat/plugins/save-file-browser.js @@ -0,0 +1,70 @@ +const { Deduplicator } = require("../utils/dedupe"); + +const saveFileInBrowser = { + name: "save-file-to-browser", + startupConfig: { + params: {}, + }, + plugin: function () { + return { + name: this.name, + setup(aibitat) { + // List and summarize the contents of files that are embedded in the workspace + aibitat.function({ + super: aibitat, + tracker: new Deduplicator(), + name: this.name, + description: + "Save content to a file when the user explicity asks for a download of the file.", + parameters: { + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + properties: { + file_content: { + type: "string", + description: "The content of the file that will be saved.", + }, + filename: { + type: "string", + description: + "filename to save the file as with extension. Extension should be plaintext file extension.", + }, + }, + additionalProperties: false, + }, + handler: async function ({ file_content = "", filename }) { + try { + if ( + this.tracker.isDuplicate(this.name, { file_content, filename }) + ) { + this.super.handlerProps.log( + `${this.name} was called, but exited early since it was not a unique call.` + ); + return `${filename} file has been saved successfully!`; + } + + this.super.socket.send("fileDownload", { + filename, + b64Content: + "data:text/plain;base64," + + Buffer.from(file_content, "utf8").toString("base64"), + }); + this.super.introspect(`${this.caller}: Saving file ${filename}.`); + this.tracker.trackRun(this.name, { file_content, filename }); + return `${filename} file has been saved successfully and will be downloaded automatically to the users browser.`; + } catch (error) { + this.super.handlerProps.log( + `save-file-to-browser raised an error. ${error.message}` + ); + return `Let the user know this action was not successful. An error was raised while saving a file to the browser. ${error.message}`; + } + }, + }); + }, + }; + }, +}; + +module.exports = { + saveFileInBrowser, +}; diff --git a/server/utils/agents/aibitat/plugins/summarize.js b/server/utils/agents/aibitat/plugins/summarize.js new file mode 100644 index 000000000..0c1a9f4e9 --- /dev/null +++ b/server/utils/agents/aibitat/plugins/summarize.js @@ -0,0 +1,130 @@ +const { Document } = require("../../../../models/documents"); +const { safeJsonParse } = require("../../../http"); +const { validate } = require("uuid"); +const { summarizeContent } = require("../utils/summarize"); + +const docSummarizer = { + name: "document-summarizer", + startupConfig: { + params: {}, + }, + plugin: function () { + return { + name: this.name, + setup(aibitat) { + aibitat.function({ + super: aibitat, + name: this.name, + controller: new AbortController(), + description: + "Can get the list of files available to search with descriptions and can select a single file to open and summarize.", + parameters: { + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + properties: { + action: { + type: "string", + enum: ["list", "summarize"], + description: + "The action to take. 'list' will return all files available and their document ids. 'summarize' will open and summarize the file by the document_id, in the format of a uuid.", + }, + document_id: { + type: "string", + "x-nullable": true, + description: + "A document id to summarize the content of. Document id must be a uuid.", + }, + }, + additionalProperties: false, + }, + handler: async function ({ action, document_id }) { + if (action === "list") return await this.listDocuments(); + if (action === "summarize") + return await this.summarizeDoc(document_id); + return "There is nothing we can do. This function call returns no information."; + }, + + /** + * List all documents available in a workspace + * @returns List of files and their descriptions if available. + */ + listDocuments: async function () { + try { + this.super.introspect( + `${this.caller}: Looking at the available documents.` + ); + const documents = await Document.where({ + workspaceId: this.super.handlerProps.invocation.workspace_id, + }); + if (documents.length === 0) + return "No documents found - nothing can be done. Stop."; + + this.super.introspect( + `${this.caller}: Found ${documents.length} documents` + ); + const foundDocuments = documents.map((doc) => { + const metadata = safeJsonParse(doc.metadata, {}); + return { + document_id: doc.docId, + filename: metadata?.title ?? "unknown.txt", + description: metadata?.description ?? "no description", + }; + }); + + return JSON.stringify(foundDocuments); + } catch (error) { + this.super.handlerProps.log( + `document-summarizer.list raised an error. ${error.message}` + ); + return `Let the user know this action was not successful. An error was raised while listing available files. ${error.message}`; + } + }, + + summarizeDoc: async function (documentId) { + try { + if (!validate(documentId)) { + this.super.handlerProps.log( + `${this.caller}: documentId ${documentId} is not a valid UUID` + ); + return "This was not a valid documentID because it was not a uuid. No content was found."; + } + + const document = await Document.content(documentId); + this.super.introspect( + `${this.caller}: Grabbing all content for ${ + document?.title ?? "a discovered file." + }` + ); + if (document?.content?.length < 8000) return content; + + this.super.introspect( + `${this.caller}: Summarizing ${document?.title ?? ""}...` + ); + + this.super.onAbort(() => { + this.super.handlerProps.log( + "Abort was triggered, exiting summarization early." + ); + this.controller.abort(); + }); + + return await summarizeContent( + this.controller.signal, + document.content + ); + } catch (error) { + this.super.handlerProps.log( + `document-summarizer.summarizeDoc raised an error. ${error.message}` + ); + return `Let the user know this action was not successful. An error was raised while summarizing the file. ${error.message}`; + } + }, + }); + }, + }; + }, +}; + +module.exports = { + docSummarizer, +}; diff --git a/server/utils/agents/aibitat/plugins/web-browsing.js b/server/utils/agents/aibitat/plugins/web-browsing.js new file mode 100644 index 000000000..889de840f --- /dev/null +++ b/server/utils/agents/aibitat/plugins/web-browsing.js @@ -0,0 +1,169 @@ +const { SystemSettings } = require("../../../../models/systemSettings"); + +const webBrowsing = { + name: "web-browsing", + startupConfig: { + params: {}, + }, + plugin: function () { + return { + name: this.name, + setup(aibitat) { + aibitat.function({ + super: aibitat, + name: this.name, + description: + "Searches for a given query online using a search engine.", + parameters: { + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + properties: { + query: { + type: "string", + description: "A search query.", + }, + }, + additionalProperties: false, + }, + handler: async function ({ query }) { + try { + if (query) return await this.search(query); + return "There is nothing we can do. This function call returns no information."; + } catch (error) { + return `There was an error while calling the function. No data or response was found. Let the user know this was the error: ${error.message}`; + } + }, + + /** + * Use Google Custom Search Engines + * Free to set up, easy to use, 100 calls/day! + * https://programmablesearchengine.google.com/controlpanel/create + */ + search: async function (query) { + const provider = + (await SystemSettings.get({ label: "agent_search_provider" })) + ?.value ?? "unknown"; + let engine; + switch (provider) { + case "google-search-engine": + engine = "_googleSearchEngine"; + break; + case "serper-dot-dev": + engine = "_serperDotDev"; + break; + default: + engine = "_googleSearchEngine"; + } + return await this[engine](query); + }, + + /** + * Use Google Custom Search Engines + * Free to set up, easy to use, 100 calls/day + * https://programmablesearchengine.google.com/controlpanel/create + */ + _googleSearchEngine: async function (query) { + if (!process.env.AGENT_GSE_CTX || !process.env.AGENT_GSE_KEY) { + this.super.introspect( + `${this.caller}: I can't use Google searching because the user has not defined the required API keys.\nVisit: https://programmablesearchengine.google.com/controlpanel/create to create the API keys.` + ); + return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`; + } + + const searchURL = new URL( + "https://www.googleapis.com/customsearch/v1" + ); + searchURL.searchParams.append("key", process.env.AGENT_GSE_KEY); + searchURL.searchParams.append("cx", process.env.AGENT_GSE_CTX); + searchURL.searchParams.append("q", query); + + this.super.introspect( + `${this.caller}: Searching on Google for "${ + query.length > 100 ? `${query.slice(0, 100)}...` : query + }"` + ); + const searchResponse = await fetch(searchURL) + .then((res) => res.json()) + .then((searchResult) => searchResult?.items || []) + .then((items) => { + return items.map((item) => { + return { + title: item.title, + link: item.link, + snippet: item.snippet, + }; + }); + }) + .catch((e) => { + console.log(e); + return {}; + }); + + return JSON.stringify(searchResponse); + }, + + /** + * Use Serper.dev + * Free to set up, easy to use, 2,500 calls for free one-time + * https://serper.dev + */ + _serperDotDev: async function (query) { + if (!process.env.AGENT_SERPER_DEV_KEY) { + this.super.introspect( + `${this.caller}: I can't use Serper.dev searching because the user has not defined the required API key.\nVisit: https://serper.dev to create the API key for free.` + ); + return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`; + } + + this.super.introspect( + `${this.caller}: Using Serper.dev to search for "${ + query.length > 100 ? `${query.slice(0, 100)}...` : query + }"` + ); + const { response, error } = await fetch( + "https://google.serper.dev/search", + { + method: "POST", + headers: { + "X-API-KEY": process.env.AGENT_SERPER_DEV_KEY, + "Content-Type": "application/json", + }, + body: JSON.stringify({ q: query }), + redirect: "follow", + } + ) + .then((res) => res.json()) + .then((data) => { + return { response: data, error: null }; + }) + .catch((e) => { + return { response: null, error: e.message }; + }); + if (error) + return `There was an error searching for content. ${error}`; + + const data = []; + if (response.hasOwnProperty("knowledgeGraph")) + data.push(response.knowledgeGraph); + response.organic?.forEach((searchResult) => { + const { title, link, snippet } = searchResult; + data.push({ + title, + link, + snippet, + }); + }); + + if (data.length === 0) + return `No information was found online for the search query.`; + return JSON.stringify(data); + }, + }); + }, + }; + }, +}; + +module.exports = { + webBrowsing, +}; diff --git a/server/utils/agents/aibitat/plugins/web-scraping.js b/server/utils/agents/aibitat/plugins/web-scraping.js new file mode 100644 index 000000000..90e226c0f --- /dev/null +++ b/server/utils/agents/aibitat/plugins/web-scraping.js @@ -0,0 +1,87 @@ +const { CollectorApi } = require("../../../collectorApi"); +const { summarizeContent } = require("../utils/summarize"); + +const webScraping = { + name: "web-scraping", + startupConfig: { + params: {}, + }, + plugin: function () { + return { + name: this.name, + setup(aibitat) { + aibitat.function({ + super: aibitat, + name: this.name, + controller: new AbortController(), + description: + "Scrapes the content of a webpage or online resource from a URL.", + parameters: { + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + properties: { + url: { + type: "string", + format: "uri", + description: "A web URL.", + }, + }, + additionalProperties: false, + }, + handler: async function ({ url }) { + try { + if (url) return await this.scrape(url); + return "There is nothing we can do. This function call returns no information."; + } catch (error) { + return `There was an error while calling the function. No data or response was found. Let the user know this was the error: ${error.message}`; + } + }, + + /** + * Scrape a website and summarize the content based on objective if the content is too large. + * Objective is the original objective & task that user give to the agent, url is the url of the website to be scraped. + * Here we can leverage the document collector to get raw website text quickly. + * + * @param url + * @returns + */ + scrape: async function (url) { + this.super.introspect( + `${this.caller}: Scraping the content of ${url}` + ); + const { success, content } = + await new CollectorApi().getLinkContent(url); + + if (!success) { + this.super.introspect( + `${this.caller}: could not scrape ${url}. I can't use this page's content.` + ); + throw new Error( + `URL could not be scraped and no content was found.` + ); + } + + if (content?.length <= 8000) { + return content; + } + + this.super.introspect( + `${this.caller}: This page's content is way too long. I will summarize it right now.` + ); + this.super.onAbort(() => { + this.super.handlerProps.log( + "Abort was triggered, exiting summarization early." + ); + this.controller.abort(); + }); + return summarizeContent(this.controller.signal, content); + }, + }); + }, + }; + }, +}; + +module.exports = { + webScraping, +}; diff --git a/server/utils/agents/aibitat/plugins/websocket.js b/server/utils/agents/aibitat/plugins/websocket.js new file mode 100644 index 000000000..af691ca55 --- /dev/null +++ b/server/utils/agents/aibitat/plugins/websocket.js @@ -0,0 +1,150 @@ +const chalk = require("chalk"); +const { RetryError } = require("../error"); +const { Telemetry } = require("../../../../models/telemetry"); +const SOCKET_TIMEOUT_MS = 300 * 1_000; // 5 mins + +/** + * Websocket Interface plugin. It prints the messages on the console and asks for feedback + * while the conversation is running in the background. + */ + +// export interface AIbitatWebSocket extends ServerWebSocket { +// askForFeedback?: any +// awaitResponse?: any +// handleFeedback?: (message: string) => void; +// } + +const WEBSOCKET_BAIL_COMMANDS = [ + "exit", + "/exit", + "stop", + "/stop", + "halt", + "/halt", +]; +const websocket = { + name: "websocket", + startupConfig: { + params: { + socket: { + required: true, + }, + muteUserReply: { + required: false, + default: true, + }, + introspection: { + required: false, + default: true, + }, + }, + }, + plugin: function ({ + socket, // @type AIbitatWebSocket + muteUserReply = true, // Do not post messages to "USER" back to frontend. + introspection = false, // when enabled will attach socket to Aibitat object with .introspect method which reports status updates to frontend. + }) { + return { + name: this.name, + setup(aibitat) { + aibitat.onError(async (error) => { + console.error(chalk.red(` error: ${error?.message}`)); + if (error instanceof RetryError) { + console.error(chalk.red(` retrying in 60 seconds...`)); + setTimeout(() => { + aibitat.retry(); + }, 60000); + return; + } + }); + + aibitat.introspect = (messageText) => { + if (!introspection) return; // Dump thoughts when not wanted. + socket.send( + JSON.stringify({ type: "statusResponse", content: messageText }) + ); + }; + + // expose function for sockets across aibitat + // type param must be set or else msg will not be shown or handled in UI. + aibitat.socket = { + send: (type = "__unhandled", content = "") => { + socket.send(JSON.stringify({ type, content })); + }, + }; + + // aibitat.onStart(() => { + // console.log("🚀 starting chat ..."); + // }); + + aibitat.onMessage((message) => { + if (message.from !== "USER") + Telemetry.sendTelemetry("agent_chat_sent"); + if (message.from === "USER" && muteUserReply) return; + socket.send(JSON.stringify(message)); + }); + + aibitat.onTerminate(() => { + // console.log("🚀 chat finished"); + socket.close(); + }); + + aibitat.onInterrupt(async (node) => { + const feedback = await socket.askForFeedback(socket, node); + if (WEBSOCKET_BAIL_COMMANDS.includes(feedback)) { + socket.close(); + return; + } + + await aibitat.continue(feedback); + }); + + /** + * Socket wait for feedback on socket + * + * @param socket The content to summarize. // AIbitatWebSocket & { receive: any, echo: any } + * @param node The chat node // { from: string; to: string } + * @returns The summarized content. + */ + socket.askForFeedback = (socket, node) => { + socket.awaitResponse = (question = "waiting...") => { + socket.send(JSON.stringify({ type: "WAITING_ON_INPUT", question })); + + return new Promise(function (resolve) { + let socketTimeout = null; + socket.handleFeedback = (message) => { + const data = JSON.parse(message); + if (data.type !== "awaitingFeedback") return; + delete socket.handleFeedback; + clearTimeout(socketTimeout); + resolve(data.feedback); + return; + }; + + socketTimeout = setTimeout(() => { + console.log( + chalk.red( + `Client took too long to respond, chat thread is dead after ${SOCKET_TIMEOUT_MS}ms` + ) + ); + resolve("exit"); + return; + }, SOCKET_TIMEOUT_MS); + }); + }; + + return socket.awaitResponse(`Provide feedback to ${chalk.yellow( + node.to + )} as ${chalk.yellow(node.from)}. + Press enter to skip and use auto-reply, or type 'exit' to end the conversation: \n`); + }; + // console.log("🚀 WS plugin is complete."); + }, + }; + }, +}; + +module.exports = { + websocket, + WEBSOCKET_BAIL_COMMANDS, +}; diff --git a/server/utils/agents/aibitat/providers/ai-provider.js b/server/utils/agents/aibitat/providers/ai-provider.js new file mode 100644 index 000000000..3f9181bc4 --- /dev/null +++ b/server/utils/agents/aibitat/providers/ai-provider.js @@ -0,0 +1,19 @@ +/** + * A service that provides an AI client to create a completion. + */ + +class Provider { + _client; + constructor(client) { + if (this.constructor == Provider) { + throw new Error("Class is of abstract type and can't be instantiated"); + } + this._client = client; + } + + get client() { + return this._client; + } +} + +module.exports = Provider; diff --git a/server/utils/agents/aibitat/providers/anthropic.js b/server/utils/agents/aibitat/providers/anthropic.js new file mode 100644 index 000000000..d160d9ab6 --- /dev/null +++ b/server/utils/agents/aibitat/providers/anthropic.js @@ -0,0 +1,151 @@ +const Anthropic = require("@anthropic-ai/sdk"); +const { RetryError } = require("../error.js"); +const Provider = require("./ai-provider.js"); + +/** + * The provider for the Anthropic API. + * By default, the model is set to 'claude-2'. + */ +class AnthropicProvider extends Provider { + model; + + constructor(config = {}) { + const { + options = { + apiKey: process.env.ANTHROPIC_API_KEY, + maxRetries: 3, + }, + model = "claude-2", + } = config; + + const client = new Anthropic(options); + + super(client); + + this.model = model; + } + + /** + * Create a completion based on the received messages. + * + * @param messages A list of messages to send to the Anthropic API. + * @param functions + * @returns The completion. + */ + async complete(messages, functions) { + // clone messages to avoid mutating the original array + const promptMessages = [...messages]; + + if (functions) { + const functionPrompt = this.getFunctionPrompt(functions); + + // add function prompt after the first message + promptMessages.splice(1, 0, { + content: functionPrompt, + role: "system", + }); + } + + const prompt = promptMessages + .map((message) => { + const { content, role } = message; + + switch (role) { + case "system": + return content + ? `${Anthropic.HUMAN_PROMPT} ${content}` + : ""; + + case "function": + case "user": + return `${Anthropic.HUMAN_PROMPT} ${content}`; + + case "assistant": + return `${Anthropic.AI_PROMPT} ${content}`; + + default: + return content; + } + }) + .filter(Boolean) + .join("\n") + .concat(` ${Anthropic.AI_PROMPT}`); + + try { + const response = await this.client.completions.create({ + model: this.model, + max_tokens_to_sample: 3000, + stream: false, + prompt, + }); + + const result = response.completion.trim(); + // TODO: get cost from response + const cost = 0; + + // Handle function calls if the model returns a function call + if (result.includes("function_name") && functions) { + let functionCall; + try { + functionCall = JSON.parse(result); + } catch (error) { + // call the complete function again in case it gets a json error + return await this.complete( + [ + ...messages, + { + role: "function", + content: `You gave me this function call: ${result} but I couldn't parse it. + ${error?.message} + + Please try again.`, + }, + ], + functions + ); + } + + return { + result: null, + functionCall, + cost, + }; + } + + return { + result, + cost, + }; + } catch (error) { + if ( + error instanceof Anthropic.RateLimitError || + error instanceof Anthropic.InternalServerError || + error instanceof Anthropic.APIError + ) { + throw new RetryError(error.message); + } + + throw error; + } + } + + getFunctionPrompt(functions = []) { + const functionPrompt = `You have been trained to directly call a Javascript function passing a JSON Schema parameter as a response to this chat. This function will return a string that you can use to keep chatting. + + Here is a list of functions available to you: + ${JSON.stringify(functions, null, 2)} + + When calling any of those function in order to complete your task, respond only this JSON format. Do not include any other information or any other stuff. + + Function call format: + { + function_name: "givenfunctionname", + parameters: {} + } + `; + + return functionPrompt; + } +} + +module.exports = AnthropicProvider; diff --git a/server/utils/agents/aibitat/providers/index.js b/server/utils/agents/aibitat/providers/index.js new file mode 100644 index 000000000..b163b4cd0 --- /dev/null +++ b/server/utils/agents/aibitat/providers/index.js @@ -0,0 +1,7 @@ +const OpenAIProvider = require("./openai.js"); +const AnthropicProvider = require("./anthropic.js"); + +module.exports = { + OpenAIProvider, + AnthropicProvider, +}; diff --git a/server/utils/agents/aibitat/providers/openai.js b/server/utils/agents/aibitat/providers/openai.js new file mode 100644 index 000000000..82cd7741e --- /dev/null +++ b/server/utils/agents/aibitat/providers/openai.js @@ -0,0 +1,144 @@ +const OpenAI = require("openai:latest"); +const Provider = require("./ai-provider.js"); +const { RetryError } = require("../error.js"); + +/** + * The provider for the OpenAI API. + * By default, the model is set to 'gpt-3.5-turbo'. + */ +class OpenAIProvider extends Provider { + model; + static COST_PER_TOKEN = { + "gpt-4": { + input: 0.03, + output: 0.06, + }, + "gpt-4-32k": { + input: 0.06, + output: 0.12, + }, + "gpt-3.5-turbo": { + input: 0.0015, + output: 0.002, + }, + "gpt-3.5-turbo-16k": { + input: 0.003, + output: 0.004, + }, + }; + + constructor(config = {}) { + const { + options = { + apiKey: process.env.OPEN_AI_KEY, + maxRetries: 3, + }, + model = "gpt-3.5-turbo", + } = config; + + const client = new OpenAI(options); + + super(client); + + this.model = model; + } + + /** + * Create a completion based on the received messages. + * + * @param messages A list of messages to send to the OpenAI API. + * @param functions + * @returns The completion. + */ + async complete(messages, functions = null) { + try { + const response = await this.client.chat.completions.create({ + model: this.model, + // stream: true, + messages, + ...(Array.isArray(functions) && functions?.length > 0 + ? { functions } + : {}), + }); + + // Right now, we only support one completion, + // so we just take the first one in the list + const completion = response.choices[0].message; + const cost = this.getCost(response.usage); + // treat function calls + if (completion.function_call) { + let functionArgs = {}; + try { + functionArgs = JSON.parse(completion.function_call.arguments); + } catch (error) { + // call the complete function again in case it gets a json error + return this.complete( + [ + ...messages, + { + role: "function", + name: completion.function_call.name, + function_call: completion.function_call, + content: error?.message, + }, + ], + functions + ); + } + + // console.log(completion, { functionArgs }) + return { + result: null, + functionCall: { + name: completion.function_call.name, + arguments: functionArgs, + }, + cost, + }; + } + + return { + result: completion.content, + cost, + }; + } catch (error) { + console.log(error); + if ( + error instanceof OpenAI.RateLimitError || + error instanceof OpenAI.InternalServerError || + error instanceof OpenAI.APIError + ) { + throw new RetryError(error.message); + } + + throw error; + } + } + + /** + * Get the cost of the completion. + * + * @param usage The completion to get the cost for. + * @returns The cost of the completion. + */ + getCost(usage) { + if (!usage) { + return Number.NaN; + } + + // regex to remove the version number from the model + const modelBase = this.model.replace(/-(\d{4})$/, ""); + + if (!(modelBase in OpenAIProvider.COST_PER_TOKEN)) { + return Number.NaN; + } + + const costPerToken = OpenAIProvider.COST_PER_TOKEN?.[modelBase]; + const inputCost = (usage.prompt_tokens / 1000) * costPerToken.input; + const outputCost = (usage.completion_tokens / 1000) * costPerToken.output; + + return inputCost + outputCost; + } +} + +module.exports = OpenAIProvider; diff --git a/server/utils/agents/aibitat/utils/dedupe.js b/server/utils/agents/aibitat/utils/dedupe.js new file mode 100644 index 000000000..b59efec6f --- /dev/null +++ b/server/utils/agents/aibitat/utils/dedupe.js @@ -0,0 +1,35 @@ +// Some models may attempt to call an expensive or annoying function many times and in that case we will want +// to implement some stateful tracking during that agent session. GPT4 and other more powerful models are smart +// enough to realize this, but models like 3.5 lack this. Open source models suffer greatly from this issue. +// eg: "save something to file..." +// agent -> saves +// agent -> saves +// agent -> saves +// agent -> saves +// ... do random # of times. +// We want to block all the reruns of a plugin, so we can add this to prevent that behavior from +// spamming the user (or other costly function) that have the exact same signatures. +const crypto = require("crypto"); + +class Deduplicator { + #hashes = {}; + constructor() {} + + trackRun(key, params = {}) { + const hash = crypto + .createHash("sha256") + .update(JSON.stringify({ key, params })) + .digest("hex"); + this.#hashes[hash] = Number(new Date()); + } + + isDuplicate(key, params = {}) { + const newSig = crypto + .createHash("sha256") + .update(JSON.stringify({ key, params })) + .digest("hex"); + return this.#hashes.hasOwnProperty(newSig); + } +} + +module.exports.Deduplicator = Deduplicator; diff --git a/server/utils/agents/aibitat/utils/summarize.js b/server/utils/agents/aibitat/utils/summarize.js new file mode 100644 index 000000000..26eae988a --- /dev/null +++ b/server/utils/agents/aibitat/utils/summarize.js @@ -0,0 +1,52 @@ +const { loadSummarizationChain } = require("langchain/chains"); +const { ChatOpenAI } = require("langchain/chat_models/openai"); +const { PromptTemplate } = require("langchain/prompts"); +const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter"); +/** + * Summarize content using OpenAI's GPT-3.5 model. + * + * @param self The context of the caller function + * @param content The content to summarize. + * @returns The summarized content. + */ +async function summarizeContent(controllerSignal, content) { + const llm = new ChatOpenAI({ + openAIApiKey: process.env.OPEN_AI_KEY, + temperature: 0, + modelName: "gpt-3.5-turbo-16k-0613", + }); + + const textSplitter = new RecursiveCharacterTextSplitter({ + separators: ["\n\n", "\n"], + chunkSize: 10000, + chunkOverlap: 500, + }); + const docs = await textSplitter.createDocuments([content]); + + const mapPrompt = ` + Write a detailed summary of the following text for a research purpose: + "{text}" + SUMMARY: + `; + + const mapPromptTemplate = new PromptTemplate({ + template: mapPrompt, + inputVariables: ["text"], + }); + + // This convenience function creates a document chain prompted to summarize a set of documents. + const chain = loadSummarizationChain(llm, { + type: "map_reduce", + combinePrompt: mapPromptTemplate, + combineMapPrompt: mapPromptTemplate, + verbose: process.env.NODE_ENV === "development", + }); + const res = await chain.call({ + ...(controllerSignal ? { signal: controllerSignal } : {}), + input_documents: docs, + }); + + return res.text; +} + +module.exports = { summarizeContent }; diff --git a/server/utils/agents/defaults.js b/server/utils/agents/defaults.js new file mode 100644 index 000000000..a030778f4 --- /dev/null +++ b/server/utils/agents/defaults.js @@ -0,0 +1,42 @@ +const AgentPlugins = require("./aibitat/plugins"); +const { SystemSettings } = require("../../models/systemSettings"); +const { safeJsonParse } = require("../http"); + +const USER_AGENT = { + name: "USER", + getDefinition: async () => { + return { + interrupt: "ALWAYS", + role: "I am the human monitor and oversee this chat. Any questions on action or decision making should be directed to me.", + }; + }, +}; + +const WORKSPACE_AGENT = { + name: "@agent", + getDefinition: async () => { + const defaultFunctions = [ + AgentPlugins.memory.name, // RAG + AgentPlugins.docSummarizer.name, // Doc Summary + AgentPlugins.webScraping.name, // Collector web-scraping + ]; + + const _setting = ( + await SystemSettings.get({ label: "default_agent_skills" }) + )?.value; + safeJsonParse(_setting, []).forEach((skillName) => { + if (!AgentPlugins.hasOwnProperty(skillName)) return; + defaultFunctions.push(AgentPlugins[skillName].name); + }); + + return { + role: "You are a helpful ai assistant who can assist the user and use tools available to help answer the users prompts and questions.", + functions: defaultFunctions, + }; + }, +}; + +module.exports = { + USER_AGENT, + WORKSPACE_AGENT, +}; diff --git a/server/utils/agents/index.js b/server/utils/agents/index.js new file mode 100644 index 000000000..ff66c982b --- /dev/null +++ b/server/utils/agents/index.js @@ -0,0 +1,201 @@ +const AIbitat = require("./aibitat"); +const AgentPlugins = require("./aibitat/plugins"); +const { + WorkspaceAgentInvocation, +} = require("../../models/workspaceAgentInvocation"); +const { WorkspaceChats } = require("../../models/workspaceChats"); +const { safeJsonParse } = require("../http"); +const { USER_AGENT, WORKSPACE_AGENT } = require("./defaults"); + +class AgentHandler { + #invocationUUID; + #funcsToLoad = []; + invocation = null; + aibitat = null; + channel = null; + provider = null; + model = null; + + constructor({ uuid }) { + this.#invocationUUID = uuid; + } + + log(text, ...args) { + console.log(`\x1b[36m[AgentHandler]\x1b[0m ${text}`, ...args); + } + + closeAlert() { + this.log(`End ${this.#invocationUUID}::${this.provider}:${this.model}`); + } + + async #chatHistory(limit = 10) { + try { + const rawHistory = ( + await WorkspaceChats.where( + { + workspaceId: this.invocation.workspace_id, + user_id: this.invocation.user_id || null, + thread_id: this.invocation.user_id || null, + include: true, + }, + limit, + { id: "desc" } + ) + ).reverse(); + + const agentHistory = []; + rawHistory.forEach((chatLog) => { + agentHistory.push( + { + from: USER_AGENT.name, + to: WORKSPACE_AGENT.name, + content: chatLog.prompt, + }, + { + from: WORKSPACE_AGENT.name, + to: USER_AGENT.name, + content: safeJsonParse(chatLog.response)?.text || "", + state: "success", + } + ); + }); + return agentHistory; + } catch (e) { + this.log("Error loading chat history", e.message); + return []; + } + } + + #checkSetup() { + switch (this.provider) { + case "openai": + if (!process.env.OPEN_AI_KEY) + throw new Error("OpenAI API key must be provided to use agents."); + break; + case "anthropic": + if (!process.env.ANTHROPIC_API_KEY) + throw new Error("Anthropic API key must be provided to use agents."); + break; + default: + throw new Error("No provider found to power agent cluster."); + } + } + + #providerSetupAndCheck() { + this.provider = this.invocation.workspace.agentProvider || "openai"; + this.model = this.invocation.workspace.agentModel || "gpt-3.5-turbo"; + this.log(`Start ${this.#invocationUUID}::${this.provider}:${this.model}`); + this.#checkSetup(); + } + + async #validInvocation() { + const invocation = await WorkspaceAgentInvocation.getWithWorkspace({ + uuid: String(this.#invocationUUID), + }); + if (invocation?.closed) + throw new Error("This agent invocation is already closed"); + this.invocation = invocation ?? null; + } + + #attachPlugins(args) { + for (const name of this.#funcsToLoad) { + if (!AgentPlugins.hasOwnProperty(name)) { + this.log( + `${name} is not a valid plugin. Skipping inclusion to agent cluster.` + ); + continue; + } + + const callOpts = {}; + for (const [param, definition] of Object.entries( + AgentPlugins[name].startupConfig.params + )) { + if ( + definition.required && + (!args.hasOwnProperty(param) || args[param] === null) + ) { + this.log( + `'${param}' required parameter for '${name}' plugin is missing. Plugin may not function or crash agent.` + ); + continue; + } + callOpts[param] = args.hasOwnProperty(param) + ? args[param] + : definition.default || null; + } + + const AIbitatPlugin = AgentPlugins[name]; + this.aibitat.use(AIbitatPlugin.plugin(callOpts)); + this.log(`Attached ${name} plugin to Agent cluster`); + } + } + + async #loadAgents() { + // Default User agent and workspace agent + this.log(`Attaching user and default agent to Agent cluster.`); + this.aibitat.agent(USER_AGENT.name, await USER_AGENT.getDefinition()); + this.aibitat.agent( + WORKSPACE_AGENT.name, + await WORKSPACE_AGENT.getDefinition() + ); + + this.#funcsToLoad = [ + ...((await USER_AGENT.getDefinition())?.functions || []), + ...((await WORKSPACE_AGENT.getDefinition())?.functions || []), + ]; + } + + async init() { + await this.#validInvocation(); + this.#providerSetupAndCheck(); + return this; + } + + async createAIbitat( + args = { + socket, + } + ) { + this.aibitat = new AIbitat({ + provider: "openai", + model: "gpt-3.5-turbo", + chats: await this.#chatHistory(20), + handlerProps: { + invocation: this.invocation, + log: this.log, + }, + }); + + // Attach standard websocket plugin for frontend communication. + this.log(`Attached ${AgentPlugins.websocket.name} plugin to Agent cluster`); + this.aibitat.use( + AgentPlugins.websocket.plugin({ + socket: args.socket, + muteUserReply: true, + introspection: true, + }) + ); + + // Attach standard chat-history plugin for message storage. + this.log( + `Attached ${AgentPlugins.chatHistory.name} plugin to Agent cluster` + ); + this.aibitat.use(AgentPlugins.chatHistory.plugin()); + + // Load required agents (Default + custom) + await this.#loadAgents(); + + // Attach all required plugins for functions to operate. + this.#attachPlugins(args); + } + + startAgentCluster() { + return this.aibitat.start({ + from: USER_AGENT.name, + to: this.channel ?? WORKSPACE_AGENT.name, + content: this.invocation.prompt, + }); + } +} + +module.exports.AgentHandler = AgentHandler; diff --git a/server/utils/chats/agents.js b/server/utils/chats/agents.js new file mode 100644 index 000000000..cd127d07f --- /dev/null +++ b/server/utils/chats/agents.js @@ -0,0 +1,71 @@ +const pluralize = require("pluralize"); +const { + WorkspaceAgentInvocation, +} = require("../../models/workspaceAgentInvocation"); +const { writeResponseChunk } = require("../helpers/chat/responses"); + +async function grepAgents({ + uuid, + response, + message, + workspace, + user = null, + thread = null, +}) { + const agentHandles = WorkspaceAgentInvocation.parseAgents(message); + if (agentHandles.length > 0) { + const { invocation: newInvocation } = await WorkspaceAgentInvocation.new({ + prompt: message, + workspace: workspace, + user: user, + thread: thread, + }); + + if (!newInvocation) { + writeResponseChunk(response, { + id: uuid, + type: "statusResponse", + textResponse: `${pluralize( + "Agent", + agentHandles.length + )} ${agentHandles.join( + ", " + )} could not be called. Chat will be handled as default chat.`, + sources: [], + close: true, + error: null, + }); + return; + } + + writeResponseChunk(response, { + id: uuid, + type: "agentInitWebsocketConnection", + textResponse: null, + sources: [], + close: false, + error: null, + websocketUUID: newInvocation.uuid, + }); + + // Close HTTP stream-able chunk response method because we will swap to agents now. + writeResponseChunk(response, { + id: uuid, + type: "statusResponse", + textResponse: `${pluralize( + "Agent", + agentHandles.length + )} ${agentHandles.join( + ", " + )} invoked.\nSwapping over to agent chat. Type /exit to exit agent execution loop early.`, + sources: [], + close: true, + error: null, + }); + return true; + } + + return false; +} + +module.exports = { grepAgents }; diff --git a/server/utils/chats/stream.js b/server/utils/chats/stream.js index 0ec969eba..b5128c193 100644 --- a/server/utils/chats/stream.js +++ b/server/utils/chats/stream.js @@ -3,6 +3,7 @@ const { DocumentManager } = require("../DocumentManager"); const { WorkspaceChats } = require("../../models/workspaceChats"); const { getVectorDbClass, getLLMProvider } = require("../helpers"); const { writeResponseChunk } = require("../helpers/chat/responses"); +const { grepAgents } = require("./agents"); const { grepCommand, VALID_COMMANDS, @@ -35,6 +36,17 @@ async function streamChatWithWorkspace( return; } + // If is agent enabled chat we will exit this flow early. + const isAgentChat = await grepAgents({ + uuid, + response, + message, + user, + workspace, + thread, + }); + if (isAgentChat) return; + const LLMConnector = getLLMProvider({ provider: workspace?.chatProvider, model: workspace?.chatModel, diff --git a/server/utils/collectorApi/index.js b/server/utils/collectorApi/index.js index d96cd1e6d..1a1431ac1 100644 --- a/server/utils/collectorApi/index.js +++ b/server/utils/collectorApi/index.js @@ -133,6 +133,29 @@ class CollectorApi { return { success: false, data: {}, reason: e.message }; }); } + + async getLinkContent(link = "") { + if (!link) return false; + + const data = JSON.stringify({ link }); + return await fetch(`${this.endpoint}/util/get-link`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Integrity": this.comkey.sign(data), + }, + body: data, + }) + .then((res) => { + if (!res.ok) throw new Error("Response could not be completed"); + return res.json(); + }) + .then((res) => res) + .catch((e) => { + this.log(e.message); + return { success: false, content: null }; + }); + } } module.exports.CollectorApi = CollectorApi; diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index b23855877..811911f3e 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -292,6 +292,20 @@ const KEY_MAPPING = { envKey: "DISABLE_TELEMETRY", checks: [], }, + + // Agent Integration ENVs + AgentGoogleSearchEngineId: { + envKey: "AGENT_GSE_CTX", + checks: [], + }, + AgentGoogleSearchEngineKey: { + envKey: "AGENT_GSE_KEY", + checks: [], + }, + AgentSerperApiKey: { + envKey: "AGENT_SERPER_DEV_KEY", + checks: [], + }, }; function isNotEmpty(input = "") { @@ -571,6 +585,12 @@ async function dumpENV() { "HTTPS_KEY_PATH", // DISABLED TELEMETRY "DISABLE_TELEMETRY", + + // Agent Integrations + // Search engine integrations + "AGENT_GSE_CTX", + "AGENT_GSE_KEY", + "AGENT_SERPER_DEV_KEY", ]; // Simple sanitization of each value to prevent ENV injection via newline or quote escaping. diff --git a/server/yarn.lock b/server/yarn.lock index 4cfbb7ffa..51dda19f7 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -302,6 +302,119 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz#e5211452df060fa8522b55c7b3c0c4d1981cb044" integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw== +"@inquirer/checkbox@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@inquirer/checkbox/-/checkbox-2.2.1.tgz#100fcade0209a9b5eaef80403e06130401a0b438" + integrity sha512-eYdhZWZMOaliMBPOL/AO3uId58lp+zMyrJdoZ2xw9hfUY4IYJlIMvgW80RJdvCY3q9fGMUyZI5GwguH2tO51ew== + dependencies: + "@inquirer/core" "^7.1.1" + "@inquirer/type" "^1.2.1" + ansi-escapes "^4.3.2" + chalk "^4.1.2" + figures "^3.2.0" + +"@inquirer/confirm@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-3.1.1.tgz#e17c9eafa3d8f494fad3f848ba1e4c61d0a7ddcf" + integrity sha512-epf2RVHJJxX5qF85U41PBq9qq2KTJW9sKNLx6+bb2/i2rjXgeoHVGUm8kJxZHavrESgXgBLKCABcfOJYIso8cQ== + dependencies: + "@inquirer/core" "^7.1.1" + "@inquirer/type" "^1.2.1" + +"@inquirer/core@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-7.1.1.tgz#9339095720c00cfd1f85943977ae15d2f66f336a" + integrity sha512-rD1UI3eARN9qJBcLRXPOaZu++Bg+xsk0Tuz1EUOXEW+UbYif1sGjr0Tw7lKejHzKD9IbXE1CEtZ+xR/DrNlQGQ== + dependencies: + "@inquirer/type" "^1.2.1" + "@types/mute-stream" "^0.0.4" + "@types/node" "^20.11.30" + "@types/wrap-ansi" "^3.0.0" + ansi-escapes "^4.3.2" + chalk "^4.1.2" + cli-spinners "^2.9.2" + cli-width "^4.1.0" + figures "^3.2.0" + mute-stream "^1.0.0" + signal-exit "^4.1.0" + strip-ansi "^6.0.1" + wrap-ansi "^6.2.0" + +"@inquirer/editor@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@inquirer/editor/-/editor-2.1.1.tgz#e2d50246fd7dd4b4c2f20b86c969912be4c36899" + integrity sha512-SGVAmSKY2tt62+5KUySYFeMwJEXX866Ws5MyjwbrbB+WqC8iZAtPcK0pz8KVsO0ak/DB3/vCZw0k2nl7TifV5g== + dependencies: + "@inquirer/core" "^7.1.1" + "@inquirer/type" "^1.2.1" + external-editor "^3.1.0" + +"@inquirer/expand@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@inquirer/expand/-/expand-2.1.1.tgz#5364c5ddf0fb6358c5610103efde6a4aa366c2fe" + integrity sha512-FTHf56CgE24CtweB+3sF4mOFa6Q7H8NfTO+SvYio3CgQwhIWylSNueEeJ7sYBnWaXHNUfiX883akgvSbWqSBoQ== + dependencies: + "@inquirer/core" "^7.1.1" + "@inquirer/type" "^1.2.1" + chalk "^4.1.2" + +"@inquirer/input@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@inquirer/input/-/input-2.1.1.tgz#a293a1d1bef103a1f4176d5b41df6d3272b7b48f" + integrity sha512-Ag5PDh3/V3B68WGD/5LKXDqbdWKlF7zyfPAlstzu0NoZcZGBbZFjfgXlZIcb6Gs+AfdSi7wNf7soVAaMGH7moQ== + dependencies: + "@inquirer/core" "^7.1.1" + "@inquirer/type" "^1.2.1" + +"@inquirer/password@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@inquirer/password/-/password-2.1.1.tgz#9465dc1afa28bc75de2ee5fdb18852a25b2fe00e" + integrity sha512-R5R6NVXDKXEjAOGBqgRGrchFlfdZIx/qiDvH63m1u1NQVOQFUMfHth9VzVwuTZ2LHzbb9UrYpBumh2YytFE9iQ== + dependencies: + "@inquirer/core" "^7.1.1" + "@inquirer/type" "^1.2.1" + ansi-escapes "^4.3.2" + +"@inquirer/prompts@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@inquirer/prompts/-/prompts-4.3.1.tgz#f2906a5d7b4c2c8af9bd5bd8d495466bdd52f411" + integrity sha512-FI8jhVm3GRJ/z40qf7YZnSP0TfPKDPdIYZT9W6hmiYuaSmAXL66YMXqonKyysE5DwtKQBhIqt0oSoTKp7FCvQQ== + dependencies: + "@inquirer/checkbox" "^2.2.1" + "@inquirer/confirm" "^3.1.1" + "@inquirer/core" "^7.1.1" + "@inquirer/editor" "^2.1.1" + "@inquirer/expand" "^2.1.1" + "@inquirer/input" "^2.1.1" + "@inquirer/password" "^2.1.1" + "@inquirer/rawlist" "^2.1.1" + "@inquirer/select" "^2.2.1" + +"@inquirer/rawlist@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@inquirer/rawlist/-/rawlist-2.1.1.tgz#07ba2f9c4185e3787954e4023ae16d1a44d6da92" + integrity sha512-PIpJdNqVhjnl2bDz8iUKqMmgGdspN4s7EZiuNPnNrqZLP+LRUDDHVyd7X7xjiEMulBt3lt2id4SjTbra+v/Ajg== + dependencies: + "@inquirer/core" "^7.1.1" + "@inquirer/type" "^1.2.1" + chalk "^4.1.2" + +"@inquirer/select@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@inquirer/select/-/select-2.2.1.tgz#cd1f8b7869a74ff7f409a01f27998d06e234ea98" + integrity sha512-JR4FeHvuxPSPWQy8DzkIvoIsJ4SWtSFb4xVLvLto84dL+jkv12lm8ILtuax4bMHvg5MBj3wYUF6Tk9izJ07gdw== + dependencies: + "@inquirer/core" "^7.1.1" + "@inquirer/type" "^1.2.1" + ansi-escapes "^4.3.2" + chalk "^4.1.2" + figures "^3.2.0" + +"@inquirer/type@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-1.2.1.tgz#fbc7ab3a2e5050d0c150642d5e8f5e88faa066b8" + integrity sha512-xwMfkPAxeo8Ji/IxfUSqzRi0/+F2GIqJmpc5/thelgMGsjNZcjDDRBO9TLXT1s/hdx/mK5QbVIvgoLIFgXhTMQ== + "@kwsites/file-exists@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@kwsites/file-exists/-/file-exists-1.1.1.tgz#ad1efcac13e1987d8dbaf235ef3be5b0d96faa99" @@ -802,6 +915,13 @@ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== +"@types/mute-stream@^0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@types/mute-stream/-/mute-stream-0.0.4.tgz#77208e56a08767af6c5e1237be8888e2f255c478" + integrity sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow== + dependencies: + "@types/node" "*" + "@types/node-fetch@^2.6.4": version "2.6.7" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.7.tgz#a1abe2ce24228b58ad97f99480fdcf9bbc6ab16d" @@ -841,6 +961,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@^20.11.30": + version "20.12.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.4.tgz#af5921bd75ccdf3a3d8b3fa75bf3d3359268cd11" + integrity sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw== + dependencies: + undici-types "~5.26.4" + "@types/pad-left@2.1.1": version "2.1.1" resolved "https://registry.yarnpkg.com/@types/pad-left/-/pad-left-2.1.1.tgz#17d906fc75804e1cc722da73623f1d978f16a137" @@ -861,6 +988,11 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.7.tgz#b14cebc75455eeeb160d5fe23c2fcc0c64f724d8" integrity sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g== +"@types/wrap-ansi@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd" + integrity sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g== + "@types/yauzl@^2.9.1": version "2.10.0" resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599" @@ -982,6 +1114,13 @@ ajv@^8.12.0: require-from-string "^2.0.2" uri-js "^4.2.2" +ansi-escapes@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -1335,6 +1474,11 @@ body-parser@^1.20.2: type-is "~1.6.18" unpipe "1.0.0" +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + bottleneck@^2.15.3: version "2.19.5" resolved "https://registry.yarnpkg.com/bottleneck/-/bottleneck-2.19.5.tgz#5df0b90f59fd47656ebe63c78a98419205cadd91" @@ -1475,7 +1619,7 @@ chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0: +chalk@^4, chalk@^4.0.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1488,6 +1632,11 @@ chalk@^5.0.0, chalk@^5.3.0: resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + charenc@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" @@ -1554,11 +1703,16 @@ cli-progress@^3.12.0: dependencies: string-width "^4.2.3" -cli-spinners@^2.9.0: +cli-spinners@^2.9.0, cli-spinners@^2.9.2: version "2.9.2" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== +cli-width@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5" + integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ== + cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -1796,6 +1950,22 @@ crypt@0.0.2: resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + dayjs@^1.11.7: version "1.11.10" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" @@ -1919,6 +2089,36 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + dotenv@^16.0.3: version "16.3.1" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" @@ -1980,6 +2180,11 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" +entities@^4.2.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + env-paths@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" @@ -2273,6 +2478,13 @@ expr-eval@^2.0.2: resolved "https://registry.yarnpkg.com/expr-eval/-/expr-eval-2.0.2.tgz#fa6f044a7b0c93fde830954eb9c5b0f7fbc7e201" integrity sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg== +express-ws@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/express-ws/-/express-ws-5.0.2.tgz#5b02d41b937d05199c6c266d7cc931c823bda8eb" + integrity sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ== + dependencies: + ws "^7.4.6" + express@^4.18.2: version "4.18.2" resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" @@ -2315,6 +2527,15 @@ extend@^3.0.2: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +external-editor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + extract-files@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-9.0.0.tgz#8a7744f2437f81f5ed3250ed9f1550de902fe54a" @@ -2380,6 +2601,13 @@ fecha@^4.2.0: resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== +figures@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -2868,6 +3096,11 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + hermes-eslint@^0.15.0: version "0.15.1" resolved "https://registry.yarnpkg.com/hermes-eslint/-/hermes-eslint-0.15.1.tgz#c5919a6fdbd151febc3d5ed8ff17e5433913528c" @@ -2950,7 +3183,7 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -iconv-lite@0.4.24: +iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -3928,6 +4161,11 @@ multer@^1.4.5-lts.1: type-is "^1.6.4" xtend "^4.0.0" +mute-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e" + integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== + napi-build-utils@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" @@ -4008,6 +4246,21 @@ node-gyp@8.x: tar "^6.1.2" which "^2.0.2" +node-html-markdown@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/node-html-markdown/-/node-html-markdown-1.3.0.tgz#ef0b19a3bbfc0f1a880abb9ff2a0c9aa6bbff2a9" + integrity sha512-OeFi3QwC/cPjvVKZ114tzzu+YoR+v9UXW5RwSXGUqGb0qCl0DvP406tzdL7SFn8pZrMyzXoisfG2zcuF9+zw4g== + dependencies: + node-html-parser "^6.1.1" + +node-html-parser@^6.1.1: + version "6.1.13" + resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-6.1.13.tgz#a1df799b83df5c6743fcd92740ba14682083b7e4" + integrity sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg== + dependencies: + css-select "^5.1.0" + he "1.2.0" + node-llama-cpp@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/node-llama-cpp/-/node-llama-cpp-2.8.0.tgz#c01e469761caa4b9c51dbcf7555260caf7fb7bd6" @@ -4090,6 +4343,13 @@ npmlog@^6.0.0, npmlog@^6.0.2: gauge "^4.0.3" set-blocking "^2.0.0" +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + num-sort@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/num-sort/-/num-sort-2.1.0.tgz#1cbb37aed071329fdf41151258bc011898577a9b" @@ -4235,6 +4495,20 @@ onnxruntime-web@1.14.0: onnxruntime-common "~1.14.0" platform "^1.3.6" +"openai:latest@npm:openai@latest": + version "4.32.1" + resolved "https://registry.yarnpkg.com/openai/-/openai-4.32.1.tgz#9e375fdbc727330c5ea5d287beb325db3e6f9ad7" + integrity sha512-3e9QyCY47tgOkxBe2CSVKlXOE2lLkMa24Y0s3LYZR40yYjiBU9dtVze+C3mu1TwWDGiRX52STpQAEJZvRNuIrA== + dependencies: + "@types/node" "^18.11.18" + "@types/node-fetch" "^2.6.4" + abort-controller "^3.0.0" + agentkeepalive "^4.2.1" + form-data-encoder "1.7.2" + formdata-node "^4.3.2" + node-fetch "^2.6.7" + web-streams-polyfill "^3.2.1" + openai@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/openai/-/openai-3.3.0.tgz#a6408016ad0945738e1febf43f2fccca83a3f532" @@ -4290,6 +4564,11 @@ ora@^7.0.1: string-width "^6.1.0" strip-ansi "^7.1.0" +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -4412,6 +4691,11 @@ platform@^1.3.6: resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + posthog-node@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/posthog-node/-/posthog-node-3.1.1.tgz#f92c44a871552c9bfb98bf4cc8fd326d36af6cbd" @@ -4931,6 +5215,11 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.7: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +signal-exit@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + simple-concat@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" @@ -5271,6 +5560,13 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -5334,6 +5630,11 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + type-is@^1.6.4, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -5675,6 +5976,15 @@ wordwrapjs@^4.0.0: reduce-flatten "^2.0.0" typical "^5.2.0" +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -5689,6 +5999,11 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== +ws@^7.4.6: + version "7.5.9" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" + integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + xtend@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" From e193bb8f59dbdae8ed46ce06f2f0fdf861d76437 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Tue, 16 Apr 2024 13:52:11 -0700 Subject: [PATCH 05/10] update line underline css --- frontend/src/index.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/index.css b/frontend/src/index.css index 69d629c2f..9241d6184 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -553,6 +553,10 @@ dialog::backdrop { padding-top: 10px; } +.markdown ol li a { + text-decoration: underline; +} + .markdown ol li p a { text-decoration: underline; } From 661563408a8b21b57e7b8f71af85a62fa32922dc Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Tue, 16 Apr 2024 14:54:39 -0700 Subject: [PATCH 06/10] Enable dynamic GPT model dropdown (#1111) * Enable dynamic GPT model dropdown --- .../LLMSelection/OpenAiOptions/index.jsx | 56 +++++------- frontend/src/hooks/useGetProvidersModels.js | 17 ++-- server/utils/AiProviders/openAi/index.js | 30 +++---- server/utils/helpers/customModels.js | 90 ++++++++++++++++--- 4 files changed, 117 insertions(+), 76 deletions(-) diff --git a/frontend/src/components/LLMSelection/OpenAiOptions/index.jsx b/frontend/src/components/LLMSelection/OpenAiOptions/index.jsx index c5ec337d0..67f7d291b 100644 --- a/frontend/src/components/LLMSelection/OpenAiOptions/index.jsx +++ b/frontend/src/components/LLMSelection/OpenAiOptions/index.jsx @@ -32,22 +32,26 @@ export default function OpenAiOptions({ settings }) { } function OpenAIModelSelection({ apiKey, settings }) { - const [customModels, setCustomModels] = useState([]); + const [groupedModels, setGroupedModels] = useState({}); const [loading, setLoading] = useState(true); useEffect(() => { async function findCustomModels() { - if (!apiKey) { - setCustomModels([]); - setLoading(false); - return; - } setLoading(true); const { models } = await System.customModels( "openai", typeof apiKey === "boolean" ? null : apiKey ); - setCustomModels(models || []); + + if (models?.length > 0) { + const modelsByOrganization = models.reduce((acc, model) => { + acc[model.organization] = acc[model.organization] || []; + acc[model.organization].push(model); + return acc; + }, {}); + setGroupedModels(modelsByOrganization); + } + setLoading(false); } findCustomModels(); @@ -82,41 +86,21 @@ function OpenAIModelSelection({ apiKey, settings }) { required={true} className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5" > - - {[ - "gpt-3.5-turbo", - "gpt-3.5-turbo-1106", - "gpt-4", - "gpt-4-turbo-preview", - "gpt-4-1106-preview", - "gpt-4-32k", - ].map((model) => { - return ( - - ); - })} - - {customModels.length > 0 && ( - - {customModels.map((model) => { - return ( + {Object.keys(groupedModels) + .sort() + .map((organization) => ( + + {groupedModels[organization].map((model) => ( - ); - })} - - )} + ))} + + ))}
); diff --git a/frontend/src/hooks/useGetProvidersModels.js b/frontend/src/hooks/useGetProvidersModels.js index 95df82a3a..513bfdbe1 100644 --- a/frontend/src/hooks/useGetProvidersModels.js +++ b/frontend/src/hooks/useGetProvidersModels.js @@ -4,14 +4,7 @@ import { useEffect, useState } from "react"; // Providers which cannot use this feature for workspace<>model selection export const DISABLED_PROVIDERS = ["azure", "lmstudio", "native"]; const PROVIDER_DEFAULT_MODELS = { - openai: [ - "gpt-3.5-turbo", - "gpt-3.5-turbo-1106", - "gpt-4", - "gpt-4-turbo-preview", - "gpt-4-1106-preview", - "gpt-4-32k", - ], + openai: [], gemini: ["gemini-pro"], anthropic: [ "claude-instant-1.2", @@ -41,6 +34,7 @@ function groupModels(models) { }, {}); } +const groupedProviders = ["togetherai", "openai"]; export default function useGetProviderModels(provider = null) { const [defaultModels, setDefaultModels] = useState([]); const [customModels, setCustomModels] = useState([]); @@ -50,9 +44,12 @@ export default function useGetProviderModels(provider = null) { async function fetchProviderModels() { if (!provider) return; const { models = [] } = await System.customModels(provider); - if (PROVIDER_DEFAULT_MODELS.hasOwnProperty(provider)) + if ( + PROVIDER_DEFAULT_MODELS.hasOwnProperty(provider) && + !groupedProviders.includes(provider) + ) setDefaultModels(PROVIDER_DEFAULT_MODELS[provider]); - provider === "togetherai" + groupedProviders.includes(provider) ? setCustomModels(groupModels(models)) : setCustomModels(models); setLoading(false); diff --git a/server/utils/AiProviders/openAi/index.js b/server/utils/AiProviders/openAi/index.js index d4dc14dc6..e09e11c21 100644 --- a/server/utils/AiProviders/openAi/index.js +++ b/server/utils/AiProviders/openAi/index.js @@ -46,32 +46,28 @@ class OpenAiLLM { promptWindowLimit() { switch (this.model) { case "gpt-3.5-turbo": - return 4096; case "gpt-3.5-turbo-1106": - return 16385; - case "gpt-4": - return 8192; + return 16_385; + case "gpt-4-turbo": case "gpt-4-1106-preview": - return 128000; case "gpt-4-turbo-preview": - return 128000; + return 128_000; + case "gpt-4": + return 8_192; case "gpt-4-32k": - return 32000; + return 32_000; default: - return 4096; // assume a fine-tune 3.5 + return 4_096; // assume a fine-tune 3.5? } } + // Short circuit if name has 'gpt' since we now fetch models from OpenAI API + // via the user API key, so the model must be relevant and real. + // and if somehow it is not, chat will fail but that is caught. + // we don't want to hit the OpenAI api every chat because it will get spammed + // and introduce latency for no reason. async isValidChatCompletionModel(modelName = "") { - const validModels = [ - "gpt-4", - "gpt-3.5-turbo", - "gpt-3.5-turbo-1106", - "gpt-4-1106-preview", - "gpt-4-turbo-preview", - "gpt-4-32k", - ]; - const isPreset = validModels.some((model) => modelName === model); + const isPreset = modelName.toLowerCase().includes("gpt"); if (isPreset) return true; const model = await this.openai diff --git a/server/utils/helpers/customModels.js b/server/utils/helpers/customModels.js index 5dfa30e31..d0d162c4a 100644 --- a/server/utils/helpers/customModels.js +++ b/server/utils/helpers/customModels.js @@ -47,21 +47,85 @@ async function openAiModels(apiKey = null) { apiKey: apiKey || process.env.OPEN_AI_KEY, }); const openai = new OpenAIApi(config); - const models = ( - await openai - .listModels() - .then((res) => res.data.data) - .catch((e) => { - console.error(`OpenAI:listModels`, e.message); - return []; - }) - ).filter( - (model) => !model.owned_by.includes("openai") && model.owned_by !== "system" - ); + const allModels = await openai + .listModels() + .then((res) => res.data.data) + .catch((e) => { + console.error(`OpenAI:listModels`, e.message); + return [ + { + name: "gpt-3.5-turbo", + id: "gpt-3.5-turbo", + object: "model", + created: 1677610602, + owned_by: "openai", + organization: "OpenAi", + }, + { + name: "gpt-4", + id: "gpt-4", + object: "model", + created: 1687882411, + owned_by: "openai", + organization: "OpenAi", + }, + { + name: "gpt-4-turbo", + id: "gpt-4-turbo", + object: "model", + created: 1712361441, + owned_by: "system", + organization: "OpenAi", + }, + { + name: "gpt-4-32k", + id: "gpt-4-32k", + object: "model", + created: 1687979321, + owned_by: "openai", + organization: "OpenAi", + }, + { + name: "gpt-3.5-turbo-16k", + id: "gpt-3.5-turbo-16k", + object: "model", + created: 1683758102, + owned_by: "openai-internal", + organization: "OpenAi", + }, + ]; + }); + + const gpts = allModels + .filter((model) => model.id.startsWith("gpt")) + .filter( + (model) => !model.id.includes("vision") && !model.id.includes("instruct") + ) + .map((model) => { + return { + ...model, + name: model.id, + organization: "OpenAi", + }; + }); + + const customModels = allModels + .filter( + (model) => + !model.owned_by.includes("openai") && model.owned_by !== "system" + ) + .map((model) => { + return { + ...model, + name: model.id, + organization: "Your Fine-Tunes", + }; + }); // Api Key was successful so lets save it for future uses - if (models.length > 0 && !!apiKey) process.env.OPEN_AI_KEY = apiKey; - return { models, error: null }; + if ((gpts.length > 0 || customModels.length > 0) && !!apiKey) + process.env.OPEN_AI_KEY = apiKey; + return { models: [...gpts, ...customModels], error: null }; } async function localAIModels(basePath = null, apiKey = null) { From f9ac27e9a40cd6bcabbefde79a18f9d49321b1a3 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Tue, 16 Apr 2024 16:25:32 -0700 Subject: [PATCH 07/10] Handle Anthropic streamable errors (#1113) --- server/utils/AiProviders/anthropic/index.js | 24 ++++++++++++++++++- server/utils/chats/stream.js | 26 ++++++++++++++------- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/server/utils/AiProviders/anthropic/index.js b/server/utils/AiProviders/anthropic/index.js index 5e8e40a30..6a8ad3c42 100644 --- a/server/utils/AiProviders/anthropic/index.js +++ b/server/utils/AiProviders/anthropic/index.js @@ -138,7 +138,7 @@ class AnthropicLLM { async streamGetChatCompletion(messages = null, { temperature = 0.7 }) { if (!this.isValidChatCompletionModel(this.model)) throw new Error( - `OpenAI chat: ${this.model} is not valid for chat completion!` + `Anthropic chat: ${this.model} is not valid for chat completion!` ); const streamRequest = await this.anthropic.messages.stream({ @@ -163,6 +163,28 @@ class AnthropicLLM { const handleAbort = () => clientAbortedHandler(resolve, fullText); response.on("close", handleAbort); + stream.on("error", (event) => { + const parseErrorMsg = (event) => { + const error = event?.error?.error; + if (!!error) + return `Anthropic Error:${error?.type || "unknown"} ${ + error?.message || "unknown error." + }`; + return event.message; + }; + + writeResponseChunk(response, { + uuid, + sources: [], + type: "abort", + textResponse: null, + close: true, + error: parseErrorMsg(event), + }); + response.removeListener("close", handleAbort); + resolve(fullText); + }); + stream.on("streamEvent", (message) => { const data = message; if ( diff --git a/server/utils/chats/stream.js b/server/utils/chats/stream.js index b5128c193..57025da30 100644 --- a/server/utils/chats/stream.js +++ b/server/utils/chats/stream.js @@ -205,20 +205,30 @@ async function streamChatWithWorkspace( }); } - const { chat } = await WorkspaceChats.new({ - workspaceId: workspace.id, - prompt: message, - response: { text: completeText, sources, type: chatMode }, - threadId: thread?.id || null, - user, - }); + if (completeText?.length > 0) { + const { chat } = await WorkspaceChats.new({ + workspaceId: workspace.id, + prompt: message, + response: { text: completeText, sources, type: chatMode }, + threadId: thread?.id || null, + user, + }); + + writeResponseChunk(response, { + uuid, + type: "finalizeResponseStream", + close: true, + error: false, + chatId: chat.id, + }); + return; + } writeResponseChunk(response, { uuid, type: "finalizeResponseStream", close: true, error: false, - chatId: chat.id, }); return; } From 8ebe1a515b892abd7b69530fcb888ef8fe09b159 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Tue, 16 Apr 2024 16:42:06 -0700 Subject: [PATCH 08/10] Gracefully handle bad agent auth (#1115) use provider that is set --- server/utils/agents/aibitat/plugins/websocket.js | 8 +++++++- server/utils/agents/aibitat/providers/anthropic.js | 6 +++++- server/utils/agents/aibitat/providers/openai.js | 7 +++++-- server/utils/agents/index.js | 4 ++-- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/server/utils/agents/aibitat/plugins/websocket.js b/server/utils/agents/aibitat/plugins/websocket.js index af691ca55..b6154984d 100644 --- a/server/utils/agents/aibitat/plugins/websocket.js +++ b/server/utils/agents/aibitat/plugins/websocket.js @@ -48,7 +48,13 @@ const websocket = { name: this.name, setup(aibitat) { aibitat.onError(async (error) => { - console.error(chalk.red(` error: ${error?.message}`)); + if (!!error?.message) { + console.error(chalk.red(` error: ${error.message}`)); + aibitat.introspect( + `Error encountered while running: ${error.message}` + ); + } + if (error instanceof RetryError) { console.error(chalk.red(` retrying in 60 seconds...`)); setTimeout(() => { diff --git a/server/utils/agents/aibitat/providers/anthropic.js b/server/utils/agents/aibitat/providers/anthropic.js index d160d9ab6..5189dc2ef 100644 --- a/server/utils/agents/aibitat/providers/anthropic.js +++ b/server/utils/agents/aibitat/providers/anthropic.js @@ -117,10 +117,14 @@ class AnthropicProvider extends Provider { cost, }; } catch (error) { + // If invalid Auth error we need to abort because no amount of waiting + // will make auth better. + if (error instanceof Anthropic.AuthenticationError) throw error; + if ( error instanceof Anthropic.RateLimitError || error instanceof Anthropic.InternalServerError || - error instanceof Anthropic.APIError + error instanceof Anthropic.APIError // Also will catch AuthenticationError!!! ) { throw new RetryError(error.message); } diff --git a/server/utils/agents/aibitat/providers/openai.js b/server/utils/agents/aibitat/providers/openai.js index 82cd7741e..4458afe8f 100644 --- a/server/utils/agents/aibitat/providers/openai.js +++ b/server/utils/agents/aibitat/providers/openai.js @@ -102,11 +102,14 @@ class OpenAIProvider extends Provider { cost, }; } catch (error) { - console.log(error); + // If invalid Auth error we need to abort because no amount of waiting + // will make auth better. + if (error instanceof OpenAI.AuthenticationError) throw error; + if ( error instanceof OpenAI.RateLimitError || error instanceof OpenAI.InternalServerError || - error instanceof OpenAI.APIError + error instanceof OpenAI.APIError // Also will catch AuthenticationError!!! ) { throw new RetryError(error.message); } diff --git a/server/utils/agents/index.js b/server/utils/agents/index.js index ff66c982b..dd42a6b99 100644 --- a/server/utils/agents/index.js +++ b/server/utils/agents/index.js @@ -157,8 +157,8 @@ class AgentHandler { } ) { this.aibitat = new AIbitat({ - provider: "openai", - model: "gpt-3.5-turbo", + provider: this.provider ?? "openai", + model: this.model ?? "gpt-3.5-turbo", chats: await this.#chatHistory(20), handlerProps: { invocation: this.invocation, From a025dfd76ef49861f7eb3b778eff40128789d587 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Tue, 16 Apr 2024 19:44:32 -0700 Subject: [PATCH 09/10] hide anthropic agent support due to incompatibilty for now --- .../ChatContainer/PromptInput/AgentMenu/index.jsx | 4 ++-- .../AgentConfig/AgentLLMSelection/index.jsx | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/AgentMenu/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/AgentMenu/index.jsx index ef73cb656..8407c68db 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/AgentMenu/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/AgentMenu/index.jsx @@ -162,8 +162,8 @@ function FirstTimeAgentUser() { more.

- Currently, agents only work with OpenAI and Anthropic as your - agent LLM. All providers will be supported in the future. + Currently, agents only work with OpenAI as your agent LLM. All + LLM providers will be supported in the future.

This feature is currently early access and fully custom agents diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx index f1b997470..af6ae7549 100644 --- a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx @@ -5,7 +5,10 @@ import { AVAILABLE_LLM_PROVIDERS } from "@/pages/GeneralSettings/LLMPreference"; import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react"; import AgentModelSelection from "../AgentModelSelection"; -const ENABLED_PROVIDERS = ["openai", "anthropic"]; +const ENABLED_PROVIDERS = [ + "openai", + // "anthropic" +]; const LLM_DEFAULT = { name: "Please make a selection", From 9449fcd7379594fdedde51d97d3fc1831b046506 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Wed, 17 Apr 2024 11:54:58 -0700 Subject: [PATCH 10/10] Add Anthropic agent support with new API and tool_calling (#1116) * Add Anthropic agent support with new API and tool_calling * patch useProviderHook to unset default models on provider change --- frontend/src/hooks/useGetProvidersModels.js | 6 +- .../AgentConfig/AgentLLMSelection/index.jsx | 5 +- .../agents/aibitat/providers/anthropic.js | 237 +++++++++++------- server/utils/agents/index.js | 1 + 4 files changed, 153 insertions(+), 96 deletions(-) diff --git a/frontend/src/hooks/useGetProvidersModels.js b/frontend/src/hooks/useGetProvidersModels.js index 513bfdbe1..5dc5cd2ed 100644 --- a/frontend/src/hooks/useGetProvidersModels.js +++ b/frontend/src/hooks/useGetProvidersModels.js @@ -47,8 +47,12 @@ export default function useGetProviderModels(provider = null) { if ( PROVIDER_DEFAULT_MODELS.hasOwnProperty(provider) && !groupedProviders.includes(provider) - ) + ) { setDefaultModels(PROVIDER_DEFAULT_MODELS[provider]); + } else { + setDefaultModels([]); + } + groupedProviders.includes(provider) ? setCustomModels(groupModels(models)) : setCustomModels(models); diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx index af6ae7549..f1b997470 100644 --- a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx @@ -5,10 +5,7 @@ import { AVAILABLE_LLM_PROVIDERS } from "@/pages/GeneralSettings/LLMPreference"; import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react"; import AgentModelSelection from "../AgentModelSelection"; -const ENABLED_PROVIDERS = [ - "openai", - // "anthropic" -]; +const ENABLED_PROVIDERS = ["openai", "anthropic"]; const LLM_DEFAULT = { name: "Please make a selection", diff --git a/server/utils/agents/aibitat/providers/anthropic.js b/server/utils/agents/aibitat/providers/anthropic.js index 5189dc2ef..8d7e40ed7 100644 --- a/server/utils/agents/aibitat/providers/anthropic.js +++ b/server/utils/agents/aibitat/providers/anthropic.js @@ -25,6 +25,90 @@ class AnthropicProvider extends Provider { this.model = model; } + // For Anthropic we will always need to ensure the message sequence is role,content + // as we can attach any data to message nodes and this keeps the message property + // sent to the API always in spec. + #sanitize(chats) { + const sanitized = [...chats]; + + // If the first message is not a USER, Anthropic will abort so keep shifting the + // message array until that is the case. + while (sanitized.length > 0 && sanitized[0].role !== "user") + sanitized.shift(); + + return sanitized.map((msg) => { + const { role, content } = msg; + return { role, content }; + }); + } + + #normalizeChats(messages = []) { + if (!messages.length) return messages; + const normalized = []; + + [...messages].forEach((msg, i) => { + if (msg.role !== "function") return normalized.push(msg); + + // If the last message is a role "function" this is our special aibitat message node. + // and we need to remove it from the array of messages. + // Since Anthropic needs to have the tool call resolved, we look at the previous chat to "function" + // and go through its content "thought" from ~ln:143 and get the tool_call id so we can resolve + // this tool call properly. + const functionCompletion = msg; + const toolCallId = messages[i - 1]?.content?.find( + (msg) => msg.type === "tool_use" + )?.id; + + // Append the Anthropic acceptable node to the message chain so function can resolve. + normalized.push({ + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: toolCallId, + content: functionCompletion.content, + }, + ], + }); + }); + return normalized; + } + + // Anthropic handles system message as a property, so here we split the system message prompt + // from all the chats and then normalize them so they will be useable in case of tool_calls or general chat. + #parseSystemPrompt(messages = []) { + const chats = []; + let systemPrompt = + "You are a helpful ai assistant who can assist the user and use tools available to help answer the users prompts and questions."; + for (const msg of messages) { + if (msg.role === "system") { + systemPrompt = msg.content; + continue; + } + chats.push(msg); + } + + return [systemPrompt, this.#normalizeChats(chats)]; + } + + // Anthropic does not use the regular schema for functions so here we need to ensure it is in there specific format + // so that the call can run correctly. + #formatFunctions(functions = []) { + return functions.map((func) => { + const { name, description, parameters, required } = func; + const { type, properties } = parameters; + return { + name, + description, + input_schema: { + type, + properties, + required, + }, + }; + }); + } + /** * Create a completion based on the received messages. * @@ -32,89 +116,78 @@ class AnthropicProvider extends Provider { * @param functions * @returns The completion. */ - async complete(messages, functions) { - // clone messages to avoid mutating the original array - const promptMessages = [...messages]; - - if (functions) { - const functionPrompt = this.getFunctionPrompt(functions); - - // add function prompt after the first message - promptMessages.splice(1, 0, { - content: functionPrompt, - role: "system", - }); - } - - const prompt = promptMessages - .map((message) => { - const { content, role } = message; - - switch (role) { - case "system": - return content - ? `${Anthropic.HUMAN_PROMPT} ${content}` - : ""; - - case "function": - case "user": - return `${Anthropic.HUMAN_PROMPT} ${content}`; - - case "assistant": - return `${Anthropic.AI_PROMPT} ${content}`; - - default: - return content; - } - }) - .filter(Boolean) - .join("\n") - .concat(` ${Anthropic.AI_PROMPT}`); - + async complete(messages, functions = null) { try { - const response = await this.client.completions.create({ - model: this.model, - max_tokens_to_sample: 3000, - stream: false, - prompt, - }); + const [systemPrompt, chats] = this.#parseSystemPrompt(messages); + const response = await this.client.messages.create( + { + model: this.model, + max_tokens: 4096, + system: systemPrompt, + messages: this.#sanitize(chats), + stream: false, + ...(Array.isArray(functions) && functions?.length > 0 + ? { tools: this.#formatFunctions(functions) } + : {}), + }, + { headers: { "anthropic-beta": "tools-2024-04-04" } } // Required to we can use tools. + ); - const result = response.completion.trim(); - // TODO: get cost from response - const cost = 0; + // We know that we need to call a tool. So we are about to recurse through completions/handleExecution + // https://docs.anthropic.com/claude/docs/tool-use#how-tool-use-works + if (response.stop_reason === "tool_use") { + // Get the tool call explicitly. + const toolCall = response.content.find( + (res) => res.type === "tool_use" + ); - // Handle function calls if the model returns a function call - if (result.includes("function_name") && functions) { - let functionCall; - try { - functionCall = JSON.parse(result); - } catch (error) { - // call the complete function again in case it gets a json error - return await this.complete( - [ - ...messages, - { - role: "function", - content: `You gave me this function call: ${result} but I couldn't parse it. - ${error?.message} - - Please try again.`, - }, - ], - functions - ); - } + // Here we need the chain of thought the model may or may not have generated alongside the call. + // this needs to be in a very specific format so we always ensure there is a 2-item content array + // so that we can ensure the tool_call content is correct. For anthropic all text items must not + // be empty, but the api will still return empty text so we need to make 100% sure text is not empty + // or the tool call will fail. + // wtf. + let thought = response.content.find((res) => res.type === "text"); + thought = + thought?.content?.length > 0 + ? { + role: thought.role, + content: [ + { type: "text", text: thought.content }, + { ...toolCall }, + ], + } + : { + role: "assistant", + content: [ + { + type: "text", + text: `Okay, im going to use ${toolCall.name} to help me.`, + }, + { ...toolCall }, + ], + }; + // Modify messages forcefully by adding system thought so that tool_use/tool_result + // messaging works with Anthropic's disastrous tool calling API. + messages.push(thought); + + const functionArgs = toolCall.input; return { result: null, - functionCall, - cost, + functionCall: { + name: toolCall.name, + arguments: functionArgs, + }, + cost: 0, }; } + const completion = response.content.find((msg) => msg.type === "text"); return { - result, - cost, + result: + completion?.text ?? "I could not generate a response from this.", + cost: 0, }; } catch (error) { // If invalid Auth error we need to abort because no amount of waiting @@ -132,24 +205,6 @@ class AnthropicProvider extends Provider { throw error; } } - - getFunctionPrompt(functions = []) { - const functionPrompt = `You have been trained to directly call a Javascript function passing a JSON Schema parameter as a response to this chat. This function will return a string that you can use to keep chatting. - - Here is a list of functions available to you: - ${JSON.stringify(functions, null, 2)} - - When calling any of those function in order to complete your task, respond only this JSON format. Do not include any other information or any other stuff. - - Function call format: - { - function_name: "givenfunctionname", - parameters: {} - } - `; - - return functionPrompt; - } } module.exports = AnthropicProvider; diff --git a/server/utils/agents/index.js b/server/utils/agents/index.js index dd42a6b99..ce80fff44 100644 --- a/server/utils/agents/index.js +++ b/server/utils/agents/index.js @@ -50,6 +50,7 @@ class AgentHandler { from: USER_AGENT.name, to: WORKSPACE_AGENT.name, content: chatLog.prompt, + state: "success", }, { from: WORKSPACE_AGENT.name,