Marco Beretta commited on
Commit
3b6afc0
1 Parent(s): cbb93e4

LibreChat upload repo

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .devcontainer/devcontainer.json +57 -0
  2. .devcontainer/docker-compose.yml +76 -0
  3. .dockerignore +5 -0
  4. .env.example +263 -0
  5. .eslintrc.js +136 -0
  6. .github/FUNDING.yml +13 -0
  7. .github/ISSUE_TEMPLATE/BUG-REPORT.yml +64 -0
  8. .github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml +57 -0
  9. .github/ISSUE_TEMPLATE/QUESTION.yml +58 -0
  10. .github/dependabot.yml +47 -0
  11. .github/playwright.yml +62 -0
  12. .github/pull_request_template.md +35 -0
  13. .github/wip-playwright.yml +28 -0
  14. .github/workflows/backend-review.yml +44 -0
  15. .github/workflows/build.yml +38 -0
  16. .github/workflows/container.yml +47 -0
  17. .github/workflows/deploy.yml +38 -0
  18. .github/workflows/frontend-review.yml +34 -0
  19. .github/workflows/mkdocs.yaml +24 -0
  20. .gitignore +78 -0
  21. .husky/pre-commit +5 -0
  22. .prettierrc.js +19 -0
  23. CODE_OF_CONDUCT.md +132 -0
  24. CONTRIBUTING.md +100 -0
  25. Dockerfile +26 -0
  26. LICENSE.md +29 -0
  27. README.md +148 -8
  28. SECURITY.md +63 -0
  29. api/app/bingai.js +100 -0
  30. api/app/chatgpt-browser.js +50 -0
  31. api/app/clients/AnthropicClient.js +324 -0
  32. api/app/clients/BaseClient.js +561 -0
  33. api/app/clients/ChatGPTClient.js +587 -0
  34. api/app/clients/GoogleClient.js +280 -0
  35. api/app/clients/OpenAIClient.js +369 -0
  36. api/app/clients/PluginsClient.js +569 -0
  37. api/app/clients/TextStream.js +59 -0
  38. api/app/clients/agents/CustomAgent/CustomAgent.js +50 -0
  39. api/app/clients/agents/CustomAgent/initializeCustomAgent.js +54 -0
  40. api/app/clients/agents/CustomAgent/instructions.js +203 -0
  41. api/app/clients/agents/CustomAgent/outputParser.js +218 -0
  42. api/app/clients/agents/Functions/FunctionsAgent.js +120 -0
  43. api/app/clients/agents/Functions/initializeFunctionsAgent.js +28 -0
  44. api/app/clients/agents/index.js +7 -0
  45. api/app/clients/index.js +17 -0
  46. api/app/clients/prompts/instructions.js +10 -0
  47. api/app/clients/prompts/refinePrompt.js +24 -0
  48. api/app/clients/specs/BaseClient.test.js +369 -0
  49. api/app/clients/specs/FakeClient.js +193 -0
  50. 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@example.com
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@example.com
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@example.com
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
- title: LibreChat
3
- emoji: 📉
4
- colorFrom: yellow
5
- colorTo: indigo
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
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
+ });