Compare commits
231 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f101419af3 | ||
|
|
251d8ac410 | ||
|
|
bdccadbe06 | ||
|
|
70a56ac04a | ||
|
|
193ed93ee8 | ||
|
|
b096fb98ce | ||
|
|
002bba20f9 | ||
|
|
b896225bd8 | ||
|
|
de34d8b47c | ||
|
|
759e585c29 | ||
|
|
c6f5d5d65c | ||
|
|
cb3cf9b33e | ||
|
|
68ad46a9be | ||
|
|
600a0d15b1 | ||
|
|
92f87b8dcc | ||
|
|
06a7fba39b | ||
|
|
96d29f7390 | ||
|
|
5828200197 | ||
|
|
173b8ce2da | ||
|
|
1f5a79f073 | ||
|
|
495ffaeb06 | ||
|
|
c7b586ba4c | ||
|
|
d6dbd56e33 | ||
|
|
315faf707e | ||
|
|
d79f585052 | ||
|
|
6ee0dbfdbd | ||
|
|
60d0e97425 | ||
|
|
956aa6c674 | ||
|
|
fb99e5a7da | ||
|
|
0630b54193 | ||
|
|
851dce720f | ||
|
|
30a49ae611 | ||
|
|
2faeebfae2 | ||
|
|
41ed33e792 | ||
|
|
da60d77a14 | ||
|
|
b5aadc4b6d | ||
|
|
545342bbcb | ||
|
|
2c00279aaf | ||
|
|
77a76f8511 | ||
|
|
488b373695 | ||
|
|
94764c9c2a | ||
|
|
e9c981c202 | ||
|
|
bec1d245bd | ||
|
|
131cb6cddb | ||
|
|
a2b6e9a6a8 | ||
|
|
428fd5bed8 | ||
|
|
9cacf76c10 | ||
|
|
7b8036a369 | ||
|
|
d56817850c | ||
|
|
f88a0685f7 | ||
|
|
ae51e6153f | ||
|
|
745eef2eb0 | ||
|
|
1f8520cdad | ||
|
|
dae2805d27 | ||
|
|
ba2e95db04 | ||
|
|
2a6e000217 | ||
|
|
32281d1b8d | ||
|
|
369b1f4eba | ||
|
|
777d64088b | ||
|
|
d59a3f20cb | ||
|
|
8959576d75 | ||
|
|
dd8bc39001 | ||
|
|
4898f7489b | ||
|
|
c9c77d6fdf | ||
|
|
4dc86c4c18 | ||
|
|
6ae807c404 | ||
|
|
b5353e2640 | ||
|
|
f4f1199a55 | ||
|
|
b6028a3434 | ||
|
|
abef8c02c1 | ||
|
|
19af2b06ce | ||
|
|
2f7658e39f | ||
|
|
d3138c79fc | ||
|
|
1e49b7ecb1 | ||
|
|
3b865fbc59 | ||
|
|
afd894553c | ||
|
|
df485e5bfe | ||
|
|
77252bafc1 | ||
|
|
bd1d5e991d | ||
|
|
18c4883ae0 | ||
|
|
197307d514 | ||
|
|
130356654c | ||
|
|
8f9f09698b | ||
|
|
5da833e066 | ||
|
|
b64273957a | ||
|
|
4148c6d219 | ||
|
|
e9d68e3bef | ||
|
|
bbe690cc4b | ||
|
|
a1ad471d87 | ||
|
|
c319d709f3 | ||
|
|
6943f1c2c7 | ||
|
|
e38483a8b9 | ||
|
|
2a2e6d9991 | ||
|
|
deb1472aa5 | ||
|
|
8aa58ea240 | ||
|
|
3a112a344d | ||
|
|
712be248be | ||
|
|
530f9d303f | ||
|
|
ad29d25396 | ||
|
|
1ef53a41f0 | ||
|
|
0246f164b0 | ||
|
|
514f625b8f | ||
|
|
39ac8d3858 | ||
|
|
15987abe0a | ||
|
|
dd19323280 | ||
|
|
af47a68632 | ||
|
|
9303ea2f57 | ||
|
|
20dde44512 | ||
|
|
732a0b8029 | ||
|
|
50374f7539 | ||
|
|
1a21eb5bae | ||
|
|
1a5144be76 | ||
|
|
e5336039fc | ||
|
|
637bb6bc11 | ||
|
|
1b999108e4 | ||
|
|
3e5c5a828d | ||
|
|
e3bf674cb7 | ||
|
|
c7e57cd3a2 | ||
|
|
9e931229e2 | ||
|
|
981d009508 | ||
|
|
f5672ddcf8 | ||
|
|
747e087cf5 | ||
|
|
c17c1488ca | ||
|
|
47e5493744 | ||
|
|
13627c7f4f | ||
|
|
9e15747455 | ||
|
|
ce6490109f | ||
|
|
f0e2639269 | ||
|
|
cf3889d8e4 | ||
|
|
6efb5bd88e | ||
|
|
a64342f515 | ||
|
|
9eefa3e24c | ||
|
|
2607f157d3 | ||
|
|
69d192bac3 | ||
|
|
fabd85ff40 | ||
|
|
12e2826d39 | ||
|
|
206fc50bc9 | ||
|
|
b6a2634a0a | ||
|
|
5ad0ef331f | ||
|
|
5b30ab5d43 | ||
|
|
8b91145953 | ||
|
|
b6f21af69b | ||
|
|
684ffd8e66 | ||
|
|
da17649557 | ||
|
|
c343ae16c6 | ||
|
|
77d5fb0c58 | ||
|
|
4e317c85fd | ||
|
|
3c1aeab340 | ||
|
|
75250f3a5f | ||
|
|
04e4259005 | ||
|
|
d0078d478d | ||
|
|
8819e83d2c | ||
|
|
10c772c9f2 | ||
|
|
8e1473c3d8 | ||
|
|
88683b9cc5 | ||
|
|
6b843429c5 | ||
|
|
b89d46dac0 | ||
|
|
e61eb7b5bc | ||
|
|
d2ce2ef2cd | ||
|
|
df2a68e1e7 | ||
|
|
d7270a1676 | ||
|
|
8f6855116d | ||
|
|
83eea4825b | ||
|
|
4a0cf11c90 | ||
|
|
5f3266c1eb | ||
|
|
abd1b10b46 | ||
|
|
25211d6f23 | ||
|
|
fdc5265f48 | ||
|
|
eceba36f54 | ||
|
|
7efb90366f | ||
|
|
731304f96a | ||
|
|
3d40dce76a | ||
|
|
d1d7f61fe1 | ||
|
|
f84da37c9c | ||
|
|
49e2cdf76c | ||
|
|
76e51b8ac5 | ||
|
|
ac537b96f6 | ||
|
|
4f47da8f0d | ||
|
|
f1f33de4db | ||
|
|
9778e73087 | ||
|
|
4353d42035 | ||
|
|
d0be2e6f4a | ||
|
|
5b1efc48d1 | ||
|
|
7053d76f48 | ||
|
|
9f930ecf7d | ||
|
|
dfec4bfe3a | ||
|
|
7541e9b3d3 | ||
|
|
bffa9ad016 | ||
|
|
d339c291fa | ||
|
|
a42ef2944c | ||
|
|
1b3215c55d | ||
|
|
71d812403e | ||
|
|
6e183b91e1 | ||
|
|
198f60c536 | ||
|
|
3caddd6854 | ||
|
|
550e566097 | ||
|
|
3634d8691a | ||
|
|
2da81db440 | ||
|
|
3e98486190 | ||
|
|
ff2c8e6614 | ||
|
|
36a524a630 | ||
|
|
42583e7344 | ||
|
|
bccd0cb3dd | ||
|
|
07fec3b958 | ||
|
|
4dc3c31df8 | ||
|
|
821b507e0e | ||
|
|
9d3e749104 | ||
|
|
2003480fed | ||
|
|
ee52533339 | ||
|
|
72e9828b76 | ||
|
|
f92e4f28be | ||
|
|
60bcd7ae49 | ||
|
|
87fa9f9ab0 | ||
|
|
6c5bea0096 | ||
|
|
c5325cee3a | ||
|
|
e726e42cb5 | ||
|
|
4c340fd0ba | ||
|
|
cefdd1fb88 | ||
|
|
3ff8d6f90b | ||
|
|
9a965907a2 | ||
|
|
297291c541 | ||
|
|
46bd04301d | ||
|
|
8c536cb93c | ||
|
|
931a89204c | ||
|
|
c661276888 | ||
|
|
544d72ee1d | ||
|
|
f171628948 | ||
|
|
11775915d0 | ||
|
|
365fae58ff | ||
|
|
5e3809f22c | ||
|
|
3dadedaf69 |
58
.devcontainer/devcontainer.json
Normal file
58
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,58 @@
|
||||
// {
|
||||
// "name": "LibreChat_dev",
|
||||
// // Update the 'dockerComposeFile' list if you have more compose files or use different names.
|
||||
// "dockerComposeFile": "docker-compose.yml",
|
||||
// // The 'service' property is the name of the service for the container that VS Code should
|
||||
// // use. Update this value and .devcontainer/docker-compose.yml to the real service name.
|
||||
// "service": "librechat",
|
||||
// // The 'workspaceFolder' property is the path VS Code should open by default when
|
||||
// // connected. Corresponds to a volume mount in .devcontainer/docker-compose.yml
|
||||
// "workspaceFolder": "/workspace"
|
||||
// //,
|
||||
// // // Set *default* container specific settings.json values on container create.
|
||||
// // "settings": {},
|
||||
// // // Add the IDs of extensions you want installed when the container is created.
|
||||
// // "extensions": [],
|
||||
// // Uncomment the next line if you want to keep your containers running after VS Code shuts down.
|
||||
// // "shutdownAction": "none",
|
||||
// // Uncomment the next line to use 'postCreateCommand' to run commands after the container is created.
|
||||
// // "postCreateCommand": "uname -a",
|
||||
// // Comment out to connect as root instead. To add a non-root user, see: https://aka.ms/vscode-remote/containers/non-root.
|
||||
// // "remoteUser": "vscode"
|
||||
// }
|
||||
{
|
||||
// "name": "LibreChat_dev",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "app",
|
||||
// "image": "node:19-alpine",
|
||||
// "workspaceFolder": "/workspaces",
|
||||
"workspaceFolder": "/workspace",
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
// "overrideCommand": true,
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [],
|
||||
"settings": {
|
||||
"terminal.integrated.profiles.linux": {
|
||||
"bash": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "",
|
||||
// "workspaceMount": "src=${localWorkspaceFolder},dst=/code,type=bind,consistency=cached"
|
||||
|
||||
// "runArgs": [
|
||||
// "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined",
|
||||
// "-v", "/tmp/.X11-unix:/tmp/.X11-unix",
|
||||
// "-v", "${env:XAUTHORITY}:/root/.Xauthority:rw",
|
||||
// "-v", "/home/${env:USER}/.cdh:/root/.cdh",
|
||||
// "-e", "DISPLAY=${env:DISPLAY}",
|
||||
// "--name=tgw_assistant_backend_dev",
|
||||
// "--network=host"
|
||||
// ],
|
||||
// "settings": {
|
||||
// "terminal.integrated.shell.linux": "/bin/bash"
|
||||
// },
|
||||
"features": {"ghcr.io/devcontainers/features/git:1": {}}
|
||||
}
|
||||
76
.devcontainer/docker-compose.yml
Normal file
76
.devcontainer/docker-compose.yml
Normal file
@@ -0,0 +1,76 @@
|
||||
version: '3.4'
|
||||
|
||||
services:
|
||||
app:
|
||||
# container_name: LibreChat_dev
|
||||
image: node:19-alpine
|
||||
# Using a Dockerfile is optional, but included for completeness.
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile
|
||||
# # [Optional] You can use build args to set options. e.g. 'VARIANT' below affects the image in the Dockerfile
|
||||
# args:
|
||||
# VARIANT: buster
|
||||
network_mode: "host"
|
||||
# ports:
|
||||
# - 3080:3080 # Change it to 9000:3080 to use nginx
|
||||
extra_hosts: # if you are running APIs on docker you need access to, you will need to uncomment this line and next
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
volumes:
|
||||
# # This is where VS Code should expect to find your project's source code and the value of "workspaceFolder" in .devcontainer/devcontainer.json
|
||||
- ..:/workspace:cached
|
||||
# # - /app/client/node_modules
|
||||
# # - ./api:/app/api
|
||||
# # - ./.env:/app/.env
|
||||
# # - ./.env.development:/app/.env.development
|
||||
# # - ./.env.production:/app/.env.production
|
||||
# # - /app/api/node_modules
|
||||
|
||||
# # Uncomment the next line to use Docker from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker-compose for details.
|
||||
# # - /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
# Runs app on the same network as the service container, allows "forwardPorts" in devcontainer.json function.
|
||||
# network_mode: service:another-service
|
||||
|
||||
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
|
||||
# Uncomment the next line to use a non-root user for all processes - See https://aka.ms/vscode-remote/containers/non-root for details.
|
||||
# user: vscode
|
||||
|
||||
# Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust.
|
||||
# cap_add:
|
||||
# - SYS_PTRACE
|
||||
# security_opt:
|
||||
# - seccomp:unconfined
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: /bin/sh -c "while sleep 1000; do :; done"
|
||||
|
||||
mongodb:
|
||||
container_name: chat-mongodb
|
||||
network_mode: "host"
|
||||
# ports:
|
||||
# - 27018:27017
|
||||
image: mongo
|
||||
# restart: always
|
||||
volumes:
|
||||
- ./data-node:/data/db
|
||||
command: mongod --noauth
|
||||
meilisearch:
|
||||
container_name: chat-meilisearch
|
||||
image: getmeili/meilisearch:v1.0
|
||||
network_mode: "host"
|
||||
# ports:
|
||||
# - 7700:7700
|
||||
# env_file:
|
||||
# - .env
|
||||
environment:
|
||||
- SEARCH=false
|
||||
- MEILI_HOST=http://0.0.0.0:7700
|
||||
- MEILI_HTTP_ADDR=0.0.0.0:7700
|
||||
- MEILI_MASTER_KEY=5c71cf56d672d009e36070b5bc5e47b743535ae55c818ae3b735bb6ebfb4ba63
|
||||
volumes:
|
||||
- ./meili_data:/meili_data
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
**/node_modules
|
||||
api/.env
|
||||
client/dist/images
|
||||
data-node
|
||||
.env
|
||||
client/dist/images
|
||||
**/.env
|
||||
109
.env.example
109
.env.example
@@ -2,6 +2,8 @@
|
||||
# Server configuration:
|
||||
##########################
|
||||
|
||||
APP_TITLE=LibreChat
|
||||
|
||||
# The server will listen to localhost:3080 by default. You can change the target IP as you want.
|
||||
# If you want to make this server available externally, for example to share the server with others
|
||||
# or expose this from a Docker container, set host to 0.0.0.0 or your external IP interface.
|
||||
@@ -16,7 +18,7 @@ PORT=3080
|
||||
# PROXY=
|
||||
|
||||
# Change this to your MongoDB URI if different. I recommend appending LibreChat.
|
||||
MONGO_URI=mongodb://127.0.0.1:27017/LibreChat
|
||||
MONGO_URI=mongodb://127.0.0.1:27018/LibreChat
|
||||
|
||||
##########################
|
||||
# OpenAI Endpoint:
|
||||
@@ -25,12 +27,12 @@ MONGO_URI=mongodb://127.0.0.1:27017/LibreChat
|
||||
# Access key from OpenAI platform.
|
||||
# Leave it blank to disable this feature.
|
||||
# Set to "user_provided" to allow the user to provide their API key from the UI.
|
||||
OPENAI_API_KEY=user_provided
|
||||
OPENAI_API_KEY="user_provided"
|
||||
|
||||
# Identify the available models, separated by commas *without spaces*.
|
||||
# The first will be default.
|
||||
# Leave it blank to use internal settings.
|
||||
OPENAI_MODELS=gpt-3.5-turbo,gpt-3.5-turbo-0301,text-davinci-003,gpt-4,gpt-4-0314
|
||||
# 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
|
||||
|
||||
# Reverse proxy settings for OpenAI:
|
||||
# https://github.com/waylaidwanderer/node-chatgpt-api#using-a-reverse-proxy
|
||||
@@ -47,13 +49,24 @@ OPENAI_MODELS=gpt-3.5-turbo,gpt-3.5-turbo-0301,text-davinci-003,gpt-4,gpt-4-0314
|
||||
# Note: I've noticed that the Azure API is much faster than the OpenAI API, so the streaming looks almost instantaneous.
|
||||
# Note "AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME" and "AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME" are optional but might be used in the future
|
||||
|
||||
# AZURE_OPENAI_API_KEY=
|
||||
# AZURE_API_KEY=
|
||||
# AZURE_OPENAI_API_INSTANCE_NAME=
|
||||
# AZURE_OPENAI_API_DEPLOYMENT_NAME=
|
||||
# AZURE_OPENAI_API_VERSION=
|
||||
# AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME=
|
||||
# AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME=
|
||||
|
||||
# Identify the available models, separated by commas *without spaces*.
|
||||
# The first will be default.
|
||||
# Leave it blank to use internal settings.
|
||||
AZURE_OPENAI_MODELS=gpt-3.5-turbo,gpt-4
|
||||
|
||||
# To use Azure with the Plugins endpoint, you need the variables above, and uncomment the following variable:
|
||||
# NOTE: This may not work as expected and Azure OpenAI may not support OpenAI Functions yet
|
||||
# Omit/leave it commented to use the default OpenAI API
|
||||
|
||||
# PLUGINS_USE_AZURE="true"
|
||||
|
||||
##########################
|
||||
# BingAI Endpoint:
|
||||
##########################
|
||||
@@ -94,10 +107,25 @@ CHATGPT_MODELS=text-davinci-002-render-sha,gpt-4
|
||||
# By default it will use the node-chatgpt-api recommended proxy, (it's a third party server)
|
||||
# CHATGPT_REVERSE_PROXY=<YOUR REVERSE PROXY>
|
||||
|
||||
##########################
|
||||
# Anthropic Endpoint:
|
||||
##########################
|
||||
# Access key from https://console.anthropic.com/
|
||||
# Leave it blank to disable this feature.
|
||||
# Set to "user_provided" to allow the user to provide their API key from the UI.
|
||||
# Note that access to claude-1 may potentially become unavailable with the release of claude-2.
|
||||
ANTHROPIC_API_KEY="user_provided"
|
||||
ANTHROPIC_MODELS=claude-1,claude-instant-1,claude-2
|
||||
|
||||
#############################
|
||||
# Plugins:
|
||||
#############################
|
||||
|
||||
# Identify the available models, separated by commas *without spaces*.
|
||||
# The first will be default.
|
||||
# Leave it blank to use internal settings.
|
||||
# PLUGIN_MODELS=gpt-3.5-turbo,gpt-3.5-turbo-16k,gpt-3.5-turbo-0301,gpt-4,gpt-4-0314,gpt-4-0613
|
||||
|
||||
# For securely storing credentials, you need a fixed key and IV. You can set them here for prod and dev environments
|
||||
# If you don't set them, the app will crash on startup.
|
||||
# You need a 32-byte key (64 characters in hex) and 16-byte IV (32 characters in hex)
|
||||
@@ -109,13 +137,15 @@ CREDS_IV=e2341419ec3dd3d19b13a1a87fafcbfb
|
||||
|
||||
# AI-Assisted Google Search
|
||||
# This bot supports searching google for answers to your questions with assistance from GPT!
|
||||
# See detailed instructions here: https://github.com/danny-avila/chatgpt-clone/blob/main/docs/features/plugins/google_search.md
|
||||
# See detailed instructions here: https://github.com/danny-avila/LibreChat/blob/main/docs/features/plugins/google_search.md
|
||||
GOOGLE_API_KEY=
|
||||
GOOGLE_CSE_ID=
|
||||
|
||||
# StableDiffusion WebUI
|
||||
# This bot supports StableDiffusion WebUI, using it's API to generated requested images.
|
||||
SD_WEBUI_URL=http://0.0.0.0:7860
|
||||
# See detailed instructions here: https://github.com/danny-avila/LibreChat/blob/main/docs/features/plugins/stable_diffusion.md
|
||||
# Use "http://127.0.0.1:7860" with local install and "http://host.docker.internal:7860" for docker
|
||||
SD_WEBUI_URL=http://host.docker.internal:7860
|
||||
|
||||
##########################
|
||||
# PaLM (Google) Endpoint:
|
||||
@@ -142,7 +172,10 @@ PROXY=
|
||||
# ENABLING SEARCH MESSAGES/CONVOS
|
||||
# Requires the installation of the free self-hosted Meilisearch or a paid Remote Plan (Remote not tested)
|
||||
# The easiest setup for this is through docker-compose, which takes care of it for you.
|
||||
SEARCH=false
|
||||
SEARCH=true
|
||||
|
||||
# HIGHLY RECOMMENDED: Disable anonymized telemetry analytics for MeiliSearch for absolute privacy.
|
||||
MEILI_NO_ANALYTICS=true
|
||||
|
||||
# REQUIRED FOR SEARCH: MeiliSearch Host, mainly for the API server to connect to the search server.
|
||||
# Replace '0.0.0.0' with 'meilisearch' if serving MeiliSearch with docker-compose.
|
||||
@@ -164,6 +197,12 @@ MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFCt
|
||||
# User System:
|
||||
##########################
|
||||
|
||||
# Allow Public Registration
|
||||
ALLOW_REGISTRATION=true
|
||||
|
||||
# Allow Social Registration
|
||||
ALLOW_SOCIAL_LOGIN=false
|
||||
|
||||
# JWT Secrets
|
||||
JWT_SECRET=secret
|
||||
JWT_REFRESH_SECRET=secret
|
||||
@@ -175,32 +214,60 @@ GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
GOOGLE_CALLBACK_URL=/oauth/google/callback
|
||||
|
||||
# OpenID:
|
||||
# See OpenID provider to get the below values
|
||||
# Create random string for OPENID_SESSION_SECRET
|
||||
# For Azure AD
|
||||
# ISSUER: https://login.microsoftonline.com/(tenant id)/v2.0/
|
||||
# SCOPE: openid profile email
|
||||
OPENID_CLIENT_ID=
|
||||
OPENID_CLIENT_SECRET=
|
||||
OPENID_ISSUER=
|
||||
OPENID_SESSION_SECRET=
|
||||
OPENID_SCOPE="openid profile email"
|
||||
OPENID_CALLBACK_URL=/oauth/openid/callback
|
||||
# If LABEL and URL are left empty, then the default OpenID label and logo are used.
|
||||
OPENID_BUTTON_LABEL=
|
||||
OPENID_IMAGE_URL=
|
||||
|
||||
# Set the expiration delay for the secure cookie with the JWT token
|
||||
# Delay is in millisecond e.g. 7 days is 1000*60*60*24*7
|
||||
SESSION_EXPIRY=(1000 * 60 * 60 * 24) * 7
|
||||
|
||||
# Github:
|
||||
# Get the Client ID and Secret from your Discord Application
|
||||
# Add your Discord Client ID and Client Secret here:
|
||||
|
||||
GITHUB_CLIENT_ID=your_client_id
|
||||
GITHUB_CLIENT_SECRET=your_client_secret
|
||||
GITHUB_CALLBACK_URL=/oauth/github/callback # this should be the same for everyone
|
||||
|
||||
# Discord:
|
||||
# Get the Client ID and Secret from your Discord Application
|
||||
# Add your Github Client ID and Client Secret here:
|
||||
|
||||
DISCORD_CLIENT_ID=your_client_id
|
||||
DISCORD_CLIENT_SECRET=your_client_secret
|
||||
DISCORD_CALLBACK_URL=/oauth/discord/callback # this should be the same for everyone
|
||||
|
||||
###########################
|
||||
# Application Domains
|
||||
###########################
|
||||
|
||||
# Note: server = backend, client = public (the client is the url you visit)
|
||||
# For the google login to work in dev mode, you will likely need to change DOMAIN_SERVER to localhost:3090 or place it in .env.development
|
||||
# Note:
|
||||
# Server = Backend
|
||||
# Client = Public (the client is the url you visit)
|
||||
# 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
|
||||
|
||||
DOMAIN_CLIENT=http://localhost:3080
|
||||
DOMAIN_SERVER=http://localhost:3080
|
||||
|
||||
###########################
|
||||
# Frontend Configuration (Vite):
|
||||
# Email
|
||||
###########################
|
||||
|
||||
# Custom app name, this text will be displayed in the landing page and the footer.
|
||||
VITE_APP_TITLE="LibreChat"
|
||||
|
||||
# Enable Social Login
|
||||
# This enables/disables the Login with Google button on the login page.
|
||||
# Set to true if you have registered the app with google cloud services
|
||||
# and have set the GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in the /api/.env file
|
||||
VITE_SHOW_GOOGLE_LOGIN_OPTION=false
|
||||
|
||||
# Allow Public Registration
|
||||
ALLOW_REGISTRATION=true
|
||||
# Email is used for password reset. Note that all 4 values must be set for email to work.
|
||||
EMAIL_SERVICE= # eg. gmail
|
||||
EMAIL_USERNAME= # eg. your email address if using gmail
|
||||
EMAIL_PASSWORD= # eg. this is the "app password" if using gmail
|
||||
EMAIL_FROM= # eg. email address for from field like noreply@librechat.ai
|
||||
|
||||
100
.eslintrc.js
100
.eslintrc.js
@@ -4,54 +4,64 @@ module.exports = {
|
||||
es2021: true,
|
||||
node: true,
|
||||
commonjs: true,
|
||||
es6: true
|
||||
es6: true,
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
"plugin:jest/recommended",
|
||||
'prettier'
|
||||
'plugin:jest/recommended',
|
||||
'prettier',
|
||||
],
|
||||
ignorePatterns: [
|
||||
'client/dist/**/*',
|
||||
'client/public/**/*',
|
||||
'e2e/playwright-report/**/*',
|
||||
'packages/data-provider/types/**/*',
|
||||
'packages/data-provider/dist/**/*',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
plugins: ['react', 'react-hooks', '@typescript-eslint'],
|
||||
plugins: ['react', 'react-hooks', '@typescript-eslint', 'import'],
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow-with-description' }],
|
||||
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow' }],
|
||||
indent: ['error', 2, { SwitchCase: 1 }],
|
||||
'max-len': [
|
||||
'error',
|
||||
{
|
||||
code: 150,
|
||||
code: 120,
|
||||
ignoreStrings: true,
|
||||
ignoreTemplateLiterals: true,
|
||||
ignoreComments: true
|
||||
}
|
||||
ignoreComments: true,
|
||||
},
|
||||
],
|
||||
'linebreak-style': 0,
|
||||
curly: ['error', 'all'],
|
||||
semi: ['error', 'always'],
|
||||
'object-curly-spacing': ['error', 'always'],
|
||||
'no-multiple-empty-lines': ['error', { max: 1 }],
|
||||
'no-trailing-spaces': 'error',
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
// "arrow-parens": [2, "as-needed", { requireForBlockBody: true }],
|
||||
// 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
|
||||
'no-console': 'off',
|
||||
'import/no-cycle': 'error',
|
||||
'import/no-self-import': 'error',
|
||||
'import/extensions': 'off',
|
||||
'no-use-before-define': [
|
||||
'error',
|
||||
{
|
||||
functions: false
|
||||
}
|
||||
],
|
||||
'no-promise-executor-return': 'off',
|
||||
'no-param-reassign': 'off',
|
||||
'no-continue': 'off',
|
||||
'no-restricted-syntax': 'off',
|
||||
'react/prop-types': ['off'],
|
||||
'react/display-name': ['off']
|
||||
'react/display-name': ['off'],
|
||||
quotes: ['error', 'single'],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
@@ -59,14 +69,14 @@ module.exports = {
|
||||
rules: {
|
||||
'no-unused-vars': 'off', // off because it conflicts with '@typescript-eslint/no-unused-vars'
|
||||
'react/display-name': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'warn'
|
||||
}
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['rollup.config.js', '.eslintrc.js', 'jest.config.js'],
|
||||
env: {
|
||||
node: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
@@ -78,30 +88,45 @@ module.exports = {
|
||||
'**/*.spec.jsx',
|
||||
'**/*.spec.ts',
|
||||
'**/*.spec.tsx',
|
||||
'setupTests.js'
|
||||
'setupTests.js',
|
||||
],
|
||||
env: {
|
||||
jest: true,
|
||||
node: true
|
||||
node: true,
|
||||
},
|
||||
rules: {
|
||||
'react/display-name': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react/no-unescaped-entities': 'off'
|
||||
}
|
||||
'react/no-unescaped-entities': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: '**/*.+(ts)',
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: './client/tsconfig.json'
|
||||
project: './client/tsconfig.json',
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin', 'jest'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended'
|
||||
]
|
||||
}
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: './packages/data-provider/**/*.ts',
|
||||
overrides: [
|
||||
{
|
||||
files: '**/*.ts',
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: './packages/data-provider/tsconfig.json',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
@@ -109,7 +134,18 @@ module.exports = {
|
||||
// default to "createReactClass"
|
||||
pragma: 'React', // Pragma to use, default to "React"
|
||||
fragment: 'Fragment', // Fragment to use (may be a property of <pragma>), default to "Fragment"
|
||||
version: 'detect' // React version. "detect" automatically picks the version you have installed.
|
||||
}
|
||||
}
|
||||
version: 'detect', // React version. "detect" automatically picks the version you have installed.
|
||||
},
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||
},
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project: ['./client/tsconfig.json'],
|
||||
},
|
||||
node: {
|
||||
project: ['./client/tsconfig.json'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
2
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
@@ -58,7 +58,7 @@ body:
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/chatgpt-clone/blob/main/documents/contributions/code_of_conduct.md)
|
||||
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)
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
vendored
2
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
vendored
@@ -51,7 +51,7 @@ body:
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/chatgpt-clone/blob/main/documents/contributions/code_of_conduct.md)
|
||||
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)
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
2
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
@@ -52,7 +52,7 @@ body:
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/chatgpt-clone/blob/main/documents/contributions/code_of_conduct.md)
|
||||
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)
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
|
||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -7,7 +7,7 @@ version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/api" # Location of package manifests
|
||||
target-branch: "develop"
|
||||
target-branch: "dev"
|
||||
versioning-strategy: increase-if-necessary
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -20,7 +20,7 @@ updates:
|
||||
include: "scope"
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/client" # Location of package manifests
|
||||
target-branch: "develop"
|
||||
target-branch: "dev"
|
||||
versioning-strategy: increase-if-necessary
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -33,7 +33,7 @@ updates:
|
||||
include: "scope"
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
target-branch: "develop"
|
||||
target-branch: "dev"
|
||||
versioning-strategy: increase-if-necessary
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
72
.github/playwright.yml
vendored
72
.github/playwright.yml
vendored
@@ -1,72 +0,0 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
jobs:
|
||||
tests_e2e:
|
||||
name: Run end-to-end tests
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BINGAI_TOKEN: ${{ secrets.BINGAI_TOKEN }}
|
||||
CHATGPT_TOKEN: ${{ secrets.CHATGPT_TOKEN }}
|
||||
MONGO_URI: ${{ secrets.MONGO_URI }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
|
||||
- name: Cache API dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ./api/node_modules
|
||||
key: api-${{ runner.os }}-node-${{ hashFiles('./api/package-lock.json') }}
|
||||
restore-keys: |
|
||||
api-${{ runner.os }}-node-
|
||||
|
||||
- name: Install API dependencies
|
||||
working-directory: ./api
|
||||
run: npm ci
|
||||
|
||||
- name: Cache Client dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ./client/node_modules
|
||||
key: client-${{ runner.os }}-node-${{ hashFiles('./client/package-lock.json') }}
|
||||
restore-keys: |
|
||||
client-${{ runner.os }}-node-
|
||||
|
||||
- name: Install Client dependencies
|
||||
working-directory: ./client
|
||||
run: npm ci
|
||||
|
||||
- name: Build Client
|
||||
working-directory: ./client
|
||||
run: npm run build
|
||||
|
||||
- name: Install global dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Start API server
|
||||
working-directory: ./api
|
||||
run: |
|
||||
npm run start &
|
||||
sleep 10 # Wait for the server to start
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: e2e/playwright-report/
|
||||
retention-days: 30
|
||||
53
.github/pull_request_template.md
vendored
53
.github/pull_request_template.md
vendored
@@ -1,35 +1,48 @@
|
||||
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.
|
||||
# Pull Request Template
|
||||
|
||||
|
||||
### ⚠️ Pre-Submission Steps:
|
||||
|
||||
## Type of change
|
||||
1. Before starting work, make sure your main branch has the latest commits with `npm run update`
|
||||
2. Run linting command to find errors: `npm run lint`. Alternatively, ensure husky pre-commit checks are functioning.
|
||||
3. After your changes, reinstall packages in your current branch using `npm run reinstall` and ensure everything still works.
|
||||
- Restart the ESLint server ("ESLint: Restart ESLint Server" in VS Code command bar) and your IDE after reinstalling or updating.
|
||||
4. Clear web app localStorage and cookies before and after changes.
|
||||
5. For frontend changes:
|
||||
- Install typescript globally: `npm i -g typescript`.
|
||||
- Compile typescript before and after changes to check for introduced errors: `tsc --noEmit`.
|
||||
6. Run tests locally:
|
||||
- Backend unit tests: `npm run test:api`
|
||||
- Frontend unit tests: `npm run test:client`
|
||||
- Integration tests: `npm run e2e` (requires playwright installed, `npx install playwright`)
|
||||
|
||||
Please delete options that are not relevant.
|
||||
## Summary
|
||||
|
||||
Please provide a brief summary of your changes and the related issue. Include any motivation and context that is relevant to your changes. If there are any dependencies necessary for your changes, please list them here.
|
||||
|
||||
## Change Type
|
||||
|
||||
Please delete any irrelevant options.
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] This change requires a documentation update
|
||||
- [ ] Documentation update
|
||||
|
||||
- [ ] Documentation update
|
||||
|
||||
## How Has This Been Tested?
|
||||
|
||||
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:
|
||||
##
|
||||
## Testing
|
||||
|
||||
Please describe your test process and include instructions so that we can reproduce your test. If there are any important variables for your testing configuration, list them here.
|
||||
|
||||
### **Test Configuration**:
|
||||
##
|
||||
|
||||
## Checklist
|
||||
|
||||
## Checklist:
|
||||
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I have performed a self-review of my code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] New and existing unit tests pass locally with my changes
|
||||
- [ ] Any dependent changes have been merged and published in downstream modules
|
||||
- [ ] My code adheres to this project's style guidelines
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented in any complex areas of my code
|
||||
- [ ] I have made pertinent documentation changes
|
||||
- [ ] My changes do not introduce new warnings
|
||||
- [ ] I have written tests demonstrating that my changes are effective or that my feature works
|
||||
- [ ] Local unit tests pass with my changes
|
||||
- [ ] Any changes dependent on mine have been merged and published in downstream modules.
|
||||
|
||||
28
.github/wip-playwright.yml
vendored
28
.github/wip-playwright.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
jobs:
|
||||
tests_e2e:
|
||||
name: Run end-to-end tests
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: e2e/playwright-report/
|
||||
retention-days: 30
|
||||
46
.github/workflows/backend-review.yml
vendored
Normal file
46
.github/workflows/backend-review.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Backend Unit Tests
|
||||
on:
|
||||
# push:
|
||||
# branches:
|
||||
# - main
|
||||
# - dev
|
||||
# - release/*
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- release/*
|
||||
paths:
|
||||
- 'api/**'
|
||||
jobs:
|
||||
tests_Backend:
|
||||
name: Run Backend unit tests
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
MONGO_URI: ${{ secrets.MONGO_URI }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
JWT_SECRET: ${{ secrets.JWT_SECRET }}
|
||||
CREDS_KEY: ${{ secrets.CREDS_KEY }}
|
||||
CREDS_IV: ${{ secrets.CREDS_IV }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
# - name: Install Linux X64 Sharp
|
||||
# run: npm install --platform=linux --arch=x64 --verbose sharp
|
||||
|
||||
- name: Run unit tests
|
||||
run: cd api && npm run test:ci
|
||||
|
||||
- name: Run linters
|
||||
uses: wearerequired/lint-action@v2
|
||||
with:
|
||||
eslint: true
|
||||
38
.github/workflows/build.yml
vendored
Normal file
38
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Linux_Container_Workflow
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
RUNNER_VERSION: 2.293.0
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# checkout the repo
|
||||
- name: 'Checkout GitHub Action'
|
||||
uses: actions/checkout@main
|
||||
|
||||
- name: 'Login via Azure CLI'
|
||||
uses: azure/login@v1
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
||||
|
||||
- name: 'Build GitHub Runner container image'
|
||||
uses: azure/docker-login@v1
|
||||
with:
|
||||
login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
- run: |
|
||||
docker build --build-arg RUNNER_VERSION=${{ env.RUNNER_VERSION }} -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/pwd9000-github-runner-lin:${{ env.RUNNER_VERSION }} .
|
||||
|
||||
- name: 'Push container image to ACR'
|
||||
uses: azure/docker-login@v1
|
||||
with:
|
||||
login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
- run: |
|
||||
docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/pwd9000-github-runner-lin:${{ env.RUNNER_VERSION }}
|
||||
47
.github/workflows/container.yml
vendored
Normal file
47
.github/workflows/container.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Docker Compose Build on Tag
|
||||
|
||||
# The workflow is triggered when a tag is pushed
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Check out the repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Set up Docker
|
||||
- name: Set up Docker
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Run docker-compose build
|
||||
- name: Build Docker images
|
||||
run: |
|
||||
cp .env.example .env
|
||||
docker-compose build
|
||||
|
||||
# Get Tag Name
|
||||
- name: Get Tag Name
|
||||
id: tag_name
|
||||
run: echo "TAG_NAME=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||
|
||||
# Tag it properly before push to github
|
||||
- name: tag image and push
|
||||
run: |
|
||||
docker tag librechat:latest ghcr.io/${{ github.repository_owner }}/librechat:${{ env.TAG_NAME }}
|
||||
docker push ghcr.io/${{ github.repository_owner }}/librechat:${{ env.TAG_NAME }}
|
||||
docker tag librechat:latest ghcr.io/${{ github.repository_owner }}/librechat:latest
|
||||
docker push ghcr.io/${{ github.repository_owner }}/librechat:latest
|
||||
34
.github/workflows/data-provider.yml
vendored
Normal file
34
.github/workflows/data-provider.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Node.js Package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'packages/data-provider/package.json'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- run: cd packages/data-provider && npm ci
|
||||
- run: cd packages/data-provider && npm run build
|
||||
|
||||
publish-npm:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: cd packages/data-provider && npm ci
|
||||
- run: cd packages/data-provider && npm run build
|
||||
- run: cd packages/data-provider && npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
38
.github/workflows/deploy.yml
vendored
Normal file
38
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Deploy_GHRunner_Linux_ACI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
RUNNER_VERSION: 2.293.0
|
||||
ACI_RESOURCE_GROUP: 'Demo-ACI-GitHub-Runners-RG'
|
||||
ACI_NAME: 'gh-runner-linux-01'
|
||||
DNS_NAME_LABEL: 'gh-lin-01'
|
||||
GH_OWNER: ${{ github.repository_owner }}
|
||||
GH_REPOSITORY: 'LibreChat' #Change here to deploy self hosted runner ACI to another repo.
|
||||
|
||||
jobs:
|
||||
deploy-gh-runner-aci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# checkout the repo
|
||||
- name: 'Checkout GitHub Action'
|
||||
uses: actions/checkout@main
|
||||
|
||||
- name: 'Login via Azure CLI'
|
||||
uses: azure/login@v1
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
||||
|
||||
- name: 'Deploy to Azure Container Instances'
|
||||
uses: 'azure/aci-deploy@v1'
|
||||
with:
|
||||
resource-group: ${{ env.ACI_RESOURCE_GROUP }}
|
||||
image: ${{ secrets.REGISTRY_LOGIN_SERVER }}/pwd9000-github-runner-lin:${{ env.RUNNER_VERSION }}
|
||||
registry-login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}
|
||||
registry-username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
registry-password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
name: ${{ env.ACI_NAME }}
|
||||
dns-name-label: ${{ env.DNS_NAME_LABEL }}
|
||||
environment-variables: GH_TOKEN=${{ secrets.PAT_TOKEN }} GH_OWNER=${{ env.GH_OWNER }} GH_REPOSITORY=${{ env.GH_REPOSITORY }}
|
||||
location: 'eastus'
|
||||
51
.github/workflows/dev-images.yml
vendored
Normal file
51
.github/workflows/dev-images.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Docker Dev Images Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'api/**'
|
||||
- 'client/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Check out the repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Set up Docker
|
||||
- name: Set up Docker
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Build Docker images
|
||||
- name: Build Docker images
|
||||
run: |
|
||||
cp .env.example .env
|
||||
docker build -f Dockerfile.multi --target api-build -t librechat-dev-api .
|
||||
docker build -f Dockerfile -t librechat-dev .
|
||||
|
||||
# Tag and push the images to GitHub Container Registry
|
||||
- name: Tag and push images
|
||||
run: |
|
||||
docker tag librechat-dev-api:latest ghcr.io/${{ github.repository_owner }}/librechat-dev-api:${{ github.sha }}
|
||||
docker push ghcr.io/${{ github.repository_owner }}/librechat-dev-api:${{ github.sha }}
|
||||
docker tag librechat-dev-api:latest ghcr.io/${{ github.repository_owner }}/librechat-dev-api:latest
|
||||
docker push ghcr.io/${{ github.repository_owner }}/librechat-dev-api:latest
|
||||
|
||||
docker tag librechat-dev:latest ghcr.io/${{ github.repository_owner }}/librechat-dev:${{ github.sha }}
|
||||
docker push ghcr.io/${{ github.repository_owner }}/librechat-dev:${{ github.sha }}
|
||||
docker tag librechat-dev:latest ghcr.io/${{ github.repository_owner }}/librechat-dev:latest
|
||||
docker push ghcr.io/${{ github.repository_owner }}/librechat-dev:latest
|
||||
37
.github/workflows/frontend-review.yml
vendored
Normal file
37
.github/workflows/frontend-review.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
#github action to run unit tests for frontend with jest
|
||||
name: Frontend Unit Tests
|
||||
on:
|
||||
# push:
|
||||
# branches:
|
||||
# - main
|
||||
# - dev
|
||||
# - release/*
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- release/*
|
||||
paths:
|
||||
- 'client/**'
|
||||
- 'packages/**'
|
||||
jobs:
|
||||
tests_frontend:
|
||||
name: Run frontend unit tests
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build Client
|
||||
run: npm run frontend:ci
|
||||
|
||||
- name: Run unit tests
|
||||
run: cd client && npm run test:ci
|
||||
24
.github/workflows/mkdocs.yaml
vendored
Normal file
24
.github/workflows/mkdocs.yaml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: mkdocs
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
key: mkdocs-material-${{ env.cache_id }}
|
||||
path: .cache
|
||||
restore-keys: |
|
||||
mkdocs-material-
|
||||
- run: pip install mkdocs-material
|
||||
- run: mkdocs gh-deploy --force
|
||||
81
.github/workflows/playwright.yml
vendored
Normal file
81
.github/workflows/playwright.yml
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- release/*
|
||||
paths:
|
||||
- 'api/**'
|
||||
- 'client/**'
|
||||
- 'packages/**'
|
||||
- 'e2e/**'
|
||||
jobs:
|
||||
tests_e2e:
|
||||
name: Run Playwright tests
|
||||
if: github.event.pull_request.head.repo.full_name == 'danny-avila/LibreChat'
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_ENV: ci
|
||||
CI: true
|
||||
SEARCH: false
|
||||
BINGAI_TOKEN: user_provided
|
||||
CHATGPT_TOKEN: user_provided
|
||||
MONGO_URI: ${{ secrets.MONGO_URI }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
E2E_USER_EMAIL: ${{ secrets.E2E_USER_EMAIL }}
|
||||
E2E_USER_PASSWORD: ${{ secrets.E2E_USER_PASSWORD }}
|
||||
JWT_SECRET: ${{ secrets.JWT_SECRET }}
|
||||
CREDS_KEY: ${{ secrets.CREDS_KEY }}
|
||||
CREDS_IV: ${{ secrets.CREDS_IV }}
|
||||
DOMAIN_CLIENT: ${{ secrets.DOMAIN_CLIENT }}
|
||||
DOMAIN_SERVER: ${{ secrets.DOMAIN_SERVER }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
|
||||
# - name: Cache Node.js modules
|
||||
# uses: actions/cache@v3
|
||||
# with:
|
||||
# path: ~/.npm
|
||||
# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-node-
|
||||
|
||||
- name: Install global dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Remove sharp dependency
|
||||
run: rm -rf node_modules/sharp
|
||||
|
||||
- name: Install sharp with linux dependencies
|
||||
run: cd api && SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install --arch=x64 --platform=linux --libc=glibc sharp
|
||||
|
||||
- name: Build Client
|
||||
run: npm run frontend
|
||||
|
||||
# - name: Cache Playwright installations
|
||||
# uses: actions/cache@v3
|
||||
# with:
|
||||
# path: ~/.cache/ms-playwright/
|
||||
# key: ${{ runner.os }}-pw-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-pw-
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps chromium && npm install -D @playwright/test@latest
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: npm run e2e:ci
|
||||
|
||||
- name: Upload playwright report
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: e2e/playwright-report/
|
||||
retention-days: 30
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -39,6 +39,7 @@ meili_data/
|
||||
api/node_modules/
|
||||
client/node_modules/
|
||||
bower_components/
|
||||
types/
|
||||
|
||||
# Floobits
|
||||
.floo
|
||||
@@ -48,10 +49,10 @@ bower_components/
|
||||
|
||||
# Environment
|
||||
.npmrc
|
||||
.env
|
||||
!.env.example
|
||||
!.env.test.example
|
||||
.env*
|
||||
my.secrets
|
||||
!**/.env.example
|
||||
!**/.env.test.example
|
||||
cache.json
|
||||
api/data/
|
||||
owner.yml
|
||||
@@ -64,9 +65,16 @@ src/style - official.css
|
||||
.DS_Store
|
||||
*.code-workspace
|
||||
.idea
|
||||
*.pem
|
||||
config.local.ts
|
||||
**/storageState.json
|
||||
junit.xml
|
||||
|
||||
# meilisearch
|
||||
meilisearch
|
||||
meilisearch.exe
|
||||
data.ms/*
|
||||
auth.json
|
||||
|
||||
/packages/ux-shared/
|
||||
/images
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
[ -n "$CI" ] && exit 0
|
||||
npx lint-staged
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
@@ -127,6 +127,6 @@ For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
|
||||
##
|
||||
---
|
||||
|
||||
## [Go Back to ReadMe](README.md)
|
||||
|
||||
175
CONTRIBUTING.md
175
CONTRIBUTING.md
@@ -1,41 +1,34 @@
|
||||
# Contributor Guidelines
|
||||
# Contributor Guidelines
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Contributing Guidelines
|
||||
|
||||
If the feature you would like to contribute has not already received prior approval from the project maintainers (ie. 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/chatgpt-clone/discussions/categories/proposals) of the discussions board before beginning work on it.
|
||||
- Proposals should include specific implementation details including areas of the application that will be effected by the change inlcuding designs if applicable, and any other relevant information that might be required for a speedy review.
|
||||
- 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/chatgpt-clone/issues) and included in the corresponding pull request for tracking purposes.
|
||||
|
||||
*Please note that a pull request involving a feature that has not been reviewed and approved by the project maintainers may be rejected.*
|
||||
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.
|
||||
|
||||
If you would like to discuss the changes you wish to make, join our [Discord community](https://discord.gg/uDyZ5Tzhct).
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Please read our [Coding Standards and Conventions](docs/contributions/coding_conventions.md) before beginning on a contribution.
|
||||
We strive to maintain a positive and inclusive environment within our project community. We expect all contributors to adhere to the following standards:
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
- Using welcoming and inclusive language.
|
||||
- Being respectful of differing viewpoints and experiences.
|
||||
- Gracefully accepting constructive criticism.
|
||||
- Focusing on what is best for the community.
|
||||
- Showing empathy towards other community members.
|
||||
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions when necessary.
|
||||
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.
|
||||
|
||||
## To contribute to this project, please adhere to the following guidelines:
|
||||
|
||||
## 1. Git Workflow
|
||||
|
||||
We use a GitFlow workflow to manage changes to this project's codebase. Follow these general steps when contributing code:
|
||||
We utilize a GitFlow workflow to manage changes to this project's codebase. Follow these general steps when contributing code:
|
||||
|
||||
1. Fork the repository and create a new branch with a descriptive slash based name (e.g., new/feature/x).
|
||||
1. Fork the repository and create a new branch with a descriptive slash-based name (e.g., `new/feature/x`).
|
||||
2. Implement your changes and ensure that all tests pass.
|
||||
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`).
|
||||
4. Submit a pull request with a clear and concise description of your changes and the reasons behind them.
|
||||
@@ -43,143 +36,65 @@ We use a GitFlow workflow to manage changes to this project's codebase. Follow t
|
||||
|
||||
## 2. Commit Message Format
|
||||
|
||||
We have very precise rules over how our Git commit messages must be formatted.
|
||||
This format leads to **easier to read commit history**.
|
||||
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.
|
||||
|
||||
Each commit message consists of a **header**, a **body**, and a **footer**.
|
||||
### Commit Message Header
|
||||
|
||||
|
||||
```
|
||||
<header>
|
||||
<BLANK LINE>
|
||||
<body>
|
||||
<BLANK LINE>
|
||||
<footer>
|
||||
```
|
||||
|
||||
The `header` is mandatory and must conform to the [Commit Message Header](#commit-header) format.
|
||||
|
||||
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 must conform to the [Commit Message Body](#commit-body) format.
|
||||
|
||||
The `footer` is optional. The [Commit Message Footer](#commit-footer) format describes what the footer is used for and the structure it must have.
|
||||
|
||||
|
||||
#### <a name="commit-header"></a>Commit Message Header
|
||||
The header is mandatory and must conform to the following format:
|
||||
|
||||
```
|
||||
<type>(<scope>): <short summary>
|
||||
│ │ │
|
||||
│ │ └─⫸ Summary in present tense. Not capitalized. No period at the end.
|
||||
│ │
|
||||
│ └─⫸ Commit Scope: common|plays (2048, analog-clock, basic-calculator, etc.)|infra|etc.
|
||||
│
|
||||
└─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test
|
||||
```
|
||||
|
||||
The `<type>` and `<summary>` fields are mandatory, the `(<scope>)` field is optional.
|
||||
- `<type>`: Must be one of the following:
|
||||
- **build**: Changes that affect the build system or external dependencies.
|
||||
- **ci**: Changes to our CI configuration files and script.
|
||||
- **docs**: Documentation-only changes.
|
||||
- **feat**: A new feature.
|
||||
- **fix**: A bug fix.
|
||||
- **perf**: A code change that improves performance.
|
||||
- **refactor**: A code change that neither fixes a bug nor adds a feature.
|
||||
- **test**: Adding missing tests or correcting existing tests.
|
||||
|
||||
- `<scope>`: Optional. Indicates the scope of the commit, such as `common`, `plays`, `infra`, etc.
|
||||
|
||||
##### Type
|
||||
- `<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.
|
||||
|
||||
Must be one of the following:
|
||||
### Commit Message Body
|
||||
|
||||
* **build**: Changes that affect the build system or external dependencies
|
||||
* **ci**: Changes to our CI configuration files and script
|
||||
* **docs**: Documentation only changes
|
||||
* **feat**: A new feature
|
||||
* **fix**: A bug fix
|
||||
* **perf**: A code change that improves performance
|
||||
* **refactor**: A code change that neither fixes a bug nor adds a feature
|
||||
* **test**: Adding missing tests or correcting existing tests
|
||||
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.
|
||||
|
||||
### Commit Message Footer
|
||||
|
||||
##### Summary
|
||||
|
||||
Use the summary field to provide a succinct description of the change:
|
||||
|
||||
* use the imperative, present tense: "change" not "changed" nor "changes"
|
||||
* don't capitalize the first letter
|
||||
* no dot (.) at the end
|
||||
|
||||
|
||||
#### <a name="commit-body"></a>Commit Message Body
|
||||
|
||||
Just as in the summary, use the imperative, present tense: "fix" not "fixed" nor "fixes".
|
||||
|
||||
Explain the motivation for the change in the commit message body. This commit message should explain _why_ you are making the change.
|
||||
You can include a comparison of the previous behavior with the new behavior in order to illustrate the impact of the change.
|
||||
|
||||
|
||||
#### <a name="commit-footer"></a>Commit Message Footer
|
||||
|
||||
The footer can contain information about breaking changes and deprecations and is also the place to reference GitHub issues, Jira tickets, and other PRs that this commit closes or is related to.
|
||||
For example:
|
||||
|
||||
```
|
||||
BREAKING CHANGE: <breaking change summary>
|
||||
<BLANK LINE>
|
||||
<breaking change description + migration instructions>
|
||||
<BLANK LINE>
|
||||
<BLANK LINE>
|
||||
Fixes #<issue number>
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
DEPRECATED: <what is deprecated>
|
||||
<BLANK LINE>
|
||||
<deprecation description + recommended update path>
|
||||
<BLANK LINE>
|
||||
<BLANK LINE>
|
||||
Closes #<pr number>
|
||||
```
|
||||
|
||||
Breaking Change section should start with the phrase "BREAKING CHANGE: " followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions.
|
||||
|
||||
Similarly, a Deprecation section should start with "DEPRECATED: " followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path.
|
||||
|
||||
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.
|
||||
|
||||
### Revert commits
|
||||
|
||||
If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit.
|
||||
|
||||
The content of the commit message body should contain:
|
||||
|
||||
- information about the SHA of the commit being reverted in the following format: `This reverts commit <SHA>`,
|
||||
- a clear description of the reason for reverting the commit message.
|
||||
|
||||
Each commit message should start with a tag indicating the change type and a brief summary of the changes. This format enables quick identification of each commit's purpose and can be used to generate changelogs.
|
||||
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.
|
||||
|
||||
## 3. Pull Request Process
|
||||
|
||||
When submitting a pull request, please follow these guidelines:
|
||||
|
||||
### Note: Submit a pull request with a clear and concise description of your changes and the reasons behind them. Be sure to include the steps to test the PR.
|
||||
- Ensure that any installation or build dependencies are removed before the end of the layer when doing a build.
|
||||
- Update the README.md with details of changes to the interface, including new environment variables, exposed ports, useful file locations, and container parameters.
|
||||
- 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.
|
||||
|
||||
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
|
||||
build.
|
||||
2. Update the README.md with details of changes to the interface, this includes new environment
|
||||
variables, exposed ports, useful file locations and container parameters.
|
||||
3. Increase the version numbers in any examples files and the README.md to the new version that this
|
||||
Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
|
||||
Ensure that your changes meet the following criteria:
|
||||
|
||||
Ensure that your changes meet the following criteria when submitting a pull request:
|
||||
- All tests pass.
|
||||
- The code is well-formatted and adheres to our coding standards.
|
||||
- The commit history is clean and easy to follow. (Use Squash to clean your commit history)
|
||||
- The pull request description clearly outlines the changes and the reasons behind them.
|
||||
- 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.
|
||||
- The pull request description clearly outlines the changes and the reasons behind them. Be sure to include the steps to test the pull request.
|
||||
|
||||
## 4. Naming Conventions
|
||||
|
||||
Apply the following naming conventions to branches, labels, and other Git-related entities:
|
||||
|
||||
- Branch names: descriptive and slash based (e.g., new/feature/x)
|
||||
- Labels: descriptive and snake_case (e.g., `bug_fix`).
|
||||
- Directories and file names: descriptive and snake_case (e.g., `config_file.yaml`).
|
||||
- Branch names: Descriptive and slash-based (e.g., `new/feature/x`).
|
||||
- Labels: Descriptive and snake_case (e.g., `bug_fix`).
|
||||
- Directories and file names: Descriptive and snake_case (e.g., `config_file.yaml`).
|
||||
|
||||
##
|
||||
---
|
||||
|
||||
## [Go Back to ReadMe](README.md)
|
||||
|
||||
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@@ -1,18 +1,14 @@
|
||||
# Base node image
|
||||
FROM node:19-alpine AS node
|
||||
|
||||
# Install curl for health check
|
||||
RUN apk --no-cache add curl
|
||||
|
||||
COPY . /app
|
||||
# Install dependencies
|
||||
WORKDIR /app
|
||||
RUN npm ci
|
||||
|
||||
# Frontend variables as build args
|
||||
ARG VITE_APP_TITLE
|
||||
ARG VITE_SHOW_GOOGLE_LOGIN_OPTION
|
||||
|
||||
# You will need to add your VITE variables to the docker-compose file
|
||||
ENV VITE_APP_TITLE=$VITE_APP_TITLE
|
||||
ENV VITE_SHOW_GOOGLE_LOGIN_OPTION=$VITE_SHOW_GOOGLE_LOGIN_OPTION
|
||||
|
||||
# React client build
|
||||
ENV NODE_OPTIONS="--max-old-space-size=2048"
|
||||
RUN npm run frontend
|
||||
|
||||
40
Dockerfile.multi
Normal file
40
Dockerfile.multi
Normal file
@@ -0,0 +1,40 @@
|
||||
# Build API, Client and Data Provider
|
||||
FROM node:19-alpine AS base
|
||||
|
||||
WORKDIR /app
|
||||
COPY config/loader.js ./config/
|
||||
RUN npm install dotenv
|
||||
|
||||
WORKDIR /app/api
|
||||
COPY api/package*.json ./
|
||||
COPY api/ ./
|
||||
RUN npm install
|
||||
|
||||
# React client build
|
||||
FROM base AS client-build
|
||||
WORKDIR /app/client
|
||||
COPY ./client/ ./
|
||||
|
||||
WORKDIR /app/packages/data-provider
|
||||
COPY ./packages/data-provider ./
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
RUN mkdir -p /app/client/node_modules/librechat-data-provider/
|
||||
RUN cp -R /app/packages/data-provider/* /app/client/node_modules/librechat-data-provider/
|
||||
|
||||
WORKDIR /app/client
|
||||
RUN npm install
|
||||
ENV NODE_OPTIONS="--max-old-space-size=2048"
|
||||
RUN npm run build
|
||||
|
||||
# Node API setup
|
||||
FROM base AS api-build
|
||||
COPY --from=client-build /app/client/dist /app/client/dist
|
||||
EXPOSE 3080
|
||||
ENV HOST=0.0.0.0
|
||||
CMD ["node", "server/index.js"]
|
||||
|
||||
# Nginx setup
|
||||
FROM nginx:1.21.1-alpine AS prod-stage
|
||||
COPY ./client/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,7 +1,9 @@
|
||||
# MIT License
|
||||
# MIT License
|
||||
|
||||
Copyright (c) 2023 Danny Avila
|
||||
##
|
||||
|
||||
---
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
@@ -22,6 +24,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
##
|
||||
---
|
||||
|
||||
## [Go Back to ReadMe](README.md)
|
||||
|
||||
102
README.md
102
README.md
@@ -1,19 +1,28 @@
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/NGaa9RPCft">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/110412045/228325485-9d3e618f-a980-44fe-89e9-d6d39164680e.png">
|
||||
<img src="https://user-images.githubusercontent.com/110412045/228325485-9d3e618f-a980-44fe-89e9-d6d39164680e.png" height="128">
|
||||
</picture>
|
||||
<a href="https://docs.librechat.ai">
|
||||
<img src="docs/assets/LibreChat.svg" height="256">
|
||||
</a>
|
||||
<a href="https://docs.librechat.ai">
|
||||
<h1 align="center">LibreChat</h1>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/NGaa9RPCft">
|
||||
<img src="https://img.shields.io/discord/1086345563026489514?label=&logo=discord&style=for-the-badge&logoWidth=20&labelColor=000000&color=blueviolet">
|
||||
<img
|
||||
src="https://img.shields.io/discord/1086345563026489514?label=&logo=discord&style=for-the-badge&logoWidth=20&logoColor=white&labelColor=000000&color=blueviolet">
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@LibreChat">
|
||||
<img
|
||||
src="https://img.shields.io/badge/YOUTUBE-red.svg?style=for-the-badge&logo=youtube&logoColor=white&labelColor=000000&logoWidth=20">
|
||||
</a>
|
||||
<a href="https://docs.librechat.ai">
|
||||
<img
|
||||
src="https://img.shields.io/badge/DOCS-blue.svg?style=for-the-badge&logo=read-the-docs&logoColor=white&labelColor=000000&logoWidth=20">
|
||||
</a>
|
||||
<a aria-label="Sponsors" href="#sponsors">
|
||||
<img alt="" src="https://img.shields.io/badge/SPONSORS-brightgreen.svg?style=for-the-badge&labelColor=000000&logoWidth=20">
|
||||
<img
|
||||
src="https://img.shields.io/badge/SPONSORS-brightgreen.svg?style=for-the-badge&logo=github-sponsors&logoColor=white&labelColor=000000&logoWidth=20">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -22,47 +31,30 @@ LibreChat brings together the future of assistant AIs with the revolutionary tec
|
||||
|
||||
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.
|
||||
|
||||
<!--  -->
|
||||
https://github.com/danny-avila/LibreChat/assets/110412045/c1eb0c0f-41f6-4335-b982-84b278b53d59
|
||||
<!-- https://github.com/danny-avila/LibreChat/assets/110412045/c1eb0c0f-41f6-4335-b982-84b278b53d59 -->
|
||||
|
||||
[](https://youtu.be/pNIOs1ovsXw)
|
||||
Click on the thumbnail to open the video☝️
|
||||
|
||||
# Features
|
||||
|
||||
- Response streaming identical to ChatGPT through server-sent events
|
||||
- UI from original ChatGPT, including Dark mode
|
||||
- AI model selection (through 5 endpoints: OpenAI API, BingAI, ChatGPT Browser, PaLM2, Plugins)
|
||||
- Create, Save, & Share custom presets - [More info on prompt presets here](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.3.0)
|
||||
- AI model selection: OpenAI API, BingAI, ChatGPT Browser, PaLM2, Anthropic (Claude), Plugins
|
||||
- Create, Save, & Share custom presets - [More info on prompt presets here](https://github.com/danny-avila/LibreChat/releases/tag/v0.3.0)
|
||||
- Edit and Resubmit messages with conversation branching
|
||||
- Search all messages/conversations - [More info here](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.1.0)
|
||||
- Search all messages/conversations - [More info here](https://github.com/danny-avila/LibreChat/releases/tag/v0.1.0)
|
||||
- Plugins now available (including web access, image generation and more)
|
||||
|
||||
---
|
||||
# ⚠️ **Breaking Changes** ⚠️
|
||||
Note: These changes only apply to users who are updating from a previous version of the app.
|
||||
|
||||
- We have simplified the configuration process by using a single `.env` file in the root folder instead of separate `/api/.env` and `/client/.env` files.
|
||||
- If you had installed a previous version, you can run `npm run upgrade` to automatically copy the content of both files to the new `.env` file and backup the old ones in the root dir.
|
||||
- If you are installing the project for the first time, it's recommend you run the installation script `npm run install` to guide your local setup (otherwise continue to use docker)
|
||||
- The docker-compose file had some change. Review the [new docker instructions](docs\install\docker_install.md) to make sure you are setup properly. This is still the simplest and most effective method.
|
||||
- The upgrade script requires both `/api/.env` and `/client/.env` files to run properly. If you get an error about a missing client env file, just rename the `/client/.env.example` file to `/client/.env` and run the script again.
|
||||
- We have renamed the `OPENAI_KEY` variable to `OPENAI_API_KEY` to match the official documentation. The upgrade script should do this automatically for you, but please double-check that your key is correct in the new `.env` file.
|
||||
- After running the upgrade script, the `OPENAI_API_KEY` variable might be placed in a different section in the new `.env` file than before. This does not affect the functionality of the app, but if you want to keep it organized, you can look for it near the bottom of the file and move it to its usual section.
|
||||
## ⚠️ [Breaking Changes](docs/general_info/breaking_changes.md) ⚠️
|
||||
|
||||
##
|
||||
|
||||
- For enhanced security, we are now asking for crypto keys for securely storing credentials in the `.env` file. Crypto keys are used to encrypt and decrypt sensitive data such as passwords and access keys. If you don't set them, the app will crash on startup.
|
||||
- You need to fill the following variables in the `.env` file with 32-byte (64 characters in hex) or 16-byte (32 characters in hex) values:
|
||||
- `CREDS_KEY` (32-byte)
|
||||
- `CREDS_IV` (16-byte)
|
||||
- `JWT_SECRET` (32-byte, optional but recommended)
|
||||
- You can use this replit to generate some crypto keys quickly: https://replit.com/@daavila/crypto#index.js
|
||||
- Make sure you keep your crypto keys safe and don't share them with anyone.
|
||||
|
||||
We apologize for any inconvenience caused by these changes. We hope you enjoy the new and improved version of our app!
|
||||
**Please read this before updating from a previous version**
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
- Keep up with the latest updates by visiting the releases page - [Releases](https://github.com/danny-avila/LibreChat/releases)
|
||||
Keep up with the latest updates by visiting the releases page - [Releases](https://github.com/danny-avila/LibreChat/releases)
|
||||
|
||||
---
|
||||
|
||||
@@ -71,11 +63,16 @@ We apologize for any inconvenience caused by these changes. We hope you enjoy th
|
||||
<details open>
|
||||
<summary><strong>Getting Started</strong></summary>
|
||||
|
||||
* [Docker Install](/docs/install/docker_install.md)
|
||||
* [Linux Install](docs/install/linux_install.md)
|
||||
* [Mac Install](docs/install/mac_install.md)
|
||||
* [Windows Install](docs/install/windows_install.md)
|
||||
* [APIs and Tokens](docs/install/apis_and_tokens.md)
|
||||
* Installation
|
||||
* [Docker Install🐳](docs/install/docker_install.md)
|
||||
* [Linux Install🐧](docs/install/linux_install.md)
|
||||
* [Mac Install🍎](docs/install/mac_install.md)
|
||||
* [Windows Install💙](docs/install/windows_install.md)
|
||||
* Configuration
|
||||
* [APIs and Tokens](docs/install/apis_and_tokens.md)
|
||||
* [User Auth System](docs/install/user_auth_system.md)
|
||||
* [Online MongoDB Database](docs/install/mongodb.md)
|
||||
* [Languages](docs/install/languages.md)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -84,9 +81,7 @@ We apologize for any inconvenience caused by these changes. We hope you enjoy th
|
||||
* [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||
* [Project Origin](docs/general_info/project_origin.md)
|
||||
* [Multilingual Information](docs/general_info/multilingual_information.md)
|
||||
* [Tech Stack](docs/general_info/tech_stack.md)
|
||||
* [Changelog](CHANGELOG.md)
|
||||
* [Bing Jailbreak Info](docs/general_info/bing_jailbreak_info.md)
|
||||
* [Tech Stack](docs/general_info/tech_stack.md)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -98,26 +93,33 @@ We apologize for any inconvenience caused by these changes. We hope you enjoy th
|
||||
* [Stable Diffusion](docs/features/plugins/stable_diffusion.md)
|
||||
* [Wolfram](docs/features/plugins/wolfram.md)
|
||||
* [Make Your Own Plugin](docs/features/plugins/make_your_own.md)
|
||||
* [Using official ChatGPT Plugins](docs/features/plugins/chatgpt_plugins_openapi.md)
|
||||
|
||||
* [User Auth System](docs/features/user_auth_system.md)
|
||||
* [Proxy](docs/features/proxy.md)
|
||||
* [Bing Jailbreak](docs/features/bing_jailbreak.md)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Cloud Deployment</strong></summary>
|
||||
|
||||
* [Hetzner](docs/deployment/hetzner_ubuntu.md)
|
||||
* [Heroku](docs/deployment/heroku.md)
|
||||
* [Linode](docs/deployment/linode.md)
|
||||
* [Cloudflare](docs/deployment/cloudflare.md)
|
||||
* [Ngrok](docs/deployment/ngrok.md)
|
||||
* [Render](docs/deployment/render.md)
|
||||
* [Azure](docs/deployment/azure-terraform.md)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Contributions</strong></summary>
|
||||
|
||||
|
||||
* [Contributor Guidelines](CONTRIBUTING.md)
|
||||
* [Documentation Guidelines](docs/contributions/documentation_guidelines.md)
|
||||
* [Code Standards and Conventions](docs/contributions/coding_conventions.md)
|
||||
* [Testing](docs/contributions/testing.md)
|
||||
* [Security](SECURITY.md)
|
||||
* [Trello Board](https://trello.com/b/17z094kq/chatgpt-clone)
|
||||
* [Trello Board](https://trello.com/b/17z094kq/LibreChate)
|
||||
</details>
|
||||
|
||||
|
||||
@@ -125,14 +127,14 @@ We apologize for any inconvenience caused by these changes. We hope you enjoy th
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#danny-avila/chatgpt-clone&Date)
|
||||
[](https://star-history.com/#danny-avila/LibreChat&Date)
|
||||
|
||||
---
|
||||
|
||||
## Sponsors
|
||||
|
||||
Sponsored by <a href="https://github.com/DavidDev1334"><b>@DavidDev1334</b></a>, <a href="https://github.com/mjtechguy"><b>@mjtechguy</b></a>, <a href="https://github.com/Pharrcyde"><b>@Pharrcyde</b></a>, & <a href="https://github.com/fuegovic"><b>@fuegovic</b></a>
|
||||
|
||||
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>
|
||||
|
||||
---
|
||||
|
||||
## Contributors
|
||||
@@ -147,6 +149,6 @@ For new features, components, or extensions, please open an issue and discuss be
|
||||
|
||||
This project exists in its current state thanks to all the people who contribute
|
||||
---
|
||||
<a href="https://github.com/danny-avila/chatgpt-clone/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=danny-avila/chatgpt-clone" />
|
||||
<a href="https://github.com/danny-avila/LibreChat/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=danny-avila/LibreChat" />
|
||||
</a>
|
||||
|
||||
58
SECURITY.md
58
SECURITY.md
@@ -1,55 +1,63 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
We take security seriously and appreciate the efforts of security researchers to improve the security of our codebase.
|
||||
If you discover a security vulnerability within our project, please follow these guidelines to report it to us:
|
||||
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:
|
||||
|
||||
**Note: Only report sensible vulnerability report details via Github Security Advisory System. Every other communication channel are public and should be used only to initiate first contact and to initiate a private communication channel.**
|
||||
**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.**
|
||||
|
||||
### Communication channels
|
||||
- **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. You can find more information on how to submit a security advisory report in the [GitHub Security Advisories documentation](https://docs.github.com/en/code-security/getting-started-with-security-vulnerability-alerts/about-github-security-advisories).
|
||||
- **Option 2: Github issues**: You can initiate first contact via Github Issues. **Please note that initial contact through Discord should not include any sensitive details.**
|
||||
- **Option 3: Discord Server**: You can join our [Discord community](https://discord.gg/5rbRxn4uME) and initiate first contact in the `#issues` channel. **Please note that initial contact through Discord should not include any sensitive details.**
|
||||
## Communication Channels
|
||||
|
||||
_After initial contact, we will use this initial contact to establish a private communication channel for further discussion._
|
||||
When reporting a security vulnerability, you have the following options to reach out to us:
|
||||
|
||||
- **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).
|
||||
|
||||
- **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.
|
||||
|
||||
- **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.
|
||||
|
||||
_After the initial contact, we will establish a private communication channel for further discussion._
|
||||
|
||||
### When submitting a vulnerability report, please provide us with the following information:
|
||||
- A clear description of the vulnerability, including steps to reproduce it
|
||||
- The version(s) of the project affected by the vulnerability
|
||||
- Any additional information that may be useful for understanding and addressing the issue
|
||||
We will make every effort to acknowledge your report within 72 hours and keep you informed of its progress towards resolution.
|
||||
|
||||
- A clear description of the vulnerability, including steps to reproduce it.
|
||||
- The version(s) of the project affected by the vulnerability.
|
||||
- Any additional information that may be useful for understanding and addressing the issue.
|
||||
|
||||
We strive to acknowledge vulnerability reports within 72 hours and will keep you informed of the progress towards resolution.
|
||||
|
||||
## Security Updates and Patching
|
||||
We are committed to maintaining the security of our open-source project named LibreChat and promptly addressing any identified vulnerabilities. To ensure the security of our project, we follow these practices:
|
||||
- We prioritize security updates for the current major release of our software.
|
||||
- We actively monitor the GitHub Security Advisory system and the `#issues` channel on Discord for any vulnerability reports.
|
||||
- We promptly review and validate reported vulnerabilities and take appropriate actions to address them.
|
||||
- We release security patches and updates in a timely manner to mitigate any identified vulnerabilities.
|
||||
|
||||
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:
|
||||
|
||||
- We prioritize security updates for the current major release of our software.
|
||||
- We actively monitor the GitHub Security Advisory system and the `#issues` channel on Discord for any vulnerability reports.
|
||||
- We promptly review and validate reported vulnerabilities and take appropriate actions to address them.
|
||||
- We release security patches and updates in a timely manner to mitigate any identified vulnerabilities.
|
||||
|
||||
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.
|
||||
|
||||
## Scope
|
||||
|
||||
This security policy applies to the following GitHub repository:
|
||||
- Repository: [LibreChat](https://github.com/danny-avila/chatgpt-clone)
|
||||
|
||||
- Repository: [LibreChat](https://github.com/danny-avila/LibreChat)
|
||||
|
||||
## Contact
|
||||
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/chatgpt-clone/issues/new) on GitHub.
|
||||
Please note that the response time may vary depending on the nature and severity of the inquiry.
|
||||
|
||||
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.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
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.
|
||||
|
||||
## Bug Bounty Program
|
||||
We do not currently have a bug bounty program in place. However, we welcome and appreciate any 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.
|
||||
|
||||
We currently do not have a bug bounty program in place. However, we welcome and appreciate any
|
||||
|
||||
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.
|
||||
|
||||
**Reference**
|
||||
- https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html
|
||||
|
||||
##
|
||||
---
|
||||
|
||||
## [Go Back to ReadMe](README.md)
|
||||
|
||||
@@ -14,11 +14,11 @@ const askBing = async ({
|
||||
invocationId,
|
||||
toneStyle,
|
||||
token,
|
||||
onProgress
|
||||
onProgress,
|
||||
}) => {
|
||||
const { BingAIClient } = await import('@waylaidwanderer/chatgpt-api');
|
||||
const store = {
|
||||
store: new KeyvFile({ filename: './data/cache.json' })
|
||||
store: new KeyvFile({ filename: './data/cache.json' }),
|
||||
};
|
||||
|
||||
const bingAIClient = new BingAIClient({
|
||||
@@ -30,7 +30,7 @@ const askBing = async ({
|
||||
debug: false,
|
||||
cache: store,
|
||||
host: process.env.BINGAI_HOST || null,
|
||||
proxy: process.env.PROXY || null
|
||||
proxy: process.env.PROXY || null,
|
||||
});
|
||||
|
||||
let options = {};
|
||||
@@ -39,23 +39,43 @@ const askBing = async ({
|
||||
jailbreakConversationId = false;
|
||||
}
|
||||
|
||||
if (jailbreak)
|
||||
if (jailbreak) {
|
||||
options = {
|
||||
jailbreakConversationId: jailbreakConversationId || jailbreak,
|
||||
context,
|
||||
systemMessage,
|
||||
parentMessageId,
|
||||
toneStyle,
|
||||
onProgress
|
||||
onProgress,
|
||||
clientOptions: {
|
||||
features: {
|
||||
genImage: {
|
||||
server: {
|
||||
enable: true,
|
||||
type: 'markdown_list',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
else {
|
||||
} else {
|
||||
options = {
|
||||
conversationId,
|
||||
context,
|
||||
systemMessage,
|
||||
parentMessageId,
|
||||
toneStyle,
|
||||
onProgress
|
||||
onProgress,
|
||||
clientOptions: {
|
||||
features: {
|
||||
genImage: {
|
||||
server: {
|
||||
enable: true,
|
||||
type: 'markdown_list',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// don't give those parameters for new conversation
|
||||
@@ -10,11 +10,11 @@ const browserClient = async ({
|
||||
onProgress,
|
||||
onEventMessage,
|
||||
abortController,
|
||||
userId
|
||||
userId,
|
||||
}) => {
|
||||
const { ChatGPTBrowserClient } = await import('@waylaidwanderer/chatgpt-api');
|
||||
const store = {
|
||||
store: new KeyvFile({ filename: './data/cache.json' })
|
||||
store: new KeyvFile({ filename: './data/cache.json' }),
|
||||
};
|
||||
|
||||
const clientOptions = {
|
||||
@@ -27,7 +27,7 @@ const browserClient = async ({
|
||||
model: model,
|
||||
debug: false,
|
||||
proxy: process.env.PROXY || null,
|
||||
user: userId
|
||||
user: userId,
|
||||
};
|
||||
|
||||
const client = new ChatGPTBrowserClient(clientOptions, store);
|
||||
324
api/app/clients/AnthropicClient.js
Normal file
324
api/app/clients/AnthropicClient.js
Normal file
@@ -0,0 +1,324 @@
|
||||
const Keyv = require('keyv');
|
||||
// const { Agent, ProxyAgent } = require('undici');
|
||||
const BaseClient = require('./BaseClient');
|
||||
const {
|
||||
encoding_for_model: encodingForModel,
|
||||
get_encoding: getEncoding,
|
||||
} = require('@dqbd/tiktoken');
|
||||
const Anthropic = require('@anthropic-ai/sdk');
|
||||
|
||||
const HUMAN_PROMPT = '\n\nHuman:';
|
||||
const AI_PROMPT = '\n\nAssistant:';
|
||||
|
||||
const tokenizersCache = {};
|
||||
|
||||
class AnthropicClient extends BaseClient {
|
||||
constructor(apiKey, options = {}, cacheOptions = {}) {
|
||||
super(apiKey, options, cacheOptions);
|
||||
cacheOptions.namespace = cacheOptions.namespace || 'anthropic';
|
||||
this.conversationsCache = new Keyv(cacheOptions);
|
||||
this.apiKey = apiKey || process.env.ANTHROPIC_API_KEY;
|
||||
this.sender = 'Anthropic';
|
||||
this.userLabel = HUMAN_PROMPT;
|
||||
this.assistantLabel = AI_PROMPT;
|
||||
this.setOptions(options);
|
||||
}
|
||||
|
||||
setOptions(options) {
|
||||
if (this.options && !this.options.replaceOptions) {
|
||||
// nested options aren't spread properly, so we need to do this manually
|
||||
this.options.modelOptions = {
|
||||
...this.options.modelOptions,
|
||||
...options.modelOptions,
|
||||
};
|
||||
delete options.modelOptions;
|
||||
// now we can merge options
|
||||
this.options = {
|
||||
...this.options,
|
||||
...options,
|
||||
};
|
||||
} else {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
const modelOptions = this.options.modelOptions || {};
|
||||
this.modelOptions = {
|
||||
...modelOptions,
|
||||
// set some good defaults (check for undefined in some cases because they may be 0)
|
||||
model: modelOptions.model || 'claude-1',
|
||||
temperature: typeof modelOptions.temperature === 'undefined' ? 0.7 : modelOptions.temperature, // 0 - 1, 0.7 is recommended
|
||||
topP: typeof modelOptions.topP === 'undefined' ? 0.7 : modelOptions.topP, // 0 - 1, default: 0.7
|
||||
topK: typeof modelOptions.topK === 'undefined' ? 40 : modelOptions.topK, // 1-40, default: 40
|
||||
stop: modelOptions.stop, // no stop method for now
|
||||
};
|
||||
|
||||
this.maxContextTokens = this.options.maxContextTokens || 99999;
|
||||
this.maxResponseTokens = this.modelOptions.maxOutputTokens || 1500;
|
||||
this.maxPromptTokens =
|
||||
this.options.maxPromptTokens || this.maxContextTokens - this.maxResponseTokens;
|
||||
|
||||
if (this.maxPromptTokens + this.maxResponseTokens > this.maxContextTokens) {
|
||||
throw new Error(
|
||||
`maxPromptTokens + maxOutputTokens (${this.maxPromptTokens} + ${this.maxResponseTokens} = ${
|
||||
this.maxPromptTokens + this.maxResponseTokens
|
||||
}) must be less than or equal to maxContextTokens (${this.maxContextTokens})`,
|
||||
);
|
||||
}
|
||||
|
||||
this.startToken = '||>';
|
||||
this.endToken = '';
|
||||
this.gptEncoder = this.constructor.getTokenizer('cl100k_base');
|
||||
|
||||
if (!this.modelOptions.stop) {
|
||||
const stopTokens = [this.startToken];
|
||||
if (this.endToken && this.endToken !== this.startToken) {
|
||||
stopTokens.push(this.endToken);
|
||||
}
|
||||
stopTokens.push(`${this.userLabel}`);
|
||||
stopTokens.push('<|diff_marker|>');
|
||||
|
||||
this.modelOptions.stop = stopTokens;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
getClient() {
|
||||
if (this.options.reverseProxyUrl) {
|
||||
return new Anthropic({
|
||||
apiKey: this.apiKey,
|
||||
baseURL: this.options.reverseProxyUrl,
|
||||
});
|
||||
} else {
|
||||
return new Anthropic({
|
||||
apiKey: this.apiKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async buildMessages(messages, parentMessageId) {
|
||||
const orderedMessages = this.constructor.getMessagesForConversation(messages, parentMessageId);
|
||||
if (this.options.debug) {
|
||||
console.debug('AnthropicClient: orderedMessages', orderedMessages, parentMessageId);
|
||||
}
|
||||
|
||||
const formattedMessages = orderedMessages.map((message) => ({
|
||||
author: message.isCreatedByUser ? this.userLabel : this.assistantLabel,
|
||||
content: message?.content ?? message.text,
|
||||
}));
|
||||
|
||||
let identityPrefix = '';
|
||||
if (this.options.userLabel) {
|
||||
identityPrefix = `\nHuman's name: ${this.options.userLabel}`;
|
||||
}
|
||||
|
||||
if (this.options.modelLabel) {
|
||||
identityPrefix = `${identityPrefix}\nYou are ${this.options.modelLabel}`;
|
||||
}
|
||||
|
||||
let promptPrefix = (this.options.promptPrefix || '').trim();
|
||||
if (promptPrefix) {
|
||||
// If the prompt prefix doesn't end with the end token, add it.
|
||||
if (!promptPrefix.endsWith(`${this.endToken}`)) {
|
||||
promptPrefix = `${promptPrefix.trim()}${this.endToken}\n\n`;
|
||||
}
|
||||
promptPrefix = `\nContext:\n${promptPrefix}`;
|
||||
}
|
||||
|
||||
if (identityPrefix) {
|
||||
promptPrefix = `${identityPrefix}${promptPrefix}`;
|
||||
}
|
||||
|
||||
const promptSuffix = `${promptPrefix}${this.assistantLabel}\n`; // Prompt AI to respond.
|
||||
let currentTokenCount = this.getTokenCount(promptSuffix);
|
||||
|
||||
let promptBody = '';
|
||||
const maxTokenCount = this.maxPromptTokens;
|
||||
|
||||
const context = [];
|
||||
|
||||
// Iterate backwards through the messages, adding them to the prompt until we reach the max token count.
|
||||
// Do this within a recursive async function so that it doesn't block the event loop for too long.
|
||||
// Also, remove the next message when the message that puts us over the token limit is created by the user.
|
||||
// Otherwise, remove only the exceeding message. This is due to Anthropic's strict payload rule to start with "Human:".
|
||||
const nextMessage = {
|
||||
remove: false,
|
||||
tokenCount: 0,
|
||||
messageString: '',
|
||||
};
|
||||
|
||||
const buildPromptBody = async () => {
|
||||
if (currentTokenCount < maxTokenCount && formattedMessages.length > 0) {
|
||||
const message = formattedMessages.pop();
|
||||
const isCreatedByUser = message.author === this.userLabel;
|
||||
const messageString = `${message.author}\n${message.content}${this.endToken}\n`;
|
||||
let newPromptBody = `${messageString}${promptBody}`;
|
||||
|
||||
context.unshift(message);
|
||||
|
||||
const tokenCountForMessage = this.getTokenCount(messageString);
|
||||
const newTokenCount = currentTokenCount + tokenCountForMessage;
|
||||
|
||||
if (!isCreatedByUser) {
|
||||
nextMessage.messageString = messageString;
|
||||
nextMessage.tokenCount = tokenCountForMessage;
|
||||
}
|
||||
|
||||
if (newTokenCount > maxTokenCount) {
|
||||
if (!promptBody) {
|
||||
// This is the first message, so we can't add it. Just throw an error.
|
||||
throw new Error(
|
||||
`Prompt is too long. Max token count is ${maxTokenCount}, but prompt is ${newTokenCount} tokens long.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise, ths message would put us over the token limit, so don't add it.
|
||||
// if created by user, remove next message, otherwise remove only this message
|
||||
if (isCreatedByUser) {
|
||||
nextMessage.remove = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
promptBody = newPromptBody;
|
||||
currentTokenCount = newTokenCount;
|
||||
// wait for next tick to avoid blocking the event loop
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
return buildPromptBody();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
await buildPromptBody();
|
||||
|
||||
if (nextMessage.remove) {
|
||||
promptBody = promptBody.replace(nextMessage.messageString, '');
|
||||
currentTokenCount -= nextMessage.tokenCount;
|
||||
context.shift();
|
||||
}
|
||||
|
||||
const prompt = `${promptBody}${promptSuffix}`;
|
||||
// Add 2 tokens for metadata after all messages have been counted.
|
||||
currentTokenCount += 2;
|
||||
|
||||
// Use up to `this.maxContextTokens` tokens (prompt + response), but try to leave `this.maxTokens` tokens for the response.
|
||||
this.modelOptions.maxOutputTokens = Math.min(
|
||||
this.maxContextTokens - currentTokenCount,
|
||||
this.maxResponseTokens,
|
||||
);
|
||||
|
||||
return { prompt, context };
|
||||
}
|
||||
|
||||
getCompletion() {
|
||||
console.log('AnthropicClient doesn\'t use getCompletion (all handled in sendCompletion)');
|
||||
}
|
||||
|
||||
// TODO: implement abortController usage
|
||||
async sendCompletion(payload, { onProgress, abortController }) {
|
||||
if (!abortController) {
|
||||
abortController = new AbortController();
|
||||
}
|
||||
|
||||
const { signal } = abortController;
|
||||
|
||||
const modelOptions = { ...this.modelOptions };
|
||||
if (typeof onProgress === 'function') {
|
||||
modelOptions.stream = true;
|
||||
}
|
||||
|
||||
const { debug } = this.options;
|
||||
if (debug) {
|
||||
console.debug();
|
||||
console.debug(modelOptions);
|
||||
console.debug();
|
||||
}
|
||||
|
||||
const client = this.getClient();
|
||||
const metadata = {
|
||||
user_id: this.user,
|
||||
};
|
||||
|
||||
let text = '';
|
||||
const requestOptions = {
|
||||
prompt: payload,
|
||||
model: this.modelOptions.model,
|
||||
stream: this.modelOptions.stream || true,
|
||||
max_tokens_to_sample: this.modelOptions.maxOutputTokens || 1500,
|
||||
metadata,
|
||||
...modelOptions,
|
||||
};
|
||||
if (this.options.debug) {
|
||||
console.log('AnthropicClient: requestOptions');
|
||||
console.dir(requestOptions, { depth: null });
|
||||
}
|
||||
const response = await client.completions.create(requestOptions);
|
||||
|
||||
signal.addEventListener('abort', () => {
|
||||
if (this.options.debug) {
|
||||
console.log('AnthropicClient: message aborted!');
|
||||
}
|
||||
response.controller.abort();
|
||||
});
|
||||
|
||||
for await (const completion of response) {
|
||||
if (this.options.debug) {
|
||||
// Uncomment to debug message stream
|
||||
// console.debug(completion);
|
||||
}
|
||||
text += completion.completion;
|
||||
onProgress(completion.completion);
|
||||
}
|
||||
|
||||
signal.removeEventListener('abort', () => {
|
||||
if (this.options.debug) {
|
||||
console.log('AnthropicClient: message aborted!');
|
||||
}
|
||||
response.controller.abort();
|
||||
});
|
||||
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
// I commented this out because I will need to refactor this for the BaseClient/all clients
|
||||
// getMessageMapMethod() {
|
||||
// return ((message) => ({
|
||||
// author: message.isCreatedByUser ? this.userLabel : this.assistantLabel,
|
||||
// content: message?.content ?? message.text
|
||||
// })).bind(this);
|
||||
// }
|
||||
|
||||
getSaveOptions() {
|
||||
return {
|
||||
promptPrefix: this.options.promptPrefix,
|
||||
modelLabel: this.options.modelLabel,
|
||||
...this.modelOptions,
|
||||
};
|
||||
}
|
||||
|
||||
getBuildMessagesOptions() {
|
||||
if (this.options.debug) {
|
||||
console.log('AnthropicClient doesn\'t use getBuildMessagesOptions');
|
||||
}
|
||||
}
|
||||
|
||||
static getTokenizer(encoding, isModelName = false, extendSpecialTokens = {}) {
|
||||
if (tokenizersCache[encoding]) {
|
||||
return tokenizersCache[encoding];
|
||||
}
|
||||
let tokenizer;
|
||||
if (isModelName) {
|
||||
tokenizer = encodingForModel(encoding, extendSpecialTokens);
|
||||
} else {
|
||||
tokenizer = getEncoding(encoding, extendSpecialTokens);
|
||||
}
|
||||
tokenizersCache[encoding] = tokenizer;
|
||||
return tokenizer;
|
||||
}
|
||||
|
||||
getTokenCount(text) {
|
||||
return this.gptEncoder.encode(text, 'all').length;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AnthropicClient;
|
||||
561
api/app/clients/BaseClient.js
Normal file
561
api/app/clients/BaseClient.js
Normal file
@@ -0,0 +1,561 @@
|
||||
const crypto = require('crypto');
|
||||
const TextStream = require('./TextStream');
|
||||
const { RecursiveCharacterTextSplitter } = require('langchain/text_splitter');
|
||||
const { ChatOpenAI } = require('langchain/chat_models/openai');
|
||||
const { loadSummarizationChain } = require('langchain/chains');
|
||||
const { refinePrompt } = require('./prompts/refinePrompt');
|
||||
const { getConvo, getMessages, saveMessage, updateMessage, saveConvo } = require('../../models');
|
||||
|
||||
class BaseClient {
|
||||
constructor(apiKey, options = {}) {
|
||||
this.apiKey = apiKey;
|
||||
this.sender = options.sender || 'AI';
|
||||
this.contextStrategy = null;
|
||||
this.currentDateString = new Date().toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
setOptions() {
|
||||
throw new Error('Method \'setOptions\' must be implemented.');
|
||||
}
|
||||
|
||||
getCompletion() {
|
||||
throw new Error('Method \'getCompletion\' must be implemented.');
|
||||
}
|
||||
|
||||
async sendCompletion() {
|
||||
throw new Error('Method \'sendCompletion\' must be implemented.');
|
||||
}
|
||||
|
||||
getSaveOptions() {
|
||||
throw new Error('Subclasses must implement getSaveOptions');
|
||||
}
|
||||
|
||||
async buildMessages() {
|
||||
throw new Error('Subclasses must implement buildMessages');
|
||||
}
|
||||
|
||||
getBuildMessagesOptions() {
|
||||
throw new Error('Subclasses must implement getBuildMessagesOptions');
|
||||
}
|
||||
|
||||
async generateTextStream(text, onProgress, options = {}) {
|
||||
const stream = new TextStream(text, options);
|
||||
await stream.processTextStream(onProgress);
|
||||
}
|
||||
|
||||
async setMessageOptions(opts = {}) {
|
||||
if (opts && typeof opts === 'object') {
|
||||
this.setOptions(opts);
|
||||
}
|
||||
const user = opts.user || null;
|
||||
const conversationId = opts.conversationId || crypto.randomUUID();
|
||||
const parentMessageId = opts.parentMessageId || '00000000-0000-0000-0000-000000000000';
|
||||
const userMessageId = opts.overrideParentMessageId || crypto.randomUUID();
|
||||
const responseMessageId = crypto.randomUUID();
|
||||
const saveOptions = this.getSaveOptions();
|
||||
this.abortController = opts.abortController || new AbortController();
|
||||
this.currentMessages = (await this.loadHistory(conversationId, parentMessageId)) ?? [];
|
||||
|
||||
return {
|
||||
...opts,
|
||||
user,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
userMessageId,
|
||||
responseMessageId,
|
||||
saveOptions,
|
||||
};
|
||||
}
|
||||
|
||||
createUserMessage({ messageId, parentMessageId, conversationId, text }) {
|
||||
const userMessage = {
|
||||
messageId,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
sender: 'User',
|
||||
text,
|
||||
isCreatedByUser: true,
|
||||
};
|
||||
return userMessage;
|
||||
}
|
||||
|
||||
async handleStartMethods(message, opts) {
|
||||
const { user, conversationId, parentMessageId, userMessageId, responseMessageId, saveOptions } =
|
||||
await this.setMessageOptions(opts);
|
||||
|
||||
const userMessage = this.createUserMessage({
|
||||
messageId: userMessageId,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
text: message,
|
||||
});
|
||||
|
||||
if (typeof opts?.getIds === 'function') {
|
||||
opts.getIds({
|
||||
userMessage,
|
||||
conversationId,
|
||||
responseMessageId,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof opts?.onStart === 'function') {
|
||||
opts.onStart(userMessage);
|
||||
}
|
||||
|
||||
return {
|
||||
...opts,
|
||||
user,
|
||||
conversationId,
|
||||
responseMessageId,
|
||||
saveOptions,
|
||||
userMessage,
|
||||
};
|
||||
}
|
||||
|
||||
addInstructions(messages, instructions) {
|
||||
const payload = [];
|
||||
if (!instructions) {
|
||||
return messages;
|
||||
}
|
||||
if (messages.length > 1) {
|
||||
payload.push(...messages.slice(0, -1));
|
||||
}
|
||||
|
||||
payload.push(instructions);
|
||||
|
||||
if (messages.length > 0) {
|
||||
payload.push(messages[messages.length - 1]);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
async handleTokenCountMap(tokenCountMap) {
|
||||
if (this.currentMessages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.currentMessages.length; i++) {
|
||||
// Skip the last message, which is the user message.
|
||||
if (i === this.currentMessages.length - 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
const message = this.currentMessages[i];
|
||||
const { messageId } = message;
|
||||
const update = {};
|
||||
|
||||
if (messageId === tokenCountMap.refined?.messageId) {
|
||||
if (this.options.debug) {
|
||||
console.debug(`Adding refined props to ${messageId}.`);
|
||||
}
|
||||
|
||||
update.refinedMessageText = tokenCountMap.refined.content;
|
||||
update.refinedTokenCount = tokenCountMap.refined.tokenCount;
|
||||
}
|
||||
|
||||
if (message.tokenCount && !update.refinedTokenCount) {
|
||||
if (this.options.debug) {
|
||||
console.debug(`Skipping ${messageId}: already had a token count.`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const tokenCount = tokenCountMap[messageId];
|
||||
if (tokenCount) {
|
||||
message.tokenCount = tokenCount;
|
||||
update.tokenCount = tokenCount;
|
||||
await this.updateMessageInDatabase({ messageId, ...update });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
concatenateMessages(messages) {
|
||||
return messages.reduce((acc, message) => {
|
||||
const nameOrRole = message.name ?? message.role;
|
||||
return acc + `${nameOrRole}:\n${message.content}\n\n`;
|
||||
}, '');
|
||||
}
|
||||
|
||||
async refineMessages(messagesToRefine, remainingContextTokens) {
|
||||
const model = new ChatOpenAI({ temperature: 0 });
|
||||
const chain = loadSummarizationChain(model, {
|
||||
type: 'refine',
|
||||
verbose: this.options.debug,
|
||||
refinePrompt,
|
||||
});
|
||||
const splitter = new RecursiveCharacterTextSplitter({
|
||||
chunkSize: 1500,
|
||||
chunkOverlap: 100,
|
||||
});
|
||||
const userMessages = this.concatenateMessages(
|
||||
messagesToRefine.filter((m) => m.role === 'user'),
|
||||
);
|
||||
const assistantMessages = this.concatenateMessages(
|
||||
messagesToRefine.filter((m) => m.role !== 'user'),
|
||||
);
|
||||
const userDocs = await splitter.createDocuments([userMessages], [], {
|
||||
chunkHeader: 'DOCUMENT NAME: User Message\n\n---\n\n',
|
||||
appendChunkOverlapHeader: true,
|
||||
});
|
||||
const assistantDocs = await splitter.createDocuments([assistantMessages], [], {
|
||||
chunkHeader: 'DOCUMENT NAME: Assistant Message\n\n---\n\n',
|
||||
appendChunkOverlapHeader: true,
|
||||
});
|
||||
// const chunkSize = Math.round(concatenatedMessages.length / 512);
|
||||
const input_documents = userDocs.concat(assistantDocs);
|
||||
if (this.options.debug) {
|
||||
console.debug('Refining messages...');
|
||||
}
|
||||
try {
|
||||
const res = await chain.call({
|
||||
input_documents,
|
||||
signal: this.abortController.signal,
|
||||
});
|
||||
|
||||
const refinedMessage = {
|
||||
role: 'assistant',
|
||||
content: res.output_text,
|
||||
tokenCount: this.getTokenCount(res.output_text),
|
||||
};
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('Refined messages', refinedMessage);
|
||||
console.debug(
|
||||
`remainingContextTokens: ${remainingContextTokens}, after refining: ${
|
||||
remainingContextTokens - refinedMessage.tokenCount
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
return refinedMessage;
|
||||
} catch (e) {
|
||||
console.error('Error refining messages');
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method processes an array of messages and returns a context of messages that fit within a token limit.
|
||||
* It iterates over the messages from newest to oldest, adding them to the context until the token limit is reached.
|
||||
* 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.
|
||||
* 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.
|
||||
* The method also includes a mechanism to avoid blocking the event loop by waiting for the next tick after each iteration.
|
||||
*
|
||||
* @param {Array} messages - An array of messages, each with a `tokenCount` property. The messages should be ordered from oldest to newest.
|
||||
* @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.
|
||||
*/
|
||||
async getMessagesWithinTokenLimit(messages) {
|
||||
let currentTokenCount = 0;
|
||||
let context = [];
|
||||
let messagesToRefine = [];
|
||||
let refineIndex = -1;
|
||||
let remainingContextTokens = this.maxContextTokens;
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i];
|
||||
const newTokenCount = currentTokenCount + message.tokenCount;
|
||||
const exceededLimit = newTokenCount > this.maxContextTokens;
|
||||
let shouldRefine = exceededLimit && this.shouldRefineContext;
|
||||
let refineNextMessage = i !== 0 && i !== 1 && context.length > 0;
|
||||
|
||||
if (shouldRefine) {
|
||||
messagesToRefine.push(message);
|
||||
|
||||
if (refineIndex === -1) {
|
||||
refineIndex = i;
|
||||
}
|
||||
|
||||
if (refineNextMessage) {
|
||||
refineIndex = i + 1;
|
||||
const removedMessage = context.pop();
|
||||
messagesToRefine.push(removedMessage);
|
||||
currentTokenCount -= removedMessage.tokenCount;
|
||||
remainingContextTokens = this.maxContextTokens - currentTokenCount;
|
||||
refineNextMessage = false;
|
||||
}
|
||||
|
||||
continue;
|
||||
} else if (exceededLimit) {
|
||||
break;
|
||||
}
|
||||
|
||||
context.push(message);
|
||||
currentTokenCount = newTokenCount;
|
||||
remainingContextTokens = this.maxContextTokens - currentTokenCount;
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
}
|
||||
|
||||
return {
|
||||
context: context.reverse(),
|
||||
remainingContextTokens,
|
||||
messagesToRefine: messagesToRefine.reverse(),
|
||||
refineIndex,
|
||||
};
|
||||
}
|
||||
|
||||
async handleContextStrategy({ instructions, orderedMessages, formattedMessages }) {
|
||||
let payload = this.addInstructions(formattedMessages, instructions);
|
||||
let orderedWithInstructions = this.addInstructions(orderedMessages, instructions);
|
||||
let { context, remainingContextTokens, messagesToRefine, refineIndex } =
|
||||
await this.getMessagesWithinTokenLimit(payload);
|
||||
|
||||
payload = context;
|
||||
let refinedMessage;
|
||||
|
||||
// if (messagesToRefine.length > 0) {
|
||||
// refinedMessage = await this.refineMessages(messagesToRefine, remainingContextTokens);
|
||||
// payload.unshift(refinedMessage);
|
||||
// remainingContextTokens -= refinedMessage.tokenCount;
|
||||
// }
|
||||
// if (remainingContextTokens <= instructions?.tokenCount) {
|
||||
// if (this.options.debug) {
|
||||
// console.debug(`Remaining context (${remainingContextTokens}) is less than instructions token count: ${instructions.tokenCount}`);
|
||||
// }
|
||||
|
||||
// ({ context, remainingContextTokens, messagesToRefine, refineIndex } = await this.getMessagesWithinTokenLimit(payload));
|
||||
// payload = context;
|
||||
// }
|
||||
|
||||
// Calculate the difference in length to determine how many messages were discarded if any
|
||||
let diff = orderedWithInstructions.length - payload.length;
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('<---------------------------------DIFF--------------------------------->');
|
||||
console.debug(
|
||||
`Difference between payload (${payload.length}) and orderedWithInstructions (${orderedWithInstructions.length}): ${diff}`,
|
||||
);
|
||||
console.debug(
|
||||
'remainingContextTokens, this.maxContextTokens (1/2)',
|
||||
remainingContextTokens,
|
||||
this.maxContextTokens,
|
||||
);
|
||||
}
|
||||
|
||||
// If the difference is positive, slice the orderedWithInstructions array
|
||||
if (diff > 0) {
|
||||
orderedWithInstructions = orderedWithInstructions.slice(diff);
|
||||
}
|
||||
|
||||
if (messagesToRefine.length > 0) {
|
||||
refinedMessage = await this.refineMessages(messagesToRefine, remainingContextTokens);
|
||||
payload.unshift(refinedMessage);
|
||||
remainingContextTokens -= refinedMessage.tokenCount;
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug(
|
||||
'remainingContextTokens, this.maxContextTokens (2/2)',
|
||||
remainingContextTokens,
|
||||
this.maxContextTokens,
|
||||
);
|
||||
}
|
||||
|
||||
let tokenCountMap = orderedWithInstructions.reduce((map, message, index) => {
|
||||
if (!message.messageId) {
|
||||
return map;
|
||||
}
|
||||
|
||||
if (index === refineIndex) {
|
||||
map.refined = { ...refinedMessage, messageId: message.messageId };
|
||||
}
|
||||
|
||||
map[message.messageId] = payload[index].tokenCount;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
const promptTokens = this.maxContextTokens - remainingContextTokens;
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('<-------------------------PAYLOAD/TOKEN COUNT MAP------------------------->');
|
||||
console.debug('Payload:', payload);
|
||||
console.debug('Token Count Map:', tokenCountMap);
|
||||
console.debug('Prompt Tokens', promptTokens, remainingContextTokens, this.maxContextTokens);
|
||||
}
|
||||
|
||||
return { payload, tokenCountMap, promptTokens, messages: orderedWithInstructions };
|
||||
}
|
||||
|
||||
async sendMessage(message, opts = {}) {
|
||||
const { user, conversationId, responseMessageId, saveOptions, userMessage } =
|
||||
await this.handleStartMethods(message, opts);
|
||||
|
||||
this.user = user;
|
||||
// It's not necessary to push to currentMessages
|
||||
// depending on subclass implementation of handling messages
|
||||
this.currentMessages.push(userMessage);
|
||||
|
||||
let {
|
||||
prompt: payload,
|
||||
tokenCountMap,
|
||||
promptTokens,
|
||||
} = await this.buildMessages(
|
||||
this.currentMessages,
|
||||
// When the userMessage is pushed to currentMessages, the parentMessage is the userMessageId.
|
||||
// this only matters when buildMessages is utilizing the parentMessageId, and may vary on implementation
|
||||
userMessage.messageId,
|
||||
this.getBuildMessagesOptions(opts),
|
||||
);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('payload');
|
||||
console.debug(payload);
|
||||
}
|
||||
|
||||
if (tokenCountMap) {
|
||||
console.dir(tokenCountMap, { depth: null });
|
||||
if (tokenCountMap[userMessage.messageId]) {
|
||||
userMessage.tokenCount = tokenCountMap[userMessage.messageId];
|
||||
console.log('userMessage.tokenCount', userMessage.tokenCount);
|
||||
console.log('userMessage', userMessage);
|
||||
}
|
||||
|
||||
payload = payload.map((message) => {
|
||||
const messageWithoutTokenCount = message;
|
||||
delete messageWithoutTokenCount.tokenCount;
|
||||
return messageWithoutTokenCount;
|
||||
});
|
||||
this.handleTokenCountMap(tokenCountMap);
|
||||
}
|
||||
|
||||
await this.saveMessageToDatabase(userMessage, saveOptions, user);
|
||||
const responseMessage = {
|
||||
messageId: responseMessageId,
|
||||
conversationId,
|
||||
parentMessageId: userMessage.messageId,
|
||||
isCreatedByUser: false,
|
||||
model: this.modelOptions.model,
|
||||
sender: this.sender,
|
||||
text: await this.sendCompletion(payload, opts),
|
||||
promptTokens,
|
||||
};
|
||||
|
||||
if (tokenCountMap && this.getTokenCountForResponse) {
|
||||
responseMessage.tokenCount = this.getTokenCountForResponse(responseMessage);
|
||||
responseMessage.completionTokens = responseMessage.tokenCount;
|
||||
}
|
||||
await this.saveMessageToDatabase(responseMessage, saveOptions, user);
|
||||
delete responseMessage.tokenCount;
|
||||
return responseMessage;
|
||||
}
|
||||
|
||||
async getConversation(conversationId, user = null) {
|
||||
return await getConvo(user, conversationId);
|
||||
}
|
||||
|
||||
async loadHistory(conversationId, parentMessageId = null) {
|
||||
if (this.options.debug) {
|
||||
console.debug('Loading history for conversation', conversationId, parentMessageId);
|
||||
}
|
||||
|
||||
const messages = (await getMessages({ conversationId })) || [];
|
||||
|
||||
if (messages.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let mapMethod = null;
|
||||
if (this.getMessageMapMethod) {
|
||||
mapMethod = this.getMessageMapMethod();
|
||||
}
|
||||
|
||||
return this.constructor.getMessagesForConversation(messages, parentMessageId, mapMethod);
|
||||
}
|
||||
|
||||
async saveMessageToDatabase(message, endpointOptions, user = null) {
|
||||
await saveMessage({ ...message, unfinished: false, cancelled: false });
|
||||
await saveConvo(user, {
|
||||
conversationId: message.conversationId,
|
||||
endpoint: this.options.endpoint,
|
||||
...endpointOptions,
|
||||
});
|
||||
}
|
||||
|
||||
async updateMessageInDatabase(message) {
|
||||
await updateMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through messages, building an array based on the parentMessageId.
|
||||
* Each message has an id and a parentMessageId. The parentMessageId is the id of the message that this message is a reply to.
|
||||
* @param messages
|
||||
* @param parentMessageId
|
||||
* @returns {*[]} An array containing the messages in the order they should be displayed, starting with the root message.
|
||||
*/
|
||||
static getMessagesForConversation(messages, parentMessageId, mapMethod = null) {
|
||||
if (!messages || messages.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const orderedMessages = [];
|
||||
let currentMessageId = parentMessageId;
|
||||
while (currentMessageId) {
|
||||
const message = messages.find((msg) => {
|
||||
const messageId = msg.messageId ?? msg.id;
|
||||
return messageId === currentMessageId;
|
||||
});
|
||||
if (!message) {
|
||||
break;
|
||||
}
|
||||
orderedMessages.unshift(message);
|
||||
currentMessageId = message.parentMessageId;
|
||||
}
|
||||
|
||||
if (mapMethod) {
|
||||
return orderedMessages.map(mapMethod);
|
||||
}
|
||||
|
||||
return orderedMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm adapted from "6. Counting tokens for chat API calls" of
|
||||
* https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
|
||||
*
|
||||
* An additional 2 tokens need to be added for metadata after all messages have been counted.
|
||||
*
|
||||
* @param {*} message
|
||||
*/
|
||||
getTokenCountForMessage(message) {
|
||||
let tokensPerMessage;
|
||||
let nameAdjustment;
|
||||
if (this.modelOptions.model.startsWith('gpt-4')) {
|
||||
tokensPerMessage = 3;
|
||||
nameAdjustment = 1;
|
||||
} else {
|
||||
tokensPerMessage = 4;
|
||||
nameAdjustment = -1;
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('getTokenCountForMessage', message);
|
||||
}
|
||||
|
||||
// Map each property of the message to the number of tokens it contains
|
||||
const propertyTokenCounts = Object.entries(message).map(([key, value]) => {
|
||||
if (key === 'tokenCount' || typeof value !== 'string') {
|
||||
return 0;
|
||||
}
|
||||
// Count the number of tokens in the property value
|
||||
const numTokens = this.getTokenCount(value);
|
||||
|
||||
// Adjust by `nameAdjustment` tokens if the property key is 'name'
|
||||
const adjustment = key === 'name' ? nameAdjustment : 0;
|
||||
return numTokens + adjustment;
|
||||
});
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('propertyTokenCounts', propertyTokenCounts);
|
||||
}
|
||||
|
||||
// Sum the number of tokens in all properties and add `tokensPerMessage` for metadata
|
||||
return propertyTokenCounts.reduce((a, b) => a + b, tokensPerMessage);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseClient;
|
||||
587
api/app/clients/ChatGPTClient.js
Normal file
587
api/app/clients/ChatGPTClient.js
Normal file
@@ -0,0 +1,587 @@
|
||||
const crypto = require('crypto');
|
||||
const Keyv = require('keyv');
|
||||
const {
|
||||
encoding_for_model: encodingForModel,
|
||||
get_encoding: getEncoding,
|
||||
} = require('@dqbd/tiktoken');
|
||||
const { fetchEventSource } = require('@waylaidwanderer/fetch-event-source');
|
||||
const { Agent, ProxyAgent } = require('undici');
|
||||
const BaseClient = require('./BaseClient');
|
||||
|
||||
const CHATGPT_MODEL = 'gpt-3.5-turbo';
|
||||
const tokenizersCache = {};
|
||||
|
||||
class ChatGPTClient extends BaseClient {
|
||||
constructor(apiKey, options = {}, cacheOptions = {}) {
|
||||
super(apiKey, options, cacheOptions);
|
||||
|
||||
cacheOptions.namespace = cacheOptions.namespace || 'chatgpt';
|
||||
this.conversationsCache = new Keyv(cacheOptions);
|
||||
this.setOptions(options);
|
||||
}
|
||||
|
||||
setOptions(options) {
|
||||
if (this.options && !this.options.replaceOptions) {
|
||||
// nested options aren't spread properly, so we need to do this manually
|
||||
this.options.modelOptions = {
|
||||
...this.options.modelOptions,
|
||||
...options.modelOptions,
|
||||
};
|
||||
delete options.modelOptions;
|
||||
// now we can merge options
|
||||
this.options = {
|
||||
...this.options,
|
||||
...options,
|
||||
};
|
||||
} else {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
if (this.options.openaiApiKey) {
|
||||
this.apiKey = this.options.openaiApiKey;
|
||||
}
|
||||
|
||||
const modelOptions = this.options.modelOptions || {};
|
||||
this.modelOptions = {
|
||||
...modelOptions,
|
||||
// set some good defaults (check for undefined in some cases because they may be 0)
|
||||
model: modelOptions.model || CHATGPT_MODEL,
|
||||
temperature: typeof modelOptions.temperature === 'undefined' ? 0.8 : modelOptions.temperature,
|
||||
top_p: typeof modelOptions.top_p === 'undefined' ? 1 : modelOptions.top_p,
|
||||
presence_penalty:
|
||||
typeof modelOptions.presence_penalty === 'undefined' ? 1 : modelOptions.presence_penalty,
|
||||
stop: modelOptions.stop,
|
||||
};
|
||||
|
||||
this.isChatGptModel = this.modelOptions.model.startsWith('gpt-');
|
||||
const { isChatGptModel } = this;
|
||||
this.isUnofficialChatGptModel =
|
||||
this.modelOptions.model.startsWith('text-chat') ||
|
||||
this.modelOptions.model.startsWith('text-davinci-002-render');
|
||||
const { isUnofficialChatGptModel } = this;
|
||||
|
||||
// Davinci models have a max context length of 4097 tokens.
|
||||
this.maxContextTokens = this.options.maxContextTokens || (isChatGptModel ? 4095 : 4097);
|
||||
// I decided to reserve 1024 tokens for the response.
|
||||
// The max prompt tokens is determined by the max context tokens minus the max response tokens.
|
||||
// Earlier messages will be dropped until the prompt is within the limit.
|
||||
this.maxResponseTokens = this.modelOptions.max_tokens || 1024;
|
||||
this.maxPromptTokens =
|
||||
this.options.maxPromptTokens || this.maxContextTokens - this.maxResponseTokens;
|
||||
|
||||
if (this.maxPromptTokens + this.maxResponseTokens > this.maxContextTokens) {
|
||||
throw new Error(
|
||||
`maxPromptTokens + max_tokens (${this.maxPromptTokens} + ${this.maxResponseTokens} = ${
|
||||
this.maxPromptTokens + this.maxResponseTokens
|
||||
}) must be less than or equal to maxContextTokens (${this.maxContextTokens})`,
|
||||
);
|
||||
}
|
||||
|
||||
this.userLabel = this.options.userLabel || 'User';
|
||||
this.chatGptLabel = this.options.chatGptLabel || 'ChatGPT';
|
||||
|
||||
if (isChatGptModel) {
|
||||
// Use these faux tokens to help the AI understand the context since we are building the chat log ourselves.
|
||||
// Trying to use "<|im_start|>" causes the AI to still generate "<" or "<|" at the end sometimes for some reason,
|
||||
// without tripping the stop sequences, so I'm using "||>" instead.
|
||||
this.startToken = '||>';
|
||||
this.endToken = '';
|
||||
this.gptEncoder = this.constructor.getTokenizer('cl100k_base');
|
||||
} else if (isUnofficialChatGptModel) {
|
||||
this.startToken = '<|im_start|>';
|
||||
this.endToken = '<|im_end|>';
|
||||
this.gptEncoder = this.constructor.getTokenizer('text-davinci-003', true, {
|
||||
'<|im_start|>': 100264,
|
||||
'<|im_end|>': 100265,
|
||||
});
|
||||
} else {
|
||||
// Previously I was trying to use "<|endoftext|>" but there seems to be some bug with OpenAI's token counting
|
||||
// system that causes only the first "<|endoftext|>" to be counted as 1 token, and the rest are not treated
|
||||
// as a single token. So we're using this instead.
|
||||
this.startToken = '||>';
|
||||
this.endToken = '';
|
||||
try {
|
||||
this.gptEncoder = this.constructor.getTokenizer(this.modelOptions.model, true);
|
||||
} catch {
|
||||
this.gptEncoder = this.constructor.getTokenizer('text-davinci-003', true);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.modelOptions.stop) {
|
||||
const stopTokens = [this.startToken];
|
||||
if (this.endToken && this.endToken !== this.startToken) {
|
||||
stopTokens.push(this.endToken);
|
||||
}
|
||||
stopTokens.push(`\n${this.userLabel}:`);
|
||||
stopTokens.push('<|diff_marker|>');
|
||||
// I chose not to do one for `chatGptLabel` because I've never seen it happen
|
||||
this.modelOptions.stop = stopTokens;
|
||||
}
|
||||
|
||||
if (this.options.reverseProxyUrl) {
|
||||
this.completionsUrl = this.options.reverseProxyUrl;
|
||||
} else if (isChatGptModel) {
|
||||
this.completionsUrl = 'https://api.openai.com/v1/chat/completions';
|
||||
} else {
|
||||
this.completionsUrl = 'https://api.openai.com/v1/completions';
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
static getTokenizer(encoding, isModelName = false, extendSpecialTokens = {}) {
|
||||
if (tokenizersCache[encoding]) {
|
||||
return tokenizersCache[encoding];
|
||||
}
|
||||
let tokenizer;
|
||||
if (isModelName) {
|
||||
tokenizer = encodingForModel(encoding, extendSpecialTokens);
|
||||
} else {
|
||||
tokenizer = getEncoding(encoding, extendSpecialTokens);
|
||||
}
|
||||
tokenizersCache[encoding] = tokenizer;
|
||||
return tokenizer;
|
||||
}
|
||||
|
||||
async getCompletion(input, onProgress, abortController = null) {
|
||||
if (!abortController) {
|
||||
abortController = new AbortController();
|
||||
}
|
||||
const modelOptions = { ...this.modelOptions };
|
||||
if (typeof onProgress === 'function') {
|
||||
modelOptions.stream = true;
|
||||
}
|
||||
if (this.isChatGptModel) {
|
||||
modelOptions.messages = input;
|
||||
} else {
|
||||
modelOptions.prompt = input;
|
||||
}
|
||||
const { debug } = this.options;
|
||||
const url = this.completionsUrl;
|
||||
if (debug) {
|
||||
console.debug();
|
||||
console.debug(url);
|
||||
console.debug(modelOptions);
|
||||
console.debug();
|
||||
}
|
||||
const opts = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(modelOptions),
|
||||
dispatcher: new Agent({
|
||||
bodyTimeout: 0,
|
||||
headersTimeout: 0,
|
||||
}),
|
||||
};
|
||||
|
||||
if (this.apiKey && this.options.azure) {
|
||||
opts.headers['api-key'] = this.apiKey;
|
||||
} else if (this.apiKey) {
|
||||
opts.headers.Authorization = `Bearer ${this.apiKey}`;
|
||||
}
|
||||
|
||||
if (this.options.headers) {
|
||||
opts.headers = { ...opts.headers, ...this.options.headers };
|
||||
}
|
||||
|
||||
if (this.options.proxy) {
|
||||
opts.dispatcher = new ProxyAgent(this.options.proxy);
|
||||
}
|
||||
|
||||
if (modelOptions.stream) {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
let done = false;
|
||||
await fetchEventSource(url, {
|
||||
...opts,
|
||||
signal: abortController.signal,
|
||||
async onopen(response) {
|
||||
if (response.status === 200) {
|
||||
return;
|
||||
}
|
||||
if (debug) {
|
||||
console.debug(response);
|
||||
}
|
||||
let error;
|
||||
try {
|
||||
const body = await response.text();
|
||||
error = new Error(`Failed to send message. HTTP ${response.status} - ${body}`);
|
||||
error.status = response.status;
|
||||
error.json = JSON.parse(body);
|
||||
} catch {
|
||||
error = error || new Error(`Failed to send message. HTTP ${response.status}`);
|
||||
}
|
||||
throw error;
|
||||
},
|
||||
onclose() {
|
||||
if (debug) {
|
||||
console.debug('Server closed the connection unexpectedly, returning...');
|
||||
}
|
||||
// workaround for private API not sending [DONE] event
|
||||
if (!done) {
|
||||
onProgress('[DONE]');
|
||||
abortController.abort();
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
onerror(err) {
|
||||
if (debug) {
|
||||
console.debug(err);
|
||||
}
|
||||
// rethrow to stop the operation
|
||||
throw err;
|
||||
},
|
||||
onmessage(message) {
|
||||
if (debug) {
|
||||
// console.debug(message);
|
||||
}
|
||||
if (!message.data || message.event === 'ping') {
|
||||
return;
|
||||
}
|
||||
if (message.data === '[DONE]') {
|
||||
onProgress('[DONE]');
|
||||
abortController.abort();
|
||||
resolve();
|
||||
done = true;
|
||||
return;
|
||||
}
|
||||
onProgress(JSON.parse(message.data));
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
const response = await fetch(url, {
|
||||
...opts,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
const body = await response.text();
|
||||
const error = new Error(`Failed to send message. HTTP ${response.status} - ${body}`);
|
||||
error.status = response.status;
|
||||
try {
|
||||
error.json = JSON.parse(body);
|
||||
} catch {
|
||||
error.body = body;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async generateTitle(userMessage, botMessage) {
|
||||
const instructionsPayload = {
|
||||
role: 'system',
|
||||
content: `Write an extremely concise subtitle for this conversation with no more than a few words. All words should be capitalized. Exclude punctuation.
|
||||
|
||||
||>Message:
|
||||
${userMessage.message}
|
||||
||>Response:
|
||||
${botMessage.message}
|
||||
|
||||
||>Title:`,
|
||||
};
|
||||
|
||||
const titleGenClientOptions = JSON.parse(JSON.stringify(this.options));
|
||||
titleGenClientOptions.modelOptions = {
|
||||
model: 'gpt-3.5-turbo',
|
||||
temperature: 0,
|
||||
presence_penalty: 0,
|
||||
frequency_penalty: 0,
|
||||
};
|
||||
const titleGenClient = new ChatGPTClient(this.apiKey, titleGenClientOptions);
|
||||
const result = await titleGenClient.getCompletion([instructionsPayload], null);
|
||||
// remove any non-alphanumeric characters, replace multiple spaces with 1, and then trim
|
||||
return result.choices[0].message.content
|
||||
.replace(/[^a-zA-Z0-9' ]/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
async sendMessage(message, opts = {}) {
|
||||
if (opts.clientOptions && typeof opts.clientOptions === 'object') {
|
||||
this.setOptions(opts.clientOptions);
|
||||
}
|
||||
|
||||
const conversationId = opts.conversationId || crypto.randomUUID();
|
||||
const parentMessageId = opts.parentMessageId || crypto.randomUUID();
|
||||
|
||||
let conversation =
|
||||
typeof opts.conversation === 'object'
|
||||
? opts.conversation
|
||||
: await this.conversationsCache.get(conversationId);
|
||||
|
||||
let isNewConversation = false;
|
||||
if (!conversation) {
|
||||
conversation = {
|
||||
messages: [],
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
isNewConversation = true;
|
||||
}
|
||||
|
||||
const shouldGenerateTitle = opts.shouldGenerateTitle && isNewConversation;
|
||||
|
||||
const userMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
parentMessageId,
|
||||
role: 'User',
|
||||
message,
|
||||
};
|
||||
conversation.messages.push(userMessage);
|
||||
|
||||
// Doing it this way instead of having each message be a separate element in the array seems to be more reliable,
|
||||
// especially when it comes to keeping the AI in character. It also seems to improve coherency and context retention.
|
||||
const { prompt: payload, context } = await this.buildPrompt(
|
||||
conversation.messages,
|
||||
userMessage.id,
|
||||
{
|
||||
isChatGptModel: this.isChatGptModel,
|
||||
promptPrefix: opts.promptPrefix,
|
||||
},
|
||||
);
|
||||
|
||||
if (this.options.keepNecessaryMessagesOnly) {
|
||||
conversation.messages = context;
|
||||
}
|
||||
|
||||
let reply = '';
|
||||
let result = null;
|
||||
if (typeof opts.onProgress === 'function') {
|
||||
await this.getCompletion(
|
||||
payload,
|
||||
(progressMessage) => {
|
||||
if (progressMessage === '[DONE]') {
|
||||
return;
|
||||
}
|
||||
const token = this.isChatGptModel
|
||||
? progressMessage.choices[0].delta.content
|
||||
: progressMessage.choices[0].text;
|
||||
// first event's delta content is always undefined
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
if (this.options.debug) {
|
||||
console.debug(token);
|
||||
}
|
||||
if (token === this.endToken) {
|
||||
return;
|
||||
}
|
||||
opts.onProgress(token);
|
||||
reply += token;
|
||||
},
|
||||
opts.abortController || new AbortController(),
|
||||
);
|
||||
} else {
|
||||
result = await this.getCompletion(
|
||||
payload,
|
||||
null,
|
||||
opts.abortController || new AbortController(),
|
||||
);
|
||||
if (this.options.debug) {
|
||||
console.debug(JSON.stringify(result));
|
||||
}
|
||||
if (this.isChatGptModel) {
|
||||
reply = result.choices[0].message.content;
|
||||
} else {
|
||||
reply = result.choices[0].text.replace(this.endToken, '');
|
||||
}
|
||||
}
|
||||
|
||||
// avoids some rendering issues when using the CLI app
|
||||
if (this.options.debug) {
|
||||
console.debug();
|
||||
}
|
||||
|
||||
reply = reply.trim();
|
||||
|
||||
const replyMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
parentMessageId: userMessage.id,
|
||||
role: 'ChatGPT',
|
||||
message: reply,
|
||||
};
|
||||
conversation.messages.push(replyMessage);
|
||||
|
||||
const returnData = {
|
||||
response: replyMessage.message,
|
||||
conversationId,
|
||||
parentMessageId: replyMessage.parentMessageId,
|
||||
messageId: replyMessage.id,
|
||||
details: result || {},
|
||||
};
|
||||
|
||||
if (shouldGenerateTitle) {
|
||||
conversation.title = await this.generateTitle(userMessage, replyMessage);
|
||||
returnData.title = conversation.title;
|
||||
}
|
||||
|
||||
await this.conversationsCache.set(conversationId, conversation);
|
||||
|
||||
if (this.options.returnConversation) {
|
||||
returnData.conversation = conversation;
|
||||
}
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
async buildPrompt(messages, parentMessageId, { isChatGptModel = false, promptPrefix = null }) {
|
||||
const orderedMessages = this.constructor.getMessagesForConversation(messages, parentMessageId);
|
||||
|
||||
promptPrefix = (promptPrefix || this.options.promptPrefix || '').trim();
|
||||
if (promptPrefix) {
|
||||
// If the prompt prefix doesn't end with the end token, add it.
|
||||
if (!promptPrefix.endsWith(`${this.endToken}`)) {
|
||||
promptPrefix = `${promptPrefix.trim()}${this.endToken}\n\n`;
|
||||
}
|
||||
promptPrefix = `${this.startToken}Instructions:\n${promptPrefix}`;
|
||||
} else {
|
||||
const currentDateString = new Date().toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
promptPrefix = `${this.startToken}Instructions:\nYou are ChatGPT, a large language model trained by OpenAI. Respond conversationally.\nCurrent date: ${currentDateString}${this.endToken}\n\n`;
|
||||
}
|
||||
|
||||
const promptSuffix = `${this.startToken}${this.chatGptLabel}:\n`; // Prompt ChatGPT to respond.
|
||||
|
||||
const instructionsPayload = {
|
||||
role: 'system',
|
||||
name: 'instructions',
|
||||
content: promptPrefix,
|
||||
};
|
||||
|
||||
const messagePayload = {
|
||||
role: 'system',
|
||||
content: promptSuffix,
|
||||
};
|
||||
|
||||
let currentTokenCount;
|
||||
if (isChatGptModel) {
|
||||
currentTokenCount =
|
||||
this.getTokenCountForMessage(instructionsPayload) +
|
||||
this.getTokenCountForMessage(messagePayload);
|
||||
} else {
|
||||
currentTokenCount = this.getTokenCount(`${promptPrefix}${promptSuffix}`);
|
||||
}
|
||||
let promptBody = '';
|
||||
const maxTokenCount = this.maxPromptTokens;
|
||||
|
||||
const context = [];
|
||||
|
||||
// Iterate backwards through the messages, adding them to the prompt until we reach the max token count.
|
||||
// Do this within a recursive async function so that it doesn't block the event loop for too long.
|
||||
const buildPromptBody = async () => {
|
||||
if (currentTokenCount < maxTokenCount && orderedMessages.length > 0) {
|
||||
const message = orderedMessages.pop();
|
||||
const roleLabel =
|
||||
message?.isCreatedByUser || message?.role?.toLowerCase() === 'user'
|
||||
? this.userLabel
|
||||
: this.chatGptLabel;
|
||||
const messageString = `${this.startToken}${roleLabel}:\n${
|
||||
message?.text ?? message?.message
|
||||
}${this.endToken}\n`;
|
||||
let newPromptBody;
|
||||
if (promptBody || isChatGptModel) {
|
||||
newPromptBody = `${messageString}${promptBody}`;
|
||||
} else {
|
||||
// Always insert prompt prefix before the last user message, if not gpt-3.5-turbo.
|
||||
// This makes the AI obey the prompt instructions better, which is important for custom instructions.
|
||||
// After a bunch of testing, it doesn't seem to cause the AI any confusion, even if you ask it things
|
||||
// like "what's the last thing I wrote?".
|
||||
newPromptBody = `${promptPrefix}${messageString}${promptBody}`;
|
||||
}
|
||||
|
||||
context.unshift(message);
|
||||
|
||||
const tokenCountForMessage = this.getTokenCount(messageString);
|
||||
const newTokenCount = currentTokenCount + tokenCountForMessage;
|
||||
if (newTokenCount > maxTokenCount) {
|
||||
if (promptBody) {
|
||||
// This message would put us over the token limit, so don't add it.
|
||||
return false;
|
||||
}
|
||||
// This is the first message, so we can't add it. Just throw an error.
|
||||
throw new Error(
|
||||
`Prompt is too long. Max token count is ${maxTokenCount}, but prompt is ${newTokenCount} tokens long.`,
|
||||
);
|
||||
}
|
||||
promptBody = newPromptBody;
|
||||
currentTokenCount = newTokenCount;
|
||||
// wait for next tick to avoid blocking the event loop
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
return buildPromptBody();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
await buildPromptBody();
|
||||
|
||||
const prompt = `${promptBody}${promptSuffix}`;
|
||||
if (isChatGptModel) {
|
||||
messagePayload.content = prompt;
|
||||
// Add 2 tokens for metadata after all messages have been counted.
|
||||
currentTokenCount += 2;
|
||||
}
|
||||
|
||||
// Use up to `this.maxContextTokens` tokens (prompt + response), but try to leave `this.maxTokens` tokens for the response.
|
||||
this.modelOptions.max_tokens = Math.min(
|
||||
this.maxContextTokens - currentTokenCount,
|
||||
this.maxResponseTokens,
|
||||
);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug(`Prompt : ${prompt}`);
|
||||
}
|
||||
|
||||
if (isChatGptModel) {
|
||||
return { prompt: [instructionsPayload, messagePayload], context };
|
||||
}
|
||||
return { prompt, context };
|
||||
}
|
||||
|
||||
getTokenCount(text) {
|
||||
return this.gptEncoder.encode(text, 'all').length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm adapted from "6. Counting tokens for chat API calls" of
|
||||
* https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
|
||||
*
|
||||
* An additional 2 tokens need to be added for metadata after all messages have been counted.
|
||||
*
|
||||
* @param {*} message
|
||||
*/
|
||||
getTokenCountForMessage(message) {
|
||||
let tokensPerMessage;
|
||||
let nameAdjustment;
|
||||
if (this.modelOptions.model.startsWith('gpt-4')) {
|
||||
tokensPerMessage = 3;
|
||||
nameAdjustment = 1;
|
||||
} else {
|
||||
tokensPerMessage = 4;
|
||||
nameAdjustment = -1;
|
||||
}
|
||||
|
||||
// Map each property of the message to the number of tokens it contains
|
||||
const propertyTokenCounts = Object.entries(message).map(([key, value]) => {
|
||||
// Count the number of tokens in the property value
|
||||
const numTokens = this.getTokenCount(value);
|
||||
|
||||
// Adjust by `nameAdjustment` tokens if the property key is 'name'
|
||||
const adjustment = key === 'name' ? nameAdjustment : 0;
|
||||
return numTokens + adjustment;
|
||||
});
|
||||
|
||||
// Sum the number of tokens in all properties and add `tokensPerMessage` for metadata
|
||||
return propertyTokenCounts.reduce((a, b) => a + b, tokensPerMessage);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ChatGPTClient;
|
||||
@@ -1,51 +1,62 @@
|
||||
const crypto = require('crypto');
|
||||
const TextStream = require('../stream');
|
||||
const BaseClient = require('./BaseClient');
|
||||
const { google } = require('googleapis');
|
||||
const { Agent, ProxyAgent } = require('undici');
|
||||
const { getMessages, saveMessage, saveConvo } = require('../../models');
|
||||
const {
|
||||
encoding_for_model: encodingForModel,
|
||||
get_encoding: getEncoding
|
||||
get_encoding: getEncoding,
|
||||
} = require('@dqbd/tiktoken');
|
||||
|
||||
const tokenizersCache = {};
|
||||
|
||||
class GoogleAgent {
|
||||
class GoogleClient extends BaseClient {
|
||||
constructor(credentials, options = {}) {
|
||||
super('apiKey', options);
|
||||
this.client_email = credentials.client_email;
|
||||
this.project_id = credentials.project_id;
|
||||
this.private_key = credentials.private_key;
|
||||
this.sender = 'PaLM2';
|
||||
this.setOptions(options);
|
||||
this.currentDateString = new Date().toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
/* Google/PaLM2 specific methods */
|
||||
constructUrl() {
|
||||
return `https://us-central1-aiplatform.googleapis.com/v1/projects/${this.project_id}/locations/us-central1/publishers/google/models/${this.modelOptions.model}:predict`;
|
||||
}
|
||||
|
||||
async getClient() {
|
||||
const scopes = ['https://www.googleapis.com/auth/cloud-platform'];
|
||||
const jwtClient = new google.auth.JWT(this.client_email, null, this.private_key, scopes);
|
||||
|
||||
jwtClient.authorize((err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
return jwtClient;
|
||||
}
|
||||
|
||||
/* Required Client methods */
|
||||
setOptions(options) {
|
||||
if (this.options && !this.options.replaceOptions) {
|
||||
// nested options aren't spread properly, so we need to do this manually
|
||||
this.options.modelOptions = {
|
||||
...this.options.modelOptions,
|
||||
...options.modelOptions
|
||||
...options.modelOptions,
|
||||
};
|
||||
delete options.modelOptions;
|
||||
// now we can merge options
|
||||
this.options = {
|
||||
...this.options,
|
||||
...options
|
||||
...options,
|
||||
};
|
||||
} else {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
this.options.examples = this.options.examples.filter(
|
||||
(obj) => obj.input.content !== '' && obj.output.content !== ''
|
||||
(obj) => obj.input.content !== '' && obj.output.content !== '',
|
||||
);
|
||||
|
||||
const modelOptions = this.options.modelOptions || {};
|
||||
@@ -55,7 +66,7 @@ class GoogleAgent {
|
||||
model: modelOptions.model || 'chat-bison',
|
||||
temperature: typeof modelOptions.temperature === 'undefined' ? 0.2 : modelOptions.temperature, // 0 - 1, 0.2 is recommended
|
||||
topP: typeof modelOptions.topP === 'undefined' ? 0.95 : modelOptions.topP, // 0 - 1, default: 0.95
|
||||
topK: typeof modelOptions.topK === 'undefined' ? 40 : modelOptions.topK // 1-40, default: 40
|
||||
topK: typeof modelOptions.topK === 'undefined' ? 40 : modelOptions.topK, // 1-40, default: 40
|
||||
// stop: modelOptions.stop // no stop method for now
|
||||
};
|
||||
|
||||
@@ -75,7 +86,7 @@ class GoogleAgent {
|
||||
throw new Error(
|
||||
`maxPromptTokens + maxOutputTokens (${this.maxPromptTokens} + ${this.maxResponseTokens} = ${
|
||||
this.maxPromptTokens + this.maxResponseTokens
|
||||
}) must be less than or equal to maxContextTokens (${this.maxContextTokens})`
|
||||
}) must be less than or equal to maxContextTokens (${this.maxContextTokens})`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,7 +105,7 @@ class GoogleAgent {
|
||||
this.endToken = '<|im_end|>';
|
||||
this.gptEncoder = this.constructor.getTokenizer('text-davinci-003', true, {
|
||||
'<|im_start|>': 100264,
|
||||
'<|im_end|>': 100265
|
||||
'<|im_end|>': 100265,
|
||||
});
|
||||
} else {
|
||||
// Previously I was trying to use "<|endoftext|>" but there seems to be some bug with OpenAI's token counting
|
||||
@@ -129,42 +140,22 @@ class GoogleAgent {
|
||||
return this;
|
||||
}
|
||||
|
||||
static getTokenizer(encoding, isModelName = false, extendSpecialTokens = {}) {
|
||||
if (tokenizersCache[encoding]) {
|
||||
return tokenizersCache[encoding];
|
||||
}
|
||||
let tokenizer;
|
||||
if (isModelName) {
|
||||
tokenizer = encodingForModel(encoding, extendSpecialTokens);
|
||||
} else {
|
||||
tokenizer = getEncoding(encoding, extendSpecialTokens);
|
||||
}
|
||||
tokenizersCache[encoding] = tokenizer;
|
||||
return tokenizer;
|
||||
getMessageMapMethod() {
|
||||
return ((message) => ({
|
||||
author: message?.author ?? (message.isCreatedByUser ? this.userLabel : this.modelLabel),
|
||||
content: message?.content ?? message.text,
|
||||
})).bind(this);
|
||||
}
|
||||
|
||||
async getClient() {
|
||||
const scopes = ['https://www.googleapis.com/auth/cloud-platform'];
|
||||
const jwtClient = new google.auth.JWT(this.client_email, null, this.private_key, scopes);
|
||||
|
||||
jwtClient.authorize((err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
return jwtClient;
|
||||
}
|
||||
|
||||
buildPayload(input, { messages = [] }) {
|
||||
buildMessages(messages = []) {
|
||||
const formattedMessages = messages.map(this.getMessageMapMethod());
|
||||
let payload = {
|
||||
instances: [
|
||||
{
|
||||
messages: [...messages, { author: this.userLabel, content: input }]
|
||||
}
|
||||
messages: formattedMessages,
|
||||
},
|
||||
],
|
||||
parameters: this.options.modelOptions
|
||||
parameters: this.options.modelOptions,
|
||||
};
|
||||
|
||||
if (this.options.promptPrefix) {
|
||||
@@ -175,23 +166,24 @@ class GoogleAgent {
|
||||
payload.instances[0].examples = this.options.examples;
|
||||
}
|
||||
|
||||
/* TO-DO: text model needs more context since it can't process an array of messages */
|
||||
if (this.isTextModel) {
|
||||
payload.instances = [
|
||||
{
|
||||
prompt: input
|
||||
}
|
||||
prompt: messages[messages.length - 1].content,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('buildPayload');
|
||||
console.debug('GoogleClient buildMessages');
|
||||
console.dir(payload, { depth: null });
|
||||
}
|
||||
|
||||
return payload;
|
||||
return { prompt: payload };
|
||||
}
|
||||
|
||||
async getCompletion(input, messages = [], abortController = null) {
|
||||
async getCompletion(payload, abortController = null) {
|
||||
if (!abortController) {
|
||||
abortController = new AbortController();
|
||||
}
|
||||
@@ -207,9 +199,9 @@ class GoogleAgent {
|
||||
method: 'POST',
|
||||
agent: new Agent({
|
||||
bodyTimeout: 0,
|
||||
headersTimeout: 0
|
||||
headersTimeout: 0,
|
||||
}),
|
||||
signal: abortController.signal
|
||||
signal: abortController.signal,
|
||||
};
|
||||
|
||||
if (this.options.proxy) {
|
||||
@@ -217,83 +209,29 @@ class GoogleAgent {
|
||||
}
|
||||
|
||||
const client = await this.getClient();
|
||||
const payload = this.buildPayload(input, { messages });
|
||||
const res = await client.request({ url, method: 'POST', data: payload });
|
||||
console.dir(res.data, { depth: null });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async loadHistory(conversationId, parentMessageId = null) {
|
||||
if (this.options.debug) {
|
||||
console.debug('Loading history for conversation', conversationId, parentMessageId);
|
||||
}
|
||||
|
||||
if (!parentMessageId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const messages = (await getMessages({ conversationId })) || [];
|
||||
|
||||
if (messages.length === 0) {
|
||||
this.currentMessages = [];
|
||||
return [];
|
||||
}
|
||||
|
||||
const orderedMessages = this.constructor.getMessagesForConversation(messages, parentMessageId);
|
||||
return orderedMessages.map((message) => {
|
||||
return {
|
||||
author: message.isCreatedByUser ? this.userLabel : this.modelLabel,
|
||||
content: message.content
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async saveMessageToDatabase(message, user = null) {
|
||||
await saveMessage({ ...message, unfinished: false });
|
||||
await saveConvo(user, {
|
||||
conversationId: message.conversationId,
|
||||
endpoint: 'google',
|
||||
...this.modelOptions
|
||||
});
|
||||
}
|
||||
|
||||
async sendMessage(message, opts = {}) {
|
||||
if (opts && typeof opts === 'object') {
|
||||
this.setOptions(opts);
|
||||
}
|
||||
console.log('sendMessage', message, opts);
|
||||
|
||||
const user = opts.user || null;
|
||||
const conversationId = opts.conversationId || crypto.randomUUID();
|
||||
const parentMessageId = opts.parentMessageId || '00000000-0000-0000-0000-000000000000';
|
||||
const userMessageId = opts.overrideParentMessageId || crypto.randomUUID();
|
||||
const responseMessageId = crypto.randomUUID();
|
||||
const messages = await this.loadHistory(conversationId, this.options?.parentMessageId);
|
||||
|
||||
const userMessage = {
|
||||
messageId: userMessageId,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
sender: 'User',
|
||||
text: message,
|
||||
isCreatedByUser: true
|
||||
getSaveOptions() {
|
||||
return {
|
||||
promptPrefix: this.options.promptPrefix,
|
||||
modelLabel: this.options.modelLabel,
|
||||
...this.modelOptions,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof opts?.getIds === 'function') {
|
||||
opts.getIds({
|
||||
userMessage,
|
||||
conversationId,
|
||||
responseMessageId
|
||||
});
|
||||
}
|
||||
getBuildMessagesOptions() {
|
||||
// console.log('GoogleClient doesn\'t use getBuildMessagesOptions');
|
||||
}
|
||||
|
||||
console.log('userMessage', userMessage);
|
||||
|
||||
await this.saveMessageToDatabase(userMessage, user);
|
||||
async sendCompletion(payload, opts = {}) {
|
||||
console.log('GoogleClient: sendcompletion', payload, opts);
|
||||
let reply = '';
|
||||
let blocked = false;
|
||||
try {
|
||||
const result = await this.getCompletion(message, messages, opts.abortController);
|
||||
const result = await this.getCompletion(payload, opts.abortController);
|
||||
blocked = result?.predictions?.[0]?.safetyAttributes?.blocked;
|
||||
reply =
|
||||
result?.predictions?.[0]?.candidates?.[0]?.content ||
|
||||
@@ -301,7 +239,7 @@ class GoogleAgent {
|
||||
'';
|
||||
if (blocked === true) {
|
||||
reply = `Google blocked a proper response to your message:\n${JSON.stringify(
|
||||
result.predictions[0].safetyAttributes
|
||||
result.predictions[0].safetyAttributes,
|
||||
)}${reply.length > 0 ? `\nAI Response:\n${reply}` : ''}`;
|
||||
}
|
||||
if (this.options.debug) {
|
||||
@@ -312,86 +250,31 @@ class GoogleAgent {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('options');
|
||||
console.debug(this.options);
|
||||
}
|
||||
|
||||
if (!blocked) {
|
||||
const textStream = new TextStream(reply, { delay: 0.5 });
|
||||
await textStream.processTextStream(opts.onProgress);
|
||||
await this.generateTextStream(reply, opts.onProgress, { delay: 0.5 });
|
||||
}
|
||||
|
||||
const responseMessage = {
|
||||
messageId: responseMessageId,
|
||||
conversationId,
|
||||
parentMessageId: userMessage.messageId,
|
||||
sender: 'PaLM2',
|
||||
text: reply,
|
||||
error: blocked,
|
||||
isCreatedByUser: false
|
||||
};
|
||||
return reply.trim();
|
||||
}
|
||||
|
||||
await this.saveMessageToDatabase(responseMessage, user);
|
||||
return responseMessage;
|
||||
/* TO-DO: Handle tokens with Google tokenization NOTE: these are required */
|
||||
static getTokenizer(encoding, isModelName = false, extendSpecialTokens = {}) {
|
||||
if (tokenizersCache[encoding]) {
|
||||
return tokenizersCache[encoding];
|
||||
}
|
||||
let tokenizer;
|
||||
if (isModelName) {
|
||||
tokenizer = encodingForModel(encoding, extendSpecialTokens);
|
||||
} else {
|
||||
tokenizer = getEncoding(encoding, extendSpecialTokens);
|
||||
}
|
||||
tokenizersCache[encoding] = tokenizer;
|
||||
return tokenizer;
|
||||
}
|
||||
|
||||
getTokenCount(text) {
|
||||
return this.gptEncoder.encode(text, 'all').length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm adapted from "6. Counting tokens for chat API calls" of
|
||||
* https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
|
||||
*
|
||||
* An additional 2 tokens need to be added for metadata after all messages have been counted.
|
||||
*
|
||||
* @param {*} message
|
||||
*/
|
||||
getTokenCountForMessage(message) {
|
||||
// Map each property of the message to the number of tokens it contains
|
||||
const propertyTokenCounts = Object.entries(message).map(([key, value]) => {
|
||||
// Count the number of tokens in the property value
|
||||
const numTokens = this.getTokenCount(value);
|
||||
|
||||
// Subtract 1 token if the property key is 'name'
|
||||
const adjustment = key === 'name' ? 1 : 0;
|
||||
return numTokens - adjustment;
|
||||
});
|
||||
|
||||
// Sum the number of tokens in all properties and add 4 for metadata
|
||||
return propertyTokenCounts.reduce((a, b) => a + b, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through messages, building an array based on the parentMessageId.
|
||||
* Each message has an id and a parentMessageId. The parentMessageId is the id of the message that this message is a reply to.
|
||||
* @param messages
|
||||
* @param parentMessageId
|
||||
* @returns {*[]} An array containing the messages in the order they should be displayed, starting with the root message.
|
||||
*/
|
||||
static getMessagesForConversation(messages, parentMessageId) {
|
||||
const orderedMessages = [];
|
||||
let currentMessageId = parentMessageId;
|
||||
while (currentMessageId) {
|
||||
// eslint-disable-next-line no-loop-func
|
||||
const message = messages.find((m) => m.messageId === currentMessageId);
|
||||
if (!message) {
|
||||
break;
|
||||
}
|
||||
orderedMessages.unshift(message);
|
||||
currentMessageId = message.parentMessageId;
|
||||
}
|
||||
|
||||
if (orderedMessages.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return orderedMessages.map((msg) => ({
|
||||
isCreatedByUser: msg.isCreatedByUser,
|
||||
content: msg.text
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GoogleAgent;
|
||||
module.exports = GoogleClient;
|
||||
369
api/app/clients/OpenAIClient.js
Normal file
369
api/app/clients/OpenAIClient.js
Normal file
@@ -0,0 +1,369 @@
|
||||
const BaseClient = require('./BaseClient');
|
||||
const ChatGPTClient = require('./ChatGPTClient');
|
||||
const {
|
||||
encoding_for_model: encodingForModel,
|
||||
get_encoding: getEncoding,
|
||||
} = require('@dqbd/tiktoken');
|
||||
const { maxTokensMap, genAzureChatCompletion } = require('../../utils');
|
||||
|
||||
// Cache to store Tiktoken instances
|
||||
const tokenizersCache = {};
|
||||
// Counter for keeping track of the number of tokenizer calls
|
||||
let tokenizerCallsCount = 0;
|
||||
|
||||
class OpenAIClient extends BaseClient {
|
||||
constructor(apiKey, options = {}) {
|
||||
super(apiKey, options);
|
||||
this.ChatGPTClient = new ChatGPTClient();
|
||||
this.buildPrompt = this.ChatGPTClient.buildPrompt.bind(this);
|
||||
this.getCompletion = this.ChatGPTClient.getCompletion.bind(this);
|
||||
this.sender = options.sender ?? 'ChatGPT';
|
||||
this.contextStrategy = options.contextStrategy
|
||||
? options.contextStrategy.toLowerCase()
|
||||
: 'discard';
|
||||
this.shouldRefineContext = this.contextStrategy === 'refine';
|
||||
this.azure = options.azure || false;
|
||||
if (this.azure) {
|
||||
this.azureEndpoint = genAzureChatCompletion(this.azure);
|
||||
}
|
||||
this.setOptions(options);
|
||||
}
|
||||
|
||||
setOptions(options) {
|
||||
if (this.options && !this.options.replaceOptions) {
|
||||
this.options.modelOptions = {
|
||||
...this.options.modelOptions,
|
||||
...options.modelOptions,
|
||||
};
|
||||
delete options.modelOptions;
|
||||
this.options = {
|
||||
...this.options,
|
||||
...options,
|
||||
};
|
||||
} else {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
if (this.options.openaiApiKey) {
|
||||
this.apiKey = this.options.openaiApiKey;
|
||||
}
|
||||
|
||||
const modelOptions = this.options.modelOptions || {};
|
||||
if (!this.modelOptions) {
|
||||
this.modelOptions = {
|
||||
...modelOptions,
|
||||
model: modelOptions.model || 'gpt-3.5-turbo',
|
||||
temperature:
|
||||
typeof modelOptions.temperature === 'undefined' ? 0.8 : modelOptions.temperature,
|
||||
top_p: typeof modelOptions.top_p === 'undefined' ? 1 : modelOptions.top_p,
|
||||
presence_penalty:
|
||||
typeof modelOptions.presence_penalty === 'undefined' ? 1 : modelOptions.presence_penalty,
|
||||
stop: modelOptions.stop,
|
||||
};
|
||||
}
|
||||
|
||||
this.isChatCompletion =
|
||||
this.options.reverseProxyUrl ||
|
||||
this.options.localAI ||
|
||||
this.modelOptions.model.startsWith('gpt-');
|
||||
this.isChatGptModel = this.isChatCompletion;
|
||||
if (this.modelOptions.model === 'text-davinci-003') {
|
||||
this.isChatCompletion = false;
|
||||
this.isChatGptModel = false;
|
||||
}
|
||||
const { isChatGptModel } = this;
|
||||
this.isUnofficialChatGptModel =
|
||||
this.modelOptions.model.startsWith('text-chat') ||
|
||||
this.modelOptions.model.startsWith('text-davinci-002-render');
|
||||
this.maxContextTokens = maxTokensMap[this.modelOptions.model] ?? 4095; // 1 less than maximum
|
||||
this.maxResponseTokens = this.modelOptions.max_tokens || 1024;
|
||||
this.maxPromptTokens =
|
||||
this.options.maxPromptTokens || this.maxContextTokens - this.maxResponseTokens;
|
||||
|
||||
if (this.maxPromptTokens + this.maxResponseTokens > this.maxContextTokens) {
|
||||
throw new Error(
|
||||
`maxPromptTokens + max_tokens (${this.maxPromptTokens} + ${this.maxResponseTokens} = ${
|
||||
this.maxPromptTokens + this.maxResponseTokens
|
||||
}) must be less than or equal to maxContextTokens (${this.maxContextTokens})`,
|
||||
);
|
||||
}
|
||||
|
||||
this.userLabel = this.options.userLabel || 'User';
|
||||
this.chatGptLabel = this.options.chatGptLabel || 'Assistant';
|
||||
|
||||
this.setupTokens();
|
||||
|
||||
if (!this.modelOptions.stop) {
|
||||
const stopTokens = [this.startToken];
|
||||
if (this.endToken && this.endToken !== this.startToken) {
|
||||
stopTokens.push(this.endToken);
|
||||
}
|
||||
stopTokens.push(`\n${this.userLabel}:`);
|
||||
stopTokens.push('<|diff_marker|>');
|
||||
this.modelOptions.stop = stopTokens;
|
||||
}
|
||||
|
||||
if (this.options.reverseProxyUrl) {
|
||||
this.completionsUrl = this.options.reverseProxyUrl;
|
||||
} else if (isChatGptModel) {
|
||||
this.completionsUrl = 'https://api.openai.com/v1/chat/completions';
|
||||
} else {
|
||||
this.completionsUrl = 'https://api.openai.com/v1/completions';
|
||||
}
|
||||
|
||||
if (this.azureEndpoint) {
|
||||
this.completionsUrl = this.azureEndpoint;
|
||||
}
|
||||
|
||||
if (this.azureEndpoint && this.options.debug) {
|
||||
console.debug(`Using Azure endpoint: ${this.azureEndpoint}`, this.azure);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setupTokens() {
|
||||
if (this.isChatCompletion) {
|
||||
this.startToken = '||>';
|
||||
this.endToken = '';
|
||||
} else if (this.isUnofficialChatGptModel) {
|
||||
this.startToken = '<|im_start|>';
|
||||
this.endToken = '<|im_end|>';
|
||||
} else {
|
||||
this.startToken = '||>';
|
||||
this.endToken = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Selects an appropriate tokenizer based on the current configuration of the client instance.
|
||||
// It takes into account factors such as whether it's a chat completion, an unofficial chat GPT model, etc.
|
||||
selectTokenizer() {
|
||||
let tokenizer;
|
||||
this.encoding = 'text-davinci-003';
|
||||
if (this.isChatCompletion) {
|
||||
this.encoding = 'cl100k_base';
|
||||
tokenizer = this.constructor.getTokenizer(this.encoding);
|
||||
} else if (this.isUnofficialChatGptModel) {
|
||||
const extendSpecialTokens = {
|
||||
'<|im_start|>': 100264,
|
||||
'<|im_end|>': 100265,
|
||||
};
|
||||
tokenizer = this.constructor.getTokenizer(this.encoding, true, extendSpecialTokens);
|
||||
} else {
|
||||
try {
|
||||
this.encoding = this.modelOptions.model;
|
||||
tokenizer = this.constructor.getTokenizer(this.modelOptions.model, true);
|
||||
} catch {
|
||||
tokenizer = this.constructor.getTokenizer(this.encoding, true);
|
||||
}
|
||||
}
|
||||
|
||||
return tokenizer;
|
||||
}
|
||||
|
||||
// Retrieves a tokenizer either from the cache or creates a new one if one doesn't exist in the cache.
|
||||
// If a tokenizer is being created, it's also added to the cache.
|
||||
static getTokenizer(encoding, isModelName = false, extendSpecialTokens = {}) {
|
||||
let tokenizer;
|
||||
if (tokenizersCache[encoding]) {
|
||||
tokenizer = tokenizersCache[encoding];
|
||||
} else {
|
||||
if (isModelName) {
|
||||
tokenizer = encodingForModel(encoding, extendSpecialTokens);
|
||||
} else {
|
||||
tokenizer = getEncoding(encoding, extendSpecialTokens);
|
||||
}
|
||||
tokenizersCache[encoding] = tokenizer;
|
||||
}
|
||||
return tokenizer;
|
||||
}
|
||||
|
||||
// Frees all encoders in the cache and resets the count.
|
||||
static freeAndResetAllEncoders() {
|
||||
try {
|
||||
Object.keys(tokenizersCache).forEach((key) => {
|
||||
if (tokenizersCache[key]) {
|
||||
tokenizersCache[key].free();
|
||||
delete tokenizersCache[key];
|
||||
}
|
||||
});
|
||||
// Reset count
|
||||
tokenizerCallsCount = 1;
|
||||
} catch (error) {
|
||||
console.log('Free and reset encoders error');
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if the cache of tokenizers has reached a certain size. If it has, it frees and resets all tokenizers.
|
||||
resetTokenizersIfNecessary() {
|
||||
if (tokenizerCallsCount >= 25) {
|
||||
if (this.options.debug) {
|
||||
console.debug('freeAndResetAllEncoders: reached 25 encodings, resetting...');
|
||||
}
|
||||
this.constructor.freeAndResetAllEncoders();
|
||||
}
|
||||
tokenizerCallsCount++;
|
||||
}
|
||||
|
||||
// Returns the token count of a given text. It also checks and resets the tokenizers if necessary.
|
||||
getTokenCount(text) {
|
||||
this.resetTokenizersIfNecessary();
|
||||
try {
|
||||
const tokenizer = this.selectTokenizer();
|
||||
return tokenizer.encode(text, 'all').length;
|
||||
} catch (error) {
|
||||
this.constructor.freeAndResetAllEncoders();
|
||||
const tokenizer = this.selectTokenizer();
|
||||
return tokenizer.encode(text, 'all').length;
|
||||
}
|
||||
}
|
||||
|
||||
getSaveOptions() {
|
||||
return {
|
||||
chatGptLabel: this.options.chatGptLabel,
|
||||
promptPrefix: this.options.promptPrefix,
|
||||
...this.modelOptions,
|
||||
};
|
||||
}
|
||||
|
||||
getBuildMessagesOptions(opts) {
|
||||
return {
|
||||
isChatCompletion: this.isChatCompletion,
|
||||
promptPrefix: opts.promptPrefix,
|
||||
abortController: opts.abortController,
|
||||
};
|
||||
}
|
||||
|
||||
async buildMessages(
|
||||
messages,
|
||||
parentMessageId,
|
||||
{ isChatCompletion = false, promptPrefix = null },
|
||||
) {
|
||||
if (!isChatCompletion) {
|
||||
return await this.buildPrompt(messages, parentMessageId, {
|
||||
isChatGptModel: isChatCompletion,
|
||||
promptPrefix,
|
||||
});
|
||||
}
|
||||
|
||||
let payload;
|
||||
let instructions;
|
||||
let tokenCountMap;
|
||||
let promptTokens;
|
||||
let orderedMessages = this.constructor.getMessagesForConversation(messages, parentMessageId);
|
||||
|
||||
promptPrefix = (promptPrefix || this.options.promptPrefix || '').trim();
|
||||
if (promptPrefix) {
|
||||
promptPrefix = `Instructions:\n${promptPrefix}`;
|
||||
instructions = {
|
||||
role: 'system',
|
||||
name: 'instructions',
|
||||
content: promptPrefix,
|
||||
};
|
||||
|
||||
if (this.contextStrategy) {
|
||||
instructions.tokenCount = this.getTokenCountForMessage(instructions);
|
||||
}
|
||||
}
|
||||
|
||||
const formattedMessages = orderedMessages.map((message) => {
|
||||
let { role: _role, sender, text } = message;
|
||||
const role = _role ?? sender;
|
||||
const content = text ?? '';
|
||||
const formattedMessage = {
|
||||
role: role?.toLowerCase() === 'user' ? 'user' : 'assistant',
|
||||
content,
|
||||
};
|
||||
|
||||
if (this.options?.name && formattedMessage.role === 'user') {
|
||||
formattedMessage.name = this.options.name;
|
||||
}
|
||||
|
||||
if (this.contextStrategy) {
|
||||
formattedMessage.tokenCount =
|
||||
message.tokenCount ?? this.getTokenCountForMessage(formattedMessage);
|
||||
}
|
||||
|
||||
return formattedMessage;
|
||||
});
|
||||
|
||||
// TODO: need to handle interleaving instructions better
|
||||
if (this.contextStrategy) {
|
||||
({ payload, tokenCountMap, promptTokens, messages } = await this.handleContextStrategy({
|
||||
instructions,
|
||||
orderedMessages,
|
||||
formattedMessages,
|
||||
}));
|
||||
}
|
||||
|
||||
const result = {
|
||||
prompt: payload,
|
||||
promptTokens,
|
||||
messages,
|
||||
};
|
||||
|
||||
if (tokenCountMap) {
|
||||
tokenCountMap.instructions = instructions?.tokenCount;
|
||||
result.tokenCountMap = tokenCountMap;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async sendCompletion(payload, opts = {}) {
|
||||
let reply = '';
|
||||
let result = null;
|
||||
if (typeof opts.onProgress === 'function') {
|
||||
await this.getCompletion(
|
||||
payload,
|
||||
(progressMessage) => {
|
||||
if (progressMessage === '[DONE]') {
|
||||
return;
|
||||
}
|
||||
const token = this.isChatCompletion
|
||||
? progressMessage.choices?.[0]?.delta?.content
|
||||
: progressMessage.choices?.[0]?.text;
|
||||
// first event's delta content is always undefined
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
if (this.options.debug) {
|
||||
// console.debug(token);
|
||||
}
|
||||
if (token === this.endToken) {
|
||||
return;
|
||||
}
|
||||
opts.onProgress(token);
|
||||
reply += token;
|
||||
},
|
||||
opts.abortController || new AbortController(),
|
||||
);
|
||||
} else {
|
||||
result = await this.getCompletion(
|
||||
payload,
|
||||
null,
|
||||
opts.abortController || new AbortController(),
|
||||
);
|
||||
if (this.options.debug) {
|
||||
console.debug(JSON.stringify(result));
|
||||
}
|
||||
if (this.isChatCompletion) {
|
||||
reply = result.choices[0].message.content;
|
||||
} else {
|
||||
reply = result.choices[0].text.replace(this.endToken, '');
|
||||
}
|
||||
}
|
||||
|
||||
return reply.trim();
|
||||
}
|
||||
|
||||
getTokenCountForResponse(response) {
|
||||
return this.getTokenCountForMessage({
|
||||
role: 'assistant',
|
||||
content: response.text,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OpenAIClient;
|
||||
560
api/app/clients/PluginsClient.js
Normal file
560
api/app/clients/PluginsClient.js
Normal file
@@ -0,0 +1,560 @@
|
||||
const OpenAIClient = require('./OpenAIClient');
|
||||
const { ChatOpenAI } = require('langchain/chat_models/openai');
|
||||
const { CallbackManager } = require('langchain/callbacks');
|
||||
const { initializeCustomAgent, initializeFunctionsAgent } = require('./agents/');
|
||||
const { findMessageContent } = require('../../utils');
|
||||
const { loadTools } = require('./tools/util');
|
||||
const { SelfReflectionTool } = require('./tools/');
|
||||
const { HumanChatMessage, AIChatMessage } = require('langchain/schema');
|
||||
const { instructions, imageInstructions, errorInstructions } = require('./prompts/instructions');
|
||||
|
||||
class PluginsClient extends OpenAIClient {
|
||||
constructor(apiKey, options = {}) {
|
||||
super(apiKey, options);
|
||||
this.sender = options.sender ?? 'Assistant';
|
||||
this.tools = [];
|
||||
this.actions = [];
|
||||
this.openAIApiKey = apiKey;
|
||||
this.setOptions(options);
|
||||
this.executor = null;
|
||||
}
|
||||
|
||||
getActions(input = null) {
|
||||
let output = 'Internal thoughts & actions taken:\n"';
|
||||
let actions = input || this.actions;
|
||||
|
||||
if (actions[0]?.action && this.functionsAgent) {
|
||||
actions = actions.map((step) => ({
|
||||
log: `Action: ${step.action?.tool || ''}\nInput: ${
|
||||
JSON.stringify(step.action?.toolInput) || ''
|
||||
}\nObservation: ${step.observation}`,
|
||||
}));
|
||||
} else if (actions[0]?.action) {
|
||||
actions = actions.map((step) => ({
|
||||
log: `${step.action.log}\nObservation: ${step.observation}`,
|
||||
}));
|
||||
}
|
||||
|
||||
actions.forEach((actionObj, index) => {
|
||||
output += `${actionObj.log}`;
|
||||
if (index < actions.length - 1) {
|
||||
output += '\n';
|
||||
}
|
||||
});
|
||||
|
||||
return output + '"';
|
||||
}
|
||||
|
||||
buildErrorInput(message, errorMessage) {
|
||||
const log = errorMessage.includes('Could not parse LLM output:')
|
||||
? `A formatting error occurred with your response to the human's last message. You didn't follow the formatting instructions. Remember to ${instructions}`
|
||||
: `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}`;
|
||||
|
||||
return `
|
||||
${log}
|
||||
|
||||
${this.getActions()}
|
||||
|
||||
Human's last message: ${message}
|
||||
`;
|
||||
}
|
||||
|
||||
buildPromptPrefix(result, message) {
|
||||
if ((result.output && result.output.includes('N/A')) || result.output === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
result?.intermediateSteps?.length === 1 &&
|
||||
result?.intermediateSteps[0]?.action?.toolInput === 'N/A'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const internalActions =
|
||||
result?.intermediateSteps?.length > 0
|
||||
? this.getActions(result.intermediateSteps)
|
||||
: 'Internal Actions Taken: None';
|
||||
|
||||
const toolBasedInstructions = internalActions.toLowerCase().includes('image')
|
||||
? imageInstructions
|
||||
: '';
|
||||
|
||||
const errorMessage = result.errorMessage ? `${errorInstructions} ${result.errorMessage}\n` : '';
|
||||
|
||||
const preliminaryAnswer =
|
||||
result.output?.length > 0 ? `Preliminary Answer: "${result.output.trim()}"` : '';
|
||||
const prefix = preliminaryAnswer
|
||||
? '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.'
|
||||
: 'respond to the User Message below based on your preliminary thoughts & actions.';
|
||||
|
||||
return `As a helpful AI Assistant, ${prefix}${errorMessage}\n${internalActions}
|
||||
${preliminaryAnswer}
|
||||
Reply conversationally to the User based on your ${
|
||||
preliminaryAnswer ? 'preliminary answer, ' : ''
|
||||
}internal actions, thoughts, and observations, making improvements wherever possible, but do not modify URLs.
|
||||
${
|
||||
preliminaryAnswer
|
||||
? ''
|
||||
: '\nIf there is an incomplete thought or action, you are expected to complete it in your response now.\n'
|
||||
}You must cite sources if you are using any web links. ${toolBasedInstructions}
|
||||
Only respond with your conversational reply to the following User Message:
|
||||
"${message}"`;
|
||||
}
|
||||
|
||||
setOptions(options) {
|
||||
this.agentOptions = options.agentOptions;
|
||||
this.functionsAgent = this.agentOptions?.agent === 'functions';
|
||||
this.agentIsGpt3 = this.agentOptions?.model.startsWith('gpt-3');
|
||||
if (this.functionsAgent && this.agentOptions.model) {
|
||||
this.agentOptions.model = this.getFunctionModelName(this.agentOptions.model);
|
||||
}
|
||||
|
||||
super.setOptions(options);
|
||||
this.isGpt3 = this.modelOptions.model.startsWith('gpt-3');
|
||||
|
||||
if (this.options.reverseProxyUrl) {
|
||||
this.langchainProxy = this.options.reverseProxyUrl.match(/.*v1/)[0];
|
||||
}
|
||||
}
|
||||
|
||||
getSaveOptions() {
|
||||
return {
|
||||
chatGptLabel: this.options.chatGptLabel,
|
||||
promptPrefix: this.options.promptPrefix,
|
||||
...this.modelOptions,
|
||||
agentOptions: this.agentOptions,
|
||||
};
|
||||
}
|
||||
|
||||
saveLatestAction(action) {
|
||||
this.actions.push(action);
|
||||
}
|
||||
|
||||
getFunctionModelName(input) {
|
||||
if (input.startsWith('gpt-3.5-turbo')) {
|
||||
return 'gpt-3.5-turbo';
|
||||
} else if (input.startsWith('gpt-4')) {
|
||||
return 'gpt-4';
|
||||
} else {
|
||||
return 'gpt-3.5-turbo';
|
||||
}
|
||||
}
|
||||
|
||||
getBuildMessagesOptions(opts) {
|
||||
return {
|
||||
isChatCompletion: true,
|
||||
promptPrefix: opts.promptPrefix,
|
||||
abortController: opts.abortController,
|
||||
};
|
||||
}
|
||||
|
||||
createLLM(modelOptions, configOptions) {
|
||||
let credentials = { openAIApiKey: this.openAIApiKey };
|
||||
let configuration = {
|
||||
apiKey: this.openAIApiKey,
|
||||
};
|
||||
|
||||
if (this.azure) {
|
||||
credentials = {};
|
||||
configuration = {};
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('createLLM: configOptions');
|
||||
console.debug(configOptions);
|
||||
}
|
||||
|
||||
return new ChatOpenAI({ credentials, configuration, ...modelOptions }, configOptions);
|
||||
}
|
||||
|
||||
async initialize({ user, message, onAgentAction, onChainEnd, signal }) {
|
||||
const modelOptions = {
|
||||
modelName: this.agentOptions.model,
|
||||
temperature: this.agentOptions.temperature,
|
||||
};
|
||||
|
||||
const configOptions = {};
|
||||
|
||||
if (this.langchainProxy) {
|
||||
configOptions.basePath = this.langchainProxy;
|
||||
}
|
||||
|
||||
const model = this.createLLM(modelOptions, configOptions);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug(
|
||||
`<-----Agent Model: ${model.modelName} | Temp: ${model.temperature} | Functions: ${this.functionsAgent}----->`,
|
||||
);
|
||||
}
|
||||
|
||||
this.availableTools = await loadTools({
|
||||
user,
|
||||
model,
|
||||
tools: this.options.tools,
|
||||
functions: this.functionsAgent,
|
||||
options: {
|
||||
openAIApiKey: this.openAIApiKey,
|
||||
debug: this.options?.debug,
|
||||
message,
|
||||
},
|
||||
});
|
||||
// load tools
|
||||
for (const tool of this.options.tools) {
|
||||
const validTool = this.availableTools[tool];
|
||||
|
||||
if (tool === 'plugins') {
|
||||
const plugins = await validTool();
|
||||
this.tools = [...this.tools, ...plugins];
|
||||
} else if (validTool) {
|
||||
this.tools.push(await validTool());
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('Requested Tools');
|
||||
console.debug(this.options.tools);
|
||||
console.debug('Loaded Tools');
|
||||
console.debug(this.tools.map((tool) => tool.name));
|
||||
}
|
||||
|
||||
if (this.tools.length > 0 && !this.functionsAgent) {
|
||||
this.tools.push(new SelfReflectionTool({ message, isGpt3: false }));
|
||||
} else if (this.tools.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleAction = (action, callback = null) => {
|
||||
this.saveLatestAction(action);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('Latest Agent Action ', this.actions[this.actions.length - 1]);
|
||||
}
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback(action);
|
||||
}
|
||||
};
|
||||
|
||||
// Map Messages to Langchain format
|
||||
const pastMessages = this.currentMessages
|
||||
.slice(0, -1)
|
||||
.map((msg) =>
|
||||
msg?.isCreatedByUser || msg?.role?.toLowerCase() === 'user'
|
||||
? new HumanChatMessage(msg.text)
|
||||
: new AIChatMessage(msg.text),
|
||||
);
|
||||
|
||||
// initialize agent
|
||||
const initializer = this.functionsAgent ? initializeFunctionsAgent : initializeCustomAgent;
|
||||
this.executor = await initializer({
|
||||
model,
|
||||
signal,
|
||||
pastMessages,
|
||||
tools: this.tools,
|
||||
currentDateString: this.currentDateString,
|
||||
verbose: this.options.debug,
|
||||
returnIntermediateSteps: true,
|
||||
callbackManager: CallbackManager.fromHandlers({
|
||||
async handleAgentAction(action) {
|
||||
handleAction(action, onAgentAction);
|
||||
},
|
||||
async handleChainEnd(action) {
|
||||
if (typeof onChainEnd === 'function') {
|
||||
onChainEnd(action);
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('Loaded agent.');
|
||||
}
|
||||
}
|
||||
|
||||
async executorCall(message, signal) {
|
||||
let errorMessage = '';
|
||||
const maxAttempts = 1;
|
||||
|
||||
for (let attempts = 1; attempts <= maxAttempts; attempts++) {
|
||||
const errorInput = this.buildErrorInput(message, errorMessage);
|
||||
const input = attempts > 1 ? errorInput : message;
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug(`Attempt ${attempts} of ${maxAttempts}`);
|
||||
}
|
||||
|
||||
if (this.options.debug && errorMessage.length > 0) {
|
||||
console.debug('Caught error, input:', input);
|
||||
}
|
||||
|
||||
try {
|
||||
this.result = await this.executor.call({ input, signal });
|
||||
break; // Exit the loop if the function call is successful
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
errorMessage = err.message;
|
||||
const content = findMessageContent(message);
|
||||
if (content) {
|
||||
errorMessage = content;
|
||||
break;
|
||||
}
|
||||
if (attempts === maxAttempts) {
|
||||
this.result.output = `Encountered an error while attempting to respond. Error: ${err.message}`;
|
||||
this.result.intermediateSteps = this.actions;
|
||||
this.result.errorMessage = errorMessage;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addImages(intermediateSteps, responseMessage) {
|
||||
if (!intermediateSteps || !responseMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
intermediateSteps.forEach((step) => {
|
||||
const { observation } = step;
|
||||
if (!observation || !observation.includes('![')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract the image file path from the observation
|
||||
const observedImagePath = observation.match(/\(\/images\/.*\.\w*\)/g)[0];
|
||||
|
||||
// Check if the responseMessage already includes the image file path
|
||||
if (!responseMessage.text.includes(observedImagePath)) {
|
||||
// If the image file path is not found, append the whole observation
|
||||
responseMessage.text += '\n' + observation;
|
||||
if (this.options.debug) {
|
||||
console.debug('added image from intermediateSteps');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async handleResponseMessage(responseMessage, saveOptions, user) {
|
||||
responseMessage.tokenCount = this.getTokenCountForResponse(responseMessage);
|
||||
responseMessage.completionTokens = responseMessage.tokenCount;
|
||||
await this.saveMessageToDatabase(responseMessage, saveOptions, user);
|
||||
delete responseMessage.tokenCount;
|
||||
return { ...responseMessage, ...this.result };
|
||||
}
|
||||
|
||||
async sendMessage(message, opts = {}) {
|
||||
const completionMode = this.options.tools.length === 0;
|
||||
if (completionMode) {
|
||||
this.setOptions(opts);
|
||||
return super.sendMessage(message, opts);
|
||||
}
|
||||
console.log('Plugins sendMessage', message, opts);
|
||||
const {
|
||||
user,
|
||||
conversationId,
|
||||
responseMessageId,
|
||||
saveOptions,
|
||||
userMessage,
|
||||
onAgentAction,
|
||||
onChainEnd,
|
||||
} = await this.handleStartMethods(message, opts);
|
||||
|
||||
this.currentMessages.push(userMessage);
|
||||
|
||||
let {
|
||||
prompt: payload,
|
||||
tokenCountMap,
|
||||
promptTokens,
|
||||
messages,
|
||||
} = await this.buildMessages(
|
||||
this.currentMessages,
|
||||
userMessage.messageId,
|
||||
this.getBuildMessagesOptions({
|
||||
promptPrefix: null,
|
||||
abortController: this.abortController,
|
||||
}),
|
||||
);
|
||||
|
||||
if (tokenCountMap) {
|
||||
console.dir(tokenCountMap, { depth: null });
|
||||
if (tokenCountMap[userMessage.messageId]) {
|
||||
userMessage.tokenCount = tokenCountMap[userMessage.messageId];
|
||||
console.log('userMessage.tokenCount', userMessage.tokenCount);
|
||||
}
|
||||
payload = payload.map((message) => {
|
||||
const messageWithoutTokenCount = message;
|
||||
delete messageWithoutTokenCount.tokenCount;
|
||||
return messageWithoutTokenCount;
|
||||
});
|
||||
this.handleTokenCountMap(tokenCountMap);
|
||||
}
|
||||
|
||||
this.result = {};
|
||||
if (messages) {
|
||||
this.currentMessages = messages;
|
||||
}
|
||||
await this.saveMessageToDatabase(userMessage, saveOptions, user);
|
||||
const responseMessage = {
|
||||
messageId: responseMessageId,
|
||||
conversationId,
|
||||
parentMessageId: userMessage.messageId,
|
||||
isCreatedByUser: false,
|
||||
model: this.modelOptions.model,
|
||||
sender: this.sender,
|
||||
promptTokens,
|
||||
};
|
||||
|
||||
await this.initialize({
|
||||
user,
|
||||
message,
|
||||
onAgentAction,
|
||||
onChainEnd,
|
||||
signal: this.abortController.signal,
|
||||
});
|
||||
await this.executorCall(message, this.abortController.signal);
|
||||
|
||||
// If message was aborted mid-generation
|
||||
if (this.result?.errorMessage?.length > 0 && this.result?.errorMessage?.includes('cancel')) {
|
||||
responseMessage.text = 'Cancelled.';
|
||||
return await this.handleResponseMessage(responseMessage, saveOptions, user);
|
||||
}
|
||||
|
||||
if (this.agentOptions.skipCompletion && this.result.output) {
|
||||
responseMessage.text = this.result.output;
|
||||
this.addImages(this.result.intermediateSteps, responseMessage);
|
||||
await this.generateTextStream(this.result.output, opts.onProgress, { delay: 8 });
|
||||
return await this.handleResponseMessage(responseMessage, saveOptions, user);
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('Plugins completion phase: this.result');
|
||||
console.debug(this.result);
|
||||
}
|
||||
|
||||
const promptPrefix = this.buildPromptPrefix(this.result, message);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('Plugins: promptPrefix');
|
||||
console.debug(promptPrefix);
|
||||
}
|
||||
|
||||
payload = await this.buildCompletionPrompt({
|
||||
messages: this.currentMessages,
|
||||
promptPrefix,
|
||||
});
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('buildCompletionPrompt Payload');
|
||||
console.debug(payload);
|
||||
}
|
||||
responseMessage.text = await this.sendCompletion(payload, opts);
|
||||
return await this.handleResponseMessage(responseMessage, saveOptions, user);
|
||||
}
|
||||
|
||||
async buildCompletionPrompt({ messages, promptPrefix: _promptPrefix }) {
|
||||
if (this.options.debug) {
|
||||
console.debug('buildCompletionPrompt messages', messages);
|
||||
}
|
||||
|
||||
const orderedMessages = messages;
|
||||
let promptPrefix = _promptPrefix.trim();
|
||||
// If the prompt prefix doesn't end with the end token, add it.
|
||||
if (!promptPrefix.endsWith(`${this.endToken}`)) {
|
||||
promptPrefix = `${promptPrefix.trim()}${this.endToken}\n\n`;
|
||||
}
|
||||
promptPrefix = `${this.startToken}Instructions:\n${promptPrefix}`;
|
||||
const promptSuffix = `${this.startToken}${this.chatGptLabel ?? 'Assistant'}:\n`;
|
||||
|
||||
const instructionsPayload = {
|
||||
role: 'system',
|
||||
name: 'instructions',
|
||||
content: promptPrefix,
|
||||
};
|
||||
|
||||
const messagePayload = {
|
||||
role: 'system',
|
||||
content: promptSuffix,
|
||||
};
|
||||
|
||||
if (this.isGpt3) {
|
||||
instructionsPayload.role = 'user';
|
||||
messagePayload.role = 'user';
|
||||
instructionsPayload.content += `\n${promptSuffix}`;
|
||||
}
|
||||
|
||||
// testing if this works with browser endpoint
|
||||
if (!this.isGpt3 && this.options.reverseProxyUrl) {
|
||||
instructionsPayload.role = 'user';
|
||||
}
|
||||
|
||||
let currentTokenCount =
|
||||
this.getTokenCountForMessage(instructionsPayload) +
|
||||
this.getTokenCountForMessage(messagePayload);
|
||||
|
||||
let promptBody = '';
|
||||
const maxTokenCount = this.maxPromptTokens;
|
||||
// Iterate backwards through the messages, adding them to the prompt until we reach the max token count.
|
||||
// Do this within a recursive async function so that it doesn't block the event loop for too long.
|
||||
const buildPromptBody = async () => {
|
||||
if (currentTokenCount < maxTokenCount && orderedMessages.length > 0) {
|
||||
const message = orderedMessages.pop();
|
||||
const isCreatedByUser = message.isCreatedByUser || message.role?.toLowerCase() === 'user';
|
||||
const roleLabel = isCreatedByUser ? this.userLabel : this.chatGptLabel;
|
||||
let messageString = `${this.startToken}${roleLabel}:\n${message.text}${this.endToken}\n`;
|
||||
let newPromptBody = `${messageString}${promptBody}`;
|
||||
|
||||
const tokenCountForMessage = this.getTokenCount(messageString);
|
||||
const newTokenCount = currentTokenCount + tokenCountForMessage;
|
||||
if (newTokenCount > maxTokenCount) {
|
||||
if (promptBody) {
|
||||
// This message would put us over the token limit, so don't add it.
|
||||
return false;
|
||||
}
|
||||
// This is the first message, so we can't add it. Just throw an error.
|
||||
throw new Error(
|
||||
`Prompt is too long. Max token count is ${maxTokenCount}, but prompt is ${newTokenCount} tokens long.`,
|
||||
);
|
||||
}
|
||||
promptBody = newPromptBody;
|
||||
currentTokenCount = newTokenCount;
|
||||
// wait for next tick to avoid blocking the event loop
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
return buildPromptBody();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
await buildPromptBody();
|
||||
const prompt = promptBody;
|
||||
messagePayload.content = prompt;
|
||||
// Add 2 tokens for metadata after all messages have been counted.
|
||||
currentTokenCount += 2;
|
||||
|
||||
if (this.isGpt3 && messagePayload.content.length > 0) {
|
||||
const context = 'Chat History:\n';
|
||||
messagePayload.content = `${context}${prompt}`;
|
||||
currentTokenCount += this.getTokenCount(context);
|
||||
}
|
||||
|
||||
// Use up to `this.maxContextTokens` tokens (prompt + response), but try to leave `this.maxTokens` tokens for the response.
|
||||
this.modelOptions.max_tokens = Math.min(
|
||||
this.maxContextTokens - currentTokenCount,
|
||||
this.maxResponseTokens,
|
||||
);
|
||||
|
||||
if (this.isGpt3) {
|
||||
messagePayload.content += promptSuffix;
|
||||
return [instructionsPayload, messagePayload];
|
||||
}
|
||||
|
||||
const result = [messagePayload, instructionsPayload];
|
||||
|
||||
if (this.functionsAgent && !this.isGpt3) {
|
||||
result[1].content = `${result[1].content}\n${this.startToken}${this.chatGptLabel}:\nSure thing! Here is the output you requested:\n`;
|
||||
}
|
||||
|
||||
return result.filter((message) => message.content.length > 0);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PluginsClient;
|
||||
@@ -8,7 +8,7 @@ class CustomAgent extends ZeroShotAgent {
|
||||
}
|
||||
|
||||
_stop() {
|
||||
return [`\nObservation:`, `\nObservation 1:`];
|
||||
return ['\nObservation:', '\nObservation 1:'];
|
||||
}
|
||||
|
||||
static createPrompt(tools, opts = {}) {
|
||||
@@ -32,17 +32,17 @@ class CustomAgent extends ZeroShotAgent {
|
||||
.join('\n');
|
||||
const toolNames = tools.map((tool) => tool.name);
|
||||
const formatInstructions = (0, renderTemplate)(instructions, 'f-string', {
|
||||
tool_names: toolNames
|
||||
tool_names: toolNames,
|
||||
});
|
||||
const template = [
|
||||
`Date: ${currentDateString}\n${prefix}`,
|
||||
toolStrings,
|
||||
formatInstructions,
|
||||
suffix
|
||||
suffix,
|
||||
].join('\n\n');
|
||||
return new PromptTemplate({
|
||||
template,
|
||||
inputVariables
|
||||
inputVariables,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ const { BufferMemory, ChatMessageHistory } = require('langchain/memory');
|
||||
const {
|
||||
ChatPromptTemplate,
|
||||
SystemMessagePromptTemplate,
|
||||
HumanMessagePromptTemplate
|
||||
HumanMessagePromptTemplate,
|
||||
} = require('langchain/prompts');
|
||||
|
||||
const initializeCustomAgent = async ({
|
||||
@@ -22,7 +22,7 @@ const initializeCustomAgent = async ({
|
||||
new SystemMessagePromptTemplate(prompt),
|
||||
HumanMessagePromptTemplate.fromTemplate(`{chat_history}
|
||||
Query: {input}
|
||||
{agent_scratchpad}`)
|
||||
{agent_scratchpad}`),
|
||||
]);
|
||||
|
||||
const outputParser = new CustomOutputParser({ tools });
|
||||
@@ -34,23 +34,21 @@ Query: {input}
|
||||
humanPrefix: 'User',
|
||||
aiPrefix: 'Assistant',
|
||||
inputKey: 'input',
|
||||
outputKey: 'output'
|
||||
outputKey: 'output',
|
||||
});
|
||||
|
||||
const llmChain = new LLMChain({
|
||||
prompt: chatPrompt,
|
||||
llm: model
|
||||
llm: model,
|
||||
});
|
||||
|
||||
const agent = new CustomAgent({
|
||||
llmChain,
|
||||
outputParser,
|
||||
allowedTools: tools.map((tool) => tool.name)
|
||||
allowedTools: tools.map((tool) => tool.name),
|
||||
});
|
||||
|
||||
return AgentExecutor.fromAgentAndTools({ agent, tools, memory, ...rest });
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
initializeCustomAgent
|
||||
};
|
||||
module.exports = initializeCustomAgent;
|
||||
@@ -57,7 +57,7 @@ class CustomOutputParser extends ZeroShotAgentOutputParser {
|
||||
const output = text.substring(finalMatch.index + finalMatch[0].length).trim();
|
||||
return {
|
||||
returnValues: { output },
|
||||
log: text
|
||||
log: text,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ class CustomOutputParser extends ZeroShotAgentOutputParser {
|
||||
if (!match) {
|
||||
console.log(
|
||||
'\n\n<----------------------HIT NO MATCH PARSING ERROR---------------------->\n\n',
|
||||
match
|
||||
match,
|
||||
);
|
||||
const thoughts = text.replace(/[tT]hought:/, '').split('\n');
|
||||
// return {
|
||||
@@ -77,7 +77,7 @@ class CustomOutputParser extends ZeroShotAgentOutputParser {
|
||||
|
||||
return {
|
||||
returnValues: { output: thoughts[0] },
|
||||
log: thoughts.slice(1).join('\n')
|
||||
log: thoughts.slice(1).join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -86,12 +86,12 @@ class CustomOutputParser extends ZeroShotAgentOutputParser {
|
||||
if (match && selectedTool === 'n/a') {
|
||||
console.log(
|
||||
'\n\n<----------------------HIT N/A PARSING ERROR---------------------->\n\n',
|
||||
match
|
||||
match,
|
||||
);
|
||||
return {
|
||||
tool: 'self-reflection',
|
||||
toolInput: match[2]?.trim().replace(/^"+|"+$/g, '') ?? '',
|
||||
log: text
|
||||
log: text,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,15 +99,15 @@ class CustomOutputParser extends ZeroShotAgentOutputParser {
|
||||
if (match && !toolIsValid) {
|
||||
console.log(
|
||||
'\n\n<----------------Tool invalid: Re-assigning Selected Tool---------------->\n\n',
|
||||
match
|
||||
match,
|
||||
);
|
||||
selectedTool = this.getValidTool(selectedTool);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (match && !selectedTool) {
|
||||
console.log(
|
||||
'\n\n<----------------------HIT INVALID TOOL PARSING ERROR---------------------->\n\n',
|
||||
match
|
||||
match,
|
||||
);
|
||||
selectedTool = 'self-reflection';
|
||||
}
|
||||
@@ -115,7 +115,7 @@ class CustomOutputParser extends ZeroShotAgentOutputParser {
|
||||
if (match && !match[2]) {
|
||||
console.log(
|
||||
'\n\n<----------------------HIT NO ACTION INPUT PARSING ERROR---------------------->\n\n',
|
||||
match
|
||||
match,
|
||||
);
|
||||
|
||||
// In case there is no action input, let's double-check if there is an action input in 'text' variable
|
||||
@@ -125,7 +125,7 @@ class CustomOutputParser extends ZeroShotAgentOutputParser {
|
||||
return {
|
||||
tool: selectedTool,
|
||||
toolInput: actionInputMatch[1].trim(),
|
||||
log: text
|
||||
log: text,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ class CustomOutputParser extends ZeroShotAgentOutputParser {
|
||||
return {
|
||||
tool: selectedTool,
|
||||
toolInput: thoughtMatch[1].trim(),
|
||||
log: text
|
||||
log: text,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -158,12 +158,12 @@ class CustomOutputParser extends ZeroShotAgentOutputParser {
|
||||
if (action && actionInputMatch) {
|
||||
console.log(
|
||||
'\n\n<------Matched Action Input in Long Parsing Error------>\n\n',
|
||||
actionInputMatch
|
||||
actionInputMatch,
|
||||
);
|
||||
return {
|
||||
tool: action,
|
||||
toolInput: actionInputMatch[1].trim().replaceAll('"', ''),
|
||||
log: text
|
||||
log: text,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ class CustomOutputParser extends ZeroShotAgentOutputParser {
|
||||
const returnValues = {
|
||||
tool: action,
|
||||
toolInput: input,
|
||||
log: thought || inputText
|
||||
log: thought || inputText,
|
||||
};
|
||||
|
||||
const inputMatch = this.actionValues.exec(returnValues.log); //new
|
||||
@@ -197,7 +197,7 @@ class CustomOutputParser extends ZeroShotAgentOutputParser {
|
||||
return {
|
||||
tool: 'self-reflection',
|
||||
toolInput: 'Hypothetical actions: \n"' + text + '"\n',
|
||||
log: 'Thought: I need to look at my hypothetical actions and try one'
|
||||
log: 'Thought: I need to look at my hypothetical actions and try one',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ class CustomOutputParser extends ZeroShotAgentOutputParser {
|
||||
return {
|
||||
tool: selectedTool,
|
||||
toolInput: match[2]?.trim()?.replace(/^"+|"+$/g, '') ?? '',
|
||||
log: text
|
||||
log: text,
|
||||
};
|
||||
}
|
||||
}
|
||||
120
api/app/clients/agents/Functions/FunctionsAgent.js
Normal file
120
api/app/clients/agents/Functions/FunctionsAgent.js
Normal file
@@ -0,0 +1,120 @@
|
||||
const { Agent } = require('langchain/agents');
|
||||
const { LLMChain } = require('langchain/chains');
|
||||
const { FunctionChatMessage, AIChatMessage } = require('langchain/schema');
|
||||
const {
|
||||
ChatPromptTemplate,
|
||||
MessagesPlaceholder,
|
||||
SystemMessagePromptTemplate,
|
||||
HumanMessagePromptTemplate,
|
||||
} = require('langchain/prompts');
|
||||
const PREFIX = 'You are a helpful AI assistant.';
|
||||
|
||||
function parseOutput(message) {
|
||||
if (message.additional_kwargs.function_call) {
|
||||
const function_call = message.additional_kwargs.function_call;
|
||||
return {
|
||||
tool: function_call.name,
|
||||
toolInput: function_call.arguments ? JSON.parse(function_call.arguments) : {},
|
||||
log: message.text,
|
||||
};
|
||||
} else {
|
||||
return { returnValues: { output: message.text }, log: message.text };
|
||||
}
|
||||
}
|
||||
|
||||
class FunctionsAgent extends Agent {
|
||||
constructor(input) {
|
||||
super({ ...input, outputParser: undefined });
|
||||
this.tools = input.tools;
|
||||
}
|
||||
|
||||
lc_namespace = ['langchain', 'agents', 'openai'];
|
||||
|
||||
_agentType() {
|
||||
return 'openai-functions';
|
||||
}
|
||||
|
||||
observationPrefix() {
|
||||
return 'Observation: ';
|
||||
}
|
||||
|
||||
llmPrefix() {
|
||||
return 'Thought:';
|
||||
}
|
||||
|
||||
_stop() {
|
||||
return ['Observation:'];
|
||||
}
|
||||
|
||||
static createPrompt(_tools, fields) {
|
||||
const { prefix = PREFIX, currentDateString } = fields || {};
|
||||
|
||||
return ChatPromptTemplate.fromPromptMessages([
|
||||
SystemMessagePromptTemplate.fromTemplate(`Date: ${currentDateString}\n${prefix}`),
|
||||
new MessagesPlaceholder('chat_history'),
|
||||
HumanMessagePromptTemplate.fromTemplate('Query: {input}'),
|
||||
new MessagesPlaceholder('agent_scratchpad'),
|
||||
]);
|
||||
}
|
||||
|
||||
static fromLLMAndTools(llm, tools, args) {
|
||||
FunctionsAgent.validateTools(tools);
|
||||
const prompt = FunctionsAgent.createPrompt(tools, args);
|
||||
const chain = new LLMChain({
|
||||
prompt,
|
||||
llm,
|
||||
callbacks: args?.callbacks,
|
||||
});
|
||||
return new FunctionsAgent({
|
||||
llmChain: chain,
|
||||
allowedTools: tools.map((t) => t.name),
|
||||
tools,
|
||||
});
|
||||
}
|
||||
|
||||
async constructScratchPad(steps) {
|
||||
return steps.flatMap(({ action, observation }) => [
|
||||
new AIChatMessage('', {
|
||||
function_call: {
|
||||
name: action.tool,
|
||||
arguments: JSON.stringify(action.toolInput),
|
||||
},
|
||||
}),
|
||||
new FunctionChatMessage(observation, action.tool),
|
||||
]);
|
||||
}
|
||||
|
||||
async plan(steps, inputs, callbackManager) {
|
||||
// Add scratchpad and stop to inputs
|
||||
const thoughts = await this.constructScratchPad(steps);
|
||||
const newInputs = Object.assign({}, inputs, { agent_scratchpad: thoughts });
|
||||
if (this._stop().length !== 0) {
|
||||
newInputs.stop = this._stop();
|
||||
}
|
||||
|
||||
// Split inputs between prompt and llm
|
||||
const llm = this.llmChain.llm;
|
||||
const valuesForPrompt = Object.assign({}, newInputs);
|
||||
const valuesForLLM = {
|
||||
tools: this.tools,
|
||||
};
|
||||
for (let i = 0; i < this.llmChain.llm.callKeys.length; i++) {
|
||||
const key = this.llmChain.llm.callKeys[i];
|
||||
if (key in inputs) {
|
||||
valuesForLLM[key] = inputs[key];
|
||||
delete valuesForPrompt[key];
|
||||
}
|
||||
}
|
||||
|
||||
const promptValue = await this.llmChain.prompt.formatPromptValue(valuesForPrompt);
|
||||
const message = await llm.predictMessages(
|
||||
promptValue.toChatMessages(),
|
||||
valuesForLLM,
|
||||
callbackManager,
|
||||
);
|
||||
console.log('message', message);
|
||||
return parseOutput(message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FunctionsAgent;
|
||||
28
api/app/clients/agents/Functions/initializeFunctionsAgent.js
Normal file
28
api/app/clients/agents/Functions/initializeFunctionsAgent.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const { initializeAgentExecutorWithOptions } = require('langchain/agents');
|
||||
const { BufferMemory, ChatMessageHistory } = require('langchain/memory');
|
||||
|
||||
const initializeFunctionsAgent = async ({
|
||||
tools,
|
||||
model,
|
||||
pastMessages,
|
||||
// currentDateString,
|
||||
...rest
|
||||
}) => {
|
||||
const memory = new BufferMemory({
|
||||
chatHistory: new ChatMessageHistory(pastMessages),
|
||||
memoryKey: 'chat_history',
|
||||
humanPrefix: 'User',
|
||||
aiPrefix: 'Assistant',
|
||||
inputKey: 'input',
|
||||
outputKey: 'output',
|
||||
returnMessages: true,
|
||||
});
|
||||
|
||||
return await initializeAgentExecutorWithOptions(tools, model, {
|
||||
agentType: 'openai-functions',
|
||||
memory,
|
||||
...rest,
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = initializeFunctionsAgent;
|
||||
7
api/app/clients/agents/index.js
Normal file
7
api/app/clients/agents/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const initializeCustomAgent = require('./CustomAgent/initializeCustomAgent');
|
||||
const initializeFunctionsAgent = require('./Functions/initializeFunctionsAgent');
|
||||
|
||||
module.exports = {
|
||||
initializeCustomAgent,
|
||||
initializeFunctionsAgent,
|
||||
};
|
||||
@@ -1,94 +0,0 @@
|
||||
require('dotenv').config();
|
||||
const { KeyvFile } = require('keyv-file');
|
||||
const { genAzureChatCompletion } = require('../../utils/genAzureEndpoints');
|
||||
const tiktoken = require('@dqbd/tiktoken');
|
||||
const tiktokenModels = require('../../utils/tiktokenModels');
|
||||
const encoding_for_model = tiktoken.encoding_for_model;
|
||||
|
||||
const askClient = async ({
|
||||
text,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
model,
|
||||
oaiApiKey,
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
temperature,
|
||||
top_p,
|
||||
presence_penalty,
|
||||
frequency_penalty,
|
||||
onProgress,
|
||||
abortController,
|
||||
userId
|
||||
}) => {
|
||||
const { ChatGPTClient } = await import('@waylaidwanderer/chatgpt-api');
|
||||
const store = {
|
||||
store: new KeyvFile({ filename: './data/cache.json' })
|
||||
};
|
||||
|
||||
const azure = process.env.AZURE_OPENAI_API_KEY ? true : false;
|
||||
let promptText = 'You are ChatGPT, a large language model trained by OpenAI.';
|
||||
if (promptPrefix) {
|
||||
promptText = promptPrefix;
|
||||
}
|
||||
const maxContextTokens = model === 'gpt-4-32k' ? 32767 : model.startsWith('gpt-4') ? 8191 : 4095; // 1 less than maximum
|
||||
const clientOptions = {
|
||||
reverseProxyUrl: process.env.OPENAI_REVERSE_PROXY || null,
|
||||
azure,
|
||||
maxContextTokens,
|
||||
modelOptions: {
|
||||
model,
|
||||
temperature,
|
||||
top_p,
|
||||
presence_penalty,
|
||||
frequency_penalty
|
||||
},
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
proxy: process.env.PROXY || null
|
||||
// debug: true
|
||||
};
|
||||
|
||||
let apiKey = oaiApiKey ? oaiApiKey : process.env.OPENAI_API_KEY || null;
|
||||
|
||||
if (azure) {
|
||||
apiKey = oaiApiKey ? oaiApiKey : process.env.AZURE_OPENAI_API_KEY || null;
|
||||
clientOptions.reverseProxyUrl = genAzureChatCompletion({
|
||||
azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME,
|
||||
azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME,
|
||||
azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION
|
||||
});
|
||||
}
|
||||
|
||||
const client = new ChatGPTClient(apiKey, clientOptions, store);
|
||||
|
||||
const options = {
|
||||
onProgress,
|
||||
abortController,
|
||||
...(parentMessageId && conversationId ? { parentMessageId, conversationId } : {})
|
||||
};
|
||||
|
||||
let usage = {};
|
||||
let enc = null;
|
||||
try {
|
||||
enc = encoding_for_model(tiktokenModels.has(model) ? model : 'gpt-3.5-turbo');
|
||||
usage.prompt_tokens = (enc.encode(promptText)).length + (enc.encode(text)).length;
|
||||
} catch (e) {
|
||||
console.log('Error encoding prompt text', e);
|
||||
}
|
||||
|
||||
const res = await client.sendMessage(text, { ...options, userId });
|
||||
|
||||
try {
|
||||
usage.completion_tokens = (enc.encode(res.response)).length;
|
||||
enc.free();
|
||||
usage.total_tokens = usage.prompt_tokens + usage.completion_tokens;
|
||||
res.usage = usage;
|
||||
} catch (e) {
|
||||
console.log('Error encoding response text', e);
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
module.exports = { askClient };
|
||||
17
api/app/clients/index.js
Normal file
17
api/app/clients/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const ChatGPTClient = require('./ChatGPTClient');
|
||||
const OpenAIClient = require('./OpenAIClient');
|
||||
const PluginsClient = require('./PluginsClient');
|
||||
const GoogleClient = require('./GoogleClient');
|
||||
const TextStream = require('./TextStream');
|
||||
const AnthropicClient = require('./AnthropicClient');
|
||||
const toolUtils = require('./tools/util');
|
||||
|
||||
module.exports = {
|
||||
ChatGPTClient,
|
||||
OpenAIClient,
|
||||
PluginsClient,
|
||||
GoogleClient,
|
||||
TextStream,
|
||||
AnthropicClient,
|
||||
...toolUtils,
|
||||
};
|
||||
10
api/app/clients/prompts/instructions.js
Normal file
10
api/app/clients/prompts/instructions.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
instructions:
|
||||
'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.',
|
||||
errorInstructions:
|
||||
'\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:',
|
||||
imageInstructions:
|
||||
'You must include the exact image paths from above, formatted in Markdown syntax: ',
|
||||
completionInstructions:
|
||||
'Instructions:\nYou are ChatGPT, a large language model trained by OpenAI. Respond conversationally.\nCurrent date:',
|
||||
};
|
||||
24
api/app/clients/prompts/refinePrompt.js
Normal file
24
api/app/clients/prompts/refinePrompt.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const { PromptTemplate } = require('langchain/prompts');
|
||||
|
||||
const refinePromptTemplate = `Your job is to produce a final summary of the following conversation.
|
||||
We have provided an existing summary up to a certain point: "{existing_answer}"
|
||||
We have the opportunity to refine the existing summary
|
||||
(only if needed) with some more context below.
|
||||
------------
|
||||
"{text}"
|
||||
------------
|
||||
|
||||
Given the new context, refine the original summary of the conversation.
|
||||
Do note who is speaking in the conversation to give proper context.
|
||||
If the context isn't useful, return the original summary.
|
||||
|
||||
REFINED CONVERSATION SUMMARY:`;
|
||||
|
||||
const refinePrompt = new PromptTemplate({
|
||||
template: refinePromptTemplate,
|
||||
inputVariables: ['existing_answer', 'text'],
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
refinePrompt,
|
||||
};
|
||||
369
api/app/clients/specs/BaseClient.test.js
Normal file
369
api/app/clients/specs/BaseClient.test.js
Normal file
@@ -0,0 +1,369 @@
|
||||
const { initializeFakeClient } = require('./FakeClient');
|
||||
|
||||
jest.mock('../../../lib/db/connectDb');
|
||||
jest.mock('../../../models', () => {
|
||||
return function () {
|
||||
return {
|
||||
save: jest.fn(),
|
||||
deleteConvos: jest.fn(),
|
||||
getConvo: jest.fn(),
|
||||
getMessages: jest.fn(),
|
||||
saveMessage: jest.fn(),
|
||||
updateMessage: jest.fn(),
|
||||
saveConvo: jest.fn(),
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('langchain/text_splitter', () => {
|
||||
return {
|
||||
RecursiveCharacterTextSplitter: jest.fn().mockImplementation(() => {
|
||||
return { createDocuments: jest.fn().mockResolvedValue([]) };
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('langchain/chat_models/openai', () => {
|
||||
return {
|
||||
ChatOpenAI: jest.fn().mockImplementation(() => {
|
||||
return {};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('langchain/chains', () => {
|
||||
return {
|
||||
loadSummarizationChain: jest.fn().mockReturnValue({
|
||||
call: jest.fn().mockResolvedValue({ output_text: 'Refined answer' }),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
let parentMessageId;
|
||||
let conversationId;
|
||||
const fakeMessages = [];
|
||||
const userMessage = 'Hello, ChatGPT!';
|
||||
const apiKey = 'fake-api-key';
|
||||
|
||||
describe('BaseClient', () => {
|
||||
let TestClient;
|
||||
const options = {
|
||||
// debug: true,
|
||||
modelOptions: {
|
||||
model: 'gpt-3.5-turbo',
|
||||
temperature: 0,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestClient = initializeFakeClient(apiKey, options, fakeMessages);
|
||||
});
|
||||
|
||||
test('returns the input messages without instructions when addInstructions() is called with empty instructions', () => {
|
||||
const messages = [{ content: 'Hello' }, { content: 'How are you?' }, { content: 'Goodbye' }];
|
||||
const instructions = '';
|
||||
const result = TestClient.addInstructions(messages, instructions);
|
||||
expect(result).toEqual(messages);
|
||||
});
|
||||
|
||||
test('returns the input messages with instructions properly added when addInstructions() is called with non-empty instructions', () => {
|
||||
const messages = [{ content: 'Hello' }, { content: 'How are you?' }, { content: 'Goodbye' }];
|
||||
const instructions = { content: 'Please respond to the question.' };
|
||||
const result = TestClient.addInstructions(messages, instructions);
|
||||
const expected = [
|
||||
{ content: 'Hello' },
|
||||
{ content: 'How are you?' },
|
||||
{ content: 'Please respond to the question.' },
|
||||
{ content: 'Goodbye' },
|
||||
];
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('concats messages correctly in concatenateMessages()', () => {
|
||||
const messages = [
|
||||
{ name: 'User', content: 'Hello' },
|
||||
{ name: 'Assistant', content: 'How can I help you?' },
|
||||
{ name: 'User', content: 'I have a question.' },
|
||||
];
|
||||
const result = TestClient.concatenateMessages(messages);
|
||||
const expected =
|
||||
'User:\nHello\n\nAssistant:\nHow can I help you?\n\nUser:\nI have a question.\n\n';
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('refines messages correctly in refineMessages()', async () => {
|
||||
const messagesToRefine = [
|
||||
{ role: 'user', content: 'Hello', tokenCount: 10 },
|
||||
{ role: 'assistant', content: 'How can I help you?', tokenCount: 20 },
|
||||
];
|
||||
const remainingContextTokens = 100;
|
||||
const expectedRefinedMessage = {
|
||||
role: 'assistant',
|
||||
content: 'Refined answer',
|
||||
tokenCount: 14, // 'Refined answer'.length
|
||||
};
|
||||
|
||||
const result = await TestClient.refineMessages(messagesToRefine, remainingContextTokens);
|
||||
expect(result).toEqual(expectedRefinedMessage);
|
||||
});
|
||||
|
||||
test('gets messages within token limit (under limit) correctly in getMessagesWithinTokenLimit()', async () => {
|
||||
TestClient.maxContextTokens = 100;
|
||||
TestClient.shouldRefineContext = true;
|
||||
TestClient.refineMessages = jest.fn().mockResolvedValue({
|
||||
role: 'assistant',
|
||||
content: 'Refined answer',
|
||||
tokenCount: 30,
|
||||
});
|
||||
|
||||
const messages = [
|
||||
{ role: 'user', content: 'Hello', tokenCount: 5 },
|
||||
{ role: 'assistant', content: 'How can I help you?', tokenCount: 19 },
|
||||
{ role: 'user', content: 'I have a question.', tokenCount: 18 },
|
||||
];
|
||||
const expectedContext = [
|
||||
{ role: 'user', content: 'Hello', tokenCount: 5 }, // 'Hello'.length
|
||||
{ role: 'assistant', content: 'How can I help you?', tokenCount: 19 },
|
||||
{ role: 'user', content: 'I have a question.', tokenCount: 18 },
|
||||
];
|
||||
const expectedRemainingContextTokens = 58; // 100 - 5 - 19 - 18
|
||||
const expectedMessagesToRefine = [];
|
||||
|
||||
const result = await TestClient.getMessagesWithinTokenLimit(messages);
|
||||
expect(result.context).toEqual(expectedContext);
|
||||
expect(result.remainingContextTokens).toBe(expectedRemainingContextTokens);
|
||||
expect(result.messagesToRefine).toEqual(expectedMessagesToRefine);
|
||||
});
|
||||
|
||||
test('gets messages within token limit (over limit) correctly in getMessagesWithinTokenLimit()', async () => {
|
||||
TestClient.maxContextTokens = 50; // Set a lower limit
|
||||
TestClient.shouldRefineContext = true;
|
||||
TestClient.refineMessages = jest.fn().mockResolvedValue({
|
||||
role: 'assistant',
|
||||
content: 'Refined answer',
|
||||
tokenCount: 4,
|
||||
});
|
||||
|
||||
const messages = [
|
||||
{ role: 'user', content: 'I need a coffee, stat!', tokenCount: 30 },
|
||||
{ role: 'assistant', content: 'Sure, I can help with that.', tokenCount: 30 },
|
||||
{ role: 'user', content: 'Hello', tokenCount: 5 },
|
||||
{ role: 'assistant', content: 'How can I help you?', tokenCount: 19 },
|
||||
{ role: 'user', content: 'I have a question.', tokenCount: 18 },
|
||||
];
|
||||
const expectedContext = [
|
||||
{ role: 'user', content: 'Hello', tokenCount: 5 },
|
||||
{ role: 'assistant', content: 'How can I help you?', tokenCount: 19 },
|
||||
{ role: 'user', content: 'I have a question.', tokenCount: 18 },
|
||||
];
|
||||
const expectedRemainingContextTokens = 8; // 50 - 18 - 19 - 5
|
||||
const expectedMessagesToRefine = [
|
||||
{ role: 'user', content: 'I need a coffee, stat!', tokenCount: 30 },
|
||||
{ role: 'assistant', content: 'Sure, I can help with that.', tokenCount: 30 },
|
||||
];
|
||||
|
||||
const result = await TestClient.getMessagesWithinTokenLimit(messages);
|
||||
expect(result.context).toEqual(expectedContext);
|
||||
expect(result.remainingContextTokens).toBe(expectedRemainingContextTokens);
|
||||
expect(result.messagesToRefine).toEqual(expectedMessagesToRefine);
|
||||
});
|
||||
|
||||
test('handles context strategy correctly in handleContextStrategy()', async () => {
|
||||
TestClient.addInstructions = jest
|
||||
.fn()
|
||||
.mockReturnValue([
|
||||
{ content: 'Hello' },
|
||||
{ content: 'How can I help you?' },
|
||||
{ content: 'Please provide more details.' },
|
||||
{ content: 'I can assist you with that.' },
|
||||
]);
|
||||
TestClient.getMessagesWithinTokenLimit = jest.fn().mockReturnValue({
|
||||
context: [
|
||||
{ content: 'How can I help you?' },
|
||||
{ content: 'Please provide more details.' },
|
||||
{ content: 'I can assist you with that.' },
|
||||
],
|
||||
remainingContextTokens: 80,
|
||||
messagesToRefine: [{ content: 'Hello' }],
|
||||
refineIndex: 3,
|
||||
});
|
||||
TestClient.refineMessages = jest.fn().mockResolvedValue({
|
||||
role: 'assistant',
|
||||
content: 'Refined answer',
|
||||
tokenCount: 30,
|
||||
});
|
||||
TestClient.getTokenCountForResponse = jest.fn().mockReturnValue(40);
|
||||
|
||||
const instructions = { content: 'Please provide more details.' };
|
||||
const orderedMessages = [
|
||||
{ content: 'Hello' },
|
||||
{ content: 'How can I help you?' },
|
||||
{ content: 'Please provide more details.' },
|
||||
{ content: 'I can assist you with that.' },
|
||||
];
|
||||
const formattedMessages = [
|
||||
{ content: 'Hello' },
|
||||
{ content: 'How can I help you?' },
|
||||
{ content: 'Please provide more details.' },
|
||||
{ content: 'I can assist you with that.' },
|
||||
];
|
||||
const expectedResult = {
|
||||
payload: [
|
||||
{
|
||||
content: 'Refined answer',
|
||||
role: 'assistant',
|
||||
tokenCount: 30,
|
||||
},
|
||||
{ content: 'How can I help you?' },
|
||||
{ content: 'Please provide more details.' },
|
||||
{ content: 'I can assist you with that.' },
|
||||
],
|
||||
promptTokens: expect.any(Number),
|
||||
tokenCountMap: {},
|
||||
messages: expect.any(Array),
|
||||
};
|
||||
|
||||
const result = await TestClient.handleContextStrategy({
|
||||
instructions,
|
||||
orderedMessages,
|
||||
formattedMessages,
|
||||
});
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
describe('sendMessage', () => {
|
||||
test('sendMessage should return a response message', async () => {
|
||||
const expectedResult = expect.objectContaining({
|
||||
sender: TestClient.sender,
|
||||
text: expect.any(String),
|
||||
isCreatedByUser: false,
|
||||
messageId: expect.any(String),
|
||||
parentMessageId: expect.any(String),
|
||||
conversationId: expect.any(String),
|
||||
});
|
||||
|
||||
const response = await TestClient.sendMessage(userMessage);
|
||||
parentMessageId = response.messageId;
|
||||
conversationId = response.conversationId;
|
||||
expect(response).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
test('sendMessage should work with provided conversationId and parentMessageId', async () => {
|
||||
const userMessage = 'Second message in the conversation';
|
||||
const opts = {
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
getIds: jest.fn(),
|
||||
onStart: jest.fn(),
|
||||
};
|
||||
|
||||
const expectedResult = expect.objectContaining({
|
||||
sender: TestClient.sender,
|
||||
text: expect.any(String),
|
||||
isCreatedByUser: false,
|
||||
messageId: expect.any(String),
|
||||
parentMessageId: expect.any(String),
|
||||
conversationId: opts.conversationId,
|
||||
});
|
||||
|
||||
const response = await TestClient.sendMessage(userMessage, opts);
|
||||
parentMessageId = response.messageId;
|
||||
expect(response.conversationId).toEqual(conversationId);
|
||||
expect(response).toEqual(expectedResult);
|
||||
expect(opts.getIds).toHaveBeenCalled();
|
||||
expect(opts.onStart).toHaveBeenCalled();
|
||||
expect(TestClient.getBuildMessagesOptions).toHaveBeenCalled();
|
||||
expect(TestClient.getSaveOptions).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should return chat history', async () => {
|
||||
const chatMessages = await TestClient.loadHistory(conversationId, parentMessageId);
|
||||
expect(TestClient.currentMessages).toHaveLength(4);
|
||||
expect(chatMessages[0].text).toEqual(userMessage);
|
||||
});
|
||||
|
||||
test('setOptions is called with the correct arguments', async () => {
|
||||
TestClient.setOptions = jest.fn();
|
||||
const opts = { conversationId: '123', parentMessageId: '456' };
|
||||
await TestClient.sendMessage('Hello, world!', opts);
|
||||
expect(TestClient.setOptions).toHaveBeenCalledWith(opts);
|
||||
TestClient.setOptions.mockClear();
|
||||
});
|
||||
|
||||
test('loadHistory is called with the correct arguments', async () => {
|
||||
const opts = { conversationId: '123', parentMessageId: '456' };
|
||||
await TestClient.sendMessage('Hello, world!', opts);
|
||||
expect(TestClient.loadHistory).toHaveBeenCalledWith(
|
||||
opts.conversationId,
|
||||
opts.parentMessageId,
|
||||
);
|
||||
});
|
||||
|
||||
test('getIds is called with the correct arguments', async () => {
|
||||
const getIds = jest.fn();
|
||||
const opts = { getIds };
|
||||
const response = await TestClient.sendMessage('Hello, world!', opts);
|
||||
expect(getIds).toHaveBeenCalledWith({
|
||||
userMessage: expect.objectContaining({ text: 'Hello, world!' }),
|
||||
conversationId: response.conversationId,
|
||||
responseMessageId: response.messageId,
|
||||
});
|
||||
});
|
||||
|
||||
test('onStart is called with the correct arguments', async () => {
|
||||
const onStart = jest.fn();
|
||||
const opts = { onStart };
|
||||
await TestClient.sendMessage('Hello, world!', opts);
|
||||
expect(onStart).toHaveBeenCalledWith(expect.objectContaining({ text: 'Hello, world!' }));
|
||||
});
|
||||
|
||||
test('saveMessageToDatabase is called with the correct arguments', async () => {
|
||||
const saveOptions = TestClient.getSaveOptions();
|
||||
const user = {}; // Mock user
|
||||
const opts = { user };
|
||||
await TestClient.sendMessage('Hello, world!', opts);
|
||||
expect(TestClient.saveMessageToDatabase).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sender: expect.any(String),
|
||||
text: expect.any(String),
|
||||
isCreatedByUser: expect.any(Boolean),
|
||||
messageId: expect.any(String),
|
||||
parentMessageId: expect.any(String),
|
||||
conversationId: expect.any(String),
|
||||
}),
|
||||
saveOptions,
|
||||
user,
|
||||
);
|
||||
});
|
||||
|
||||
test('sendCompletion is called with the correct arguments', async () => {
|
||||
const payload = {}; // Mock payload
|
||||
TestClient.buildMessages.mockReturnValue({ prompt: payload, tokenCountMap: null });
|
||||
const opts = {};
|
||||
await TestClient.sendMessage('Hello, world!', opts);
|
||||
expect(TestClient.sendCompletion).toHaveBeenCalledWith(payload, opts);
|
||||
});
|
||||
|
||||
test('getTokenCountForResponse is called with the correct arguments', async () => {
|
||||
const tokenCountMap = {}; // Mock tokenCountMap
|
||||
TestClient.buildMessages.mockReturnValue({ prompt: [], tokenCountMap });
|
||||
TestClient.getTokenCountForResponse = jest.fn();
|
||||
const response = await TestClient.sendMessage('Hello, world!', {});
|
||||
expect(TestClient.getTokenCountForResponse).toHaveBeenCalledWith(response);
|
||||
});
|
||||
|
||||
test('returns an object with the correct shape', async () => {
|
||||
const response = await TestClient.sendMessage('Hello, world!', {});
|
||||
expect(response).toEqual(
|
||||
expect.objectContaining({
|
||||
sender: expect.any(String),
|
||||
text: expect.any(String),
|
||||
isCreatedByUser: expect.any(Boolean),
|
||||
messageId: expect.any(String),
|
||||
parentMessageId: expect.any(String),
|
||||
conversationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
193
api/app/clients/specs/FakeClient.js
Normal file
193
api/app/clients/specs/FakeClient.js
Normal file
@@ -0,0 +1,193 @@
|
||||
const crypto = require('crypto');
|
||||
const BaseClient = require('../BaseClient');
|
||||
const { maxTokensMap } = require('../../../utils');
|
||||
|
||||
class FakeClient extends BaseClient {
|
||||
constructor(apiKey, options = {}) {
|
||||
super(apiKey, options);
|
||||
this.sender = 'AI Assistant';
|
||||
this.setOptions(options);
|
||||
}
|
||||
setOptions(options) {
|
||||
if (this.options && !this.options.replaceOptions) {
|
||||
this.options.modelOptions = {
|
||||
...this.options.modelOptions,
|
||||
...options.modelOptions,
|
||||
};
|
||||
delete options.modelOptions;
|
||||
this.options = {
|
||||
...this.options,
|
||||
...options,
|
||||
};
|
||||
} else {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
if (this.options.openaiApiKey) {
|
||||
this.apiKey = this.options.openaiApiKey;
|
||||
}
|
||||
|
||||
const modelOptions = this.options.modelOptions || {};
|
||||
if (!this.modelOptions) {
|
||||
this.modelOptions = {
|
||||
...modelOptions,
|
||||
model: modelOptions.model || 'gpt-3.5-turbo',
|
||||
temperature:
|
||||
typeof modelOptions.temperature === 'undefined' ? 0.8 : modelOptions.temperature,
|
||||
top_p: typeof modelOptions.top_p === 'undefined' ? 1 : modelOptions.top_p,
|
||||
presence_penalty:
|
||||
typeof modelOptions.presence_penalty === 'undefined' ? 1 : modelOptions.presence_penalty,
|
||||
stop: modelOptions.stop,
|
||||
};
|
||||
}
|
||||
|
||||
this.maxContextTokens = maxTokensMap[this.modelOptions.model] ?? 4097;
|
||||
}
|
||||
getCompletion() {}
|
||||
buildMessages() {}
|
||||
getTokenCount(str) {
|
||||
return str.length;
|
||||
}
|
||||
getTokenCountForMessage(message) {
|
||||
return message?.content?.length || message.length;
|
||||
}
|
||||
}
|
||||
|
||||
const initializeFakeClient = (apiKey, options, fakeMessages) => {
|
||||
let TestClient = new FakeClient(apiKey);
|
||||
TestClient.options = options;
|
||||
TestClient.abortController = { abort: jest.fn() };
|
||||
TestClient.saveMessageToDatabase = jest.fn();
|
||||
TestClient.loadHistory = jest
|
||||
.fn()
|
||||
.mockImplementation((conversationId, parentMessageId = null) => {
|
||||
if (!conversationId) {
|
||||
TestClient.currentMessages = [];
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const orderedMessages = TestClient.constructor.getMessagesForConversation(
|
||||
fakeMessages,
|
||||
parentMessageId,
|
||||
);
|
||||
|
||||
TestClient.currentMessages = orderedMessages;
|
||||
return Promise.resolve(orderedMessages);
|
||||
});
|
||||
|
||||
TestClient.getSaveOptions = jest.fn().mockImplementation(() => {
|
||||
return {};
|
||||
});
|
||||
|
||||
TestClient.getBuildMessagesOptions = jest.fn().mockImplementation(() => {
|
||||
return {};
|
||||
});
|
||||
|
||||
TestClient.sendCompletion = jest.fn(async () => {
|
||||
return 'Mock response text';
|
||||
});
|
||||
|
||||
TestClient.sendMessage = jest.fn().mockImplementation(async (message, opts = {}) => {
|
||||
if (opts && typeof opts === 'object') {
|
||||
TestClient.setOptions(opts);
|
||||
}
|
||||
|
||||
const user = opts.user || null;
|
||||
const conversationId = opts.conversationId || crypto.randomUUID();
|
||||
const parentMessageId = opts.parentMessageId || '00000000-0000-0000-0000-000000000000';
|
||||
const userMessageId = opts.overrideParentMessageId || crypto.randomUUID();
|
||||
const saveOptions = TestClient.getSaveOptions();
|
||||
|
||||
this.pastMessages = await TestClient.loadHistory(
|
||||
conversationId,
|
||||
TestClient.options?.parentMessageId,
|
||||
);
|
||||
|
||||
const userMessage = {
|
||||
text: message,
|
||||
sender: TestClient.sender,
|
||||
isCreatedByUser: true,
|
||||
messageId: userMessageId,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
};
|
||||
|
||||
const response = {
|
||||
sender: TestClient.sender,
|
||||
text: 'Hello, User!',
|
||||
isCreatedByUser: false,
|
||||
messageId: crypto.randomUUID(),
|
||||
parentMessageId: userMessage.messageId,
|
||||
conversationId,
|
||||
};
|
||||
|
||||
fakeMessages.push(userMessage);
|
||||
fakeMessages.push(response);
|
||||
|
||||
if (typeof opts.getIds === 'function') {
|
||||
opts.getIds({
|
||||
userMessage,
|
||||
conversationId,
|
||||
responseMessageId: response.messageId,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof opts.onStart === 'function') {
|
||||
opts.onStart(userMessage);
|
||||
}
|
||||
|
||||
let { prompt: payload, tokenCountMap } = await TestClient.buildMessages(
|
||||
this.currentMessages,
|
||||
userMessage.messageId,
|
||||
TestClient.getBuildMessagesOptions(opts),
|
||||
);
|
||||
|
||||
if (tokenCountMap) {
|
||||
payload = payload.map((message, i) => {
|
||||
const { tokenCount, ...messageWithoutTokenCount } = message;
|
||||
// userMessage is always the last one in the payload
|
||||
if (i === payload.length - 1) {
|
||||
userMessage.tokenCount = message.tokenCount;
|
||||
console.debug(
|
||||
`Token count for user message: ${tokenCount}`,
|
||||
`Instruction Tokens: ${tokenCountMap.instructions || 'N/A'}`,
|
||||
);
|
||||
}
|
||||
return messageWithoutTokenCount;
|
||||
});
|
||||
TestClient.handleTokenCountMap(tokenCountMap);
|
||||
}
|
||||
|
||||
await TestClient.saveMessageToDatabase(userMessage, saveOptions, user);
|
||||
response.text = await TestClient.sendCompletion(payload, opts);
|
||||
if (tokenCountMap && TestClient.getTokenCountForResponse) {
|
||||
response.tokenCount = TestClient.getTokenCountForResponse(response);
|
||||
}
|
||||
await TestClient.saveMessageToDatabase(response, saveOptions, user);
|
||||
return response;
|
||||
});
|
||||
|
||||
TestClient.buildMessages = jest.fn(async (messages, parentMessageId) => {
|
||||
const orderedMessages = TestClient.constructor.getMessagesForConversation(
|
||||
messages,
|
||||
parentMessageId,
|
||||
);
|
||||
const formattedMessages = orderedMessages.map((message) => {
|
||||
let { role: _role, sender, text } = message;
|
||||
const role = _role ?? sender;
|
||||
const content = text ?? '';
|
||||
return {
|
||||
role: role?.toLowerCase() === 'user' ? 'user' : 'assistant',
|
||||
content,
|
||||
};
|
||||
});
|
||||
return {
|
||||
prompt: formattedMessages,
|
||||
tokenCountMap: null, // Simplified for the mock
|
||||
};
|
||||
});
|
||||
|
||||
return TestClient;
|
||||
};
|
||||
|
||||
module.exports = { FakeClient, initializeFakeClient };
|
||||
211
api/app/clients/specs/OpenAIClient.test.js
Normal file
211
api/app/clients/specs/OpenAIClient.test.js
Normal file
@@ -0,0 +1,211 @@
|
||||
const OpenAIClient = require('../OpenAIClient');
|
||||
|
||||
describe('OpenAIClient', () => {
|
||||
let client, client2;
|
||||
const model = 'gpt-4';
|
||||
const parentMessageId = '1';
|
||||
const messages = [
|
||||
{ role: 'user', sender: 'User', text: 'Hello', messageId: parentMessageId },
|
||||
{ role: 'assistant', sender: 'Assistant', text: 'Hi', messageId: '2' },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
const options = {
|
||||
// debug: true,
|
||||
openaiApiKey: 'new-api-key',
|
||||
modelOptions: {
|
||||
model,
|
||||
temperature: 0.7,
|
||||
},
|
||||
};
|
||||
client = new OpenAIClient('test-api-key', options);
|
||||
client2 = new OpenAIClient('test-api-key', options);
|
||||
client.refineMessages = jest.fn().mockResolvedValue({
|
||||
role: 'assistant',
|
||||
content: 'Refined answer',
|
||||
tokenCount: 30,
|
||||
});
|
||||
client.constructor.freeAndResetAllEncoders();
|
||||
});
|
||||
|
||||
describe('setOptions', () => {
|
||||
it('should set the options correctly', () => {
|
||||
expect(client.apiKey).toBe('new-api-key');
|
||||
expect(client.modelOptions.model).toBe(model);
|
||||
expect(client.modelOptions.temperature).toBe(0.7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectTokenizer', () => {
|
||||
it('should get the correct tokenizer based on the instance state', () => {
|
||||
const tokenizer = client.selectTokenizer();
|
||||
expect(tokenizer).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('freeAllTokenizers', () => {
|
||||
it('should free all tokenizers', () => {
|
||||
// Create a tokenizer
|
||||
const tokenizer = client.selectTokenizer();
|
||||
|
||||
// Mock 'free' method on the tokenizer
|
||||
tokenizer.free = jest.fn();
|
||||
|
||||
client.constructor.freeAndResetAllEncoders();
|
||||
|
||||
// Check if 'free' method has been called on the tokenizer
|
||||
expect(tokenizer.free).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTokenCount', () => {
|
||||
it('should return the correct token count', () => {
|
||||
const count = client.getTokenCount('Hello, world!');
|
||||
expect(count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should reset the encoder and count when count reaches 25', () => {
|
||||
const freeAndResetEncoderSpy = jest.spyOn(client.constructor, 'freeAndResetAllEncoders');
|
||||
|
||||
// Call getTokenCount 25 times
|
||||
for (let i = 0; i < 25; i++) {
|
||||
client.getTokenCount('test text');
|
||||
}
|
||||
|
||||
expect(freeAndResetEncoderSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not reset the encoder and count when count is less than 25', () => {
|
||||
const freeAndResetEncoderSpy = jest.spyOn(client.constructor, 'freeAndResetAllEncoders');
|
||||
freeAndResetEncoderSpy.mockClear();
|
||||
|
||||
// Call getTokenCount 24 times
|
||||
for (let i = 0; i < 24; i++) {
|
||||
client.getTokenCount('test text');
|
||||
}
|
||||
|
||||
expect(freeAndResetEncoderSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle errors and reset the encoder', () => {
|
||||
const freeAndResetEncoderSpy = jest.spyOn(client.constructor, 'freeAndResetAllEncoders');
|
||||
|
||||
// Mock encode function to throw an error
|
||||
client.selectTokenizer().encode = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Test error');
|
||||
});
|
||||
|
||||
client.getTokenCount('test text');
|
||||
|
||||
expect(freeAndResetEncoderSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not throw null pointer error when freeing the same encoder twice', () => {
|
||||
client.constructor.freeAndResetAllEncoders();
|
||||
client2.constructor.freeAndResetAllEncoders();
|
||||
|
||||
const count = client2.getTokenCount('test text');
|
||||
expect(count).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSaveOptions', () => {
|
||||
it('should return the correct save options', () => {
|
||||
const options = client.getSaveOptions();
|
||||
expect(options).toHaveProperty('chatGptLabel');
|
||||
expect(options).toHaveProperty('promptPrefix');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBuildMessagesOptions', () => {
|
||||
it('should return the correct build messages options', () => {
|
||||
const options = client.getBuildMessagesOptions({ promptPrefix: 'Hello' });
|
||||
expect(options).toHaveProperty('isChatCompletion');
|
||||
expect(options).toHaveProperty('promptPrefix');
|
||||
expect(options.promptPrefix).toBe('Hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildMessages', () => {
|
||||
it('should build messages correctly for chat completion', async () => {
|
||||
const result = await client.buildMessages(messages, parentMessageId, {
|
||||
isChatCompletion: true,
|
||||
});
|
||||
expect(result).toHaveProperty('prompt');
|
||||
});
|
||||
|
||||
it('should build messages correctly for non-chat completion', async () => {
|
||||
const result = await client.buildMessages(messages, parentMessageId, {
|
||||
isChatCompletion: false,
|
||||
});
|
||||
expect(result).toHaveProperty('prompt');
|
||||
});
|
||||
|
||||
it('should build messages correctly with a promptPrefix', async () => {
|
||||
const result = await client.buildMessages(messages, parentMessageId, {
|
||||
isChatCompletion: true,
|
||||
promptPrefix: 'Test Prefix',
|
||||
});
|
||||
expect(result).toHaveProperty('prompt');
|
||||
const instructions = result.prompt.find((item) => item.name === 'instructions');
|
||||
expect(instructions).toBeDefined();
|
||||
expect(instructions.content).toContain('Test Prefix');
|
||||
});
|
||||
|
||||
it('should handle context strategy correctly', async () => {
|
||||
client.contextStrategy = 'refine';
|
||||
const result = await client.buildMessages(messages, parentMessageId, {
|
||||
isChatCompletion: true,
|
||||
});
|
||||
expect(result).toHaveProperty('prompt');
|
||||
expect(result).toHaveProperty('tokenCountMap');
|
||||
});
|
||||
|
||||
it('should assign name property for user messages when options.name is set', async () => {
|
||||
client.options.name = 'Test User';
|
||||
const result = await client.buildMessages(messages, parentMessageId, {
|
||||
isChatCompletion: true,
|
||||
});
|
||||
const hasUserWithName = result.prompt.some(
|
||||
(item) => item.role === 'user' && item.name === 'Test User',
|
||||
);
|
||||
expect(hasUserWithName).toBe(true);
|
||||
});
|
||||
|
||||
it('should calculate tokenCount for each message when contextStrategy is set', async () => {
|
||||
client.contextStrategy = 'refine';
|
||||
const result = await client.buildMessages(messages, parentMessageId, {
|
||||
isChatCompletion: true,
|
||||
});
|
||||
const hasUserWithTokenCount = result.prompt.some(
|
||||
(item) => item.role === 'user' && item.tokenCount > 0,
|
||||
);
|
||||
expect(hasUserWithTokenCount).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle promptPrefix from options when promptPrefix argument is not provided', async () => {
|
||||
client.options.promptPrefix = 'Test Prefix from options';
|
||||
const result = await client.buildMessages(messages, parentMessageId, {
|
||||
isChatCompletion: true,
|
||||
});
|
||||
const instructions = result.prompt.find((item) => item.name === 'instructions');
|
||||
expect(instructions.content).toContain('Test Prefix from options');
|
||||
});
|
||||
|
||||
it('should handle case when neither promptPrefix argument nor options.promptPrefix is set', async () => {
|
||||
const result = await client.buildMessages(messages, parentMessageId, {
|
||||
isChatCompletion: true,
|
||||
});
|
||||
const instructions = result.prompt.find((item) => item.name === 'instructions');
|
||||
expect(instructions).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle case when getMessagesForConversation returns null or an empty array', async () => {
|
||||
const messages = [];
|
||||
const result = await client.buildMessages(messages, parentMessageId, {
|
||||
isChatCompletion: true,
|
||||
});
|
||||
expect(result.prompt).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,25 @@
|
||||
/*
|
||||
This is a test script to see how much memory is used by the client when encoding.
|
||||
On my work machine, it was able to process 10,000 encoding requests / 48.686 seconds = approximately 205.4 RPS
|
||||
I've significantly reduced the amount of encoding needed by saving token counts in the database, so these
|
||||
numbers should only be hit with a large amount of concurrent users
|
||||
It would take 103 concurrent users sending 1 message every 1 second to hit these numbers, which is rather unrealistic,
|
||||
and at that point, out-sourcing the encoding to a separate server would be a better solution
|
||||
Also, for scaling, could increase the rate at which the encoder resets; the trade-off is more resource usage on the server.
|
||||
Initial memory usage: 25.93 megabytes
|
||||
Peak memory usage: 55 megabytes
|
||||
Final memory usage: 28.03 megabytes
|
||||
Post-test (timeout of 15s): 21.91 megabytes
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const { OpenAIClient } = require('../');
|
||||
|
||||
function timeout(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
const { ChatGPTClient } = await import('@waylaidwanderer/chatgpt-api');
|
||||
const text = `
|
||||
The standard Lorem Ipsum passage, used since the 1500s
|
||||
|
||||
@@ -28,7 +46,7 @@ const run = async () => {
|
||||
model,
|
||||
},
|
||||
proxy: process.env.PROXY || null,
|
||||
debug: true
|
||||
debug: true,
|
||||
};
|
||||
|
||||
let apiKey = process.env.OPENAI_API_KEY;
|
||||
@@ -37,34 +55,39 @@ const run = async () => {
|
||||
|
||||
// Calculate initial percentage of memory used
|
||||
const initialMemoryUsage = process.memoryUsage().heapUsed;
|
||||
|
||||
|
||||
function printProgressBar(percentageUsed) {
|
||||
const filledBlocks = Math.round(percentageUsed / 2); // Each block represents 2%
|
||||
const emptyBlocks = 50 - filledBlocks; // Total blocks is 50 (each represents 2%), so the rest are empty
|
||||
const progressBar = '[' + '█'.repeat(filledBlocks) + ' '.repeat(emptyBlocks) + '] ' + percentageUsed.toFixed(2) + '%';
|
||||
const progressBar =
|
||||
'[' +
|
||||
'█'.repeat(filledBlocks) +
|
||||
' '.repeat(emptyBlocks) +
|
||||
'] ' +
|
||||
percentageUsed.toFixed(2) +
|
||||
'%';
|
||||
console.log(progressBar);
|
||||
}
|
||||
|
||||
const iterations = 16000;
|
||||
const iterations = 10000;
|
||||
console.time('loopTime');
|
||||
// Trying to catch the error doesn't help; all future calls will immediately crash
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
try {
|
||||
console.log(`Iteration ${i}`);
|
||||
const client = new ChatGPTClient(apiKey, clientOptions);
|
||||
const client = new OpenAIClient(apiKey, clientOptions);
|
||||
|
||||
client.getTokenCount(text);
|
||||
// const encoder = client.constructor.getTokenizer('cl100k_base');
|
||||
// console.log(`Iteration ${i}: call encode()...`);
|
||||
// encoder.encode(text, 'all');
|
||||
// encoder.free();
|
||||
|
||||
|
||||
const memoryUsageDuringLoop = process.memoryUsage().heapUsed;
|
||||
const percentageUsed = memoryUsageDuringLoop / maxMemory * 100;
|
||||
const percentageUsed = (memoryUsageDuringLoop / maxMemory) * 100;
|
||||
printProgressBar(percentageUsed);
|
||||
|
||||
if (i === (iterations - 1)) {
|
||||
if (i === iterations - 1) {
|
||||
console.log(' done');
|
||||
// encoder.free();
|
||||
}
|
||||
@@ -80,10 +103,23 @@ const run = async () => {
|
||||
// const finalPercentageUsed = finalMemoryUsage / maxMemory * 100;
|
||||
console.log(`Initial memory usage: ${initialMemoryUsage / 1024 / 1024} megabytes`);
|
||||
console.log(`Final memory usage: ${finalMemoryUsage / 1024 / 1024} megabytes`);
|
||||
setTimeout(() => {
|
||||
const memoryUsageAfterTimeout = process.memoryUsage().heapUsed;
|
||||
console.log(`Post timeout: ${memoryUsageAfterTimeout / 1024 / 1024} megabytes`);
|
||||
} , 10000);
|
||||
}
|
||||
await timeout(15000);
|
||||
const memoryUsageAfterTimeout = process.memoryUsage().heapUsed;
|
||||
console.log(`Post timeout: ${memoryUsageAfterTimeout / 1024 / 1024} megabytes`);
|
||||
};
|
||||
|
||||
run();
|
||||
run();
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
if (!err.message.includes('fetch failed')) {
|
||||
console.error('There was an uncaught error:');
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
if (err.message.includes('fetch failed')) {
|
||||
console.log('fetch failed error caught');
|
||||
// process.exit(0);
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
148
api/app/clients/specs/PluginsClient.test.js
Normal file
148
api/app/clients/specs/PluginsClient.test.js
Normal file
@@ -0,0 +1,148 @@
|
||||
const { HumanChatMessage, AIChatMessage } = require('langchain/schema');
|
||||
const PluginsClient = require('../PluginsClient');
|
||||
const crypto = require('crypto');
|
||||
|
||||
jest.mock('../../../lib/db/connectDb');
|
||||
jest.mock('../../../models/Conversation', () => {
|
||||
return function () {
|
||||
return {
|
||||
save: jest.fn(),
|
||||
deleteConvos: jest.fn(),
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
describe('PluginsClient', () => {
|
||||
let TestAgent;
|
||||
let options = {
|
||||
tools: [],
|
||||
modelOptions: {
|
||||
model: 'gpt-3.5-turbo',
|
||||
temperature: 0,
|
||||
max_tokens: 2,
|
||||
},
|
||||
agentOptions: {
|
||||
model: 'gpt-3.5-turbo',
|
||||
},
|
||||
};
|
||||
let parentMessageId;
|
||||
let conversationId;
|
||||
const fakeMessages = [];
|
||||
const userMessage = 'Hello, ChatGPT!';
|
||||
const apiKey = 'fake-api-key';
|
||||
|
||||
beforeEach(() => {
|
||||
TestAgent = new PluginsClient(apiKey, options);
|
||||
TestAgent.loadHistory = jest
|
||||
.fn()
|
||||
.mockImplementation((conversationId, parentMessageId = null) => {
|
||||
if (!conversationId) {
|
||||
TestAgent.currentMessages = [];
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const orderedMessages = TestAgent.constructor.getMessagesForConversation(
|
||||
fakeMessages,
|
||||
parentMessageId,
|
||||
);
|
||||
|
||||
const chatMessages = orderedMessages.map((msg) =>
|
||||
msg?.isCreatedByUser || msg?.role?.toLowerCase() === 'user'
|
||||
? new HumanChatMessage(msg.text)
|
||||
: new AIChatMessage(msg.text),
|
||||
);
|
||||
|
||||
TestAgent.currentMessages = orderedMessages;
|
||||
return Promise.resolve(chatMessages);
|
||||
});
|
||||
TestAgent.sendMessage = jest.fn().mockImplementation(async (message, opts = {}) => {
|
||||
if (opts && typeof opts === 'object') {
|
||||
TestAgent.setOptions(opts);
|
||||
}
|
||||
const conversationId = opts.conversationId || crypto.randomUUID();
|
||||
const parentMessageId = opts.parentMessageId || '00000000-0000-0000-0000-000000000000';
|
||||
const userMessageId = opts.overrideParentMessageId || crypto.randomUUID();
|
||||
this.pastMessages = await TestAgent.loadHistory(
|
||||
conversationId,
|
||||
TestAgent.options?.parentMessageId,
|
||||
);
|
||||
|
||||
const userMessage = {
|
||||
text: message,
|
||||
sender: 'ChatGPT',
|
||||
isCreatedByUser: true,
|
||||
messageId: userMessageId,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
};
|
||||
|
||||
const response = {
|
||||
sender: 'ChatGPT',
|
||||
text: 'Hello, User!',
|
||||
isCreatedByUser: false,
|
||||
messageId: crypto.randomUUID(),
|
||||
parentMessageId: userMessage.messageId,
|
||||
conversationId,
|
||||
};
|
||||
|
||||
fakeMessages.push(userMessage);
|
||||
fakeMessages.push(response);
|
||||
return response;
|
||||
});
|
||||
});
|
||||
|
||||
test('initializes PluginsClient without crashing', () => {
|
||||
expect(TestAgent).toBeInstanceOf(PluginsClient);
|
||||
});
|
||||
|
||||
test('check setOptions function', () => {
|
||||
expect(TestAgent.agentIsGpt3).toBe(true);
|
||||
});
|
||||
|
||||
describe('sendMessage', () => {
|
||||
test('sendMessage should return a response message', async () => {
|
||||
const expectedResult = expect.objectContaining({
|
||||
sender: 'ChatGPT',
|
||||
text: expect.any(String),
|
||||
isCreatedByUser: false,
|
||||
messageId: expect.any(String),
|
||||
parentMessageId: expect.any(String),
|
||||
conversationId: expect.any(String),
|
||||
});
|
||||
|
||||
const response = await TestAgent.sendMessage(userMessage);
|
||||
console.log(response);
|
||||
parentMessageId = response.messageId;
|
||||
conversationId = response.conversationId;
|
||||
expect(response).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
test('sendMessage should work with provided conversationId and parentMessageId', async () => {
|
||||
const userMessage = 'Second message in the conversation';
|
||||
const opts = {
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
};
|
||||
|
||||
const expectedResult = expect.objectContaining({
|
||||
sender: 'ChatGPT',
|
||||
text: expect.any(String),
|
||||
isCreatedByUser: false,
|
||||
messageId: expect.any(String),
|
||||
parentMessageId: expect.any(String),
|
||||
conversationId: opts.conversationId,
|
||||
});
|
||||
|
||||
const response = await TestAgent.sendMessage(userMessage, opts);
|
||||
parentMessageId = response.messageId;
|
||||
expect(response.conversationId).toEqual(conversationId);
|
||||
expect(response).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
test('should return chat history', async () => {
|
||||
const chatMessages = await TestAgent.loadHistory(conversationId, parentMessageId);
|
||||
expect(TestAgent.currentMessages).toHaveLength(4);
|
||||
expect(chatMessages[0].text).toEqual(userMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
18
api/app/clients/tools/.well-known/Ai_PDF.json
Normal file
18
api/app/clients/tools/.well-known/Ai_PDF.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Ai PDF",
|
||||
"name_for_model": "Ai_PDF",
|
||||
"description_for_human": "Super-fast, interactive chats with PDFs of any size, complete with page references for fact checking.",
|
||||
"description_for_model": "Provide a URL to a PDF and search the document. Break the user question in multiple semantic search queries and calls as needed. Think step by step.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://plugin-3c56b9d4c8a6465998395f28b6a445b2-jexkai4vea-uc.a.run.app/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://plugin-3c56b9d4c8a6465998395f28b6a445b2-jexkai4vea-uc.a.run.app/logo.png",
|
||||
"contact_email": "support@promptapps.ai",
|
||||
"legal_info_url": "https://plugin-3c56b9d4c8a6465998395f28b6a445b2-jexkai4vea-uc.a.run.app/legal.html"
|
||||
}
|
||||
18
api/app/clients/tools/.well-known/Diagrams.json
Normal file
18
api/app/clients/tools/.well-known/Diagrams.json
Normal file
File diff suppressed because one or more lines are too long
89
api/app/clients/tools/.well-known/Dr_Thoths_Tarot.json
Normal file
89
api/app/clients/tools/.well-known/Dr_Thoths_Tarot.json
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Dr. Thoth's Tarot",
|
||||
"name_for_model": "Dr_Thoths_Tarot",
|
||||
"description_for_human": "Tarot card novelty entertainment & analysis, by Mnemosyne Labs.",
|
||||
"description_for_model": "Intelligent analysis program for tarot card entertaiment, data, & prompts, by Mnemosyne Labs, a division of AzothCorp.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://dr-thoth-tarot.herokuapp.com/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://dr-thoth-tarot.herokuapp.com/logo.png",
|
||||
"contact_email": "legal@AzothCorp.com",
|
||||
"legal_info_url": "http://AzothCorp.com/legal",
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "Draw Card",
|
||||
"path": "/drawcard",
|
||||
"method": "GET",
|
||||
"description": "Generate a single tarot card from the deck of 78 cards."
|
||||
},
|
||||
{
|
||||
"name": "Occult Card",
|
||||
"path": "/occult_card",
|
||||
"method": "GET",
|
||||
"description": "Generate a tarot card using the specified planet's Kamea matrix.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "planet",
|
||||
"type": "string",
|
||||
"enum": ["Saturn", "Jupiter", "Mars", "Sun", "Venus", "Mercury", "Moon"],
|
||||
"required": true,
|
||||
"description": "The planet name to use the corresponding Kamea matrix."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Three Card Spread",
|
||||
"path": "/threecardspread",
|
||||
"method": "GET",
|
||||
"description": "Perform a three-card tarot spread."
|
||||
},
|
||||
{
|
||||
"name": "Celtic Cross Spread",
|
||||
"path": "/celticcross",
|
||||
"method": "GET",
|
||||
"description": "Perform a Celtic Cross tarot spread with 10 cards."
|
||||
},
|
||||
{
|
||||
"name": "Past, Present, Future Spread",
|
||||
"path": "/pastpresentfuture",
|
||||
"method": "GET",
|
||||
"description": "Perform a Past, Present, Future tarot spread with 3 cards."
|
||||
},
|
||||
{
|
||||
"name": "Horseshoe Spread",
|
||||
"path": "/horseshoe",
|
||||
"method": "GET",
|
||||
"description": "Perform a Horseshoe tarot spread with 7 cards."
|
||||
},
|
||||
{
|
||||
"name": "Relationship Spread",
|
||||
"path": "/relationship",
|
||||
"method": "GET",
|
||||
"description": "Perform a Relationship tarot spread."
|
||||
},
|
||||
{
|
||||
"name": "Career Spread",
|
||||
"path": "/career",
|
||||
"method": "GET",
|
||||
"description": "Perform a Career tarot spread."
|
||||
},
|
||||
{
|
||||
"name": "Yes/No Spread",
|
||||
"path": "/yesno",
|
||||
"method": "GET",
|
||||
"description": "Perform a Yes/No tarot spread."
|
||||
},
|
||||
{
|
||||
"name": "Chakra Spread",
|
||||
"path": "/chakra",
|
||||
"method": "GET",
|
||||
"description": "Perform a Chakra tarot spread with 7 cards."
|
||||
}
|
||||
]
|
||||
}
|
||||
18
api/app/clients/tools/.well-known/DreamInterpreter.json
Normal file
18
api/app/clients/tools/.well-known/DreamInterpreter.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_model": "DreamInterpreter",
|
||||
"name_for_human": "Dream Interpreter",
|
||||
"description_for_model": "Interprets your dreams using advanced techniques.",
|
||||
"description_for_human": "Interprets your dreams using advanced techniques.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://dreamplugin.bgnetmobile.com/.well-known/openapi.json",
|
||||
"has_user_authentication": false
|
||||
},
|
||||
"logo_url": "https://dreamplugin.bgnetmobile.com/.well-known/logo.png",
|
||||
"contact_email": "ismail.orkler@bgnetmobile.com",
|
||||
"legal_info_url": "https://dreamplugin.bgnetmobile.com/terms.html"
|
||||
}
|
||||
22
api/app/clients/tools/.well-known/VoxScript.json
Normal file
22
api/app/clients/tools/.well-known/VoxScript.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "VoxScript",
|
||||
"name_for_model": "VoxScript",
|
||||
"description_for_human": "Enables searching of YouTube transcripts, financial data sources Google Search results, and more!",
|
||||
"description_for_model": "Plugin for searching through varius data sources.",
|
||||
"auth": {
|
||||
"type": "service_http",
|
||||
"authorization_type": "bearer",
|
||||
"verification_tokens": {
|
||||
"openai": "ffc5226d1af346c08a98dee7deec9f76"
|
||||
}
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://voxscript.awt.icu/swagger/v1/swagger.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://voxscript.awt.icu/images/VoxScript_logo_32x32.png",
|
||||
"contact_email": "voxscript@allwiretech.com",
|
||||
"legal_info_url": "https://voxscript.awt.icu/legal/"
|
||||
}
|
||||
22
api/app/clients/tools/.well-known/aitoolhunt.json
Normal file
22
api/app/clients/tools/.well-known/aitoolhunt.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Ai Tool Hunt",
|
||||
"name_for_model": "aitoolhunt",
|
||||
"description_for_human": "Find the perfect AI tools for all your needs, drawn from the most comprehensive global database of AI tools.",
|
||||
"description_for_model": "This universal plugin is designed to identify and suggest AI tools for any imaginable task or challenge. It harnesses a database of over 1600+ AI tools and 120+ categories, ensuring a solution for every requirement. Simply input any task, problem, or use case, and the plugin will generate a tailored list of AI tools. Note: it's crucial to rearrange the tools suggested by the plugin based on their relevance to the user's specific use case. This plugin can tackle everything from the simplest to the most intricate tasks. If there's a way an AI tool can help, this plugin will not only locate it but also customize the list to the user's unique needs. Utilize this plugin whenever a task or activity emerges, regardless of its context or complexity. When formulating a search query, avoid stop words or other irrelevant keywords. For instance, 'copywriting' is acceptable, but 'ai for copywriting' is not. If you believe none of the suggested tools are a suitable match for the user's needs, indicate that these are related tools.",
|
||||
"auth": {
|
||||
"type": "service_http",
|
||||
"authorization_type": "bearer",
|
||||
"verification_tokens": {
|
||||
"openai": "06a0f9391a5e48c7a7eeaca1e7e1e8d3"
|
||||
}
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://www.aitoolhunt.com/openapi.json",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://www.aitoolhunt.com/images/aitoolhunt_logo.png",
|
||||
"contact_email": "aitoolhunt@gmail.com",
|
||||
"legal_info_url": "https://www.aitoolhunt.com/terms-and-conditions"
|
||||
}
|
||||
18
api/app/clients/tools/.well-known/askyourpdf.json
Normal file
18
api/app/clients/tools/.well-known/askyourpdf.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_model": "askyourpdf",
|
||||
"name_for_human": "AskYourPDF",
|
||||
"description_for_model": "This plugin is designed to expedite the extraction of information from PDF documents. It works by accepting a URL link to a PDF or a document ID (doc_id) from the user. If a URL is provided, the plugin first validates that it is a correct URL. \\nAfter validating the URL, the plugin proceeds to download the PDF and store its content in a vector database. If the user provides a doc_id, the plugin directly retrieves the document from the database. The plugin then scans through the stored PDFs to find answers to user queries or retrieve specific details.\\n\\nHowever, if an error occurs while querying the API, the user is prompted to download their document first, then manually upload it to [](https://askyourpdf.com/upload). Once the upload is complete, the user should copy the resulting doc_id and paste it back into the chat for further interaction.\nThe plugin is particularly useful when the user's question pertains to content within a PDF document. When providing answers, the plugin also specifies the page number (highlighted in bold) where the relevant information was found. Remember, the URL must be valid for a successful query. Failure to validate the URL may lead to errors or unsuccessful queries.",
|
||||
"description_for_human": "Unlock the power of your PDFs!, dive into your documents, find answers, and bring information to your fingertips.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "askyourpdf.yaml",
|
||||
"has_user_authentication": false
|
||||
},
|
||||
"logo_url": "https://plugin.askyourpdf.com/.well-known/logo.png",
|
||||
"contact_email": "plugin@askyourpdf.com",
|
||||
"legal_info_url": "https://askyourpdf.com/terms"
|
||||
}
|
||||
18
api/app/clients/tools/.well-known/drink_maestro.json
Normal file
18
api/app/clients/tools/.well-known/drink_maestro.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Drink Maestro",
|
||||
"name_for_model": "drink_maestro",
|
||||
"description_for_human": "Learn to mix any drink you can imagine (real or made-up), and discover new ones. Includes drink images.",
|
||||
"description_for_model": "You are a silly bartender/comic who knows how to make any drink imaginable. You provide recipes for specific drinks, suggest new drinks, and show pictures of drinks. Be creative in your descriptions and make jokes and puns. Use a lot of emojis. If the user makes a request in another language, send API call in English, and then translate the response.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://api.drinkmaestro.space/.well-known/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://i.imgur.com/6q8HWdz.png",
|
||||
"contact_email": "nikkmitchell@gmail.com",
|
||||
"legal_info_url": "https://github.com/nikkmitchell/DrinkMaestro/blob/main/Legal.txt"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Earth",
|
||||
"name_for_model": "earthImagesAndVisualizations",
|
||||
"description_for_human": "Generates a map image based on provided location, tilt and style.",
|
||||
"description_for_model": "Generates a map image based on provided coordinates or location, tilt and style, and even geoJson to provide markers, paths, and polygons. Responds with an image-link. For the styles choose one of these: [light, dark, streets, outdoors, satellite, satellite-streets]",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://api.earth-plugin.com/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://api.earth-plugin.com/logo.png",
|
||||
"contact_email": "contact@earth-plugin.com",
|
||||
"legal_info_url": "https://api.earth-plugin.com/legal.html"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Scholarly Graph Link",
|
||||
"name_for_model": "scholarly_graph_link",
|
||||
"description_for_human": "You can search papers, authors, datasets and software. It has access to Figshare, Arxiv, and many others.",
|
||||
"description_for_model": "Run GraphQL queries against an API hosted by DataCite API. The API supports most GraphQL query but does not support mutations statements. Use `{ __schema { types { name kind } } }` to get all the types in the GraphQL schema. Use `{ datasets { nodes { id sizes citations { nodes { id titles { title } } } } } }` to get all the citations of all datasets in the API. Use `{ datasets { nodes { id sizes citations { nodes { id titles { title } } } } } }` to get all the citations of all datasets in the API. Use `{person(id:ORCID) {works(first:50) {nodes {id titles(first: 1){title} publicationYear}}}}` to get the first 50 works of a person based on their ORCID. All Ids are urls, e.g., https://orcid.org/0012-0000-1012-1110. Mutations statements are not allowed.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://api.datacite.org/graphql-openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://raw.githubusercontent.com/kjgarza/scholarly_graph_link/master/logo.png",
|
||||
"contact_email": "kj.garza@gmail.com",
|
||||
"legal_info_url": "https://github.com/kjgarza/scholarly_graph_link/blob/master/LICENSE"
|
||||
}
|
||||
24
api/app/clients/tools/.well-known/has-issues/web_pilot.json
Normal file
24
api/app/clients/tools/.well-known/has-issues/web_pilot.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "WebPilot",
|
||||
"name_for_model": "web_pilot",
|
||||
"description_for_human": "Browse & QA Webpage/PDF/Data. Generate articles, from one or more URLs.",
|
||||
"description_for_model": "This tool allows users to provide a URL(or URLs) and optionally requests for interacting with, extracting specific information or how to do with the content from the URL. Requests may include rewrite, translate, and others. If there any requests, when accessing the /api/visit-web endpoint, the parameter 'user_has_request' should be set to 'true. And if there's no any requests, 'user_has_request' should be set to 'false'.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://webreader.webpilotai.com/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://webreader.webpilotai.com/logo.png",
|
||||
"contact_email": "dev@webpilot.ai",
|
||||
"legal_info_url": "https://webreader.webpilotai.com/legal_info.html",
|
||||
"headers": {
|
||||
"id": "WebPilot-Friend-UID"
|
||||
},
|
||||
"params": {
|
||||
"user_has_request": true
|
||||
}
|
||||
}
|
||||
18
api/app/clients/tools/.well-known/image_prompt_enhancer.json
Normal file
18
api/app/clients/tools/.well-known/image_prompt_enhancer.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Image Prompt Enhancer",
|
||||
"name_for_model": "image_prompt_enhancer",
|
||||
"description_for_human": "Transform your ideas into complex, personalized image generation prompts.",
|
||||
"description_for_model": "Provides instructions for crafting an enhanced image prompt. Use this whenever the user wants to enhance a prompt.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://image-prompt-enhancer.gafo.tech/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://image-prompt-enhancer.gafo.tech/logo.png",
|
||||
"contact_email": "gafotech1@gmail.com",
|
||||
"legal_info_url": "https://image-prompt-enhancer.gafo.tech/legal"
|
||||
}
|
||||
157
api/app/clients/tools/.well-known/openapi/askyourpdf.yaml
Normal file
157
api/app/clients/tools/.well-known/openapi/askyourpdf.yaml
Normal file
@@ -0,0 +1,157 @@
|
||||
openapi: 3.0.2
|
||||
info:
|
||||
title: FastAPI
|
||||
version: 0.1.0
|
||||
servers:
|
||||
- url: https://plugin.askyourpdf.com
|
||||
paths:
|
||||
/api/download_pdf:
|
||||
post:
|
||||
summary: Download Pdf
|
||||
description: Download a PDF file from a URL and save it to the vector database.
|
||||
operationId: download_pdf_api_download_pdf_post
|
||||
parameters:
|
||||
- required: true
|
||||
schema:
|
||||
title: Url
|
||||
type: string
|
||||
name: url
|
||||
in: query
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/FileResponse'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/query:
|
||||
post:
|
||||
summary: Perform Query
|
||||
description: Perform a query on a document.
|
||||
operationId: perform_query_query_post
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/InputData'
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ResponseModel'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
components:
|
||||
schemas:
|
||||
DocumentMetadata:
|
||||
title: DocumentMetadata
|
||||
required:
|
||||
- source
|
||||
- page_number
|
||||
- author
|
||||
type: object
|
||||
properties:
|
||||
source:
|
||||
title: Source
|
||||
type: string
|
||||
page_number:
|
||||
title: Page Number
|
||||
type: integer
|
||||
author:
|
||||
title: Author
|
||||
type: string
|
||||
FileResponse:
|
||||
title: FileResponse
|
||||
required:
|
||||
- docId
|
||||
type: object
|
||||
properties:
|
||||
docId:
|
||||
title: Docid
|
||||
type: string
|
||||
error:
|
||||
title: Error
|
||||
type: string
|
||||
HTTPValidationError:
|
||||
title: HTTPValidationError
|
||||
type: object
|
||||
properties:
|
||||
detail:
|
||||
title: Detail
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
InputData:
|
||||
title: InputData
|
||||
required:
|
||||
- doc_id
|
||||
- query
|
||||
type: object
|
||||
properties:
|
||||
doc_id:
|
||||
title: Doc Id
|
||||
type: string
|
||||
query:
|
||||
title: Query
|
||||
type: string
|
||||
ResponseModel:
|
||||
title: ResponseModel
|
||||
required:
|
||||
- results
|
||||
type: object
|
||||
properties:
|
||||
results:
|
||||
title: Results
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/SearchResult'
|
||||
SearchResult:
|
||||
title: SearchResult
|
||||
required:
|
||||
- doc_id
|
||||
- text
|
||||
- metadata
|
||||
type: object
|
||||
properties:
|
||||
doc_id:
|
||||
title: Doc Id
|
||||
type: string
|
||||
text:
|
||||
title: Text
|
||||
type: string
|
||||
metadata:
|
||||
$ref: '#/components/schemas/DocumentMetadata'
|
||||
ValidationError:
|
||||
title: ValidationError
|
||||
required:
|
||||
- loc
|
||||
- msg
|
||||
- type
|
||||
type: object
|
||||
properties:
|
||||
loc:
|
||||
title: Location
|
||||
type: array
|
||||
items:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: integer
|
||||
msg:
|
||||
title: Message
|
||||
type: string
|
||||
type:
|
||||
title: Error Type
|
||||
type: string
|
||||
185
api/app/clients/tools/.well-known/openapi/scholarai.yaml
Normal file
185
api/app/clients/tools/.well-known/openapi/scholarai.yaml
Normal file
@@ -0,0 +1,185 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: ScholarAI
|
||||
description: Allows the user to search facts and findings from scientific articles
|
||||
version: 'v1'
|
||||
servers:
|
||||
- url: https://scholar-ai.net
|
||||
paths:
|
||||
/api/abstracts:
|
||||
get:
|
||||
operationId: searchAbstracts
|
||||
summary: Get relevant paper abstracts by keywords search
|
||||
parameters:
|
||||
- name: keywords
|
||||
in: query
|
||||
description: Keywords of inquiry which should appear in article. Must be in English.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: sort
|
||||
in: query
|
||||
description: The sort order for results. Valid values are cited_by_count or publication_date. Excluding this value does a relevance based search.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- cited_by_count
|
||||
- publication_date
|
||||
- name: query
|
||||
in: query
|
||||
description: The user query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: peer_reviewed_only
|
||||
in: query
|
||||
description: Whether to only return peer reviewed articles. Defaults to true, ChatGPT should cautiously suggest this value can be set to false
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: start_year
|
||||
in: query
|
||||
description: The first year, inclusive, to include in the search range. Excluding this value will include all years.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: end_year
|
||||
in: query
|
||||
description: The last year, inclusive, to include in the search range. Excluding this value will include all years.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: offset
|
||||
in: query
|
||||
description: The offset of the first result to return. Defaults to 0.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/searchAbstractsResponse'
|
||||
/api/fulltext:
|
||||
get:
|
||||
operationId: getFullText
|
||||
summary: Get full text of a paper by URL for PDF
|
||||
parameters:
|
||||
- name: pdf_url
|
||||
in: query
|
||||
description: URL for PDF
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: chunk
|
||||
in: query
|
||||
description: chunk number to retrieve, defaults to 1
|
||||
required: false
|
||||
schema:
|
||||
type: number
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/getFullTextResponse'
|
||||
/api/save-citation:
|
||||
get:
|
||||
operationId: saveCitation
|
||||
summary: Save citation to reference manager
|
||||
parameters:
|
||||
- name: doi
|
||||
in: query
|
||||
description: Digital Object Identifier (DOI) of article
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: zotero_user_id
|
||||
in: query
|
||||
description: Zotero User ID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: zotero_api_key
|
||||
in: query
|
||||
description: Zotero API Key
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/saveCitationResponse'
|
||||
components:
|
||||
schemas:
|
||||
searchAbstractsResponse:
|
||||
type: object
|
||||
properties:
|
||||
next_offset:
|
||||
type: number
|
||||
description: The offset of the next page of results.
|
||||
total_num_results:
|
||||
type: number
|
||||
description: The total number of results.
|
||||
abstracts:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
abstract:
|
||||
type: string
|
||||
description: Summary of the context, methods, results, and conclusions of the paper.
|
||||
doi:
|
||||
type: string
|
||||
description: The DOI of the paper.
|
||||
landing_page_url:
|
||||
type: string
|
||||
description: Link to the paper on its open-access host.
|
||||
pdf_url:
|
||||
type: string
|
||||
description: Link to the paper PDF.
|
||||
publicationDate:
|
||||
type: string
|
||||
description: The date the paper was published in YYYY-MM-DD format.
|
||||
relevance:
|
||||
type: number
|
||||
description: The relevance of the paper to the search query. 1 is the most relevant.
|
||||
creators:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: The name of the creator.
|
||||
cited_by_count:
|
||||
type: number
|
||||
description: The number of citations of the article.
|
||||
description: The list of relevant abstracts.
|
||||
getFullTextResponse:
|
||||
type: object
|
||||
properties:
|
||||
full_text:
|
||||
type: string
|
||||
description: The full text of the paper.
|
||||
pdf_url:
|
||||
type: string
|
||||
description: The PDF URL of the paper.
|
||||
chunk:
|
||||
type: number
|
||||
description: The chunk of the paper.
|
||||
total_chunk_num:
|
||||
type: number
|
||||
description: The total chunks of the paper.
|
||||
saveCitationResponse:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
description: Confirmation of successful save or error message.
|
||||
17
api/app/clients/tools/.well-known/qrCodes.json
Normal file
17
api/app/clients/tools/.well-known/qrCodes.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "QR Codes",
|
||||
"name_for_model": "qrCodes",
|
||||
"description_for_human": "Create QR codes.",
|
||||
"description_for_model": "Plugin for generating QR codes.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://chatgpt-qrcode-46d7d4ebefc8.herokuapp.com/openapi.yaml"
|
||||
},
|
||||
"logo_url": "https://chatgpt-qrcode-46d7d4ebefc8.herokuapp.com/logo.png",
|
||||
"contact_email": "chrismountzou@gmail.com",
|
||||
"legal_info_url": "https://raw.githubusercontent.com/mountzou/qrCodeGPTv1/master/legal"
|
||||
}
|
||||
18
api/app/clients/tools/.well-known/rephrase.json
Normal file
18
api/app/clients/tools/.well-known/rephrase.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Prompt Perfect",
|
||||
"name_for_model": "rephrase",
|
||||
"description_for_human": "Type 'perfect' to craft the perfect prompt, every time.",
|
||||
"description_for_model": "Plugin that can rephrase user inputs to improve the quality of ChatGPT's responses. The plugin evaluates user inputs and, if necessary, transforms them into clearer, more specific, and contextual prompts. It processes a JSON object containing the user input to be rephrased and uses the GPT-3.5-turbo model for the rephrasing process. The rephrased input is then returned as raw data to be incorporated into ChatGPT's response. The user can initiate the plugin by typing 'perfect'.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://promptperfect.xyz/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://promptperfect.xyz/static/prompt_perfect_logo.png",
|
||||
"contact_email": "heyo@promptperfect.xyz",
|
||||
"legal_info_url": "https://promptperfect.xyz/static/terms.html"
|
||||
}
|
||||
22
api/app/clients/tools/.well-known/scholarai.json
Normal file
22
api/app/clients/tools/.well-known/scholarai.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "ScholarAI",
|
||||
"name_for_model": "scholarai",
|
||||
"description_for_human": "Unleash scientific research: search 40M+ peer-reviewed papers, explore scientific PDFs, and save to reference managers.",
|
||||
"description_for_model": "Access open access scientific literature from peer-reviewed journals. The abstract endpoint finds relevant papers based on 2 to 6 keywords. After getting abstracts, ALWAYS prompt the user offering to go into more detail. Use the fulltext endpoint to retrieve the entire paper's text and access specific details using the provided pdf_url, if available. ALWAYS hyperlink the pdf_url from the responses if available. Offer to dive into the fulltext or search for additional papers. Always ask if the user wants save any paper to the user’s Zotero reference manager by using the save-citation endpoint and providing the doi and requesting the user’s zotero_user_id and zotero_api_key.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "scholarai.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"params": {
|
||||
"sort": "cited_by_count"
|
||||
},
|
||||
"logo_url": "https://scholar-ai.net/logo.png",
|
||||
"contact_email": "lakshb429@gmail.com",
|
||||
"legal_info_url": "https://scholar-ai.net/legal.txt",
|
||||
"HttpAuthorizationType": "basic"
|
||||
}
|
||||
18
api/app/clients/tools/.well-known/uberchord.json
Normal file
18
api/app/clients/tools/.well-known/uberchord.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Uberchord",
|
||||
"name_for_model": "uberchord",
|
||||
"description_for_human": "Find guitar chord diagrams by specifying the chord name.",
|
||||
"description_for_model": "Fetch guitar chord diagrams, their positions on the guitar fretboard.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://guitarchords.pluginboost.com/.well-known/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://guitarchords.pluginboost.com/logo.png",
|
||||
"contact_email": "info.bluelightweb@gmail.com",
|
||||
"legal_info_url": "https://guitarchords.pluginboost.com/legal"
|
||||
}
|
||||
18
api/app/clients/tools/.well-known/web_search.json
Normal file
18
api/app/clients/tools/.well-known/web_search.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Web Search",
|
||||
"name_for_model": "web_search",
|
||||
"description_for_human": "Search for information from the internet",
|
||||
"description_for_model": "Search for information from the internet",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://websearch.plugsugar.com/api/openapi_yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://websearch.plugsugar.com/200x200.png",
|
||||
"contact_email": "support@plugsugar.com",
|
||||
"legal_info_url": "https://websearch.plugsugar.com/contact"
|
||||
}
|
||||
@@ -10,7 +10,6 @@ export interface AIPluginToolParams {
|
||||
model: BaseLanguageModel;
|
||||
}
|
||||
|
||||
|
||||
export interface PathParameter {
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -58,7 +57,7 @@ function extractShortVersion(openapiSpec) {
|
||||
const shortApiSpec = {
|
||||
openapi: fullApiSpec.openapi,
|
||||
info: fullApiSpec.info,
|
||||
paths: {}
|
||||
paths: {},
|
||||
};
|
||||
|
||||
for (let path in fullApiSpec.paths) {
|
||||
@@ -69,8 +68,8 @@ function extractShortVersion(openapiSpec) {
|
||||
operationId: fullApiSpec.paths[path][method].operationId,
|
||||
parameters: fullApiSpec.paths[path][method].parameters?.map((parameter) => ({
|
||||
name: parameter.name,
|
||||
description: parameter.description
|
||||
}))
|
||||
description: parameter.description,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -200,14 +199,16 @@ class AIPluginTool extends Tool {
|
||||
const apiUrlRes = await fetch(aiPluginJson.api.url, {});
|
||||
if (!apiUrlRes.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch API spec from ${aiPluginJson.api.url} with status ${apiUrlRes.status}`
|
||||
`Failed to fetch API spec from ${aiPluginJson.api.url} with status ${apiUrlRes.status}`,
|
||||
);
|
||||
}
|
||||
const apiUrlJson = await apiUrlRes.text();
|
||||
const shortApiSpec = extractShortVersion(apiUrlJson);
|
||||
return new AIPluginTool({
|
||||
name: aiPluginJson.name_for_model.toLowerCase(),
|
||||
description: `A \`tool\` to learn the API documentation for ${aiPluginJson.name_for_model.toLowerCase()}, after which you can use 'http_request' to make the actual API call. Short description of how to use the API's results: ${aiPluginJson.description_for_model})`,
|
||||
description: `A \`tool\` to learn the API documentation for ${aiPluginJson.name_for_model.toLowerCase()}, after which you can use 'http_request' to make the actual API call. Short description of how to use the API's results: ${
|
||||
aiPluginJson.description_for_model
|
||||
})`,
|
||||
apiSpec: `
|
||||
As an AI, your task is to identify the operationId of the relevant API path based on the condensed OpenAPI specifications provided.
|
||||
|
||||
@@ -229,7 +230,7 @@ ${shortApiSpec}
|
||||
\`\`\`
|
||||
`,
|
||||
openaiSpec: apiUrlJson,
|
||||
model: model
|
||||
model: model,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// To use this tool, you must pass in a configured OpenAIApi object.
|
||||
const fs = require('fs');
|
||||
const { Configuration, OpenAIApi } = require('openai');
|
||||
const { genAzureEndpoint } = require('../../../utils/genAzureEndpoints');
|
||||
// const { genAzureEndpoint } = require('../../../utils/genAzureEndpoints');
|
||||
const { Tool } = require('langchain/tools');
|
||||
const saveImageFromUrl = require('./saveImageFromUrl');
|
||||
const path = require('path');
|
||||
@@ -11,31 +11,31 @@ class OpenAICreateImage extends Tool {
|
||||
constructor(fields = {}) {
|
||||
super();
|
||||
|
||||
let apiKey = fields.OPENAI_API_KEY || process.env.OPENAI_API_KEY;
|
||||
let azureKey = fields.AZURE_OPENAI_API_KEY || process.env.AZURE_OPENAI_API_KEY;
|
||||
let apiKey = fields.DALLE_API_KEY || this.getApiKey();
|
||||
// let azureKey = fields.AZURE_API_KEY || process.env.AZURE_API_KEY;
|
||||
let config = { apiKey };
|
||||
|
||||
if (azureKey) {
|
||||
apiKey = azureKey;
|
||||
const azureConfig = {
|
||||
apiKey,
|
||||
azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME || fields.azureOpenAIApiInstanceName,
|
||||
azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME || fields.azureOpenAIApiDeploymentName,
|
||||
azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION || fields.azureOpenAIApiVersion
|
||||
};
|
||||
config = {
|
||||
apiKey,
|
||||
basePath: genAzureEndpoint({
|
||||
...azureConfig,
|
||||
}),
|
||||
baseOptions: {
|
||||
headers: { 'api-key': apiKey },
|
||||
params: {
|
||||
'api-version': azureConfig.azureOpenAIApiVersion // this might change. I got the current value from the sample code at https://oai.azure.com/portal/chat
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
// if (azureKey) {
|
||||
// apiKey = azureKey;
|
||||
// const azureConfig = {
|
||||
// apiKey,
|
||||
// azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME || fields.azureOpenAIApiInstanceName,
|
||||
// azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME || fields.azureOpenAIApiDeploymentName,
|
||||
// azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION || fields.azureOpenAIApiVersion
|
||||
// };
|
||||
// config = {
|
||||
// apiKey,
|
||||
// basePath: genAzureEndpoint({
|
||||
// ...azureConfig,
|
||||
// }),
|
||||
// baseOptions: {
|
||||
// headers: { 'api-key': apiKey },
|
||||
// params: {
|
||||
// 'api-version': azureConfig.azureOpenAIApiVersion // this might change. I got the current value from the sample code at https://oai.azure.com/portal/chat
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
this.openaiApi = new OpenAIApi(new Configuration(config));
|
||||
this.name = 'dall-e';
|
||||
this.description = `You can generate images with 'dall-e'. This tool is exclusively for visual content.
|
||||
@@ -46,18 +46,27 @@ Guidelines:
|
||||
"Subject: [subject], Style: [style], Color: [color], Details: [details], Emotion: [emotion]"
|
||||
- Generate images only once per human query unless explicitly requested by the user`;
|
||||
}
|
||||
// "Subject": "Mona Lisa",
|
||||
// "Style": "Chinese traditional painting",
|
||||
// "Color": "Mainly wash tones of ink, with small color blocks in some parts",
|
||||
// "Details": "Mona Lisa should have long hair, a silk dress, holding a fan. The background should have mountains and trees.",
|
||||
// "Emotion": "Serene and elegant"
|
||||
|
||||
getApiKey() {
|
||||
const apiKey = process.env.DALLE_API_KEY || '';
|
||||
if (!apiKey) {
|
||||
throw new Error('Missing DALLE_API_KEY environment variable.');
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
replaceUnwantedChars(inputString) {
|
||||
return inputString.replace(/\r\n|\r|\n/g, ' ').replace('"', '').trim();
|
||||
return inputString
|
||||
.replace(/\r\n|\r|\n/g, ' ')
|
||||
.replace('"', '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
getMarkdownImageUrl(imageName) {
|
||||
const imageUrl = path.join(this.relativeImageUrl, imageName).replace(/\\/g, '/').replace('public/', '');
|
||||
const imageUrl = path
|
||||
.join(this.relativeImageUrl, imageName)
|
||||
.replace(/\\/g, '/')
|
||||
.replace('public/', '');
|
||||
return ``;
|
||||
}
|
||||
|
||||
@@ -67,13 +76,13 @@ Guidelines:
|
||||
// TODO: Future idea -- could we ask an LLM to extract these arguments from an input that might contain them?
|
||||
n: 1,
|
||||
// size: '1024x1024'
|
||||
size: '512x512'
|
||||
size: '512x512',
|
||||
});
|
||||
|
||||
const theImageUrl = resp.data.data[0].url;
|
||||
|
||||
if (!theImageUrl) {
|
||||
throw new Error(`No image URL returned from OpenAI API.`);
|
||||
throw new Error('No image URL returned from OpenAI API.');
|
||||
}
|
||||
|
||||
const regex = /img-[\w\d]+.png/;
|
||||
@@ -23,7 +23,8 @@ class GoogleSearchAPI extends Tool {
|
||||
* A description for the agent to use
|
||||
* @type {string}
|
||||
*/
|
||||
description = `Use the 'google' tool to retrieve internet search results relevant to your input. The results will return links and snippets of text from the webpages`;
|
||||
description =
|
||||
'Use the \'google\' tool to retrieve internet search results relevant to your input. The results will return links and snippets of text from the webpages';
|
||||
|
||||
getCx() {
|
||||
const cx = process.env.GOOGLE_CSE_ID || '';
|
||||
@@ -79,7 +80,7 @@ class GoogleSearchAPI extends Tool {
|
||||
q: input,
|
||||
cx: this.cx,
|
||||
auth: this.apiKey,
|
||||
num: 5 // Limit the number of results to 5
|
||||
num: 5, // Limit the number of results to 5
|
||||
});
|
||||
|
||||
// return response.data;
|
||||
@@ -87,7 +88,7 @@ class GoogleSearchAPI extends Tool {
|
||||
|
||||
if (!response.data.items || response.data.items.length === 0) {
|
||||
return this.resultsToReadableFormat([
|
||||
{ title: 'No good Google Search Result was found', link: '' }
|
||||
{ title: 'No good Google Search Result was found', link: '' },
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -97,7 +98,7 @@ class GoogleSearchAPI extends Tool {
|
||||
for (const result of results) {
|
||||
const metadataResult = {
|
||||
title: result.title || '',
|
||||
link: result.link || ''
|
||||
link: result.link || '',
|
||||
};
|
||||
if (result.snippet) {
|
||||
metadataResult.snippet = result.snippet;
|
||||
@@ -6,7 +6,7 @@ const { Tool } = require('langchain/tools');
|
||||
// this.name = 'requests_get';
|
||||
// this.headers = headers;
|
||||
// this.maxOutputLength = maxOutputLength || 2000;
|
||||
// this.description = `A portal to the internet. Use this when you need to get specific content from a website.
|
||||
// this.description = `A portal to the internet. Use this when you need to get specific content from a website.
|
||||
// - Input should be a url (i.e. https://www.google.com). The output will be the text response of the GET request.`;
|
||||
// }
|
||||
|
||||
@@ -27,7 +27,7 @@ const { Tool } = require('langchain/tools');
|
||||
// this.maxOutputLength = maxOutputLength || Infinity;
|
||||
// this.description = `Use this when you want to POST to a website.
|
||||
// - Input should be a json string with two keys: "url" and "data".
|
||||
// - The value of "url" should be a string, and the value of "data" should be a dictionary of
|
||||
// - The value of "url" should be a string, and the value of "data" should be a dictionary of
|
||||
// - key-value pairs you want to POST to the url as a JSON body.
|
||||
// - Be careful to always use double quotes for strings in the json string
|
||||
// - The output will be the text response of the POST request.`;
|
||||
@@ -55,7 +55,8 @@ class HttpRequestTool extends Tool {
|
||||
this.headers = headers;
|
||||
this.name = 'http_request';
|
||||
this.maxOutputLength = maxOutputLength;
|
||||
this.description = `Executes HTTP methods (GET, POST, PUT, DELETE, etc.). The input is an object with three keys: "url", "method", and "data". Even for GET or DELETE, include "data" key as an empty string. "method" is the HTTP method, and "url" is the desired endpoint. If POST or PUT, "data" should contain a stringified JSON representing the body to send. Only one url per use.`;
|
||||
this.description =
|
||||
'Executes HTTP methods (GET, POST, PUT, DELETE, etc.). The input is an object with three keys: "url", "method", and "data". Even for GET or DELETE, include "data" key as an empty string. "method" is the HTTP method, and "url" is the desired endpoint. If POST or PUT, "data" should contain a stringified JSON representing the body to send. Only one url per use.';
|
||||
}
|
||||
|
||||
async _call(input) {
|
||||
@@ -63,23 +64,23 @@ class HttpRequestTool extends Tool {
|
||||
const urlPattern = /"url":\s*"([^"]*)"/;
|
||||
const methodPattern = /"method":\s*"([^"]*)"/;
|
||||
const dataPattern = /"data":\s*"([^"]*)"/;
|
||||
|
||||
|
||||
const url = input.match(urlPattern)[1];
|
||||
const method = input.match(methodPattern)[1];
|
||||
let data = input.match(dataPattern)[1];
|
||||
|
||||
|
||||
// Parse 'data' back to JSON if possible
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
// If it's not a JSON string, keep it as is
|
||||
}
|
||||
|
||||
|
||||
let options = {
|
||||
method: method,
|
||||
headers: this.headers
|
||||
headers: this.headers,
|
||||
};
|
||||
|
||||
|
||||
if (['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) && data) {
|
||||
if (typeof data === 'object') {
|
||||
options.body = JSON.stringify(data);
|
||||
@@ -88,20 +89,20 @@ class HttpRequestTool extends Tool {
|
||||
}
|
||||
options.headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
|
||||
const res = await fetch(url, options);
|
||||
|
||||
|
||||
const text = await res.text();
|
||||
if (text.includes('<html')) {
|
||||
return 'This tool is not designed to browse web pages. Only use it for API calls.';
|
||||
}
|
||||
|
||||
|
||||
return text.slice(0, this.maxOutputLength);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return `${error}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HttpRequestTool;
|
||||
28
api/app/clients/tools/SelfReflection.js
Normal file
28
api/app/clients/tools/SelfReflection.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const { Tool } = require('langchain/tools');
|
||||
|
||||
class SelfReflectionTool extends Tool {
|
||||
constructor({ message, isGpt3 }) {
|
||||
super();
|
||||
this.reminders = 0;
|
||||
this.name = 'self-reflection';
|
||||
this.description =
|
||||
'Take this action to reflect on your thoughts & actions. For your input, provide answers for self-evaluation as part of one input, using this space as a canvas to explore and organize your ideas in response to the user\'s message. You can use multiple lines for your input. Perform this action sparingly and only when you are stuck.';
|
||||
this.message = message;
|
||||
this.isGpt3 = isGpt3;
|
||||
// this.returnDirect = true;
|
||||
}
|
||||
|
||||
async _call(input) {
|
||||
return this.selfReflect(input);
|
||||
}
|
||||
|
||||
async selfReflect() {
|
||||
if (this.isGpt3) {
|
||||
return 'I should finalize my reply as soon as I have satisfied the user\'s query.';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SelfReflectionTool;
|
||||
@@ -26,7 +26,10 @@ Guidelines:
|
||||
}
|
||||
|
||||
getMarkdownImageUrl(imageName) {
|
||||
const imageUrl = path.join(this.relativeImageUrl, imageName).replace(/\\/g, '/').replace('public/', '');
|
||||
const imageUrl = path
|
||||
.join(this.relativeImageUrl, imageName)
|
||||
.replace(/\\/g, '/')
|
||||
.replace('public/', '');
|
||||
return ``;
|
||||
}
|
||||
|
||||
@@ -43,7 +46,7 @@ Guidelines:
|
||||
const payload = {
|
||||
prompt: input.split('|')[0],
|
||||
negative_prompt: input.split('|')[1],
|
||||
steps: 20
|
||||
steps: 20,
|
||||
};
|
||||
const response = await axios.post(`${url}/sdapi/v1/txt2img`, payload);
|
||||
const image = response.data.images[0];
|
||||
@@ -68,8 +71,8 @@ Guidelines:
|
||||
await sharp(buffer)
|
||||
.withMetadata({
|
||||
iptcpng: {
|
||||
parameters: info
|
||||
}
|
||||
parameters: info,
|
||||
},
|
||||
})
|
||||
.toFile(this.outputPath + '/' + imageName);
|
||||
this.result = this.getMarkdownImageUrl(imageName);
|
||||
@@ -71,7 +71,7 @@ General guidelines:
|
||||
console.log('Error data:', error.response.data);
|
||||
return error.response.data;
|
||||
} else {
|
||||
console.log(`Error querying Wolfram Alpha`, error.message);
|
||||
console.log('Error querying Wolfram Alpha', error.message);
|
||||
// throw error;
|
||||
return 'There was an error querying Wolfram Alpha.';
|
||||
}
|
||||
139
api/app/clients/tools/dynamic/OpenAPIPlugin.js
Normal file
139
api/app/clients/tools/dynamic/OpenAPIPlugin.js
Normal file
@@ -0,0 +1,139 @@
|
||||
require('dotenv').config();
|
||||
const { z } = require('zod');
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
const path = require('path');
|
||||
const { DynamicStructuredTool } = require('langchain/tools');
|
||||
const { createOpenAPIChain } = require('langchain/chains');
|
||||
const SUFFIX = 'Prioritize using responses for subsequent requests to better fulfill the query.';
|
||||
|
||||
const AuthBearer = z
|
||||
.object({
|
||||
type: z.string().includes('service_http'),
|
||||
authorization_type: z.string().includes('bearer'),
|
||||
verification_tokens: z.object({
|
||||
openai: z.string(),
|
||||
}),
|
||||
})
|
||||
.catch(() => false);
|
||||
|
||||
const AuthDefinition = z
|
||||
.object({
|
||||
type: z.string(),
|
||||
authorization_type: z.string(),
|
||||
verification_tokens: z.object({
|
||||
openai: z.string(),
|
||||
}),
|
||||
})
|
||||
.catch(() => false);
|
||||
|
||||
async function readSpecFile(filePath) {
|
||||
try {
|
||||
const fileContents = await fs.promises.readFile(filePath, 'utf8');
|
||||
if (path.extname(filePath) === '.json') {
|
||||
return JSON.parse(fileContents);
|
||||
}
|
||||
return yaml.load(fileContents);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getSpec(url) {
|
||||
const RegularUrl = z
|
||||
.string()
|
||||
.url()
|
||||
.catch(() => false);
|
||||
|
||||
if (RegularUrl.parse(url) && path.extname(url) === '.json') {
|
||||
const response = await fetch(url);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
const ValidSpecPath = z
|
||||
.string()
|
||||
.url()
|
||||
.catch(async () => {
|
||||
const spec = path.join(__dirname, '..', '.well-known', 'openapi', url);
|
||||
if (!fs.existsSync(spec)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await readSpecFile(spec);
|
||||
});
|
||||
|
||||
return ValidSpecPath.parse(url);
|
||||
}
|
||||
|
||||
async function createOpenAPIPlugin({ data, llm, user, message, verbose = false }) {
|
||||
let spec;
|
||||
try {
|
||||
spec = await getSpec(data.api.url, verbose);
|
||||
} catch (error) {
|
||||
verbose && console.debug('getSpec error', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!spec) {
|
||||
verbose && console.debug('No spec found');
|
||||
return null;
|
||||
}
|
||||
|
||||
const headers = {};
|
||||
const { auth, description_for_model } = data;
|
||||
if (auth && AuthDefinition.parse(auth)) {
|
||||
verbose && console.debug('auth detected', auth);
|
||||
const { openai } = auth.verification_tokens;
|
||||
if (AuthBearer.parse(auth)) {
|
||||
headers.authorization = `Bearer ${openai}`;
|
||||
verbose && console.debug('added auth bearer', headers);
|
||||
}
|
||||
}
|
||||
|
||||
return new DynamicStructuredTool({
|
||||
name: data.name_for_model,
|
||||
description: `${data.description_for_human} ${SUFFIX}`,
|
||||
schema: z.object({
|
||||
query: z
|
||||
.string()
|
||||
.describe(
|
||||
'For the query, be specific in a conversational manner. It will be interpreted by a human.',
|
||||
),
|
||||
}),
|
||||
func: async () => {
|
||||
const chainOptions = {
|
||||
llm,
|
||||
verbose,
|
||||
};
|
||||
|
||||
if (data.headers && data.headers['librechat_user_id']) {
|
||||
verbose && console.debug('id detected', headers);
|
||||
headers[data.headers['librechat_user_id']] = user;
|
||||
}
|
||||
|
||||
if (Object.keys(headers).length > 0) {
|
||||
verbose && console.debug('headers detected', headers);
|
||||
chainOptions.headers = headers;
|
||||
}
|
||||
|
||||
if (data.params) {
|
||||
verbose && console.debug('params detected', data.params);
|
||||
chainOptions.params = data.params;
|
||||
}
|
||||
|
||||
const chain = await createOpenAPIChain(spec, chainOptions);
|
||||
const result = await chain.run(
|
||||
`${message}\n\n||>Instructions: ${description_for_model}\n${SUFFIX}`,
|
||||
);
|
||||
console.log('api chain run result', result);
|
||||
return result;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSpec,
|
||||
readSpecFile,
|
||||
createOpenAPIPlugin,
|
||||
};
|
||||
65
api/app/clients/tools/dynamic/OpenAPIPlugin.spec.js
Normal file
65
api/app/clients/tools/dynamic/OpenAPIPlugin.spec.js
Normal file
@@ -0,0 +1,65 @@
|
||||
const fs = require('fs');
|
||||
const { createOpenAPIPlugin, getSpec, readSpecFile } = require('./OpenAPIPlugin');
|
||||
|
||||
jest.mock('node-fetch');
|
||||
jest.mock('fs', () => ({
|
||||
promises: {
|
||||
readFile: jest.fn(),
|
||||
},
|
||||
existsSync: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('readSpecFile', () => {
|
||||
it('reads JSON file correctly', async () => {
|
||||
fs.promises.readFile.mockResolvedValue(JSON.stringify({ test: 'value' }));
|
||||
const result = await readSpecFile('test.json');
|
||||
expect(result).toEqual({ test: 'value' });
|
||||
});
|
||||
|
||||
it('reads YAML file correctly', async () => {
|
||||
fs.promises.readFile.mockResolvedValue('test: value');
|
||||
const result = await readSpecFile('test.yaml');
|
||||
expect(result).toEqual({ test: 'value' });
|
||||
});
|
||||
|
||||
it('handles error correctly', async () => {
|
||||
fs.promises.readFile.mockRejectedValue(new Error('test error'));
|
||||
const result = await readSpecFile('test.json');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSpec', () => {
|
||||
it('fetches spec from url correctly', async () => {
|
||||
const parsedJson = await getSpec('https://www.instacart.com/.well-known/ai-plugin.json');
|
||||
const isObject = typeof parsedJson === 'object';
|
||||
expect(isObject).toEqual(true);
|
||||
});
|
||||
|
||||
it('reads spec from file correctly', async () => {
|
||||
fs.existsSync.mockReturnValue(true);
|
||||
fs.promises.readFile.mockResolvedValue(JSON.stringify({ test: 'value' }));
|
||||
const result = await getSpec('test.json');
|
||||
expect(result).toEqual({ test: 'value' });
|
||||
});
|
||||
|
||||
it('returns false when file does not exist', async () => {
|
||||
fs.existsSync.mockReturnValue(false);
|
||||
const result = await getSpec('test.json');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createOpenAPIPlugin', () => {
|
||||
it('returns null when getSpec throws an error', async () => {
|
||||
const result = await createOpenAPIPlugin({ data: { api: { url: 'invalid' } } });
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('returns null when no spec is found', async () => {
|
||||
const result = await createOpenAPIPlugin({});
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
// Add more tests here for different scenarios
|
||||
});
|
||||
23
api/app/clients/tools/index.js
Normal file
23
api/app/clients/tools/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const GoogleSearchAPI = require('./GoogleSearch');
|
||||
const HttpRequestTool = require('./HttpRequestTool');
|
||||
const AIPluginTool = require('./AIPluginTool');
|
||||
const OpenAICreateImage = require('./DALL-E');
|
||||
const StructuredSD = require('./structured/StableDiffusion');
|
||||
const StableDiffusionAPI = require('./StableDiffusion');
|
||||
const WolframAlphaAPI = require('./Wolfram');
|
||||
const StructuredWolfram = require('./structured/Wolfram');
|
||||
const SelfReflectionTool = require('./SelfReflection');
|
||||
const availableTools = require('./manifest.json');
|
||||
|
||||
module.exports = {
|
||||
availableTools,
|
||||
GoogleSearchAPI,
|
||||
HttpRequestTool,
|
||||
AIPluginTool,
|
||||
OpenAICreateImage,
|
||||
StableDiffusionAPI,
|
||||
StructuredSD,
|
||||
WolframAlphaAPI,
|
||||
StructuredWolfram,
|
||||
SelfReflectionTool,
|
||||
};
|
||||
@@ -8,12 +8,12 @@
|
||||
{
|
||||
"authField": "GOOGLE_CSE_ID",
|
||||
"label": "Google CSE ID",
|
||||
"description": "This is your Google Custom Search Engine ID. For instructions on how to obtain this, see <a href='https://github.com/danny-avila/chatgpt-clone/blob/main/guides/GOOGLE_SEARCH.md'>Our Docs</a>."
|
||||
"description": "This is your Google Custom Search Engine ID. For instructions on how to obtain this, see <a href='https://github.com/danny-avila/LibreChat/blob/main/docs/features/plugins/google_search.md'>Our Docs</a>."
|
||||
},
|
||||
{
|
||||
"authField": "GOOGLE_API_KEY",
|
||||
"label": "Google API Key",
|
||||
"description": "This is your Google Custom Search API Key. For instructions on how to obtain this, see <a href='https://github.com/danny-avila/chatgpt-clone/blob/main/guides/GOOGLE_SEARCH.md'>Our Docs</a>."
|
||||
"description": "This is your Google Custom Search API Key. For instructions on how to obtain this, see <a href='https://github.com/danny-avila/LibreChat/blob/main/docs/features/plugins/google_search.md'>Our Docs</a>."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -32,9 +32,9 @@
|
||||
},
|
||||
{
|
||||
"name": "Browser",
|
||||
"pluginKey": "browser",
|
||||
"pluginKey": "web-browser",
|
||||
"description": "Scrape and summarize webpage data",
|
||||
"icon": "/assets/web-browser.png",
|
||||
"icon": "/assets/web-browser.svg",
|
||||
"authConfig": [
|
||||
{
|
||||
"authField": "OPENAI_API_KEY",
|
||||
@@ -7,7 +7,7 @@ async function saveImageFromUrl(url, outputPath, outputFilename) {
|
||||
// Fetch the image from the URL
|
||||
const response = await axios({
|
||||
url,
|
||||
responseType: 'stream'
|
||||
responseType: 'stream',
|
||||
});
|
||||
|
||||
// Check if the output directory exists, if not, create it
|
||||
110
api/app/clients/tools/structured/StableDiffusion.js
Normal file
110
api/app/clients/tools/structured/StableDiffusion.js
Normal file
@@ -0,0 +1,110 @@
|
||||
// Generates image using stable diffusion webui's api (automatic1111)
|
||||
const fs = require('fs');
|
||||
const { StructuredTool } = require('langchain/tools');
|
||||
const { z } = require('zod');
|
||||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
const sharp = require('sharp');
|
||||
|
||||
class StableDiffusionAPI extends StructuredTool {
|
||||
constructor(fields) {
|
||||
super();
|
||||
this.name = 'stable-diffusion';
|
||||
this.url = fields.SD_WEBUI_URL || this.getServerURL();
|
||||
this.description = `You can generate images with 'stable-diffusion'. This tool is exclusively for visual content.
|
||||
Guidelines:
|
||||
- Visually describe the moods, details, structures, styles, and/or proportions of the image. Remember, the focus is on visual attributes.
|
||||
- Craft your input by "showing" and not "telling" the imagery. Think in terms of what you'd want to see in a photograph or a painting.
|
||||
- Here's an example for generating a realistic portrait photo of a man:
|
||||
"prompt":"photo of a man in black clothes, half body, high detailed skin, coastline, overcast weather, wind, waves, 8k uhd, dslr, soft lighting, high quality, film grain, Fujifilm XT3"
|
||||
"negative_prompt":"semi-realistic, cgi, 3d, render, sketch, cartoon, drawing, anime, out of frame, low quality, ugly, mutation, deformed"
|
||||
- Generate images only once per human query unless explicitly requested by the user`;
|
||||
this.schema = z.object({
|
||||
prompt: z
|
||||
.string()
|
||||
.describe(
|
||||
'Detailed keywords to describe the subject, using at least 7 keywords to accurately describe the image, separated by comma',
|
||||
),
|
||||
negative_prompt: z
|
||||
.string()
|
||||
.describe(
|
||||
'Keywords we want to exclude from the final image, using at least 7 keywords to accurately describe the image, separated by comma',
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
replaceNewLinesWithSpaces(inputString) {
|
||||
return inputString.replace(/\r\n|\r|\n/g, ' ');
|
||||
}
|
||||
|
||||
getMarkdownImageUrl(imageName) {
|
||||
const imageUrl = path
|
||||
.join(this.relativeImageUrl, imageName)
|
||||
.replace(/\\/g, '/')
|
||||
.replace('public/', '');
|
||||
return ``;
|
||||
}
|
||||
|
||||
getServerURL() {
|
||||
const url = process.env.SD_WEBUI_URL || '';
|
||||
if (!url) {
|
||||
throw new Error('Missing SD_WEBUI_URL environment variable.');
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
async _call(data) {
|
||||
const url = this.url;
|
||||
const { prompt, negative_prompt } = data;
|
||||
const payload = {
|
||||
prompt,
|
||||
negative_prompt,
|
||||
steps: 20,
|
||||
};
|
||||
const response = await axios.post(`${url}/sdapi/v1/txt2img`, payload);
|
||||
const image = response.data.images[0];
|
||||
const pngPayload = { image: `data:image/png;base64,${image}` };
|
||||
const response2 = await axios.post(`${url}/sdapi/v1/png-info`, pngPayload);
|
||||
const info = response2.data.info;
|
||||
|
||||
// Generate unique name
|
||||
const imageName = `${Date.now()}.png`;
|
||||
this.outputPath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'client',
|
||||
'public',
|
||||
'images',
|
||||
);
|
||||
const appRoot = path.resolve(__dirname, '..', '..', '..', '..', '..', 'client');
|
||||
this.relativeImageUrl = path.relative(appRoot, this.outputPath);
|
||||
|
||||
// Check if directory exists, if not create it
|
||||
if (!fs.existsSync(this.outputPath)) {
|
||||
fs.mkdirSync(this.outputPath, { recursive: true });
|
||||
}
|
||||
|
||||
try {
|
||||
const buffer = Buffer.from(image.split(',', 1)[0], 'base64');
|
||||
await sharp(buffer)
|
||||
.withMetadata({
|
||||
iptcpng: {
|
||||
parameters: info,
|
||||
},
|
||||
})
|
||||
.toFile(this.outputPath + '/' + imageName);
|
||||
this.result = this.getMarkdownImageUrl(imageName);
|
||||
} catch (error) {
|
||||
console.error('Error while saving the image:', error);
|
||||
// this.result = theImageUrl;
|
||||
}
|
||||
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StableDiffusionAPI;
|
||||
74
api/app/clients/tools/structured/Wolfram.js
Normal file
74
api/app/clients/tools/structured/Wolfram.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/* eslint-disable no-useless-escape */
|
||||
const axios = require('axios');
|
||||
const { StructuredTool } = require('langchain/tools');
|
||||
const { z } = require('zod');
|
||||
|
||||
class WolframAlphaAPI extends StructuredTool {
|
||||
constructor(fields) {
|
||||
super();
|
||||
this.name = 'wolfram';
|
||||
this.apiKey = fields.WOLFRAM_APP_ID || this.getAppId();
|
||||
this.description = `WolframAlpha offers computation, math, curated knowledge, and real-time data. It handles natural language queries and performs complex calculations.
|
||||
Guidelines include:
|
||||
- Use English for queries and inform users if information isn't from Wolfram.
|
||||
- Use "6*10^14" for exponent notation and single-line strings for input.
|
||||
- Use Markdown for formulas and simplify queries to keywords.
|
||||
- Use single-letter variable names and named physical constants.
|
||||
- Include a space between compound units and consider equations without units when solving.
|
||||
- Make separate calls for each property and choose relevant 'Assumptions' if results aren't relevant.
|
||||
- The tool also performs data analysis, plotting, and information retrieval.`;
|
||||
this.schema = z.object({
|
||||
nl_query: z
|
||||
.string()
|
||||
.describe('Natural language query to WolframAlpha following the guidelines'),
|
||||
});
|
||||
}
|
||||
|
||||
async fetchRawText(url) {
|
||||
try {
|
||||
const response = await axios.get(url, { responseType: 'text' });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching raw text: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getAppId() {
|
||||
const appId = process.env.WOLFRAM_APP_ID || '';
|
||||
if (!appId) {
|
||||
throw new Error('Missing WOLFRAM_APP_ID environment variable.');
|
||||
}
|
||||
return appId;
|
||||
}
|
||||
|
||||
createWolframAlphaURL(query) {
|
||||
// Clean up query
|
||||
const formattedQuery = query.replaceAll(/`/g, '').replaceAll(/\n/g, ' ');
|
||||
const baseURL = 'https://www.wolframalpha.com/api/v1/llm-api';
|
||||
const encodedQuery = encodeURIComponent(formattedQuery);
|
||||
const appId = this.apiKey || this.getAppId();
|
||||
const url = `${baseURL}?input=${encodedQuery}&appid=${appId}`;
|
||||
return url;
|
||||
}
|
||||
|
||||
async _call(data) {
|
||||
try {
|
||||
const { nl_query } = data;
|
||||
const url = this.createWolframAlphaURL(nl_query);
|
||||
const response = await this.fetchRawText(url);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
console.log('Error data:', error.response.data);
|
||||
return error.response.data;
|
||||
} else {
|
||||
console.log('Error querying Wolfram Alpha', error.message);
|
||||
// throw error;
|
||||
return 'There was an error querying Wolfram Alpha.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WolframAlphaAPI;
|
||||
31
api/app/clients/tools/util/addOpenAPISpecs.js
Normal file
31
api/app/clients/tools/util/addOpenAPISpecs.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const { loadSpecs } = require('./loadSpecs');
|
||||
|
||||
function transformSpec(input) {
|
||||
return {
|
||||
name: input.name_for_human,
|
||||
pluginKey: input.name_for_model,
|
||||
description: input.description_for_human,
|
||||
icon: input?.logo_url ?? 'https://placehold.co/70x70.png',
|
||||
// TODO: add support for authentication
|
||||
isAuthRequired: 'false',
|
||||
authConfig: [],
|
||||
};
|
||||
}
|
||||
|
||||
async function addOpenAPISpecs(availableTools) {
|
||||
try {
|
||||
const specs = (await loadSpecs({})).map(transformSpec);
|
||||
if (specs.length > 0) {
|
||||
return [...specs, ...availableTools];
|
||||
}
|
||||
return availableTools;
|
||||
} catch (error) {
|
||||
console.log('addOpenAPISpecs error', error);
|
||||
return availableTools;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
transformSpec,
|
||||
addOpenAPISpecs,
|
||||
};
|
||||
76
api/app/clients/tools/util/addOpenAPISpecs.spec.js
Normal file
76
api/app/clients/tools/util/addOpenAPISpecs.spec.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const { addOpenAPISpecs, transformSpec } = require('./addOpenAPISpecs');
|
||||
const { loadSpecs } = require('./loadSpecs');
|
||||
const { createOpenAPIPlugin } = require('../dynamic/OpenAPIPlugin');
|
||||
|
||||
jest.mock('./loadSpecs');
|
||||
jest.mock('../dynamic/OpenAPIPlugin');
|
||||
|
||||
describe('transformSpec', () => {
|
||||
it('should transform input spec to a desired format', () => {
|
||||
const input = {
|
||||
name_for_human: 'Human Name',
|
||||
name_for_model: 'Model Name',
|
||||
description_for_human: 'Human Description',
|
||||
logo_url: 'https://example.com/logo.png',
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
name: 'Human Name',
|
||||
pluginKey: 'Model Name',
|
||||
description: 'Human Description',
|
||||
icon: 'https://example.com/logo.png',
|
||||
isAuthRequired: 'false',
|
||||
authConfig: [],
|
||||
};
|
||||
|
||||
expect(transformSpec(input)).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it('should use default icon if logo_url is not provided', () => {
|
||||
const input = {
|
||||
name_for_human: 'Human Name',
|
||||
name_for_model: 'Model Name',
|
||||
description_for_human: 'Human Description',
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
name: 'Human Name',
|
||||
pluginKey: 'Model Name',
|
||||
description: 'Human Description',
|
||||
icon: 'https://placehold.co/70x70.png',
|
||||
isAuthRequired: 'false',
|
||||
authConfig: [],
|
||||
};
|
||||
|
||||
expect(transformSpec(input)).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addOpenAPISpecs', () => {
|
||||
it('should add specs to available tools', async () => {
|
||||
const availableTools = ['Tool1', 'Tool2'];
|
||||
const specs = [
|
||||
{
|
||||
name_for_human: 'Human Name',
|
||||
name_for_model: 'Model Name',
|
||||
description_for_human: 'Human Description',
|
||||
logo_url: 'https://example.com/logo.png',
|
||||
},
|
||||
];
|
||||
|
||||
loadSpecs.mockResolvedValue(specs);
|
||||
createOpenAPIPlugin.mockReturnValue('Plugin');
|
||||
|
||||
const result = await addOpenAPISpecs(availableTools);
|
||||
expect(result).toEqual([...specs.map(transformSpec), ...availableTools]);
|
||||
});
|
||||
|
||||
it('should return available tools if specs loading fails', async () => {
|
||||
const availableTools = ['Tool1', 'Tool2'];
|
||||
|
||||
loadSpecs.mockRejectedValue(new Error('Failed to load specs'));
|
||||
|
||||
const result = await addOpenAPISpecs(availableTools);
|
||||
expect(result).toEqual(availableTools);
|
||||
});
|
||||
});
|
||||
@@ -1,26 +1,28 @@
|
||||
const { getUserPluginAuthValue } = require('../../../../server/services/PluginService');
|
||||
const { OpenAIEmbeddings } = require('langchain/embeddings/openai');
|
||||
const { ZapierToolKit } = require('langchain/agents');
|
||||
const {
|
||||
SerpAPI,
|
||||
ZapierNLAWrapper
|
||||
} = require('langchain/tools');
|
||||
const { SerpAPI, ZapierNLAWrapper } = require('langchain/tools');
|
||||
const { ChatOpenAI } = require('langchain/chat_models/openai');
|
||||
const { Calculator } = require('langchain/tools/calculator');
|
||||
const { WebBrowser } = require('langchain/tools/webbrowser');
|
||||
const GoogleSearchAPI = require('./GoogleSearch');
|
||||
const HttpRequestTool = require('./HttpRequestTool');
|
||||
const AIPluginTool = require('./AIPluginTool');
|
||||
const OpenAICreateImage = require('./DALL-E');
|
||||
const StableDiffusionAPI = require('./StableDiffusion');
|
||||
const WolframAlphaAPI = require('./Wolfram');
|
||||
const availableTools = require('./manifest.json');
|
||||
const { getUserPluginAuthValue } = require('../../../server/services/PluginService');
|
||||
const {
|
||||
availableTools,
|
||||
AIPluginTool,
|
||||
GoogleSearchAPI,
|
||||
WolframAlphaAPI,
|
||||
StructuredWolfram,
|
||||
HttpRequestTool,
|
||||
OpenAICreateImage,
|
||||
StableDiffusionAPI,
|
||||
StructuredSD,
|
||||
} = require('../');
|
||||
const { loadSpecs } = require('./loadSpecs');
|
||||
|
||||
const validateTools = async (user, tools = []) => {
|
||||
try {
|
||||
const validToolsSet = new Set(tools);
|
||||
const availableToolsToValidate = availableTools.filter((tool) =>
|
||||
validToolsSet.has(tool.pluginKey)
|
||||
validToolsSet.has(tool.pluginKey),
|
||||
);
|
||||
|
||||
const validateCredentials = async (authField, toolName) => {
|
||||
@@ -69,21 +71,20 @@ const loadToolWithAuth = async (user, authFields, ToolConstructor, options = {})
|
||||
};
|
||||
};
|
||||
|
||||
const loadTools = async ({ user, model, tools = [], options = {} }) => {
|
||||
const loadTools = async ({ user, model, functions = null, tools = [], options = {} }) => {
|
||||
const toolConstructors = {
|
||||
calculator: Calculator,
|
||||
google: GoogleSearchAPI,
|
||||
wolfram: WolframAlphaAPI,
|
||||
wolfram: functions ? StructuredWolfram : WolframAlphaAPI,
|
||||
'dall-e': OpenAICreateImage,
|
||||
'stable-diffusion': StableDiffusionAPI
|
||||
'stable-diffusion': functions ? StructuredSD : StableDiffusionAPI,
|
||||
};
|
||||
|
||||
const customConstructors = {
|
||||
browser: async () => {
|
||||
let openAIApiKey = process.env.OPENAI_API_KEY;
|
||||
if (!openAIApiKey) {
|
||||
openAIApiKey = await getUserPluginAuthValue(user, 'OPENAI_API_KEY');
|
||||
}
|
||||
'web-browser': async () => {
|
||||
let openAIApiKey = options.openAIApiKey ?? process.env.OPENAI_API_KEY;
|
||||
openAIApiKey = openAIApiKey === 'user_provided' ? null : openAIApiKey;
|
||||
openAIApiKey = openAIApiKey || (await getUserPluginAuthValue(user, 'OPENAI_API_KEY'));
|
||||
return new WebBrowser({ model, embeddings: new OpenAIEmbeddings({ openAIApiKey }) });
|
||||
},
|
||||
serpapi: async () => {
|
||||
@@ -94,7 +95,7 @@ const loadTools = async ({ user, model, tools = [], options = {} }) => {
|
||||
return new SerpAPI(apiKey, {
|
||||
location: 'Austin,Texas,United States',
|
||||
hl: 'en',
|
||||
gl: 'us'
|
||||
gl: 'us',
|
||||
});
|
||||
},
|
||||
zapier: async () => {
|
||||
@@ -109,16 +110,28 @@ const loadTools = async ({ user, model, tools = [], options = {} }) => {
|
||||
return [
|
||||
new HttpRequestTool(),
|
||||
await AIPluginTool.fromPluginUrl(
|
||||
"https://www.klarna.com/.well-known/ai-plugin.json", new ChatOpenAI({ openAIApiKey: options.openAIApiKey, temperature: 0 })
|
||||
'https://www.klarna.com/.well-known/ai-plugin.json',
|
||||
new ChatOpenAI({ openAIApiKey: options.openAIApiKey, temperature: 0 }),
|
||||
),
|
||||
]
|
||||
}
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
const requestedTools = {};
|
||||
let specs = null;
|
||||
if (functions) {
|
||||
specs = await loadSpecs({
|
||||
llm: model,
|
||||
user,
|
||||
message: options.message,
|
||||
map: true,
|
||||
verbose: options?.debug,
|
||||
});
|
||||
console.dir(specs, { depth: null });
|
||||
}
|
||||
|
||||
const toolOptions = {
|
||||
serpapi: { location: 'Austin,Texas,United States', hl: 'en', gl: 'us' }
|
||||
serpapi: { location: 'Austin,Texas,United States', hl: 'en', gl: 'us' },
|
||||
};
|
||||
|
||||
const toolAuthFields = {};
|
||||
@@ -137,13 +150,18 @@ const loadTools = async ({ user, model, tools = [], options = {} }) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (specs && specs[tool]) {
|
||||
requestedTools[tool] = specs[tool];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (toolConstructors[tool]) {
|
||||
const options = toolOptions[tool] || {};
|
||||
const toolInstance = await loadToolWithAuth(
|
||||
user,
|
||||
toolAuthFields[tool],
|
||||
toolConstructors[tool],
|
||||
options
|
||||
options,
|
||||
);
|
||||
requestedTools[tool] = toolInstance;
|
||||
}
|
||||
@@ -154,5 +172,5 @@ const loadTools = async ({ user, model, tools = [], options = {} }) => {
|
||||
|
||||
module.exports = {
|
||||
validateTools,
|
||||
loadTools
|
||||
loadTools,
|
||||
};
|
||||
@@ -1,27 +1,53 @@
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
require('dotenv').config({ path: '../../../.env' });
|
||||
const mongoose = require('mongoose');
|
||||
const User = require('../../../models/User');
|
||||
const connectDb = require('../../../lib/db/connectDb');
|
||||
const { validateTools, loadTools, availableTools } = require('./index');
|
||||
const PluginService = require('../../../server/services/PluginService');
|
||||
const mockUser = {
|
||||
_id: 'fakeId',
|
||||
save: jest.fn(),
|
||||
findByIdAndDelete: jest.fn(),
|
||||
};
|
||||
|
||||
var mockPluginService = {
|
||||
updateUserPluginAuth: jest.fn(),
|
||||
deleteUserPluginAuth: jest.fn(),
|
||||
getUserPluginAuthValue: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('../../../../models/User', () => {
|
||||
return function () {
|
||||
return mockUser;
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../server/services/PluginService', () => mockPluginService);
|
||||
|
||||
const User = require('../../../../models/User');
|
||||
const { validateTools, loadTools } = require('./');
|
||||
const PluginService = require('../../../../server/services/PluginService');
|
||||
const { BaseChatModel } = require('langchain/chat_models/openai');
|
||||
const { Calculator } = require('langchain/tools/calculator');
|
||||
const OpenAICreateImage = require('./DALL-E');
|
||||
const GoogleSearchAPI = require('./GoogleSearch');
|
||||
const { availableTools, OpenAICreateImage, GoogleSearchAPI, StructuredSD } = require('../');
|
||||
|
||||
describe('Tool Handlers', () => {
|
||||
let fakeUser;
|
||||
let pluginKey = 'dall-e';
|
||||
let pluginKey2 = 'wolfram';
|
||||
let sampleTools = [pluginKey, pluginKey2];
|
||||
let ToolClass = OpenAICreateImage;
|
||||
let mockCredential = 'mock-credential';
|
||||
const pluginKey = 'dall-e';
|
||||
const pluginKey2 = 'wolfram';
|
||||
const initialTools = [pluginKey, pluginKey2];
|
||||
const ToolClass = OpenAICreateImage;
|
||||
const mockCredential = 'mock-credential';
|
||||
const mainPlugin = availableTools.find((tool) => tool.pluginKey === pluginKey);
|
||||
const authConfigs = mainPlugin.authConfig;
|
||||
|
||||
beforeAll(async () => {
|
||||
await connectDb();
|
||||
mockUser.save.mockResolvedValue(undefined);
|
||||
|
||||
const userAuthValues = {};
|
||||
mockPluginService.getUserPluginAuthValue.mockImplementation((userId, authField) => {
|
||||
return userAuthValues[`${userId}-${authField}`];
|
||||
});
|
||||
mockPluginService.updateUserPluginAuth.mockImplementation(
|
||||
(userId, authField, _pluginKey, credential) => {
|
||||
userAuthValues[`${userId}-${authField}`] = credential;
|
||||
},
|
||||
);
|
||||
|
||||
fakeUser = new User({
|
||||
name: 'Fake User',
|
||||
username: 'fakeuser',
|
||||
@@ -33,30 +59,29 @@ describe('Tool Handlers', () => {
|
||||
role: 'USER',
|
||||
googleId: null,
|
||||
plugins: [],
|
||||
refreshToken: []
|
||||
refreshToken: [],
|
||||
});
|
||||
await fakeUser.save();
|
||||
for (const authConfig of authConfigs) {
|
||||
await PluginService.updateUserPluginAuth(fakeUser._id, authConfig.authField, pluginKey, mockCredential);
|
||||
await PluginService.updateUserPluginAuth(
|
||||
fakeUser._id,
|
||||
authConfig.authField,
|
||||
pluginKey,
|
||||
mockCredential,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// afterEach(async () => {
|
||||
// // Clean up any test-specific data.
|
||||
// });
|
||||
|
||||
afterAll(async () => {
|
||||
// Delete the fake user & plugin auth
|
||||
await User.findByIdAndDelete(fakeUser._id);
|
||||
await mockUser.findByIdAndDelete(fakeUser._id);
|
||||
for (const authConfig of authConfigs) {
|
||||
await PluginService.deleteUserPluginAuth(fakeUser._id, authConfig.authField);
|
||||
}
|
||||
await mongoose.connection.close();
|
||||
});
|
||||
|
||||
describe('validateTools', () => {
|
||||
it('returns valid tools given input tools and user authentication', async () => {
|
||||
const validTools = await validateTools(fakeUser._id, sampleTools);
|
||||
const validTools = await validateTools(fakeUser._id, initialTools);
|
||||
expect(validTools).toBeDefined();
|
||||
console.log('validateTools: validTools', validTools);
|
||||
expect(validTools.some((tool) => tool === pluginKey)).toBeTruthy();
|
||||
@@ -64,7 +89,7 @@ describe('Tool Handlers', () => {
|
||||
});
|
||||
|
||||
it('removes tools without valid credentials from the validTools array', async () => {
|
||||
const validTools = await validateTools(fakeUser._id, sampleTools);
|
||||
const validTools = await validateTools(fakeUser._id, initialTools);
|
||||
expect(validTools.some((tool) => tool.pluginKey === pluginKey2)).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -92,17 +117,17 @@ describe('Tool Handlers', () => {
|
||||
let loadTool1;
|
||||
let loadTool2;
|
||||
let loadTool3;
|
||||
sampleTools = [...sampleTools, 'calculator'];
|
||||
const sampleTools = [...initialTools, 'calculator'];
|
||||
let ToolClass2 = Calculator;
|
||||
let remainingTools = availableTools.filter(
|
||||
(tool) => sampleTools.indexOf(tool.pluginKey) === -1
|
||||
(tool) => sampleTools.indexOf(tool.pluginKey) === -1,
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
toolFunctions = await loadTools({
|
||||
user: fakeUser._id,
|
||||
model: BaseChatModel,
|
||||
tools: sampleTools
|
||||
tools: sampleTools,
|
||||
});
|
||||
loadTool1 = toolFunctions[sampleTools[0]];
|
||||
loadTool2 = toolFunctions[sampleTools[1]];
|
||||
@@ -128,6 +153,7 @@ describe('Tool Handlers', () => {
|
||||
try {
|
||||
await loadTool2();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
});
|
||||
@@ -142,7 +168,7 @@ describe('Tool Handlers', () => {
|
||||
toolFunctions = await loadTools({
|
||||
user: fakeUser._id,
|
||||
model: BaseChatModel,
|
||||
tools: [testPluginKey]
|
||||
tools: [testPluginKey],
|
||||
});
|
||||
const Tool = await toolFunctions[testPluginKey]();
|
||||
expect(Tool).toBeInstanceOf(TestClass);
|
||||
@@ -150,9 +176,21 @@ describe('Tool Handlers', () => {
|
||||
it('returns an empty object when no tools are requested', async () => {
|
||||
toolFunctions = await loadTools({
|
||||
user: fakeUser._id,
|
||||
model: BaseChatModel
|
||||
model: BaseChatModel,
|
||||
});
|
||||
expect(toolFunctions).toEqual({});
|
||||
});
|
||||
it('should return the StructuredTool version when using functions', async () => {
|
||||
process.env.SD_WEBUI_URL = mockCredential;
|
||||
toolFunctions = await loadTools({
|
||||
user: fakeUser._id,
|
||||
model: BaseChatModel,
|
||||
tools: ['stable-diffusion'],
|
||||
functions: true,
|
||||
});
|
||||
const structuredTool = await toolFunctions['stable-diffusion']();
|
||||
expect(structuredTool).toBeInstanceOf(StructuredSD);
|
||||
delete process.env.SD_WEBUI_URL;
|
||||
});
|
||||
});
|
||||
});
|
||||
6
api/app/clients/tools/util/index.js
Normal file
6
api/app/clients/tools/util/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const { validateTools, loadTools } = require('./handleTools');
|
||||
|
||||
module.exports = {
|
||||
validateTools,
|
||||
loadTools,
|
||||
};
|
||||
104
api/app/clients/tools/util/loadSpecs.js
Normal file
104
api/app/clients/tools/util/loadSpecs.js
Normal file
@@ -0,0 +1,104 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { z } = require('zod');
|
||||
const { createOpenAPIPlugin } = require('../dynamic/OpenAPIPlugin');
|
||||
|
||||
// The minimum Manifest definition
|
||||
const ManifestDefinition = z.object({
|
||||
schema_version: z.string().optional(),
|
||||
name_for_human: z.string(),
|
||||
name_for_model: z.string(),
|
||||
description_for_human: z.string(),
|
||||
description_for_model: z.string(),
|
||||
auth: z.object({}).optional(),
|
||||
api: z.object({
|
||||
// Spec URL or can be the filename of the OpenAPI spec yaml file,
|
||||
// located in api\app\clients\tools\.well-known\openapi
|
||||
url: z.string(),
|
||||
type: z.string().optional(),
|
||||
is_user_authenticated: z.boolean().nullable().optional(),
|
||||
has_user_authentication: z.boolean().nullable().optional(),
|
||||
}),
|
||||
// use to override any params that the LLM will consistently get wrong
|
||||
params: z.object({}).optional(),
|
||||
logo_url: z.string().optional(),
|
||||
contact_email: z.string().optional(),
|
||||
legal_info_url: z.string().optional(),
|
||||
});
|
||||
|
||||
function validateJson(json, verbose = true) {
|
||||
try {
|
||||
return ManifestDefinition.parse(json);
|
||||
} catch (error) {
|
||||
if (verbose) {
|
||||
console.debug('validateJson error', error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// omit the LLM to return the well known jsons as objects
|
||||
async function loadSpecs({ llm, user, message, map = false, verbose = false }) {
|
||||
const directoryPath = path.join(__dirname, '..', '.well-known');
|
||||
const files = (await fs.promises.readdir(directoryPath)).filter(
|
||||
(file) => path.extname(file) === '.json',
|
||||
);
|
||||
|
||||
const validJsons = [];
|
||||
const constructorMap = {};
|
||||
|
||||
if (verbose) {
|
||||
console.debug('files', files);
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
if (path.extname(file) === '.json') {
|
||||
const filePath = path.join(directoryPath, file);
|
||||
const fileContent = await fs.promises.readFile(filePath, 'utf8');
|
||||
const json = JSON.parse(fileContent);
|
||||
|
||||
if (!validateJson(json)) {
|
||||
verbose && console.debug('Invalid json', json);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (llm && map) {
|
||||
constructorMap[json.name_for_model] = async () =>
|
||||
await createOpenAPIPlugin({
|
||||
data: json,
|
||||
llm,
|
||||
message,
|
||||
user,
|
||||
verbose,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (llm) {
|
||||
validJsons.push(createOpenAPIPlugin({ data: json, llm, verbose }));
|
||||
continue;
|
||||
}
|
||||
|
||||
validJsons.push(json);
|
||||
}
|
||||
}
|
||||
|
||||
if (map) {
|
||||
return constructorMap;
|
||||
}
|
||||
|
||||
const plugins = (await Promise.all(validJsons)).filter((plugin) => plugin);
|
||||
|
||||
// if (verbose) {
|
||||
// console.debug('plugins', plugins);
|
||||
// console.debug(plugins[0].name);
|
||||
// }
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadSpecs,
|
||||
validateJson,
|
||||
ManifestDefinition,
|
||||
};
|
||||
101
api/app/clients/tools/util/loadSpecs.spec.js
Normal file
101
api/app/clients/tools/util/loadSpecs.spec.js
Normal file
@@ -0,0 +1,101 @@
|
||||
const fs = require('fs');
|
||||
const { validateJson, loadSpecs, ManifestDefinition } = require('./loadSpecs');
|
||||
const { createOpenAPIPlugin } = require('../dynamic/OpenAPIPlugin');
|
||||
|
||||
jest.mock('../dynamic/OpenAPIPlugin');
|
||||
|
||||
describe('ManifestDefinition', () => {
|
||||
it('should validate correct json', () => {
|
||||
const json = {
|
||||
name_for_human: 'Test',
|
||||
name_for_model: 'Test',
|
||||
description_for_human: 'Test',
|
||||
description_for_model: 'Test',
|
||||
api: {
|
||||
url: 'http://test.com',
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => ManifestDefinition.parse(json)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not validate incorrect json', () => {
|
||||
const json = {
|
||||
name_for_human: 'Test',
|
||||
name_for_model: 'Test',
|
||||
description_for_human: 'Test',
|
||||
description_for_model: 'Test',
|
||||
api: {
|
||||
url: 123, // incorrect type
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => ManifestDefinition.parse(json)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateJson', () => {
|
||||
it('should return parsed json if valid', () => {
|
||||
const json = {
|
||||
name_for_human: 'Test',
|
||||
name_for_model: 'Test',
|
||||
description_for_human: 'Test',
|
||||
description_for_model: 'Test',
|
||||
api: {
|
||||
url: 'http://test.com',
|
||||
},
|
||||
};
|
||||
|
||||
expect(validateJson(json)).toEqual(json);
|
||||
});
|
||||
|
||||
it('should return false if json is not valid', () => {
|
||||
const json = {
|
||||
name_for_human: 'Test',
|
||||
name_for_model: 'Test',
|
||||
description_for_human: 'Test',
|
||||
description_for_model: 'Test',
|
||||
api: {
|
||||
url: 123, // incorrect type
|
||||
},
|
||||
};
|
||||
|
||||
expect(validateJson(json)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadSpecs', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(fs.promises, 'readdir').mockResolvedValue(['test.json']);
|
||||
jest.spyOn(fs.promises, 'readFile').mockResolvedValue(
|
||||
JSON.stringify({
|
||||
name_for_human: 'Test',
|
||||
name_for_model: 'Test',
|
||||
description_for_human: 'Test',
|
||||
description_for_model: 'Test',
|
||||
api: {
|
||||
url: 'http://test.com',
|
||||
},
|
||||
}),
|
||||
);
|
||||
createOpenAPIPlugin.mockResolvedValue({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should return plugins', async () => {
|
||||
const plugins = await loadSpecs({ llm: true, verbose: false });
|
||||
|
||||
expect(plugins).toHaveLength(1);
|
||||
expect(createOpenAPIPlugin).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return constructorMap if map is true', async () => {
|
||||
const plugins = await loadSpecs({ llm: {}, map: true, verbose: false });
|
||||
|
||||
expect(plugins).toHaveProperty('Test');
|
||||
expect(createOpenAPIPlugin).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,17 @@
|
||||
const { askClient } = require('./clients/chatgpt-client');
|
||||
const { browserClient } = require('./clients/chatgpt-browser');
|
||||
const { askBing } = require('./clients/bingai');
|
||||
const { browserClient } = require('./chatgpt-browser');
|
||||
const { askBing } = require('./bingai');
|
||||
const clients = require('./clients');
|
||||
const titleConvo = require('./titleConvo');
|
||||
const titleConvoBing = require('./titleConvoBing');
|
||||
const getCitations = require('../lib/parse/getCitations');
|
||||
const citeText = require('../lib/parse/citeText');
|
||||
|
||||
module.exports = {
|
||||
askClient,
|
||||
browserClient,
|
||||
askBing,
|
||||
titleConvo,
|
||||
titleConvoBing,
|
||||
getCitations,
|
||||
citeText
|
||||
citeText,
|
||||
...clients,
|
||||
};
|
||||
|
||||
@@ -1,904 +0,0 @@
|
||||
const crypto = require('crypto');
|
||||
const { genAzureChatCompletion } = require('../../utils/genAzureEndpoints');
|
||||
const {
|
||||
encoding_for_model: encodingForModel,
|
||||
get_encoding: getEncoding
|
||||
} = require('@dqbd/tiktoken');
|
||||
const { fetchEventSource } = require('@waylaidwanderer/fetch-event-source');
|
||||
const { Agent, ProxyAgent } = require('undici');
|
||||
const TextStream = require('../stream');
|
||||
const { ChatOpenAI } = require('langchain/chat_models/openai');
|
||||
const { CallbackManager } = require('langchain/callbacks');
|
||||
const { HumanChatMessage, AIChatMessage } = require('langchain/schema');
|
||||
const { initializeCustomAgent } = require('./agents/CustomAgent/initializeCustomAgent');
|
||||
const { getMessages, saveMessage, saveConvo } = require('../../models');
|
||||
const { loadTools, SelfReflectionTool } = require('./tools');
|
||||
const {
|
||||
instructions,
|
||||
imageInstructions,
|
||||
errorInstructions,
|
||||
completionInstructions
|
||||
} = require('./instructions');
|
||||
|
||||
const tokenizersCache = {};
|
||||
|
||||
class ChatAgent {
|
||||
constructor(apiKey, options = {}) {
|
||||
this.tools = [];
|
||||
this.actions = [];
|
||||
this.openAIApiKey = apiKey;
|
||||
this.azure = options.azure || false;
|
||||
if (this.azure) {
|
||||
const { azureOpenAIApiInstanceName, azureOpenAIApiDeploymentName, azureOpenAIApiVersion } =
|
||||
this.azure;
|
||||
this.azureEndpoint = genAzureChatCompletion({
|
||||
azureOpenAIApiInstanceName,
|
||||
azureOpenAIApiDeploymentName,
|
||||
azureOpenAIApiVersion
|
||||
});
|
||||
}
|
||||
this.setOptions(options);
|
||||
this.executor = null;
|
||||
this.currentDateString = new Date().toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
getActions(input = null) {
|
||||
let output = 'Internal thoughts & actions taken:\n"';
|
||||
let actions = input || this.actions;
|
||||
|
||||
if (actions[0]?.action) {
|
||||
actions = actions.map((step) => ({
|
||||
log: `${step.action.log}\nObservation: ${step.observation}`
|
||||
}));
|
||||
}
|
||||
|
||||
actions.forEach((actionObj, index) => {
|
||||
output += `${actionObj.log}`;
|
||||
if (index < actions.length - 1) {
|
||||
output += '\n';
|
||||
}
|
||||
});
|
||||
|
||||
return output + '"';
|
||||
}
|
||||
|
||||
buildErrorInput(message, errorMessage) {
|
||||
const log = errorMessage.includes('Could not parse LLM output:')
|
||||
? `A formatting error occurred with your response to the human's last message. You didn't follow the formatting instructions. Remember to ${instructions}`
|
||||
: `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}`;
|
||||
|
||||
return `
|
||||
${log}
|
||||
|
||||
${this.getActions()}
|
||||
|
||||
Human's last message: ${message}
|
||||
`;
|
||||
}
|
||||
|
||||
buildPromptPrefix(result, message) {
|
||||
if ((result.output && result.output.includes('N/A')) || result.output === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
result?.intermediateSteps?.length === 1 &&
|
||||
result?.intermediateSteps[0]?.action?.toolInput === 'N/A'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const internalActions =
|
||||
result?.intermediateSteps?.length > 0
|
||||
? this.getActions(result.intermediateSteps)
|
||||
: 'Internal Actions Taken: None';
|
||||
|
||||
const toolBasedInstructions = internalActions.toLowerCase().includes('image')
|
||||
? imageInstructions
|
||||
: '';
|
||||
|
||||
const errorMessage = result.errorMessage ? `${errorInstructions} ${result.errorMessage}\n` : '';
|
||||
|
||||
const preliminaryAnswer =
|
||||
result.output?.length > 0 ? `Preliminary Answer: "${result.output.trim()}"` : '';
|
||||
const prefix = preliminaryAnswer
|
||||
? `review and improve the answer you generated using plugins in response to the User Message below. The answer hasn't been sent to the user yet.`
|
||||
: 'respond to the User Message below based on your preliminary thoughts & actions.';
|
||||
|
||||
return `As ChatGPT, ${prefix}${errorMessage}\n${internalActions}
|
||||
${preliminaryAnswer}
|
||||
Reply conversationally to the User based on your ${
|
||||
preliminaryAnswer ? 'preliminary answer, ' : ''
|
||||
}internal actions, thoughts, and observations, making improvements wherever possible, but do not modify URLs.
|
||||
${
|
||||
preliminaryAnswer
|
||||
? ''
|
||||
: '\nIf there is an incomplete thought or action, you are expected to complete it in your response now.\n'
|
||||
}You must cite sources if you are using any web links. ${toolBasedInstructions}
|
||||
Only respond with your conversational reply to the following User Message:
|
||||
"${message}"`;
|
||||
}
|
||||
|
||||
setOptions(options) {
|
||||
if (this.options && !this.options.replaceOptions) {
|
||||
// nested options aren't spread properly, so we need to do this manually
|
||||
this.options.modelOptions = {
|
||||
...this.options.modelOptions,
|
||||
...options.modelOptions
|
||||
};
|
||||
this.options.agentOptions = {
|
||||
...this.options.agentOptions,
|
||||
...options.agentOptions
|
||||
};
|
||||
delete options.modelOptions;
|
||||
delete options.agentOptions;
|
||||
// now we can merge options
|
||||
this.options = {
|
||||
...this.options,
|
||||
...options
|
||||
};
|
||||
} else {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
this.agentOptions = this.options.agentOptions || {};
|
||||
this.agentIsGpt3 = this.agentOptions.model.startsWith('gpt-3');
|
||||
const modelOptions = this.options.modelOptions || {};
|
||||
this.modelOptions = {
|
||||
...modelOptions,
|
||||
model: modelOptions.model || 'gpt-3.5-turbo',
|
||||
temperature: typeof modelOptions.temperature === 'undefined' ? 0.8 : modelOptions.temperature,
|
||||
top_p: typeof modelOptions.top_p === 'undefined' ? 1 : modelOptions.top_p,
|
||||
presence_penalty:
|
||||
typeof modelOptions.presence_penalty === 'undefined' ? 0 : modelOptions.presence_penalty,
|
||||
frequency_penalty:
|
||||
typeof modelOptions.frequency_penalty === 'undefined' ? 0 : modelOptions.frequency_penalty,
|
||||
stop: modelOptions.stop
|
||||
};
|
||||
|
||||
this.isChatGptModel = this.modelOptions.model.startsWith('gpt-');
|
||||
this.isGpt3 = this.modelOptions.model.startsWith('gpt-3');
|
||||
this.maxContextTokens = this.modelOptions.model === 'gpt-4-32k' ? 32767 : this.modelOptions.model.startsWith('gpt-4') ? 8191 : 4095,
|
||||
|
||||
// Reserve 1024 tokens for the response.
|
||||
// The max prompt tokens is determined by the max context tokens minus the max response tokens.
|
||||
// Earlier messages will be dropped until the prompt is within the limit.
|
||||
this.maxResponseTokens = this.modelOptions.max_tokens || 1024;
|
||||
this.maxPromptTokens =
|
||||
this.options.maxPromptTokens || this.maxContextTokens - this.maxResponseTokens;
|
||||
|
||||
if (this.maxPromptTokens + this.maxResponseTokens > this.maxContextTokens) {
|
||||
throw new Error(
|
||||
`maxPromptTokens + max_tokens (${this.maxPromptTokens} + ${this.maxResponseTokens} = ${
|
||||
this.maxPromptTokens + this.maxResponseTokens
|
||||
}) must be less than or equal to maxContextTokens (${this.maxContextTokens})`
|
||||
);
|
||||
}
|
||||
|
||||
this.userLabel = this.options.userLabel || 'User';
|
||||
this.chatGptLabel = this.options.chatGptLabel || 'ChatGPT';
|
||||
|
||||
// Use these faux tokens to help the AI understand the context since we are building the chat log ourselves.
|
||||
// Trying to use "<|im_start|>" causes the AI to still generate "<" or "<|" at the end sometimes for some reason,
|
||||
// without tripping the stop sequences, so I'm using "||>" instead.
|
||||
this.startToken = '||>';
|
||||
this.endToken = '';
|
||||
this.gptEncoder = this.constructor.getTokenizer('cl100k_base');
|
||||
this.completionsUrl = 'https://api.openai.com/v1/chat/completions';
|
||||
this.reverseProxyUrl = this.options.reverseProxyUrl || process.env.OPENAI_REVERSE_PROXY;
|
||||
|
||||
if (this.reverseProxyUrl) {
|
||||
this.completionsUrl = this.reverseProxyUrl;
|
||||
this.langchainProxy = this.reverseProxyUrl.substring(0, this.reverseProxyUrl.indexOf('v1') + 'v1'.length)
|
||||
}
|
||||
|
||||
if (this.azureEndpoint) {
|
||||
this.completionsUrl = this.azureEndpoint;
|
||||
}
|
||||
|
||||
if (this.azureEndpoint && this.options.debug) {
|
||||
console.debug(`Using Azure endpoint: ${this.azureEndpoint}`, this.azure);
|
||||
}
|
||||
}
|
||||
|
||||
static getTokenizer(encoding, isModelName = false, extendSpecialTokens = {}) {
|
||||
if (tokenizersCache[encoding]) {
|
||||
return tokenizersCache[encoding];
|
||||
}
|
||||
let tokenizer;
|
||||
if (isModelName) {
|
||||
tokenizer = encodingForModel(encoding, extendSpecialTokens);
|
||||
} else {
|
||||
tokenizer = getEncoding(encoding, extendSpecialTokens);
|
||||
}
|
||||
tokenizersCache[encoding] = tokenizer;
|
||||
return tokenizer;
|
||||
}
|
||||
|
||||
async getCompletion(input, onProgress, abortController = null) {
|
||||
if (!abortController) {
|
||||
abortController = new AbortController();
|
||||
}
|
||||
|
||||
const modelOptions = this.modelOptions;
|
||||
if (typeof onProgress === 'function') {
|
||||
modelOptions.stream = true;
|
||||
}
|
||||
if (this.isChatGptModel) {
|
||||
modelOptions.messages = input;
|
||||
} else {
|
||||
modelOptions.prompt = input;
|
||||
}
|
||||
const { debug } = this.options;
|
||||
const url = this.completionsUrl;
|
||||
if (debug) {
|
||||
console.debug();
|
||||
console.debug(url);
|
||||
console.debug(modelOptions);
|
||||
console.debug();
|
||||
}
|
||||
const opts = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(modelOptions),
|
||||
dispatcher: new Agent({
|
||||
bodyTimeout: 0,
|
||||
headersTimeout: 0
|
||||
})
|
||||
};
|
||||
|
||||
if (this.azureEndpoint) {
|
||||
opts.headers['api-key'] = this.azure.azureOpenAIApiKey;
|
||||
} else if (this.openAIApiKey) {
|
||||
opts.headers.Authorization = `Bearer ${this.openAIApiKey}`;
|
||||
}
|
||||
|
||||
if (this.options.proxy) {
|
||||
opts.dispatcher = new ProxyAgent(this.options.proxy);
|
||||
}
|
||||
|
||||
if (modelOptions.stream) {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
let done = false;
|
||||
await fetchEventSource(url, {
|
||||
...opts,
|
||||
signal: abortController.signal,
|
||||
async onopen(response) {
|
||||
if (response.status === 200) {
|
||||
return;
|
||||
}
|
||||
if (debug) {
|
||||
// console.debug(response);
|
||||
}
|
||||
let error;
|
||||
try {
|
||||
const body = await response.text();
|
||||
error = new Error(`Failed to send message. HTTP ${response.status} - ${body}`);
|
||||
error.status = response.status;
|
||||
error.json = JSON.parse(body);
|
||||
} catch {
|
||||
error = error || new Error(`Failed to send message. HTTP ${response.status}`);
|
||||
}
|
||||
throw error;
|
||||
},
|
||||
onclose() {
|
||||
if (debug) {
|
||||
console.debug('Server closed the connection unexpectedly, returning...');
|
||||
}
|
||||
// workaround for private API not sending [DONE] event
|
||||
if (!done) {
|
||||
onProgress('[DONE]');
|
||||
abortController.abort();
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
onerror(err) {
|
||||
if (debug) {
|
||||
console.debug(err);
|
||||
}
|
||||
// rethrow to stop the operation
|
||||
throw err;
|
||||
},
|
||||
onmessage(message) {
|
||||
if (debug) {
|
||||
// console.debug(message);
|
||||
}
|
||||
if (!message.data || message.event === 'ping') {
|
||||
return;
|
||||
}
|
||||
if (message.data === '[DONE]') {
|
||||
onProgress('[DONE]');
|
||||
abortController.abort();
|
||||
resolve();
|
||||
done = true;
|
||||
return;
|
||||
}
|
||||
onProgress(JSON.parse(message.data));
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
const response = await fetch(url, {
|
||||
...opts,
|
||||
signal: abortController.signal
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
const body = await response.text();
|
||||
const error = new Error(`Failed to send message. HTTP ${response.status} - ${body}`);
|
||||
error.status = response.status;
|
||||
try {
|
||||
error.json = JSON.parse(body);
|
||||
} catch {
|
||||
error.body = body;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async loadHistory(conversationId, parentMessageId = null) {
|
||||
if (this.options.debug) {
|
||||
console.debug('Loading history for conversation', conversationId, parentMessageId);
|
||||
}
|
||||
|
||||
const messages = (await getMessages({ conversationId })) || [];
|
||||
|
||||
if (messages.length === 0) {
|
||||
this.currentMessages = [];
|
||||
return [];
|
||||
}
|
||||
|
||||
const orderedMessages = this.constructor.getMessagesForConversation(messages, parentMessageId);
|
||||
// Convert Message documents into appropriate ChatMessage instances
|
||||
const chatMessages = orderedMessages.map((msg) =>
|
||||
msg?.isCreatedByUser || msg?.role.toLowerCase() === 'user'
|
||||
? new HumanChatMessage(msg.text)
|
||||
: new AIChatMessage(msg.text)
|
||||
);
|
||||
|
||||
this.currentMessages = orderedMessages;
|
||||
|
||||
return chatMessages;
|
||||
}
|
||||
|
||||
async saveMessageToDatabase(message, user = null) {
|
||||
await saveMessage({ ...message, unfinished: false });
|
||||
await saveConvo(user, {
|
||||
conversationId: message.conversationId,
|
||||
endpoint: 'gptPlugins',
|
||||
chatGptLabel: this.options.chatGptLabel,
|
||||
promptPrefix: this.options.promptPrefix,
|
||||
...this.modelOptions,
|
||||
agentOptions: this.agentOptions
|
||||
});
|
||||
}
|
||||
|
||||
saveLatestAction(action) {
|
||||
this.actions.push(action);
|
||||
}
|
||||
|
||||
async initialize({ user, message, onAgentAction, onChainEnd, signal }) {
|
||||
const modelOptions = {
|
||||
modelName: this.agentOptions.model,
|
||||
temperature: this.agentOptions.temperature
|
||||
};
|
||||
|
||||
const configOptions = {};
|
||||
|
||||
if (this.langchainProxy) {
|
||||
configOptions.basePath = this.langchainProxy;
|
||||
}
|
||||
|
||||
const model = this.azure
|
||||
? new ChatOpenAI({
|
||||
...this.azure,
|
||||
...modelOptions
|
||||
})
|
||||
: new ChatOpenAI(
|
||||
{
|
||||
openAIApiKey: this.openAIApiKey,
|
||||
...modelOptions
|
||||
},
|
||||
configOptions
|
||||
// {
|
||||
// basePath: 'http://localhost:8080/v1'
|
||||
// }
|
||||
);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug(`<-----Agent Model: ${model.modelName} | Temp: ${model.temperature}----->`);
|
||||
}
|
||||
|
||||
this.availableTools = await loadTools({
|
||||
user,
|
||||
model,
|
||||
tools: this.options.tools,
|
||||
options: {
|
||||
openAIApiKey: this.openAIApiKey
|
||||
}
|
||||
});
|
||||
// load tools
|
||||
for (const tool of this.options.tools) {
|
||||
const validTool = this.availableTools[tool];
|
||||
|
||||
if (tool === 'plugins') {
|
||||
const plugins = await validTool();
|
||||
this.tools = [...this.tools, ...plugins];
|
||||
} else if (validTool) {
|
||||
this.tools.push(await validTool());
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('Requested Tools');
|
||||
console.debug(this.options.tools);
|
||||
console.debug('Loaded Tools');
|
||||
console.debug(this.tools.map((tool) => tool.name));
|
||||
}
|
||||
|
||||
if (this.tools.length > 0) {
|
||||
this.tools.push(new SelfReflectionTool({ message, isGpt3: false }));
|
||||
} else if (this.tools.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleAction = (action, callback = null) => {
|
||||
this.saveLatestAction(action);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('Latest Agent Action ', this.actions[this.actions.length - 1]);
|
||||
}
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback(action);
|
||||
}
|
||||
};
|
||||
|
||||
// initialize agent
|
||||
this.executor = await initializeCustomAgent({
|
||||
model,
|
||||
signal,
|
||||
tools: this.tools,
|
||||
pastMessages: this.pastMessages,
|
||||
currentDateString: this.currentDateString,
|
||||
verbose: this.options.debug,
|
||||
returnIntermediateSteps: true,
|
||||
callbackManager: CallbackManager.fromHandlers({
|
||||
async handleAgentAction(action) {
|
||||
handleAction(action, onAgentAction);
|
||||
},
|
||||
async handleChainEnd(action) {
|
||||
if (typeof onChainEnd === 'function') {
|
||||
onChainEnd(action);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('Loaded agent.');
|
||||
}
|
||||
}
|
||||
|
||||
async sendApiMessage(messages, userMessage, opts = {}) {
|
||||
// Doing it this way instead of having each message be a separate element in the array seems to be more reliable,
|
||||
// especially when it comes to keeping the AI in character. It also seems to improve coherency and context retention.
|
||||
let payload = await this.buildPrompt({
|
||||
messages: [
|
||||
...messages,
|
||||
{
|
||||
messageId: userMessage.messageId,
|
||||
parentMessageId: userMessage.parentMessageId,
|
||||
role: 'User',
|
||||
text: userMessage.text
|
||||
}
|
||||
],
|
||||
...opts
|
||||
});
|
||||
|
||||
let reply = '';
|
||||
let result = {};
|
||||
if (typeof opts.onProgress === 'function') {
|
||||
await this.getCompletion(
|
||||
payload,
|
||||
(progressMessage) => {
|
||||
if (progressMessage === '[DONE]') {
|
||||
return;
|
||||
}
|
||||
const token = this.isChatGptModel
|
||||
? progressMessage.choices[0].delta.content
|
||||
: progressMessage.choices[0].text;
|
||||
// first event's delta content is always undefined
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (token === this.endToken) {
|
||||
return;
|
||||
}
|
||||
opts.onProgress(token);
|
||||
reply += token;
|
||||
},
|
||||
opts.abortController || new AbortController()
|
||||
);
|
||||
} else {
|
||||
result = await this.getCompletion(
|
||||
payload,
|
||||
null,
|
||||
opts.abortController || new AbortController()
|
||||
);
|
||||
if (this.options.debug) {
|
||||
console.debug(JSON.stringify(result));
|
||||
}
|
||||
if (this.isChatGptModel) {
|
||||
reply = result.choices[0].message.content;
|
||||
} else {
|
||||
reply = result.choices[0].text.replace(this.endToken, '');
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug();
|
||||
}
|
||||
|
||||
return reply.trim();
|
||||
}
|
||||
|
||||
async executorCall(message, signal) {
|
||||
let errorMessage = '';
|
||||
const maxAttempts = 1;
|
||||
|
||||
for (let attempts = 1; attempts <= maxAttempts; attempts++) {
|
||||
const errorInput = this.buildErrorInput(message, errorMessage);
|
||||
const input = attempts > 1 ? errorInput : message;
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug(`Attempt ${attempts} of ${maxAttempts}`);
|
||||
}
|
||||
|
||||
if (this.options.debug && errorMessage.length > 0) {
|
||||
console.debug('Caught error, input:', input);
|
||||
}
|
||||
|
||||
try {
|
||||
this.result = await this.executor.call({ input, signal });
|
||||
break; // Exit the loop if the function call is successful
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
errorMessage = err.message;
|
||||
if (attempts === maxAttempts) {
|
||||
this.result.output = `Encountered an error while attempting to respond. Error: ${err.message}`;
|
||||
this.result.intermediateSteps = this.actions;
|
||||
this.result.errorMessage = errorMessage;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage(message, opts = {}) {
|
||||
if (opts && typeof opts === 'object') {
|
||||
this.setOptions(opts);
|
||||
}
|
||||
console.log('sendMessage', message, opts);
|
||||
|
||||
const user = opts.user || null;
|
||||
const { onAgentAction, onChainEnd, onProgress } = opts;
|
||||
const conversationId = opts.conversationId || crypto.randomUUID();
|
||||
const parentMessageId = opts.parentMessageId || '00000000-0000-0000-0000-000000000000';
|
||||
const userMessageId = opts.overrideParentMessageId || crypto.randomUUID();
|
||||
const responseMessageId = crypto.randomUUID();
|
||||
this.pastMessages = await this.loadHistory(conversationId, this.options?.parentMessageId);
|
||||
|
||||
const userMessage = {
|
||||
messageId: userMessageId,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
sender: 'User',
|
||||
text: message,
|
||||
isCreatedByUser: true
|
||||
};
|
||||
|
||||
if (typeof opts?.getIds === 'function') {
|
||||
opts.getIds({
|
||||
userMessage,
|
||||
conversationId,
|
||||
responseMessageId
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof opts?.onStart === 'function') {
|
||||
opts.onStart(userMessage);
|
||||
}
|
||||
|
||||
await this.saveMessageToDatabase(userMessage, user);
|
||||
|
||||
this.result = {};
|
||||
const responseMessage = {
|
||||
messageId: responseMessageId,
|
||||
conversationId,
|
||||
parentMessageId: userMessage.messageId,
|
||||
isCreatedByUser: false,
|
||||
model: this.modelOptions.model,
|
||||
sender: 'ChatGPT'
|
||||
};
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('options');
|
||||
console.debug(this.options);
|
||||
}
|
||||
|
||||
const completionMode = this.options.tools.length === 0;
|
||||
if (!completionMode) {
|
||||
await this.initialize({
|
||||
user,
|
||||
message,
|
||||
onAgentAction,
|
||||
onChainEnd,
|
||||
signal: opts.abortController.signal
|
||||
});
|
||||
await this.executorCall(message, opts.abortController.signal);
|
||||
}
|
||||
|
||||
// If message was aborted mid-generation
|
||||
if (this.result?.errorMessage?.length > 0 && this.result?.errorMessage?.includes('cancel')) {
|
||||
responseMessage.text = 'Cancelled.';
|
||||
await this.saveMessageToDatabase(responseMessage, user);
|
||||
return { ...responseMessage, ...this.result };
|
||||
}
|
||||
|
||||
if (!this.agentIsGpt3 && this.result.output) {
|
||||
responseMessage.text = this.result.output;
|
||||
await this.saveMessageToDatabase(responseMessage, user);
|
||||
const textStream = new TextStream(this.result.output);
|
||||
await textStream.processTextStream(onProgress);
|
||||
return { ...responseMessage, ...this.result };
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('this.result', this.result);
|
||||
}
|
||||
|
||||
const userProvidedPrefix = completionMode && this.options?.promptPrefix?.length > 0;
|
||||
const promptPrefix = userProvidedPrefix
|
||||
? this.options.promptPrefix
|
||||
: this.buildPromptPrefix(this.result, message);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('promptPrefix', promptPrefix);
|
||||
}
|
||||
|
||||
const finalReply = await this.sendApiMessage(this.currentMessages, userMessage, { ...opts, completionMode, promptPrefix });
|
||||
responseMessage.text = finalReply;
|
||||
await this.saveMessageToDatabase(responseMessage, user);
|
||||
return { ...responseMessage, ...this.result };
|
||||
}
|
||||
|
||||
async buildPrompt({ messages, promptPrefix: _promptPrefix, completionMode = false, isChatGptModel = true }) {
|
||||
if (this.options.debug) {
|
||||
console.debug('buildPrompt messages', messages);
|
||||
}
|
||||
|
||||
const orderedMessages = messages;
|
||||
let promptPrefix = _promptPrefix;
|
||||
if (promptPrefix) {
|
||||
promptPrefix = promptPrefix.trim();
|
||||
// If the prompt prefix doesn't end with the end token, add it.
|
||||
if (!promptPrefix.endsWith(`${this.endToken}`)) {
|
||||
promptPrefix = `${promptPrefix.trim()}${this.endToken}\n\n`;
|
||||
}
|
||||
promptPrefix = `${this.startToken}Instructions:\n${promptPrefix}`;
|
||||
} else {
|
||||
promptPrefix = `${this.startToken}${completionInstructions} ${this.currentDateString}${this.endToken}\n\n`;
|
||||
}
|
||||
|
||||
const promptSuffix = `${this.startToken}${this.chatGptLabel}:\n`; // Prompt ChatGPT to respond.
|
||||
|
||||
const instructionsPayload = {
|
||||
role: 'system',
|
||||
name: 'instructions',
|
||||
content: promptPrefix
|
||||
};
|
||||
|
||||
const messagePayload = {
|
||||
role: 'system',
|
||||
content: promptSuffix
|
||||
};
|
||||
|
||||
if (this.isGpt3) {
|
||||
instructionsPayload.role = 'user';
|
||||
messagePayload.role = 'user';
|
||||
}
|
||||
|
||||
if (this.isGpt3 && completionMode) {
|
||||
instructionsPayload.content += `\n${promptSuffix}`;
|
||||
}
|
||||
|
||||
// testing if this works with browser endpoint
|
||||
if (!this.isGpt3 && this.reverseProxyUrl) {
|
||||
instructionsPayload.role = 'user';
|
||||
}
|
||||
|
||||
let currentTokenCount;
|
||||
if (isChatGptModel) {
|
||||
currentTokenCount =
|
||||
this.getTokenCountForMessage(instructionsPayload) +
|
||||
this.getTokenCountForMessage(messagePayload);
|
||||
} else {
|
||||
currentTokenCount = this.getTokenCount(`${promptPrefix}${promptSuffix}`);
|
||||
}
|
||||
let promptBody = '';
|
||||
const maxTokenCount = this.maxPromptTokens;
|
||||
|
||||
// Iterate backwards through the messages, adding them to the prompt until we reach the max token count.
|
||||
// Do this within a recursive async function so that it doesn't block the event loop for too long.
|
||||
const buildPromptBody = async () => {
|
||||
if (currentTokenCount < maxTokenCount && orderedMessages.length > 0) {
|
||||
const message = orderedMessages.pop();
|
||||
// const roleLabel = message.role === 'User' ? this.userLabel : this.chatGptLabel;
|
||||
const roleLabel = message.role;
|
||||
let messageString = `${this.startToken}${roleLabel}:\n${message.text}${this.endToken}\n`;
|
||||
let newPromptBody;
|
||||
if (promptBody || isChatGptModel) {
|
||||
newPromptBody = `${messageString}${promptBody}`;
|
||||
} else {
|
||||
// Always insert prompt prefix before the last user message, if not gpt-3.5-turbo.
|
||||
// This makes the AI obey the prompt instructions better, which is important for custom instructions.
|
||||
// After a bunch of testing, it doesn't seem to cause the AI any confusion, even if you ask it things
|
||||
// like "what's the last thing I wrote?".
|
||||
newPromptBody = `${promptPrefix}${messageString}${promptBody}`;
|
||||
}
|
||||
|
||||
const tokenCountForMessage = this.getTokenCount(messageString);
|
||||
const newTokenCount = currentTokenCount + tokenCountForMessage;
|
||||
if (newTokenCount > maxTokenCount) {
|
||||
if (promptBody) {
|
||||
// This message would put us over the token limit, so don't add it.
|
||||
return false;
|
||||
}
|
||||
// This is the first message, so we can't add it. Just throw an error.
|
||||
throw new Error(
|
||||
`Prompt is too long. Max token count is ${maxTokenCount}, but prompt is ${newTokenCount} tokens long.`
|
||||
);
|
||||
}
|
||||
promptBody = newPromptBody;
|
||||
currentTokenCount = newTokenCount;
|
||||
// wait for next tick to avoid blocking the event loop
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
return buildPromptBody();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
await buildPromptBody();
|
||||
|
||||
// const prompt = `${promptBody}${promptSuffix}`;
|
||||
const prompt = promptBody;
|
||||
if (isChatGptModel) {
|
||||
messagePayload.content = prompt;
|
||||
// Add 2 tokens for metadata after all messages have been counted.
|
||||
currentTokenCount += 2;
|
||||
}
|
||||
|
||||
if (this.isGpt3 && messagePayload.content.length > 0) {
|
||||
const context = `Chat History:\n`;
|
||||
messagePayload.content = `${context}${prompt}`;
|
||||
currentTokenCount += this.getTokenCount(context);
|
||||
}
|
||||
|
||||
// Use up to `this.maxContextTokens` tokens (prompt + response), but try to leave `this.maxTokens` tokens for the response.
|
||||
this.modelOptions.max_tokens = Math.min(
|
||||
this.maxContextTokens - currentTokenCount,
|
||||
this.maxResponseTokens
|
||||
);
|
||||
|
||||
if (this.isGpt3 && !completionMode) {
|
||||
messagePayload.content += promptSuffix;
|
||||
return [instructionsPayload, messagePayload];
|
||||
}
|
||||
|
||||
if (isChatGptModel) {
|
||||
const result = [messagePayload, instructionsPayload];
|
||||
return result.filter((message) => message.content.length > 0);
|
||||
}
|
||||
|
||||
this.completionPromptTokens = currentTokenCount;
|
||||
return prompt;
|
||||
}
|
||||
|
||||
getTokenCount(text) {
|
||||
return this.gptEncoder.encode(text, 'all').length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm adapted from "6. Counting tokens for chat API calls" of
|
||||
* https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
|
||||
*
|
||||
* An additional 2 tokens need to be added for metadata after all messages have been counted.
|
||||
*
|
||||
* @param {*} message
|
||||
*/
|
||||
getTokenCountForMessage(message) {
|
||||
// Map each property of the message to the number of tokens it contains
|
||||
const propertyTokenCounts = Object.entries(message).map(([key, value]) => {
|
||||
// Count the number of tokens in the property value
|
||||
const numTokens = this.getTokenCount(value);
|
||||
|
||||
// Subtract 1 token if the property key is 'name'
|
||||
const adjustment = key === 'name' ? 1 : 0;
|
||||
return numTokens - adjustment;
|
||||
});
|
||||
|
||||
// Sum the number of tokens in all properties and add 4 for metadata
|
||||
return propertyTokenCounts.reduce((a, b) => a + b, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through messages, building an array based on the parentMessageId.
|
||||
* Each message has an id and a parentMessageId. The parentMessageId is the id of the message that this message is a reply to.
|
||||
* @param messages
|
||||
* @param parentMessageId
|
||||
* @returns {*[]} An array containing the messages in the order they should be displayed, starting with the root message.
|
||||
*/
|
||||
static getMessagesForConversation(messages, parentMessageId) {
|
||||
const orderedMessages = [];
|
||||
let currentMessageId = parentMessageId;
|
||||
while (currentMessageId) {
|
||||
// eslint-disable-next-line no-loop-func
|
||||
const message = messages.find((m) => m.messageId === currentMessageId);
|
||||
if (!message) {
|
||||
break;
|
||||
}
|
||||
orderedMessages.unshift(message);
|
||||
currentMessageId = message.parentMessageId;
|
||||
}
|
||||
|
||||
if (orderedMessages.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return orderedMessages.map((msg) => ({
|
||||
messageId: msg.messageId,
|
||||
parentMessageId: msg.parentMessageId,
|
||||
role: msg.isCreatedByUser ? 'User' : 'ChatGPT',
|
||||
text: msg.text
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the action tool values from the intermediate steps array.
|
||||
* Each step object in the array contains an action object with a tool property.
|
||||
* This function returns an array of tool values.
|
||||
*
|
||||
* @param {Object[]} intermediateSteps - An array of intermediate step objects.
|
||||
* @returns {string} An string of action tool values from each step.
|
||||
*/
|
||||
extractToolValues(intermediateSteps) {
|
||||
const tools = intermediateSteps.map((step) => step.action.tool);
|
||||
|
||||
if (tools.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const uniqueTools = [...new Set(tools)];
|
||||
|
||||
if (tools.length === 1) {
|
||||
return tools[0] + ' plugin';
|
||||
}
|
||||
|
||||
return uniqueTools.join(' plugin, ');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ChatAgent;
|
||||
@@ -1,92 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
const ChatAgent = require('./ChatAgent');
|
||||
const connectDb = require('../../lib/db/connectDb');
|
||||
const Conversation = require('../../models/Conversation');
|
||||
|
||||
describe('ChatAgent', () => {
|
||||
let TestAgent;
|
||||
let options = {
|
||||
tools: [],
|
||||
modelOptions: {
|
||||
model: 'gpt-3.5-turbo',
|
||||
temperature: 0,
|
||||
max_tokens: 2
|
||||
},
|
||||
agentOptions: {
|
||||
model: 'gpt-3.5-turbo',
|
||||
}
|
||||
};
|
||||
let parentMessageId;
|
||||
let conversationId;
|
||||
const userMessage = 'Hello, ChatGPT!';
|
||||
const apiKey = process.env.OPENAI_API_KEY;
|
||||
|
||||
beforeAll(async () => {
|
||||
await connectDb();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
TestAgent = new ChatAgent(apiKey, options);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Delete the messages and conversation created by the test
|
||||
await Conversation.deleteConvos(null, { conversationId });
|
||||
await mongoose.connection.close();
|
||||
});
|
||||
|
||||
test('initializes ChatAgent without crashing', () => {
|
||||
expect(TestAgent).toBeInstanceOf(ChatAgent);
|
||||
});
|
||||
|
||||
test('check setOptions function', () => {
|
||||
expect(TestAgent.agentIsGpt3).toBe(true);
|
||||
});
|
||||
|
||||
describe('sendMessage', () => {
|
||||
test('sendMessage should return a response message', async () => {
|
||||
const expectedResult = expect.objectContaining({
|
||||
sender: 'ChatGPT',
|
||||
text: expect.any(String),
|
||||
isCreatedByUser: false,
|
||||
messageId: expect.any(String),
|
||||
parentMessageId: expect.any(String),
|
||||
conversationId: expect.any(String)
|
||||
});
|
||||
|
||||
const response = await TestAgent.sendMessage(userMessage);
|
||||
console.log(response);
|
||||
parentMessageId = response.messageId;
|
||||
conversationId = response.conversationId;
|
||||
expect(response).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
test('sendMessage should work with provided conversationId and parentMessageId', async () => {
|
||||
const userMessage = 'Second message in the conversation';
|
||||
const opts = {
|
||||
conversationId,
|
||||
parentMessageId
|
||||
};
|
||||
|
||||
const expectedResult = expect.objectContaining({
|
||||
sender: 'ChatGPT',
|
||||
text: expect.any(String),
|
||||
isCreatedByUser: false,
|
||||
messageId: expect.any(String),
|
||||
parentMessageId: expect.any(String),
|
||||
conversationId: opts.conversationId
|
||||
});
|
||||
|
||||
const response = await TestAgent.sendMessage(userMessage, opts);
|
||||
parentMessageId = response.messageId;
|
||||
expect(response.conversationId).toEqual(conversationId);
|
||||
expect(response).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
test('should return chat history', async () => {
|
||||
const chatMessages = await TestAgent.loadHistory(conversationId, parentMessageId);
|
||||
expect(TestAgent.currentMessages).toHaveLength(4);
|
||||
expect(chatMessages[0].text).toEqual(userMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user