Marco Beretta
commited on
Commit
•
3b6afc0
1
Parent(s):
cbb93e4
LibreChat upload repo
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .devcontainer/devcontainer.json +57 -0
- .devcontainer/docker-compose.yml +76 -0
- .dockerignore +5 -0
- .env.example +263 -0
- .eslintrc.js +136 -0
- .github/FUNDING.yml +13 -0
- .github/ISSUE_TEMPLATE/BUG-REPORT.yml +64 -0
- .github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml +57 -0
- .github/ISSUE_TEMPLATE/QUESTION.yml +58 -0
- .github/dependabot.yml +47 -0
- .github/playwright.yml +62 -0
- .github/pull_request_template.md +35 -0
- .github/wip-playwright.yml +28 -0
- .github/workflows/backend-review.yml +44 -0
- .github/workflows/build.yml +38 -0
- .github/workflows/container.yml +47 -0
- .github/workflows/deploy.yml +38 -0
- .github/workflows/frontend-review.yml +34 -0
- .github/workflows/mkdocs.yaml +24 -0
- .gitignore +78 -0
- .husky/pre-commit +5 -0
- .prettierrc.js +19 -0
- CODE_OF_CONDUCT.md +132 -0
- CONTRIBUTING.md +100 -0
- Dockerfile +26 -0
- LICENSE.md +29 -0
- README.md +148 -8
- SECURITY.md +63 -0
- api/app/bingai.js +100 -0
- api/app/chatgpt-browser.js +50 -0
- api/app/clients/AnthropicClient.js +324 -0
- api/app/clients/BaseClient.js +561 -0
- api/app/clients/ChatGPTClient.js +587 -0
- api/app/clients/GoogleClient.js +280 -0
- api/app/clients/OpenAIClient.js +369 -0
- api/app/clients/PluginsClient.js +569 -0
- api/app/clients/TextStream.js +59 -0
- api/app/clients/agents/CustomAgent/CustomAgent.js +50 -0
- api/app/clients/agents/CustomAgent/initializeCustomAgent.js +54 -0
- api/app/clients/agents/CustomAgent/instructions.js +203 -0
- api/app/clients/agents/CustomAgent/outputParser.js +218 -0
- api/app/clients/agents/Functions/FunctionsAgent.js +120 -0
- api/app/clients/agents/Functions/initializeFunctionsAgent.js +28 -0
- api/app/clients/agents/index.js +7 -0
- api/app/clients/index.js +17 -0
- api/app/clients/prompts/instructions.js +10 -0
- api/app/clients/prompts/refinePrompt.js +24 -0
- api/app/clients/specs/BaseClient.test.js +369 -0
- api/app/clients/specs/FakeClient.js +193 -0
- api/app/clients/specs/OpenAIClient.test.js +211 -0
.devcontainer/devcontainer.json
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// {
|
2 |
+
// "name": "LibreChat_dev",
|
3 |
+
// // Update the 'dockerComposeFile' list if you have more compose files or use different names.
|
4 |
+
// "dockerComposeFile": "docker-compose.yml",
|
5 |
+
// // The 'service' property is the name of the service for the container that VS Code should
|
6 |
+
// // use. Update this value and .devcontainer/docker-compose.yml to the real service name.
|
7 |
+
// "service": "librechat",
|
8 |
+
// // The 'workspaceFolder' property is the path VS Code should open by default when
|
9 |
+
// // connected. Corresponds to a volume mount in .devcontainer/docker-compose.yml
|
10 |
+
// "workspaceFolder": "/workspace"
|
11 |
+
// //,
|
12 |
+
// // // Set *default* container specific settings.json values on container create.
|
13 |
+
// // "settings": {},
|
14 |
+
// // // Add the IDs of extensions you want installed when the container is created.
|
15 |
+
// // "extensions": [],
|
16 |
+
// // Uncomment the next line if you want to keep your containers running after VS Code shuts down.
|
17 |
+
// // "shutdownAction": "none",
|
18 |
+
// // Uncomment the next line to use 'postCreateCommand' to run commands after the container is created.
|
19 |
+
// // "postCreateCommand": "uname -a",
|
20 |
+
// // Comment out to connect as root instead. To add a non-root user, see: https://aka.ms/vscode-remote/containers/non-root.
|
21 |
+
// // "remoteUser": "vscode"
|
22 |
+
// }
|
23 |
+
{
|
24 |
+
// "name": "LibreChat_dev",
|
25 |
+
"dockerComposeFile": "docker-compose.yml",
|
26 |
+
"service": "app",
|
27 |
+
// "image": "node:19-alpine",
|
28 |
+
// "workspaceFolder": "/workspaces",
|
29 |
+
"workspaceFolder": "/workspace",
|
30 |
+
// Set *default* container specific settings.json values on container create.
|
31 |
+
// "overrideCommand": true,
|
32 |
+
"customizations": {
|
33 |
+
"vscode": {
|
34 |
+
"extensions": [],
|
35 |
+
"settings": {
|
36 |
+
"terminal.integrated.profiles.linux": {
|
37 |
+
"bash": null
|
38 |
+
}
|
39 |
+
}
|
40 |
+
}
|
41 |
+
},
|
42 |
+
"postCreateCommand": ""
|
43 |
+
// "workspaceMount": "src=${localWorkspaceFolder},dst=/code,type=bind,consistency=cached"
|
44 |
+
|
45 |
+
// "runArgs": [
|
46 |
+
// "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined",
|
47 |
+
// "-v", "/tmp/.X11-unix:/tmp/.X11-unix",
|
48 |
+
// "-v", "${env:XAUTHORITY}:/root/.Xauthority:rw",
|
49 |
+
// "-v", "/home/${env:USER}/.cdh:/root/.cdh",
|
50 |
+
// "-e", "DISPLAY=${env:DISPLAY}",
|
51 |
+
// "--name=tgw_assistant_backend_dev",
|
52 |
+
// "--network=host"
|
53 |
+
// ],
|
54 |
+
// "settings": {
|
55 |
+
// "terminal.integrated.shell.linux": "/bin/bash"
|
56 |
+
// },
|
57 |
+
}
|
.devcontainer/docker-compose.yml
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
version: '3.4'
|
2 |
+
|
3 |
+
services:
|
4 |
+
app:
|
5 |
+
# container_name: LibreChat_dev
|
6 |
+
image: node:19-alpine
|
7 |
+
# Using a Dockerfile is optional, but included for completeness.
|
8 |
+
# build:
|
9 |
+
# context: .
|
10 |
+
# dockerfile: Dockerfile
|
11 |
+
# # [Optional] You can use build args to set options. e.g. 'VARIANT' below affects the image in the Dockerfile
|
12 |
+
# args:
|
13 |
+
# VARIANT: buster
|
14 |
+
network_mode: "host"
|
15 |
+
# ports:
|
16 |
+
# - 3080:3080 # Change it to 9000:3080 to use nginx
|
17 |
+
extra_hosts: # if you are running APIs on docker you need access to, you will need to uncomment this line and next
|
18 |
+
- "host.docker.internal:host-gateway"
|
19 |
+
|
20 |
+
volumes:
|
21 |
+
# # This is where VS Code should expect to find your project's source code and the value of "workspaceFolder" in .devcontainer/devcontainer.json
|
22 |
+
- ..:/workspace:cached
|
23 |
+
# # - /app/client/node_modules
|
24 |
+
# # - ./api:/app/api
|
25 |
+
# # - ./.env:/app/.env
|
26 |
+
# # - ./.env.development:/app/.env.development
|
27 |
+
# # - ./.env.production:/app/.env.production
|
28 |
+
# # - /app/api/node_modules
|
29 |
+
|
30 |
+
# # Uncomment the next line to use Docker from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker-compose for details.
|
31 |
+
# # - /var/run/docker.sock:/var/run/docker.sock
|
32 |
+
|
33 |
+
# Runs app on the same network as the service container, allows "forwardPorts" in devcontainer.json function.
|
34 |
+
# network_mode: service:another-service
|
35 |
+
|
36 |
+
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
37 |
+
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
38 |
+
|
39 |
+
# Uncomment the next line to use a non-root user for all processes - See https://aka.ms/vscode-remote/containers/non-root for details.
|
40 |
+
# user: vscode
|
41 |
+
|
42 |
+
# Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust.
|
43 |
+
# cap_add:
|
44 |
+
# - SYS_PTRACE
|
45 |
+
# security_opt:
|
46 |
+
# - seccomp:unconfined
|
47 |
+
|
48 |
+
# Overrides default command so things don't shut down after the process ends.
|
49 |
+
command: /bin/sh -c "while sleep 1000; do :; done"
|
50 |
+
|
51 |
+
mongodb:
|
52 |
+
container_name: chat-mongodb
|
53 |
+
network_mode: "host"
|
54 |
+
# ports:
|
55 |
+
# - 27018:27017
|
56 |
+
image: mongo
|
57 |
+
# restart: always
|
58 |
+
volumes:
|
59 |
+
- ./data-node:/data/db
|
60 |
+
command: mongod --noauth
|
61 |
+
meilisearch:
|
62 |
+
container_name: chat-meilisearch
|
63 |
+
image: getmeili/meilisearch:v1.0
|
64 |
+
network_mode: "host"
|
65 |
+
# ports:
|
66 |
+
# - 7700:7700
|
67 |
+
# env_file:
|
68 |
+
# - .env
|
69 |
+
environment:
|
70 |
+
- SEARCH=false
|
71 |
+
- MEILI_HOST=http://0.0.0.0:7700
|
72 |
+
- MEILI_HTTP_ADDR=0.0.0.0:7700
|
73 |
+
- MEILI_MASTER_KEY=5c71cf56d672d009e36070b5bc5e47b743535ae55c818ae3b735bb6ebfb4ba63
|
74 |
+
volumes:
|
75 |
+
- ./meili_data:/meili_data
|
76 |
+
|
.dockerignore
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
**/node_modules
|
2 |
+
client/dist/images
|
3 |
+
data-node
|
4 |
+
.env
|
5 |
+
**/.env
|
.env.example
ADDED
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
##########################
|
2 |
+
# Server configuration:
|
3 |
+
##########################
|
4 |
+
|
5 |
+
APP_TITLE=LibreChat
|
6 |
+
|
7 |
+
# The server will listen to localhost:3080 by default. You can change the target IP as you want.
|
8 |
+
# If you want to make this server available externally, for example to share the server with others
|
9 |
+
# or expose this from a Docker container, set host to 0.0.0.0 or your external IP interface.
|
10 |
+
# Tips: Setting host to 0.0.0.0 means listening on all interfaces. It's not a real IP.
|
11 |
+
# Use localhost:port rather than 0.0.0.0:port to access the server.
|
12 |
+
# Set Node env to development if running in dev mode.
|
13 |
+
HOST=localhost
|
14 |
+
PORT=3080
|
15 |
+
|
16 |
+
# Change this to proxy any API request.
|
17 |
+
# It's useful if your machine has difficulty calling the original API server.
|
18 |
+
# PROXY=
|
19 |
+
|
20 |
+
# Change this to your MongoDB URI if different. I recommend appending LibreChat.
|
21 |
+
MONGO_URI=mongodb://127.0.0.1:27018/LibreChat
|
22 |
+
|
23 |
+
##########################
|
24 |
+
# OpenAI Endpoint:
|
25 |
+
##########################
|
26 |
+
|
27 |
+
# Access key from OpenAI platform.
|
28 |
+
# Leave it blank to disable this feature.
|
29 |
+
# Set to "user_provided" to allow the user to provide their API key from the UI.
|
30 |
+
OPENAI_API_KEY="user_provided"
|
31 |
+
|
32 |
+
# Identify the available models, separated by commas *without spaces*.
|
33 |
+
# The first will be default.
|
34 |
+
# Leave it blank to use internal settings.
|
35 |
+
# OPENAI_MODELS=gpt-3.5-turbo,gpt-3.5-turbo-16k,gpt-3.5-turbo-0301,text-davinci-003,gpt-4,gpt-4-0314,gpt-4-0613
|
36 |
+
|
37 |
+
# Reverse proxy settings for OpenAI:
|
38 |
+
# https://github.com/waylaidwanderer/node-chatgpt-api#using-a-reverse-proxy
|
39 |
+
# OPENAI_REVERSE_PROXY=
|
40 |
+
|
41 |
+
##########################
|
42 |
+
# AZURE Endpoint:
|
43 |
+
##########################
|
44 |
+
|
45 |
+
# To use Azure with this project, set the following variables. These will be used to build the API URL.
|
46 |
+
# Chat completion:
|
47 |
+
# `https://{AZURE_OPENAI_API_INSTANCE_NAME}.openai.azure.com/openai/deployments/{AZURE_OPENAI_API_DEPLOYMENT_NAME}/chat/completions?api-version={AZURE_OPENAI_API_VERSION}`;
|
48 |
+
# You should also consider changing the `OPENAI_MODELS` variable above to the models available in your instance/deployment.
|
49 |
+
# Note: I've noticed that the Azure API is much faster than the OpenAI API, so the streaming looks almost instantaneous.
|
50 |
+
# Note "AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME" and "AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME" are optional but might be used in the future
|
51 |
+
|
52 |
+
# AZURE_API_KEY=
|
53 |
+
# AZURE_OPENAI_API_INSTANCE_NAME=
|
54 |
+
# AZURE_OPENAI_API_DEPLOYMENT_NAME=
|
55 |
+
# AZURE_OPENAI_API_VERSION=
|
56 |
+
# AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME=
|
57 |
+
# AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME=
|
58 |
+
|
59 |
+
# Identify the available models, separated by commas *without spaces*.
|
60 |
+
# The first will be default.
|
61 |
+
# Leave it blank to use internal settings.
|
62 |
+
AZURE_OPENAI_MODELS=gpt-3.5-turbo,gpt-4
|
63 |
+
|
64 |
+
# To use Azure with the Plugins endpoint, you need the variables above, and uncomment the following variable:
|
65 |
+
# NOTE: This may not work as expected and Azure OpenAI may not support OpenAI Functions yet
|
66 |
+
# Omit/leave it commented to use the default OpenAI API
|
67 |
+
|
68 |
+
# PLUGINS_USE_AZURE="true"
|
69 |
+
|
70 |
+
##########################
|
71 |
+
# BingAI Endpoint:
|
72 |
+
##########################
|
73 |
+
|
74 |
+
# Also used for Sydney and jailbreak
|
75 |
+
# To get your Access token for Bing, login to https://www.bing.com
|
76 |
+
# Use dev tools or an extension while logged into the site to copy the content of the _U cookie.
|
77 |
+
#If this fails, follow these instructions https://github.com/danny-avila/LibreChat/issues/370#issuecomment-1560382302 to provide the full cookie strings.
|
78 |
+
# Set to "user_provided" to allow the user to provide its token from the UI.
|
79 |
+
# Leave it blank to disable this endpoint.
|
80 |
+
BINGAI_TOKEN="user_provided"
|
81 |
+
|
82 |
+
# BingAI Host:
|
83 |
+
# Necessary for some people in different countries, e.g. China (https://cn.bing.com)
|
84 |
+
# Leave it blank to use default server.
|
85 |
+
# BINGAI_HOST=https://cn.bing.com
|
86 |
+
|
87 |
+
##########################
|
88 |
+
# ChatGPT Endpoint:
|
89 |
+
##########################
|
90 |
+
|
91 |
+
# ChatGPT Browser Client (free but use at your own risk)
|
92 |
+
# Access token from https://chat.openai.com/api/auth/session
|
93 |
+
# Exposes your access token to `CHATGPT_REVERSE_PROXY`
|
94 |
+
# Set to "user_provided" to allow the user to provide its token from the UI.
|
95 |
+
# Leave it blank to disable this endpoint
|
96 |
+
CHATGPT_TOKEN="user_provided"
|
97 |
+
|
98 |
+
# Identify the available models, separated by commas. The first will be default.
|
99 |
+
# Leave it blank to use internal settings.
|
100 |
+
CHATGPT_MODELS=text-davinci-002-render-sha,gpt-4
|
101 |
+
# NOTE: you can add gpt-4-plugins, gpt-4-code-interpreter, and gpt-4-browsing to the list above and use the models for these features;
|
102 |
+
# however, the view/display portion of these features are not supported, but you can use the underlying models, which have higher token context
|
103 |
+
# Also: text-davinci-002-render-paid is deprecated as of May 2023
|
104 |
+
|
105 |
+
# Reverse proxy setting for OpenAI
|
106 |
+
# https://github.com/waylaidwanderer/node-chatgpt-api#using-a-reverse-proxy
|
107 |
+
# By default it will use the node-chatgpt-api recommended proxy, (it's a third party server)
|
108 |
+
# CHATGPT_REVERSE_PROXY=<YOUR REVERSE PROXY>
|
109 |
+
|
110 |
+
##########################
|
111 |
+
# Anthropic Endpoint:
|
112 |
+
##########################
|
113 |
+
# Access key from https://console.anthropic.com/
|
114 |
+
# Leave it blank to disable this feature.
|
115 |
+
# Set to "user_provided" to allow the user to provide their API key from the UI.
|
116 |
+
# Note that access to claude-1 may potentially become unavailable with the release of claude-2.
|
117 |
+
ANTHROPIC_API_KEY="user_provided"
|
118 |
+
ANTHROPIC_MODELS=claude-1,claude-instant-1,claude-2
|
119 |
+
|
120 |
+
#############################
|
121 |
+
# Plugins:
|
122 |
+
#############################
|
123 |
+
|
124 |
+
# Identify the available models, separated by commas *without spaces*.
|
125 |
+
# The first will be default.
|
126 |
+
# Leave it blank to use internal settings.
|
127 |
+
# PLUGIN_MODELS=gpt-3.5-turbo,gpt-3.5-turbo-16k,gpt-3.5-turbo-0301,gpt-4,gpt-4-0314,gpt-4-0613
|
128 |
+
|
129 |
+
# For securely storing credentials, you need a fixed key and IV. You can set them here for prod and dev environments
|
130 |
+
# If you don't set them, the app will crash on startup.
|
131 |
+
# You need a 32-byte key (64 characters in hex) and 16-byte IV (32 characters in hex)
|
132 |
+
# Use this replit to generate some quickly: https://replit.com/@daavila/crypto#index.js
|
133 |
+
# Here are some examples (THESE ARE NOT SECURE!)
|
134 |
+
CREDS_KEY=f34be427ebb29de8d88c107a71546019685ed8b241d8f2ed00c3df97ad2566f0
|
135 |
+
CREDS_IV=e2341419ec3dd3d19b13a1a87fafcbfb
|
136 |
+
|
137 |
+
|
138 |
+
# AI-Assisted Google Search
|
139 |
+
# This bot supports searching google for answers to your questions with assistance from GPT!
|
140 |
+
# See detailed instructions here: https://github.com/danny-avila/LibreChat/blob/main/docs/features/plugins/google_search.md
|
141 |
+
GOOGLE_API_KEY=
|
142 |
+
GOOGLE_CSE_ID=
|
143 |
+
|
144 |
+
# StableDiffusion WebUI
|
145 |
+
# This bot supports StableDiffusion WebUI, using it's API to generated requested images.
|
146 |
+
# See detailed instructions here: https://github.com/danny-avila/LibreChat/blob/main/docs/features/plugins/stable_diffusion.md
|
147 |
+
# Use "http://127.0.0.1:7860" with local install and "http://host.docker.internal:7860" for docker
|
148 |
+
SD_WEBUI_URL=http://host.docker.internal:7860
|
149 |
+
|
150 |
+
##########################
|
151 |
+
# PaLM (Google) Endpoint:
|
152 |
+
##########################
|
153 |
+
|
154 |
+
# Follow the instruction here to setup:
|
155 |
+
# https://github.com/danny-avila/LibreChat/blob/main/docs/install/apis_and_tokens.md
|
156 |
+
|
157 |
+
PALM_KEY="user_provided"
|
158 |
+
|
159 |
+
# In case you need a reverse proxy for this endpoint:
|
160 |
+
# GOOGLE_REVERSE_PROXY=
|
161 |
+
|
162 |
+
##########################
|
163 |
+
# Proxy: To be Used by all endpoints
|
164 |
+
##########################
|
165 |
+
|
166 |
+
PROXY=
|
167 |
+
|
168 |
+
##########################
|
169 |
+
# Search:
|
170 |
+
##########################
|
171 |
+
|
172 |
+
# ENABLING SEARCH MESSAGES/CONVOS
|
173 |
+
# Requires the installation of the free self-hosted Meilisearch or a paid Remote Plan (Remote not tested)
|
174 |
+
# The easiest setup for this is through docker-compose, which takes care of it for you.
|
175 |
+
SEARCH=true
|
176 |
+
|
177 |
+
# HIGHLY RECOMMENDED: Disable anonymized telemetry analytics for MeiliSearch for absolute privacy.
|
178 |
+
MEILI_NO_ANALYTICS=true
|
179 |
+
|
180 |
+
# REQUIRED FOR SEARCH: MeiliSearch Host, mainly for the API server to connect to the search server.
|
181 |
+
# Replace '0.0.0.0' with 'meilisearch' if serving MeiliSearch with docker-compose.
|
182 |
+
MEILI_HOST=http://0.0.0.0:7700
|
183 |
+
|
184 |
+
# REQUIRED FOR SEARCH: MeiliSearch HTTP Address, mainly for docker-compose to expose the search server.
|
185 |
+
# Replace '0.0.0.0' with 'meilisearch' if serving MeiliSearch with docker-compose.
|
186 |
+
MEILI_HTTP_ADDR=0.0.0.0:7700
|
187 |
+
|
188 |
+
# REQUIRED FOR SEARCH: In production env., a secure key is needed. You can generate your own.
|
189 |
+
# This master key must be at least 16 bytes, composed of valid UTF-8 characters.
|
190 |
+
# MeiliSearch will throw an error and refuse to launch if no master key is provided,
|
191 |
+
# or if it is under 16 bytes. MeiliSearch will suggest a secure autogenerated master key.
|
192 |
+
# Using docker, it seems recognized as production so use a secure key.
|
193 |
+
# This is a ready made secure key for docker-compose, you can replace it with your own.
|
194 |
+
MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFCt
|
195 |
+
|
196 |
+
##########################
|
197 |
+
# User System:
|
198 |
+
##########################
|
199 |
+
|
200 |
+
# Allow Public Registration
|
201 |
+
ALLOW_REGISTRATION=true
|
202 |
+
|
203 |
+
# Allow Social Registration
|
204 |
+
ALLOW_SOCIAL_LOGIN=false
|
205 |
+
|
206 |
+
# JWT Secrets
|
207 |
+
JWT_SECRET=secret
|
208 |
+
JWT_REFRESH_SECRET=secret
|
209 |
+
|
210 |
+
# Google:
|
211 |
+
# Add your Google Client ID and Secret here, you must register an app with Google Cloud to get these values
|
212 |
+
# https://cloud.google.com/
|
213 |
+
GOOGLE_CLIENT_ID=
|
214 |
+
GOOGLE_CLIENT_SECRET=
|
215 |
+
GOOGLE_CALLBACK_URL=/oauth/google/callback
|
216 |
+
|
217 |
+
# OpenID:
|
218 |
+
# See OpenID provider to get the below values
|
219 |
+
# Create random string for OPENID_SESSION_SECRET
|
220 |
+
# For Azure AD
|
221 |
+
# ISSUER: https://login.microsoftonline.com/(tenant id)/v2.0/
|
222 |
+
# SCOPE: openid profile email
|
223 |
+
OPENID_CLIENT_ID=
|
224 |
+
OPENID_CLIENT_SECRET=
|
225 |
+
OPENID_ISSUER=
|
226 |
+
OPENID_SESSION_SECRET=
|
227 |
+
OPENID_SCOPE="openid profile email"
|
228 |
+
OPENID_CALLBACK_URL=/oauth/openid/callback
|
229 |
+
# If LABEL and URL are left empty, then the default OpenID label and logo are used.
|
230 |
+
OPENID_BUTTON_LABEL=
|
231 |
+
OPENID_IMAGE_URL=
|
232 |
+
|
233 |
+
# Set the expiration delay for the secure cookie with the JWT token
|
234 |
+
# Delay is in millisecond e.g. 7 days is 1000*60*60*24*7
|
235 |
+
SESSION_EXPIRY=(1000 * 60 * 60 * 24) * 7
|
236 |
+
|
237 |
+
# Github:
|
238 |
+
# Get the Client ID and Secret from your Discord Application
|
239 |
+
# Add your Discord Client ID and Client Secret here:
|
240 |
+
|
241 |
+
GITHUB_CLIENT_ID=your_client_id
|
242 |
+
GITHUB_CLIENT_SECRET=your_client_secret
|
243 |
+
GITHUB_CALLBACK_URL=/oauth/github/callback # this should be the same for everyone
|
244 |
+
|
245 |
+
# Discord:
|
246 |
+
# Get the Client ID and Secret from your Discord Application
|
247 |
+
# Add your Github Client ID and Client Secret here:
|
248 |
+
|
249 |
+
DISCORD_CLIENT_ID=your_client_id
|
250 |
+
DISCORD_CLIENT_SECRET=your_client_secret
|
251 |
+
DISCORD_CALLBACK_URL=/oauth/discord/callback # this should be the same for everyone
|
252 |
+
|
253 |
+
###########################
|
254 |
+
# Application Domains
|
255 |
+
###########################
|
256 |
+
|
257 |
+
# Note:
|
258 |
+
# Server = Backend
|
259 |
+
# Client = Public (the client is the url you visit)
|
260 |
+
# For the Google login to work in dev mode, you will need to change DOMAIN_SERVER to localhost:3090 or place it in .env.development
|
261 |
+
|
262 |
+
DOMAIN_CLIENT=http://localhost:3080
|
263 |
+
DOMAIN_SERVER=http://localhost:3080
|
.eslintrc.js
ADDED
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
module.exports = {
|
2 |
+
env: {
|
3 |
+
browser: true,
|
4 |
+
es2021: true,
|
5 |
+
node: true,
|
6 |
+
commonjs: true,
|
7 |
+
es6: true,
|
8 |
+
},
|
9 |
+
extends: [
|
10 |
+
'eslint:recommended',
|
11 |
+
'plugin:react/recommended',
|
12 |
+
'plugin:react-hooks/recommended',
|
13 |
+
'plugin:jest/recommended',
|
14 |
+
'prettier',
|
15 |
+
],
|
16 |
+
// ignorePatterns: ['packages/data-provider/types/**/*'],
|
17 |
+
ignorePatterns: [
|
18 |
+
'client/dist/**/*',
|
19 |
+
'client/public/**/*',
|
20 |
+
'e2e/playwright-report/**/*',
|
21 |
+
'packages/data-provider/types/**/*',
|
22 |
+
'packages/data-provider/dist/**/*',
|
23 |
+
],
|
24 |
+
parser: '@typescript-eslint/parser',
|
25 |
+
parserOptions: {
|
26 |
+
ecmaVersion: 'latest',
|
27 |
+
sourceType: 'module',
|
28 |
+
ecmaFeatures: {
|
29 |
+
jsx: true,
|
30 |
+
},
|
31 |
+
},
|
32 |
+
plugins: ['react', 'react-hooks', '@typescript-eslint'],
|
33 |
+
rules: {
|
34 |
+
'react/react-in-jsx-scope': 'off',
|
35 |
+
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow' }],
|
36 |
+
indent: ['error', 2, { SwitchCase: 1 }],
|
37 |
+
'max-len': [
|
38 |
+
'error',
|
39 |
+
{
|
40 |
+
code: 120,
|
41 |
+
ignoreStrings: true,
|
42 |
+
ignoreTemplateLiterals: true,
|
43 |
+
ignoreComments: true,
|
44 |
+
},
|
45 |
+
],
|
46 |
+
'linebreak-style': 0,
|
47 |
+
'curly': ['error', 'all'],
|
48 |
+
'semi': ['error', 'always'],
|
49 |
+
'no-trailing-spaces': 'error',
|
50 |
+
'object-curly-spacing': ['error', 'always'],
|
51 |
+
'no-multiple-empty-lines': ['error', { max: 1 }],
|
52 |
+
'comma-dangle': ['error', 'always-multiline'],
|
53 |
+
// "arrow-parens": [2, "as-needed", { requireForBlockBody: true }],
|
54 |
+
// 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
|
55 |
+
'no-console': 'off',
|
56 |
+
'import/extensions': 'off',
|
57 |
+
'no-promise-executor-return': 'off',
|
58 |
+
'no-param-reassign': 'off',
|
59 |
+
'no-continue': 'off',
|
60 |
+
'no-restricted-syntax': 'off',
|
61 |
+
'react/prop-types': ['off'],
|
62 |
+
'react/display-name': ['off'],
|
63 |
+
quotes: ['error', 'single'],
|
64 |
+
},
|
65 |
+
overrides: [
|
66 |
+
{
|
67 |
+
files: ['**/*.ts', '**/*.tsx'],
|
68 |
+
rules: {
|
69 |
+
'no-unused-vars': 'off', // off because it conflicts with '@typescript-eslint/no-unused-vars'
|
70 |
+
'react/display-name': 'off',
|
71 |
+
'@typescript-eslint/no-unused-vars': 'warn',
|
72 |
+
},
|
73 |
+
},
|
74 |
+
{
|
75 |
+
files: ['rollup.config.js', '.eslintrc.js', 'jest.config.js'],
|
76 |
+
env: {
|
77 |
+
node: true,
|
78 |
+
},
|
79 |
+
},
|
80 |
+
{
|
81 |
+
files: [
|
82 |
+
'**/*.test.js',
|
83 |
+
'**/*.test.jsx',
|
84 |
+
'**/*.test.ts',
|
85 |
+
'**/*.test.tsx',
|
86 |
+
'**/*.spec.js',
|
87 |
+
'**/*.spec.jsx',
|
88 |
+
'**/*.spec.ts',
|
89 |
+
'**/*.spec.tsx',
|
90 |
+
'setupTests.js',
|
91 |
+
],
|
92 |
+
env: {
|
93 |
+
jest: true,
|
94 |
+
node: true,
|
95 |
+
},
|
96 |
+
rules: {
|
97 |
+
'react/display-name': 'off',
|
98 |
+
'react/prop-types': 'off',
|
99 |
+
'react/no-unescaped-entities': 'off',
|
100 |
+
},
|
101 |
+
},
|
102 |
+
{
|
103 |
+
files: '**/*.+(ts)',
|
104 |
+
parser: '@typescript-eslint/parser',
|
105 |
+
parserOptions: {
|
106 |
+
project: './client/tsconfig.json',
|
107 |
+
},
|
108 |
+
plugins: ['@typescript-eslint/eslint-plugin', 'jest'],
|
109 |
+
extends: [
|
110 |
+
'plugin:@typescript-eslint/eslint-recommended',
|
111 |
+
'plugin:@typescript-eslint/recommended',
|
112 |
+
],
|
113 |
+
},
|
114 |
+
{
|
115 |
+
files: './packages/data-provider/**/*.ts',
|
116 |
+
overrides: [
|
117 |
+
{
|
118 |
+
files: '**/*.ts',
|
119 |
+
parser: '@typescript-eslint/parser',
|
120 |
+
parserOptions: {
|
121 |
+
project: './packages/data-provider/tsconfig.json',
|
122 |
+
},
|
123 |
+
},
|
124 |
+
],
|
125 |
+
},
|
126 |
+
],
|
127 |
+
settings: {
|
128 |
+
react: {
|
129 |
+
createClass: 'createReactClass', // Regex for Component Factory to use,
|
130 |
+
// default to "createReactClass"
|
131 |
+
pragma: 'React', // Pragma to use, default to "React"
|
132 |
+
fragment: 'Fragment', // Fragment to use (may be a property of <pragma>), default to "Fragment"
|
133 |
+
version: 'detect', // React version. "detect" automatically picks the version you have installed.
|
134 |
+
},
|
135 |
+
},
|
136 |
+
};
|
.github/FUNDING.yml
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# These are supported funding model platforms
|
2 |
+
|
3 |
+
github: [danny-avila]
|
4 |
+
patreon: # Replace with a single Patreon username
|
5 |
+
open_collective: # Replace with a single Open Collective username
|
6 |
+
ko_fi: # Replace with a single Ko-fi username
|
7 |
+
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
8 |
+
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
9 |
+
liberapay: # Replace with a single Liberapay username
|
10 |
+
issuehunt: # Replace with a single IssueHunt username
|
11 |
+
otechie: # Replace with a single Otechie username
|
12 |
+
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
13 |
+
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Bug Report
|
2 |
+
description: File a bug report
|
3 |
+
title: "[Bug]: "
|
4 |
+
labels: ["bug"]
|
5 |
+
body:
|
6 |
+
- type: markdown
|
7 |
+
attributes:
|
8 |
+
value: |
|
9 |
+
Thanks for taking the time to fill out this bug report!
|
10 |
+
- type: input
|
11 |
+
id: contact
|
12 |
+
attributes:
|
13 |
+
label: Contact Details
|
14 |
+
description: How can we get in touch with you if we need more info?
|
15 |
+
placeholder: ex. [email protected]
|
16 |
+
validations:
|
17 |
+
required: false
|
18 |
+
- type: textarea
|
19 |
+
id: what-happened
|
20 |
+
attributes:
|
21 |
+
label: What happened?
|
22 |
+
description: Also tell us, what did you expect to happen?
|
23 |
+
placeholder: Please give as many details as possible
|
24 |
+
validations:
|
25 |
+
required: true
|
26 |
+
- type: textarea
|
27 |
+
id: steps-to-reproduce
|
28 |
+
attributes:
|
29 |
+
label: Steps to Reproduce
|
30 |
+
description: Please list the steps needed to reproduce the issue.
|
31 |
+
placeholder: "1. Step 1\n2. Step 2\n3. Step 3"
|
32 |
+
validations:
|
33 |
+
required: true
|
34 |
+
- type: dropdown
|
35 |
+
id: browsers
|
36 |
+
attributes:
|
37 |
+
label: What browsers are you seeing the problem on?
|
38 |
+
multiple: true
|
39 |
+
options:
|
40 |
+
- Firefox
|
41 |
+
- Chrome
|
42 |
+
- Safari
|
43 |
+
- Microsoft Edge
|
44 |
+
- Mobile (iOS)
|
45 |
+
- Mobile (Android)
|
46 |
+
- type: textarea
|
47 |
+
id: logs
|
48 |
+
attributes:
|
49 |
+
label: Relevant log output
|
50 |
+
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
51 |
+
render: shell
|
52 |
+
- type: textarea
|
53 |
+
id: screenshots
|
54 |
+
attributes:
|
55 |
+
label: Screenshots
|
56 |
+
description: If applicable, add screenshots to help explain your problem. You can drag and drop, paste images directly here or link to them.
|
57 |
+
- type: checkboxes
|
58 |
+
id: terms
|
59 |
+
attributes:
|
60 |
+
label: Code of Conduct
|
61 |
+
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/CODE_OF_CONDUCT.md)
|
62 |
+
options:
|
63 |
+
- label: I agree to follow this project's Code of Conduct
|
64 |
+
required: true
|
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Feature Request
|
2 |
+
description: File a feature request
|
3 |
+
title: "Enhancement: "
|
4 |
+
labels: ["enhancement"]
|
5 |
+
body:
|
6 |
+
- type: markdown
|
7 |
+
attributes:
|
8 |
+
value: |
|
9 |
+
Thank you for taking the time to fill this out!
|
10 |
+
- type: input
|
11 |
+
id: contact
|
12 |
+
attributes:
|
13 |
+
label: Contact Details
|
14 |
+
description: How can we contact you if we need more information?
|
15 |
+
placeholder: ex. [email protected]
|
16 |
+
validations:
|
17 |
+
required: false
|
18 |
+
- type: textarea
|
19 |
+
id: what
|
20 |
+
attributes:
|
21 |
+
label: What features would you like to see added?
|
22 |
+
description: Please provide as many details as possible.
|
23 |
+
placeholder: Please provide as many details as possible.
|
24 |
+
validations:
|
25 |
+
required: true
|
26 |
+
- type: textarea
|
27 |
+
id: details
|
28 |
+
attributes:
|
29 |
+
label: More details
|
30 |
+
description: Please provide additional details if needed.
|
31 |
+
placeholder: Please provide additional details if needed.
|
32 |
+
validations:
|
33 |
+
required: true
|
34 |
+
- type: dropdown
|
35 |
+
id: subject
|
36 |
+
attributes:
|
37 |
+
label: Which components are impacted by your request?
|
38 |
+
multiple: true
|
39 |
+
options:
|
40 |
+
- General
|
41 |
+
- UI
|
42 |
+
- Endpoints
|
43 |
+
- Plugins
|
44 |
+
- Other
|
45 |
+
- type: textarea
|
46 |
+
id: screenshots
|
47 |
+
attributes:
|
48 |
+
label: Pictures
|
49 |
+
description: If relevant, please include images to help clarify your request. You can drag and drop images directly here, paste them, or provide a link to them.
|
50 |
+
- type: checkboxes
|
51 |
+
id: terms
|
52 |
+
attributes:
|
53 |
+
label: Code of Conduct
|
54 |
+
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/CODE_OF_CONDUCT.md)
|
55 |
+
options:
|
56 |
+
- label: I agree to follow this project's Code of Conduct
|
57 |
+
required: true
|
.github/ISSUE_TEMPLATE/QUESTION.yml
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Question
|
2 |
+
description: Ask your question
|
3 |
+
title: "[Question]: "
|
4 |
+
labels: ["question"]
|
5 |
+
body:
|
6 |
+
- type: markdown
|
7 |
+
attributes:
|
8 |
+
value: |
|
9 |
+
Thanks for taking the time to fill this!
|
10 |
+
- type: input
|
11 |
+
id: contact
|
12 |
+
attributes:
|
13 |
+
label: Contact Details
|
14 |
+
description: How can we get in touch with you if we need more info?
|
15 |
+
placeholder: ex. [email protected]
|
16 |
+
validations:
|
17 |
+
required: false
|
18 |
+
- type: textarea
|
19 |
+
id: what-is-your-question
|
20 |
+
attributes:
|
21 |
+
label: What is your question?
|
22 |
+
description: Please give as many details as possible
|
23 |
+
placeholder: Please give as many details as possible
|
24 |
+
validations:
|
25 |
+
required: true
|
26 |
+
- type: textarea
|
27 |
+
id: more-details
|
28 |
+
attributes:
|
29 |
+
label: More Details
|
30 |
+
description: Please provide more details if needed.
|
31 |
+
placeholder: Please provide more details if needed.
|
32 |
+
validations:
|
33 |
+
required: true
|
34 |
+
- type: dropdown
|
35 |
+
id: browsers
|
36 |
+
attributes:
|
37 |
+
label: What is the main subject of your question?
|
38 |
+
multiple: true
|
39 |
+
options:
|
40 |
+
- Documentation
|
41 |
+
- Installation
|
42 |
+
- UI
|
43 |
+
- Endpoints
|
44 |
+
- User System/OAuth
|
45 |
+
- Other
|
46 |
+
- type: textarea
|
47 |
+
id: screenshots
|
48 |
+
attributes:
|
49 |
+
label: Screenshots
|
50 |
+
description: If applicable, add screenshots to help explain your problem. You can drag and drop, paste images directly here or link to them.
|
51 |
+
- type: checkboxes
|
52 |
+
id: terms
|
53 |
+
attributes:
|
54 |
+
label: Code of Conduct
|
55 |
+
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/CODE_OF_CONDUCT.md)
|
56 |
+
options:
|
57 |
+
- label: I agree to follow this project's Code of Conduct
|
58 |
+
required: true
|
.github/dependabot.yml
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# To get started with Dependabot version updates, you'll need to specify which
|
2 |
+
# package ecosystems to update and where the package manifests are located.
|
3 |
+
# Please see the documentation for all configuration options:
|
4 |
+
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
5 |
+
|
6 |
+
version: 2
|
7 |
+
updates:
|
8 |
+
- package-ecosystem: "npm" # See documentation for possible values
|
9 |
+
directory: "/api" # Location of package manifests
|
10 |
+
target-branch: "develop"
|
11 |
+
versioning-strategy: increase-if-necessary
|
12 |
+
schedule:
|
13 |
+
interval: "weekly"
|
14 |
+
allow:
|
15 |
+
# Allow both direct and indirect updates for all packages
|
16 |
+
- dependency-type: "all"
|
17 |
+
commit-message:
|
18 |
+
prefix: "npm api prod"
|
19 |
+
prefix-development: "npm api dev"
|
20 |
+
include: "scope"
|
21 |
+
- package-ecosystem: "npm" # See documentation for possible values
|
22 |
+
directory: "/client" # Location of package manifests
|
23 |
+
target-branch: "develop"
|
24 |
+
versioning-strategy: increase-if-necessary
|
25 |
+
schedule:
|
26 |
+
interval: "weekly"
|
27 |
+
allow:
|
28 |
+
# Allow both direct and indirect updates for all packages
|
29 |
+
- dependency-type: "all"
|
30 |
+
commit-message:
|
31 |
+
prefix: "npm client prod"
|
32 |
+
prefix-development: "npm client dev"
|
33 |
+
include: "scope"
|
34 |
+
- package-ecosystem: "npm" # See documentation for possible values
|
35 |
+
directory: "/" # Location of package manifests
|
36 |
+
target-branch: "develop"
|
37 |
+
versioning-strategy: increase-if-necessary
|
38 |
+
schedule:
|
39 |
+
interval: "weekly"
|
40 |
+
allow:
|
41 |
+
# Allow both direct and indirect updates for all packages
|
42 |
+
- dependency-type: "all"
|
43 |
+
commit-message:
|
44 |
+
prefix: "npm all prod"
|
45 |
+
prefix-development: "npm all dev"
|
46 |
+
include: "scope"
|
47 |
+
|
.github/playwright.yml
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Playwright Tests
|
2 |
+
on:
|
3 |
+
push:
|
4 |
+
branches: [feat/playwright-jest-cicd]
|
5 |
+
pull_request:
|
6 |
+
branches: [feat/playwright-jest-cicd]
|
7 |
+
jobs:
|
8 |
+
tests_e2e:
|
9 |
+
name: Run Playwright tests
|
10 |
+
timeout-minutes: 60
|
11 |
+
runs-on: ubuntu-latest
|
12 |
+
env:
|
13 |
+
# BINGAI_TOKEN: ${{ secrets.BINGAI_TOKEN }}
|
14 |
+
# CHATGPT_TOKEN: ${{ secrets.CHATGPT_TOKEN }}
|
15 |
+
MONGO_URI: ${{ secrets.MONGO_URI }}
|
16 |
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
17 |
+
E2E_USER_EMAIL: ${{ secrets.E2E_USER_EMAIL }}
|
18 |
+
E2E_USER_PASSWORD: ${{ secrets.E2E_USER_PASSWORD }}
|
19 |
+
JWT_SECRET: ${{ secrets.JWT_SECRET }}
|
20 |
+
CREDS_KEY: ${{ secrets.CREDS_KEY }}
|
21 |
+
CREDS_IV: ${{ secrets.CREDS_IV }}
|
22 |
+
# NODE_ENV: ${{ vars.NODE_ENV }}
|
23 |
+
DOMAIN_CLIENT: ${{ vars.DOMAIN_CLIENT }}
|
24 |
+
DOMAIN_SERVER: ${{ vars.DOMAIN_SERVER }}
|
25 |
+
# PALM_KEY: ${{ secrets.PALM_KEY }}
|
26 |
+
steps:
|
27 |
+
- uses: actions/checkout@v3
|
28 |
+
- uses: actions/setup-node@v3
|
29 |
+
with:
|
30 |
+
node-version: 18
|
31 |
+
cache: 'npm'
|
32 |
+
|
33 |
+
- name: Install global dependencies
|
34 |
+
run: npm ci --ignore-scripts
|
35 |
+
|
36 |
+
- name: Install API dependencies
|
37 |
+
working-directory: ./api
|
38 |
+
run: npm ci --ignore-scripts
|
39 |
+
|
40 |
+
- name: Install Client dependencies
|
41 |
+
working-directory: ./client
|
42 |
+
run: npm ci --ignore-scripts
|
43 |
+
|
44 |
+
- name: Build Client
|
45 |
+
run: cd client && npm run build:ci
|
46 |
+
|
47 |
+
- name: Install Playwright Browsers
|
48 |
+
run: npx playwright install --with-deps && npm install -D @playwright/test
|
49 |
+
|
50 |
+
- name: Start server
|
51 |
+
run: |
|
52 |
+
npm run backend & sleep 10
|
53 |
+
|
54 |
+
- name: Run Playwright tests
|
55 |
+
run: npx playwright test --config=e2e/playwright.config.ts
|
56 |
+
|
57 |
+
- uses: actions/upload-artifact@v3
|
58 |
+
if: always()
|
59 |
+
with:
|
60 |
+
name: playwright-report
|
61 |
+
path: e2e/playwright-report/
|
62 |
+
retention-days: 30
|
.github/pull_request_template.md
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change.
|
2 |
+
|
3 |
+
|
4 |
+
|
5 |
+
## Type of change
|
6 |
+
|
7 |
+
Please delete options that are not relevant.
|
8 |
+
|
9 |
+
- [ ] Bug fix (non-breaking change which fixes an issue)
|
10 |
+
- [ ] New feature (non-breaking change which adds functionality)
|
11 |
+
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
12 |
+
- [ ] This change requires a documentation update
|
13 |
+
- [ ] Documentation update
|
14 |
+
|
15 |
+
|
16 |
+
## How Has This Been Tested?
|
17 |
+
|
18 |
+
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration:
|
19 |
+
##
|
20 |
+
|
21 |
+
|
22 |
+
### **Test Configuration**:
|
23 |
+
##
|
24 |
+
|
25 |
+
|
26 |
+
## Checklist:
|
27 |
+
|
28 |
+
- [ ] My code follows the style guidelines of this project
|
29 |
+
- [ ] I have performed a self-review of my code
|
30 |
+
- [ ] I have commented my code, particularly in hard-to-understand areas
|
31 |
+
- [ ] I have made corresponding changes to the documentation
|
32 |
+
- [ ] My changes generate no new warnings
|
33 |
+
- [ ] I have added tests that prove my fix is effective or that my feature works
|
34 |
+
- [ ] New and existing unit tests pass locally with my changes
|
35 |
+
- [ ] Any dependent changes have been merged and published in downstream modules
|
.github/wip-playwright.yml
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Playwright Tests
|
2 |
+
on:
|
3 |
+
push:
|
4 |
+
branches: [ main, master ]
|
5 |
+
pull_request:
|
6 |
+
branches: [ main, master ]
|
7 |
+
jobs:
|
8 |
+
tests_e2e:
|
9 |
+
name: Run end-to-end tests
|
10 |
+
timeout-minutes: 60
|
11 |
+
runs-on: ubuntu-latest
|
12 |
+
steps:
|
13 |
+
- uses: actions/checkout@v3
|
14 |
+
- uses: actions/setup-node@v3
|
15 |
+
with:
|
16 |
+
node-version: 18
|
17 |
+
- name: Install dependencies
|
18 |
+
run: npm ci
|
19 |
+
- name: Install Playwright Browsers
|
20 |
+
run: npx playwright install --with-deps
|
21 |
+
- name: Run Playwright tests
|
22 |
+
run: npx playwright test
|
23 |
+
- uses: actions/upload-artifact@v3
|
24 |
+
if: always()
|
25 |
+
with:
|
26 |
+
name: playwright-report
|
27 |
+
path: e2e/playwright-report/
|
28 |
+
retention-days: 30
|
.github/workflows/backend-review.yml
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Backend Unit Tests
|
2 |
+
on:
|
3 |
+
push:
|
4 |
+
branches:
|
5 |
+
- main
|
6 |
+
- dev
|
7 |
+
- release/*
|
8 |
+
pull_request:
|
9 |
+
branches:
|
10 |
+
- main
|
11 |
+
- dev
|
12 |
+
- release/*
|
13 |
+
jobs:
|
14 |
+
tests_Backend:
|
15 |
+
name: Run Backend unit tests
|
16 |
+
timeout-minutes: 60
|
17 |
+
runs-on: ubuntu-latest
|
18 |
+
env:
|
19 |
+
MONGO_URI: ${{ secrets.MONGO_URI }}
|
20 |
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
21 |
+
JWT_SECRET: ${{ secrets.JWT_SECRET }}
|
22 |
+
CREDS_KEY: ${{ secrets.CREDS_KEY }}
|
23 |
+
CREDS_IV: ${{ secrets.CREDS_IV }}
|
24 |
+
steps:
|
25 |
+
- uses: actions/checkout@v2
|
26 |
+
- name: Use Node.js 19.x
|
27 |
+
uses: actions/setup-node@v3
|
28 |
+
with:
|
29 |
+
node-version: 19.x
|
30 |
+
cache: 'npm'
|
31 |
+
|
32 |
+
- name: Install dependencies
|
33 |
+
run: npm ci
|
34 |
+
|
35 |
+
# - name: Install Linux X64 Sharp
|
36 |
+
# run: npm install --platform=linux --arch=x64 --verbose sharp
|
37 |
+
|
38 |
+
- name: Run unit tests
|
39 |
+
run: cd api && npm run test:ci
|
40 |
+
|
41 |
+
- name: Run linters
|
42 |
+
uses: wearerequired/lint-action@v2
|
43 |
+
with:
|
44 |
+
eslint: true
|
.github/workflows/build.yml
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Linux_Container_Workflow
|
2 |
+
|
3 |
+
on:
|
4 |
+
workflow_dispatch:
|
5 |
+
|
6 |
+
env:
|
7 |
+
RUNNER_VERSION: 2.293.0
|
8 |
+
|
9 |
+
jobs:
|
10 |
+
build-and-push:
|
11 |
+
runs-on: ubuntu-latest
|
12 |
+
steps:
|
13 |
+
# checkout the repo
|
14 |
+
- name: 'Checkout GitHub Action'
|
15 |
+
uses: actions/checkout@main
|
16 |
+
|
17 |
+
- name: 'Login via Azure CLI'
|
18 |
+
uses: azure/login@v1
|
19 |
+
with:
|
20 |
+
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
21 |
+
|
22 |
+
- name: 'Build GitHub Runner container image'
|
23 |
+
uses: azure/docker-login@v1
|
24 |
+
with:
|
25 |
+
login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}
|
26 |
+
username: ${{ secrets.REGISTRY_USERNAME }}
|
27 |
+
password: ${{ secrets.REGISTRY_PASSWORD }}
|
28 |
+
- run: |
|
29 |
+
docker build --build-arg RUNNER_VERSION=${{ env.RUNNER_VERSION }} -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/pwd9000-github-runner-lin:${{ env.RUNNER_VERSION }} .
|
30 |
+
|
31 |
+
- name: 'Push container image to ACR'
|
32 |
+
uses: azure/docker-login@v1
|
33 |
+
with:
|
34 |
+
login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}
|
35 |
+
username: ${{ secrets.REGISTRY_USERNAME }}
|
36 |
+
password: ${{ secrets.REGISTRY_PASSWORD }}
|
37 |
+
- run: |
|
38 |
+
docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/pwd9000-github-runner-lin:${{ env.RUNNER_VERSION }}
|
.github/workflows/container.yml
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Docker Compose Build on Tag
|
2 |
+
|
3 |
+
# The workflow is triggered when a tag is pushed
|
4 |
+
on:
|
5 |
+
push:
|
6 |
+
tags:
|
7 |
+
- "*"
|
8 |
+
|
9 |
+
jobs:
|
10 |
+
build:
|
11 |
+
runs-on: ubuntu-latest
|
12 |
+
|
13 |
+
steps:
|
14 |
+
# Check out the repository
|
15 |
+
- name: Checkout
|
16 |
+
uses: actions/checkout@v2
|
17 |
+
|
18 |
+
# Set up Docker
|
19 |
+
- name: Set up Docker
|
20 |
+
uses: docker/setup-buildx-action@v1
|
21 |
+
|
22 |
+
# Log in to GitHub Container Registry
|
23 |
+
- name: Log in to GitHub Container Registry
|
24 |
+
uses: docker/login-action@v2
|
25 |
+
with:
|
26 |
+
registry: ghcr.io
|
27 |
+
username: ${{ github.actor }}
|
28 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
29 |
+
|
30 |
+
# Run docker-compose build
|
31 |
+
- name: Build Docker images
|
32 |
+
run: |
|
33 |
+
cp .env.example .env
|
34 |
+
docker-compose build
|
35 |
+
|
36 |
+
# Get Tag Name
|
37 |
+
- name: Get Tag Name
|
38 |
+
id: tag_name
|
39 |
+
run: echo "TAG_NAME=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
40 |
+
|
41 |
+
# Tag it properly before push to github
|
42 |
+
- name: tag image and push
|
43 |
+
run: |
|
44 |
+
docker tag librechat:latest ghcr.io/${{ github.repository_owner }}/librechat:${{ env.TAG_NAME }}
|
45 |
+
docker push ghcr.io/${{ github.repository_owner }}/librechat:${{ env.TAG_NAME }}
|
46 |
+
docker tag librechat:latest ghcr.io/${{ github.repository_owner }}/librechat:latest
|
47 |
+
docker push ghcr.io/${{ github.repository_owner }}/librechat:latest
|
.github/workflows/deploy.yml
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Deploy_GHRunner_Linux_ACI
|
2 |
+
|
3 |
+
on:
|
4 |
+
workflow_dispatch:
|
5 |
+
|
6 |
+
env:
|
7 |
+
RUNNER_VERSION: 2.293.0
|
8 |
+
ACI_RESOURCE_GROUP: 'Demo-ACI-GitHub-Runners-RG'
|
9 |
+
ACI_NAME: 'gh-runner-linux-01'
|
10 |
+
DNS_NAME_LABEL: 'gh-lin-01'
|
11 |
+
GH_OWNER: ${{ github.repository_owner }}
|
12 |
+
GH_REPOSITORY: 'LibreChat' #Change here to deploy self hosted runner ACI to another repo.
|
13 |
+
|
14 |
+
jobs:
|
15 |
+
deploy-gh-runner-aci:
|
16 |
+
runs-on: ubuntu-latest
|
17 |
+
steps:
|
18 |
+
# checkout the repo
|
19 |
+
- name: 'Checkout GitHub Action'
|
20 |
+
uses: actions/checkout@main
|
21 |
+
|
22 |
+
- name: 'Login via Azure CLI'
|
23 |
+
uses: azure/login@v1
|
24 |
+
with:
|
25 |
+
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
26 |
+
|
27 |
+
- name: 'Deploy to Azure Container Instances'
|
28 |
+
uses: 'azure/aci-deploy@v1'
|
29 |
+
with:
|
30 |
+
resource-group: ${{ env.ACI_RESOURCE_GROUP }}
|
31 |
+
image: ${{ secrets.REGISTRY_LOGIN_SERVER }}/pwd9000-github-runner-lin:${{ env.RUNNER_VERSION }}
|
32 |
+
registry-login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}
|
33 |
+
registry-username: ${{ secrets.REGISTRY_USERNAME }}
|
34 |
+
registry-password: ${{ secrets.REGISTRY_PASSWORD }}
|
35 |
+
name: ${{ env.ACI_NAME }}
|
36 |
+
dns-name-label: ${{ env.DNS_NAME_LABEL }}
|
37 |
+
environment-variables: GH_TOKEN=${{ secrets.PAT_TOKEN }} GH_OWNER=${{ env.GH_OWNER }} GH_REPOSITORY=${{ env.GH_REPOSITORY }}
|
38 |
+
location: 'eastus'
|
.github/workflows/frontend-review.yml
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#github action to run unit tests for frontend with jest
|
2 |
+
name: Frontend Unit Tests
|
3 |
+
on:
|
4 |
+
push:
|
5 |
+
branches:
|
6 |
+
- main
|
7 |
+
- dev
|
8 |
+
- release/*
|
9 |
+
pull_request:
|
10 |
+
branches:
|
11 |
+
- main
|
12 |
+
- dev
|
13 |
+
- release/*
|
14 |
+
jobs:
|
15 |
+
tests_frontend:
|
16 |
+
name: Run frontend unit tests
|
17 |
+
timeout-minutes: 60
|
18 |
+
runs-on: ubuntu-latest
|
19 |
+
steps:
|
20 |
+
- uses: actions/checkout@v2
|
21 |
+
- name: Use Node.js 19.x
|
22 |
+
uses: actions/setup-node@v3
|
23 |
+
with:
|
24 |
+
node-version: 19.x
|
25 |
+
cache: 'npm'
|
26 |
+
|
27 |
+
- name: Install dependencies
|
28 |
+
run: npm ci
|
29 |
+
|
30 |
+
- name: Build Client
|
31 |
+
run: npm run frontend:ci
|
32 |
+
|
33 |
+
- name: Run unit tests
|
34 |
+
run: cd client && npm run test:ci
|
.github/workflows/mkdocs.yaml
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: mkdocs
|
2 |
+
on:
|
3 |
+
push:
|
4 |
+
branches:
|
5 |
+
- main
|
6 |
+
permissions:
|
7 |
+
contents: write
|
8 |
+
jobs:
|
9 |
+
deploy:
|
10 |
+
runs-on: ubuntu-latest
|
11 |
+
steps:
|
12 |
+
- uses: actions/checkout@v3
|
13 |
+
- uses: actions/setup-python@v4
|
14 |
+
with:
|
15 |
+
python-version: 3.x
|
16 |
+
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
17 |
+
- uses: actions/cache@v3
|
18 |
+
with:
|
19 |
+
key: mkdocs-material-${{ env.cache_id }}
|
20 |
+
path: .cache
|
21 |
+
restore-keys: |
|
22 |
+
mkdocs-material-
|
23 |
+
- run: pip install mkdocs-material
|
24 |
+
- run: mkdocs gh-deploy --force
|
.gitignore
ADDED
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
### node etc ###
|
2 |
+
|
3 |
+
# Logs
|
4 |
+
data-node
|
5 |
+
meili_data
|
6 |
+
logs
|
7 |
+
*.log
|
8 |
+
|
9 |
+
# Runtime data
|
10 |
+
pids
|
11 |
+
*.pid
|
12 |
+
*.seed
|
13 |
+
|
14 |
+
# Directory for instrumented libs generated by jscoverage/JSCover
|
15 |
+
lib-cov
|
16 |
+
|
17 |
+
# Coverage directory used by tools like istanbul
|
18 |
+
coverage
|
19 |
+
|
20 |
+
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
21 |
+
.grunt
|
22 |
+
|
23 |
+
# Compiled Dirs (http://nodejs.org/api/addons.html)
|
24 |
+
build/
|
25 |
+
dist/
|
26 |
+
public/main.js
|
27 |
+
public/main.js.map
|
28 |
+
public/main.js.LICENSE.txt
|
29 |
+
client/public/images/
|
30 |
+
client/public/main.js
|
31 |
+
client/public/main.js.map
|
32 |
+
client/public/main.js.LICENSE.txt
|
33 |
+
|
34 |
+
# Dependency directorys
|
35 |
+
# Deployed apps should consider commenting these lines out:
|
36 |
+
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
|
37 |
+
node_modules/
|
38 |
+
meili_data/
|
39 |
+
api/node_modules/
|
40 |
+
client/node_modules/
|
41 |
+
bower_components/
|
42 |
+
types/
|
43 |
+
|
44 |
+
# Floobits
|
45 |
+
.floo
|
46 |
+
.floobit
|
47 |
+
.floo
|
48 |
+
.flooignore
|
49 |
+
|
50 |
+
# Environment
|
51 |
+
.npmrc
|
52 |
+
.env*
|
53 |
+
!**/.env.example
|
54 |
+
!**/.env.test.example
|
55 |
+
cache.json
|
56 |
+
api/data/
|
57 |
+
owner.yml
|
58 |
+
archive
|
59 |
+
.vscode/settings.json
|
60 |
+
src/style - official.css
|
61 |
+
/e2e/specs/.test-results/
|
62 |
+
/e2e/playwright-report/
|
63 |
+
/playwright/.cache/
|
64 |
+
.DS_Store
|
65 |
+
*.code-workspace
|
66 |
+
.idea
|
67 |
+
*.pem
|
68 |
+
config.local.ts
|
69 |
+
**/storageState.json
|
70 |
+
junit.xml
|
71 |
+
|
72 |
+
# meilisearch
|
73 |
+
meilisearch
|
74 |
+
data.ms/*
|
75 |
+
auth.json
|
76 |
+
|
77 |
+
/packages/ux-shared/
|
78 |
+
/images
|
.husky/pre-commit
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env sh
|
2 |
+
. "$(dirname -- "$0")/_/husky.sh"
|
3 |
+
[ -n "$CI" ] && exit 0
|
4 |
+
npx lint-staged
|
5 |
+
|
.prettierrc.js
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
module.exports = {
|
2 |
+
printWidth: 100,
|
3 |
+
tabWidth: 2,
|
4 |
+
useTabs: false,
|
5 |
+
semi: true,
|
6 |
+
singleQuote: true,
|
7 |
+
// bracketSpacing: false,
|
8 |
+
trailingComma: 'all',
|
9 |
+
arrowParens: 'always',
|
10 |
+
embeddedLanguageFormatting: 'auto',
|
11 |
+
insertPragma: false,
|
12 |
+
proseWrap: 'preserve',
|
13 |
+
quoteProps: 'as-needed',
|
14 |
+
requirePragma: false,
|
15 |
+
rangeStart: 0,
|
16 |
+
endOfLine: 'auto',
|
17 |
+
jsxBracketSameLine: false,
|
18 |
+
jsxSingleQuote: false,
|
19 |
+
};
|
CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Contributor Covenant Code of Conduct
|
2 |
+
|
3 |
+
## Our Pledge
|
4 |
+
|
5 |
+
We as members, contributors, and leaders pledge to make participation in our
|
6 |
+
community a harassment-free experience for everyone, regardless of age, body
|
7 |
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
8 |
+
identity and expression, level of experience, education, socio-economic status,
|
9 |
+
nationality, personal appearance, race, religion, or sexual identity
|
10 |
+
and orientation.
|
11 |
+
|
12 |
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
13 |
+
diverse, inclusive, and healthy community.
|
14 |
+
|
15 |
+
## Our Standards
|
16 |
+
|
17 |
+
Examples of behavior that contributes to a positive environment for our
|
18 |
+
community include:
|
19 |
+
|
20 |
+
* Demonstrating empathy and kindness toward other people
|
21 |
+
* Being respectful of differing opinions, viewpoints, and experiences
|
22 |
+
* Giving and gracefully accepting constructive feedback
|
23 |
+
* Accepting responsibility and apologizing to those affected by our mistakes,
|
24 |
+
and learning from the experience
|
25 |
+
* Focusing on what is best not just for us as individuals, but for the
|
26 |
+
overall community
|
27 |
+
|
28 |
+
Examples of unacceptable behavior include:
|
29 |
+
|
30 |
+
* The use of sexualized language or imagery, and sexual attention or
|
31 |
+
advances of any kind
|
32 |
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
33 |
+
* Public or private harassment
|
34 |
+
* Publishing others' private information, such as a physical or email
|
35 |
+
address, without their explicit permission
|
36 |
+
* Other conduct which could reasonably be considered inappropriate in a
|
37 |
+
professional setting
|
38 |
+
|
39 |
+
## Enforcement Responsibilities
|
40 |
+
|
41 |
+
Community leaders are responsible for clarifying and enforcing our standards of
|
42 |
+
acceptable behavior and will take appropriate and fair corrective action in
|
43 |
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
44 |
+
or harmful.
|
45 |
+
|
46 |
+
Community leaders have the right and responsibility to remove, edit, or reject
|
47 |
+
comments, commits, code, wiki edits, issues, and other contributions that are
|
48 |
+
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
49 |
+
decisions when appropriate.
|
50 |
+
|
51 |
+
## Scope
|
52 |
+
|
53 |
+
This Code of Conduct applies within all community spaces, and also applies when
|
54 |
+
an individual is officially representing the community in public spaces.
|
55 |
+
Examples of representing our community include using an official e-mail address,
|
56 |
+
posting via an official social media account, or acting as an appointed
|
57 |
+
representative at an online or offline event.
|
58 |
+
|
59 |
+
## Enforcement
|
60 |
+
|
61 |
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
62 |
+
reported to the community leaders responsible for enforcement here on GitHub or
|
63 |
+
on the official [Discord Server](https://discord.gg/uDyZ5Tzhct).
|
64 |
+
All complaints will be reviewed and investigated promptly and fairly.
|
65 |
+
|
66 |
+
All community leaders are obligated to respect the privacy and security of the
|
67 |
+
reporter of any incident.
|
68 |
+
|
69 |
+
## Enforcement Guidelines
|
70 |
+
|
71 |
+
Community leaders will follow these Community Impact Guidelines in determining
|
72 |
+
the consequences for any action they deem in violation of this Code of Conduct:
|
73 |
+
|
74 |
+
### 1. Correction
|
75 |
+
|
76 |
+
**Community Impact**: Use of inappropriate language or other behavior deemed
|
77 |
+
unprofessional or unwelcome in the community.
|
78 |
+
|
79 |
+
**Consequence**: A private, written warning from community leaders, providing
|
80 |
+
clarity around the nature of the violation and an explanation of why the
|
81 |
+
behavior was inappropriate. A public apology may be requested.
|
82 |
+
|
83 |
+
### 2. Warning
|
84 |
+
|
85 |
+
**Community Impact**: A violation through a single incident or series
|
86 |
+
of actions.
|
87 |
+
|
88 |
+
**Consequence**: A warning with consequences for continued behavior. No
|
89 |
+
interaction with the people involved, including unsolicited interaction with
|
90 |
+
those enforcing the Code of Conduct, for a specified period of time. This
|
91 |
+
includes avoiding interactions in community spaces as well as external channels
|
92 |
+
like social media. Violating these terms may lead to a temporary or
|
93 |
+
permanent ban.
|
94 |
+
|
95 |
+
### 3. Temporary Ban
|
96 |
+
|
97 |
+
**Community Impact**: A serious violation of community standards, including
|
98 |
+
sustained inappropriate behavior.
|
99 |
+
|
100 |
+
**Consequence**: A temporary ban from any sort of interaction or public
|
101 |
+
communication with the community for a specified period of time. No public or
|
102 |
+
private interaction with the people involved, including unsolicited interaction
|
103 |
+
with those enforcing the Code of Conduct, is allowed during this period.
|
104 |
+
Violating these terms may lead to a permanent ban.
|
105 |
+
|
106 |
+
### 4. Permanent Ban
|
107 |
+
|
108 |
+
**Community Impact**: Demonstrating a pattern of violation of community
|
109 |
+
standards, including sustained inappropriate behavior, harassment of an
|
110 |
+
individual, or aggression toward or disparagement of classes of individuals.
|
111 |
+
|
112 |
+
**Consequence**: A permanent ban from any sort of public interaction within
|
113 |
+
the community.
|
114 |
+
|
115 |
+
## Attribution
|
116 |
+
|
117 |
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
118 |
+
version 2.0, available at
|
119 |
+
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
120 |
+
|
121 |
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
122 |
+
enforcement ladder](https://github.com/mozilla/diversity).
|
123 |
+
|
124 |
+
[homepage]: https://www.contributor-covenant.org
|
125 |
+
|
126 |
+
For answers to common questions about this code of conduct, see the FAQ at
|
127 |
+
https://www.contributor-covenant.org/faq. Translations are available at
|
128 |
+
https://www.contributor-covenant.org/translations.
|
129 |
+
|
130 |
+
---
|
131 |
+
|
132 |
+
## [Go Back to ReadMe](README.md)
|
CONTRIBUTING.md
ADDED
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Contributor Guidelines
|
2 |
+
|
3 |
+
Thank you to all the contributors who have helped make this project possible! We welcome various types of contributions, such as bug reports, documentation improvements, feature requests, and code contributions.
|
4 |
+
|
5 |
+
## Contributing Guidelines
|
6 |
+
|
7 |
+
If the feature you would like to contribute has not already received prior approval from the project maintainers (i.e., the feature is currently on the roadmap or on the [Trello board]()), please submit a proposal in the [proposals category](https://github.com/danny-avila/LibreChat/discussions/categories/proposals) of the discussions board before beginning work on it. The proposals should include specific implementation details, including areas of the application that will be affected by the change (including designs if applicable), and any other relevant information that might be required for a speedy review. However, proposals are not required for small changes, bug fixes, or documentation improvements. Small changes and bug fixes should be tied to an [issue](https://github.com/danny-avila/LibreChat/issues) and included in the corresponding pull request for tracking purposes.
|
8 |
+
|
9 |
+
Please note that a pull request involving a feature that has not been reviewed and approved by the project maintainers may be rejected. We appreciate your understanding and cooperation.
|
10 |
+
|
11 |
+
If you would like to discuss the changes you wish to make, join our [Discord community](https://discord.gg/uDyZ5Tzhct), where you can engage with other contributors and seek guidance from the community.
|
12 |
+
|
13 |
+
## Our Standards
|
14 |
+
|
15 |
+
We strive to maintain a positive and inclusive environment within our project community. We expect all contributors to adhere to the following standards:
|
16 |
+
|
17 |
+
- Using welcoming and inclusive language.
|
18 |
+
- Being respectful of differing viewpoints and experiences.
|
19 |
+
- Gracefully accepting constructive criticism.
|
20 |
+
- Focusing on what is best for the community.
|
21 |
+
- Showing empathy towards other community members.
|
22 |
+
|
23 |
+
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that do not align with these standards.
|
24 |
+
|
25 |
+
## To contribute to this project, please adhere to the following guidelines:
|
26 |
+
|
27 |
+
## 1. Git Workflow
|
28 |
+
|
29 |
+
We utilize a GitFlow workflow to manage changes to this project's codebase. Follow these general steps when contributing code:
|
30 |
+
|
31 |
+
1. Fork the repository and create a new branch with a descriptive slash-based name (e.g., `new/feature/x`).
|
32 |
+
2. Implement your changes and ensure that all tests pass.
|
33 |
+
3. Commit your changes using conventional commit messages with GitFlow flags. Begin the commit message with a tag indicating the change type, such as "feat" (new feature), "fix" (bug fix), "docs" (documentation), or "refactor" (code refactoring), followed by a brief summary of the changes (e.g., `feat: Add new feature X to the project`).
|
34 |
+
4. Submit a pull request with a clear and concise description of your changes and the reasons behind them.
|
35 |
+
5. We will review your pull request, provide feedback as needed, and eventually merge the approved changes into the main branch.
|
36 |
+
|
37 |
+
## 2. Commit Message Format
|
38 |
+
|
39 |
+
We have defined precise rules for formatting our Git commit messages. This format leads to an easier-to-read commit history. Each commit message consists of a header, a body, and an optional footer.
|
40 |
+
|
41 |
+
### Commit Message Header
|
42 |
+
|
43 |
+
The header is mandatory and must conform to the following format:
|
44 |
+
|
45 |
+
```
|
46 |
+
<type>(<scope>): <short summary>
|
47 |
+
```
|
48 |
+
|
49 |
+
- `<type>`: Must be one of the following:
|
50 |
+
- **build**: Changes that affect the build system or external dependencies.
|
51 |
+
- **ci**: Changes to our CI configuration files and script.
|
52 |
+
- **docs**: Documentation-only changes.
|
53 |
+
- **feat**: A new feature.
|
54 |
+
- **fix**: A bug fix.
|
55 |
+
- **perf**: A code change that improves performance.
|
56 |
+
- **refactor**: A code change that neither fixes a bug nor adds a feature.
|
57 |
+
- **test**: Adding missing tests or correcting existing tests.
|
58 |
+
|
59 |
+
- `<scope>`: Optional. Indicates the scope of the commit, such as `common`, `plays`, `infra`, etc.
|
60 |
+
|
61 |
+
- `<short summary>`: A brief, concise summary of the change in the present tense. It should not be capitalized and should not end with a period.
|
62 |
+
|
63 |
+
### Commit Message Body
|
64 |
+
|
65 |
+
The body is mandatory for all commits except for those of type "docs". When the body is present, it must be at least 20 characters long and should explain the motivation behind the change. You can include a comparison of the previous behavior with the new behavior to illustrate the impact of the change.
|
66 |
+
|
67 |
+
### Commit Message Footer
|
68 |
+
|
69 |
+
The footer is optional and can contain information about breaking changes, deprecations, and references to related GitHub issues, Jira tickets, or other pull requests. For example, you can include a "BREAKING CHANGE" section that describes a breaking change along with migration instructions. Additionally, you can include a "Closes" section to reference the issue or pull request that this commit closes or is related to.
|
70 |
+
|
71 |
+
### Revert commits
|
72 |
+
|
73 |
+
If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit. The commit message body should include the SHA of the commit being reverted and a clear description of the reason for reverting the commit.
|
74 |
+
|
75 |
+
## 3. Pull Request Process
|
76 |
+
|
77 |
+
When submitting a pull request, please follow these guidelines:
|
78 |
+
|
79 |
+
- Ensure that any installation or build dependencies are removed before the end of the layer when doing a build.
|
80 |
+
- Update the README.md with details of changes to the interface, including new environment variables, exposed ports, useful file locations, and container parameters.
|
81 |
+
- Increase the version numbers in any example files and the README.md to reflect the new version that the pull request represents. We use [SemVer](http://semver.org/) for versioning.
|
82 |
+
|
83 |
+
Ensure that your changes meet the following criteria:
|
84 |
+
|
85 |
+
- All tests pass.
|
86 |
+
- The code is well-formatted and adheres to our coding standards.
|
87 |
+
- The commit history is clean and easy to follow. You can use `git rebase` or `git merge --squash` to clean your commit history before submitting the pull request.
|
88 |
+
- The pull request description clearly outlines the changes and the reasons behind them. Be sure to include the steps to test the pull request.
|
89 |
+
|
90 |
+
## 4. Naming Conventions
|
91 |
+
|
92 |
+
Apply the following naming conventions to branches, labels, and other Git-related entities:
|
93 |
+
|
94 |
+
- Branch names: Descriptive and slash-based (e.g., `new/feature/x`).
|
95 |
+
- Labels: Descriptive and snake_case (e.g., `bug_fix`).
|
96 |
+
- Directories and file names: Descriptive and snake_case (e.g., `config_file.yaml`).
|
97 |
+
|
98 |
+
---
|
99 |
+
|
100 |
+
## [Go Back to ReadMe](README.md)
|
Dockerfile
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Base node image
|
2 |
+
FROM node:19-alpine AS node
|
3 |
+
|
4 |
+
# Install curl for health check
|
5 |
+
RUN apk --no-cache add curl
|
6 |
+
|
7 |
+
COPY . /app
|
8 |
+
# Install dependencies
|
9 |
+
WORKDIR /app
|
10 |
+
RUN npm ci
|
11 |
+
|
12 |
+
# React client build
|
13 |
+
ENV NODE_OPTIONS="--max-old-space-size=2048"
|
14 |
+
RUN npm run frontend
|
15 |
+
|
16 |
+
# Node API setup
|
17 |
+
EXPOSE 3080
|
18 |
+
ENV HOST=0.0.0.0
|
19 |
+
CMD ["npm", "run", "backend"]
|
20 |
+
|
21 |
+
# Optional: for client with nginx routing
|
22 |
+
# FROM nginx:stable-alpine AS nginx-client
|
23 |
+
# WORKDIR /usr/share/nginx/html
|
24 |
+
# COPY --from=node /app/client/dist /usr/share/nginx/html
|
25 |
+
# COPY client/nginx.conf /etc/nginx/conf.d/default.conf
|
26 |
+
# ENTRYPOINT ["nginx", "-g", "daemon off;"]
|
LICENSE.md
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# MIT License
|
2 |
+
|
3 |
+
Copyright (c) 2023 Danny Avila
|
4 |
+
|
5 |
+
---
|
6 |
+
|
7 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
8 |
+
of this software and associated documentation files (the "Software"), to deal
|
9 |
+
in the Software without restriction, including without limitation the rights
|
10 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11 |
+
copies of the Software, and to permit persons to whom the Software is
|
12 |
+
furnished to do so, subject to the following conditions:
|
13 |
+
|
14 |
+
The above copyright notice and this permission notice shall be included in all
|
15 |
+
copies or substantial portions of the Software.
|
16 |
+
|
17 |
+
##
|
18 |
+
|
19 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
20 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
21 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
22 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
23 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
24 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
25 |
+
SOFTWARE.
|
26 |
+
|
27 |
+
---
|
28 |
+
|
29 |
+
## [Go Back to ReadMe](README.md)
|
README.md
CHANGED
@@ -1,11 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
---
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
pinned: false
|
8 |
-
license: mit
|
9 |
---
|
10 |
|
11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<p align="center">
|
2 |
+
<a href="https://docs.librechat.ai">
|
3 |
+
<img src="docs/assets/LibreChat.svg" height="256">
|
4 |
+
</a>
|
5 |
+
<a href="https://docs.librechat.ai">
|
6 |
+
<h1 align="center">LibreChat</h1>
|
7 |
+
</a>
|
8 |
+
</p>
|
9 |
+
|
10 |
+
<p align="center">
|
11 |
+
<a href="https://discord.gg/NGaa9RPCft">
|
12 |
+
<img
|
13 |
+
src="https://img.shields.io/discord/1086345563026489514?label=&logo=discord&style=for-the-badge&logoWidth=20&logoColor=white&labelColor=000000&color=blueviolet">
|
14 |
+
</a>
|
15 |
+
<a href="https://www.youtube.com/@LibreChat">
|
16 |
+
<img
|
17 |
+
src="https://img.shields.io/badge/YOUTUBE-red.svg?style=for-the-badge&logo=youtube&logoColor=white&labelColor=000000&logoWidth=20">
|
18 |
+
</a>
|
19 |
+
<a href="https://docs.librechat.ai">
|
20 |
+
<img
|
21 |
+
src="https://img.shields.io/badge/DOCS-blue.svg?style=for-the-badge&logo=read-the-docs&logoColor=white&labelColor=000000&logoWidth=20">
|
22 |
+
</a>
|
23 |
+
<a aria-label="Sponsors" href="#sponsors">
|
24 |
+
<img
|
25 |
+
src="https://img.shields.io/badge/SPONSORS-brightgreen.svg?style=for-the-badge&logo=github-sponsors&logoColor=white&labelColor=000000&logoWidth=20">
|
26 |
+
</a>
|
27 |
+
</p>
|
28 |
+
|
29 |
+
## All-In-One AI Conversations with LibreChat ##
|
30 |
+
LibreChat brings together the future of assistant AIs with the revolutionary technology of OpenAI's ChatGPT. Celebrating the original styling, LibreChat gives you the ability to integrate multiple AI models. It also integrates and enhances original client features such as conversation and message search, prompt templates and plugins.
|
31 |
+
|
32 |
+
With LibreChat, you no longer need to opt for ChatGPT Plus and can instead use free or pay-per-call APIs. We welcome contributions, cloning, and forking to enhance the capabilities of this advanced chatbot platform.
|
33 |
+
|
34 |
+
<!-- https://github.com/danny-avila/LibreChat/assets/110412045/c1eb0c0f-41f6-4335-b982-84b278b53d59 -->
|
35 |
+
|
36 |
+
[![Watch the video](https://img.youtube.com/vi/pNIOs1ovsXw/maxresdefault.jpg)](https://youtu.be/pNIOs1ovsXw)
|
37 |
+
Click on the thumbnail to open the video☝️
|
38 |
+
|
39 |
+
# Features
|
40 |
+
- Response streaming identical to ChatGPT through server-sent events
|
41 |
+
- UI from original ChatGPT, including Dark mode
|
42 |
+
- AI model selection: OpenAI API, BingAI, ChatGPT Browser, PaLM2, Anthropic (Claude), Plugins
|
43 |
+
- Create, Save, & Share custom presets - [More info on prompt presets here](https://github.com/danny-avila/LibreChat/releases/tag/v0.3.0)
|
44 |
+
- Edit and Resubmit messages with conversation branching
|
45 |
+
- Search all messages/conversations - [More info here](https://github.com/danny-avila/LibreChat/releases/tag/v0.1.0)
|
46 |
+
- Plugins now available (including web access, image generation and more)
|
47 |
+
|
48 |
+
---
|
49 |
+
|
50 |
+
## ⚠️ [Breaking Changes](docs/general_info/breaking_changes.md) ⚠️
|
51 |
+
**Applies to [v0.5.4](docs/general_info/breaking_changes.md#v054) & [v0.5.5](docs/general_info/breaking_changes.md#v055)**
|
52 |
+
|
53 |
+
**Please read this before updating from a previous version**
|
54 |
+
|
55 |
+
---
|
56 |
+
|
57 |
+
## Changelog
|
58 |
+
Keep up with the latest updates by visiting the releases page - [Releases](https://github.com/danny-avila/LibreChat/releases)
|
59 |
+
|
60 |
+
---
|
61 |
+
|
62 |
+
<h1>Table of Contents</h1>
|
63 |
+
|
64 |
+
<details open>
|
65 |
+
<summary><strong>Getting Started</strong></summary>
|
66 |
+
|
67 |
+
* [Docker Install](docs/install/docker_install.md)
|
68 |
+
* [Linux Install](docs/install/linux_install.md)
|
69 |
+
* [Mac Install](docs/install/mac_install.md)
|
70 |
+
* [Windows Install](docs/install/windows_install.md)
|
71 |
+
* [APIs and Tokens](docs/install/apis_and_tokens.md)
|
72 |
+
* [User Auth System](docs/install/user_auth_system.md)
|
73 |
+
* [Online MongoDB Database](docs/install/mongodb.md)
|
74 |
+
</details>
|
75 |
+
|
76 |
+
<details>
|
77 |
+
<summary><strong>General Information</strong></summary>
|
78 |
+
|
79 |
+
* [Code of Conduct](CODE_OF_CONDUCT.md)
|
80 |
+
* [Project Origin](docs/general_info/project_origin.md)
|
81 |
+
* [Multilingual Information](docs/general_info/multilingual_information.md)
|
82 |
+
* [Tech Stack](docs/general_info/tech_stack.md)
|
83 |
+
</details>
|
84 |
+
|
85 |
+
<details>
|
86 |
+
<summary><strong>Features</strong></summary>
|
87 |
+
|
88 |
+
* **Plugins**
|
89 |
+
* [Introduction](docs/features/plugins/introduction.md)
|
90 |
+
* [Google](docs/features/plugins/google_search.md)
|
91 |
+
* [Stable Diffusion](docs/features/plugins/stable_diffusion.md)
|
92 |
+
* [Wolfram](docs/features/plugins/wolfram.md)
|
93 |
+
* [Make Your Own Plugin](docs/features/plugins/make_your_own.md)
|
94 |
+
* [Using official ChatGPT Plugins](docs/features/plugins/chatgpt_plugins_openapi.md)
|
95 |
+
|
96 |
+
* [Proxy](docs/features/proxy.md)
|
97 |
+
* [Bing Jailbreak](docs/features/bing_jailbreak.md)
|
98 |
+
</details>
|
99 |
+
|
100 |
+
<details>
|
101 |
+
<summary><strong>Cloud Deployment</strong></summary>
|
102 |
+
|
103 |
+
* [Hetzner](docs/deployment/hetzner_ubuntu.md)
|
104 |
+
* [Heroku](docs/deployment/heroku.md)
|
105 |
+
* [Linode](docs/deployment/linode.md)
|
106 |
+
* [Cloudflare](docs/deployment/cloudflare.md)
|
107 |
+
* [Ngrok](docs/deployment/ngrok.md)
|
108 |
+
* [Render](docs/deployment/render.md)
|
109 |
+
</details>
|
110 |
+
|
111 |
+
<details>
|
112 |
+
<summary><strong>Contributions</strong></summary>
|
113 |
+
|
114 |
+
* [Contributor Guidelines](CONTRIBUTING.md)
|
115 |
+
* [Documentation Guidelines](docs/contributions/documentation_guidelines.md)
|
116 |
+
* [Code Standards and Conventions](docs/contributions/coding_conventions.md)
|
117 |
+
* [Testing](docs/contributions/testing.md)
|
118 |
+
* [Security](SECURITY.md)
|
119 |
+
* [Trello Board](https://trello.com/b/17z094kq/LibreChate)
|
120 |
+
</details>
|
121 |
+
|
122 |
+
|
123 |
+
---
|
124 |
+
|
125 |
+
## Star History
|
126 |
+
|
127 |
+
[![Star History Chart](https://api.star-history.com/svg?repos=danny-avila/LibreChat&type=Date)](https://star-history.com/#danny-avila/LibreChat&Date)
|
128 |
+
|
129 |
+
---
|
130 |
+
|
131 |
+
## Sponsors
|
132 |
+
|
133 |
+
Sponsored by <a href="https://github.com/mjtechguy"><b>@mjtechguy</b></a>, <a href="https://github.com/SphaeroX"><b>@SphaeroX</b></a>, <a href="https://github.com/DavidDev1334"><b>@DavidDev1334</b></a>, <a href="https://github.com/fuegovic"><b>@fuegovic</b></a>, <a href="https://github.com/Pharrcyde"><b>@Pharrcyde</b></a>
|
134 |
+
|
135 |
---
|
136 |
+
|
137 |
+
## Contributors
|
138 |
+
Contributions and suggestions bug reports and fixes are welcome!
|
139 |
+
Please read the documentation before you do!
|
140 |
+
|
|
|
|
|
141 |
---
|
142 |
|
143 |
+
For new features, components, or extensions, please open an issue and discuss before sending a PR.
|
144 |
+
|
145 |
+
- Join the [Discord community](https://discord.gg/uDyZ5Tzhct)
|
146 |
+
|
147 |
+
This project exists in its current state thanks to all the people who contribute
|
148 |
+
---
|
149 |
+
<a href="https://github.com/danny-avila/LibreChat/graphs/contributors">
|
150 |
+
<img src="https://contrib.rocks/image?repo=danny-avila/LibreChat" />
|
151 |
+
</a>
|
SECURITY.md
ADDED
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Security Policy
|
2 |
+
|
3 |
+
At LibreChat, we prioritize the security of our project and value the contributions of security researchers in helping us improve the security of our codebase. If you discover a security vulnerability within our project, we appreciate your responsible disclosure. Please follow the guidelines below to report any vulnerabilities to us:
|
4 |
+
|
5 |
+
**Note: Only report sensitive vulnerability details via the appropriate private communication channels mentioned below. Public channels, such as GitHub issues and Discord, should be used for initiating contact and establishing private communication channels.**
|
6 |
+
|
7 |
+
## Communication Channels
|
8 |
+
|
9 |
+
When reporting a security vulnerability, you have the following options to reach out to us:
|
10 |
+
|
11 |
+
- **Option 1: GitHub Security Advisory System**: We encourage you to use GitHub's Security Advisory system to report any security vulnerabilities you find. This allows us to receive vulnerability reports directly through GitHub. For more information on how to submit a security advisory report, please refer to the [GitHub Security Advisories documentation](https://docs.github.com/en/code-security/getting-started-with-security-vulnerability-alerts/about-github-security-advisories).
|
12 |
+
|
13 |
+
- **Option 2: GitHub Issues**: You can initiate first contact via GitHub Issues. However, please note that initial contact through GitHub Issues should not include any sensitive details.
|
14 |
+
|
15 |
+
- **Option 3: Discord Server**: You can join our [Discord community](https://discord.gg/5rbRxn4uME) and initiate first contact in the `#issues` channel. However, please ensure that initial contact through Discord does not include any sensitive details.
|
16 |
+
|
17 |
+
_After the initial contact, we will establish a private communication channel for further discussion._
|
18 |
+
|
19 |
+
### When submitting a vulnerability report, please provide us with the following information:
|
20 |
+
|
21 |
+
- A clear description of the vulnerability, including steps to reproduce it.
|
22 |
+
- The version(s) of the project affected by the vulnerability.
|
23 |
+
- Any additional information that may be useful for understanding and addressing the issue.
|
24 |
+
|
25 |
+
We strive to acknowledge vulnerability reports within 72 hours and will keep you informed of the progress towards resolution.
|
26 |
+
|
27 |
+
## Security Updates and Patching
|
28 |
+
|
29 |
+
We are committed to maintaining the security of our open-source project, LibreChat, and promptly addressing any identified vulnerabilities. To ensure the security of our project, we adhere to the following practices:
|
30 |
+
|
31 |
+
- We prioritize security updates for the current major release of our software.
|
32 |
+
- We actively monitor the GitHub Security Advisory system and the `#issues` channel on Discord for any vulnerability reports.
|
33 |
+
- We promptly review and validate reported vulnerabilities and take appropriate actions to address them.
|
34 |
+
- We release security patches and updates in a timely manner to mitigate any identified vulnerabilities.
|
35 |
+
|
36 |
+
Please note that as a security-conscious community, we may not always disclose detailed information about security issues until we have determined that doing so would not put our users or the project at risk. We appreciate your understanding and cooperation in these matters.
|
37 |
+
|
38 |
+
## Scope
|
39 |
+
|
40 |
+
This security policy applies to the following GitHub repository:
|
41 |
+
|
42 |
+
- Repository: [LibreChat](https://github.com/danny-avila/LibreChat)
|
43 |
+
|
44 |
+
## Contact
|
45 |
+
|
46 |
+
If you have any questions or concerns regarding the security of our project, please join our [Discord community](https://discord.gg/NGaa9RPCft) and report them in the appropriate channel. You can also reach out to us by [opening an issue](https://github.com/danny-avila/LibreChat/issues/new) on GitHub. Please note that the response time may vary depending on the nature and severity of the inquiry.
|
47 |
+
|
48 |
+
## Acknowledgments
|
49 |
+
|
50 |
+
We would like to express our gratitude to the security researchers and community members who help us improve the security of our project. Your contributions are invaluable, and we sincerely appreciate your efforts.
|
51 |
+
|
52 |
+
## Bug Bounty Program
|
53 |
+
|
54 |
+
We currently do not have a bug bounty program in place. However, we welcome and appreciate any
|
55 |
+
|
56 |
+
security-related contributions through pull requests (PRs) that address vulnerabilities in our codebase. We believe in the power of collaboration to improve the security of our project and invite you to join us in making it more robust.
|
57 |
+
|
58 |
+
**Reference**
|
59 |
+
- https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html
|
60 |
+
|
61 |
+
---
|
62 |
+
|
63 |
+
## [Go Back to ReadMe](README.md)
|
api/app/bingai.js
ADDED
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
require('dotenv').config();
|
2 |
+
const { KeyvFile } = require('keyv-file');
|
3 |
+
|
4 |
+
const askBing = async ({
|
5 |
+
text,
|
6 |
+
parentMessageId,
|
7 |
+
conversationId,
|
8 |
+
jailbreak,
|
9 |
+
jailbreakConversationId,
|
10 |
+
context,
|
11 |
+
systemMessage,
|
12 |
+
conversationSignature,
|
13 |
+
clientId,
|
14 |
+
invocationId,
|
15 |
+
toneStyle,
|
16 |
+
token,
|
17 |
+
onProgress,
|
18 |
+
}) => {
|
19 |
+
const { BingAIClient } = await import('@waylaidwanderer/chatgpt-api');
|
20 |
+
const store = {
|
21 |
+
store: new KeyvFile({ filename: './data/cache.json' }),
|
22 |
+
};
|
23 |
+
|
24 |
+
const bingAIClient = new BingAIClient({
|
25 |
+
// "_U" cookie from bing.com
|
26 |
+
// userToken:
|
27 |
+
// process.env.BINGAI_TOKEN == 'user_provided' ? token : process.env.BINGAI_TOKEN ?? null,
|
28 |
+
// If the above doesn't work, provide all your cookies as a string instead
|
29 |
+
cookies: process.env.BINGAI_TOKEN == 'user_provided' ? token : process.env.BINGAI_TOKEN ?? null,
|
30 |
+
debug: false,
|
31 |
+
cache: store,
|
32 |
+
host: process.env.BINGAI_HOST || null,
|
33 |
+
proxy: process.env.PROXY || null,
|
34 |
+
});
|
35 |
+
|
36 |
+
let options = {};
|
37 |
+
|
38 |
+
if (jailbreakConversationId == 'false') {
|
39 |
+
jailbreakConversationId = false;
|
40 |
+
}
|
41 |
+
|
42 |
+
if (jailbreak) {
|
43 |
+
options = {
|
44 |
+
jailbreakConversationId: jailbreakConversationId || jailbreak,
|
45 |
+
context,
|
46 |
+
systemMessage,
|
47 |
+
parentMessageId,
|
48 |
+
toneStyle,
|
49 |
+
onProgress,
|
50 |
+
clientOptions: {
|
51 |
+
features: {
|
52 |
+
genImage: {
|
53 |
+
server: {
|
54 |
+
enable: true,
|
55 |
+
type: 'markdown_list',
|
56 |
+
},
|
57 |
+
},
|
58 |
+
},
|
59 |
+
},
|
60 |
+
};
|
61 |
+
} else {
|
62 |
+
options = {
|
63 |
+
conversationId,
|
64 |
+
context,
|
65 |
+
systemMessage,
|
66 |
+
parentMessageId,
|
67 |
+
toneStyle,
|
68 |
+
onProgress,
|
69 |
+
clientOptions: {
|
70 |
+
features: {
|
71 |
+
genImage: {
|
72 |
+
server: {
|
73 |
+
enable: true,
|
74 |
+
type: 'markdown_list',
|
75 |
+
},
|
76 |
+
},
|
77 |
+
},
|
78 |
+
},
|
79 |
+
};
|
80 |
+
|
81 |
+
// don't give those parameters for new conversation
|
82 |
+
// for new conversation, conversationSignature always is null
|
83 |
+
if (conversationSignature) {
|
84 |
+
options.conversationSignature = conversationSignature;
|
85 |
+
options.clientId = clientId;
|
86 |
+
options.invocationId = invocationId;
|
87 |
+
}
|
88 |
+
}
|
89 |
+
|
90 |
+
console.log('bing options', options);
|
91 |
+
|
92 |
+
const res = await bingAIClient.sendMessage(text, options);
|
93 |
+
|
94 |
+
return res;
|
95 |
+
|
96 |
+
// for reference:
|
97 |
+
// https://github.com/waylaidwanderer/node-chatgpt-api/blob/main/demos/use-bing-client.js
|
98 |
+
};
|
99 |
+
|
100 |
+
module.exports = { askBing };
|
api/app/chatgpt-browser.js
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
require('dotenv').config();
|
2 |
+
const { KeyvFile } = require('keyv-file');
|
3 |
+
|
4 |
+
const browserClient = async ({
|
5 |
+
text,
|
6 |
+
parentMessageId,
|
7 |
+
conversationId,
|
8 |
+
model,
|
9 |
+
token,
|
10 |
+
onProgress,
|
11 |
+
onEventMessage,
|
12 |
+
abortController,
|
13 |
+
userId,
|
14 |
+
}) => {
|
15 |
+
const { ChatGPTBrowserClient } = await import('@waylaidwanderer/chatgpt-api');
|
16 |
+
const store = {
|
17 |
+
store: new KeyvFile({ filename: './data/cache.json' }),
|
18 |
+
};
|
19 |
+
|
20 |
+
const clientOptions = {
|
21 |
+
// Warning: This will expose your access token to a third party. Consider the risks before using this.
|
22 |
+
reverseProxyUrl:
|
23 |
+
process.env.CHATGPT_REVERSE_PROXY || 'https://ai.fakeopen.com/api/conversation',
|
24 |
+
// Access token from https://chat.openai.com/api/auth/session
|
25 |
+
accessToken:
|
26 |
+
process.env.CHATGPT_TOKEN == 'user_provided' ? token : process.env.CHATGPT_TOKEN ?? null,
|
27 |
+
model: model,
|
28 |
+
debug: false,
|
29 |
+
proxy: process.env.PROXY || null,
|
30 |
+
user: userId,
|
31 |
+
};
|
32 |
+
|
33 |
+
const client = new ChatGPTBrowserClient(clientOptions, store);
|
34 |
+
let options = { onProgress, onEventMessage, abortController };
|
35 |
+
|
36 |
+
if (!!parentMessageId && !!conversationId) {
|
37 |
+
options = { ...options, parentMessageId, conversationId };
|
38 |
+
}
|
39 |
+
|
40 |
+
console.log('gptBrowser clientOptions', clientOptions);
|
41 |
+
|
42 |
+
if (parentMessageId === '00000000-0000-0000-0000-000000000000') {
|
43 |
+
delete options.conversationId;
|
44 |
+
}
|
45 |
+
|
46 |
+
const res = await client.sendMessage(text, options);
|
47 |
+
return res;
|
48 |
+
};
|
49 |
+
|
50 |
+
module.exports = { browserClient };
|
api/app/clients/AnthropicClient.js
ADDED
@@ -0,0 +1,324 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const Keyv = require('keyv');
|
2 |
+
// const { Agent, ProxyAgent } = require('undici');
|
3 |
+
const BaseClient = require('./BaseClient');
|
4 |
+
const {
|
5 |
+
encoding_for_model: encodingForModel,
|
6 |
+
get_encoding: getEncoding,
|
7 |
+
} = require('@dqbd/tiktoken');
|
8 |
+
const Anthropic = require('@anthropic-ai/sdk');
|
9 |
+
|
10 |
+
const HUMAN_PROMPT = '\n\nHuman:';
|
11 |
+
const AI_PROMPT = '\n\nAssistant:';
|
12 |
+
|
13 |
+
const tokenizersCache = {};
|
14 |
+
|
15 |
+
class AnthropicClient extends BaseClient {
|
16 |
+
constructor(apiKey, options = {}, cacheOptions = {}) {
|
17 |
+
super(apiKey, options, cacheOptions);
|
18 |
+
cacheOptions.namespace = cacheOptions.namespace || 'anthropic';
|
19 |
+
this.conversationsCache = new Keyv(cacheOptions);
|
20 |
+
this.apiKey = apiKey || process.env.ANTHROPIC_API_KEY;
|
21 |
+
this.sender = 'Anthropic';
|
22 |
+
this.userLabel = HUMAN_PROMPT;
|
23 |
+
this.assistantLabel = AI_PROMPT;
|
24 |
+
this.setOptions(options);
|
25 |
+
}
|
26 |
+
|
27 |
+
setOptions(options) {
|
28 |
+
if (this.options && !this.options.replaceOptions) {
|
29 |
+
// nested options aren't spread properly, so we need to do this manually
|
30 |
+
this.options.modelOptions = {
|
31 |
+
...this.options.modelOptions,
|
32 |
+
...options.modelOptions,
|
33 |
+
};
|
34 |
+
delete options.modelOptions;
|
35 |
+
// now we can merge options
|
36 |
+
this.options = {
|
37 |
+
...this.options,
|
38 |
+
...options,
|
39 |
+
};
|
40 |
+
} else {
|
41 |
+
this.options = options;
|
42 |
+
}
|
43 |
+
|
44 |
+
const modelOptions = this.options.modelOptions || {};
|
45 |
+
this.modelOptions = {
|
46 |
+
...modelOptions,
|
47 |
+
// set some good defaults (check for undefined in some cases because they may be 0)
|
48 |
+
model: modelOptions.model || 'claude-1',
|
49 |
+
temperature: typeof modelOptions.temperature === 'undefined' ? 0.7 : modelOptions.temperature, // 0 - 1, 0.7 is recommended
|
50 |
+
topP: typeof modelOptions.topP === 'undefined' ? 0.7 : modelOptions.topP, // 0 - 1, default: 0.7
|
51 |
+
topK: typeof modelOptions.topK === 'undefined' ? 40 : modelOptions.topK, // 1-40, default: 40
|
52 |
+
stop: modelOptions.stop, // no stop method for now
|
53 |
+
};
|
54 |
+
|
55 |
+
this.maxContextTokens = this.options.maxContextTokens || 99999;
|
56 |
+
this.maxResponseTokens = this.modelOptions.maxOutputTokens || 1500;
|
57 |
+
this.maxPromptTokens =
|
58 |
+
this.options.maxPromptTokens || this.maxContextTokens - this.maxResponseTokens;
|
59 |
+
|
60 |
+
if (this.maxPromptTokens + this.maxResponseTokens > this.maxContextTokens) {
|
61 |
+
throw new Error(
|
62 |
+
`maxPromptTokens + maxOutputTokens (${this.maxPromptTokens} + ${this.maxResponseTokens} = ${
|
63 |
+
this.maxPromptTokens + this.maxResponseTokens
|
64 |
+
}) must be less than or equal to maxContextTokens (${this.maxContextTokens})`,
|
65 |
+
);
|
66 |
+
}
|
67 |
+
|
68 |
+
this.startToken = '||>';
|
69 |
+
this.endToken = '';
|
70 |
+
this.gptEncoder = this.constructor.getTokenizer('cl100k_base');
|
71 |
+
|
72 |
+
if (!this.modelOptions.stop) {
|
73 |
+
const stopTokens = [this.startToken];
|
74 |
+
if (this.endToken && this.endToken !== this.startToken) {
|
75 |
+
stopTokens.push(this.endToken);
|
76 |
+
}
|
77 |
+
stopTokens.push(`${this.userLabel}`);
|
78 |
+
stopTokens.push('<|diff_marker|>');
|
79 |
+
|
80 |
+
this.modelOptions.stop = stopTokens;
|
81 |
+
}
|
82 |
+
|
83 |
+
return this;
|
84 |
+
}
|
85 |
+
|
86 |
+
getClient() {
|
87 |
+
if (this.options.reverseProxyUrl) {
|
88 |
+
return new Anthropic({
|
89 |
+
apiKey: this.apiKey,
|
90 |
+
baseURL: this.options.reverseProxyUrl,
|
91 |
+
});
|
92 |
+
} else {
|
93 |
+
return new Anthropic({
|
94 |
+
apiKey: this.apiKey,
|
95 |
+
});
|
96 |
+
}
|
97 |
+
}
|
98 |
+
|
99 |
+
async buildMessages(messages, parentMessageId) {
|
100 |
+
const orderedMessages = this.constructor.getMessagesForConversation(messages, parentMessageId);
|
101 |
+
if (this.options.debug) {
|
102 |
+
console.debug('AnthropicClient: orderedMessages', orderedMessages, parentMessageId);
|
103 |
+
}
|
104 |
+
|
105 |
+
const formattedMessages = orderedMessages.map((message) => ({
|
106 |
+
author: message.isCreatedByUser ? this.userLabel : this.assistantLabel,
|
107 |
+
content: message?.content ?? message.text,
|
108 |
+
}));
|
109 |
+
|
110 |
+
let identityPrefix = '';
|
111 |
+
if (this.options.userLabel) {
|
112 |
+
identityPrefix = `\nHuman's name: ${this.options.userLabel}`;
|
113 |
+
}
|
114 |
+
|
115 |
+
if (this.options.modelLabel) {
|
116 |
+
identityPrefix = `${identityPrefix}\nYou are ${this.options.modelLabel}`;
|
117 |
+
}
|
118 |
+
|
119 |
+
let promptPrefix = (this.options.promptPrefix || '').trim();
|
120 |
+
if (promptPrefix) {
|
121 |
+
// If the prompt prefix doesn't end with the end token, add it.
|
122 |
+
if (!promptPrefix.endsWith(`${this.endToken}`)) {
|
123 |
+
promptPrefix = `${promptPrefix.trim()}${this.endToken}\n\n`;
|
124 |
+
}
|
125 |
+
promptPrefix = `\nContext:\n${promptPrefix}`;
|
126 |
+
}
|
127 |
+
|
128 |
+
if (identityPrefix) {
|
129 |
+
promptPrefix = `${identityPrefix}${promptPrefix}`;
|
130 |
+
}
|
131 |
+
|
132 |
+
const promptSuffix = `${promptPrefix}${this.assistantLabel}\n`; // Prompt AI to respond.
|
133 |
+
let currentTokenCount = this.getTokenCount(promptSuffix);
|
134 |
+
|
135 |
+
let promptBody = '';
|
136 |
+
const maxTokenCount = this.maxPromptTokens;
|
137 |
+
|
138 |
+
const context = [];
|
139 |
+
|
140 |
+
// Iterate backwards through the messages, adding them to the prompt until we reach the max token count.
|
141 |
+
// Do this within a recursive async function so that it doesn't block the event loop for too long.
|
142 |
+
// Also, remove the next message when the message that puts us over the token limit is created by the user.
|
143 |
+
// Otherwise, remove only the exceeding message. This is due to Anthropic's strict payload rule to start with "Human:".
|
144 |
+
const nextMessage = {
|
145 |
+
remove: false,
|
146 |
+
tokenCount: 0,
|
147 |
+
messageString: '',
|
148 |
+
};
|
149 |
+
|
150 |
+
const buildPromptBody = async () => {
|
151 |
+
if (currentTokenCount < maxTokenCount && formattedMessages.length > 0) {
|
152 |
+
const message = formattedMessages.pop();
|
153 |
+
const isCreatedByUser = message.author === this.userLabel;
|
154 |
+
const messageString = `${message.author}\n${message.content}${this.endToken}\n`;
|
155 |
+
let newPromptBody = `${messageString}${promptBody}`;
|
156 |
+
|
157 |
+
context.unshift(message);
|
158 |
+
|
159 |
+
const tokenCountForMessage = this.getTokenCount(messageString);
|
160 |
+
const newTokenCount = currentTokenCount + tokenCountForMessage;
|
161 |
+
|
162 |
+
if (!isCreatedByUser) {
|
163 |
+
nextMessage.messageString = messageString;
|
164 |
+
nextMessage.tokenCount = tokenCountForMessage;
|
165 |
+
}
|
166 |
+
|
167 |
+
if (newTokenCount > maxTokenCount) {
|
168 |
+
if (!promptBody) {
|
169 |
+
// This is the first message, so we can't add it. Just throw an error.
|
170 |
+
throw new Error(
|
171 |
+
`Prompt is too long. Max token count is ${maxTokenCount}, but prompt is ${newTokenCount} tokens long.`,
|
172 |
+
);
|
173 |
+
}
|
174 |
+
|
175 |
+
// Otherwise, ths message would put us over the token limit, so don't add it.
|
176 |
+
// if created by user, remove next message, otherwise remove only this message
|
177 |
+
if (isCreatedByUser) {
|
178 |
+
nextMessage.remove = true;
|
179 |
+
}
|
180 |
+
|
181 |
+
return false;
|
182 |
+
}
|
183 |
+
promptBody = newPromptBody;
|
184 |
+
currentTokenCount = newTokenCount;
|
185 |
+
// wait for next tick to avoid blocking the event loop
|
186 |
+
await new Promise((resolve) => setImmediate(resolve));
|
187 |
+
return buildPromptBody();
|
188 |
+
}
|
189 |
+
return true;
|
190 |
+
};
|
191 |
+
|
192 |
+
await buildPromptBody();
|
193 |
+
|
194 |
+
if (nextMessage.remove) {
|
195 |
+
promptBody = promptBody.replace(nextMessage.messageString, '');
|
196 |
+
currentTokenCount -= nextMessage.tokenCount;
|
197 |
+
context.shift();
|
198 |
+
}
|
199 |
+
|
200 |
+
const prompt = `${promptBody}${promptSuffix}`;
|
201 |
+
// Add 2 tokens for metadata after all messages have been counted.
|
202 |
+
currentTokenCount += 2;
|
203 |
+
|
204 |
+
// Use up to `this.maxContextTokens` tokens (prompt + response), but try to leave `this.maxTokens` tokens for the response.
|
205 |
+
this.modelOptions.maxOutputTokens = Math.min(
|
206 |
+
this.maxContextTokens - currentTokenCount,
|
207 |
+
this.maxResponseTokens,
|
208 |
+
);
|
209 |
+
|
210 |
+
return { prompt, context };
|
211 |
+
}
|
212 |
+
|
213 |
+
getCompletion() {
|
214 |
+
console.log('AnthropicClient doesn\'t use getCompletion (all handled in sendCompletion)');
|
215 |
+
}
|
216 |
+
|
217 |
+
// TODO: implement abortController usage
|
218 |
+
async sendCompletion(payload, { onProgress, abortController }) {
|
219 |
+
if (!abortController) {
|
220 |
+
abortController = new AbortController();
|
221 |
+
}
|
222 |
+
|
223 |
+
const { signal } = abortController;
|
224 |
+
|
225 |
+
const modelOptions = { ...this.modelOptions };
|
226 |
+
if (typeof onProgress === 'function') {
|
227 |
+
modelOptions.stream = true;
|
228 |
+
}
|
229 |
+
|
230 |
+
const { debug } = this.options;
|
231 |
+
if (debug) {
|
232 |
+
console.debug();
|
233 |
+
console.debug(modelOptions);
|
234 |
+
console.debug();
|
235 |
+
}
|
236 |
+
|
237 |
+
const client = this.getClient();
|
238 |
+
const metadata = {
|
239 |
+
user_id: this.user,
|
240 |
+
};
|
241 |
+
|
242 |
+
let text = '';
|
243 |
+
const requestOptions = {
|
244 |
+
prompt: payload,
|
245 |
+
model: this.modelOptions.model,
|
246 |
+
stream: this.modelOptions.stream || true,
|
247 |
+
max_tokens_to_sample: this.modelOptions.maxOutputTokens || 1500,
|
248 |
+
metadata,
|
249 |
+
...modelOptions,
|
250 |
+
};
|
251 |
+
if (this.options.debug) {
|
252 |
+
console.log('AnthropicClient: requestOptions');
|
253 |
+
console.dir(requestOptions, { depth: null });
|
254 |
+
}
|
255 |
+
const response = await client.completions.create(requestOptions);
|
256 |
+
|
257 |
+
signal.addEventListener('abort', () => {
|
258 |
+
if (this.options.debug) {
|
259 |
+
console.log('AnthropicClient: message aborted!');
|
260 |
+
}
|
261 |
+
response.controller.abort();
|
262 |
+
});
|
263 |
+
|
264 |
+
for await (const completion of response) {
|
265 |
+
if (this.options.debug) {
|
266 |
+
// Uncomment to debug message stream
|
267 |
+
// console.debug(completion);
|
268 |
+
}
|
269 |
+
text += completion.completion;
|
270 |
+
onProgress(completion.completion);
|
271 |
+
}
|
272 |
+
|
273 |
+
signal.removeEventListener('abort', () => {
|
274 |
+
if (this.options.debug) {
|
275 |
+
console.log('AnthropicClient: message aborted!');
|
276 |
+
}
|
277 |
+
response.controller.abort();
|
278 |
+
});
|
279 |
+
|
280 |
+
return text.trim();
|
281 |
+
}
|
282 |
+
|
283 |
+
// I commented this out because I will need to refactor this for the BaseClient/all clients
|
284 |
+
// getMessageMapMethod() {
|
285 |
+
// return ((message) => ({
|
286 |
+
// author: message.isCreatedByUser ? this.userLabel : this.assistantLabel,
|
287 |
+
// content: message?.content ?? message.text
|
288 |
+
// })).bind(this);
|
289 |
+
// }
|
290 |
+
|
291 |
+
getSaveOptions() {
|
292 |
+
return {
|
293 |
+
promptPrefix: this.options.promptPrefix,
|
294 |
+
modelLabel: this.options.modelLabel,
|
295 |
+
...this.modelOptions,
|
296 |
+
};
|
297 |
+
}
|
298 |
+
|
299 |
+
getBuildMessagesOptions() {
|
300 |
+
if (this.options.debug) {
|
301 |
+
console.log('AnthropicClient doesn\'t use getBuildMessagesOptions');
|
302 |
+
}
|
303 |
+
}
|
304 |
+
|
305 |
+
static getTokenizer(encoding, isModelName = false, extendSpecialTokens = {}) {
|
306 |
+
if (tokenizersCache[encoding]) {
|
307 |
+
return tokenizersCache[encoding];
|
308 |
+
}
|
309 |
+
let tokenizer;
|
310 |
+
if (isModelName) {
|
311 |
+
tokenizer = encodingForModel(encoding, extendSpecialTokens);
|
312 |
+
} else {
|
313 |
+
tokenizer = getEncoding(encoding, extendSpecialTokens);
|
314 |
+
}
|
315 |
+
tokenizersCache[encoding] = tokenizer;
|
316 |
+
return tokenizer;
|
317 |
+
}
|
318 |
+
|
319 |
+
getTokenCount(text) {
|
320 |
+
return this.gptEncoder.encode(text, 'all').length;
|
321 |
+
}
|
322 |
+
}
|
323 |
+
|
324 |
+
module.exports = AnthropicClient;
|
api/app/clients/BaseClient.js
ADDED
@@ -0,0 +1,561 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const crypto = require('crypto');
|
2 |
+
const TextStream = require('./TextStream');
|
3 |
+
const { RecursiveCharacterTextSplitter } = require('langchain/text_splitter');
|
4 |
+
const { ChatOpenAI } = require('langchain/chat_models/openai');
|
5 |
+
const { loadSummarizationChain } = require('langchain/chains');
|
6 |
+
const { refinePrompt } = require('./prompts/refinePrompt');
|
7 |
+
const { getConvo, getMessages, saveMessage, updateMessage, saveConvo } = require('../../models');
|
8 |
+
|
9 |
+
class BaseClient {
|
10 |
+
constructor(apiKey, options = {}) {
|
11 |
+
this.apiKey = apiKey;
|
12 |
+
this.sender = options.sender || 'AI';
|
13 |
+
this.contextStrategy = null;
|
14 |
+
this.currentDateString = new Date().toLocaleDateString('en-us', {
|
15 |
+
year: 'numeric',
|
16 |
+
month: 'long',
|
17 |
+
day: 'numeric',
|
18 |
+
});
|
19 |
+
}
|
20 |
+
|
21 |
+
setOptions() {
|
22 |
+
throw new Error('Method \'setOptions\' must be implemented.');
|
23 |
+
}
|
24 |
+
|
25 |
+
getCompletion() {
|
26 |
+
throw new Error('Method \'getCompletion\' must be implemented.');
|
27 |
+
}
|
28 |
+
|
29 |
+
async sendCompletion() {
|
30 |
+
throw new Error('Method \'sendCompletion\' must be implemented.');
|
31 |
+
}
|
32 |
+
|
33 |
+
getSaveOptions() {
|
34 |
+
throw new Error('Subclasses must implement getSaveOptions');
|
35 |
+
}
|
36 |
+
|
37 |
+
async buildMessages() {
|
38 |
+
throw new Error('Subclasses must implement buildMessages');
|
39 |
+
}
|
40 |
+
|
41 |
+
getBuildMessagesOptions() {
|
42 |
+
throw new Error('Subclasses must implement getBuildMessagesOptions');
|
43 |
+
}
|
44 |
+
|
45 |
+
async generateTextStream(text, onProgress, options = {}) {
|
46 |
+
const stream = new TextStream(text, options);
|
47 |
+
await stream.processTextStream(onProgress);
|
48 |
+
}
|
49 |
+
|
50 |
+
async setMessageOptions(opts = {}) {
|
51 |
+
if (opts && typeof opts === 'object') {
|
52 |
+
this.setOptions(opts);
|
53 |
+
}
|
54 |
+
const user = opts.user || null;
|
55 |
+
const conversationId = opts.conversationId || crypto.randomUUID();
|
56 |
+
const parentMessageId = opts.parentMessageId || '00000000-0000-0000-0000-000000000000';
|
57 |
+
const userMessageId = opts.overrideParentMessageId || crypto.randomUUID();
|
58 |
+
const responseMessageId = crypto.randomUUID();
|
59 |
+
const saveOptions = this.getSaveOptions();
|
60 |
+
this.abortController = opts.abortController || new AbortController();
|
61 |
+
this.currentMessages = (await this.loadHistory(conversationId, parentMessageId)) ?? [];
|
62 |
+
|
63 |
+
return {
|
64 |
+
...opts,
|
65 |
+
user,
|
66 |
+
conversationId,
|
67 |
+
parentMessageId,
|
68 |
+
userMessageId,
|
69 |
+
responseMessageId,
|
70 |
+
saveOptions,
|
71 |
+
};
|
72 |
+
}
|
73 |
+
|
74 |
+
createUserMessage({ messageId, parentMessageId, conversationId, text }) {
|
75 |
+
const userMessage = {
|
76 |
+
messageId,
|
77 |
+
parentMessageId,
|
78 |
+
conversationId,
|
79 |
+
sender: 'User',
|
80 |
+
text,
|
81 |
+
isCreatedByUser: true,
|
82 |
+
};
|
83 |
+
return userMessage;
|
84 |
+
}
|
85 |
+
|
86 |
+
async handleStartMethods(message, opts) {
|
87 |
+
const { user, conversationId, parentMessageId, userMessageId, responseMessageId, saveOptions } =
|
88 |
+
await this.setMessageOptions(opts);
|
89 |
+
|
90 |
+
const userMessage = this.createUserMessage({
|
91 |
+
messageId: userMessageId,
|
92 |
+
parentMessageId,
|
93 |
+
conversationId,
|
94 |
+
text: message,
|
95 |
+
});
|
96 |
+
|
97 |
+
if (typeof opts?.getIds === 'function') {
|
98 |
+
opts.getIds({
|
99 |
+
userMessage,
|
100 |
+
conversationId,
|
101 |
+
responseMessageId,
|
102 |
+
});
|
103 |
+
}
|
104 |
+
|
105 |
+
if (typeof opts?.onStart === 'function') {
|
106 |
+
opts.onStart(userMessage);
|
107 |
+
}
|
108 |
+
|
109 |
+
return {
|
110 |
+
...opts,
|
111 |
+
user,
|
112 |
+
conversationId,
|
113 |
+
responseMessageId,
|
114 |
+
saveOptions,
|
115 |
+
userMessage,
|
116 |
+
};
|
117 |
+
}
|
118 |
+
|
119 |
+
addInstructions(messages, instructions) {
|
120 |
+
const payload = [];
|
121 |
+
if (!instructions) {
|
122 |
+
return messages;
|
123 |
+
}
|
124 |
+
if (messages.length > 1) {
|
125 |
+
payload.push(...messages.slice(0, -1));
|
126 |
+
}
|
127 |
+
|
128 |
+
payload.push(instructions);
|
129 |
+
|
130 |
+
if (messages.length > 0) {
|
131 |
+
payload.push(messages[messages.length - 1]);
|
132 |
+
}
|
133 |
+
|
134 |
+
return payload;
|
135 |
+
}
|
136 |
+
|
137 |
+
async handleTokenCountMap(tokenCountMap) {
|
138 |
+
if (this.currentMessages.length === 0) {
|
139 |
+
return;
|
140 |
+
}
|
141 |
+
|
142 |
+
for (let i = 0; i < this.currentMessages.length; i++) {
|
143 |
+
// Skip the last message, which is the user message.
|
144 |
+
if (i === this.currentMessages.length - 1) {
|
145 |
+
break;
|
146 |
+
}
|
147 |
+
|
148 |
+
const message = this.currentMessages[i];
|
149 |
+
const { messageId } = message;
|
150 |
+
const update = {};
|
151 |
+
|
152 |
+
if (messageId === tokenCountMap.refined?.messageId) {
|
153 |
+
if (this.options.debug) {
|
154 |
+
console.debug(`Adding refined props to ${messageId}.`);
|
155 |
+
}
|
156 |
+
|
157 |
+
update.refinedMessageText = tokenCountMap.refined.content;
|
158 |
+
update.refinedTokenCount = tokenCountMap.refined.tokenCount;
|
159 |
+
}
|
160 |
+
|
161 |
+
if (message.tokenCount && !update.refinedTokenCount) {
|
162 |
+
if (this.options.debug) {
|
163 |
+
console.debug(`Skipping ${messageId}: already had a token count.`);
|
164 |
+
}
|
165 |
+
continue;
|
166 |
+
}
|
167 |
+
|
168 |
+
const tokenCount = tokenCountMap[messageId];
|
169 |
+
if (tokenCount) {
|
170 |
+
message.tokenCount = tokenCount;
|
171 |
+
update.tokenCount = tokenCount;
|
172 |
+
await this.updateMessageInDatabase({ messageId, ...update });
|
173 |
+
}
|
174 |
+
}
|
175 |
+
}
|
176 |
+
|
177 |
+
concatenateMessages(messages) {
|
178 |
+
return messages.reduce((acc, message) => {
|
179 |
+
const nameOrRole = message.name ?? message.role;
|
180 |
+
return acc + `${nameOrRole}:\n${message.content}\n\n`;
|
181 |
+
}, '');
|
182 |
+
}
|
183 |
+
|
184 |
+
async refineMessages(messagesToRefine, remainingContextTokens) {
|
185 |
+
const model = new ChatOpenAI({ temperature: 0 });
|
186 |
+
const chain = loadSummarizationChain(model, {
|
187 |
+
type: 'refine',
|
188 |
+
verbose: this.options.debug,
|
189 |
+
refinePrompt,
|
190 |
+
});
|
191 |
+
const splitter = new RecursiveCharacterTextSplitter({
|
192 |
+
chunkSize: 1500,
|
193 |
+
chunkOverlap: 100,
|
194 |
+
});
|
195 |
+
const userMessages = this.concatenateMessages(
|
196 |
+
messagesToRefine.filter((m) => m.role === 'user'),
|
197 |
+
);
|
198 |
+
const assistantMessages = this.concatenateMessages(
|
199 |
+
messagesToRefine.filter((m) => m.role !== 'user'),
|
200 |
+
);
|
201 |
+
const userDocs = await splitter.createDocuments([userMessages], [], {
|
202 |
+
chunkHeader: 'DOCUMENT NAME: User Message\n\n---\n\n',
|
203 |
+
appendChunkOverlapHeader: true,
|
204 |
+
});
|
205 |
+
const assistantDocs = await splitter.createDocuments([assistantMessages], [], {
|
206 |
+
chunkHeader: 'DOCUMENT NAME: Assistant Message\n\n---\n\n',
|
207 |
+
appendChunkOverlapHeader: true,
|
208 |
+
});
|
209 |
+
// const chunkSize = Math.round(concatenatedMessages.length / 512);
|
210 |
+
const input_documents = userDocs.concat(assistantDocs);
|
211 |
+
if (this.options.debug) {
|
212 |
+
console.debug('Refining messages...');
|
213 |
+
}
|
214 |
+
try {
|
215 |
+
const res = await chain.call({
|
216 |
+
input_documents,
|
217 |
+
signal: this.abortController.signal,
|
218 |
+
});
|
219 |
+
|
220 |
+
const refinedMessage = {
|
221 |
+
role: 'assistant',
|
222 |
+
content: res.output_text,
|
223 |
+
tokenCount: this.getTokenCount(res.output_text),
|
224 |
+
};
|
225 |
+
|
226 |
+
if (this.options.debug) {
|
227 |
+
console.debug('Refined messages', refinedMessage);
|
228 |
+
console.debug(
|
229 |
+
`remainingContextTokens: ${remainingContextTokens}, after refining: ${
|
230 |
+
remainingContextTokens - refinedMessage.tokenCount
|
231 |
+
}`,
|
232 |
+
);
|
233 |
+
}
|
234 |
+
|
235 |
+
return refinedMessage;
|
236 |
+
} catch (e) {
|
237 |
+
console.error('Error refining messages');
|
238 |
+
console.error(e);
|
239 |
+
return null;
|
240 |
+
}
|
241 |
+
}
|
242 |
+
|
243 |
+
/**
|
244 |
+
* This method processes an array of messages and returns a context of messages that fit within a token limit.
|
245 |
+
* It iterates over the messages from newest to oldest, adding them to the context until the token limit is reached.
|
246 |
+
* If the token limit would be exceeded by adding a message, that message and possibly the previous one are added to a separate array of messages to refine.
|
247 |
+
* The method uses `push` and `pop` operations for efficient array manipulation, and reverses the arrays at the end to maintain the original order of the messages.
|
248 |
+
* The method also includes a mechanism to avoid blocking the event loop by waiting for the next tick after each iteration.
|
249 |
+
*
|
250 |
+
* @param {Array} messages - An array of messages, each with a `tokenCount` property. The messages should be ordered from oldest to newest.
|
251 |
+
* @returns {Object} An object with three properties: `context`, `remainingContextTokens`, and `messagesToRefine`. `context` is an array of messages that fit within the token limit. `remainingContextTokens` is the number of tokens remaining within the limit after adding the messages to the context. `messagesToRefine` is an array of messages that were not added to the context because they would have exceeded the token limit.
|
252 |
+
*/
|
253 |
+
async getMessagesWithinTokenLimit(messages) {
|
254 |
+
let currentTokenCount = 0;
|
255 |
+
let context = [];
|
256 |
+
let messagesToRefine = [];
|
257 |
+
let refineIndex = -1;
|
258 |
+
let remainingContextTokens = this.maxContextTokens;
|
259 |
+
|
260 |
+
for (let i = messages.length - 1; i >= 0; i--) {
|
261 |
+
const message = messages[i];
|
262 |
+
const newTokenCount = currentTokenCount + message.tokenCount;
|
263 |
+
const exceededLimit = newTokenCount > this.maxContextTokens;
|
264 |
+
let shouldRefine = exceededLimit && this.shouldRefineContext;
|
265 |
+
let refineNextMessage = i !== 0 && i !== 1 && context.length > 0;
|
266 |
+
|
267 |
+
if (shouldRefine) {
|
268 |
+
messagesToRefine.push(message);
|
269 |
+
|
270 |
+
if (refineIndex === -1) {
|
271 |
+
refineIndex = i;
|
272 |
+
}
|
273 |
+
|
274 |
+
if (refineNextMessage) {
|
275 |
+
refineIndex = i + 1;
|
276 |
+
const removedMessage = context.pop();
|
277 |
+
messagesToRefine.push(removedMessage);
|
278 |
+
currentTokenCount -= removedMessage.tokenCount;
|
279 |
+
remainingContextTokens = this.maxContextTokens - currentTokenCount;
|
280 |
+
refineNextMessage = false;
|
281 |
+
}
|
282 |
+
|
283 |
+
continue;
|
284 |
+
} else if (exceededLimit) {
|
285 |
+
break;
|
286 |
+
}
|
287 |
+
|
288 |
+
context.push(message);
|
289 |
+
currentTokenCount = newTokenCount;
|
290 |
+
remainingContextTokens = this.maxContextTokens - currentTokenCount;
|
291 |
+
await new Promise((resolve) => setImmediate(resolve));
|
292 |
+
}
|
293 |
+
|
294 |
+
return {
|
295 |
+
context: context.reverse(),
|
296 |
+
remainingContextTokens,
|
297 |
+
messagesToRefine: messagesToRefine.reverse(),
|
298 |
+
refineIndex,
|
299 |
+
};
|
300 |
+
}
|
301 |
+
|
302 |
+
async handleContextStrategy({ instructions, orderedMessages, formattedMessages }) {
|
303 |
+
let payload = this.addInstructions(formattedMessages, instructions);
|
304 |
+
let orderedWithInstructions = this.addInstructions(orderedMessages, instructions);
|
305 |
+
let { context, remainingContextTokens, messagesToRefine, refineIndex } =
|
306 |
+
await this.getMessagesWithinTokenLimit(payload);
|
307 |
+
|
308 |
+
payload = context;
|
309 |
+
let refinedMessage;
|
310 |
+
|
311 |
+
// if (messagesToRefine.length > 0) {
|
312 |
+
// refinedMessage = await this.refineMessages(messagesToRefine, remainingContextTokens);
|
313 |
+
// payload.unshift(refinedMessage);
|
314 |
+
// remainingContextTokens -= refinedMessage.tokenCount;
|
315 |
+
// }
|
316 |
+
// if (remainingContextTokens <= instructions?.tokenCount) {
|
317 |
+
// if (this.options.debug) {
|
318 |
+
// console.debug(`Remaining context (${remainingContextTokens}) is less than instructions token count: ${instructions.tokenCount}`);
|
319 |
+
// }
|
320 |
+
|
321 |
+
// ({ context, remainingContextTokens, messagesToRefine, refineIndex } = await this.getMessagesWithinTokenLimit(payload));
|
322 |
+
// payload = context;
|
323 |
+
// }
|
324 |
+
|
325 |
+
// Calculate the difference in length to determine how many messages were discarded if any
|
326 |
+
let diff = orderedWithInstructions.length - payload.length;
|
327 |
+
|
328 |
+
if (this.options.debug) {
|
329 |
+
console.debug('<---------------------------------DIFF--------------------------------->');
|
330 |
+
console.debug(
|
331 |
+
`Difference between payload (${payload.length}) and orderedWithInstructions (${orderedWithInstructions.length}): ${diff}`,
|
332 |
+
);
|
333 |
+
console.debug(
|
334 |
+
'remainingContextTokens, this.maxContextTokens (1/2)',
|
335 |
+
remainingContextTokens,
|
336 |
+
this.maxContextTokens,
|
337 |
+
);
|
338 |
+
}
|
339 |
+
|
340 |
+
// If the difference is positive, slice the orderedWithInstructions array
|
341 |
+
if (diff > 0) {
|
342 |
+
orderedWithInstructions = orderedWithInstructions.slice(diff);
|
343 |
+
}
|
344 |
+
|
345 |
+
if (messagesToRefine.length > 0) {
|
346 |
+
refinedMessage = await this.refineMessages(messagesToRefine, remainingContextTokens);
|
347 |
+
payload.unshift(refinedMessage);
|
348 |
+
remainingContextTokens -= refinedMessage.tokenCount;
|
349 |
+
}
|
350 |
+
|
351 |
+
if (this.options.debug) {
|
352 |
+
console.debug(
|
353 |
+
'remainingContextTokens, this.maxContextTokens (2/2)',
|
354 |
+
remainingContextTokens,
|
355 |
+
this.maxContextTokens,
|
356 |
+
);
|
357 |
+
}
|
358 |
+
|
359 |
+
let tokenCountMap = orderedWithInstructions.reduce((map, message, index) => {
|
360 |
+
if (!message.messageId) {
|
361 |
+
return map;
|
362 |
+
}
|
363 |
+
|
364 |
+
if (index === refineIndex) {
|
365 |
+
map.refined = { ...refinedMessage, messageId: message.messageId };
|
366 |
+
}
|
367 |
+
|
368 |
+
map[message.messageId] = payload[index].tokenCount;
|
369 |
+
return map;
|
370 |
+
}, {});
|
371 |
+
|
372 |
+
const promptTokens = this.maxContextTokens - remainingContextTokens;
|
373 |
+
|
374 |
+
if (this.options.debug) {
|
375 |
+
console.debug('<-------------------------PAYLOAD/TOKEN COUNT MAP------------------------->');
|
376 |
+
console.debug('Payload:', payload);
|
377 |
+
console.debug('Token Count Map:', tokenCountMap);
|
378 |
+
console.debug('Prompt Tokens', promptTokens, remainingContextTokens, this.maxContextTokens);
|
379 |
+
}
|
380 |
+
|
381 |
+
return { payload, tokenCountMap, promptTokens, messages: orderedWithInstructions };
|
382 |
+
}
|
383 |
+
|
384 |
+
async sendMessage(message, opts = {}) {
|
385 |
+
const { user, conversationId, responseMessageId, saveOptions, userMessage } =
|
386 |
+
await this.handleStartMethods(message, opts);
|
387 |
+
|
388 |
+
this.user = user;
|
389 |
+
// It's not necessary to push to currentMessages
|
390 |
+
// depending on subclass implementation of handling messages
|
391 |
+
this.currentMessages.push(userMessage);
|
392 |
+
|
393 |
+
let {
|
394 |
+
prompt: payload,
|
395 |
+
tokenCountMap,
|
396 |
+
promptTokens,
|
397 |
+
} = await this.buildMessages(
|
398 |
+
this.currentMessages,
|
399 |
+
// When the userMessage is pushed to currentMessages, the parentMessage is the userMessageId.
|
400 |
+
// this only matters when buildMessages is utilizing the parentMessageId, and may vary on implementation
|
401 |
+
userMessage.messageId,
|
402 |
+
this.getBuildMessagesOptions(opts),
|
403 |
+
);
|
404 |
+
|
405 |
+
if (this.options.debug) {
|
406 |
+
console.debug('payload');
|
407 |
+
console.debug(payload);
|
408 |
+
}
|
409 |
+
|
410 |
+
if (tokenCountMap) {
|
411 |
+
console.dir(tokenCountMap, { depth: null });
|
412 |
+
if (tokenCountMap[userMessage.messageId]) {
|
413 |
+
userMessage.tokenCount = tokenCountMap[userMessage.messageId];
|
414 |
+
console.log('userMessage.tokenCount', userMessage.tokenCount);
|
415 |
+
console.log('userMessage', userMessage);
|
416 |
+
}
|
417 |
+
|
418 |
+
payload = payload.map((message) => {
|
419 |
+
const messageWithoutTokenCount = message;
|
420 |
+
delete messageWithoutTokenCount.tokenCount;
|
421 |
+
return messageWithoutTokenCount;
|
422 |
+
});
|
423 |
+
this.handleTokenCountMap(tokenCountMap);
|
424 |
+
}
|
425 |
+
|
426 |
+
await this.saveMessageToDatabase(userMessage, saveOptions, user);
|
427 |
+
const responseMessage = {
|
428 |
+
messageId: responseMessageId,
|
429 |
+
conversationId,
|
430 |
+
parentMessageId: userMessage.messageId,
|
431 |
+
isCreatedByUser: false,
|
432 |
+
model: this.modelOptions.model,
|
433 |
+
sender: this.sender,
|
434 |
+
text: await this.sendCompletion(payload, opts),
|
435 |
+
promptTokens,
|
436 |
+
};
|
437 |
+
|
438 |
+
if (tokenCountMap && this.getTokenCountForResponse) {
|
439 |
+
responseMessage.tokenCount = this.getTokenCountForResponse(responseMessage);
|
440 |
+
responseMessage.completionTokens = responseMessage.tokenCount;
|
441 |
+
}
|
442 |
+
await this.saveMessageToDatabase(responseMessage, saveOptions, user);
|
443 |
+
delete responseMessage.tokenCount;
|
444 |
+
return responseMessage;
|
445 |
+
}
|
446 |
+
|
447 |
+
async getConversation(conversationId, user = null) {
|
448 |
+
return await getConvo(user, conversationId);
|
449 |
+
}
|
450 |
+
|
451 |
+
async loadHistory(conversationId, parentMessageId = null) {
|
452 |
+
if (this.options.debug) {
|
453 |
+
console.debug('Loading history for conversation', conversationId, parentMessageId);
|
454 |
+
}
|
455 |
+
|
456 |
+
const messages = (await getMessages({ conversationId })) || [];
|
457 |
+
|
458 |
+
if (messages.length === 0) {
|
459 |
+
return [];
|
460 |
+
}
|
461 |
+
|
462 |
+
let mapMethod = null;
|
463 |
+
if (this.getMessageMapMethod) {
|
464 |
+
mapMethod = this.getMessageMapMethod();
|
465 |
+
}
|
466 |
+
|
467 |
+
return this.constructor.getMessagesForConversation(messages, parentMessageId, mapMethod);
|
468 |
+
}
|
469 |
+
|
470 |
+
async saveMessageToDatabase(message, endpointOptions, user = null) {
|
471 |
+
await saveMessage({ ...message, unfinished: false, cancelled: false });
|
472 |
+
await saveConvo(user, {
|
473 |
+
conversationId: message.conversationId,
|
474 |
+
endpoint: this.options.endpoint,
|
475 |
+
...endpointOptions,
|
476 |
+
});
|
477 |
+
}
|
478 |
+
|
479 |
+
async updateMessageInDatabase(message) {
|
480 |
+
await updateMessage(message);
|
481 |
+
}
|
482 |
+
|
483 |
+
/**
|
484 |
+
* Iterate through messages, building an array based on the parentMessageId.
|
485 |
+
* Each message has an id and a parentMessageId. The parentMessageId is the id of the message that this message is a reply to.
|
486 |
+
* @param messages
|
487 |
+
* @param parentMessageId
|
488 |
+
* @returns {*[]} An array containing the messages in the order they should be displayed, starting with the root message.
|
489 |
+
*/
|
490 |
+
static getMessagesForConversation(messages, parentMessageId, mapMethod = null) {
|
491 |
+
if (!messages || messages.length === 0) {
|
492 |
+
return [];
|
493 |
+
}
|
494 |
+
|
495 |
+
const orderedMessages = [];
|
496 |
+
let currentMessageId = parentMessageId;
|
497 |
+
while (currentMessageId) {
|
498 |
+
const message = messages.find((msg) => {
|
499 |
+
const messageId = msg.messageId ?? msg.id;
|
500 |
+
return messageId === currentMessageId;
|
501 |
+
});
|
502 |
+
if (!message) {
|
503 |
+
break;
|
504 |
+
}
|
505 |
+
orderedMessages.unshift(message);
|
506 |
+
currentMessageId = message.parentMessageId;
|
507 |
+
}
|
508 |
+
|
509 |
+
if (mapMethod) {
|
510 |
+
return orderedMessages.map(mapMethod);
|
511 |
+
}
|
512 |
+
|
513 |
+
return orderedMessages;
|
514 |
+
}
|
515 |
+
|
516 |
+
/**
|
517 |
+
* Algorithm adapted from "6. Counting tokens for chat API calls" of
|
518 |
+
* https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
|
519 |
+
*
|
520 |
+
* An additional 2 tokens need to be added for metadata after all messages have been counted.
|
521 |
+
*
|
522 |
+
* @param {*} message
|
523 |
+
*/
|
524 |
+
getTokenCountForMessage(message) {
|
525 |
+
let tokensPerMessage;
|
526 |
+
let nameAdjustment;
|
527 |
+
if (this.modelOptions.model.startsWith('gpt-4')) {
|
528 |
+
tokensPerMessage = 3;
|
529 |
+
nameAdjustment = 1;
|
530 |
+
} else {
|
531 |
+
tokensPerMessage = 4;
|
532 |
+
nameAdjustment = -1;
|
533 |
+
}
|
534 |
+
|
535 |
+
if (this.options.debug) {
|
536 |
+
console.debug('getTokenCountForMessage', message);
|
537 |
+
}
|
538 |
+
|
539 |
+
// Map each property of the message to the number of tokens it contains
|
540 |
+
const propertyTokenCounts = Object.entries(message).map(([key, value]) => {
|
541 |
+
if (key === 'tokenCount' || typeof value !== 'string') {
|
542 |
+
return 0;
|
543 |
+
}
|
544 |
+
// Count the number of tokens in the property value
|
545 |
+
const numTokens = this.getTokenCount(value);
|
546 |
+
|
547 |
+
// Adjust by `nameAdjustment` tokens if the property key is 'name'
|
548 |
+
const adjustment = key === 'name' ? nameAdjustment : 0;
|
549 |
+
return numTokens + adjustment;
|
550 |
+
});
|
551 |
+
|
552 |
+
if (this.options.debug) {
|
553 |
+
console.debug('propertyTokenCounts', propertyTokenCounts);
|
554 |
+
}
|
555 |
+
|
556 |
+
// Sum the number of tokens in all properties and add `tokensPerMessage` for metadata
|
557 |
+
return propertyTokenCounts.reduce((a, b) => a + b, tokensPerMessage);
|
558 |
+
}
|
559 |
+
}
|
560 |
+
|
561 |
+
module.exports = BaseClient;
|
api/app/clients/ChatGPTClient.js
ADDED
@@ -0,0 +1,587 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const crypto = require('crypto');
|
2 |
+
const Keyv = require('keyv');
|
3 |
+
const {
|
4 |
+
encoding_for_model: encodingForModel,
|
5 |
+
get_encoding: getEncoding,
|
6 |
+
} = require('@dqbd/tiktoken');
|
7 |
+
const { fetchEventSource } = require('@waylaidwanderer/fetch-event-source');
|
8 |
+
const { Agent, ProxyAgent } = require('undici');
|
9 |
+
const BaseClient = require('./BaseClient');
|
10 |
+
|
11 |
+
const CHATGPT_MODEL = 'gpt-3.5-turbo';
|
12 |
+
const tokenizersCache = {};
|
13 |
+
|
14 |
+
class ChatGPTClient extends BaseClient {
|
15 |
+
constructor(apiKey, options = {}, cacheOptions = {}) {
|
16 |
+
super(apiKey, options, cacheOptions);
|
17 |
+
|
18 |
+
cacheOptions.namespace = cacheOptions.namespace || 'chatgpt';
|
19 |
+
this.conversationsCache = new Keyv(cacheOptions);
|
20 |
+
this.setOptions(options);
|
21 |
+
}
|
22 |
+
|
23 |
+
setOptions(options) {
|
24 |
+
if (this.options && !this.options.replaceOptions) {
|
25 |
+
// nested options aren't spread properly, so we need to do this manually
|
26 |
+
this.options.modelOptions = {
|
27 |
+
...this.options.modelOptions,
|
28 |
+
...options.modelOptions,
|
29 |
+
};
|
30 |
+
delete options.modelOptions;
|
31 |
+
// now we can merge options
|
32 |
+
this.options = {
|
33 |
+
...this.options,
|
34 |
+
...options,
|
35 |
+
};
|
36 |
+
} else {
|
37 |
+
this.options = options;
|
38 |
+
}
|
39 |
+
|
40 |
+
if (this.options.openaiApiKey) {
|
41 |
+
this.apiKey = this.options.openaiApiKey;
|
42 |
+
}
|
43 |
+
|
44 |
+
const modelOptions = this.options.modelOptions || {};
|
45 |
+
this.modelOptions = {
|
46 |
+
...modelOptions,
|
47 |
+
// set some good defaults (check for undefined in some cases because they may be 0)
|
48 |
+
model: modelOptions.model || CHATGPT_MODEL,
|
49 |
+
temperature: typeof modelOptions.temperature === 'undefined' ? 0.8 : modelOptions.temperature,
|
50 |
+
top_p: typeof modelOptions.top_p === 'undefined' ? 1 : modelOptions.top_p,
|
51 |
+
presence_penalty:
|
52 |
+
typeof modelOptions.presence_penalty === 'undefined' ? 1 : modelOptions.presence_penalty,
|
53 |
+
stop: modelOptions.stop,
|
54 |
+
};
|
55 |
+
|
56 |
+
this.isChatGptModel = this.modelOptions.model.startsWith('gpt-');
|
57 |
+
const { isChatGptModel } = this;
|
58 |
+
this.isUnofficialChatGptModel =
|
59 |
+
this.modelOptions.model.startsWith('text-chat') ||
|
60 |
+
this.modelOptions.model.startsWith('text-davinci-002-render');
|
61 |
+
const { isUnofficialChatGptModel } = this;
|
62 |
+
|
63 |
+
// Davinci models have a max context length of 4097 tokens.
|
64 |
+
this.maxContextTokens = this.options.maxContextTokens || (isChatGptModel ? 4095 : 4097);
|
65 |
+
// I decided to reserve 1024 tokens for the response.
|
66 |
+
// The max prompt tokens is determined by the max context tokens minus the max response tokens.
|
67 |
+
// Earlier messages will be dropped until the prompt is within the limit.
|
68 |
+
this.maxResponseTokens = this.modelOptions.max_tokens || 1024;
|
69 |
+
this.maxPromptTokens =
|
70 |
+
this.options.maxPromptTokens || this.maxContextTokens - this.maxResponseTokens;
|
71 |
+
|
72 |
+
if (this.maxPromptTokens + this.maxResponseTokens > this.maxContextTokens) {
|
73 |
+
throw new Error(
|
74 |
+
`maxPromptTokens + max_tokens (${this.maxPromptTokens} + ${this.maxResponseTokens} = ${
|
75 |
+
this.maxPromptTokens + this.maxResponseTokens
|
76 |
+
}) must be less than or equal to maxContextTokens (${this.maxContextTokens})`,
|
77 |
+
);
|
78 |
+
}
|
79 |
+
|
80 |
+
this.userLabel = this.options.userLabel || 'User';
|
81 |
+
this.chatGptLabel = this.options.chatGptLabel || 'ChatGPT';
|
82 |
+
|
83 |
+
if (isChatGptModel) {
|
84 |
+
// Use these faux tokens to help the AI understand the context since we are building the chat log ourselves.
|
85 |
+
// Trying to use "<|im_start|>" causes the AI to still generate "<" or "<|" at the end sometimes for some reason,
|
86 |
+
// without tripping the stop sequences, so I'm using "||>" instead.
|
87 |
+
this.startToken = '||>';
|
88 |
+
this.endToken = '';
|
89 |
+
this.gptEncoder = this.constructor.getTokenizer('cl100k_base');
|
90 |
+
} else if (isUnofficialChatGptModel) {
|
91 |
+
this.startToken = '<|im_start|>';
|
92 |
+
this.endToken = '<|im_end|>';
|
93 |
+
this.gptEncoder = this.constructor.getTokenizer('text-davinci-003', true, {
|
94 |
+
'<|im_start|>': 100264,
|
95 |
+
'<|im_end|>': 100265,
|
96 |
+
});
|
97 |
+
} else {
|
98 |
+
// Previously I was trying to use "<|endoftext|>" but there seems to be some bug with OpenAI's token counting
|
99 |
+
// system that causes only the first "<|endoftext|>" to be counted as 1 token, and the rest are not treated
|
100 |
+
// as a single token. So we're using this instead.
|
101 |
+
this.startToken = '||>';
|
102 |
+
this.endToken = '';
|
103 |
+
try {
|
104 |
+
this.gptEncoder = this.constructor.getTokenizer(this.modelOptions.model, true);
|
105 |
+
} catch {
|
106 |
+
this.gptEncoder = this.constructor.getTokenizer('text-davinci-003', true);
|
107 |
+
}
|
108 |
+
}
|
109 |
+
|
110 |
+
if (!this.modelOptions.stop) {
|
111 |
+
const stopTokens = [this.startToken];
|
112 |
+
if (this.endToken && this.endToken !== this.startToken) {
|
113 |
+
stopTokens.push(this.endToken);
|
114 |
+
}
|
115 |
+
stopTokens.push(`\n${this.userLabel}:`);
|
116 |
+
stopTokens.push('<|diff_marker|>');
|
117 |
+
// I chose not to do one for `chatGptLabel` because I've never seen it happen
|
118 |
+
this.modelOptions.stop = stopTokens;
|
119 |
+
}
|
120 |
+
|
121 |
+
if (this.options.reverseProxyUrl) {
|
122 |
+
this.completionsUrl = this.options.reverseProxyUrl;
|
123 |
+
} else if (isChatGptModel) {
|
124 |
+
this.completionsUrl = 'https://api.openai.com/v1/chat/completions';
|
125 |
+
} else {
|
126 |
+
this.completionsUrl = 'https://api.openai.com/v1/completions';
|
127 |
+
}
|
128 |
+
|
129 |
+
return this;
|
130 |
+
}
|
131 |
+
|
132 |
+
static getTokenizer(encoding, isModelName = false, extendSpecialTokens = {}) {
|
133 |
+
if (tokenizersCache[encoding]) {
|
134 |
+
return tokenizersCache[encoding];
|
135 |
+
}
|
136 |
+
let tokenizer;
|
137 |
+
if (isModelName) {
|
138 |
+
tokenizer = encodingForModel(encoding, extendSpecialTokens);
|
139 |
+
} else {
|
140 |
+
tokenizer = getEncoding(encoding, extendSpecialTokens);
|
141 |
+
}
|
142 |
+
tokenizersCache[encoding] = tokenizer;
|
143 |
+
return tokenizer;
|
144 |
+
}
|
145 |
+
|
146 |
+
async getCompletion(input, onProgress, abortController = null) {
|
147 |
+
if (!abortController) {
|
148 |
+
abortController = new AbortController();
|
149 |
+
}
|
150 |
+
const modelOptions = { ...this.modelOptions };
|
151 |
+
if (typeof onProgress === 'function') {
|
152 |
+
modelOptions.stream = true;
|
153 |
+
}
|
154 |
+
if (this.isChatGptModel) {
|
155 |
+
modelOptions.messages = input;
|
156 |
+
} else {
|
157 |
+
modelOptions.prompt = input;
|
158 |
+
}
|
159 |
+
const { debug } = this.options;
|
160 |
+
const url = this.completionsUrl;
|
161 |
+
if (debug) {
|
162 |
+
console.debug();
|
163 |
+
console.debug(url);
|
164 |
+
console.debug(modelOptions);
|
165 |
+
console.debug();
|
166 |
+
}
|
167 |
+
const opts = {
|
168 |
+
method: 'POST',
|
169 |
+
headers: {
|
170 |
+
'Content-Type': 'application/json',
|
171 |
+
},
|
172 |
+
body: JSON.stringify(modelOptions),
|
173 |
+
dispatcher: new Agent({
|
174 |
+
bodyTimeout: 0,
|
175 |
+
headersTimeout: 0,
|
176 |
+
}),
|
177 |
+
};
|
178 |
+
|
179 |
+
if (this.apiKey && this.options.azure) {
|
180 |
+
opts.headers['api-key'] = this.apiKey;
|
181 |
+
} else if (this.apiKey) {
|
182 |
+
opts.headers.Authorization = `Bearer ${this.apiKey}`;
|
183 |
+
}
|
184 |
+
|
185 |
+
if (this.options.headers) {
|
186 |
+
opts.headers = { ...opts.headers, ...this.options.headers };
|
187 |
+
}
|
188 |
+
|
189 |
+
if (this.options.proxy) {
|
190 |
+
opts.dispatcher = new ProxyAgent(this.options.proxy);
|
191 |
+
}
|
192 |
+
|
193 |
+
if (modelOptions.stream) {
|
194 |
+
// eslint-disable-next-line no-async-promise-executor
|
195 |
+
return new Promise(async (resolve, reject) => {
|
196 |
+
try {
|
197 |
+
let done = false;
|
198 |
+
await fetchEventSource(url, {
|
199 |
+
...opts,
|
200 |
+
signal: abortController.signal,
|
201 |
+
async onopen(response) {
|
202 |
+
if (response.status === 200) {
|
203 |
+
return;
|
204 |
+
}
|
205 |
+
if (debug) {
|
206 |
+
console.debug(response);
|
207 |
+
}
|
208 |
+
let error;
|
209 |
+
try {
|
210 |
+
const body = await response.text();
|
211 |
+
error = new Error(`Failed to send message. HTTP ${response.status} - ${body}`);
|
212 |
+
error.status = response.status;
|
213 |
+
error.json = JSON.parse(body);
|
214 |
+
} catch {
|
215 |
+
error = error || new Error(`Failed to send message. HTTP ${response.status}`);
|
216 |
+
}
|
217 |
+
throw error;
|
218 |
+
},
|
219 |
+
onclose() {
|
220 |
+
if (debug) {
|
221 |
+
console.debug('Server closed the connection unexpectedly, returning...');
|
222 |
+
}
|
223 |
+
// workaround for private API not sending [DONE] event
|
224 |
+
if (!done) {
|
225 |
+
onProgress('[DONE]');
|
226 |
+
abortController.abort();
|
227 |
+
resolve();
|
228 |
+
}
|
229 |
+
},
|
230 |
+
onerror(err) {
|
231 |
+
if (debug) {
|
232 |
+
console.debug(err);
|
233 |
+
}
|
234 |
+
// rethrow to stop the operation
|
235 |
+
throw err;
|
236 |
+
},
|
237 |
+
onmessage(message) {
|
238 |
+
if (debug) {
|
239 |
+
// console.debug(message);
|
240 |
+
}
|
241 |
+
if (!message.data || message.event === 'ping') {
|
242 |
+
return;
|
243 |
+
}
|
244 |
+
if (message.data === '[DONE]') {
|
245 |
+
onProgress('[DONE]');
|
246 |
+
abortController.abort();
|
247 |
+
resolve();
|
248 |
+
done = true;
|
249 |
+
return;
|
250 |
+
}
|
251 |
+
onProgress(JSON.parse(message.data));
|
252 |
+
},
|
253 |
+
});
|
254 |
+
} catch (err) {
|
255 |
+
reject(err);
|
256 |
+
}
|
257 |
+
});
|
258 |
+
}
|
259 |
+
const response = await fetch(url, {
|
260 |
+
...opts,
|
261 |
+
signal: abortController.signal,
|
262 |
+
});
|
263 |
+
if (response.status !== 200) {
|
264 |
+
const body = await response.text();
|
265 |
+
const error = new Error(`Failed to send message. HTTP ${response.status} - ${body}`);
|
266 |
+
error.status = response.status;
|
267 |
+
try {
|
268 |
+
error.json = JSON.parse(body);
|
269 |
+
} catch {
|
270 |
+
error.body = body;
|
271 |
+
}
|
272 |
+
throw error;
|
273 |
+
}
|
274 |
+
return response.json();
|
275 |
+
}
|
276 |
+
|
277 |
+
async generateTitle(userMessage, botMessage) {
|
278 |
+
const instructionsPayload = {
|
279 |
+
role: 'system',
|
280 |
+
content: `Write an extremely concise subtitle for this conversation with no more than a few words. All words should be capitalized. Exclude punctuation.
|
281 |
+
|
282 |
+
||>Message:
|
283 |
+
${userMessage.message}
|
284 |
+
||>Response:
|
285 |
+
${botMessage.message}
|
286 |
+
|
287 |
+
||>Title:`,
|
288 |
+
};
|
289 |
+
|
290 |
+
const titleGenClientOptions = JSON.parse(JSON.stringify(this.options));
|
291 |
+
titleGenClientOptions.modelOptions = {
|
292 |
+
model: 'gpt-3.5-turbo',
|
293 |
+
temperature: 0,
|
294 |
+
presence_penalty: 0,
|
295 |
+
frequency_penalty: 0,
|
296 |
+
};
|
297 |
+
const titleGenClient = new ChatGPTClient(this.apiKey, titleGenClientOptions);
|
298 |
+
const result = await titleGenClient.getCompletion([instructionsPayload], null);
|
299 |
+
// remove any non-alphanumeric characters, replace multiple spaces with 1, and then trim
|
300 |
+
return result.choices[0].message.content
|
301 |
+
.replace(/[^a-zA-Z0-9' ]/g, '')
|
302 |
+
.replace(/\s+/g, ' ')
|
303 |
+
.trim();
|
304 |
+
}
|
305 |
+
|
306 |
+
async sendMessage(message, opts = {}) {
|
307 |
+
if (opts.clientOptions && typeof opts.clientOptions === 'object') {
|
308 |
+
this.setOptions(opts.clientOptions);
|
309 |
+
}
|
310 |
+
|
311 |
+
const conversationId = opts.conversationId || crypto.randomUUID();
|
312 |
+
const parentMessageId = opts.parentMessageId || crypto.randomUUID();
|
313 |
+
|
314 |
+
let conversation =
|
315 |
+
typeof opts.conversation === 'object'
|
316 |
+
? opts.conversation
|
317 |
+
: await this.conversationsCache.get(conversationId);
|
318 |
+
|
319 |
+
let isNewConversation = false;
|
320 |
+
if (!conversation) {
|
321 |
+
conversation = {
|
322 |
+
messages: [],
|
323 |
+
createdAt: Date.now(),
|
324 |
+
};
|
325 |
+
isNewConversation = true;
|
326 |
+
}
|
327 |
+
|
328 |
+
const shouldGenerateTitle = opts.shouldGenerateTitle && isNewConversation;
|
329 |
+
|
330 |
+
const userMessage = {
|
331 |
+
id: crypto.randomUUID(),
|
332 |
+
parentMessageId,
|
333 |
+
role: 'User',
|
334 |
+
message,
|
335 |
+
};
|
336 |
+
conversation.messages.push(userMessage);
|
337 |
+
|
338 |
+
// Doing it this way instead of having each message be a separate element in the array seems to be more reliable,
|
339 |
+
// especially when it comes to keeping the AI in character. It also seems to improve coherency and context retention.
|
340 |
+
const { prompt: payload, context } = await this.buildPrompt(
|
341 |
+
conversation.messages,
|
342 |
+
userMessage.id,
|
343 |
+
{
|
344 |
+
isChatGptModel: this.isChatGptModel,
|
345 |
+
promptPrefix: opts.promptPrefix,
|
346 |
+
},
|
347 |
+
);
|
348 |
+
|
349 |
+
if (this.options.keepNecessaryMessagesOnly) {
|
350 |
+
conversation.messages = context;
|
351 |
+
}
|
352 |
+
|
353 |
+
let reply = '';
|
354 |
+
let result = null;
|
355 |
+
if (typeof opts.onProgress === 'function') {
|
356 |
+
await this.getCompletion(
|
357 |
+
payload,
|
358 |
+
(progressMessage) => {
|
359 |
+
if (progressMessage === '[DONE]') {
|
360 |
+
return;
|
361 |
+
}
|
362 |
+
const token = this.isChatGptModel
|
363 |
+
? progressMessage.choices[0].delta.content
|
364 |
+
: progressMessage.choices[0].text;
|
365 |
+
// first event's delta content is always undefined
|
366 |
+
if (!token) {
|
367 |
+
return;
|
368 |
+
}
|
369 |
+
if (this.options.debug) {
|
370 |
+
console.debug(token);
|
371 |
+
}
|
372 |
+
if (token === this.endToken) {
|
373 |
+
return;
|
374 |
+
}
|
375 |
+
opts.onProgress(token);
|
376 |
+
reply += token;
|
377 |
+
},
|
378 |
+
opts.abortController || new AbortController(),
|
379 |
+
);
|
380 |
+
} else {
|
381 |
+
result = await this.getCompletion(
|
382 |
+
payload,
|
383 |
+
null,
|
384 |
+
opts.abortController || new AbortController(),
|
385 |
+
);
|
386 |
+
if (this.options.debug) {
|
387 |
+
console.debug(JSON.stringify(result));
|
388 |
+
}
|
389 |
+
if (this.isChatGptModel) {
|
390 |
+
reply = result.choices[0].message.content;
|
391 |
+
} else {
|
392 |
+
reply = result.choices[0].text.replace(this.endToken, '');
|
393 |
+
}
|
394 |
+
}
|
395 |
+
|
396 |
+
// avoids some rendering issues when using the CLI app
|
397 |
+
if (this.options.debug) {
|
398 |
+
console.debug();
|
399 |
+
}
|
400 |
+
|
401 |
+
reply = reply.trim();
|
402 |
+
|
403 |
+
const replyMessage = {
|
404 |
+
id: crypto.randomUUID(),
|
405 |
+
parentMessageId: userMessage.id,
|
406 |
+
role: 'ChatGPT',
|
407 |
+
message: reply,
|
408 |
+
};
|
409 |
+
conversation.messages.push(replyMessage);
|
410 |
+
|
411 |
+
const returnData = {
|
412 |
+
response: replyMessage.message,
|
413 |
+
conversationId,
|
414 |
+
parentMessageId: replyMessage.parentMessageId,
|
415 |
+
messageId: replyMessage.id,
|
416 |
+
details: result || {},
|
417 |
+
};
|
418 |
+
|
419 |
+
if (shouldGenerateTitle) {
|
420 |
+
conversation.title = await this.generateTitle(userMessage, replyMessage);
|
421 |
+
returnData.title = conversation.title;
|
422 |
+
}
|
423 |
+
|
424 |
+
await this.conversationsCache.set(conversationId, conversation);
|
425 |
+
|
426 |
+
if (this.options.returnConversation) {
|
427 |
+
returnData.conversation = conversation;
|
428 |
+
}
|
429 |
+
|
430 |
+
return returnData;
|
431 |
+
}
|
432 |
+
|
433 |
+
async buildPrompt(messages, parentMessageId, { isChatGptModel = false, promptPrefix = null }) {
|
434 |
+
const orderedMessages = this.constructor.getMessagesForConversation(messages, parentMessageId);
|
435 |
+
|
436 |
+
promptPrefix = (promptPrefix || this.options.promptPrefix || '').trim();
|
437 |
+
if (promptPrefix) {
|
438 |
+
// If the prompt prefix doesn't end with the end token, add it.
|
439 |
+
if (!promptPrefix.endsWith(`${this.endToken}`)) {
|
440 |
+
promptPrefix = `${promptPrefix.trim()}${this.endToken}\n\n`;
|
441 |
+
}
|
442 |
+
promptPrefix = `${this.startToken}Instructions:\n${promptPrefix}`;
|
443 |
+
} else {
|
444 |
+
const currentDateString = new Date().toLocaleDateString('en-us', {
|
445 |
+
year: 'numeric',
|
446 |
+
month: 'long',
|
447 |
+
day: 'numeric',
|
448 |
+
});
|
449 |
+
promptPrefix = `${this.startToken}Instructions:\nYou are ChatGPT, a large language model trained by OpenAI. Respond conversationally.\nCurrent date: ${currentDateString}${this.endToken}\n\n`;
|
450 |
+
}
|
451 |
+
|
452 |
+
const promptSuffix = `${this.startToken}${this.chatGptLabel}:\n`; // Prompt ChatGPT to respond.
|
453 |
+
|
454 |
+
const instructionsPayload = {
|
455 |
+
role: 'system',
|
456 |
+
name: 'instructions',
|
457 |
+
content: promptPrefix,
|
458 |
+
};
|
459 |
+
|
460 |
+
const messagePayload = {
|
461 |
+
role: 'system',
|
462 |
+
content: promptSuffix,
|
463 |
+
};
|
464 |
+
|
465 |
+
let currentTokenCount;
|
466 |
+
if (isChatGptModel) {
|
467 |
+
currentTokenCount =
|
468 |
+
this.getTokenCountForMessage(instructionsPayload) +
|
469 |
+
this.getTokenCountForMessage(messagePayload);
|
470 |
+
} else {
|
471 |
+
currentTokenCount = this.getTokenCount(`${promptPrefix}${promptSuffix}`);
|
472 |
+
}
|
473 |
+
let promptBody = '';
|
474 |
+
const maxTokenCount = this.maxPromptTokens;
|
475 |
+
|
476 |
+
const context = [];
|
477 |
+
|
478 |
+
// Iterate backwards through the messages, adding them to the prompt until we reach the max token count.
|
479 |
+
// Do this within a recursive async function so that it doesn't block the event loop for too long.
|
480 |
+
const buildPromptBody = async () => {
|
481 |
+
if (currentTokenCount < maxTokenCount && orderedMessages.length > 0) {
|
482 |
+
const message = orderedMessages.pop();
|
483 |
+
const roleLabel =
|
484 |
+
message?.isCreatedByUser || message?.role?.toLowerCase() === 'user'
|
485 |
+
? this.userLabel
|
486 |
+
: this.chatGptLabel;
|
487 |
+
const messageString = `${this.startToken}${roleLabel}:\n${
|
488 |
+
message?.text ?? message?.message
|
489 |
+
}${this.endToken}\n`;
|
490 |
+
let newPromptBody;
|
491 |
+
if (promptBody || isChatGptModel) {
|
492 |
+
newPromptBody = `${messageString}${promptBody}`;
|
493 |
+
} else {
|
494 |
+
// Always insert prompt prefix before the last user message, if not gpt-3.5-turbo.
|
495 |
+
// This makes the AI obey the prompt instructions better, which is important for custom instructions.
|
496 |
+
// After a bunch of testing, it doesn't seem to cause the AI any confusion, even if you ask it things
|
497 |
+
// like "what's the last thing I wrote?".
|
498 |
+
newPromptBody = `${promptPrefix}${messageString}${promptBody}`;
|
499 |
+
}
|
500 |
+
|
501 |
+
context.unshift(message);
|
502 |
+
|
503 |
+
const tokenCountForMessage = this.getTokenCount(messageString);
|
504 |
+
const newTokenCount = currentTokenCount + tokenCountForMessage;
|
505 |
+
if (newTokenCount > maxTokenCount) {
|
506 |
+
if (promptBody) {
|
507 |
+
// This message would put us over the token limit, so don't add it.
|
508 |
+
return false;
|
509 |
+
}
|
510 |
+
// This is the first message, so we can't add it. Just throw an error.
|
511 |
+
throw new Error(
|
512 |
+
`Prompt is too long. Max token count is ${maxTokenCount}, but prompt is ${newTokenCount} tokens long.`,
|
513 |
+
);
|
514 |
+
}
|
515 |
+
promptBody = newPromptBody;
|
516 |
+
currentTokenCount = newTokenCount;
|
517 |
+
// wait for next tick to avoid blocking the event loop
|
518 |
+
await new Promise((resolve) => setImmediate(resolve));
|
519 |
+
return buildPromptBody();
|
520 |
+
}
|
521 |
+
return true;
|
522 |
+
};
|
523 |
+
|
524 |
+
await buildPromptBody();
|
525 |
+
|
526 |
+
const prompt = `${promptBody}${promptSuffix}`;
|
527 |
+
if (isChatGptModel) {
|
528 |
+
messagePayload.content = prompt;
|
529 |
+
// Add 2 tokens for metadata after all messages have been counted.
|
530 |
+
currentTokenCount += 2;
|
531 |
+
}
|
532 |
+
|
533 |
+
// Use up to `this.maxContextTokens` tokens (prompt + response), but try to leave `this.maxTokens` tokens for the response.
|
534 |
+
this.modelOptions.max_tokens = Math.min(
|
535 |
+
this.maxContextTokens - currentTokenCount,
|
536 |
+
this.maxResponseTokens,
|
537 |
+
);
|
538 |
+
|
539 |
+
if (this.options.debug) {
|
540 |
+
console.debug(`Prompt : ${prompt}`);
|
541 |
+
}
|
542 |
+
|
543 |
+
if (isChatGptModel) {
|
544 |
+
return { prompt: [instructionsPayload, messagePayload], context };
|
545 |
+
}
|
546 |
+
return { prompt, context };
|
547 |
+
}
|
548 |
+
|
549 |
+
getTokenCount(text) {
|
550 |
+
return this.gptEncoder.encode(text, 'all').length;
|
551 |
+
}
|
552 |
+
|
553 |
+
/**
|
554 |
+
* Algorithm adapted from "6. Counting tokens for chat API calls" of
|
555 |
+
* https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
|
556 |
+
*
|
557 |
+
* An additional 2 tokens need to be added for metadata after all messages have been counted.
|
558 |
+
*
|
559 |
+
* @param {*} message
|
560 |
+
*/
|
561 |
+
getTokenCountForMessage(message) {
|
562 |
+
let tokensPerMessage;
|
563 |
+
let nameAdjustment;
|
564 |
+
if (this.modelOptions.model.startsWith('gpt-4')) {
|
565 |
+
tokensPerMessage = 3;
|
566 |
+
nameAdjustment = 1;
|
567 |
+
} else {
|
568 |
+
tokensPerMessage = 4;
|
569 |
+
nameAdjustment = -1;
|
570 |
+
}
|
571 |
+
|
572 |
+
// Map each property of the message to the number of tokens it contains
|
573 |
+
const propertyTokenCounts = Object.entries(message).map(([key, value]) => {
|
574 |
+
// Count the number of tokens in the property value
|
575 |
+
const numTokens = this.getTokenCount(value);
|
576 |
+
|
577 |
+
// Adjust by `nameAdjustment` tokens if the property key is 'name'
|
578 |
+
const adjustment = key === 'name' ? nameAdjustment : 0;
|
579 |
+
return numTokens + adjustment;
|
580 |
+
});
|
581 |
+
|
582 |
+
// Sum the number of tokens in all properties and add `tokensPerMessage` for metadata
|
583 |
+
return propertyTokenCounts.reduce((a, b) => a + b, tokensPerMessage);
|
584 |
+
}
|
585 |
+
}
|
586 |
+
|
587 |
+
module.exports = ChatGPTClient;
|
api/app/clients/GoogleClient.js
ADDED
@@ -0,0 +1,280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const BaseClient = require('./BaseClient');
|
2 |
+
const { google } = require('googleapis');
|
3 |
+
const { Agent, ProxyAgent } = require('undici');
|
4 |
+
const {
|
5 |
+
encoding_for_model: encodingForModel,
|
6 |
+
get_encoding: getEncoding,
|
7 |
+
} = require('@dqbd/tiktoken');
|
8 |
+
|
9 |
+
const tokenizersCache = {};
|
10 |
+
|
11 |
+
class GoogleClient extends BaseClient {
|
12 |
+
constructor(credentials, options = {}) {
|
13 |
+
super('apiKey', options);
|
14 |
+
this.client_email = credentials.client_email;
|
15 |
+
this.project_id = credentials.project_id;
|
16 |
+
this.private_key = credentials.private_key;
|
17 |
+
this.sender = 'PaLM2';
|
18 |
+
this.setOptions(options);
|
19 |
+
}
|
20 |
+
|
21 |
+
/* Google/PaLM2 specific methods */
|
22 |
+
constructUrl() {
|
23 |
+
return `https://us-central1-aiplatform.googleapis.com/v1/projects/${this.project_id}/locations/us-central1/publishers/google/models/${this.modelOptions.model}:predict`;
|
24 |
+
}
|
25 |
+
|
26 |
+
async getClient() {
|
27 |
+
const scopes = ['https://www.googleapis.com/auth/cloud-platform'];
|
28 |
+
const jwtClient = new google.auth.JWT(this.client_email, null, this.private_key, scopes);
|
29 |
+
|
30 |
+
jwtClient.authorize((err) => {
|
31 |
+
if (err) {
|
32 |
+
console.log(err);
|
33 |
+
throw err;
|
34 |
+
}
|
35 |
+
});
|
36 |
+
|
37 |
+
return jwtClient;
|
38 |
+
}
|
39 |
+
|
40 |
+
/* Required Client methods */
|
41 |
+
setOptions(options) {
|
42 |
+
if (this.options && !this.options.replaceOptions) {
|
43 |
+
// nested options aren't spread properly, so we need to do this manually
|
44 |
+
this.options.modelOptions = {
|
45 |
+
...this.options.modelOptions,
|
46 |
+
...options.modelOptions,
|
47 |
+
};
|
48 |
+
delete options.modelOptions;
|
49 |
+
// now we can merge options
|
50 |
+
this.options = {
|
51 |
+
...this.options,
|
52 |
+
...options,
|
53 |
+
};
|
54 |
+
} else {
|
55 |
+
this.options = options;
|
56 |
+
}
|
57 |
+
|
58 |
+
this.options.examples = this.options.examples.filter(
|
59 |
+
(obj) => obj.input.content !== '' && obj.output.content !== '',
|
60 |
+
);
|
61 |
+
|
62 |
+
const modelOptions = this.options.modelOptions || {};
|
63 |
+
this.modelOptions = {
|
64 |
+
...modelOptions,
|
65 |
+
// set some good defaults (check for undefined in some cases because they may be 0)
|
66 |
+
model: modelOptions.model || 'chat-bison',
|
67 |
+
temperature: typeof modelOptions.temperature === 'undefined' ? 0.2 : modelOptions.temperature, // 0 - 1, 0.2 is recommended
|
68 |
+
topP: typeof modelOptions.topP === 'undefined' ? 0.95 : modelOptions.topP, // 0 - 1, default: 0.95
|
69 |
+
topK: typeof modelOptions.topK === 'undefined' ? 40 : modelOptions.topK, // 1-40, default: 40
|
70 |
+
// stop: modelOptions.stop // no stop method for now
|
71 |
+
};
|
72 |
+
|
73 |
+
this.isChatModel = this.modelOptions.model.startsWith('chat-');
|
74 |
+
const { isChatModel } = this;
|
75 |
+
this.isTextModel = this.modelOptions.model.startsWith('text-');
|
76 |
+
const { isTextModel } = this;
|
77 |
+
|
78 |
+
this.maxContextTokens = this.options.maxContextTokens || (isTextModel ? 8000 : 4096);
|
79 |
+
// The max prompt tokens is determined by the max context tokens minus the max response tokens.
|
80 |
+
// Earlier messages will be dropped until the prompt is within the limit.
|
81 |
+
this.maxResponseTokens = this.modelOptions.maxOutputTokens || 1024;
|
82 |
+
this.maxPromptTokens =
|
83 |
+
this.options.maxPromptTokens || this.maxContextTokens - this.maxResponseTokens;
|
84 |
+
|
85 |
+
if (this.maxPromptTokens + this.maxResponseTokens > this.maxContextTokens) {
|
86 |
+
throw new Error(
|
87 |
+
`maxPromptTokens + maxOutputTokens (${this.maxPromptTokens} + ${this.maxResponseTokens} = ${
|
88 |
+
this.maxPromptTokens + this.maxResponseTokens
|
89 |
+
}) must be less than or equal to maxContextTokens (${this.maxContextTokens})`,
|
90 |
+
);
|
91 |
+
}
|
92 |
+
|
93 |
+
this.userLabel = this.options.userLabel || 'User';
|
94 |
+
this.modelLabel = this.options.modelLabel || 'Assistant';
|
95 |
+
|
96 |
+
if (isChatModel) {
|
97 |
+
// Use these faux tokens to help the AI understand the context since we are building the chat log ourselves.
|
98 |
+
// Trying to use "<|im_start|>" causes the AI to still generate "<" or "<|" at the end sometimes for some reason,
|
99 |
+
// without tripping the stop sequences, so I'm using "||>" instead.
|
100 |
+
this.startToken = '||>';
|
101 |
+
this.endToken = '';
|
102 |
+
this.gptEncoder = this.constructor.getTokenizer('cl100k_base');
|
103 |
+
} else if (isTextModel) {
|
104 |
+
this.startToken = '<|im_start|>';
|
105 |
+
this.endToken = '<|im_end|>';
|
106 |
+
this.gptEncoder = this.constructor.getTokenizer('text-davinci-003', true, {
|
107 |
+
'<|im_start|>': 100264,
|
108 |
+
'<|im_end|>': 100265,
|
109 |
+
});
|
110 |
+
} else {
|
111 |
+
// Previously I was trying to use "<|endoftext|>" but there seems to be some bug with OpenAI's token counting
|
112 |
+
// system that causes only the first "<|endoftext|>" to be counted as 1 token, and the rest are not treated
|
113 |
+
// as a single token. So we're using this instead.
|
114 |
+
this.startToken = '||>';
|
115 |
+
this.endToken = '';
|
116 |
+
try {
|
117 |
+
this.gptEncoder = this.constructor.getTokenizer(this.modelOptions.model, true);
|
118 |
+
} catch {
|
119 |
+
this.gptEncoder = this.constructor.getTokenizer('text-davinci-003', true);
|
120 |
+
}
|
121 |
+
}
|
122 |
+
|
123 |
+
if (!this.modelOptions.stop) {
|
124 |
+
const stopTokens = [this.startToken];
|
125 |
+
if (this.endToken && this.endToken !== this.startToken) {
|
126 |
+
stopTokens.push(this.endToken);
|
127 |
+
}
|
128 |
+
stopTokens.push(`\n${this.userLabel}:`);
|
129 |
+
stopTokens.push('<|diff_marker|>');
|
130 |
+
// I chose not to do one for `modelLabel` because I've never seen it happen
|
131 |
+
this.modelOptions.stop = stopTokens;
|
132 |
+
}
|
133 |
+
|
134 |
+
if (this.options.reverseProxyUrl) {
|
135 |
+
this.completionsUrl = this.options.reverseProxyUrl;
|
136 |
+
} else {
|
137 |
+
this.completionsUrl = this.constructUrl();
|
138 |
+
}
|
139 |
+
|
140 |
+
return this;
|
141 |
+
}
|
142 |
+
|
143 |
+
getMessageMapMethod() {
|
144 |
+
return ((message) => ({
|
145 |
+
author: message?.author ?? (message.isCreatedByUser ? this.userLabel : this.modelLabel),
|
146 |
+
content: message?.content ?? message.text,
|
147 |
+
})).bind(this);
|
148 |
+
}
|
149 |
+
|
150 |
+
buildMessages(messages = []) {
|
151 |
+
const formattedMessages = messages.map(this.getMessageMapMethod());
|
152 |
+
let payload = {
|
153 |
+
instances: [
|
154 |
+
{
|
155 |
+
messages: formattedMessages,
|
156 |
+
},
|
157 |
+
],
|
158 |
+
parameters: this.options.modelOptions,
|
159 |
+
};
|
160 |
+
|
161 |
+
if (this.options.promptPrefix) {
|
162 |
+
payload.instances[0].context = this.options.promptPrefix;
|
163 |
+
}
|
164 |
+
|
165 |
+
if (this.options.examples.length > 0) {
|
166 |
+
payload.instances[0].examples = this.options.examples;
|
167 |
+
}
|
168 |
+
|
169 |
+
/* TO-DO: text model needs more context since it can't process an array of messages */
|
170 |
+
if (this.isTextModel) {
|
171 |
+
payload.instances = [
|
172 |
+
{
|
173 |
+
prompt: messages[messages.length - 1].content,
|
174 |
+
},
|
175 |
+
];
|
176 |
+
}
|
177 |
+
|
178 |
+
if (this.options.debug) {
|
179 |
+
console.debug('GoogleClient buildMessages');
|
180 |
+
console.dir(payload, { depth: null });
|
181 |
+
}
|
182 |
+
|
183 |
+
return { prompt: payload };
|
184 |
+
}
|
185 |
+
|
186 |
+
async getCompletion(payload, abortController = null) {
|
187 |
+
if (!abortController) {
|
188 |
+
abortController = new AbortController();
|
189 |
+
}
|
190 |
+
const { debug } = this.options;
|
191 |
+
const url = this.completionsUrl;
|
192 |
+
if (debug) {
|
193 |
+
console.debug();
|
194 |
+
console.debug(url);
|
195 |
+
console.debug(this.modelOptions);
|
196 |
+
console.debug();
|
197 |
+
}
|
198 |
+
const opts = {
|
199 |
+
method: 'POST',
|
200 |
+
agent: new Agent({
|
201 |
+
bodyTimeout: 0,
|
202 |
+
headersTimeout: 0,
|
203 |
+
}),
|
204 |
+
signal: abortController.signal,
|
205 |
+
};
|
206 |
+
|
207 |
+
if (this.options.proxy) {
|
208 |
+
opts.agent = new ProxyAgent(this.options.proxy);
|
209 |
+
}
|
210 |
+
|
211 |
+
const client = await this.getClient();
|
212 |
+
const res = await client.request({ url, method: 'POST', data: payload });
|
213 |
+
console.dir(res.data, { depth: null });
|
214 |
+
return res.data;
|
215 |
+
}
|
216 |
+
|
217 |
+
getSaveOptions() {
|
218 |
+
return {
|
219 |
+
promptPrefix: this.options.promptPrefix,
|
220 |
+
modelLabel: this.options.modelLabel,
|
221 |
+
...this.modelOptions,
|
222 |
+
};
|
223 |
+
}
|
224 |
+
|
225 |
+
getBuildMessagesOptions() {
|
226 |
+
// console.log('GoogleClient doesn\'t use getBuildMessagesOptions');
|
227 |
+
}
|
228 |
+
|
229 |
+
async sendCompletion(payload, opts = {}) {
|
230 |
+
console.log('GoogleClient: sendcompletion', payload, opts);
|
231 |
+
let reply = '';
|
232 |
+
let blocked = false;
|
233 |
+
try {
|
234 |
+
const result = await this.getCompletion(payload, opts.abortController);
|
235 |
+
blocked = result?.predictions?.[0]?.safetyAttributes?.blocked;
|
236 |
+
reply =
|
237 |
+
result?.predictions?.[0]?.candidates?.[0]?.content ||
|
238 |
+
result?.predictions?.[0]?.content ||
|
239 |
+
'';
|
240 |
+
if (blocked === true) {
|
241 |
+
reply = `Google blocked a proper response to your message:\n${JSON.stringify(
|
242 |
+
result.predictions[0].safetyAttributes,
|
243 |
+
)}${reply.length > 0 ? `\nAI Response:\n${reply}` : ''}`;
|
244 |
+
}
|
245 |
+
if (this.options.debug) {
|
246 |
+
console.debug('result');
|
247 |
+
console.debug(result);
|
248 |
+
}
|
249 |
+
} catch (err) {
|
250 |
+
console.error(err);
|
251 |
+
}
|
252 |
+
|
253 |
+
if (!blocked) {
|
254 |
+
await this.generateTextStream(reply, opts.onProgress, { delay: 0.5 });
|
255 |
+
}
|
256 |
+
|
257 |
+
return reply.trim();
|
258 |
+
}
|
259 |
+
|
260 |
+
/* TO-DO: Handle tokens with Google tokenization NOTE: these are required */
|
261 |
+
static getTokenizer(encoding, isModelName = false, extendSpecialTokens = {}) {
|
262 |
+
if (tokenizersCache[encoding]) {
|
263 |
+
return tokenizersCache[encoding];
|
264 |
+
}
|
265 |
+
let tokenizer;
|
266 |
+
if (isModelName) {
|
267 |
+
tokenizer = encodingForModel(encoding, extendSpecialTokens);
|
268 |
+
} else {
|
269 |
+
tokenizer = getEncoding(encoding, extendSpecialTokens);
|
270 |
+
}
|
271 |
+
tokenizersCache[encoding] = tokenizer;
|
272 |
+
return tokenizer;
|
273 |
+
}
|
274 |
+
|
275 |
+
getTokenCount(text) {
|
276 |
+
return this.gptEncoder.encode(text, 'all').length;
|
277 |
+
}
|
278 |
+
}
|
279 |
+
|
280 |
+
module.exports = GoogleClient;
|
api/app/clients/OpenAIClient.js
ADDED
@@ -0,0 +1,369 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const BaseClient = require('./BaseClient');
|
2 |
+
const ChatGPTClient = require('./ChatGPTClient');
|
3 |
+
const {
|
4 |
+
encoding_for_model: encodingForModel,
|
5 |
+
get_encoding: getEncoding,
|
6 |
+
} = require('@dqbd/tiktoken');
|
7 |
+
const { maxTokensMap, genAzureChatCompletion } = require('../../utils');
|
8 |
+
|
9 |
+
// Cache to store Tiktoken instances
|
10 |
+
const tokenizersCache = {};
|
11 |
+
// Counter for keeping track of the number of tokenizer calls
|
12 |
+
let tokenizerCallsCount = 0;
|
13 |
+
|
14 |
+
class OpenAIClient extends BaseClient {
|
15 |
+
constructor(apiKey, options = {}) {
|
16 |
+
super(apiKey, options);
|
17 |
+
this.ChatGPTClient = new ChatGPTClient();
|
18 |
+
this.buildPrompt = this.ChatGPTClient.buildPrompt.bind(this);
|
19 |
+
this.getCompletion = this.ChatGPTClient.getCompletion.bind(this);
|
20 |
+
this.sender = options.sender ?? 'ChatGPT';
|
21 |
+
this.contextStrategy = options.contextStrategy
|
22 |
+
? options.contextStrategy.toLowerCase()
|
23 |
+
: 'discard';
|
24 |
+
this.shouldRefineContext = this.contextStrategy === 'refine';
|
25 |
+
this.azure = options.azure || false;
|
26 |
+
if (this.azure) {
|
27 |
+
this.azureEndpoint = genAzureChatCompletion(this.azure);
|
28 |
+
}
|
29 |
+
this.setOptions(options);
|
30 |
+
}
|
31 |
+
|
32 |
+
setOptions(options) {
|
33 |
+
if (this.options && !this.options.replaceOptions) {
|
34 |
+
this.options.modelOptions = {
|
35 |
+
...this.options.modelOptions,
|
36 |
+
...options.modelOptions,
|
37 |
+
};
|
38 |
+
delete options.modelOptions;
|
39 |
+
this.options = {
|
40 |
+
...this.options,
|
41 |
+
...options,
|
42 |
+
};
|
43 |
+
} else {
|
44 |
+
this.options = options;
|
45 |
+
}
|
46 |
+
|
47 |
+
if (this.options.openaiApiKey) {
|
48 |
+
this.apiKey = this.options.openaiApiKey;
|
49 |
+
}
|
50 |
+
|
51 |
+
const modelOptions = this.options.modelOptions || {};
|
52 |
+
if (!this.modelOptions) {
|
53 |
+
this.modelOptions = {
|
54 |
+
...modelOptions,
|
55 |
+
model: modelOptions.model || 'gpt-3.5-turbo',
|
56 |
+
temperature:
|
57 |
+
typeof modelOptions.temperature === 'undefined' ? 0.8 : modelOptions.temperature,
|
58 |
+
top_p: typeof modelOptions.top_p === 'undefined' ? 1 : modelOptions.top_p,
|
59 |
+
presence_penalty:
|
60 |
+
typeof modelOptions.presence_penalty === 'undefined' ? 1 : modelOptions.presence_penalty,
|
61 |
+
stop: modelOptions.stop,
|
62 |
+
};
|
63 |
+
}
|
64 |
+
|
65 |
+
this.isChatCompletion =
|
66 |
+
this.options.reverseProxyUrl ||
|
67 |
+
this.options.localAI ||
|
68 |
+
this.modelOptions.model.startsWith('gpt-');
|
69 |
+
this.isChatGptModel = this.isChatCompletion;
|
70 |
+
if (this.modelOptions.model === 'text-davinci-003') {
|
71 |
+
this.isChatCompletion = false;
|
72 |
+
this.isChatGptModel = false;
|
73 |
+
}
|
74 |
+
const { isChatGptModel } = this;
|
75 |
+
this.isUnofficialChatGptModel =
|
76 |
+
this.modelOptions.model.startsWith('text-chat') ||
|
77 |
+
this.modelOptions.model.startsWith('text-davinci-002-render');
|
78 |
+
this.maxContextTokens = maxTokensMap[this.modelOptions.model] ?? 4095; // 1 less than maximum
|
79 |
+
this.maxResponseTokens = this.modelOptions.max_tokens || 1024;
|
80 |
+
this.maxPromptTokens =
|
81 |
+
this.options.maxPromptTokens || this.maxContextTokens - this.maxResponseTokens;
|
82 |
+
|
83 |
+
if (this.maxPromptTokens + this.maxResponseTokens > this.maxContextTokens) {
|
84 |
+
throw new Error(
|
85 |
+
`maxPromptTokens + max_tokens (${this.maxPromptTokens} + ${this.maxResponseTokens} = ${
|
86 |
+
this.maxPromptTokens + this.maxResponseTokens
|
87 |
+
}) must be less than or equal to maxContextTokens (${this.maxContextTokens})`,
|
88 |
+
);
|
89 |
+
}
|
90 |
+
|
91 |
+
this.userLabel = this.options.userLabel || 'User';
|
92 |
+
this.chatGptLabel = this.options.chatGptLabel || 'Assistant';
|
93 |
+
|
94 |
+
this.setupTokens();
|
95 |
+
|
96 |
+
if (!this.modelOptions.stop) {
|
97 |
+
const stopTokens = [this.startToken];
|
98 |
+
if (this.endToken && this.endToken !== this.startToken) {
|
99 |
+
stopTokens.push(this.endToken);
|
100 |
+
}
|
101 |
+
stopTokens.push(`\n${this.userLabel}:`);
|
102 |
+
stopTokens.push('<|diff_marker|>');
|
103 |
+
this.modelOptions.stop = stopTokens;
|
104 |
+
}
|
105 |
+
|
106 |
+
if (this.options.reverseProxyUrl) {
|
107 |
+
this.completionsUrl = this.options.reverseProxyUrl;
|
108 |
+
} else if (isChatGptModel) {
|
109 |
+
this.completionsUrl = 'https://api.openai.com/v1/chat/completions';
|
110 |
+
} else {
|
111 |
+
this.completionsUrl = 'https://api.openai.com/v1/completions';
|
112 |
+
}
|
113 |
+
|
114 |
+
if (this.azureEndpoint) {
|
115 |
+
this.completionsUrl = this.azureEndpoint;
|
116 |
+
}
|
117 |
+
|
118 |
+
if (this.azureEndpoint && this.options.debug) {
|
119 |
+
console.debug(`Using Azure endpoint: ${this.azureEndpoint}`, this.azure);
|
120 |
+
}
|
121 |
+
|
122 |
+
return this;
|
123 |
+
}
|
124 |
+
|
125 |
+
setupTokens() {
|
126 |
+
if (this.isChatCompletion) {
|
127 |
+
this.startToken = '||>';
|
128 |
+
this.endToken = '';
|
129 |
+
} else if (this.isUnofficialChatGptModel) {
|
130 |
+
this.startToken = '<|im_start|>';
|
131 |
+
this.endToken = '<|im_end|>';
|
132 |
+
} else {
|
133 |
+
this.startToken = '||>';
|
134 |
+
this.endToken = '';
|
135 |
+
}
|
136 |
+
}
|
137 |
+
|
138 |
+
// Selects an appropriate tokenizer based on the current configuration of the client instance.
|
139 |
+
// It takes into account factors such as whether it's a chat completion, an unofficial chat GPT model, etc.
|
140 |
+
selectTokenizer() {
|
141 |
+
let tokenizer;
|
142 |
+
this.encoding = 'text-davinci-003';
|
143 |
+
if (this.isChatCompletion) {
|
144 |
+
this.encoding = 'cl100k_base';
|
145 |
+
tokenizer = this.constructor.getTokenizer(this.encoding);
|
146 |
+
} else if (this.isUnofficialChatGptModel) {
|
147 |
+
const extendSpecialTokens = {
|
148 |
+
'<|im_start|>': 100264,
|
149 |
+
'<|im_end|>': 100265,
|
150 |
+
};
|
151 |
+
tokenizer = this.constructor.getTokenizer(this.encoding, true, extendSpecialTokens);
|
152 |
+
} else {
|
153 |
+
try {
|
154 |
+
this.encoding = this.modelOptions.model;
|
155 |
+
tokenizer = this.constructor.getTokenizer(this.modelOptions.model, true);
|
156 |
+
} catch {
|
157 |
+
tokenizer = this.constructor.getTokenizer(this.encoding, true);
|
158 |
+
}
|
159 |
+
}
|
160 |
+
|
161 |
+
return tokenizer;
|
162 |
+
}
|
163 |
+
|
164 |
+
// Retrieves a tokenizer either from the cache or creates a new one if one doesn't exist in the cache.
|
165 |
+
// If a tokenizer is being created, it's also added to the cache.
|
166 |
+
static getTokenizer(encoding, isModelName = false, extendSpecialTokens = {}) {
|
167 |
+
let tokenizer;
|
168 |
+
if (tokenizersCache[encoding]) {
|
169 |
+
tokenizer = tokenizersCache[encoding];
|
170 |
+
} else {
|
171 |
+
if (isModelName) {
|
172 |
+
tokenizer = encodingForModel(encoding, extendSpecialTokens);
|
173 |
+
} else {
|
174 |
+
tokenizer = getEncoding(encoding, extendSpecialTokens);
|
175 |
+
}
|
176 |
+
tokenizersCache[encoding] = tokenizer;
|
177 |
+
}
|
178 |
+
return tokenizer;
|
179 |
+
}
|
180 |
+
|
181 |
+
// Frees all encoders in the cache and resets the count.
|
182 |
+
static freeAndResetAllEncoders() {
|
183 |
+
try {
|
184 |
+
Object.keys(tokenizersCache).forEach((key) => {
|
185 |
+
if (tokenizersCache[key]) {
|
186 |
+
tokenizersCache[key].free();
|
187 |
+
delete tokenizersCache[key];
|
188 |
+
}
|
189 |
+
});
|
190 |
+
// Reset count
|
191 |
+
tokenizerCallsCount = 1;
|
192 |
+
} catch (error) {
|
193 |
+
console.log('Free and reset encoders error');
|
194 |
+
console.error(error);
|
195 |
+
}
|
196 |
+
}
|
197 |
+
|
198 |
+
// Checks if the cache of tokenizers has reached a certain size. If it has, it frees and resets all tokenizers.
|
199 |
+
resetTokenizersIfNecessary() {
|
200 |
+
if (tokenizerCallsCount >= 25) {
|
201 |
+
if (this.options.debug) {
|
202 |
+
console.debug('freeAndResetAllEncoders: reached 25 encodings, resetting...');
|
203 |
+
}
|
204 |
+
this.constructor.freeAndResetAllEncoders();
|
205 |
+
}
|
206 |
+
tokenizerCallsCount++;
|
207 |
+
}
|
208 |
+
|
209 |
+
// Returns the token count of a given text. It also checks and resets the tokenizers if necessary.
|
210 |
+
getTokenCount(text) {
|
211 |
+
this.resetTokenizersIfNecessary();
|
212 |
+
try {
|
213 |
+
const tokenizer = this.selectTokenizer();
|
214 |
+
return tokenizer.encode(text, 'all').length;
|
215 |
+
} catch (error) {
|
216 |
+
this.constructor.freeAndResetAllEncoders();
|
217 |
+
const tokenizer = this.selectTokenizer();
|
218 |
+
return tokenizer.encode(text, 'all').length;
|
219 |
+
}
|
220 |
+
}
|
221 |
+
|
222 |
+
getSaveOptions() {
|
223 |
+
return {
|
224 |
+
chatGptLabel: this.options.chatGptLabel,
|
225 |
+
promptPrefix: this.options.promptPrefix,
|
226 |
+
...this.modelOptions,
|
227 |
+
};
|
228 |
+
}
|
229 |
+
|
230 |
+
getBuildMessagesOptions(opts) {
|
231 |
+
return {
|
232 |
+
isChatCompletion: this.isChatCompletion,
|
233 |
+
promptPrefix: opts.promptPrefix,
|
234 |
+
abortController: opts.abortController,
|
235 |
+
};
|
236 |
+
}
|
237 |
+
|
238 |
+
async buildMessages(
|
239 |
+
messages,
|
240 |
+
parentMessageId,
|
241 |
+
{ isChatCompletion = false, promptPrefix = null },
|
242 |
+
) {
|
243 |
+
if (!isChatCompletion) {
|
244 |
+
return await this.buildPrompt(messages, parentMessageId, {
|
245 |
+
isChatGptModel: isChatCompletion,
|
246 |
+
promptPrefix,
|
247 |
+
});
|
248 |
+
}
|
249 |
+
|
250 |
+
let payload;
|
251 |
+
let instructions;
|
252 |
+
let tokenCountMap;
|
253 |
+
let promptTokens;
|
254 |
+
let orderedMessages = this.constructor.getMessagesForConversation(messages, parentMessageId);
|
255 |
+
|
256 |
+
promptPrefix = (promptPrefix || this.options.promptPrefix || '').trim();
|
257 |
+
if (promptPrefix) {
|
258 |
+
promptPrefix = `Instructions:\n${promptPrefix}`;
|
259 |
+
instructions = {
|
260 |
+
role: 'system',
|
261 |
+
name: 'instructions',
|
262 |
+
content: promptPrefix,
|
263 |
+
};
|
264 |
+
|
265 |
+
if (this.contextStrategy) {
|
266 |
+
instructions.tokenCount = this.getTokenCountForMessage(instructions);
|
267 |
+
}
|
268 |
+
}
|
269 |
+
|
270 |
+
const formattedMessages = orderedMessages.map((message) => {
|
271 |
+
let { role: _role, sender, text } = message;
|
272 |
+
const role = _role ?? sender;
|
273 |
+
const content = text ?? '';
|
274 |
+
const formattedMessage = {
|
275 |
+
role: role?.toLowerCase() === 'user' ? 'user' : 'assistant',
|
276 |
+
content,
|
277 |
+
};
|
278 |
+
|
279 |
+
if (this.options?.name && formattedMessage.role === 'user') {
|
280 |
+
formattedMessage.name = this.options.name;
|
281 |
+
}
|
282 |
+
|
283 |
+
if (this.contextStrategy) {
|
284 |
+
formattedMessage.tokenCount =
|
285 |
+
message.tokenCount ?? this.getTokenCountForMessage(formattedMessage);
|
286 |
+
}
|
287 |
+
|
288 |
+
return formattedMessage;
|
289 |
+
});
|
290 |
+
|
291 |
+
// TODO: need to handle interleaving instructions better
|
292 |
+
if (this.contextStrategy) {
|
293 |
+
({ payload, tokenCountMap, promptTokens, messages } = await this.handleContextStrategy({
|
294 |
+
instructions,
|
295 |
+
orderedMessages,
|
296 |
+
formattedMessages,
|
297 |
+
}));
|
298 |
+
}
|
299 |
+
|
300 |
+
const result = {
|
301 |
+
prompt: payload,
|
302 |
+
promptTokens,
|
303 |
+
messages,
|
304 |
+
};
|
305 |
+
|
306 |
+
if (tokenCountMap) {
|
307 |
+
tokenCountMap.instructions = instructions?.tokenCount;
|
308 |
+
result.tokenCountMap = tokenCountMap;
|
309 |
+
}
|
310 |
+
|
311 |
+
return result;
|
312 |
+
}
|
313 |
+
|
314 |
+
async sendCompletion(payload, opts = {}) {
|
315 |
+
let reply = '';
|
316 |
+
let result = null;
|
317 |
+
if (typeof opts.onProgress === 'function') {
|
318 |
+
await this.getCompletion(
|
319 |
+
payload,
|
320 |
+
(progressMessage) => {
|
321 |
+
if (progressMessage === '[DONE]') {
|
322 |
+
return;
|
323 |
+
}
|
324 |
+
const token = this.isChatCompletion
|
325 |
+
? progressMessage.choices?.[0]?.delta?.content
|
326 |
+
: progressMessage.choices?.[0]?.text;
|
327 |
+
// first event's delta content is always undefined
|
328 |
+
if (!token) {
|
329 |
+
return;
|
330 |
+
}
|
331 |
+
if (this.options.debug) {
|
332 |
+
// console.debug(token);
|
333 |
+
}
|
334 |
+
if (token === this.endToken) {
|
335 |
+
return;
|
336 |
+
}
|
337 |
+
opts.onProgress(token);
|
338 |
+
reply += token;
|
339 |
+
},
|
340 |
+
opts.abortController || new AbortController(),
|
341 |
+
);
|
342 |
+
} else {
|
343 |
+
result = await this.getCompletion(
|
344 |
+
payload,
|
345 |
+
null,
|
346 |
+
opts.abortController || new AbortController(),
|
347 |
+
);
|
348 |
+
if (this.options.debug) {
|
349 |
+
console.debug(JSON.stringify(result));
|
350 |
+
}
|
351 |
+
if (this.isChatCompletion) {
|
352 |
+
reply = result.choices[0].message.content;
|
353 |
+
} else {
|
354 |
+
reply = result.choices[0].text.replace(this.endToken, '');
|
355 |
+
}
|
356 |
+
}
|
357 |
+
|
358 |
+
return reply.trim();
|
359 |
+
}
|
360 |
+
|
361 |
+
getTokenCountForResponse(response) {
|
362 |
+
return this.getTokenCountForMessage({
|
363 |
+
role: 'assistant',
|
364 |
+
content: response.text,
|
365 |
+
});
|
366 |
+
}
|
367 |
+
}
|
368 |
+
|
369 |
+
module.exports = OpenAIClient;
|
api/app/clients/PluginsClient.js
ADDED
@@ -0,0 +1,569 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const OpenAIClient = require('./OpenAIClient');
|
2 |
+
const { ChatOpenAI } = require('langchain/chat_models/openai');
|
3 |
+
const { CallbackManager } = require('langchain/callbacks');
|
4 |
+
const { initializeCustomAgent, initializeFunctionsAgent } = require('./agents/');
|
5 |
+
const { findMessageContent } = require('../../utils');
|
6 |
+
const { loadTools } = require('./tools/util');
|
7 |
+
const { SelfReflectionTool } = require('./tools/');
|
8 |
+
const { HumanChatMessage, AIChatMessage } = require('langchain/schema');
|
9 |
+
const { instructions, imageInstructions, errorInstructions } = require('./prompts/instructions');
|
10 |
+
|
11 |
+
class PluginsClient extends OpenAIClient {
|
12 |
+
constructor(apiKey, options = {}) {
|
13 |
+
super(apiKey, options);
|
14 |
+
this.sender = options.sender ?? 'Assistant';
|
15 |
+
this.tools = [];
|
16 |
+
this.actions = [];
|
17 |
+
this.openAIApiKey = apiKey;
|
18 |
+
this.setOptions(options);
|
19 |
+
this.executor = null;
|
20 |
+
}
|
21 |
+
|
22 |
+
getActions(input = null) {
|
23 |
+
let output = 'Internal thoughts & actions taken:\n"';
|
24 |
+
let actions = input || this.actions;
|
25 |
+
|
26 |
+
if (actions[0]?.action && this.functionsAgent) {
|
27 |
+
actions = actions.map((step) => ({
|
28 |
+
log: `Action: ${step.action?.tool || ''}\nInput: ${
|
29 |
+
JSON.stringify(step.action?.toolInput) || ''
|
30 |
+
}\nObservation: ${step.observation}`,
|
31 |
+
}));
|
32 |
+
} else if (actions[0]?.action) {
|
33 |
+
actions = actions.map((step) => ({
|
34 |
+
log: `${step.action.log}\nObservation: ${step.observation}`,
|
35 |
+
}));
|
36 |
+
}
|
37 |
+
|
38 |
+
actions.forEach((actionObj, index) => {
|
39 |
+
output += `${actionObj.log}`;
|
40 |
+
if (index < actions.length - 1) {
|
41 |
+
output += '\n';
|
42 |
+
}
|
43 |
+
});
|
44 |
+
|
45 |
+
return output + '"';
|
46 |
+
}
|
47 |
+
|
48 |
+
buildErrorInput(message, errorMessage) {
|
49 |
+
const log = errorMessage.includes('Could not parse LLM output:')
|
50 |
+
? `A formatting error occurred with your response to the human's last message. You didn't follow the formatting instructions. Remember to ${instructions}`
|
51 |
+
: `You encountered an error while replying to the human's last message. Attempt to answer again or admit an answer cannot be given.\nError: ${errorMessage}`;
|
52 |
+
|
53 |
+
return `
|
54 |
+
${log}
|
55 |
+
|
56 |
+
${this.getActions()}
|
57 |
+
|
58 |
+
Human's last message: ${message}
|
59 |
+
`;
|
60 |
+
}
|
61 |
+
|
62 |
+
buildPromptPrefix(result, message) {
|
63 |
+
if ((result.output && result.output.includes('N/A')) || result.output === undefined) {
|
64 |
+
return null;
|
65 |
+
}
|
66 |
+
|
67 |
+
if (
|
68 |
+
result?.intermediateSteps?.length === 1 &&
|
69 |
+
result?.intermediateSteps[0]?.action?.toolInput === 'N/A'
|
70 |
+
) {
|
71 |
+
return null;
|
72 |
+
}
|
73 |
+
|
74 |
+
const internalActions =
|
75 |
+
result?.intermediateSteps?.length > 0
|
76 |
+
? this.getActions(result.intermediateSteps)
|
77 |
+
: 'Internal Actions Taken: None';
|
78 |
+
|
79 |
+
const toolBasedInstructions = internalActions.toLowerCase().includes('image')
|
80 |
+
? imageInstructions
|
81 |
+
: '';
|
82 |
+
|
83 |
+
const errorMessage = result.errorMessage ? `${errorInstructions} ${result.errorMessage}\n` : '';
|
84 |
+
|
85 |
+
const preliminaryAnswer =
|
86 |
+
result.output?.length > 0 ? `Preliminary Answer: "${result.output.trim()}"` : '';
|
87 |
+
const prefix = preliminaryAnswer
|
88 |
+
? 'review and improve the answer you generated using plugins in response to the User Message below. The user hasn\'t seen your answer or thoughts yet.'
|
89 |
+
: 'respond to the User Message below based on your preliminary thoughts & actions.';
|
90 |
+
|
91 |
+
return `As a helpful AI Assistant, ${prefix}${errorMessage}\n${internalActions}
|
92 |
+
${preliminaryAnswer}
|
93 |
+
Reply conversationally to the User based on your ${
|
94 |
+
preliminaryAnswer ? 'preliminary answer, ' : ''
|
95 |
+
}internal actions, thoughts, and observations, making improvements wherever possible, but do not modify URLs.
|
96 |
+
${
|
97 |
+
preliminaryAnswer
|
98 |
+
? ''
|
99 |
+
: '\nIf there is an incomplete thought or action, you are expected to complete it in your response now.\n'
|
100 |
+
}You must cite sources if you are using any web links. ${toolBasedInstructions}
|
101 |
+
Only respond with your conversational reply to the following User Message:
|
102 |
+
"${message}"`;
|
103 |
+
}
|
104 |
+
|
105 |
+
setOptions(options) {
|
106 |
+
this.agentOptions = options.agentOptions;
|
107 |
+
this.functionsAgent = this.agentOptions?.agent === 'functions';
|
108 |
+
this.agentIsGpt3 = this.agentOptions?.model.startsWith('gpt-3');
|
109 |
+
if (this.functionsAgent && this.agentOptions.model) {
|
110 |
+
this.agentOptions.model = this.getFunctionModelName(this.agentOptions.model);
|
111 |
+
}
|
112 |
+
|
113 |
+
super.setOptions(options);
|
114 |
+
this.isGpt3 = this.modelOptions.model.startsWith('gpt-3');
|
115 |
+
|
116 |
+
if (this.options.reverseProxyUrl) {
|
117 |
+
this.langchainProxy = this.options.reverseProxyUrl.match(/.*v1/)[0];
|
118 |
+
}
|
119 |
+
}
|
120 |
+
|
121 |
+
getSaveOptions() {
|
122 |
+
return {
|
123 |
+
chatGptLabel: this.options.chatGptLabel,
|
124 |
+
promptPrefix: this.options.promptPrefix,
|
125 |
+
...this.modelOptions,
|
126 |
+
agentOptions: this.agentOptions,
|
127 |
+
};
|
128 |
+
}
|
129 |
+
|
130 |
+
saveLatestAction(action) {
|
131 |
+
this.actions.push(action);
|
132 |
+
}
|
133 |
+
|
134 |
+
getFunctionModelName(input) {
|
135 |
+
if (input.startsWith('gpt-3.5-turbo')) {
|
136 |
+
return 'gpt-3.5-turbo';
|
137 |
+
} else if (input.startsWith('gpt-4')) {
|
138 |
+
return 'gpt-4';
|
139 |
+
} else {
|
140 |
+
return 'gpt-3.5-turbo';
|
141 |
+
}
|
142 |
+
}
|
143 |
+
|
144 |
+
getBuildMessagesOptions(opts) {
|
145 |
+
return {
|
146 |
+
isChatCompletion: true,
|
147 |
+
promptPrefix: opts.promptPrefix,
|
148 |
+
abortController: opts.abortController,
|
149 |
+
};
|
150 |
+
}
|
151 |
+
|
152 |
+
createLLM(modelOptions, configOptions) {
|
153 |
+
let credentials = { openAIApiKey: this.openAIApiKey };
|
154 |
+
let configuration = {
|
155 |
+
apiKey: this.openAIApiKey,
|
156 |
+
};
|
157 |
+
|
158 |
+
if (this.azure) {
|
159 |
+
credentials = {};
|
160 |
+
configuration = {};
|
161 |
+
}
|
162 |
+
|
163 |
+
if (this.options.debug) {
|
164 |
+
console.debug('createLLM: configOptions');
|
165 |
+
console.debug(configOptions);
|
166 |
+
}
|
167 |
+
|
168 |
+
return new ChatOpenAI({ credentials, configuration, ...modelOptions }, configOptions);
|
169 |
+
}
|
170 |
+
|
171 |
+
async initialize({ user, message, onAgentAction, onChainEnd, signal }) {
|
172 |
+
const modelOptions = {
|
173 |
+
modelName: this.agentOptions.model,
|
174 |
+
temperature: this.agentOptions.temperature,
|
175 |
+
};
|
176 |
+
|
177 |
+
const configOptions = {};
|
178 |
+
|
179 |
+
if (this.langchainProxy) {
|
180 |
+
configOptions.basePath = this.langchainProxy;
|
181 |
+
}
|
182 |
+
|
183 |
+
const model = this.createLLM(modelOptions, configOptions);
|
184 |
+
|
185 |
+
if (this.options.debug) {
|
186 |
+
console.debug(
|
187 |
+
`<-----Agent Model: ${model.modelName} | Temp: ${model.temperature} | Functions: ${this.functionsAgent}----->`,
|
188 |
+
);
|
189 |
+
}
|
190 |
+
|
191 |
+
this.availableTools = await loadTools({
|
192 |
+
user,
|
193 |
+
model,
|
194 |
+
tools: this.options.tools,
|
195 |
+
functions: this.functionsAgent,
|
196 |
+
options: {
|
197 |
+
openAIApiKey: this.openAIApiKey,
|
198 |
+
debug: this.options?.debug,
|
199 |
+
message,
|
200 |
+
},
|
201 |
+
});
|
202 |
+
// load tools
|
203 |
+
for (const tool of this.options.tools) {
|
204 |
+
const validTool = this.availableTools[tool];
|
205 |
+
|
206 |
+
if (tool === 'plugins') {
|
207 |
+
const plugins = await validTool();
|
208 |
+
this.tools = [...this.tools, ...plugins];
|
209 |
+
} else if (validTool) {
|
210 |
+
this.tools.push(await validTool());
|
211 |
+
}
|
212 |
+
}
|
213 |
+
|
214 |
+
if (this.options.debug) {
|
215 |
+
console.debug('Requested Tools');
|
216 |
+
console.debug(this.options.tools);
|
217 |
+
console.debug('Loaded Tools');
|
218 |
+
console.debug(this.tools.map((tool) => tool.name));
|
219 |
+
}
|
220 |
+
|
221 |
+
if (this.tools.length > 0 && !this.functionsAgent) {
|
222 |
+
this.tools.push(new SelfReflectionTool({ message, isGpt3: false }));
|
223 |
+
} else if (this.tools.length === 0) {
|
224 |
+
return;
|
225 |
+
}
|
226 |
+
|
227 |
+
const handleAction = (action, callback = null) => {
|
228 |
+
this.saveLatestAction(action);
|
229 |
+
|
230 |
+
if (this.options.debug) {
|
231 |
+
console.debug('Latest Agent Action ', this.actions[this.actions.length - 1]);
|
232 |
+
}
|
233 |
+
|
234 |
+
if (typeof callback === 'function') {
|
235 |
+
callback(action);
|
236 |
+
}
|
237 |
+
};
|
238 |
+
|
239 |
+
// Map Messages to Langchain format
|
240 |
+
const pastMessages = this.currentMessages
|
241 |
+
.slice(0, -1)
|
242 |
+
.map((msg) =>
|
243 |
+
msg?.isCreatedByUser || msg?.role?.toLowerCase() === 'user'
|
244 |
+
? new HumanChatMessage(msg.text)
|
245 |
+
: new AIChatMessage(msg.text),
|
246 |
+
);
|
247 |
+
|
248 |
+
// initialize agent
|
249 |
+
const initializer = this.functionsAgent ? initializeFunctionsAgent : initializeCustomAgent;
|
250 |
+
this.executor = await initializer({
|
251 |
+
model,
|
252 |
+
signal,
|
253 |
+
pastMessages,
|
254 |
+
tools: this.tools,
|
255 |
+
currentDateString: this.currentDateString,
|
256 |
+
verbose: this.options.debug,
|
257 |
+
returnIntermediateSteps: true,
|
258 |
+
callbackManager: CallbackManager.fromHandlers({
|
259 |
+
async handleAgentAction(action) {
|
260 |
+
handleAction(action, onAgentAction);
|
261 |
+
},
|
262 |
+
async handleChainEnd(action) {
|
263 |
+
if (typeof onChainEnd === 'function') {
|
264 |
+
onChainEnd(action);
|
265 |
+
}
|
266 |
+
},
|
267 |
+
}),
|
268 |
+
});
|
269 |
+
|
270 |
+
if (this.options.debug) {
|
271 |
+
console.debug('Loaded agent.');
|
272 |
+
}
|
273 |
+
|
274 |
+
onAgentAction(
|
275 |
+
{
|
276 |
+
tool: 'self-reflection',
|
277 |
+
toolInput: `Processing the User's message:\n"${message}"`,
|
278 |
+
log: '',
|
279 |
+
},
|
280 |
+
true,
|
281 |
+
);
|
282 |
+
}
|
283 |
+
|
284 |
+
async executorCall(message, signal) {
|
285 |
+
let errorMessage = '';
|
286 |
+
const maxAttempts = 1;
|
287 |
+
|
288 |
+
for (let attempts = 1; attempts <= maxAttempts; attempts++) {
|
289 |
+
const errorInput = this.buildErrorInput(message, errorMessage);
|
290 |
+
const input = attempts > 1 ? errorInput : message;
|
291 |
+
|
292 |
+
if (this.options.debug) {
|
293 |
+
console.debug(`Attempt ${attempts} of ${maxAttempts}`);
|
294 |
+
}
|
295 |
+
|
296 |
+
if (this.options.debug && errorMessage.length > 0) {
|
297 |
+
console.debug('Caught error, input:', input);
|
298 |
+
}
|
299 |
+
|
300 |
+
try {
|
301 |
+
this.result = await this.executor.call({ input, signal });
|
302 |
+
break; // Exit the loop if the function call is successful
|
303 |
+
} catch (err) {
|
304 |
+
console.error(err);
|
305 |
+
errorMessage = err.message;
|
306 |
+
const content = findMessageContent(message);
|
307 |
+
if (content) {
|
308 |
+
errorMessage = content;
|
309 |
+
break;
|
310 |
+
}
|
311 |
+
if (attempts === maxAttempts) {
|
312 |
+
this.result.output = `Encountered an error while attempting to respond. Error: ${err.message}`;
|
313 |
+
this.result.intermediateSteps = this.actions;
|
314 |
+
this.result.errorMessage = errorMessage;
|
315 |
+
break;
|
316 |
+
}
|
317 |
+
}
|
318 |
+
}
|
319 |
+
}
|
320 |
+
|
321 |
+
addImages(intermediateSteps, responseMessage) {
|
322 |
+
if (!intermediateSteps || !responseMessage) {
|
323 |
+
return;
|
324 |
+
}
|
325 |
+
|
326 |
+
intermediateSteps.forEach((step) => {
|
327 |
+
const { observation } = step;
|
328 |
+
if (!observation || !observation.includes('![')) {
|
329 |
+
return;
|
330 |
+
}
|
331 |
+
|
332 |
+
// Extract the image file path from the observation
|
333 |
+
const observedImagePath = observation.match(/\(\/images\/.*\.\w*\)/g)[0];
|
334 |
+
|
335 |
+
// Check if the responseMessage already includes the image file path
|
336 |
+
if (!responseMessage.text.includes(observedImagePath)) {
|
337 |
+
// If the image file path is not found, append the whole observation
|
338 |
+
responseMessage.text += '\n' + observation;
|
339 |
+
if (this.options.debug) {
|
340 |
+
console.debug('added image from intermediateSteps');
|
341 |
+
}
|
342 |
+
}
|
343 |
+
});
|
344 |
+
}
|
345 |
+
|
346 |
+
async handleResponseMessage(responseMessage, saveOptions, user) {
|
347 |
+
responseMessage.tokenCount = this.getTokenCountForResponse(responseMessage);
|
348 |
+
responseMessage.completionTokens = responseMessage.tokenCount;
|
349 |
+
await this.saveMessageToDatabase(responseMessage, saveOptions, user);
|
350 |
+
delete responseMessage.tokenCount;
|
351 |
+
return { ...responseMessage, ...this.result };
|
352 |
+
}
|
353 |
+
|
354 |
+
async sendMessage(message, opts = {}) {
|
355 |
+
const completionMode = this.options.tools.length === 0;
|
356 |
+
if (completionMode) {
|
357 |
+
this.setOptions(opts);
|
358 |
+
return super.sendMessage(message, opts);
|
359 |
+
}
|
360 |
+
console.log('Plugins sendMessage', message, opts);
|
361 |
+
const {
|
362 |
+
user,
|
363 |
+
conversationId,
|
364 |
+
responseMessageId,
|
365 |
+
saveOptions,
|
366 |
+
userMessage,
|
367 |
+
onAgentAction,
|
368 |
+
onChainEnd,
|
369 |
+
} = await this.handleStartMethods(message, opts);
|
370 |
+
|
371 |
+
this.currentMessages.push(userMessage);
|
372 |
+
|
373 |
+
let {
|
374 |
+
prompt: payload,
|
375 |
+
tokenCountMap,
|
376 |
+
promptTokens,
|
377 |
+
messages,
|
378 |
+
} = await this.buildMessages(
|
379 |
+
this.currentMessages,
|
380 |
+
userMessage.messageId,
|
381 |
+
this.getBuildMessagesOptions({
|
382 |
+
promptPrefix: null,
|
383 |
+
abortController: this.abortController,
|
384 |
+
}),
|
385 |
+
);
|
386 |
+
|
387 |
+
if (tokenCountMap) {
|
388 |
+
console.dir(tokenCountMap, { depth: null });
|
389 |
+
if (tokenCountMap[userMessage.messageId]) {
|
390 |
+
userMessage.tokenCount = tokenCountMap[userMessage.messageId];
|
391 |
+
console.log('userMessage.tokenCount', userMessage.tokenCount);
|
392 |
+
}
|
393 |
+
payload = payload.map((message) => {
|
394 |
+
const messageWithoutTokenCount = message;
|
395 |
+
delete messageWithoutTokenCount.tokenCount;
|
396 |
+
return messageWithoutTokenCount;
|
397 |
+
});
|
398 |
+
this.handleTokenCountMap(tokenCountMap);
|
399 |
+
}
|
400 |
+
|
401 |
+
this.result = {};
|
402 |
+
if (messages) {
|
403 |
+
this.currentMessages = messages;
|
404 |
+
}
|
405 |
+
await this.saveMessageToDatabase(userMessage, saveOptions, user);
|
406 |
+
const responseMessage = {
|
407 |
+
messageId: responseMessageId,
|
408 |
+
conversationId,
|
409 |
+
parentMessageId: userMessage.messageId,
|
410 |
+
isCreatedByUser: false,
|
411 |
+
model: this.modelOptions.model,
|
412 |
+
sender: this.sender,
|
413 |
+
promptTokens,
|
414 |
+
};
|
415 |
+
|
416 |
+
await this.initialize({
|
417 |
+
user,
|
418 |
+
message,
|
419 |
+
onAgentAction,
|
420 |
+
onChainEnd,
|
421 |
+
signal: this.abortController.signal,
|
422 |
+
});
|
423 |
+
await this.executorCall(message, this.abortController.signal);
|
424 |
+
|
425 |
+
// If message was aborted mid-generation
|
426 |
+
if (this.result?.errorMessage?.length > 0 && this.result?.errorMessage?.includes('cancel')) {
|
427 |
+
responseMessage.text = 'Cancelled.';
|
428 |
+
return await this.handleResponseMessage(responseMessage, saveOptions, user);
|
429 |
+
}
|
430 |
+
|
431 |
+
if (this.agentOptions.skipCompletion && this.result.output) {
|
432 |
+
responseMessage.text = this.result.output;
|
433 |
+
this.addImages(this.result.intermediateSteps, responseMessage);
|
434 |
+
await this.generateTextStream(this.result.output, opts.onProgress, { delay: 8 });
|
435 |
+
return await this.handleResponseMessage(responseMessage, saveOptions, user);
|
436 |
+
}
|
437 |
+
|
438 |
+
if (this.options.debug) {
|
439 |
+
console.debug('Plugins completion phase: this.result');
|
440 |
+
console.debug(this.result);
|
441 |
+
}
|
442 |
+
|
443 |
+
const promptPrefix = this.buildPromptPrefix(this.result, message);
|
444 |
+
|
445 |
+
if (this.options.debug) {
|
446 |
+
console.debug('Plugins: promptPrefix');
|
447 |
+
console.debug(promptPrefix);
|
448 |
+
}
|
449 |
+
|
450 |
+
payload = await this.buildCompletionPrompt({
|
451 |
+
messages: this.currentMessages,
|
452 |
+
promptPrefix,
|
453 |
+
});
|
454 |
+
|
455 |
+
if (this.options.debug) {
|
456 |
+
console.debug('buildCompletionPrompt Payload');
|
457 |
+
console.debug(payload);
|
458 |
+
}
|
459 |
+
responseMessage.text = await this.sendCompletion(payload, opts);
|
460 |
+
return await this.handleResponseMessage(responseMessage, saveOptions, user);
|
461 |
+
}
|
462 |
+
|
463 |
+
async buildCompletionPrompt({ messages, promptPrefix: _promptPrefix }) {
|
464 |
+
if (this.options.debug) {
|
465 |
+
console.debug('buildCompletionPrompt messages', messages);
|
466 |
+
}
|
467 |
+
|
468 |
+
const orderedMessages = messages;
|
469 |
+
let promptPrefix = _promptPrefix.trim();
|
470 |
+
// If the prompt prefix doesn't end with the end token, add it.
|
471 |
+
if (!promptPrefix.endsWith(`${this.endToken}`)) {
|
472 |
+
promptPrefix = `${promptPrefix.trim()}${this.endToken}\n\n`;
|
473 |
+
}
|
474 |
+
promptPrefix = `${this.startToken}Instructions:\n${promptPrefix}`;
|
475 |
+
const promptSuffix = `${this.startToken}${this.chatGptLabel ?? 'Assistant'}:\n`;
|
476 |
+
|
477 |
+
const instructionsPayload = {
|
478 |
+
role: 'system',
|
479 |
+
name: 'instructions',
|
480 |
+
content: promptPrefix,
|
481 |
+
};
|
482 |
+
|
483 |
+
const messagePayload = {
|
484 |
+
role: 'system',
|
485 |
+
content: promptSuffix,
|
486 |
+
};
|
487 |
+
|
488 |
+
if (this.isGpt3) {
|
489 |
+
instructionsPayload.role = 'user';
|
490 |
+
messagePayload.role = 'user';
|
491 |
+
instructionsPayload.content += `\n${promptSuffix}`;
|
492 |
+
}
|
493 |
+
|
494 |
+
// testing if this works with browser endpoint
|
495 |
+
if (!this.isGpt3 && this.options.reverseProxyUrl) {
|
496 |
+
instructionsPayload.role = 'user';
|
497 |
+
}
|
498 |
+
|
499 |
+
let currentTokenCount =
|
500 |
+
this.getTokenCountForMessage(instructionsPayload) +
|
501 |
+
this.getTokenCountForMessage(messagePayload);
|
502 |
+
|
503 |
+
let promptBody = '';
|
504 |
+
const maxTokenCount = this.maxPromptTokens;
|
505 |
+
// Iterate backwards through the messages, adding them to the prompt until we reach the max token count.
|
506 |
+
// Do this within a recursive async function so that it doesn't block the event loop for too long.
|
507 |
+
const buildPromptBody = async () => {
|
508 |
+
if (currentTokenCount < maxTokenCount && orderedMessages.length > 0) {
|
509 |
+
const message = orderedMessages.pop();
|
510 |
+
const isCreatedByUser = message.isCreatedByUser || message.role?.toLowerCase() === 'user';
|
511 |
+
const roleLabel = isCreatedByUser ? this.userLabel : this.chatGptLabel;
|
512 |
+
let messageString = `${this.startToken}${roleLabel}:\n${message.text}${this.endToken}\n`;
|
513 |
+
let newPromptBody = `${messageString}${promptBody}`;
|
514 |
+
|
515 |
+
const tokenCountForMessage = this.getTokenCount(messageString);
|
516 |
+
const newTokenCount = currentTokenCount + tokenCountForMessage;
|
517 |
+
if (newTokenCount > maxTokenCount) {
|
518 |
+
if (promptBody) {
|
519 |
+
// This message would put us over the token limit, so don't add it.
|
520 |
+
return false;
|
521 |
+
}
|
522 |
+
// This is the first message, so we can't add it. Just throw an error.
|
523 |
+
throw new Error(
|
524 |
+
`Prompt is too long. Max token count is ${maxTokenCount}, but prompt is ${newTokenCount} tokens long.`,
|
525 |
+
);
|
526 |
+
}
|
527 |
+
promptBody = newPromptBody;
|
528 |
+
currentTokenCount = newTokenCount;
|
529 |
+
// wait for next tick to avoid blocking the event loop
|
530 |
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
531 |
+
return buildPromptBody();
|
532 |
+
}
|
533 |
+
return true;
|
534 |
+
};
|
535 |
+
|
536 |
+
await buildPromptBody();
|
537 |
+
const prompt = promptBody;
|
538 |
+
messagePayload.content = prompt;
|
539 |
+
// Add 2 tokens for metadata after all messages have been counted.
|
540 |
+
currentTokenCount += 2;
|
541 |
+
|
542 |
+
if (this.isGpt3 && messagePayload.content.length > 0) {
|
543 |
+
const context = 'Chat History:\n';
|
544 |
+
messagePayload.content = `${context}${prompt}`;
|
545 |
+
currentTokenCount += this.getTokenCount(context);
|
546 |
+
}
|
547 |
+
|
548 |
+
// Use up to `this.maxContextTokens` tokens (prompt + response), but try to leave `this.maxTokens` tokens for the response.
|
549 |
+
this.modelOptions.max_tokens = Math.min(
|
550 |
+
this.maxContextTokens - currentTokenCount,
|
551 |
+
this.maxResponseTokens,
|
552 |
+
);
|
553 |
+
|
554 |
+
if (this.isGpt3) {
|
555 |
+
messagePayload.content += promptSuffix;
|
556 |
+
return [instructionsPayload, messagePayload];
|
557 |
+
}
|
558 |
+
|
559 |
+
const result = [messagePayload, instructionsPayload];
|
560 |
+
|
561 |
+
if (this.functionsAgent && !this.isGpt3) {
|
562 |
+
result[1].content = `${result[1].content}\n${this.startToken}${this.chatGptLabel}:\nSure thing! Here is the output you requested:\n`;
|
563 |
+
}
|
564 |
+
|
565 |
+
return result.filter((message) => message.content.length > 0);
|
566 |
+
}
|
567 |
+
}
|
568 |
+
|
569 |
+
module.exports = PluginsClient;
|
api/app/clients/TextStream.js
ADDED
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const { Readable } = require('stream');
|
2 |
+
|
3 |
+
class TextStream extends Readable {
|
4 |
+
constructor(text, options = {}) {
|
5 |
+
super(options);
|
6 |
+
this.text = text;
|
7 |
+
this.currentIndex = 0;
|
8 |
+
this.delay = options.delay || 20; // Time in milliseconds
|
9 |
+
}
|
10 |
+
|
11 |
+
_read() {
|
12 |
+
const minChunkSize = 2;
|
13 |
+
const maxChunkSize = 4;
|
14 |
+
const { delay } = this;
|
15 |
+
|
16 |
+
if (this.currentIndex < this.text.length) {
|
17 |
+
setTimeout(() => {
|
18 |
+
const remainingChars = this.text.length - this.currentIndex;
|
19 |
+
const chunkSize = Math.min(this.randomInt(minChunkSize, maxChunkSize + 1), remainingChars);
|
20 |
+
|
21 |
+
const chunk = this.text.slice(this.currentIndex, this.currentIndex + chunkSize);
|
22 |
+
this.push(chunk);
|
23 |
+
this.currentIndex += chunkSize;
|
24 |
+
}, delay);
|
25 |
+
} else {
|
26 |
+
this.push(null); // signal end of data
|
27 |
+
}
|
28 |
+
}
|
29 |
+
|
30 |
+
randomInt(min, max) {
|
31 |
+
return Math.floor(Math.random() * (max - min)) + min;
|
32 |
+
}
|
33 |
+
|
34 |
+
async processTextStream(onProgressCallback) {
|
35 |
+
const streamPromise = new Promise((resolve, reject) => {
|
36 |
+
this.on('data', (chunk) => {
|
37 |
+
onProgressCallback(chunk.toString());
|
38 |
+
});
|
39 |
+
|
40 |
+
this.on('end', () => {
|
41 |
+
console.log('Stream ended');
|
42 |
+
resolve();
|
43 |
+
});
|
44 |
+
|
45 |
+
this.on('error', (err) => {
|
46 |
+
reject(err);
|
47 |
+
});
|
48 |
+
});
|
49 |
+
|
50 |
+
try {
|
51 |
+
await streamPromise;
|
52 |
+
} catch (err) {
|
53 |
+
console.error('Error processing text stream:', err);
|
54 |
+
// Handle the error appropriately, e.g., return an error message or throw an error
|
55 |
+
}
|
56 |
+
}
|
57 |
+
}
|
58 |
+
|
59 |
+
module.exports = TextStream;
|
api/app/clients/agents/CustomAgent/CustomAgent.js
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const { ZeroShotAgent } = require('langchain/agents');
|
2 |
+
const { PromptTemplate, renderTemplate } = require('langchain/prompts');
|
3 |
+
const { gpt3, gpt4 } = require('./instructions');
|
4 |
+
|
5 |
+
class CustomAgent extends ZeroShotAgent {
|
6 |
+
constructor(input) {
|
7 |
+
super(input);
|
8 |
+
}
|
9 |
+
|
10 |
+
_stop() {
|
11 |
+
return ['\nObservation:', '\nObservation 1:'];
|
12 |
+
}
|
13 |
+
|
14 |
+
static createPrompt(tools, opts = {}) {
|
15 |
+
const { currentDateString, model } = opts;
|
16 |
+
const inputVariables = ['input', 'chat_history', 'agent_scratchpad'];
|
17 |
+
|
18 |
+
let prefix, instructions, suffix;
|
19 |
+
if (model.startsWith('gpt-3')) {
|
20 |
+
prefix = gpt3.prefix;
|
21 |
+
instructions = gpt3.instructions;
|
22 |
+
suffix = gpt3.suffix;
|
23 |
+
} else if (model.startsWith('gpt-4')) {
|
24 |
+
prefix = gpt4.prefix;
|
25 |
+
instructions = gpt4.instructions;
|
26 |
+
suffix = gpt4.suffix;
|
27 |
+
}
|
28 |
+
|
29 |
+
const toolStrings = tools
|
30 |
+
.filter((tool) => tool.name !== 'self-reflection')
|
31 |
+
.map((tool) => `${tool.name}: ${tool.description}`)
|
32 |
+
.join('\n');
|
33 |
+
const toolNames = tools.map((tool) => tool.name);
|
34 |
+
const formatInstructions = (0, renderTemplate)(instructions, 'f-string', {
|
35 |
+
tool_names: toolNames,
|
36 |
+
});
|
37 |
+
const template = [
|
38 |
+
`Date: ${currentDateString}\n${prefix}`,
|
39 |
+
toolStrings,
|
40 |
+
formatInstructions,
|
41 |
+
suffix,
|
42 |
+
].join('\n\n');
|
43 |
+
return new PromptTemplate({
|
44 |
+
template,
|
45 |
+
inputVariables,
|
46 |
+
});
|
47 |
+
}
|
48 |
+
}
|
49 |
+
|
50 |
+
module.exports = CustomAgent;
|
api/app/clients/agents/CustomAgent/initializeCustomAgent.js
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const CustomAgent = require('./CustomAgent');
|
2 |
+
const { CustomOutputParser } = require('./outputParser');
|
3 |
+
const { AgentExecutor } = require('langchain/agents');
|
4 |
+
const { LLMChain } = require('langchain/chains');
|
5 |
+
const { BufferMemory, ChatMessageHistory } = require('langchain/memory');
|
6 |
+
const {
|
7 |
+
ChatPromptTemplate,
|
8 |
+
SystemMessagePromptTemplate,
|
9 |
+
HumanMessagePromptTemplate,
|
10 |
+
} = require('langchain/prompts');
|
11 |
+
|
12 |
+
const initializeCustomAgent = async ({
|
13 |
+
tools,
|
14 |
+
model,
|
15 |
+
pastMessages,
|
16 |
+
currentDateString,
|
17 |
+
...rest
|
18 |
+
}) => {
|
19 |
+
let prompt = CustomAgent.createPrompt(tools, { currentDateString, model: model.modelName });
|
20 |
+
|
21 |
+
const chatPrompt = ChatPromptTemplate.fromPromptMessages([
|
22 |
+
new SystemMessagePromptTemplate(prompt),
|
23 |
+
HumanMessagePromptTemplate.fromTemplate(`{chat_history}
|
24 |
+
Query: {input}
|
25 |
+
{agent_scratchpad}`),
|
26 |
+
]);
|
27 |
+
|
28 |
+
const outputParser = new CustomOutputParser({ tools });
|
29 |
+
|
30 |
+
const memory = new BufferMemory({
|
31 |
+
chatHistory: new ChatMessageHistory(pastMessages),
|
32 |
+
// returnMessages: true, // commenting this out retains memory
|
33 |
+
memoryKey: 'chat_history',
|
34 |
+
humanPrefix: 'User',
|
35 |
+
aiPrefix: 'Assistant',
|
36 |
+
inputKey: 'input',
|
37 |
+
outputKey: 'output',
|
38 |
+
});
|
39 |
+
|
40 |
+
const llmChain = new LLMChain({
|
41 |
+
prompt: chatPrompt,
|
42 |
+
llm: model,
|
43 |
+
});
|
44 |
+
|
45 |
+
const agent = new CustomAgent({
|
46 |
+
llmChain,
|
47 |
+
outputParser,
|
48 |
+
allowedTools: tools.map((tool) => tool.name),
|
49 |
+
});
|
50 |
+
|
51 |
+
return AgentExecutor.fromAgentAndTools({ agent, tools, memory, ...rest });
|
52 |
+
};
|
53 |
+
|
54 |
+
module.exports = initializeCustomAgent;
|
api/app/clients/agents/CustomAgent/instructions.js
ADDED
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/*
|
2 |
+
module.exports = `You are ChatGPT, a Large Language model with useful tools.
|
3 |
+
|
4 |
+
Talk to the human and provide meaningful answers when questions are asked.
|
5 |
+
|
6 |
+
Use the tools when you need them, but use your own knowledge if you are confident of the answer. Keep answers short and concise.
|
7 |
+
|
8 |
+
A tool is not usually needed for creative requests, so do your best to answer them without tools.
|
9 |
+
|
10 |
+
Avoid repeating identical answers if it appears before. Only fulfill the human's requests, do not create extra steps beyond what the human has asked for.
|
11 |
+
|
12 |
+
Your input for 'Action' should be the name of tool used only.
|
13 |
+
|
14 |
+
Be honest. If you can't answer something, or a tool is not appropriate, say you don't know or answer to the best of your ability.
|
15 |
+
|
16 |
+
Attempt to fulfill the human's requests in as few actions as possible`;
|
17 |
+
*/
|
18 |
+
|
19 |
+
// module.exports = `You are ChatGPT, a highly knowledgeable and versatile large language model.
|
20 |
+
|
21 |
+
// Engage with the Human conversationally, providing concise and meaningful answers to questions. Utilize built-in tools when necessary, except for creative requests, where relying on your own knowledge is preferred. Aim for variety and avoid repetitive answers.
|
22 |
+
|
23 |
+
// For your 'Action' input, state the name of the tool used only, and honor user requests without adding extra steps. Always be honest; if you cannot provide an appropriate answer or tool, admit that or do your best.
|
24 |
+
|
25 |
+
// Strive to meet the user's needs efficiently with minimal actions.`;
|
26 |
+
|
27 |
+
// import {
|
28 |
+
// BasePromptTemplate,
|
29 |
+
// BaseStringPromptTemplate,
|
30 |
+
// SerializedBasePromptTemplate,
|
31 |
+
// renderTemplate,
|
32 |
+
// } from "langchain/prompts";
|
33 |
+
|
34 |
+
// prefix: `You are ChatGPT, a highly knowledgeable and versatile large language model.
|
35 |
+
// Your objective is to help users by understanding their intent and choosing the best action. Prioritize direct, specific responses. Use concise, varied answers and rely on your knowledge for creative tasks. Utilize tools when needed, and structure results for machine compatibility.
|
36 |
+
// prefix: `Objective: to comprehend human intentions based on user input and available tools. Goal: identify the best action to directly address the human's query. In your subsequent steps, you will utilize the chosen action. You may select multiple actions and list them in a meaningful order. Prioritize actions that directly relate to the user's query over general ones. Ensure that the generated thought is highly specific and explicit to best match the user's expectations. Construct the result in a manner that an online open-API would most likely expect. Provide concise and meaningful answers to human queries. Utilize tools when necessary. Relying on your own knowledge is preferred for creative requests. Aim for variety and avoid repetitive answers.
|
37 |
+
|
38 |
+
// # Available Actions & Tools:
|
39 |
+
// N/A: no suitable action, use your own knowledge.`,
|
40 |
+
// suffix: `Remember, all your responses MUST adhere to the described format and only respond if the format is followed. Output exactly with the requested format, avoiding any other text as this will be parsed by a machine. Following 'Action:', provide only one of the actions listed above. If a tool is not necessary, deduce this quickly and finish your response. Honor the human's requests without adding extra steps. Carry out tasks in the sequence written by the human. Always be honest; if you cannot provide an appropriate answer or tool, do your best with your own knowledge. Strive to meet the user's needs efficiently with minimal actions.`;
|
41 |
+
|
42 |
+
module.exports = {
|
43 |
+
'gpt3-v1': {
|
44 |
+
prefix: `Objective: Understand human intentions using user input and available tools. Goal: Identify the most suitable actions to directly address user queries.
|
45 |
+
|
46 |
+
When responding:
|
47 |
+
- Choose actions relevant to the user's query, using multiple actions in a logical order if needed.
|
48 |
+
- Prioritize direct and specific thoughts to meet user expectations.
|
49 |
+
- Format results in a way compatible with open-API expectations.
|
50 |
+
- Offer concise, meaningful answers to user queries.
|
51 |
+
- Use tools when necessary but rely on your own knowledge for creative requests.
|
52 |
+
- Strive for variety, avoiding repetitive responses.
|
53 |
+
|
54 |
+
# Available Actions & Tools:
|
55 |
+
N/A: No suitable action; use your own knowledge.`,
|
56 |
+
instructions: `Always adhere to the following format in your response to indicate actions taken:
|
57 |
+
|
58 |
+
Thought: Summarize your thought process.
|
59 |
+
Action: Select an action from [{tool_names}].
|
60 |
+
Action Input: Define the action's input.
|
61 |
+
Observation: Report the action's result.
|
62 |
+
|
63 |
+
Repeat steps 1-4 as needed, in order. When not using a tool, use N/A for Action, provide the result as Action Input, and include an Observation.
|
64 |
+
|
65 |
+
Upon reaching the final answer, use this format after completing all necessary actions:
|
66 |
+
|
67 |
+
Thought: Indicate that you've determined the final answer.
|
68 |
+
Final Answer: Present the answer to the user's query.`,
|
69 |
+
suffix: `Keep these guidelines in mind when crafting your response:
|
70 |
+
- Strictly adhere to the Action format for all responses, as they will be machine-parsed.
|
71 |
+
- If a tool is unnecessary, quickly move to the Thought/Final Answer format.
|
72 |
+
- Follow the logical sequence provided by the user without adding extra steps.
|
73 |
+
- Be honest; if you can't provide an appropriate answer using the given tools, use your own knowledge.
|
74 |
+
- Aim for efficiency and minimal actions to meet the user's needs effectively.`,
|
75 |
+
},
|
76 |
+
'gpt3-v2': {
|
77 |
+
prefix: `Objective: Understand the human's query with available actions & tools. Let's work this out in a step by step way to be sure we fulfill the query.
|
78 |
+
|
79 |
+
When responding:
|
80 |
+
- Choose actions relevant to the user's query, using multiple actions in a logical order if needed.
|
81 |
+
- Prioritize direct and specific thoughts to meet user expectations.
|
82 |
+
- Format results in a way compatible with open-API expectations.
|
83 |
+
- Offer concise, meaningful answers to user queries.
|
84 |
+
- Use tools when necessary but rely on your own knowledge for creative requests.
|
85 |
+
- Strive for variety, avoiding repetitive responses.
|
86 |
+
|
87 |
+
# Available Actions & Tools:
|
88 |
+
N/A: No suitable action; use your own knowledge.`,
|
89 |
+
instructions: `I want you to respond with this format and this format only, without comments or explanations, to indicate actions taken:
|
90 |
+
\`\`\`
|
91 |
+
Thought: Summarize your thought process.
|
92 |
+
Action: Select an action from [{tool_names}].
|
93 |
+
Action Input: Define the action's input.
|
94 |
+
Observation: Report the action's result.
|
95 |
+
\`\`\`
|
96 |
+
|
97 |
+
Repeat the format for each action as needed. When not using a tool, use N/A for Action, provide the result as Action Input, and include an Observation.
|
98 |
+
|
99 |
+
Upon reaching the final answer, use this format after completing all necessary actions:
|
100 |
+
\`\`\`
|
101 |
+
Thought: Indicate that you've determined the final answer.
|
102 |
+
Final Answer: A conversational reply to the user's query as if you were answering them directly.
|
103 |
+
\`\`\``,
|
104 |
+
suffix: `Keep these guidelines in mind when crafting your response:
|
105 |
+
- Strictly adhere to the Action format for all responses, as they will be machine-parsed.
|
106 |
+
- If a tool is unnecessary, quickly move to the Thought/Final Answer format.
|
107 |
+
- Follow the logical sequence provided by the user without adding extra steps.
|
108 |
+
- Be honest; if you can't provide an appropriate answer using the given tools, use your own knowledge.
|
109 |
+
- Aim for efficiency and minimal actions to meet the user's needs effectively.`,
|
110 |
+
},
|
111 |
+
gpt3: {
|
112 |
+
prefix: `Objective: Understand the human's query with available actions & tools. Let's work this out in a step by step way to be sure we fulfill the query.
|
113 |
+
|
114 |
+
Use available actions and tools judiciously.
|
115 |
+
|
116 |
+
# Available Actions & Tools:
|
117 |
+
N/A: No suitable action; use your own knowledge.`,
|
118 |
+
instructions: `I want you to respond with this format and this format only, without comments or explanations, to indicate actions taken:
|
119 |
+
\`\`\`
|
120 |
+
Thought: Your thought process.
|
121 |
+
Action: Action from [{tool_names}].
|
122 |
+
Action Input: Action's input.
|
123 |
+
Observation: Action's result.
|
124 |
+
\`\`\`
|
125 |
+
|
126 |
+
For each action, repeat the format. If no tool is used, use N/A for Action, and provide the result as Action Input.
|
127 |
+
|
128 |
+
Finally, complete with:
|
129 |
+
\`\`\`
|
130 |
+
Thought: Convey final answer determination.
|
131 |
+
Final Answer: Reply to user's query conversationally.
|
132 |
+
\`\`\``,
|
133 |
+
suffix: `Remember:
|
134 |
+
- Adhere to the Action format strictly for parsing.
|
135 |
+
- Transition quickly to Thought/Final Answer format when a tool isn't needed.
|
136 |
+
- Follow user's logic without superfluous steps.
|
137 |
+
- If unable to use tools for a fitting answer, use your knowledge.
|
138 |
+
- Strive for efficient, minimal actions.`,
|
139 |
+
},
|
140 |
+
'gpt4-v1': {
|
141 |
+
prefix: `Objective: Understand the human's query with available actions & tools. Let's work this out in a step by step way to be sure we fulfill the query.
|
142 |
+
|
143 |
+
When responding:
|
144 |
+
- Choose actions relevant to the query, using multiple actions in a step by step way.
|
145 |
+
- Prioritize direct and specific thoughts to meet user expectations.
|
146 |
+
- Be precise and offer meaningful answers to user queries.
|
147 |
+
- Use tools when necessary but rely on your own knowledge for creative requests.
|
148 |
+
- Strive for variety, avoiding repetitive responses.
|
149 |
+
|
150 |
+
# Available Actions & Tools:
|
151 |
+
N/A: No suitable action; use your own knowledge.`,
|
152 |
+
instructions: `I want you to respond with this format and this format only, without comments or explanations, to indicate actions taken:
|
153 |
+
\`\`\`
|
154 |
+
Thought: Summarize your thought process.
|
155 |
+
Action: Select an action from [{tool_names}].
|
156 |
+
Action Input: Define the action's input.
|
157 |
+
Observation: Report the action's result.
|
158 |
+
\`\`\`
|
159 |
+
|
160 |
+
Repeat the format for each action as needed. When not using a tool, use N/A for Action, provide the result as Action Input, and include an Observation.
|
161 |
+
|
162 |
+
Upon reaching the final answer, use this format after completing all necessary actions:
|
163 |
+
\`\`\`
|
164 |
+
Thought: Indicate that you've determined the final answer.
|
165 |
+
Final Answer: A conversational reply to the user's query as if you were answering them directly.
|
166 |
+
\`\`\``,
|
167 |
+
suffix: `Keep these guidelines in mind when crafting your final response:
|
168 |
+
- Strictly adhere to the Action format for all responses.
|
169 |
+
- If a tool is unnecessary, quickly move to the Thought/Final Answer format, only if no further actions are possible or necessary.
|
170 |
+
- Follow the logical sequence provided by the user without adding extra steps.
|
171 |
+
- Be honest: if you can't provide an appropriate answer using the given tools, use your own knowledge.
|
172 |
+
- Aim for efficiency and minimal actions to meet the user's needs effectively.`,
|
173 |
+
},
|
174 |
+
gpt4: {
|
175 |
+
prefix: `Objective: Understand the human's query with available actions & tools. Let's work this out in a step by step way to be sure we fulfill the query.
|
176 |
+
|
177 |
+
Use available actions and tools judiciously.
|
178 |
+
|
179 |
+
# Available Actions & Tools:
|
180 |
+
N/A: No suitable action; use your own knowledge.`,
|
181 |
+
instructions: `Respond in this specific format without extraneous comments:
|
182 |
+
\`\`\`
|
183 |
+
Thought: Your thought process.
|
184 |
+
Action: Action from [{tool_names}].
|
185 |
+
Action Input: Action's input.
|
186 |
+
Observation: Action's result.
|
187 |
+
\`\`\`
|
188 |
+
|
189 |
+
For each action, repeat the format. If no tool is used, use N/A for Action, and provide the result as Action Input.
|
190 |
+
|
191 |
+
Finally, complete with:
|
192 |
+
\`\`\`
|
193 |
+
Thought: Indicate that you've determined the final answer.
|
194 |
+
Final Answer: A conversational reply to the user's query, including your full answer.
|
195 |
+
\`\`\``,
|
196 |
+
suffix: `Remember:
|
197 |
+
- Adhere to the Action format strictly for parsing.
|
198 |
+
- Transition quickly to Thought/Final Answer format when a tool isn't needed.
|
199 |
+
- Follow user's logic without superfluous steps.
|
200 |
+
- If unable to use tools for a fitting answer, use your knowledge.
|
201 |
+
- Strive for efficient, minimal actions.`,
|
202 |
+
},
|
203 |
+
};
|
api/app/clients/agents/CustomAgent/outputParser.js
ADDED
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const { ZeroShotAgentOutputParser } = require('langchain/agents');
|
2 |
+
|
3 |
+
class CustomOutputParser extends ZeroShotAgentOutputParser {
|
4 |
+
constructor(fields) {
|
5 |
+
super(fields);
|
6 |
+
this.tools = fields.tools;
|
7 |
+
this.longestToolName = '';
|
8 |
+
for (const tool of this.tools) {
|
9 |
+
if (tool.name.length > this.longestToolName.length) {
|
10 |
+
this.longestToolName = tool.name;
|
11 |
+
}
|
12 |
+
}
|
13 |
+
this.finishToolNameRegex = /(?:the\s+)?final\s+answer:\s*/i;
|
14 |
+
this.actionValues =
|
15 |
+
/(?:Action(?: [1-9])?:) ([\s\S]*?)(?:\n(?:Action Input(?: [1-9])?:) ([\s\S]*?))?$/i;
|
16 |
+
this.actionInputRegex = /(?:Action Input(?: *\d*):) ?([\s\S]*?)$/i;
|
17 |
+
this.thoughtRegex = /(?:Thought(?: *\d*):) ?([\s\S]*?)$/i;
|
18 |
+
}
|
19 |
+
|
20 |
+
getValidTool(text) {
|
21 |
+
let result = false;
|
22 |
+
for (const tool of this.tools) {
|
23 |
+
const { name } = tool;
|
24 |
+
const toolIndex = text.indexOf(name);
|
25 |
+
if (toolIndex !== -1) {
|
26 |
+
result = name;
|
27 |
+
break;
|
28 |
+
}
|
29 |
+
}
|
30 |
+
return result;
|
31 |
+
}
|
32 |
+
|
33 |
+
checkIfValidTool(text) {
|
34 |
+
let isValidTool = false;
|
35 |
+
for (const tool of this.tools) {
|
36 |
+
const { name } = tool;
|
37 |
+
if (text === name) {
|
38 |
+
isValidTool = true;
|
39 |
+
break;
|
40 |
+
}
|
41 |
+
}
|
42 |
+
return isValidTool;
|
43 |
+
}
|
44 |
+
|
45 |
+
async parse(text) {
|
46 |
+
const finalMatch = text.match(this.finishToolNameRegex);
|
47 |
+
// if (text.includes(this.finishToolName)) {
|
48 |
+
// const parts = text.split(this.finishToolName);
|
49 |
+
// const output = parts[parts.length - 1].trim();
|
50 |
+
// return {
|
51 |
+
// returnValues: { output },
|
52 |
+
// log: text
|
53 |
+
// };
|
54 |
+
// }
|
55 |
+
|
56 |
+
if (finalMatch) {
|
57 |
+
const output = text.substring(finalMatch.index + finalMatch[0].length).trim();
|
58 |
+
return {
|
59 |
+
returnValues: { output },
|
60 |
+
log: text,
|
61 |
+
};
|
62 |
+
}
|
63 |
+
|
64 |
+
const match = this.actionValues.exec(text); // old v2
|
65 |
+
|
66 |
+
if (!match) {
|
67 |
+
console.log(
|
68 |
+
'\n\n<----------------------HIT NO MATCH PARSING ERROR---------------------->\n\n',
|
69 |
+
match,
|
70 |
+
);
|
71 |
+
const thoughts = text.replace(/[tT]hought:/, '').split('\n');
|
72 |
+
// return {
|
73 |
+
// tool: 'self-reflection',
|
74 |
+
// toolInput: thoughts[0],
|
75 |
+
// log: thoughts.slice(1).join('\n')
|
76 |
+
// };
|
77 |
+
|
78 |
+
return {
|
79 |
+
returnValues: { output: thoughts[0] },
|
80 |
+
log: thoughts.slice(1).join('\n'),
|
81 |
+
};
|
82 |
+
}
|
83 |
+
|
84 |
+
let selectedTool = match?.[1].trim().toLowerCase();
|
85 |
+
|
86 |
+
if (match && selectedTool === 'n/a') {
|
87 |
+
console.log(
|
88 |
+
'\n\n<----------------------HIT N/A PARSING ERROR---------------------->\n\n',
|
89 |
+
match,
|
90 |
+
);
|
91 |
+
return {
|
92 |
+
tool: 'self-reflection',
|
93 |
+
toolInput: match[2]?.trim().replace(/^"+|"+$/g, '') ?? '',
|
94 |
+
log: text,
|
95 |
+
};
|
96 |
+
}
|
97 |
+
|
98 |
+
let toolIsValid = this.checkIfValidTool(selectedTool);
|
99 |
+
if (match && !toolIsValid) {
|
100 |
+
console.log(
|
101 |
+
'\n\n<----------------Tool invalid: Re-assigning Selected Tool---------------->\n\n',
|
102 |
+
match,
|
103 |
+
);
|
104 |
+
selectedTool = this.getValidTool(selectedTool);
|
105 |
+
}
|
106 |
+
|
107 |
+
if (match && !selectedTool) {
|
108 |
+
console.log(
|
109 |
+
'\n\n<----------------------HIT INVALID TOOL PARSING ERROR---------------------->\n\n',
|
110 |
+
match,
|
111 |
+
);
|
112 |
+
selectedTool = 'self-reflection';
|
113 |
+
}
|
114 |
+
|
115 |
+
if (match && !match[2]) {
|
116 |
+
console.log(
|
117 |
+
'\n\n<----------------------HIT NO ACTION INPUT PARSING ERROR---------------------->\n\n',
|
118 |
+
match,
|
119 |
+
);
|
120 |
+
|
121 |
+
// In case there is no action input, let's double-check if there is an action input in 'text' variable
|
122 |
+
const actionInputMatch = this.actionInputRegex.exec(text);
|
123 |
+
const thoughtMatch = this.thoughtRegex.exec(text);
|
124 |
+
if (actionInputMatch) {
|
125 |
+
return {
|
126 |
+
tool: selectedTool,
|
127 |
+
toolInput: actionInputMatch[1].trim(),
|
128 |
+
log: text,
|
129 |
+
};
|
130 |
+
}
|
131 |
+
|
132 |
+
if (thoughtMatch && !actionInputMatch) {
|
133 |
+
return {
|
134 |
+
tool: selectedTool,
|
135 |
+
toolInput: thoughtMatch[1].trim(),
|
136 |
+
log: text,
|
137 |
+
};
|
138 |
+
}
|
139 |
+
}
|
140 |
+
|
141 |
+
if (match && selectedTool.length > this.longestToolName.length) {
|
142 |
+
console.log('\n\n<----------------------HIT LONG PARSING ERROR---------------------->\n\n');
|
143 |
+
|
144 |
+
let action, input, thought;
|
145 |
+
let firstIndex = Infinity;
|
146 |
+
|
147 |
+
for (const tool of this.tools) {
|
148 |
+
const { name } = tool;
|
149 |
+
const toolIndex = text.indexOf(name);
|
150 |
+
if (toolIndex !== -1 && toolIndex < firstIndex) {
|
151 |
+
firstIndex = toolIndex;
|
152 |
+
action = name;
|
153 |
+
}
|
154 |
+
}
|
155 |
+
|
156 |
+
// In case there is no action input, let's double-check if there is an action input in 'text' variable
|
157 |
+
const actionInputMatch = this.actionInputRegex.exec(text);
|
158 |
+
if (action && actionInputMatch) {
|
159 |
+
console.log(
|
160 |
+
'\n\n<------Matched Action Input in Long Parsing Error------>\n\n',
|
161 |
+
actionInputMatch,
|
162 |
+
);
|
163 |
+
return {
|
164 |
+
tool: action,
|
165 |
+
toolInput: actionInputMatch[1].trim().replaceAll('"', ''),
|
166 |
+
log: text,
|
167 |
+
};
|
168 |
+
}
|
169 |
+
|
170 |
+
if (action) {
|
171 |
+
const actionEndIndex = text.indexOf('Action:', firstIndex + action.length);
|
172 |
+
const inputText = text
|
173 |
+
.slice(firstIndex + action.length, actionEndIndex !== -1 ? actionEndIndex : undefined)
|
174 |
+
.trim();
|
175 |
+
const inputLines = inputText.split('\n');
|
176 |
+
input = inputLines[0];
|
177 |
+
if (inputLines.length > 1) {
|
178 |
+
thought = inputLines.slice(1).join('\n');
|
179 |
+
}
|
180 |
+
const returnValues = {
|
181 |
+
tool: action,
|
182 |
+
toolInput: input,
|
183 |
+
log: thought || inputText,
|
184 |
+
};
|
185 |
+
|
186 |
+
const inputMatch = this.actionValues.exec(returnValues.log); //new
|
187 |
+
if (inputMatch) {
|
188 |
+
console.log('inputMatch');
|
189 |
+
console.dir(inputMatch, { depth: null });
|
190 |
+
returnValues.toolInput = inputMatch[1].replaceAll('"', '').trim();
|
191 |
+
returnValues.log = returnValues.log.replace(this.actionValues, '');
|
192 |
+
}
|
193 |
+
|
194 |
+
return returnValues;
|
195 |
+
} else {
|
196 |
+
console.log('No valid tool mentioned.', this.tools, text);
|
197 |
+
return {
|
198 |
+
tool: 'self-reflection',
|
199 |
+
toolInput: 'Hypothetical actions: \n"' + text + '"\n',
|
200 |
+
log: 'Thought: I need to look at my hypothetical actions and try one',
|
201 |
+
};
|
202 |
+
}
|
203 |
+
|
204 |
+
// if (action && input) {
|
205 |
+
// console.log('Action:', action);
|
206 |
+
// console.log('Input:', input);
|
207 |
+
// }
|
208 |
+
}
|
209 |
+
|
210 |
+
return {
|
211 |
+
tool: selectedTool,
|
212 |
+
toolInput: match[2]?.trim()?.replace(/^"+|"+$/g, '') ?? '',
|
213 |
+
log: text,
|
214 |
+
};
|
215 |
+
}
|
216 |
+
}
|
217 |
+
|
218 |
+
module.exports = { CustomOutputParser };
|
api/app/clients/agents/Functions/FunctionsAgent.js
ADDED
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const { Agent } = require('langchain/agents');
|
2 |
+
const { LLMChain } = require('langchain/chains');
|
3 |
+
const { FunctionChatMessage, AIChatMessage } = require('langchain/schema');
|
4 |
+
const {
|
5 |
+
ChatPromptTemplate,
|
6 |
+
MessagesPlaceholder,
|
7 |
+
SystemMessagePromptTemplate,
|
8 |
+
HumanMessagePromptTemplate,
|
9 |
+
} = require('langchain/prompts');
|
10 |
+
const PREFIX = 'You are a helpful AI assistant.';
|
11 |
+
|
12 |
+
function parseOutput(message) {
|
13 |
+
if (message.additional_kwargs.function_call) {
|
14 |
+
const function_call = message.additional_kwargs.function_call;
|
15 |
+
return {
|
16 |
+
tool: function_call.name,
|
17 |
+
toolInput: function_call.arguments ? JSON.parse(function_call.arguments) : {},
|
18 |
+
log: message.text,
|
19 |
+
};
|
20 |
+
} else {
|
21 |
+
return { returnValues: { output: message.text }, log: message.text };
|
22 |
+
}
|
23 |
+
}
|
24 |
+
|
25 |
+
class FunctionsAgent extends Agent {
|
26 |
+
constructor(input) {
|
27 |
+
super({ ...input, outputParser: undefined });
|
28 |
+
this.tools = input.tools;
|
29 |
+
}
|
30 |
+
|
31 |
+
lc_namespace = ['langchain', 'agents', 'openai'];
|
32 |
+
|
33 |
+
_agentType() {
|
34 |
+
return 'openai-functions';
|
35 |
+
}
|
36 |
+
|
37 |
+
observationPrefix() {
|
38 |
+
return 'Observation: ';
|
39 |
+
}
|
40 |
+
|
41 |
+
llmPrefix() {
|
42 |
+
return 'Thought:';
|
43 |
+
}
|
44 |
+
|
45 |
+
_stop() {
|
46 |
+
return ['Observation:'];
|
47 |
+
}
|
48 |
+
|
49 |
+
static createPrompt(_tools, fields) {
|
50 |
+
const { prefix = PREFIX, currentDateString } = fields || {};
|
51 |
+
|
52 |
+
return ChatPromptTemplate.fromPromptMessages([
|
53 |
+
SystemMessagePromptTemplate.fromTemplate(`Date: ${currentDateString}\n${prefix}`),
|
54 |
+
new MessagesPlaceholder('chat_history'),
|
55 |
+
HumanMessagePromptTemplate.fromTemplate('Query: {input}'),
|
56 |
+
new MessagesPlaceholder('agent_scratchpad'),
|
57 |
+
]);
|
58 |
+
}
|
59 |
+
|
60 |
+
static fromLLMAndTools(llm, tools, args) {
|
61 |
+
FunctionsAgent.validateTools(tools);
|
62 |
+
const prompt = FunctionsAgent.createPrompt(tools, args);
|
63 |
+
const chain = new LLMChain({
|
64 |
+
prompt,
|
65 |
+
llm,
|
66 |
+
callbacks: args?.callbacks,
|
67 |
+
});
|
68 |
+
return new FunctionsAgent({
|
69 |
+
llmChain: chain,
|
70 |
+
allowedTools: tools.map((t) => t.name),
|
71 |
+
tools,
|
72 |
+
});
|
73 |
+
}
|
74 |
+
|
75 |
+
async constructScratchPad(steps) {
|
76 |
+
return steps.flatMap(({ action, observation }) => [
|
77 |
+
new AIChatMessage('', {
|
78 |
+
function_call: {
|
79 |
+
name: action.tool,
|
80 |
+
arguments: JSON.stringify(action.toolInput),
|
81 |
+
},
|
82 |
+
}),
|
83 |
+
new FunctionChatMessage(observation, action.tool),
|
84 |
+
]);
|
85 |
+
}
|
86 |
+
|
87 |
+
async plan(steps, inputs, callbackManager) {
|
88 |
+
// Add scratchpad and stop to inputs
|
89 |
+
const thoughts = await this.constructScratchPad(steps);
|
90 |
+
const newInputs = Object.assign({}, inputs, { agent_scratchpad: thoughts });
|
91 |
+
if (this._stop().length !== 0) {
|
92 |
+
newInputs.stop = this._stop();
|
93 |
+
}
|
94 |
+
|
95 |
+
// Split inputs between prompt and llm
|
96 |
+
const llm = this.llmChain.llm;
|
97 |
+
const valuesForPrompt = Object.assign({}, newInputs);
|
98 |
+
const valuesForLLM = {
|
99 |
+
tools: this.tools,
|
100 |
+
};
|
101 |
+
for (let i = 0; i < this.llmChain.llm.callKeys.length; i++) {
|
102 |
+
const key = this.llmChain.llm.callKeys[i];
|
103 |
+
if (key in inputs) {
|
104 |
+
valuesForLLM[key] = inputs[key];
|
105 |
+
delete valuesForPrompt[key];
|
106 |
+
}
|
107 |
+
}
|
108 |
+
|
109 |
+
const promptValue = await this.llmChain.prompt.formatPromptValue(valuesForPrompt);
|
110 |
+
const message = await llm.predictMessages(
|
111 |
+
promptValue.toChatMessages(),
|
112 |
+
valuesForLLM,
|
113 |
+
callbackManager,
|
114 |
+
);
|
115 |
+
console.log('message', message);
|
116 |
+
return parseOutput(message);
|
117 |
+
}
|
118 |
+
}
|
119 |
+
|
120 |
+
module.exports = FunctionsAgent;
|
api/app/clients/agents/Functions/initializeFunctionsAgent.js
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const { initializeAgentExecutorWithOptions } = require('langchain/agents');
|
2 |
+
const { BufferMemory, ChatMessageHistory } = require('langchain/memory');
|
3 |
+
|
4 |
+
const initializeFunctionsAgent = async ({
|
5 |
+
tools,
|
6 |
+
model,
|
7 |
+
pastMessages,
|
8 |
+
// currentDateString,
|
9 |
+
...rest
|
10 |
+
}) => {
|
11 |
+
const memory = new BufferMemory({
|
12 |
+
chatHistory: new ChatMessageHistory(pastMessages),
|
13 |
+
memoryKey: 'chat_history',
|
14 |
+
humanPrefix: 'User',
|
15 |
+
aiPrefix: 'Assistant',
|
16 |
+
inputKey: 'input',
|
17 |
+
outputKey: 'output',
|
18 |
+
returnMessages: true,
|
19 |
+
});
|
20 |
+
|
21 |
+
return await initializeAgentExecutorWithOptions(tools, model, {
|
22 |
+
agentType: 'openai-functions',
|
23 |
+
memory,
|
24 |
+
...rest,
|
25 |
+
});
|
26 |
+
};
|
27 |
+
|
28 |
+
module.exports = initializeFunctionsAgent;
|
api/app/clients/agents/index.js
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const initializeCustomAgent = require('./CustomAgent/initializeCustomAgent');
|
2 |
+
const initializeFunctionsAgent = require('./Functions/initializeFunctionsAgent');
|
3 |
+
|
4 |
+
module.exports = {
|
5 |
+
initializeCustomAgent,
|
6 |
+
initializeFunctionsAgent,
|
7 |
+
};
|
api/app/clients/index.js
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const ChatGPTClient = require('./ChatGPTClient');
|
2 |
+
const OpenAIClient = require('./OpenAIClient');
|
3 |
+
const PluginsClient = require('./PluginsClient');
|
4 |
+
const GoogleClient = require('./GoogleClient');
|
5 |
+
const TextStream = require('./TextStream');
|
6 |
+
const AnthropicClient = require('./AnthropicClient');
|
7 |
+
const toolUtils = require('./tools/util');
|
8 |
+
|
9 |
+
module.exports = {
|
10 |
+
ChatGPTClient,
|
11 |
+
OpenAIClient,
|
12 |
+
PluginsClient,
|
13 |
+
GoogleClient,
|
14 |
+
TextStream,
|
15 |
+
AnthropicClient,
|
16 |
+
...toolUtils,
|
17 |
+
};
|
api/app/clients/prompts/instructions.js
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
module.exports = {
|
2 |
+
instructions:
|
3 |
+
'Remember, all your responses MUST be in the format described. Do not respond unless it\'s in the format described, using the structure of Action, Action Input, etc.',
|
4 |
+
errorInstructions:
|
5 |
+
'\nYou encountered an error in attempting a response. The user is not aware of the error so you shouldn\'t mention it.\nReview the actions taken carefully in case there is a partial or complete answer within them.\nError Message:',
|
6 |
+
imageInstructions:
|
7 |
+
'You must include the exact image paths from above, formatted in Markdown syntax: ![alt-text](URL)',
|
8 |
+
completionInstructions:
|
9 |
+
'Instructions:\nYou are ChatGPT, a large language model trained by OpenAI. Respond conversationally.\nCurrent date:',
|
10 |
+
};
|
api/app/clients/prompts/refinePrompt.js
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const { PromptTemplate } = require('langchain/prompts');
|
2 |
+
|
3 |
+
const refinePromptTemplate = `Your job is to produce a final summary of the following conversation.
|
4 |
+
We have provided an existing summary up to a certain point: "{existing_answer}"
|
5 |
+
We have the opportunity to refine the existing summary
|
6 |
+
(only if needed) with some more context below.
|
7 |
+
------------
|
8 |
+
"{text}"
|
9 |
+
------------
|
10 |
+
|
11 |
+
Given the new context, refine the original summary of the conversation.
|
12 |
+
Do note who is speaking in the conversation to give proper context.
|
13 |
+
If the context isn't useful, return the original summary.
|
14 |
+
|
15 |
+
REFINED CONVERSATION SUMMARY:`;
|
16 |
+
|
17 |
+
const refinePrompt = new PromptTemplate({
|
18 |
+
template: refinePromptTemplate,
|
19 |
+
inputVariables: ['existing_answer', 'text'],
|
20 |
+
});
|
21 |
+
|
22 |
+
module.exports = {
|
23 |
+
refinePrompt,
|
24 |
+
};
|
api/app/clients/specs/BaseClient.test.js
ADDED
@@ -0,0 +1,369 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const { initializeFakeClient } = require('./FakeClient');
|
2 |
+
|
3 |
+
jest.mock('../../../lib/db/connectDb');
|
4 |
+
jest.mock('../../../models', () => {
|
5 |
+
return function () {
|
6 |
+
return {
|
7 |
+
save: jest.fn(),
|
8 |
+
deleteConvos: jest.fn(),
|
9 |
+
getConvo: jest.fn(),
|
10 |
+
getMessages: jest.fn(),
|
11 |
+
saveMessage: jest.fn(),
|
12 |
+
updateMessage: jest.fn(),
|
13 |
+
saveConvo: jest.fn(),
|
14 |
+
};
|
15 |
+
};
|
16 |
+
});
|
17 |
+
|
18 |
+
jest.mock('langchain/text_splitter', () => {
|
19 |
+
return {
|
20 |
+
RecursiveCharacterTextSplitter: jest.fn().mockImplementation(() => {
|
21 |
+
return { createDocuments: jest.fn().mockResolvedValue([]) };
|
22 |
+
}),
|
23 |
+
};
|
24 |
+
});
|
25 |
+
|
26 |
+
jest.mock('langchain/chat_models/openai', () => {
|
27 |
+
return {
|
28 |
+
ChatOpenAI: jest.fn().mockImplementation(() => {
|
29 |
+
return {};
|
30 |
+
}),
|
31 |
+
};
|
32 |
+
});
|
33 |
+
|
34 |
+
jest.mock('langchain/chains', () => {
|
35 |
+
return {
|
36 |
+
loadSummarizationChain: jest.fn().mockReturnValue({
|
37 |
+
call: jest.fn().mockResolvedValue({ output_text: 'Refined answer' }),
|
38 |
+
}),
|
39 |
+
};
|
40 |
+
});
|
41 |
+
|
42 |
+
let parentMessageId;
|
43 |
+
let conversationId;
|
44 |
+
const fakeMessages = [];
|
45 |
+
const userMessage = 'Hello, ChatGPT!';
|
46 |
+
const apiKey = 'fake-api-key';
|
47 |
+
|
48 |
+
describe('BaseClient', () => {
|
49 |
+
let TestClient;
|
50 |
+
const options = {
|
51 |
+
// debug: true,
|
52 |
+
modelOptions: {
|
53 |
+
model: 'gpt-3.5-turbo',
|
54 |
+
temperature: 0,
|
55 |
+
},
|
56 |
+
};
|
57 |
+
|
58 |
+
beforeEach(() => {
|
59 |
+
TestClient = initializeFakeClient(apiKey, options, fakeMessages);
|
60 |
+
});
|
61 |
+
|
62 |
+
test('returns the input messages without instructions when addInstructions() is called with empty instructions', () => {
|
63 |
+
const messages = [{ content: 'Hello' }, { content: 'How are you?' }, { content: 'Goodbye' }];
|
64 |
+
const instructions = '';
|
65 |
+
const result = TestClient.addInstructions(messages, instructions);
|
66 |
+
expect(result).toEqual(messages);
|
67 |
+
});
|
68 |
+
|
69 |
+
test('returns the input messages with instructions properly added when addInstructions() is called with non-empty instructions', () => {
|
70 |
+
const messages = [{ content: 'Hello' }, { content: 'How are you?' }, { content: 'Goodbye' }];
|
71 |
+
const instructions = { content: 'Please respond to the question.' };
|
72 |
+
const result = TestClient.addInstructions(messages, instructions);
|
73 |
+
const expected = [
|
74 |
+
{ content: 'Hello' },
|
75 |
+
{ content: 'How are you?' },
|
76 |
+
{ content: 'Please respond to the question.' },
|
77 |
+
{ content: 'Goodbye' },
|
78 |
+
];
|
79 |
+
expect(result).toEqual(expected);
|
80 |
+
});
|
81 |
+
|
82 |
+
test('concats messages correctly in concatenateMessages()', () => {
|
83 |
+
const messages = [
|
84 |
+
{ name: 'User', content: 'Hello' },
|
85 |
+
{ name: 'Assistant', content: 'How can I help you?' },
|
86 |
+
{ name: 'User', content: 'I have a question.' },
|
87 |
+
];
|
88 |
+
const result = TestClient.concatenateMessages(messages);
|
89 |
+
const expected =
|
90 |
+
'User:\nHello\n\nAssistant:\nHow can I help you?\n\nUser:\nI have a question.\n\n';
|
91 |
+
expect(result).toBe(expected);
|
92 |
+
});
|
93 |
+
|
94 |
+
test('refines messages correctly in refineMessages()', async () => {
|
95 |
+
const messagesToRefine = [
|
96 |
+
{ role: 'user', content: 'Hello', tokenCount: 10 },
|
97 |
+
{ role: 'assistant', content: 'How can I help you?', tokenCount: 20 },
|
98 |
+
];
|
99 |
+
const remainingContextTokens = 100;
|
100 |
+
const expectedRefinedMessage = {
|
101 |
+
role: 'assistant',
|
102 |
+
content: 'Refined answer',
|
103 |
+
tokenCount: 14, // 'Refined answer'.length
|
104 |
+
};
|
105 |
+
|
106 |
+
const result = await TestClient.refineMessages(messagesToRefine, remainingContextTokens);
|
107 |
+
expect(result).toEqual(expectedRefinedMessage);
|
108 |
+
});
|
109 |
+
|
110 |
+
test('gets messages within token limit (under limit) correctly in getMessagesWithinTokenLimit()', async () => {
|
111 |
+
TestClient.maxContextTokens = 100;
|
112 |
+
TestClient.shouldRefineContext = true;
|
113 |
+
TestClient.refineMessages = jest.fn().mockResolvedValue({
|
114 |
+
role: 'assistant',
|
115 |
+
content: 'Refined answer',
|
116 |
+
tokenCount: 30,
|
117 |
+
});
|
118 |
+
|
119 |
+
const messages = [
|
120 |
+
{ role: 'user', content: 'Hello', tokenCount: 5 },
|
121 |
+
{ role: 'assistant', content: 'How can I help you?', tokenCount: 19 },
|
122 |
+
{ role: 'user', content: 'I have a question.', tokenCount: 18 },
|
123 |
+
];
|
124 |
+
const expectedContext = [
|
125 |
+
{ role: 'user', content: 'Hello', tokenCount: 5 }, // 'Hello'.length
|
126 |
+
{ role: 'assistant', content: 'How can I help you?', tokenCount: 19 },
|
127 |
+
{ role: 'user', content: 'I have a question.', tokenCount: 18 },
|
128 |
+
];
|
129 |
+
const expectedRemainingContextTokens = 58; // 100 - 5 - 19 - 18
|
130 |
+
const expectedMessagesToRefine = [];
|
131 |
+
|
132 |
+
const result = await TestClient.getMessagesWithinTokenLimit(messages);
|
133 |
+
expect(result.context).toEqual(expectedContext);
|
134 |
+
expect(result.remainingContextTokens).toBe(expectedRemainingContextTokens);
|
135 |
+
expect(result.messagesToRefine).toEqual(expectedMessagesToRefine);
|
136 |
+
});
|
137 |
+
|
138 |
+
test('gets messages within token limit (over limit) correctly in getMessagesWithinTokenLimit()', async () => {
|
139 |
+
TestClient.maxContextTokens = 50; // Set a lower limit
|
140 |
+
TestClient.shouldRefineContext = true;
|
141 |
+
TestClient.refineMessages = jest.fn().mockResolvedValue({
|
142 |
+
role: 'assistant',
|
143 |
+
content: 'Refined answer',
|
144 |
+
tokenCount: 4,
|
145 |
+
});
|
146 |
+
|
147 |
+
const messages = [
|
148 |
+
{ role: 'user', content: 'I need a coffee, stat!', tokenCount: 30 },
|
149 |
+
{ role: 'assistant', content: 'Sure, I can help with that.', tokenCount: 30 },
|
150 |
+
{ role: 'user', content: 'Hello', tokenCount: 5 },
|
151 |
+
{ role: 'assistant', content: 'How can I help you?', tokenCount: 19 },
|
152 |
+
{ role: 'user', content: 'I have a question.', tokenCount: 18 },
|
153 |
+
];
|
154 |
+
const expectedContext = [
|
155 |
+
{ role: 'user', content: 'Hello', tokenCount: 5 },
|
156 |
+
{ role: 'assistant', content: 'How can I help you?', tokenCount: 19 },
|
157 |
+
{ role: 'user', content: 'I have a question.', tokenCount: 18 },
|
158 |
+
];
|
159 |
+
const expectedRemainingContextTokens = 8; // 50 - 18 - 19 - 5
|
160 |
+
const expectedMessagesToRefine = [
|
161 |
+
{ role: 'user', content: 'I need a coffee, stat!', tokenCount: 30 },
|
162 |
+
{ role: 'assistant', content: 'Sure, I can help with that.', tokenCount: 30 },
|
163 |
+
];
|
164 |
+
|
165 |
+
const result = await TestClient.getMessagesWithinTokenLimit(messages);
|
166 |
+
expect(result.context).toEqual(expectedContext);
|
167 |
+
expect(result.remainingContextTokens).toBe(expectedRemainingContextTokens);
|
168 |
+
expect(result.messagesToRefine).toEqual(expectedMessagesToRefine);
|
169 |
+
});
|
170 |
+
|
171 |
+
test('handles context strategy correctly in handleContextStrategy()', async () => {
|
172 |
+
TestClient.addInstructions = jest
|
173 |
+
.fn()
|
174 |
+
.mockReturnValue([
|
175 |
+
{ content: 'Hello' },
|
176 |
+
{ content: 'How can I help you?' },
|
177 |
+
{ content: 'Please provide more details.' },
|
178 |
+
{ content: 'I can assist you with that.' },
|
179 |
+
]);
|
180 |
+
TestClient.getMessagesWithinTokenLimit = jest.fn().mockReturnValue({
|
181 |
+
context: [
|
182 |
+
{ content: 'How can I help you?' },
|
183 |
+
{ content: 'Please provide more details.' },
|
184 |
+
{ content: 'I can assist you with that.' },
|
185 |
+
],
|
186 |
+
remainingContextTokens: 80,
|
187 |
+
messagesToRefine: [{ content: 'Hello' }],
|
188 |
+
refineIndex: 3,
|
189 |
+
});
|
190 |
+
TestClient.refineMessages = jest.fn().mockResolvedValue({
|
191 |
+
role: 'assistant',
|
192 |
+
content: 'Refined answer',
|
193 |
+
tokenCount: 30,
|
194 |
+
});
|
195 |
+
TestClient.getTokenCountForResponse = jest.fn().mockReturnValue(40);
|
196 |
+
|
197 |
+
const instructions = { content: 'Please provide more details.' };
|
198 |
+
const orderedMessages = [
|
199 |
+
{ content: 'Hello' },
|
200 |
+
{ content: 'How can I help you?' },
|
201 |
+
{ content: 'Please provide more details.' },
|
202 |
+
{ content: 'I can assist you with that.' },
|
203 |
+
];
|
204 |
+
const formattedMessages = [
|
205 |
+
{ content: 'Hello' },
|
206 |
+
{ content: 'How can I help you?' },
|
207 |
+
{ content: 'Please provide more details.' },
|
208 |
+
{ content: 'I can assist you with that.' },
|
209 |
+
];
|
210 |
+
const expectedResult = {
|
211 |
+
payload: [
|
212 |
+
{
|
213 |
+
content: 'Refined answer',
|
214 |
+
role: 'assistant',
|
215 |
+
tokenCount: 30,
|
216 |
+
},
|
217 |
+
{ content: 'How can I help you?' },
|
218 |
+
{ content: 'Please provide more details.' },
|
219 |
+
{ content: 'I can assist you with that.' },
|
220 |
+
],
|
221 |
+
promptTokens: expect.any(Number),
|
222 |
+
tokenCountMap: {},
|
223 |
+
messages: expect.any(Array),
|
224 |
+
};
|
225 |
+
|
226 |
+
const result = await TestClient.handleContextStrategy({
|
227 |
+
instructions,
|
228 |
+
orderedMessages,
|
229 |
+
formattedMessages,
|
230 |
+
});
|
231 |
+
expect(result).toEqual(expectedResult);
|
232 |
+
});
|
233 |
+
|
234 |
+
describe('sendMessage', () => {
|
235 |
+
test('sendMessage should return a response message', async () => {
|
236 |
+
const expectedResult = expect.objectContaining({
|
237 |
+
sender: TestClient.sender,
|
238 |
+
text: expect.any(String),
|
239 |
+
isCreatedByUser: false,
|
240 |
+
messageId: expect.any(String),
|
241 |
+
parentMessageId: expect.any(String),
|
242 |
+
conversationId: expect.any(String),
|
243 |
+
});
|
244 |
+
|
245 |
+
const response = await TestClient.sendMessage(userMessage);
|
246 |
+
parentMessageId = response.messageId;
|
247 |
+
conversationId = response.conversationId;
|
248 |
+
expect(response).toEqual(expectedResult);
|
249 |
+
});
|
250 |
+
|
251 |
+
test('sendMessage should work with provided conversationId and parentMessageId', async () => {
|
252 |
+
const userMessage = 'Second message in the conversation';
|
253 |
+
const opts = {
|
254 |
+
conversationId,
|
255 |
+
parentMessageId,
|
256 |
+
getIds: jest.fn(),
|
257 |
+
onStart: jest.fn(),
|
258 |
+
};
|
259 |
+
|
260 |
+
const expectedResult = expect.objectContaining({
|
261 |
+
sender: TestClient.sender,
|
262 |
+
text: expect.any(String),
|
263 |
+
isCreatedByUser: false,
|
264 |
+
messageId: expect.any(String),
|
265 |
+
parentMessageId: expect.any(String),
|
266 |
+
conversationId: opts.conversationId,
|
267 |
+
});
|
268 |
+
|
269 |
+
const response = await TestClient.sendMessage(userMessage, opts);
|
270 |
+
parentMessageId = response.messageId;
|
271 |
+
expect(response.conversationId).toEqual(conversationId);
|
272 |
+
expect(response).toEqual(expectedResult);
|
273 |
+
expect(opts.getIds).toHaveBeenCalled();
|
274 |
+
expect(opts.onStart).toHaveBeenCalled();
|
275 |
+
expect(TestClient.getBuildMessagesOptions).toHaveBeenCalled();
|
276 |
+
expect(TestClient.getSaveOptions).toHaveBeenCalled();
|
277 |
+
});
|
278 |
+
|
279 |
+
test('should return chat history', async () => {
|
280 |
+
const chatMessages = await TestClient.loadHistory(conversationId, parentMessageId);
|
281 |
+
expect(TestClient.currentMessages).toHaveLength(4);
|
282 |
+
expect(chatMessages[0].text).toEqual(userMessage);
|
283 |
+
});
|
284 |
+
|
285 |
+
test('setOptions is called with the correct arguments', async () => {
|
286 |
+
TestClient.setOptions = jest.fn();
|
287 |
+
const opts = { conversationId: '123', parentMessageId: '456' };
|
288 |
+
await TestClient.sendMessage('Hello, world!', opts);
|
289 |
+
expect(TestClient.setOptions).toHaveBeenCalledWith(opts);
|
290 |
+
TestClient.setOptions.mockClear();
|
291 |
+
});
|
292 |
+
|
293 |
+
test('loadHistory is called with the correct arguments', async () => {
|
294 |
+
const opts = { conversationId: '123', parentMessageId: '456' };
|
295 |
+
await TestClient.sendMessage('Hello, world!', opts);
|
296 |
+
expect(TestClient.loadHistory).toHaveBeenCalledWith(
|
297 |
+
opts.conversationId,
|
298 |
+
opts.parentMessageId,
|
299 |
+
);
|
300 |
+
});
|
301 |
+
|
302 |
+
test('getIds is called with the correct arguments', async () => {
|
303 |
+
const getIds = jest.fn();
|
304 |
+
const opts = { getIds };
|
305 |
+
const response = await TestClient.sendMessage('Hello, world!', opts);
|
306 |
+
expect(getIds).toHaveBeenCalledWith({
|
307 |
+
userMessage: expect.objectContaining({ text: 'Hello, world!' }),
|
308 |
+
conversationId: response.conversationId,
|
309 |
+
responseMessageId: response.messageId,
|
310 |
+
});
|
311 |
+
});
|
312 |
+
|
313 |
+
test('onStart is called with the correct arguments', async () => {
|
314 |
+
const onStart = jest.fn();
|
315 |
+
const opts = { onStart };
|
316 |
+
await TestClient.sendMessage('Hello, world!', opts);
|
317 |
+
expect(onStart).toHaveBeenCalledWith(expect.objectContaining({ text: 'Hello, world!' }));
|
318 |
+
});
|
319 |
+
|
320 |
+
test('saveMessageToDatabase is called with the correct arguments', async () => {
|
321 |
+
const saveOptions = TestClient.getSaveOptions();
|
322 |
+
const user = {}; // Mock user
|
323 |
+
const opts = { user };
|
324 |
+
await TestClient.sendMessage('Hello, world!', opts);
|
325 |
+
expect(TestClient.saveMessageToDatabase).toHaveBeenCalledWith(
|
326 |
+
expect.objectContaining({
|
327 |
+
sender: expect.any(String),
|
328 |
+
text: expect.any(String),
|
329 |
+
isCreatedByUser: expect.any(Boolean),
|
330 |
+
messageId: expect.any(String),
|
331 |
+
parentMessageId: expect.any(String),
|
332 |
+
conversationId: expect.any(String),
|
333 |
+
}),
|
334 |
+
saveOptions,
|
335 |
+
user,
|
336 |
+
);
|
337 |
+
});
|
338 |
+
|
339 |
+
test('sendCompletion is called with the correct arguments', async () => {
|
340 |
+
const payload = {}; // Mock payload
|
341 |
+
TestClient.buildMessages.mockReturnValue({ prompt: payload, tokenCountMap: null });
|
342 |
+
const opts = {};
|
343 |
+
await TestClient.sendMessage('Hello, world!', opts);
|
344 |
+
expect(TestClient.sendCompletion).toHaveBeenCalledWith(payload, opts);
|
345 |
+
});
|
346 |
+
|
347 |
+
test('getTokenCountForResponse is called with the correct arguments', async () => {
|
348 |
+
const tokenCountMap = {}; // Mock tokenCountMap
|
349 |
+
TestClient.buildMessages.mockReturnValue({ prompt: [], tokenCountMap });
|
350 |
+
TestClient.getTokenCountForResponse = jest.fn();
|
351 |
+
const response = await TestClient.sendMessage('Hello, world!', {});
|
352 |
+
expect(TestClient.getTokenCountForResponse).toHaveBeenCalledWith(response);
|
353 |
+
});
|
354 |
+
|
355 |
+
test('returns an object with the correct shape', async () => {
|
356 |
+
const response = await TestClient.sendMessage('Hello, world!', {});
|
357 |
+
expect(response).toEqual(
|
358 |
+
expect.objectContaining({
|
359 |
+
sender: expect.any(String),
|
360 |
+
text: expect.any(String),
|
361 |
+
isCreatedByUser: expect.any(Boolean),
|
362 |
+
messageId: expect.any(String),
|
363 |
+
parentMessageId: expect.any(String),
|
364 |
+
conversationId: expect.any(String),
|
365 |
+
}),
|
366 |
+
);
|
367 |
+
});
|
368 |
+
});
|
369 |
+
});
|
api/app/clients/specs/FakeClient.js
ADDED
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const crypto = require('crypto');
|
2 |
+
const BaseClient = require('../BaseClient');
|
3 |
+
const { maxTokensMap } = require('../../../utils');
|
4 |
+
|
5 |
+
class FakeClient extends BaseClient {
|
6 |
+
constructor(apiKey, options = {}) {
|
7 |
+
super(apiKey, options);
|
8 |
+
this.sender = 'AI Assistant';
|
9 |
+
this.setOptions(options);
|
10 |
+
}
|
11 |
+
setOptions(options) {
|
12 |
+
if (this.options && !this.options.replaceOptions) {
|
13 |
+
this.options.modelOptions = {
|
14 |
+
...this.options.modelOptions,
|
15 |
+
...options.modelOptions,
|
16 |
+
};
|
17 |
+
delete options.modelOptions;
|
18 |
+
this.options = {
|
19 |
+
...this.options,
|
20 |
+
...options,
|
21 |
+
};
|
22 |
+
} else {
|
23 |
+
this.options = options;
|
24 |
+
}
|
25 |
+
|
26 |
+
if (this.options.openaiApiKey) {
|
27 |
+
this.apiKey = this.options.openaiApiKey;
|
28 |
+
}
|
29 |
+
|
30 |
+
const modelOptions = this.options.modelOptions || {};
|
31 |
+
if (!this.modelOptions) {
|
32 |
+
this.modelOptions = {
|
33 |
+
...modelOptions,
|
34 |
+
model: modelOptions.model || 'gpt-3.5-turbo',
|
35 |
+
temperature:
|
36 |
+
typeof modelOptions.temperature === 'undefined' ? 0.8 : modelOptions.temperature,
|
37 |
+
top_p: typeof modelOptions.top_p === 'undefined' ? 1 : modelOptions.top_p,
|
38 |
+
presence_penalty:
|
39 |
+
typeof modelOptions.presence_penalty === 'undefined' ? 1 : modelOptions.presence_penalty,
|
40 |
+
stop: modelOptions.stop,
|
41 |
+
};
|
42 |
+
}
|
43 |
+
|
44 |
+
this.maxContextTokens = maxTokensMap[this.modelOptions.model] ?? 4097;
|
45 |
+
}
|
46 |
+
getCompletion() {}
|
47 |
+
buildMessages() {}
|
48 |
+
getTokenCount(str) {
|
49 |
+
return str.length;
|
50 |
+
}
|
51 |
+
getTokenCountForMessage(message) {
|
52 |
+
return message?.content?.length || message.length;
|
53 |
+
}
|
54 |
+
}
|
55 |
+
|
56 |
+
const initializeFakeClient = (apiKey, options, fakeMessages) => {
|
57 |
+
let TestClient = new FakeClient(apiKey);
|
58 |
+
TestClient.options = options;
|
59 |
+
TestClient.abortController = { abort: jest.fn() };
|
60 |
+
TestClient.saveMessageToDatabase = jest.fn();
|
61 |
+
TestClient.loadHistory = jest
|
62 |
+
.fn()
|
63 |
+
.mockImplementation((conversationId, parentMessageId = null) => {
|
64 |
+
if (!conversationId) {
|
65 |
+
TestClient.currentMessages = [];
|
66 |
+
return Promise.resolve([]);
|
67 |
+
}
|
68 |
+
|
69 |
+
const orderedMessages = TestClient.constructor.getMessagesForConversation(
|
70 |
+
fakeMessages,
|
71 |
+
parentMessageId,
|
72 |
+
);
|
73 |
+
|
74 |
+
TestClient.currentMessages = orderedMessages;
|
75 |
+
return Promise.resolve(orderedMessages);
|
76 |
+
});
|
77 |
+
|
78 |
+
TestClient.getSaveOptions = jest.fn().mockImplementation(() => {
|
79 |
+
return {};
|
80 |
+
});
|
81 |
+
|
82 |
+
TestClient.getBuildMessagesOptions = jest.fn().mockImplementation(() => {
|
83 |
+
return {};
|
84 |
+
});
|
85 |
+
|
86 |
+
TestClient.sendCompletion = jest.fn(async () => {
|
87 |
+
return 'Mock response text';
|
88 |
+
});
|
89 |
+
|
90 |
+
TestClient.sendMessage = jest.fn().mockImplementation(async (message, opts = {}) => {
|
91 |
+
if (opts && typeof opts === 'object') {
|
92 |
+
TestClient.setOptions(opts);
|
93 |
+
}
|
94 |
+
|
95 |
+
const user = opts.user || null;
|
96 |
+
const conversationId = opts.conversationId || crypto.randomUUID();
|
97 |
+
const parentMessageId = opts.parentMessageId || '00000000-0000-0000-0000-000000000000';
|
98 |
+
const userMessageId = opts.overrideParentMessageId || crypto.randomUUID();
|
99 |
+
const saveOptions = TestClient.getSaveOptions();
|
100 |
+
|
101 |
+
this.pastMessages = await TestClient.loadHistory(
|
102 |
+
conversationId,
|
103 |
+
TestClient.options?.parentMessageId,
|
104 |
+
);
|
105 |
+
|
106 |
+
const userMessage = {
|
107 |
+
text: message,
|
108 |
+
sender: TestClient.sender,
|
109 |
+
isCreatedByUser: true,
|
110 |
+
messageId: userMessageId,
|
111 |
+
parentMessageId,
|
112 |
+
conversationId,
|
113 |
+
};
|
114 |
+
|
115 |
+
const response = {
|
116 |
+
sender: TestClient.sender,
|
117 |
+
text: 'Hello, User!',
|
118 |
+
isCreatedByUser: false,
|
119 |
+
messageId: crypto.randomUUID(),
|
120 |
+
parentMessageId: userMessage.messageId,
|
121 |
+
conversationId,
|
122 |
+
};
|
123 |
+
|
124 |
+
fakeMessages.push(userMessage);
|
125 |
+
fakeMessages.push(response);
|
126 |
+
|
127 |
+
if (typeof opts.getIds === 'function') {
|
128 |
+
opts.getIds({
|
129 |
+
userMessage,
|
130 |
+
conversationId,
|
131 |
+
responseMessageId: response.messageId,
|
132 |
+
});
|
133 |
+
}
|
134 |
+
|
135 |
+
if (typeof opts.onStart === 'function') {
|
136 |
+
opts.onStart(userMessage);
|
137 |
+
}
|
138 |
+
|
139 |
+
let { prompt: payload, tokenCountMap } = await TestClient.buildMessages(
|
140 |
+
this.currentMessages,
|
141 |
+
userMessage.messageId,
|
142 |
+
TestClient.getBuildMessagesOptions(opts),
|
143 |
+
);
|
144 |
+
|
145 |
+
if (tokenCountMap) {
|
146 |
+
payload = payload.map((message, i) => {
|
147 |
+
const { tokenCount, ...messageWithoutTokenCount } = message;
|
148 |
+
// userMessage is always the last one in the payload
|
149 |
+
if (i === payload.length - 1) {
|
150 |
+
userMessage.tokenCount = message.tokenCount;
|
151 |
+
console.debug(
|
152 |
+
`Token count for user message: ${tokenCount}`,
|
153 |
+
`Instruction Tokens: ${tokenCountMap.instructions || 'N/A'}`,
|
154 |
+
);
|
155 |
+
}
|
156 |
+
return messageWithoutTokenCount;
|
157 |
+
});
|
158 |
+
TestClient.handleTokenCountMap(tokenCountMap);
|
159 |
+
}
|
160 |
+
|
161 |
+
await TestClient.saveMessageToDatabase(userMessage, saveOptions, user);
|
162 |
+
response.text = await TestClient.sendCompletion(payload, opts);
|
163 |
+
if (tokenCountMap && TestClient.getTokenCountForResponse) {
|
164 |
+
response.tokenCount = TestClient.getTokenCountForResponse(response);
|
165 |
+
}
|
166 |
+
await TestClient.saveMessageToDatabase(response, saveOptions, user);
|
167 |
+
return response;
|
168 |
+
});
|
169 |
+
|
170 |
+
TestClient.buildMessages = jest.fn(async (messages, parentMessageId) => {
|
171 |
+
const orderedMessages = TestClient.constructor.getMessagesForConversation(
|
172 |
+
messages,
|
173 |
+
parentMessageId,
|
174 |
+
);
|
175 |
+
const formattedMessages = orderedMessages.map((message) => {
|
176 |
+
let { role: _role, sender, text } = message;
|
177 |
+
const role = _role ?? sender;
|
178 |
+
const content = text ?? '';
|
179 |
+
return {
|
180 |
+
role: role?.toLowerCase() === 'user' ? 'user' : 'assistant',
|
181 |
+
content,
|
182 |
+
};
|
183 |
+
});
|
184 |
+
return {
|
185 |
+
prompt: formattedMessages,
|
186 |
+
tokenCountMap: null, // Simplified for the mock
|
187 |
+
};
|
188 |
+
});
|
189 |
+
|
190 |
+
return TestClient;
|
191 |
+
};
|
192 |
+
|
193 |
+
module.exports = { FakeClient, initializeFakeClient };
|
api/app/clients/specs/OpenAIClient.test.js
ADDED
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const OpenAIClient = require('../OpenAIClient');
|
2 |
+
|
3 |
+
describe('OpenAIClient', () => {
|
4 |
+
let client, client2;
|
5 |
+
const model = 'gpt-4';
|
6 |
+
const parentMessageId = '1';
|
7 |
+
const messages = [
|
8 |
+
{ role: 'user', sender: 'User', text: 'Hello', messageId: parentMessageId },
|
9 |
+
{ role: 'assistant', sender: 'Assistant', text: 'Hi', messageId: '2' },
|
10 |
+
];
|
11 |
+
|
12 |
+
beforeEach(() => {
|
13 |
+
const options = {
|
14 |
+
// debug: true,
|
15 |
+
openaiApiKey: 'new-api-key',
|
16 |
+
modelOptions: {
|
17 |
+
model,
|
18 |
+
temperature: 0.7,
|
19 |
+
},
|
20 |
+
};
|
21 |
+
client = new OpenAIClient('test-api-key', options);
|
22 |
+
client2 = new OpenAIClient('test-api-key', options);
|
23 |
+
client.refineMessages = jest.fn().mockResolvedValue({
|
24 |
+
role: 'assistant',
|
25 |
+
content: 'Refined answer',
|
26 |
+
tokenCount: 30,
|
27 |
+
});
|
28 |
+
client.constructor.freeAndResetAllEncoders();
|
29 |
+
});
|
30 |
+
|
31 |
+
describe('setOptions', () => {
|
32 |
+
it('should set the options correctly', () => {
|
33 |
+
expect(client.apiKey).toBe('new-api-key');
|
34 |
+
expect(client.modelOptions.model).toBe(model);
|
35 |
+
expect(client.modelOptions.temperature).toBe(0.7);
|
36 |
+
});
|
37 |
+
});
|
38 |
+
|
39 |
+
describe('selectTokenizer', () => {
|
40 |
+
it('should get the correct tokenizer based on the instance state', () => {
|
41 |
+
const tokenizer = client.selectTokenizer();
|
42 |
+
expect(tokenizer).toBeDefined();
|
43 |
+
});
|
44 |
+
});
|
45 |
+
|
46 |
+
describe('freeAllTokenizers', () => {
|
47 |
+
it('should free all tokenizers', () => {
|
48 |
+
// Create a tokenizer
|
49 |
+
const tokenizer = client.selectTokenizer();
|
50 |
+
|
51 |
+
// Mock 'free' method on the tokenizer
|
52 |
+
tokenizer.free = jest.fn();
|
53 |
+
|
54 |
+
client.constructor.freeAndResetAllEncoders();
|
55 |
+
|
56 |
+
// Check if 'free' method has been called on the tokenizer
|
57 |
+
expect(tokenizer.free).toHaveBeenCalled();
|
58 |
+
});
|
59 |
+
});
|
60 |
+
|
61 |
+
describe('getTokenCount', () => {
|
62 |
+
it('should return the correct token count', () => {
|
63 |
+
const count = client.getTokenCount('Hello, world!');
|
64 |
+
expect(count).toBeGreaterThan(0);
|
65 |
+
});
|
66 |
+
|
67 |
+
it('should reset the encoder and count when count reaches 25', () => {
|
68 |
+
const freeAndResetEncoderSpy = jest.spyOn(client.constructor, 'freeAndResetAllEncoders');
|
69 |
+
|
70 |
+
// Call getTokenCount 25 times
|
71 |
+
for (let i = 0; i < 25; i++) {
|
72 |
+
client.getTokenCount('test text');
|
73 |
+
}
|
74 |
+
|
75 |
+
expect(freeAndResetEncoderSpy).toHaveBeenCalled();
|
76 |
+
});
|
77 |
+
|
78 |
+
it('should not reset the encoder and count when count is less than 25', () => {
|
79 |
+
const freeAndResetEncoderSpy = jest.spyOn(client.constructor, 'freeAndResetAllEncoders');
|
80 |
+
freeAndResetEncoderSpy.mockClear();
|
81 |
+
|
82 |
+
// Call getTokenCount 24 times
|
83 |
+
for (let i = 0; i < 24; i++) {
|
84 |
+
client.getTokenCount('test text');
|
85 |
+
}
|
86 |
+
|
87 |
+
expect(freeAndResetEncoderSpy).not.toHaveBeenCalled();
|
88 |
+
});
|
89 |
+
|
90 |
+
it('should handle errors and reset the encoder', () => {
|
91 |
+
const freeAndResetEncoderSpy = jest.spyOn(client.constructor, 'freeAndResetAllEncoders');
|
92 |
+
|
93 |
+
// Mock encode function to throw an error
|
94 |
+
client.selectTokenizer().encode = jest.fn().mockImplementation(() => {
|
95 |
+
throw new Error('Test error');
|
96 |
+
});
|
97 |
+
|
98 |
+
client.getTokenCount('test text');
|
99 |
+
|
100 |
+
expect(freeAndResetEncoderSpy).toHaveBeenCalled();
|
101 |
+
});
|
102 |
+
|
103 |
+
it('should not throw null pointer error when freeing the same encoder twice', () => {
|
104 |
+
client.constructor.freeAndResetAllEncoders();
|
105 |
+
client2.constructor.freeAndResetAllEncoders();
|
106 |
+
|
107 |
+
const count = client2.getTokenCount('test text');
|
108 |
+
expect(count).toBeGreaterThan(0);
|
109 |
+
});
|
110 |
+
});
|
111 |
+
|
112 |
+
describe('getSaveOptions', () => {
|
113 |
+
it('should return the correct save options', () => {
|
114 |
+
const options = client.getSaveOptions();
|
115 |
+
expect(options).toHaveProperty('chatGptLabel');
|
116 |
+
expect(options).toHaveProperty('promptPrefix');
|
117 |
+
});
|
118 |
+
});
|
119 |
+
|
120 |
+
describe('getBuildMessagesOptions', () => {
|
121 |
+
it('should return the correct build messages options', () => {
|
122 |
+
const options = client.getBuildMessagesOptions({ promptPrefix: 'Hello' });
|
123 |
+
expect(options).toHaveProperty('isChatCompletion');
|
124 |
+
expect(options).toHaveProperty('promptPrefix');
|
125 |
+
expect(options.promptPrefix).toBe('Hello');
|
126 |
+
});
|
127 |
+
});
|
128 |
+
|
129 |
+
describe('buildMessages', () => {
|
130 |
+
it('should build messages correctly for chat completion', async () => {
|
131 |
+
const result = await client.buildMessages(messages, parentMessageId, {
|
132 |
+
isChatCompletion: true,
|
133 |
+
});
|
134 |
+
expect(result).toHaveProperty('prompt');
|
135 |
+
});
|
136 |
+
|
137 |
+
it('should build messages correctly for non-chat completion', async () => {
|
138 |
+
const result = await client.buildMessages(messages, parentMessageId, {
|
139 |
+
isChatCompletion: false,
|
140 |
+
});
|
141 |
+
expect(result).toHaveProperty('prompt');
|
142 |
+
});
|
143 |
+
|
144 |
+
it('should build messages correctly with a promptPrefix', async () => {
|
145 |
+
const result = await client.buildMessages(messages, parentMessageId, {
|
146 |
+
isChatCompletion: true,
|
147 |
+
promptPrefix: 'Test Prefix',
|
148 |
+
});
|
149 |
+
expect(result).toHaveProperty('prompt');
|
150 |
+
const instructions = result.prompt.find((item) => item.name === 'instructions');
|
151 |
+
expect(instructions).toBeDefined();
|
152 |
+
expect(instructions.content).toContain('Test Prefix');
|
153 |
+
});
|
154 |
+
|
155 |
+
it('should handle context strategy correctly', async () => {
|
156 |
+
client.contextStrategy = 'refine';
|
157 |
+
const result = await client.buildMessages(messages, parentMessageId, {
|
158 |
+
isChatCompletion: true,
|
159 |
+
});
|
160 |
+
expect(result).toHaveProperty('prompt');
|
161 |
+
expect(result).toHaveProperty('tokenCountMap');
|
162 |
+
});
|
163 |
+
|
164 |
+
it('should assign name property for user messages when options.name is set', async () => {
|
165 |
+
client.options.name = 'Test User';
|
166 |
+
const result = await client.buildMessages(messages, parentMessageId, {
|
167 |
+
isChatCompletion: true,
|
168 |
+
});
|
169 |
+
const hasUserWithName = result.prompt.some(
|
170 |
+
(item) => item.role === 'user' && item.name === 'Test User',
|
171 |
+
);
|
172 |
+
expect(hasUserWithName).toBe(true);
|
173 |
+
});
|
174 |
+
|
175 |
+
it('should calculate tokenCount for each message when contextStrategy is set', async () => {
|
176 |
+
client.contextStrategy = 'refine';
|
177 |
+
const result = await client.buildMessages(messages, parentMessageId, {
|
178 |
+
isChatCompletion: true,
|
179 |
+
});
|
180 |
+
const hasUserWithTokenCount = result.prompt.some(
|
181 |
+
(item) => item.role === 'user' && item.tokenCount > 0,
|
182 |
+
);
|
183 |
+
expect(hasUserWithTokenCount).toBe(true);
|
184 |
+
});
|
185 |
+
|
186 |
+
it('should handle promptPrefix from options when promptPrefix argument is not provided', async () => {
|
187 |
+
client.options.promptPrefix = 'Test Prefix from options';
|
188 |
+
const result = await client.buildMessages(messages, parentMessageId, {
|
189 |
+
isChatCompletion: true,
|
190 |
+
});
|
191 |
+
const instructions = result.prompt.find((item) => item.name === 'instructions');
|
192 |
+
expect(instructions.content).toContain('Test Prefix from options');
|
193 |
+
});
|
194 |
+
|
195 |
+
it('should handle case when neither promptPrefix argument nor options.promptPrefix is set', async () => {
|
196 |
+
const result = await client.buildMessages(messages, parentMessageId, {
|
197 |
+
isChatCompletion: true,
|
198 |
+
});
|
199 |
+
const instructions = result.prompt.find((item) => item.name === 'instructions');
|
200 |
+
expect(instructions).toBeUndefined();
|
201 |
+
});
|
202 |
+
|
203 |
+
it('should handle case when getMessagesForConversation returns null or an empty array', async () => {
|
204 |
+
const messages = [];
|
205 |
+
const result = await client.buildMessages(messages, parentMessageId, {
|
206 |
+
isChatCompletion: true,
|
207 |
+
});
|
208 |
+
expect(result.prompt).toEqual([]);
|
209 |
+
});
|
210 |
+
});
|
211 |
+
});
|