Compare commits
500 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
e4c91dfbea | ||
|
|
aaa20309a0 | ||
|
|
8c4a3b2729 | ||
|
|
638faf9850 | ||
|
|
f845192d2d | ||
|
|
dfd93909e8 | ||
|
|
47d0184990 | ||
|
|
ee59fa40f5 | ||
|
|
68b3731d06 | ||
|
|
e4889ff8bb | ||
|
|
2c026d11a5 | ||
|
|
1a252170f5 | ||
|
|
d40aaa703d | ||
|
|
44005258fc | ||
|
|
19495a461d | ||
|
|
fcf068dddf | ||
|
|
7468b3011f | ||
|
|
dade7b450f | ||
|
|
7fbf27c5aa | ||
|
|
4705975e59 | ||
|
|
2f59c82bec | ||
|
|
6a34978e98 | ||
|
|
d437e4b8cd | ||
|
|
f40a2f8ee8 | ||
|
|
2d31c9f8b6 | ||
|
|
fd5afc09a2 | ||
|
|
c0845ad0b1 | ||
|
|
11b98d3d13 | ||
|
|
4f17e69f1b | ||
|
|
b912e7a3dd | ||
|
|
743a9315ff | ||
|
|
ea2135a237 | ||
|
|
6a1983bc6c | ||
|
|
07796d9e48 | ||
|
|
634849ec12 | ||
|
|
112c6c5b19 | ||
|
|
b8c3ae5e8f | ||
|
|
07fa0f39fd | ||
|
|
4eda4542b7 | ||
|
|
dbfef342e2 | ||
|
|
735eb159db | ||
|
|
bf911074cf | ||
|
|
7ec061c694 | ||
|
|
a6f3eb4c0d | ||
|
|
dc5f9d8474 | ||
|
|
c1349fbfaa | ||
|
|
6f9da5f7df | ||
|
|
10de50416b | ||
|
|
4beb06aa4b | ||
|
|
791b515937 | ||
|
|
8d4ef16b7f | ||
|
|
5964b71e14 | ||
|
|
8c7ad09977 | ||
|
|
cef2668f53 | ||
|
|
ab7cfc6041 | ||
|
|
a9444b66a1 | ||
|
|
ec561fcd7f | ||
|
|
ee2b3e4fb2 | ||
|
|
67716f0d2d | ||
|
|
e56d90e45a | ||
|
|
92eee52c52 | ||
|
|
f4d995be4c | ||
|
|
fbdfbdd620 | ||
|
|
fec733e10b | ||
|
|
23905dd344 | ||
|
|
ec13d74b84 | ||
|
|
231906161b | ||
|
|
5c787035e5 | ||
|
|
2694690ed0 | ||
|
|
562bf8c920 | ||
|
|
2781154df3 | ||
|
|
691b6d9029 | ||
|
|
c6c3054c22 | ||
|
|
d71b61ad71 | ||
|
|
47533736e3 | ||
|
|
a17b878617 | ||
|
|
91ef4872d6 | ||
|
|
c1ddd07166 | ||
|
|
7fdc862042 | ||
|
|
8d75b25104 | ||
|
|
26152d7e5f | ||
|
|
61a4231feb | ||
|
|
c9b035a0bd | ||
|
|
44ea3601c9 | ||
|
|
782a899ab3 | ||
|
|
14104b276f | ||
|
|
08f3a77d58 | ||
|
|
ca26732cb8 | ||
|
|
dbf45196ee | ||
|
|
45a2aaf7b8 | ||
|
|
c02c62f3b1 | ||
|
|
4718674688 | ||
|
|
0e3c115368 | ||
|
|
cc506c23af | ||
|
|
1f77d94b7e | ||
|
|
5711ff27ee | ||
|
|
3120602d6a | ||
|
|
9f36e195bc | ||
|
|
9de7da91a7 | ||
|
|
501a15a18f | ||
|
|
6049c9e3ff | ||
|
|
262b402606 | ||
|
|
56ea9563b8 | ||
|
|
2cd6612620 | ||
|
|
5d40396fb2 | ||
|
|
93dd1eb036 | ||
|
|
542a46dc7c | ||
|
|
bf31b1fea0 | ||
|
|
25d4529ff9 | ||
|
|
33d7c67c04 | ||
|
|
dc8f762bac | ||
|
|
49041e16c7 | ||
|
|
3414690e42 | ||
|
|
95c97561ae | ||
|
|
8bb4d7d590 | ||
|
|
94ad31dce3 | ||
|
|
91e9b167b3 | ||
|
|
53ea3dd9fb | ||
|
|
c72a3a0362 | ||
|
|
bd068c9a5a | ||
|
|
7997c3137a | ||
|
|
b466b36e7a | ||
|
|
03d871316a | ||
|
|
4b94af0429 | ||
|
|
5dd9c11326 | ||
|
|
e2dc994b63 | ||
|
|
177028aafc | ||
|
|
907f894ba7 | ||
|
|
3b4ed98c1d | ||
|
|
d7b415837b | ||
|
|
cc1fcbe949 | ||
|
|
bdcb7acd72 | ||
|
|
960e8c4724 | ||
|
|
dac19038a3 | ||
|
|
65543eb084 | ||
|
|
cc18938235 | ||
|
|
1049b403c3 | ||
|
|
fb3fc55e9f | ||
|
|
d07e5f5241 | ||
|
|
f7114c16c2 | ||
|
|
4699ad21c7 | ||
|
|
766bd0c587 | ||
|
|
9116e98928 | ||
|
|
8e8ccb9c8b | ||
|
|
5fd238af64 | ||
|
|
03f4e89f1c | ||
|
|
75cef1ebb1 | ||
|
|
857481c263 | ||
|
|
8f462e074c | ||
|
|
3eddc9712f | ||
|
|
52f99151ec | ||
|
|
d839ea324a | ||
|
|
e02e6152ed | ||
|
|
2b7c1507ef | ||
|
|
f5ff91cfbd | ||
|
|
901375bfa0 | ||
|
|
15a734b6c8 | ||
|
|
4f119296f4 | ||
|
|
b88828e29a | ||
|
|
3447137515 | ||
|
|
6aa540c4af | ||
|
|
163388b8a9 | ||
|
|
e68c163ef6 | ||
|
|
dcf2ee480b | ||
|
|
5c5871afd8 | ||
|
|
e0d5e75e73 | ||
|
|
fb7542c865 | ||
|
|
0bd240939a | ||
|
|
31893ec9f6 | ||
|
|
78ae220f7e | ||
|
|
478814ff1b | ||
|
|
fd0152e39f | ||
|
|
f5f327e79e | ||
|
|
9871127114 | ||
|
|
a5a0eab7f7 | ||
|
|
bbf2f8a6ca | ||
|
|
a953fc9f2b | ||
|
|
a81bd27b39 | ||
|
|
6246ffff1e | ||
|
|
b59588c6ee | ||
|
|
828e438d53 | ||
|
|
f946f90ef6 | ||
|
|
956d919751 | ||
|
|
4f1fc3f020 | ||
|
|
c1b6f96bca | ||
|
|
ae1333db37 | ||
|
|
19c0f00cf2 | ||
|
|
3c8ac933ee | ||
|
|
50ec165f71 | ||
|
|
942a85f531 | ||
|
|
83d0443c8a | ||
|
|
88aea81288 | ||
|
|
5fbefa15ce | ||
|
|
0f62be812a | ||
|
|
92c1bb6511 | ||
|
|
cb56b74b0e | ||
|
|
94078ce5d4 | ||
|
|
e5e4ee2987 | ||
|
|
100be3b42f | ||
|
|
0a80f836f0 | ||
|
|
8b952be268 | ||
|
|
285351bb53 | ||
|
|
625f63b072 | ||
|
|
bf4258c0a5 | ||
|
|
9a0e3804fa | ||
|
|
90946011f7 | ||
|
|
06b90f6a77 | ||
|
|
96b004a696 | ||
|
|
9623fe2e9f | ||
|
|
d79d297441 | ||
|
|
fd5fba45e6 | ||
|
|
6e42d4fa3d | ||
|
|
5ea8f75f70 | ||
|
|
7ec90a3585 | ||
|
|
fc91ed49bc | ||
|
|
6ce1b9d850 | ||
|
|
c983670b9e | ||
|
|
e0f9e92bfc | ||
|
|
ca720efde8 | ||
|
|
34c3663308 | ||
|
|
15999bda95 | ||
|
|
6a77401978 | ||
|
|
644ff160fc | ||
|
|
e1c6517b8f | ||
|
|
8a243e12fb | ||
|
|
3f42db4956 | ||
|
|
323e951d7f | ||
|
|
11e4928582 | ||
|
|
24cb6d4013 | ||
|
|
19183678a3 | ||
|
|
dccd766d91 | ||
|
|
d24abf6a2a | ||
|
|
6b1b0f967d | ||
|
|
a56c8696d3 | ||
|
|
7b7ba96786 | ||
|
|
0fb9820110 | ||
|
|
c271f044c7 | ||
|
|
ec2ddc168b | ||
|
|
0d5b51ec8c | ||
|
|
06a7ed31ac | ||
|
|
4eff1c03dd | ||
|
|
83df28f45d | ||
|
|
48e33fe1e9 | ||
|
|
fbeff7a461 | ||
|
|
61cb2858bb | ||
|
|
3d0bfaef51 | ||
|
|
f2d18c81fc | ||
|
|
68041d68ae | ||
|
|
93b685a1a2 | ||
|
|
9e708225aa | ||
|
|
1cb8ef9803 | ||
|
|
573112de7b | ||
|
|
dd0a91a9f6 | ||
|
|
94e0636b32 | ||
|
|
bd53b878d4 | ||
|
|
c6d3bcd457 | ||
|
|
39f53e6ddf | ||
|
|
10941bf623 | ||
|
|
8c392ac05e | ||
|
|
9dae1ade60 | ||
|
|
ccc2f392e2 | ||
|
|
2048e34311 | ||
|
|
2589754171 | ||
|
|
4510f04073 | ||
|
|
e98ce09d6b | ||
|
|
21920dd864 | ||
|
|
7d45d229af | ||
|
|
5dad9da918 | ||
|
|
e0b0b68346 | ||
|
|
31cef16cc3 | ||
|
|
4245b43140 | ||
|
|
5664a0c2a5 | ||
|
|
dde6de6bd5 | ||
|
|
e077f2b73d | ||
|
|
6e8a0a2f94 | ||
|
|
96914387a6 | ||
|
|
6f0b559927 | ||
|
|
3b94a98719 | ||
|
|
017447b064 | ||
|
|
385eb2f398 | ||
|
|
963939fe76 | ||
|
|
cf3902567e | ||
|
|
b4d451d7ae | ||
|
|
97e39b8203 | ||
|
|
60f51dfeeb | ||
|
|
e7a6589958 | ||
|
|
d4cd9411c0 | ||
|
|
e1c731299c | ||
|
|
214067cfcb | ||
|
|
7d3df376ef | ||
|
|
474ced1dbe | ||
|
|
55e949578f | ||
|
|
b730d631af | ||
|
|
03cade8bd5 | ||
|
|
22b9524ad3 | ||
|
|
a5202f84cc | ||
|
|
4b373ebc0e | ||
|
|
0bc2db08c7 | ||
|
|
a641a0e373 | ||
|
|
6abe34ee3b | ||
|
|
8c2d577e60 | ||
|
|
5aa6b516ed | ||
|
|
0846aa0436 | ||
|
|
3ab9b7f736 | ||
|
|
56e02f4968 | ||
|
|
46fba87f9a | ||
|
|
ee10e0e43e | ||
|
|
579b53de29 | ||
|
|
9f1ded7f75 | ||
|
|
efb440128a | ||
|
|
010d900c90 | ||
|
|
0c4b754fba | ||
|
|
d864da6a21 | ||
|
|
0ec68bf5a8 | ||
|
|
82b4401e49 | ||
|
|
99d238b0de | ||
|
|
617aa6f499 | ||
|
|
f922a1d102 | ||
|
|
bb75b6df3b | ||
|
|
4b04959290 | ||
|
|
28c5b21217 | ||
|
|
086267ee01 | ||
|
|
3484ff687d | ||
|
|
6e233accf5 | ||
|
|
1a196580b2 | ||
|
|
e1d52b858b | ||
|
|
cef98070e9 | ||
|
|
7353829719 | ||
|
|
89e38d67f4 | ||
|
|
825be5aa4d | ||
|
|
d8deb89e3a | ||
|
|
da69e4176a | ||
|
|
85372118ed | ||
|
|
4fe48fe6bb | ||
|
|
03f63975cc | ||
|
|
28f1e851f9 | ||
|
|
58d162d8b4 | ||
|
|
4961cc9250 | ||
|
|
8720f39def | ||
|
|
089d99e679 | ||
|
|
d2579b44d1 | ||
|
|
dae0c2d5e3 | ||
|
|
3d246df6c9 | ||
|
|
ea88e9b981 | ||
|
|
584772b3f2 | ||
|
|
e7c9ae5a7d | ||
|
|
4a95b30a0d | ||
|
|
15ada95249 | ||
|
|
ced65f8c98 | ||
|
|
871bc4c78b | ||
|
|
24a82c8018 | ||
|
|
cd7e3f3774 | ||
|
|
ab7cf8c881 | ||
|
|
aa4fd57459 | ||
|
|
8551995a51 | ||
|
|
85f3b488ce | ||
|
|
12b7e9a6bb | ||
|
|
d5e062eeed | ||
|
|
f5e120c330 | ||
|
|
afaa0253b8 | ||
|
|
3ec2942365 | ||
|
|
6d7f0448ff | ||
|
|
70427a628f | ||
|
|
b19ef425b7 | ||
|
|
b7c911534c | ||
|
|
fe6b1fc12a | ||
|
|
bd94c4233d | ||
|
|
eef2303c8e | ||
|
|
6f30b6ec9d | ||
|
|
709dbbbc4b | ||
|
|
77b46cc1a7 | ||
|
|
f069d6b621 | ||
|
|
f187da3afa | ||
|
|
0564e3ed93 | ||
|
|
6cad5d5b50 | ||
|
|
8bc4d5e3fd | ||
|
|
b14726b2dd | ||
|
|
3121a3a6ba | ||
|
|
8fa20d9356 | ||
|
|
48b901fdfe | ||
|
|
c65a3b69ff | ||
|
|
2cce2e30ba | ||
|
|
ad5fbc5fb1 | ||
|
|
eb2d9bac33 | ||
|
|
45e17da241 | ||
|
|
80ef5008dd | ||
|
|
6498ab79a4 | ||
|
|
78dabe55ae | ||
|
|
64fc2e84f2 | ||
|
|
ff757f27cf | ||
|
|
2157eb8f30 | ||
|
|
7487b59de7 | ||
|
|
efe55d8842 | ||
|
|
d76efa7874 | ||
|
|
c5805d710b | ||
|
|
f5ffa81455 | ||
|
|
4d7fa26e6c | ||
|
|
099210c10e | ||
|
|
46e3ef4049 | ||
|
|
edaf7c3ad1 | ||
|
|
60be4f12b7 | ||
|
|
b687ab30ed | ||
|
|
467e24c9ed | ||
|
|
9098362377 | ||
|
|
064dfb5336 | ||
|
|
404566c1fb | ||
|
|
92dbdcaaa2 | ||
|
|
97e3ff6b6f | ||
|
|
b5541903e4 | ||
|
|
580d82ffe8 | ||
|
|
5cb59885ec | ||
|
|
ec47879edc | ||
|
|
b67af67433 | ||
|
|
e8b2c2059d | ||
|
|
bb1f8d731b | ||
|
|
059006382d | ||
|
|
462660d554 | ||
|
|
b703d3706b | ||
|
|
8b86fd0901 | ||
|
|
a8e44d0028 | ||
|
|
babc4da7ab | ||
|
|
03f1a439d6 | ||
|
|
96639b82a8 | ||
|
|
e8e3903b78 | ||
|
|
adcc021c9e | ||
|
|
089ca5f120 | ||
|
|
7e8b31cd09 | ||
|
|
c53778e7c1 | ||
|
|
dd825dc6d4 | ||
|
|
36c126e7a5 | ||
|
|
9b04ffabca |
57
.devcontainer/devcontainer.json
Normal file
57
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,57 @@
|
||||
// {
|
||||
// "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"
|
||||
// },
|
||||
}
|
||||
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,2 +1,5 @@
|
||||
**/node_modules
|
||||
**/.env
|
||||
client/dist/images
|
||||
data-node
|
||||
.env
|
||||
**/.env
|
||||
220
.env.example
Normal file
220
.env.example
Normal file
@@ -0,0 +1,220 @@
|
||||
##########################
|
||||
# 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.
|
||||
# Tips: Setting host to 0.0.0.0 means listening on all interfaces. It's not a real IP.
|
||||
# Use localhost:port rather than 0.0.0.0:port to access the server.
|
||||
# Set Node env to development if running in dev mode.
|
||||
HOST=localhost
|
||||
PORT=3080
|
||||
|
||||
# Change this to proxy any API request.
|
||||
# It's useful if your machine has difficulty calling the original API server.
|
||||
# PROXY=
|
||||
|
||||
# Change this to your MongoDB URI if different. I recommend appending LibreChat.
|
||||
MONGO_URI=mongodb://127.0.0.1:27017/LibreChat
|
||||
|
||||
##########################
|
||||
# OpenAI Endpoint:
|
||||
##########################
|
||||
|
||||
# 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"
|
||||
|
||||
# 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-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
|
||||
# OPENAI_REVERSE_PROXY=
|
||||
|
||||
##########################
|
||||
# AZURE Endpoint:
|
||||
##########################
|
||||
|
||||
# To use Azure with this project, set the following variables. These will be used to build the API URL.
|
||||
# Chat completion:
|
||||
# `https://{AZURE_OPENAI_API_INSTANCE_NAME}.openai.azure.com/openai/deployments/{AZURE_OPENAI_API_DEPLOYMENT_NAME}/chat/completions?api-version={AZURE_OPENAI_API_VERSION}`;
|
||||
# You should also consider changing the `OPENAI_MODELS` variable above to the models available in your instance/deployment.
|
||||
# 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_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=
|
||||
|
||||
##########################
|
||||
# BingAI Endpoint:
|
||||
##########################
|
||||
|
||||
# Also used for Sydney and jailbreak
|
||||
# To get your Access token for Bing, login to https://www.bing.com
|
||||
# Use dev tools or an extension while logged into the site to copy the content of the _U cookie.
|
||||
#If this fails, follow these instructions https://github.com/danny-avila/LibreChat/issues/370#issuecomment-1560382302 to provide the full cookie strings.
|
||||
# Set to "user_provided" to allow the user to provide its token from the UI.
|
||||
# Leave it blank to disable this endpoint.
|
||||
BINGAI_TOKEN="user_provided"
|
||||
|
||||
# BingAI Host:
|
||||
# Necessary for some people in different countries, e.g. China (https://cn.bing.com)
|
||||
# Leave it blank to use default server.
|
||||
# BINGAI_HOST=https://cn.bing.com
|
||||
|
||||
##########################
|
||||
# ChatGPT Endpoint:
|
||||
##########################
|
||||
|
||||
# ChatGPT Browser Client (free but use at your own risk)
|
||||
# Access token from https://chat.openai.com/api/auth/session
|
||||
# Exposes your access token to `CHATGPT_REVERSE_PROXY`
|
||||
# Set to "user_provided" to allow the user to provide its token from the UI.
|
||||
# Leave it blank to disable this endpoint
|
||||
CHATGPT_TOKEN="user_provided"
|
||||
|
||||
# Identify the available models, separated by commas. The first will be default.
|
||||
# Leave it blank to use internal settings.
|
||||
CHATGPT_MODELS=text-davinci-002-render-sha,gpt-4
|
||||
# NOTE: you can add gpt-4-plugins, gpt-4-code-interpreter, and gpt-4-browsing to the list above and use the models for these features;
|
||||
# however, the view/display portion of these features are not supported, but you can use the underlying models, which have higher token context
|
||||
# Also: text-davinci-002-render-paid is deprecated as of May 2023
|
||||
|
||||
# Reverse proxy setting for OpenAI
|
||||
# https://github.com/waylaidwanderer/node-chatgpt-api#using-a-reverse-proxy
|
||||
# By default it will use the node-chatgpt-api recommended proxy, (it's a third party server)
|
||||
# CHATGPT_REVERSE_PROXY=<YOUR REVERSE PROXY>
|
||||
|
||||
#############################
|
||||
# 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)
|
||||
# Use this replit to generate some quickly: https://replit.com/@daavila/crypto#index.js
|
||||
# Here are some examples (THESE ARE NOT SECURE!)
|
||||
CREDS_KEY=f34be427ebb29de8d88c107a71546019685ed8b241d8f2ed00c3df97ad2566f0
|
||||
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/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.
|
||||
# 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:
|
||||
##########################
|
||||
|
||||
# Follow the instruction here to setup:
|
||||
# https://github.com/danny-avila/LibreChat/blob/main/docs/install/apis_and_tokens.md
|
||||
|
||||
PALM_KEY="user_provided"
|
||||
|
||||
# In case you need a reverse proxy for this endpoint:
|
||||
# GOOGLE_REVERSE_PROXY=
|
||||
|
||||
##########################
|
||||
# Proxy: To be Used by all endpoints
|
||||
##########################
|
||||
|
||||
PROXY=
|
||||
|
||||
##########################
|
||||
# Search:
|
||||
##########################
|
||||
|
||||
# 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=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.
|
||||
MEILI_HOST=http://0.0.0.0:7700
|
||||
|
||||
# REQUIRED FOR SEARCH: MeiliSearch HTTP Address, mainly for docker-compose to expose the search server.
|
||||
# Replace '0.0.0.0' with 'meilisearch' if serving MeiliSearch with docker-compose.
|
||||
MEILI_HTTP_ADDR=0.0.0.0:7700
|
||||
|
||||
# REQUIRED FOR SEARCH: In production env., a secure key is needed. You can generate your own.
|
||||
# This master key must be at least 16 bytes, composed of valid UTF-8 characters.
|
||||
# MeiliSearch will throw an error and refuse to launch if no master key is provided,
|
||||
# or if it is under 16 bytes. MeiliSearch will suggest a secure autogenerated master key.
|
||||
# Using docker, it seems recognized as production so use a secure key.
|
||||
# This is a ready made secure key for docker-compose, you can replace it with your own.
|
||||
MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFCt
|
||||
|
||||
##########################
|
||||
# User System:
|
||||
##########################
|
||||
|
||||
# Allow Public Registration
|
||||
ALLOW_REGISTRATION=true
|
||||
|
||||
# JWT Secrets
|
||||
JWT_SECRET=secret
|
||||
JWT_REFRESH_SECRET=secret
|
||||
|
||||
# Google:
|
||||
# Add your Google Client ID and Secret here, you must register an app with Google Cloud to get these values
|
||||
# https://cloud.google.com/
|
||||
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_AUTH_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
|
||||
|
||||
###########################
|
||||
# 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 need to change DOMAIN_SERVER to localhost:3090 or place it in .env.development
|
||||
|
||||
DOMAIN_CLIENT=http://localhost:3080
|
||||
DOMAIN_SERVER=http://localhost:3080
|
||||
115
.eslintrc.js
Normal file
115
.eslintrc.js
Normal file
@@ -0,0 +1,115 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
commonjs: true,
|
||||
es6: true
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
"plugin:jest/recommended",
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
},
|
||||
plugins: ['react', 'react-hooks', '@typescript-eslint'],
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow-with-description' }],
|
||||
indent: ['error', 2, { SwitchCase: 1 }],
|
||||
'max-len': [
|
||||
'error',
|
||||
{
|
||||
code: 150,
|
||||
ignoreStrings: true,
|
||||
ignoreTemplateLiterals: true,
|
||||
ignoreComments: true
|
||||
}
|
||||
],
|
||||
'linebreak-style': 0,
|
||||
// "arrow-parens": [2, "as-needed", { requireForBlockBody: true }],
|
||||
// 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
|
||||
'no-console': 'off',
|
||||
'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']
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
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'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['rollup.config.js', '.eslintrc.js', 'jest.config.js'],
|
||||
env: {
|
||||
node: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'**/*.test.js',
|
||||
'**/*.test.jsx',
|
||||
'**/*.test.ts',
|
||||
'**/*.test.tsx',
|
||||
'**/*.spec.js',
|
||||
'**/*.spec.jsx',
|
||||
'**/*.spec.ts',
|
||||
'**/*.spec.tsx',
|
||||
'setupTests.js'
|
||||
],
|
||||
env: {
|
||||
jest: true,
|
||||
node: true
|
||||
},
|
||||
rules: {
|
||||
'react/display-name': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react/no-unescaped-entities': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: '**/*.+(ts)',
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: './client/tsconfig.json'
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin', 'jest'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended'
|
||||
]
|
||||
}
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
createClass: 'createReactClass', // Regex for Component Factory to use,
|
||||
// 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.
|
||||
}
|
||||
}
|
||||
};
|
||||
64
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
Normal file
64
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: Bug Report
|
||||
description: File a bug report
|
||||
title: "[Bug]: "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: input
|
||||
id: contact
|
||||
attributes:
|
||||
label: Contact Details
|
||||
description: How can we get in touch with you if we need more info?
|
||||
placeholder: ex. email@example.com
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: Please give as many details as possible
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Please list the steps needed to reproduce the issue.
|
||||
placeholder: "1. Step 1\n2. Step 2\n3. Step 3"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browsers are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- Mobile (iOS)
|
||||
- Mobile (Android)
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots to help explain your problem. You can drag and drop, paste images directly here or link to them.
|
||||
- type: checkboxes
|
||||
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/LibreChat/blob/main/CODE_OF_CONDUCT.md)
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
57
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
vendored
Normal file
57
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: Feature Request
|
||||
description: File a feature request
|
||||
title: "Enhancement: "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to fill this out!
|
||||
- type: input
|
||||
id: contact
|
||||
attributes:
|
||||
label: Contact Details
|
||||
description: How can we contact you if we need more information?
|
||||
placeholder: ex. email@example.com
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: what
|
||||
attributes:
|
||||
label: What features would you like to see added?
|
||||
description: Please provide as many details as possible.
|
||||
placeholder: Please provide as many details as possible.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: details
|
||||
attributes:
|
||||
label: More details
|
||||
description: Please provide additional details if needed.
|
||||
placeholder: Please provide additional details if needed.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: subject
|
||||
attributes:
|
||||
label: Which components are impacted by your request?
|
||||
multiple: true
|
||||
options:
|
||||
- General
|
||||
- UI
|
||||
- Endpoints
|
||||
- Plugins
|
||||
- Other
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Pictures
|
||||
description: If relevant, please include images to help clarify your request. You can drag and drop images directly here, paste them, or provide a link to them.
|
||||
- type: checkboxes
|
||||
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/LibreChat/blob/main/CODE_OF_CONDUCT.md)
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
58
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
Normal file
58
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Question
|
||||
description: Ask your question
|
||||
title: "[Question]: "
|
||||
labels: ["question"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill this!
|
||||
- type: input
|
||||
id: contact
|
||||
attributes:
|
||||
label: Contact Details
|
||||
description: How can we get in touch with you if we need more info?
|
||||
placeholder: ex. email@example.com
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: what-is-your-question
|
||||
attributes:
|
||||
label: What is your question?
|
||||
description: Please give as many details as possible
|
||||
placeholder: Please give as many details as possible
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: more-details
|
||||
attributes:
|
||||
label: More Details
|
||||
description: Please provide more details if needed.
|
||||
placeholder: Please provide more details if needed.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What is the main subject of your question?
|
||||
multiple: true
|
||||
options:
|
||||
- Documentation
|
||||
- Installation
|
||||
- UI
|
||||
- Endpoints
|
||||
- User System/OAuth
|
||||
- Other
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots to help explain your problem. You can drag and drop, paste images directly here or link to them.
|
||||
- type: checkboxes
|
||||
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/LibreChat/blob/main/CODE_OF_CONDUCT.md)
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
47
.github/dependabot.yml
vendored
Normal file
47
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/api" # Location of package manifests
|
||||
target-branch: "develop"
|
||||
versioning-strategy: increase-if-necessary
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
allow:
|
||||
# Allow both direct and indirect updates for all packages
|
||||
- dependency-type: "all"
|
||||
commit-message:
|
||||
prefix: "npm api prod"
|
||||
prefix-development: "npm api dev"
|
||||
include: "scope"
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/client" # Location of package manifests
|
||||
target-branch: "develop"
|
||||
versioning-strategy: increase-if-necessary
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
allow:
|
||||
# Allow both direct and indirect updates for all packages
|
||||
- dependency-type: "all"
|
||||
commit-message:
|
||||
prefix: "npm client prod"
|
||||
prefix-development: "npm client dev"
|
||||
include: "scope"
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
target-branch: "develop"
|
||||
versioning-strategy: increase-if-necessary
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
allow:
|
||||
# Allow both direct and indirect updates for all packages
|
||||
- dependency-type: "all"
|
||||
commit-message:
|
||||
prefix: "npm all prod"
|
||||
prefix-development: "npm all dev"
|
||||
include: "scope"
|
||||
|
||||
62
.github/playwright.yml
vendored
Normal file
62
.github/playwright.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [feat/playwright-jest-cicd]
|
||||
pull_request:
|
||||
branches: [feat/playwright-jest-cicd]
|
||||
jobs:
|
||||
tests_e2e:
|
||||
name: Run Playwright 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 }}
|
||||
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 }}
|
||||
# NODE_ENV: ${{ vars.NODE_ENV }}
|
||||
DOMAIN_CLIENT: ${{ vars.DOMAIN_CLIENT }}
|
||||
DOMAIN_SERVER: ${{ vars.DOMAIN_SERVER }}
|
||||
# PALM_KEY: ${{ secrets.PALM_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install global dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
- name: Install API dependencies
|
||||
working-directory: ./api
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
- name: Install Client dependencies
|
||||
working-directory: ./client
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
- name: Build Client
|
||||
run: cd client && npm run build:ci
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps && npm install -D @playwright/test
|
||||
|
||||
- name: Start server
|
||||
run: |
|
||||
npm run backend & sleep 10
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test --config=e2e/playwright.config.ts
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: e2e/playwright-report/
|
||||
retention-days: 30
|
||||
35
.github/pull_request_template.md
vendored
Normal file
35
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
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.
|
||||
|
||||
|
||||
|
||||
## Type of change
|
||||
|
||||
Please delete options that are not relevant.
|
||||
|
||||
- [ ] 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
|
||||
|
||||
|
||||
## 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:
|
||||
##
|
||||
|
||||
|
||||
### **Test Configuration**:
|
||||
##
|
||||
|
||||
|
||||
## 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
|
||||
28
.github/wip-playwright.yml
vendored
Normal file
28
.github/wip-playwright.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
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
|
||||
39
.github/workflows/backend-review.yml
vendored
Normal file
39
.github/workflows/backend-review.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Backend Unit Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- release/*
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- release/*
|
||||
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 19.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 19.x
|
||||
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
|
||||
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/frontend-review.yml
vendored
Normal file
34
.github/workflows/frontend-review.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
#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/*
|
||||
jobs:
|
||||
tests_frontend:
|
||||
name: Run frontend unit tests
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 19.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 19.x
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
- name: Build Client
|
||||
run: cd client && npm run build: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
|
||||
21
.gitignore
vendored
21
.gitignore
vendored
@@ -26,6 +26,7 @@ dist/
|
||||
public/main.js
|
||||
public/main.js.map
|
||||
public/main.js.LICENSE.txt
|
||||
client/public/images/
|
||||
client/public/main.js
|
||||
client/public/main.js.map
|
||||
client/public/main.js.LICENSE.txt
|
||||
@@ -46,11 +47,29 @@ bower_components/
|
||||
.flooignore
|
||||
|
||||
# Environment
|
||||
.npmrc
|
||||
.env
|
||||
!.env.example
|
||||
!.env.test.example
|
||||
.env*
|
||||
cache.json
|
||||
api/data/
|
||||
owner.yml
|
||||
archive
|
||||
.vscode/settings.json
|
||||
src/style - official.css
|
||||
/e2e/specs/.test-results/
|
||||
/e2e/playwright-report/
|
||||
/playwright/.cache/
|
||||
.DS_Store
|
||||
*.code-workspace
|
||||
.idea
|
||||
*.pem
|
||||
config.local.ts
|
||||
storageState.json
|
||||
junit.xml
|
||||
|
||||
src/style - official.css
|
||||
# meilisearch
|
||||
meilisearch
|
||||
data.ms/*
|
||||
auth.json
|
||||
|
||||
5
.husky/pre-commit
Executable file
5
.husky/pre-commit
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
[ -n "$CI" ] && exit 0
|
||||
npx lint-staged
|
||||
|
||||
19
.prettierrc.js
Normal file
19
.prettierrc.js
Normal file
@@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
useTabs: false,
|
||||
tabWidth: 2,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
// bracketSpacing: false,
|
||||
trailingComma: 'none',
|
||||
arrowParens: 'always',
|
||||
embeddedLanguageFormatting: 'auto',
|
||||
insertPragma: false,
|
||||
proseWrap: 'preserve',
|
||||
quoteProps: 'as-needed',
|
||||
requirePragma: false,
|
||||
rangeStart: 0,
|
||||
endOfLine: 'auto',
|
||||
jsxBracketSameLine: false,
|
||||
jsxSingleQuote: false,
|
||||
};
|
||||
132
CODE_OF_CONDUCT.md
Normal file
132
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement here on GitHub or
|
||||
on the official [Discord Server](https://discord.gg/uDyZ5Tzhct).
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
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)
|
||||
100
CONTRIBUTING.md
Normal file
100
CONTRIBUTING.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# 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.
|
||||
|
||||
## Contributing Guidelines
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
We strive to maintain a positive and inclusive environment within our project community. We expect all contributors to adhere to the following standards:
|
||||
|
||||
- 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 that do not align with these standards.
|
||||
|
||||
## To contribute to this project, please adhere to the following guidelines:
|
||||
|
||||
## 1. Git Workflow
|
||||
|
||||
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`).
|
||||
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.
|
||||
5. We will review your pull request, provide feedback as needed, and eventually merge the approved changes into the main branch.
|
||||
|
||||
## 2. Commit Message Format
|
||||
|
||||
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.
|
||||
|
||||
### Commit Message Header
|
||||
|
||||
The header is mandatory and must conform to the following format:
|
||||
|
||||
```
|
||||
<type>(<scope>): <short summary>
|
||||
```
|
||||
|
||||
- `<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.
|
||||
|
||||
- `<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.
|
||||
|
||||
### Commit Message Body
|
||||
|
||||
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
|
||||
|
||||
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 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:
|
||||
|
||||
- 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.
|
||||
|
||||
Ensure that your changes meet the following criteria:
|
||||
|
||||
- All tests pass.
|
||||
- The code is well-formatted and adheres to our coding standards.
|
||||
- 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`).
|
||||
|
||||
---
|
||||
|
||||
## [Go Back to ReadMe](README.md)
|
||||
49
Dockerfile
49
Dockerfile
@@ -1,35 +1,26 @@
|
||||
FROM node:19-alpine AS react-client
|
||||
WORKDIR /client
|
||||
# copy package.json into the container at /client
|
||||
COPY /client/package*.json /client/
|
||||
# install dependencies
|
||||
RUN npm ci
|
||||
# Copy the current directory contents into the container at /client
|
||||
COPY /client/ /client/
|
||||
# Build webpack artifacts
|
||||
RUN npm run build
|
||||
# Base node image
|
||||
FROM node:19-alpine AS node
|
||||
|
||||
FROM node:19-alpine AS node-api
|
||||
WORKDIR /api
|
||||
# copy package.json into the container at /api
|
||||
COPY /api/package*.json /api/
|
||||
# install dependencies
|
||||
# Install curl for health check
|
||||
RUN apk --no-cache add curl
|
||||
|
||||
COPY . /app
|
||||
# Install dependencies
|
||||
WORKDIR /app
|
||||
RUN npm ci
|
||||
# Copy the current directory contents into the container at /api
|
||||
COPY /api/ /api/
|
||||
# Copy the client side code
|
||||
COPY --from=react-client /client/public /client/public
|
||||
# Make port 3080 available to the world outside this container
|
||||
|
||||
# React client build
|
||||
ENV NODE_OPTIONS="--max-old-space-size=2048"
|
||||
RUN npm run frontend
|
||||
|
||||
# Node API setup
|
||||
EXPOSE 3080
|
||||
# Expose the server to 0.0.0.0
|
||||
ENV HOST=0.0.0.0
|
||||
# Run the app when the container launches
|
||||
CMD ["npm", "start"]
|
||||
CMD ["npm", "run", "backend"]
|
||||
|
||||
# Optional: for client with nginx routing
|
||||
FROM nginx:stable-alpine AS nginx-client
|
||||
WORKDIR /usr/share/nginx/html
|
||||
COPY --from=react-client /client/public /usr/share/nginx/html
|
||||
# Add your nginx.conf
|
||||
COPY /client/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
ENTRYPOINT ["nginx", "-g", "daemon off;"]
|
||||
# FROM nginx:stable-alpine AS nginx-client
|
||||
# WORKDIR /usr/share/nginx/html
|
||||
# COPY --from=node /app/client/dist /usr/share/nginx/html
|
||||
# COPY client/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
# ENTRYPOINT ["nginx", "-g", "daemon off;"]
|
||||
|
||||
10
LICENSE.md
10
LICENSE.md
@@ -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
|
||||
@@ -12,6 +14,8 @@ furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
##
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
@@ -19,3 +23,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## [Go Back to ReadMe](README.md)
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
### Local
|
||||
- **Install the prerequisites**
|
||||
- **Download chatgpt-clone**
|
||||
- Download the latest release here: https://github.com/danny-avila/chatgpt-clone/releases/
|
||||
- Or by clicking on the green code button in the top of the page and selecting "Download ZIP"
|
||||
- Or (Recommended if you have Git installed) pull the latest release from the main branch
|
||||
- If you downloaded a zip file, extract the content in "C:/chatgpt-clone/"
|
||||
-**IMPORTANT : If you install the files somewhere else modify the instructions accordingly**
|
||||
|
||||
- **To enable the Conversation search feature:**
|
||||
-IF YOU DON'T WANT THIS FEATURE YOU CAN SKIP THIS STEP
|
||||
- Download MeileSearch latest release from : https://github.com/meilisearch/meilisearch/releases
|
||||
- Copy it to "C:/chatgpt-clone/"
|
||||
- Rename the file to "meilisearch.exe"
|
||||
- Open it by double clicking on it
|
||||
- Copy the generated Master Key and save it somewhere (You will need it later)
|
||||
|
||||
- **Download and Install Node.js**
|
||||
- Navigate to https://nodejs.org/en/download and to download the latest Node.js version for your OS (The Node.js installer includes the NPM package manager.)
|
||||
|
||||
- **Create a MongoDB database**
|
||||
- Navigate to https://www.mongodb.com/ and Sign In or Create an account
|
||||
- Create a new project
|
||||
- Build a Database using the free plan and name the cluster (example: chatgpt-clone)
|
||||
- Use the "Username and Password" method for authentication
|
||||
- Add your current IP to the access list
|
||||
- Then in the Database Deployment tab click on Connect
|
||||
- In "Choose a connection method" select "Connect your application"
|
||||
- Driver = Node.js / Version = 4.1 or later
|
||||
- Copy the connection string and save it somewhere(you will need it later)
|
||||
|
||||
- **Get your OpenAI API key** here: https://platform.openai.com/account/api-keys and save it somewhere safe (you will need it later)
|
||||
|
||||
- **Get your Bing Access Token**
|
||||
- Using MS Edge, navigate to bing.com
|
||||
- Make sure you are logged in
|
||||
- Open the DevTools by pressing F12 on your keyboard
|
||||
- Click on the tab "Application" (On the left of the DevTools)
|
||||
- Expand the "Cookies" (Under "Storage")
|
||||
- You need to copy the value of the "_U" cookie, save it somewhere, you will need it later
|
||||
|
||||
- **Create the ".env" File** You will need all your credentials, (API keys, access tokens, and Mongo Connection String, MeileSearch Master Key)
|
||||
- Open "C:/chatgpt-clone/api/.env.example" in a text editor
|
||||
- At this line **MONGO_URI="mongodb://127.0.0.1:27017/chatgpt-clone"**
|
||||
Replace mongodb://127.0.0.1:27017/chatgpt-clone with the MondoDB connection string you saved earlier, **remove "&w=majority" at the end**
|
||||
- It should look something like this: "MONGO_URI="mongodb+srv://username:password@chatgpt-clone.lfbcwz3.mongodb.net/?retryWrites=true"
|
||||
- At this line **OPENAI_KEY=** you need to add your openai API key
|
||||
- Add your Bing token to this line **BING_TOKEN=** (needed for BingChat & Sydney)
|
||||
- If you want to enable Search, **SEARCH=TRUE** if you do not want to enable search **SEARCH=FALSE**
|
||||
- Add your previously saved MeiliSearch Master key to this line **MEILI_MASTER_KEY=** (the key is needed if search is enabled even on local install or you may encounter errors)
|
||||
- Save the file as **"C:/chatgpt-clone/api/.env"**
|
||||
|
||||
**DO THIS ONCE AFTER EVERY UPDATE**
|
||||
- **Run** `npm ci` in the "C:/chatgpt-clone/api" directory
|
||||
- **Run** `npm ci` in the "C:/chatgpt-clone/client" directory
|
||||
- **Run** `npm run build` in the "C:/chatgpt-clone/client"
|
||||
|
||||
**DO THIS EVERY TIME YOU WANT TO START CHATGPT-CLONE**
|
||||
- **Run** `"meilisearch --master-key put_your_meilesearch_Master_Key_here"` in the "C:/chatgpt-clone" directory (Only if SEARCH=TRUE)
|
||||
- **Run** `npm start` in the "C:/chatgpt-clone/api" directory
|
||||
|
||||
- **Visit** http://localhost:3080 (default port) & enjoy
|
||||
|
||||
|
||||
OPTIONAL BUT RECOMMENDED
|
||||
- **Make a batch file to automate the starting process**
|
||||
- Open a text editor
|
||||
- Paste the following code in a new document
|
||||
- Put your MeiliSearch master key instead of "your_master_key_goes_here"
|
||||
- Save the file as "C:/chatgpt-clone/chatgpt-clone.bat"
|
||||
- you can make a shortcut of this batch file and put it anywhere
|
||||
|
||||
```
|
||||
REM the meilisearch executable needs to be at the root of the chatgpt-clone directory
|
||||
|
||||
start "MeiliSearch" cmd /k "meilisearch --master-key your_master_key_goes_here
|
||||
|
||||
REM ↑↑↑ meilisearch is the name of the meilisearch executable, put your own master key there
|
||||
|
||||
start "ChatGPT-Clone" cmd /k "cd api && npm start"
|
||||
|
||||
REM this batch file goes at the root of the chatgpt-clone directory (C:/chatgpt-clone/)
|
||||
```
|
||||
|
||||
If you update the chatgpt-clone project files, mannually redo the `npm ci` and `npm run build` steps
|
||||
|
||||
To share within network or serve as a public server, set `HOST` to `0.0.0.0` in `.env` file.
|
||||
418
README.md
418
README.md
@@ -1,395 +1,135 @@
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/sDfH4MwDWJ">
|
||||
<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">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/fuegovic/LibreChat/assets/32828263/fe3b9dbc-976f-4eb3-a900-fa21e0e38be6">
|
||||
<img src="https://github.com/fuegovic/LibreChat/assets/32828263/fe3b9dbc-976f-4eb3-a900-fa21e0e38be6" height="172">
|
||||
</picture>
|
||||
<h1 align="center">ChatGPT Clone</h1>
|
||||
<h1 align="center">LibreChat</h1>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a aria-label="Join the community on Discord" href="https://discord.gg/sDfH4MwDWJ">
|
||||
<img alt="" src="https://img.shields.io/badge/Join%20the%20community-blueviolet.svg?style=for-the-badge&logo=DISCORD&labelColor=000000&logoWidth=20">
|
||||
<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">
|
||||
</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">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## All AI Conversations under One Roof. ##
|
||||
Assistant AIs are the future and OpenAI revolutionized this movement with ChatGPT. While numerous UIs exist, this app commemorates the original styling of ChatGPT, with the ability to integrate any current/future AI models, while integrating and improving upon original client features, such as conversation/message search and prompt templates (currently WIP). Through this clone, you can avoid ChatGPT Plus in favor of free or pay-per-call APIs. I will soon deploy a demo of this app. Feel free to contribute, clone, or fork. Currently dockerized.
|
||||
## All-In-One AI Conversations with LibreChat ##
|
||||
LibreChat brings together the future of assistant AIs with the revolutionary technology of OpenAI's ChatGPT. Celebrating the original styling, LibreChat gives you the ability to integrate multiple AI models. It also integrates and enhances original client features such as conversation and message search, prompt templates and plugins.
|
||||
|
||||
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.
|
||||
|
||||
<div align="center">
|
||||
<video src="https://user-images.githubusercontent.com/110412045/223754183-8b7f45ce-6517-4bd5-9b39-c624745bf399.mp4" width=400/>
|
||||
</div>
|
||||
<!--  -->
|
||||
https://github.com/danny-avila/LibreChat/assets/110412045/c1eb0c0f-41f6-4335-b982-84b278b53d59
|
||||
|
||||
## Sponsors
|
||||
|
||||
Sponsored by <a href="https://github.com/DavidDev1334"><b>@DavidDev1334</b></a>
|
||||
|
||||
|
||||
## Updates
|
||||
<details open>
|
||||
<summary><strong>2023-03-23</strong></summary>
|
||||
|
||||
|
||||
|
||||
**Released [v0.1.0](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.1.0)**, **searching messages/conversations is live!** Up next is more custom parameters for customGpt's. Join the discord server for more immediate assistance and update: **[community discord server](https://discord.gg/NGaa9RPCft)**
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Previous Updates</strong></summary>
|
||||
|
||||
<details>
|
||||
<summary><strong>2023-03-22</strong></summary>
|
||||
|
||||
|
||||
|
||||
**Released [v0.0.6](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.0.6)**, the latest stable release before **Searching messages** goes live tomorrow. See exact updates to date in the tag link. By request, there is now also a **[community discord server](https://discord.gg/NGaa9RPCft)**
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-03-20</strong></summary>
|
||||
|
||||
|
||||
|
||||
**Searching messages** is almost here as I test more of its functionality. There've been a lot of great features requested and great contributions and I will work on some soon, namely, further customizing the custom gpt params with sliders similar to the OpenAI playground, and including the custom params and system messages available to Bing.
|
||||
|
||||
The above features are next and then I will have to focus on building the **test environment.** I would **greatly appreciate** help in this area with any test environment you're familiar with (mocha, chai, jest, playwright, puppeteer). This is to aid in the velocity of contributing and to save time I spend debugging.
|
||||
|
||||
On that note, I had to switch the default branch due to some breaking changes that haven't been straight forward to debug, mainly related to node-chat-gpt the main dependency of the project. Thankfully, my working branch, now switched to default as main, is working as expected.
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-03-16</strong></summary>
|
||||
|
||||
|
||||
|
||||
[Latest release (v0.0.4)](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.0.4) includes Resubmitting messages & Branching messages, which mirrors official ChatGPT feature of editing a sent message, that then branches the conversation into separate message paths (works only with ChatGPT)
|
||||
|
||||
Full details and [example here](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.0.4). Message search is on the docket
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-03-12</strong></summary>
|
||||
|
||||
|
||||
|
||||
|
||||
Really thankful for all the issues reported and contributions made, the project's features and improvements have accelerated as result. Honorable mention is [wtlyu](https://github.com/wtlyu) for contributing a lot of mindful code, namely hostname configuration and mobile styling. I will upload images on next release for faster docker setup, and starting updating them simultaneously with this repo.
|
||||
|
||||
|
||||
|
||||
Many improvements across the board, the biggest is being able to start conversations simultaneously (again thanks to [wtlyu](https://github.com/wtlyu) for bringing it to my attention), as you can switch conversations or start a new chat without any response streaming from a prior one, as the backend will still process/save client responses. Just watch out for any rate limiting from OpenAI/Microsoft if this is done excessively.
|
||||
|
||||
|
||||
Adding support for conversation search is next! Thank you [mysticaltech](https://github.com/mysticaltech) for bringing up a method I can use for this.
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-03-09</strong></summary>
|
||||
Released v.0.0.2
|
||||
|
||||
Adds Sydney (jailbroken Bing AI) to the model menu. Thank you [DavesDevFails](https://github.com/DavesDevFails) for bringing it to my attention in this [issue](https://github.com/danny-avila/chatgpt-clone/issues/13). Bing/Sydney now correctly cite links, more styling to come. Fix some overlooked bugs, and model menu doesn't close upon deleting a customGpt.
|
||||
|
||||
|
||||
I've re-enabled the ChatGPT browser client (free version) since it might be working for most people, it no longer works for me. Sydney is the best free route anyway.
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-03-07</strong></summary>
|
||||
Due to increased interest in the repo, I've dockerized the app as of this update for quick setup! See setup instructions below. I realize this still takes some time with installing docker dependencies, so it's on the roadmap to have a deployed demo. Besides this, I've made major improvements for a lot of the existing features across the board, mainly UI/UX.
|
||||
|
||||
|
||||
Also worth noting, the method to access the Free Version is no longer working, so I've removed it from model selection until further notice.
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-03-04</strong></summary>
|
||||
Custom prompt prefixing and labeling is now supported through the official API. This nets some interesting results when you need ChatGPT for specific uses or entertainment. Select 'CustomGPT' in the model menu to configure this, and you can choose to save the configuration or reference it by conversation. Model selection will change by conversation.
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-03-01</strong></summary>
|
||||
Official ChatGPT API is out! Removed davinci since the official API is extremely fast and 10x less expensive. Since user labeling and prompt prefixing is officially supported, I will add a View feature so you can set this within chat, which gives the UI an added use case. I've kept the BrowserClient, since it's free to use like the official site.
|
||||
|
||||
The Messages UI correctly mirrors code syntax highlighting. The exact replication of the cursor is not 1-to-1 yet, but pretty close. Later on in the project, I'll implement tests for code edge cases and explore the possibility of running code in-browser. Right now, unknown code defaults to javascript, but will detect language as close as possible.
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-02-21</strong></summary>
|
||||
BingAI is integrated (although sadly limited by Microsoft with the 5 msg/convo limit, 50 msgs/day). I will need to handle the case when Bing refuses to give more answers on top of the other styling features I have in mind. Official ChatGPT use is back with the new BrowserClient. Brainstorming how to handle the UI when the Ai model changes, since conversations can't be persisted between them (or perhaps build a way to achieve this at some level).
|
||||
</details>
|
||||
<details >
|
||||
<summary><strong>2023-02-15</strong></summary>
|
||||
Just got access to Bing AI so I'll be focusing on integrating that through waylaidwanderer's 'experimental' BingAIClient.
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-02-14</strong></summary>
|
||||
|
||||
Official ChatGPT use is no longer possible though I recently used it with waylaidwanderer's [reverse proxy method](https://github.com/waylaidwanderer/node-chatgpt-api/blob/main/README.md#using-a-reverse-proxy), and before that, through leaked models he also discovered.
|
||||
|
||||
Currently, this project is only functional with the `text-davinci-003` model.
|
||||
</details>
|
||||
</details>
|
||||
|
||||
# Table of Contents
|
||||
- [ChatGPT Clone](#chatgpt-clone)
|
||||
- [All AI Conversations under One Roof.](#all-ai-conversations-under-one-roof)
|
||||
- [Updates](#updates)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Features](#features)
|
||||
- [Tech Stack](#tech-stack)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Usage](#usage)
|
||||
- [Local](#local)
|
||||
- [Docker](#docker)
|
||||
- [Access Tokens](#access-tokens)
|
||||
- [Proxy](#proxy)
|
||||
- [User System](#user-system)
|
||||
- [Updating](#updating)
|
||||
- [Use Cases](#use-cases)
|
||||
- [Origin](#origin)
|
||||
- [Caveats](#caveats)
|
||||
- [Regarding use of Official ChatGPT API](#regarding-use-of-official-chatgpt-api)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
|
||||
## Roadmap
|
||||
|
||||
> **Warning**
|
||||
|
||||
> This is a work in progress. I'm building this in public. FYI there is still a lot of tech debt to cleanup. You can follow the progress here or on my [Linkedin](https://www.linkedin.com/in/danny-avila).
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><strong>Here are my recently completed and planned features:</strong></summary>
|
||||
|
||||
- [x] Persistent conversation
|
||||
- [x] Rename, delete conversations
|
||||
- [x] UI Error handling
|
||||
- [x] Bing AI integration
|
||||
- [x] AI model change handling (start new convos within existing, remembers last selected)
|
||||
- [x] Code block handling (highlighting, markdown, clipboard, language detection)
|
||||
- [x] Markdown handling
|
||||
- [x] Customize prompt prefix/label (custom ChatGPT using official API)
|
||||
- [x] Server convo pagination (limit fetch and load more with 'show more' button)
|
||||
- [x] Config file for easy startup (docker compose)
|
||||
- [x] Mobile styling (thanks to [wtlyu](https://github.com/wtlyu))
|
||||
- [x] Resubmit/edit sent messages (thanks to [wtlyu](https://github.com/wtlyu))
|
||||
- [ ] Message Search
|
||||
- [ ] Custom params for ChatGPT API (temp, top_p, presence_penalty)
|
||||
- [ ] Bing AI Styling (params, suggested responses, convo end, etc.) - **In progress**
|
||||
- [ ] Add warning before clearing convos
|
||||
- [ ] Build test suite for CI/CD
|
||||
- [ ] Prompt Templates/Search
|
||||
- [ ] Refactor/clean up code (tech debt)
|
||||
- [ ] Optional use of local storage for credentials
|
||||
- [ ] Deploy demo
|
||||
|
||||
</details>
|
||||
|
||||
### Features
|
||||
# Features
|
||||
|
||||
- Response streaming identical to ChatGPT through server-sent events
|
||||
- UI from original ChatGPT, including Dark mode
|
||||
- AI model selection (official ChatGPT API, BingAI, ChatGPT Free)
|
||||
- Create and Save custom ChatGPTs*
|
||||
- Edit and Resubmit messages just like the official site (with conversation branching)
|
||||
- Search all messages/conversations - [see details here](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.1.0)
|
||||
- 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/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/LibreChat/releases/tag/v0.1.0)
|
||||
- Plugins now available (including web access, image generation and more)
|
||||
|
||||
^* ChatGPT can be 'customized' by setting a system message or prompt prefix and alternate 'role' to the API request^
|
||||
---
|
||||
|
||||
[More info here](https://platform.openai.com/docs/guides/chat/instructing-chat-models). Here's an [example from this app.]()
|
||||
## ⚠️ [Breaking Changes as of v0.5.0](docs/general_info/breaking_changes.md#v050) ⚠️
|
||||
**Please read this before updating from a previous version**
|
||||
|
||||
### Tech Stack
|
||||
---
|
||||
|
||||
## Changelog
|
||||
Keep up with the latest updates by visiting the releases page - [Releases](https://github.com/danny-avila/LibreChat/releases)
|
||||
|
||||
<details>
|
||||
<summary><strong>This project uses:</strong></summary>
|
||||
---
|
||||
|
||||
<h1>Table of Contents</h1>
|
||||
|
||||
<details open>
|
||||
<summary><strong>Getting Started</strong></summary>
|
||||
|
||||
- [node-chatgpt-api](https://github.com/waylaidwanderer/node-chatgpt-api)
|
||||
- No React boilerplate/toolchain/clone tutorials, created from scratch with react@latest
|
||||
- Use of Tailwind CSS and [shadcn/ui](https://github.com/shadcn/ui) components
|
||||
- Docker, useSWR, Redux, Express, MongoDB, [Keyv](https://www.npmjs.com/package/keyv)
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
- npm
|
||||
- Node.js >= 19.0.0
|
||||
- MongoDB installed or [MongoDB Atlas](https://account.mongodb.com/account/login) (required if not using Docker)
|
||||
- MongoDB does not support older ARM CPUs like those found in Raspberry Pis. However, you can make it work by setting MongoDB's version to mongo:4.4.18 in docker-compose.yml, the most recent version compatible with
|
||||
- [Docker (optional)](https://www.docker.com/get-started/)
|
||||
- [OpenAI API key](https://platform.openai.com/account/api-keys)
|
||||
- BingAI, ChatGPT access tokens (optional, free AIs)
|
||||
|
||||
## Usage
|
||||
|
||||
- **Clone/download** the repo down where desired
|
||||
```bash
|
||||
git clone https://github.com/danny-avila/chatgpt-clone.git
|
||||
```
|
||||
- If using MongoDB Atlas, remove `&w=majority` from default connection string.
|
||||
|
||||
### Local
|
||||
### **[In-depth instructions here!](https://github.com/danny-avila/chatgpt-clone/blob/0d4f0f74c04337aaf51b9a3eef898165a7009156/LOCAL_INSTALL.md)**
|
||||
- thank you [@fuegovic](https://github.com/fuegovic)!
|
||||
|
||||
### Docker
|
||||
|
||||
- **Provide** all credentials, (API keys, access tokens, and Mongo Connection String) in [docker-compose.yml](docker-compose.yml) under api service
|
||||
- **Run** `docker-compose up` to start the app
|
||||
- Note: MongoDB does not support older ARM CPUs like those found in Raspberry Pis. However, you can make it work by setting MongoDB's version to mongo:4.4.18 in docker-compose.yml, the most recent version compatible with
|
||||
|
||||
### Access Tokens
|
||||
|
||||
<details>
|
||||
<summary><strong>ChatGPT Free Instructions</strong></summary>
|
||||
|
||||
To get your Access token For ChatGPT 'Free Version', login to chat.openai.com, then visit https://chat.openai.com/api/auth/session.
|
||||
|
||||
|
||||
**Warning:** There may be a high chance of your account being banned with this method. Continue doing so at your own risk.
|
||||
|
||||
* [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)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>BingAI Instructions</strong></summary>
|
||||
The Bing Access Token is the "_U" cookie from bing.com. Use dev tools or an extension while logged into the site to view it.
|
||||
<summary><strong>General Information</strong></summary>
|
||||
|
||||
**Note:** Specific error handling and styling for this model is still in progress.
|
||||
</details>
|
||||
|
||||
### Proxy
|
||||
|
||||
If your server cannot connect to the chatGPT API server by some reason, (eg in China). You can set a environment variable `PROXY`. This will be transmitted to `node-chatgpt-api` interface.
|
||||
|
||||
**Warning:** `PROXY` is not `reverseProxyUrl` in `node-chatgpt-api`
|
||||
|
||||
<details>
|
||||
<summary><strong>Set up proxy in local environment </strong></summary>
|
||||
|
||||
Here is two ways to set proxy.
|
||||
- Option 1: system level environment
|
||||
`export PROXY="http://127.0.0.1:7890"`
|
||||
- Option 2: set in .env file
|
||||
`PROXY="http://127.0.0.1:7890"`
|
||||
|
||||
**Change `http://127.0.0.1:7890` to your proxy server**
|
||||
* [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)
|
||||
* [Bing Jailbreak Info](docs/general_info/bing_jailbreak_info.md)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Set up proxy in docker environment </strong></summary>
|
||||
<summary><strong>Features</strong></summary>
|
||||
|
||||
set in docker-compose.yml file, under services - api - environment
|
||||
|
||||
```
|
||||
api:
|
||||
...
|
||||
environment:
|
||||
...
|
||||
- "PROXY=http://127.0.0.1:7890"
|
||||
# add this line ↑
|
||||
```
|
||||
|
||||
**Change `http://127.0.0.1:7890` to your proxy server**
|
||||
* **Plugins**
|
||||
* [Introduction](docs/features/plugins/introduction.md)
|
||||
* [Google](docs/features/plugins/google_search.md)
|
||||
* [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)
|
||||
|
||||
* [User Auth System](docs/features/user_auth_system.md)
|
||||
* [Proxy](docs/features/proxy.md)
|
||||
</details>
|
||||
|
||||
### User System
|
||||
<details>
|
||||
<summary><strong>Cloud Deployment</strong></summary>
|
||||
|
||||
By default, there is no user system enabled, so anyone can access your server.
|
||||
|
||||
**This project is not designed to provide a complete and full-featured user system.** It's not high priority task and might never be provided.
|
||||
|
||||
[wtlyu](https://github.com/wtlyu) provide a sample user system structure, that you can implement your own user system. It's simple and not a ready-for-use edition.
|
||||
|
||||
(If you want to implement your user system, open this ↓)
|
||||
* [Hetzner](docs/deployment/hetzner_ubuntu.md)
|
||||
* [Heroku](docs/deployment/heroku.md)
|
||||
* [Linode](docs/deployment/linode.md)
|
||||
* [Cloudflare](docs/deployment/cloudflare.md)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Implement your own user system </strong></summary>
|
||||
|
||||
To enable the user system, set `ENABLE_USER_SYSTEM=1` in your `.env` file.
|
||||
|
||||
The sample structure is simple. It provide three basic endpoint:
|
||||
|
||||
1. `/auth/login` will redirect to your own login url. In the sample code, it's `/auth/your_login_page`.
|
||||
2. `/auth/logout` will redirect to your own logout url. In the sample code, it's `/auth/your_login_page/logout`.
|
||||
3. `/api/me` will return the userinfo: `{ username, display }`.
|
||||
1. `username` will be used in db, used to distinguish between users.
|
||||
2. `display` will be displayed in UI.
|
||||
|
||||
The only one thing that drive user system work is `req.session.user`. Once it's set, the client will be trusted. Set to `null` if logout.
|
||||
|
||||
Please refer to `/api/server/routes/authYourLogin.js` file. It's very clear and simple to tell you how to implement your user system.
|
||||
|
||||
Or you can ask chatGPT to write the code for you, here is one example to connect LDAP:
|
||||
|
||||
```
|
||||
Please write me an express module, that serve the login and logout endpoint as a router. The login and logout uri is '/' and '/logout'. Once loginned, save display name and username in session.user, as {display, username}. Then redirect to '/'. Please write the code using express and other lib, and storage any server configuration in a config variable. I want the user to be connected to my LDAP server.
|
||||
```
|
||||
<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/LibreChate)
|
||||
</details>
|
||||
|
||||
|
||||
### Updating
|
||||
---
|
||||
|
||||
- As the project is still a work-in-progress, you should pull the latest and run the steps over. Reset your browser cache/clear site data.
|
||||
## Star History
|
||||
|
||||
## Use Cases ##
|
||||
[](https://star-history.com/#danny-avila/LibreChat&Date)
|
||||
|
||||
---
|
||||
|
||||
## Sponsors
|
||||
|
||||
<details>
|
||||
<summary><strong> Why use this project? </strong></summary>
|
||||
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> & <a href="https://github.com/SphaeroX"><b>@SphaeroX</b></a>
|
||||
|
||||
- One stop shop for all conversational AIs, with the added bonus of searching past conversations.
|
||||
- Using the official API, you'd have to generate 7.5 million words to expense the same cost as ChatGPT Plus ($20).
|
||||
- ChatGPT/Google Bard/Bing AI conversations are lost in space or
|
||||
cannot be searched past a certain timeframe.
|
||||
- **Customize ChatGPT**
|
||||
---
|
||||
|
||||

|
||||
## Contributors
|
||||
Contributions and suggestions bug reports and fixes are welcome!
|
||||
Please read the documentation before you do!
|
||||
|
||||
- **API is not as limited as ChatGPT Free (at [chat.openai.com](https://chat.openai.com/chat))**
|
||||
|
||||

|
||||
|
||||
- **ChatGPT Free is down.**
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## Origin ##
|
||||
This project was started early in Feb '23, anticipating the release of the official ChatGPT API from OpenAI, which is now used. It was originally created as a Minimum Viable Product (or MVP) for the [@HackReactor](https://github.com/hackreactor/) Bootcamp. It was built with OpenAI response streaming and most of the UI completed in under 20 hours. During the end of that time, I had most of the UI and basic functionality done. This was created without using any boilerplates or templates, including create-react-app and other toolchains. I didn't follow any 'un-official chatgpt' video tutorials, and simply referenced the official site for the UI. The purpose of the exercise was to learn setting up a full stack project from scratch. Please feel free to give feedback, suggestions, or fork the project for your own use.
|
||||
|
||||
|
||||
## Caveats
|
||||
### Regarding use of Official ChatGPT API
|
||||
From [@waylaidwanderer](https://github.com/waylaidwanderer/node-chatgpt-api/blob/main/README.md#caveats):
|
||||
|
||||
Since `gpt-3.5-turbo` is ChatGPT's underlying model, I had to do my best to replicate the way the official ChatGPT website uses it.
|
||||
This means my implementation or the underlying model may not behave exactly the same in some ways:
|
||||
- Conversations are not tied to any user IDs, so if that's important to you, you should implement your own user ID system.
|
||||
- ChatGPT's model parameters (temperature, frequency penalty, etc.) are unknown, so I set some defaults that I thought would be reasonable.
|
||||
- Conversations are limited to roughly the last 3000 tokens, so earlier messages may be forgotten during longer conversations.
|
||||
- This works in a similar way to ChatGPT, except I'm pretty sure they have some additional way of retrieving context from earlier messages when needed (which can probably be achieved with embeddings, but I consider that out-of-scope for now).
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions and suggestions welcome! Bug reports and fixes are welcome!
|
||||
---
|
||||
|
||||
For new features, components, or extensions, please open an issue and discuss before sending a PR.
|
||||
|
||||
- Join the [Discord community](https://discord.gg/NGaa9RPCft)
|
||||
- Join the [Discord community](https://discord.gg/uDyZ5Tzhct)
|
||||
|
||||
## License
|
||||
This project is licensed under the MIT License.
|
||||
This project exists in its current state thanks to all the people who contribute
|
||||
---
|
||||
<a href="https://github.com/danny-avila/LibreChat/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=danny-avila/LibreChat" />
|
||||
</a>
|
||||
|
||||
63
SECURITY.md
Normal file
63
SECURITY.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Security Policy
|
||||
|
||||
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 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
|
||||
|
||||
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 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, 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/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/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 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)
|
||||
@@ -1,70 +0,0 @@
|
||||
# Server configuration.
|
||||
# The server will listen to localhost:3080 request by default. You can set the target ip as you want.
|
||||
# If you want this server can be used outside your local machine, for example to share with other
|
||||
# machine or expose this from a docker container, set HOST=0.0.0.0 or your external ip interface.
|
||||
#
|
||||
# Tips: HOST=0.0.0.0 means listening on all interface. It's not a real ip. Use localhost:port rather
|
||||
# than 0.0.0.0:port to open it.
|
||||
HOST=localhost
|
||||
PORT=3080
|
||||
NODE_ENV=development
|
||||
|
||||
# Change this to proxy any API request. It's useful if your machine have difficulty calling the original API server.
|
||||
# PROXY="http://YOUR_PROXY_SERVER"
|
||||
|
||||
# Change this to your MongoDB URI if different and I recommend appending chatgpt-clone
|
||||
MONGO_URI="mongodb://127.0.0.1:27017/chatgpt-clone"
|
||||
|
||||
# API key configuration.
|
||||
# Leave blank if you don't want them.
|
||||
OPENAI_KEY=
|
||||
|
||||
# Default ChatGPT API Model, options: 'gpt-4', 'text-davinci-003', 'gpt-3.5-turbo', 'gpt-3.5-turbo-0301'
|
||||
# you will have errors if you don't have access to a model like 'gpt-4', defaults to turbo if left empty/excluded.
|
||||
DEFAULT_API_GPT=gpt-3.5-turbo
|
||||
|
||||
# _U Cookies Value from bing.com
|
||||
BING_TOKEN=
|
||||
|
||||
# ChatGPT Browser Client (free but use at your own risk)
|
||||
# Access token from https://chat.openai.com/api/auth/session
|
||||
# Exposes your access token to a 3rd party
|
||||
CHATGPT_TOKEN=
|
||||
# If you have access to other models on the official site, you can use them here.
|
||||
# Defaults to 'text-davinci-002-render-sha' if left empty.
|
||||
# options: gpt-4, text-davinci-002-render, text-davinci-002-render-paid, or text-davinci-002-render-sha
|
||||
# You cannot use a model that your account does not have access to. You can check
|
||||
# which ones you have access to by opening DevTools and going to the Network tab.
|
||||
# Refresh the page and look at the response body for https://chat.openai.com/backend-api/models.
|
||||
BROWSER_MODEL=
|
||||
|
||||
# ENABLING SEARCH MESSAGES/CONVOS
|
||||
# Requires installation of free self-hosted Meilisearch or Paid Remote Plan (Remote not tested)
|
||||
# The easiest setup for this is through docker-compose, which takes care of it for you.
|
||||
# SEARCH=TRUE
|
||||
SEARCH=TRUE
|
||||
|
||||
# REQUIRED FOR SEARCH: MeiliSearch Host, mainly for api server to connect to the search server.
|
||||
# must replace '0.0.0.0' with 'meilisearch' if serving meilisearch with docker-compose
|
||||
# MEILI_HOST='http://meilisearch:7700' # <-- docker-compose (should already be setup on docker-compose.yml)
|
||||
MEILI_HOST='http://0.0.0.0:7700' # <-- local/remote
|
||||
|
||||
# REQUIRED FOR SEARCH: MeiliSearch HTTP Address, mainly for docker-compose to expose the search server.
|
||||
# must replace '0.0.0.0' with 'meilisearch' if serving meilisearch with docker-compose
|
||||
# MEILI_HTTP_ADDR='meilisearch:7700' # <-- docker-compose (should already be setup on docker-compose.yml)
|
||||
MEILI_HTTP_ADDR='0.0.0.0:7700' # <-- local/remote
|
||||
|
||||
# REQUIRED FOR SEARCH: In production env., needs a secure key, feel free to generate your own.
|
||||
# This master key must be at least 16 bytes, composed of valid UTF-8 characters.
|
||||
# Meilisearch will throw an error and refuse to launch if no master key is provided or if it is under 16 bytes,
|
||||
# Meilisearch will suggest a secure autogenerated master key.
|
||||
# Using docker, it seems recognized as production so use a secure key.
|
||||
# MEILI_MASTER_KEY= # <-- empty/insecure key works for local/remote
|
||||
MEILI_MASTER_KEY=JKMW-hGc7v_D1FkJVdbRSDNFLZcUv3S75yrxXP0SmcU # <-- ready made secure key for docker-compose
|
||||
|
||||
|
||||
# User System
|
||||
# global enable/disable the sample user system.
|
||||
# this is not a ready to use user system.
|
||||
# dont't use it, unless you can write your own code.
|
||||
# ENABLE_USER_SYSTEM= # <-- make sure you don't comment this back in if you're not using your own user system
|
||||
@@ -1,39 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
es2021: true,
|
||||
node: true
|
||||
},
|
||||
extends: ['eslint:recommended'],
|
||||
overrides: [],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
},
|
||||
rules: {
|
||||
indent: ['error', 2, { SwitchCase: 1 }],
|
||||
'max-len': [
|
||||
'error',
|
||||
{
|
||||
code: 150,
|
||||
ignoreStrings: true,
|
||||
ignoreTemplateLiterals: true,
|
||||
ignoreComments: true
|
||||
}
|
||||
],
|
||||
'linebreak-style': 0,
|
||||
'arrow-parens': [2, 'as-needed', { requireForBlockBody: true }],
|
||||
// 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
|
||||
'no-console': 'off',
|
||||
'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'
|
||||
}
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "lf",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"singleAttributePerLine": true,
|
||||
"bracketSameLine": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"printWidth": 110,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false,
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"parser": "babel"
|
||||
}
|
||||
@@ -1,30 +1,70 @@
|
||||
require('dotenv').config();
|
||||
const { KeyvFile } = require('keyv-file');
|
||||
|
||||
const askBing = async ({ text, onProgress, convo }) => {
|
||||
const askBing = async ({
|
||||
text,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
jailbreak,
|
||||
jailbreakConversationId,
|
||||
context,
|
||||
systemMessage,
|
||||
conversationSignature,
|
||||
clientId,
|
||||
invocationId,
|
||||
toneStyle,
|
||||
token,
|
||||
onProgress
|
||||
}) => {
|
||||
const { BingAIClient } = await import('@waylaidwanderer/chatgpt-api');
|
||||
const store = {
|
||||
store: new KeyvFile({ filename: './data/cache.json' })
|
||||
};
|
||||
|
||||
const bingAIClient = new BingAIClient({
|
||||
// "_U" cookie from bing.com
|
||||
userToken: process.env.BING_TOKEN,
|
||||
// userToken:
|
||||
// process.env.BINGAI_TOKEN == 'user_provided' ? token : process.env.BINGAI_TOKEN ?? null,
|
||||
// If the above doesn't work, provide all your cookies as a string instead
|
||||
// cookies: '',
|
||||
cookies: process.env.BINGAI_TOKEN == 'user_provided' ? token : process.env.BINGAI_TOKEN ?? null,
|
||||
debug: false,
|
||||
cache: { store: new KeyvFile({ filename: './data/cache.json' }) },
|
||||
cache: store,
|
||||
host: process.env.BINGAI_HOST || null,
|
||||
proxy: process.env.PROXY || null
|
||||
});
|
||||
|
||||
let options = { onProgress };
|
||||
if (convo) {
|
||||
options = { ...options, ...convo };
|
||||
let options = {};
|
||||
|
||||
if (jailbreakConversationId == 'false') {
|
||||
jailbreakConversationId = false;
|
||||
}
|
||||
|
||||
if (options?.jailbreakConversationId == 'false') {
|
||||
options.jailbreakConversationId = false;
|
||||
}
|
||||
if (jailbreak)
|
||||
options = {
|
||||
jailbreakConversationId: jailbreakConversationId || jailbreak,
|
||||
context,
|
||||
systemMessage,
|
||||
parentMessageId,
|
||||
toneStyle,
|
||||
onProgress
|
||||
};
|
||||
else {
|
||||
options = {
|
||||
conversationId,
|
||||
context,
|
||||
systemMessage,
|
||||
parentMessageId,
|
||||
toneStyle,
|
||||
onProgress
|
||||
};
|
||||
|
||||
if (convo.toneStyle) {
|
||||
options.toneStyle = convo.toneStyle;
|
||||
// don't give those parameters for new conversation
|
||||
// for new conversation, conversationSignature always is null
|
||||
if (conversationSignature) {
|
||||
options.conversationSignature = conversationSignature;
|
||||
options.clientId = clientId;
|
||||
options.invocationId = invocationId;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('bing options', options);
|
||||
@@ -33,30 +73,8 @@ const askBing = async ({ text, onProgress, convo }) => {
|
||||
|
||||
return res;
|
||||
|
||||
// Example response for reference
|
||||
// {
|
||||
// conversationSignature: 'wwZ2GC/qRgEqP3VSNIhbPGwtno5RcuBhzZFASOM+Sxg=',
|
||||
// conversationId: '51D|BingProd|026D3A4017554DE6C446798144B6337F4D47D5B76E62A31F31D0B1D0A95ED868',
|
||||
// clientId: '914800201536527',
|
||||
// invocationId: 1,
|
||||
// conversationExpiryTime: '2023-02-15T21:48:46.2892088Z',
|
||||
// response: 'Hello, this is Bing. Nice to meet you. 😊',
|
||||
// details: {
|
||||
// text: 'Hello, this is Bing. Nice to meet you. 😊',
|
||||
// author: 'bot',
|
||||
// createdAt: '2023-02-15T15:48:43.0631898+00:00',
|
||||
// timestamp: '2023-02-15T15:48:43.0631898+00:00',
|
||||
// messageId: '9d0c9a80-91b1-49ab-b9b1-b457dc3fe247',
|
||||
// requestId: '5b252ef8-4f09-4c08-b6f5-4499d2e12fba',
|
||||
// offense: 'None',
|
||||
// adaptiveCards: [ [Object] ],
|
||||
// sourceAttributions: [],
|
||||
// feedback: { tag: null, updatedOn: null, type: 'None' },
|
||||
// contentOrigin: 'DeepLeo',
|
||||
// privacy: null,
|
||||
// suggestedResponses: [ [Object], [Object], [Object] ]
|
||||
// }
|
||||
// }
|
||||
// for reference:
|
||||
// https://github.com/waylaidwanderer/node-chatgpt-api/blob/main/demos/use-bing-client.js
|
||||
};
|
||||
|
||||
module.exports = { askBing };
|
||||
|
||||
@@ -1,40 +1,45 @@
|
||||
require('dotenv').config();
|
||||
const { KeyvFile } = require('keyv-file');
|
||||
const set = new Set(["gpt-4", "text-davinci-002-render", "text-davinci-002-render-paid", "text-davinci-002-render-sha"]);
|
||||
|
||||
const clientOptions = {
|
||||
// Warning: This will expose your access token to a third party. Consider the risks before using this.
|
||||
reverseProxyUrl: 'https://bypass.duti.tech/api/conversation',
|
||||
// Access token from https://chat.openai.com/api/auth/session
|
||||
accessToken: process.env.CHATGPT_TOKEN,
|
||||
// debug: true
|
||||
proxy: process.env.PROXY || null,
|
||||
};
|
||||
|
||||
// You can check which models you have access to by opening DevTools and going to the Network tab.
|
||||
// Refresh the page and look at the response body for https://chat.openai.com/backend-api/models.
|
||||
if (set.has(process.env.BROWSER_MODEL)) {
|
||||
clientOptions.model = process.env.BROWSER_MODEL;
|
||||
}
|
||||
|
||||
const browserClient = async ({ text, onProgress, convo, abortController }) => {
|
||||
const browserClient = async ({
|
||||
text,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
model,
|
||||
token,
|
||||
onProgress,
|
||||
onEventMessage,
|
||||
abortController,
|
||||
userId
|
||||
}) => {
|
||||
const { ChatGPTBrowserClient } = await import('@waylaidwanderer/chatgpt-api');
|
||||
|
||||
const store = {
|
||||
store: new KeyvFile({ filename: './data/cache.json' })
|
||||
};
|
||||
|
||||
const client = new ChatGPTBrowserClient(clientOptions, store);
|
||||
let options = { onProgress, abortController };
|
||||
const clientOptions = {
|
||||
// Warning: This will expose your access token to a third party. Consider the risks before using this.
|
||||
reverseProxyUrl:
|
||||
process.env.CHATGPT_REVERSE_PROXY || 'https://ai.fakeopen.com/api/conversation',
|
||||
// Access token from https://chat.openai.com/api/auth/session
|
||||
accessToken:
|
||||
process.env.CHATGPT_TOKEN == 'user_provided' ? token : process.env.CHATGPT_TOKEN ?? null,
|
||||
model: model,
|
||||
debug: false,
|
||||
proxy: process.env.PROXY || null,
|
||||
user: userId
|
||||
};
|
||||
|
||||
if (!!convo.parentMessageId && !!convo.conversationId) {
|
||||
options = { ...options, ...convo };
|
||||
const client = new ChatGPTBrowserClient(clientOptions, store);
|
||||
let options = { onProgress, onEventMessage, abortController };
|
||||
|
||||
if (!!parentMessageId && !!conversationId) {
|
||||
options = { ...options, parentMessageId, conversationId };
|
||||
}
|
||||
|
||||
console.log('gptBrowser options', options, clientOptions);
|
||||
console.log('gptBrowser clientOptions', clientOptions);
|
||||
|
||||
/* will error if given a convoId at the start */
|
||||
if (convo.parentMessageId.startsWith('0000')) {
|
||||
if (parentMessageId === '00000000-0000-0000-0000-000000000000') {
|
||||
delete options.conversationId;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +1,105 @@
|
||||
require('dotenv').config();
|
||||
const { KeyvFile } = require('keyv-file');
|
||||
const set = new Set(['gpt-4', 'text-davinci-003', 'gpt-3.5-turbo', 'gpt-3.5-turbo-0301']);
|
||||
const { genAzureChatCompletion } = require('../../utils/genAzureEndpoints');
|
||||
const tiktoken = require('@dqbd/tiktoken');
|
||||
const tiktokenModels = require('../../utils/tiktokenModels');
|
||||
const encoding_for_model = tiktoken.encoding_for_model;
|
||||
|
||||
const clientOptions = {
|
||||
modelOptions: {
|
||||
model: 'gpt-3.5-turbo'
|
||||
},
|
||||
proxy: process.env.PROXY || null,
|
||||
debug: false
|
||||
};
|
||||
|
||||
if (set.has(process.env.DEFAULT_API_GPT)) {
|
||||
clientOptions.modelOptions.model = process.env.DEFAULT_API_GPT;
|
||||
}
|
||||
|
||||
const askClient = async ({ text, onProgress, convo, abortController }) => {
|
||||
const ChatGPTClient = (await import('@waylaidwanderer/chatgpt-api')).default;
|
||||
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 client = new ChatGPTClient(process.env.OPENAI_KEY, clientOptions, store);
|
||||
let options = { onProgress, abortController };
|
||||
|
||||
if (!!convo.parentMessageId && !!convo.conversationId) {
|
||||
options = { ...options, ...convo };
|
||||
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 res = await client.sendMessage(text, options);
|
||||
const maxTokensMap = {
|
||||
'gpt-4': 8191,
|
||||
'gpt-4-0613': 8191,
|
||||
'gpt-4-32k': 32767,
|
||||
'gpt-4-32k-0613': 32767,
|
||||
'gpt-3.5-turbo': 4095,
|
||||
'gpt-3.5-turbo-0613': 4095,
|
||||
'gpt-3.5-turbo-0301': 4095,
|
||||
'gpt-3.5-turbo-16k': 15999,
|
||||
};
|
||||
|
||||
const maxContextTokens = maxTokensMap[model] ?? 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;
|
||||
};
|
||||
|
||||
|
||||
89
api/app/clients/chatgpt-client.tokens.js
Normal file
89
api/app/clients/chatgpt-client.tokens.js
Normal file
@@ -0,0 +1,89 @@
|
||||
require('dotenv').config();
|
||||
|
||||
const run = async () => {
|
||||
const { ChatGPTClient } = await import('@waylaidwanderer/chatgpt-api');
|
||||
const text = `
|
||||
The standard Lorem Ipsum passage, used since the 1500s
|
||||
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
|
||||
Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
|
||||
|
||||
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"
|
||||
1914 translation by H. Rackham
|
||||
|
||||
"But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?"
|
||||
Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
|
||||
|
||||
"At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat."
|
||||
1914 translation by H. Rackham
|
||||
|
||||
"On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness of will, which is the same as saying through shrinking from toil and pain. These cases are perfectly simple and easy to distinguish. In a free hour, when our power of choice is untrammelled and when nothing prevents our being able to do what we like best, every pleasure is to be welcomed and every pain avoided. But in certain circumstances and owing to the claims of duty or the obligations of business it will frequently occur that pleasures have to be repudiated and annoyances accepted. The wise man therefore always holds in these matters to this principle of selection: he rejects pleasures to secure other greater pleasures, or else he endures pains to avoid worse pains."
|
||||
`;
|
||||
const model = 'gpt-3.5-turbo';
|
||||
const maxContextTokens = model === 'gpt-4' ? 8191 : model === 'gpt-4-32k' ? 32767 : 4095; // 1 less than maximum
|
||||
const clientOptions = {
|
||||
reverseProxyUrl: process.env.OPENAI_REVERSE_PROXY || null,
|
||||
maxContextTokens,
|
||||
modelOptions: {
|
||||
model,
|
||||
},
|
||||
proxy: process.env.PROXY || null,
|
||||
debug: true
|
||||
};
|
||||
|
||||
let apiKey = process.env.OPENAI_API_KEY;
|
||||
|
||||
const maxMemory = 0.05 * 1024 * 1024 * 1024;
|
||||
|
||||
// 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) + '%';
|
||||
console.log(progressBar);
|
||||
}
|
||||
|
||||
const iterations = 16000;
|
||||
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);
|
||||
|
||||
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;
|
||||
printProgressBar(percentageUsed);
|
||||
|
||||
if (i === (iterations - 1)) {
|
||||
console.log(' done');
|
||||
// encoder.free();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`caught error! in Iteration ${i}`);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
console.timeEnd('loopTime');
|
||||
// Calculate final percentage of memory used
|
||||
const finalMemoryUsage = process.memoryUsage().heapUsed;
|
||||
// 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);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,35 +0,0 @@
|
||||
require('dotenv').config();
|
||||
const { KeyvFile } = require('keyv-file');
|
||||
|
||||
const clientOptions = {
|
||||
modelOptions: {
|
||||
model: 'gpt-3.5-turbo'
|
||||
},
|
||||
proxy: process.env.PROXY || null,
|
||||
debug: false
|
||||
};
|
||||
|
||||
const customClient = async ({ text, onProgress, convo, promptPrefix, chatGptLabel, abortController }) => {
|
||||
const ChatGPTClient = (await import('@waylaidwanderer/chatgpt-api')).default;
|
||||
const store = {
|
||||
store: new KeyvFile({ filename: './data/cache.json' })
|
||||
};
|
||||
|
||||
clientOptions.chatGptLabel = chatGptLabel;
|
||||
|
||||
if (promptPrefix?.length > 0) {
|
||||
clientOptions.promptPrefix = promptPrefix;
|
||||
}
|
||||
|
||||
const client = new ChatGPTClient(process.env.OPENAI_KEY, clientOptions, store);
|
||||
|
||||
let options = { onProgress, abortController };
|
||||
if (!!convo.parentMessageId && !!convo.conversationId) {
|
||||
options = { ...options, ...convo };
|
||||
}
|
||||
|
||||
const res = await client.sendMessage(text, options);
|
||||
return res;
|
||||
};
|
||||
|
||||
module.exports = customClient;
|
||||
@@ -1,40 +0,0 @@
|
||||
require('dotenv').config();
|
||||
const { KeyvFile } = require('keyv-file');
|
||||
|
||||
const askSydney = async ({ text, onProgress, convo }) => {
|
||||
const { BingAIClient } = (await import('@waylaidwanderer/chatgpt-api'));
|
||||
|
||||
const sydneyClient = new BingAIClient({
|
||||
// "_U" cookie from bing.com
|
||||
userToken: process.env.BING_TOKEN,
|
||||
// If the above doesn't work, provide all your cookies as a string instead
|
||||
// cookies: '',
|
||||
debug: false,
|
||||
cache: { store: new KeyvFile({ filename: './data/cache.json' }) }
|
||||
});
|
||||
|
||||
let options = {
|
||||
jailbreakConversationId: true,
|
||||
onProgress,
|
||||
};
|
||||
|
||||
if (convo.jailbreakConversationId) {
|
||||
options = { ...options, jailbreakConversationId: convo.jailbreakConversationId, parentMessageId: convo.parentMessageId };
|
||||
}
|
||||
|
||||
if (convo.toneStyle) {
|
||||
options.toneStyle = convo.toneStyle;
|
||||
}
|
||||
|
||||
console.log('sydney options', options);
|
||||
|
||||
const res = await sydneyClient.sendMessage(text, options
|
||||
);
|
||||
|
||||
return res;
|
||||
|
||||
// for reference:
|
||||
// https://github.com/waylaidwanderer/node-chatgpt-api/blob/main/demos/use-bing-client.js
|
||||
};
|
||||
|
||||
module.exports = { askSydney };
|
||||
397
api/app/google/GoogleClient.js
Normal file
397
api/app/google/GoogleClient.js
Normal file
@@ -0,0 +1,397 @@
|
||||
const crypto = require('crypto');
|
||||
const TextStream = require('../stream');
|
||||
const { google } = require('googleapis');
|
||||
const { Agent, ProxyAgent } = require('undici');
|
||||
const { getMessages, saveMessage, saveConvo } = require('../../models');
|
||||
const {
|
||||
encoding_for_model: encodingForModel,
|
||||
get_encoding: getEncoding
|
||||
} = require('@dqbd/tiktoken');
|
||||
|
||||
const tokenizersCache = {};
|
||||
|
||||
class GoogleAgent {
|
||||
constructor(credentials, options = {}) {
|
||||
this.client_email = credentials.client_email;
|
||||
this.project_id = credentials.project_id;
|
||||
this.private_key = credentials.private_key;
|
||||
this.setOptions(options);
|
||||
this.currentDateString = new Date().toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
constructUrl() {
|
||||
return `https://us-central1-aiplatform.googleapis.com/v1/projects/${this.project_id}/locations/us-central1/publishers/google/models/${this.modelOptions.model}:predict`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
this.options.examples = this.options.examples.filter(
|
||||
(obj) => obj.input.content !== '' && obj.output.content !== ''
|
||||
);
|
||||
|
||||
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 || '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
|
||||
// stop: modelOptions.stop // no stop method for now
|
||||
};
|
||||
|
||||
this.isChatModel = this.modelOptions.model.startsWith('chat-');
|
||||
const { isChatModel } = this;
|
||||
this.isTextModel = this.modelOptions.model.startsWith('text-');
|
||||
const { isTextModel } = this;
|
||||
|
||||
this.maxContextTokens = this.options.maxContextTokens || (isTextModel ? 8000 : 4096);
|
||||
// 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.maxOutputTokens || 1024;
|
||||
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.userLabel = this.options.userLabel || 'User';
|
||||
this.modelLabel = this.options.modelLabel || 'Assistant';
|
||||
|
||||
if (isChatModel) {
|
||||
// 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 (isTextModel) {
|
||||
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 `modelLabel` because I've never seen it happen
|
||||
this.modelOptions.stop = stopTokens;
|
||||
}
|
||||
|
||||
if (this.options.reverseProxyUrl) {
|
||||
this.completionsUrl = this.options.reverseProxyUrl;
|
||||
} else {
|
||||
this.completionsUrl = this.constructUrl();
|
||||
}
|
||||
|
||||
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 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 = [] }) {
|
||||
let payload = {
|
||||
instances: [
|
||||
{
|
||||
messages: [...messages, { author: this.userLabel, content: input }]
|
||||
}
|
||||
],
|
||||
parameters: this.options.modelOptions
|
||||
};
|
||||
|
||||
if (this.options.promptPrefix) {
|
||||
payload.instances[0].context = this.options.promptPrefix;
|
||||
}
|
||||
|
||||
if (this.options.examples.length > 0) {
|
||||
payload.instances[0].examples = this.options.examples;
|
||||
}
|
||||
|
||||
if (this.isTextModel) {
|
||||
payload.instances = [
|
||||
{
|
||||
prompt: input
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('buildPayload');
|
||||
console.dir(payload, { depth: null });
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
async getCompletion(input, messages = [], abortController = null) {
|
||||
if (!abortController) {
|
||||
abortController = new AbortController();
|
||||
}
|
||||
const { debug } = this.options;
|
||||
const url = this.completionsUrl;
|
||||
if (debug) {
|
||||
console.debug();
|
||||
console.debug(url);
|
||||
console.debug(this.modelOptions);
|
||||
console.debug();
|
||||
}
|
||||
const opts = {
|
||||
method: 'POST',
|
||||
agent: new Agent({
|
||||
bodyTimeout: 0,
|
||||
headersTimeout: 0
|
||||
}),
|
||||
signal: abortController.signal
|
||||
};
|
||||
|
||||
if (this.options.proxy) {
|
||||
opts.agent = new ProxyAgent(this.options.proxy);
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
if (typeof opts?.getIds === 'function') {
|
||||
opts.getIds({
|
||||
userMessage,
|
||||
conversationId,
|
||||
responseMessageId
|
||||
});
|
||||
}
|
||||
|
||||
console.log('userMessage', userMessage);
|
||||
|
||||
await this.saveMessageToDatabase(userMessage, user);
|
||||
let reply = '';
|
||||
let blocked = false;
|
||||
try {
|
||||
const result = await this.getCompletion(message, messages, opts.abortController);
|
||||
blocked = result?.predictions?.[0]?.safetyAttributes?.blocked;
|
||||
reply =
|
||||
result?.predictions?.[0]?.candidates?.[0]?.content ||
|
||||
result?.predictions?.[0]?.content ||
|
||||
'';
|
||||
if (blocked === true) {
|
||||
reply = `Google blocked a proper response to your message:\n${JSON.stringify(
|
||||
result.predictions[0].safetyAttributes
|
||||
)}${reply.length > 0 ? `\nAI Response:\n${reply}` : ''}`;
|
||||
}
|
||||
if (this.options.debug) {
|
||||
console.debug('result');
|
||||
console.debug(result);
|
||||
}
|
||||
} catch (err) {
|
||||
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);
|
||||
}
|
||||
|
||||
const responseMessage = {
|
||||
messageId: responseMessageId,
|
||||
conversationId,
|
||||
parentMessageId: userMessage.messageId,
|
||||
sender: 'PaLM2',
|
||||
text: reply,
|
||||
error: blocked,
|
||||
isCreatedByUser: false
|
||||
};
|
||||
|
||||
await this.saveMessageToDatabase(responseMessage, user);
|
||||
return responseMessage;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1,8 +1,6 @@
|
||||
const { askClient } = require('./clients/chatgpt-client');
|
||||
const { browserClient } = require('./clients/chatgpt-browser');
|
||||
const { askBing } = require('./clients/bingai');
|
||||
const { askSydney } = require('./clients/sydney');
|
||||
const customClient = require('./clients/chatgpt-custom');
|
||||
const titleConvo = require('./titleConvo');
|
||||
const getCitations = require('../lib/parse/getCitations');
|
||||
const citeText = require('../lib/parse/citeText');
|
||||
@@ -10,10 +8,8 @@ const citeText = require('../lib/parse/citeText');
|
||||
module.exports = {
|
||||
askClient,
|
||||
browserClient,
|
||||
customClient,
|
||||
askBing,
|
||||
askSydney,
|
||||
titleConvo,
|
||||
getCitations,
|
||||
citeText,
|
||||
citeText
|
||||
};
|
||||
|
||||
959
api/app/langchain/ChatAgent.js
Normal file
959
api/app/langchain/ChatAgent.js
Normal file
@@ -0,0 +1,959 @@
|
||||
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, initializeFunctionsAgent } = require('./agents/');
|
||||
const { getMessages, saveMessage, saveConvo } = require('../../models');
|
||||
const { loadTools } = require('./tools/util');
|
||||
const { 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 && 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
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.agentOptions = this.options.agentOptions || {};
|
||||
this.functionsAgent = this.agentOptions.agent === 'functions';
|
||||
this.agentIsGpt3 = this.agentOptions.model.startsWith('gpt-3');
|
||||
if (this.functionsAgent) {
|
||||
this.agentOptions.model = this.getFunctionModelName(this.agentOptions.model);
|
||||
}
|
||||
|
||||
this.isChatGptModel = this.modelOptions.model.startsWith('gpt-');
|
||||
this.isGpt3 = this.modelOptions.model.startsWith('gpt-3');
|
||||
const maxTokensMap = {
|
||||
'gpt-4': 8191,
|
||||
'gpt-4-0613': 8191,
|
||||
'gpt-4-32k': 32767,
|
||||
'gpt-4-32k-0613': 32767,
|
||||
'gpt-3.5-turbo': 4095,
|
||||
'gpt-3.5-turbo-0613': 4095,
|
||||
'gpt-3.5-turbo-0301': 4095,
|
||||
'gpt-3.5-turbo-16k': 15999,
|
||||
};
|
||||
|
||||
this.maxContextTokens = maxTokensMap[this.modelOptions.model] ?? 4095; // 1 less than maximum
|
||||
// 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 || 'Assistant';
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
getFunctionModelName(input) {
|
||||
const prefixMap = {
|
||||
'gpt-4': 'gpt-4-0613',
|
||||
'gpt-4-32k': 'gpt-4-32k-0613',
|
||||
'gpt-3.5-turbo': 'gpt-3.5-turbo-0613'
|
||||
};
|
||||
|
||||
const prefix = Object.keys(prefixMap).find(key => input.startsWith(key));
|
||||
return prefix ? prefixMap[prefix] : 'gpt-3.5-turbo-0613';
|
||||
}
|
||||
|
||||
createLLM(modelOptions, configOptions) {
|
||||
let credentials = { openAIApiKey: this.openAIApiKey };
|
||||
if (this.azure) {
|
||||
credentials = { ...this.azure };
|
||||
}
|
||||
|
||||
return new ChatOpenAI({ credentials, ...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}----->`);
|
||||
}
|
||||
|
||||
this.availableTools = await loadTools({
|
||||
user,
|
||||
model,
|
||||
tools: this.options.tools,
|
||||
functions: this.functionsAgent,
|
||||
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.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);
|
||||
}
|
||||
};
|
||||
|
||||
// initialize agent
|
||||
const initializer = this.functionsAgent ? initializeFunctionsAgent : initializeCustomAgent;
|
||||
this.executor = await initializer({
|
||||
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 } = 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 (!completionMode && this.agentOptions.skipCompletion && this.result.output) {
|
||||
responseMessage.text = this.result.output;
|
||||
this.addImages(this.result.intermediateSteps, responseMessage);
|
||||
await this.saveMessageToDatabase(responseMessage, user);
|
||||
const textStream = new TextStream(this.result.output);
|
||||
await textStream.processTextStream(opts.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 };
|
||||
}
|
||||
|
||||
addImages(intermediateSteps, responseMessage) {
|
||||
if (!intermediateSteps || !responseMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
intermediateSteps.forEach(step => {
|
||||
const { observation } = step;
|
||||
if (!observation || !observation.includes('![')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!responseMessage.text.includes(observation)) {
|
||||
responseMessage.text += '\n' + observation;
|
||||
if (this.options.debug) {
|
||||
console.debug('added image from intermediateSteps');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
const result = [messagePayload, instructionsPayload];
|
||||
|
||||
if (this.functionsAgent && !this.isGpt3 && !completionMode) {
|
||||
result[1].content = `${result[1].content}\nSure thing! Here is the output you requested:\n`;
|
||||
}
|
||||
|
||||
if (isChatGptModel) {
|
||||
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' : 'Assistant',
|
||||
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;
|
||||
147
api/app/langchain/ChatAgent.test.js
Normal file
147
api/app/langchain/ChatAgent.test.js
Normal file
@@ -0,0 +1,147 @@
|
||||
const { HumanChatMessage, AIChatMessage } = require('langchain/schema');
|
||||
const ChatAgent = require('./ChatAgent');
|
||||
const crypto = require('crypto');
|
||||
|
||||
jest.mock('../../lib/db/connectDb');
|
||||
jest.mock('../../models/Conversation', () => {
|
||||
return function () {
|
||||
return {
|
||||
save: jest.fn(),
|
||||
deleteConvos: jest.fn()
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
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 fakeMessages = [];
|
||||
const userMessage = 'Hello, ChatGPT!';
|
||||
const apiKey = 'fake-api-key';
|
||||
|
||||
beforeEach(() => {
|
||||
TestAgent = new ChatAgent(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 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
50
api/app/langchain/agents/CustomAgent/CustomAgent.js
Normal file
50
api/app/langchain/agents/CustomAgent/CustomAgent.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const { ZeroShotAgent } = require('langchain/agents');
|
||||
const { PromptTemplate, renderTemplate } = require('langchain/prompts');
|
||||
const { gpt3, gpt4 } = require('./instructions');
|
||||
|
||||
class CustomAgent extends ZeroShotAgent {
|
||||
constructor(input) {
|
||||
super(input);
|
||||
}
|
||||
|
||||
_stop() {
|
||||
return [`\nObservation:`, `\nObservation 1:`];
|
||||
}
|
||||
|
||||
static createPrompt(tools, opts = {}) {
|
||||
const { currentDateString, model } = opts;
|
||||
const inputVariables = ['input', 'chat_history', 'agent_scratchpad'];
|
||||
|
||||
let prefix, instructions, suffix;
|
||||
if (model.startsWith('gpt-3')) {
|
||||
prefix = gpt3.prefix;
|
||||
instructions = gpt3.instructions;
|
||||
suffix = gpt3.suffix;
|
||||
} else if (model.startsWith('gpt-4')) {
|
||||
prefix = gpt4.prefix;
|
||||
instructions = gpt4.instructions;
|
||||
suffix = gpt4.suffix;
|
||||
}
|
||||
|
||||
const toolStrings = tools
|
||||
.filter((tool) => tool.name !== 'self-reflection')
|
||||
.map((tool) => `${tool.name}: ${tool.description}`)
|
||||
.join('\n');
|
||||
const toolNames = tools.map((tool) => tool.name);
|
||||
const formatInstructions = (0, renderTemplate)(instructions, 'f-string', {
|
||||
tool_names: toolNames
|
||||
});
|
||||
const template = [
|
||||
`Date: ${currentDateString}\n${prefix}`,
|
||||
toolStrings,
|
||||
formatInstructions,
|
||||
suffix
|
||||
].join('\n\n');
|
||||
return new PromptTemplate({
|
||||
template,
|
||||
inputVariables
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CustomAgent;
|
||||
@@ -0,0 +1,54 @@
|
||||
const CustomAgent = require('./CustomAgent');
|
||||
const { CustomOutputParser } = require('./outputParser');
|
||||
const { AgentExecutor } = require('langchain/agents');
|
||||
const { LLMChain } = require('langchain/chains');
|
||||
const { BufferMemory, ChatMessageHistory } = require('langchain/memory');
|
||||
const {
|
||||
ChatPromptTemplate,
|
||||
SystemMessagePromptTemplate,
|
||||
HumanMessagePromptTemplate
|
||||
} = require('langchain/prompts');
|
||||
|
||||
const initializeCustomAgent = async ({
|
||||
tools,
|
||||
model,
|
||||
pastMessages,
|
||||
currentDateString,
|
||||
...rest
|
||||
}) => {
|
||||
let prompt = CustomAgent.createPrompt(tools, { currentDateString, model: model.modelName });
|
||||
|
||||
const chatPrompt = ChatPromptTemplate.fromPromptMessages([
|
||||
new SystemMessagePromptTemplate(prompt),
|
||||
HumanMessagePromptTemplate.fromTemplate(`{chat_history}
|
||||
Query: {input}
|
||||
{agent_scratchpad}`)
|
||||
]);
|
||||
|
||||
const outputParser = new CustomOutputParser({ tools });
|
||||
|
||||
const memory = new BufferMemory({
|
||||
chatHistory: new ChatMessageHistory(pastMessages),
|
||||
// returnMessages: true, // commenting this out retains memory
|
||||
memoryKey: 'chat_history',
|
||||
humanPrefix: 'User',
|
||||
aiPrefix: 'Assistant',
|
||||
inputKey: 'input',
|
||||
outputKey: 'output'
|
||||
});
|
||||
|
||||
const llmChain = new LLMChain({
|
||||
prompt: chatPrompt,
|
||||
llm: model
|
||||
});
|
||||
|
||||
const agent = new CustomAgent({
|
||||
llmChain,
|
||||
outputParser,
|
||||
allowedTools: tools.map((tool) => tool.name)
|
||||
});
|
||||
|
||||
return AgentExecutor.fromAgentAndTools({ agent, tools, memory, ...rest });
|
||||
};
|
||||
|
||||
module.exports = initializeCustomAgent;
|
||||
203
api/app/langchain/agents/CustomAgent/instructions.js
Normal file
203
api/app/langchain/agents/CustomAgent/instructions.js
Normal file
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
module.exports = `You are ChatGPT, a Large Language model with useful tools.
|
||||
|
||||
Talk to the human and provide meaningful answers when questions are asked.
|
||||
|
||||
Use the tools when you need them, but use your own knowledge if you are confident of the answer. Keep answers short and concise.
|
||||
|
||||
A tool is not usually needed for creative requests, so do your best to answer them without tools.
|
||||
|
||||
Avoid repeating identical answers if it appears before. Only fulfill the human's requests, do not create extra steps beyond what the human has asked for.
|
||||
|
||||
Your input for 'Action' should be the name of tool used only.
|
||||
|
||||
Be honest. If you can't answer something, or a tool is not appropriate, say you don't know or answer to the best of your ability.
|
||||
|
||||
Attempt to fulfill the human's requests in as few actions as possible`;
|
||||
*/
|
||||
|
||||
// module.exports = `You are ChatGPT, a highly knowledgeable and versatile large language model.
|
||||
|
||||
// Engage with the Human conversationally, providing concise and meaningful answers to questions. Utilize built-in tools when necessary, except for creative requests, where relying on your own knowledge is preferred. Aim for variety and avoid repetitive answers.
|
||||
|
||||
// For your 'Action' input, state the name of the tool used only, and honor user requests without adding extra steps. Always be honest; if you cannot provide an appropriate answer or tool, admit that or do your best.
|
||||
|
||||
// Strive to meet the user's needs efficiently with minimal actions.`;
|
||||
|
||||
// import {
|
||||
// BasePromptTemplate,
|
||||
// BaseStringPromptTemplate,
|
||||
// SerializedBasePromptTemplate,
|
||||
// renderTemplate,
|
||||
// } from "langchain/prompts";
|
||||
|
||||
// prefix: `You are ChatGPT, a highly knowledgeable and versatile large language model.
|
||||
// Your objective is to help users by understanding their intent and choosing the best action. Prioritize direct, specific responses. Use concise, varied answers and rely on your knowledge for creative tasks. Utilize tools when needed, and structure results for machine compatibility.
|
||||
// prefix: `Objective: to comprehend human intentions based on user input and available tools. Goal: identify the best action to directly address the human's query. In your subsequent steps, you will utilize the chosen action. You may select multiple actions and list them in a meaningful order. Prioritize actions that directly relate to the user's query over general ones. Ensure that the generated thought is highly specific and explicit to best match the user's expectations. Construct the result in a manner that an online open-API would most likely expect. Provide concise and meaningful answers to human queries. Utilize tools when necessary. Relying on your own knowledge is preferred for creative requests. Aim for variety and avoid repetitive answers.
|
||||
|
||||
// # Available Actions & Tools:
|
||||
// N/A: no suitable action, use your own knowledge.`,
|
||||
// suffix: `Remember, all your responses MUST adhere to the described format and only respond if the format is followed. Output exactly with the requested format, avoiding any other text as this will be parsed by a machine. Following 'Action:', provide only one of the actions listed above. If a tool is not necessary, deduce this quickly and finish your response. Honor the human's requests without adding extra steps. Carry out tasks in the sequence written by the human. Always be honest; if you cannot provide an appropriate answer or tool, do your best with your own knowledge. Strive to meet the user's needs efficiently with minimal actions.`;
|
||||
|
||||
module.exports = {
|
||||
'gpt3-v1': {
|
||||
prefix: `Objective: Understand human intentions using user input and available tools. Goal: Identify the most suitable actions to directly address user queries.
|
||||
|
||||
When responding:
|
||||
- Choose actions relevant to the user's query, using multiple actions in a logical order if needed.
|
||||
- Prioritize direct and specific thoughts to meet user expectations.
|
||||
- Format results in a way compatible with open-API expectations.
|
||||
- Offer concise, meaningful answers to user queries.
|
||||
- Use tools when necessary but rely on your own knowledge for creative requests.
|
||||
- Strive for variety, avoiding repetitive responses.
|
||||
|
||||
# Available Actions & Tools:
|
||||
N/A: No suitable action; use your own knowledge.`,
|
||||
instructions: `Always adhere to the following format in your response to indicate actions taken:
|
||||
|
||||
Thought: Summarize your thought process.
|
||||
Action: Select an action from [{tool_names}].
|
||||
Action Input: Define the action's input.
|
||||
Observation: Report the action's result.
|
||||
|
||||
Repeat steps 1-4 as needed, in order. When not using a tool, use N/A for Action, provide the result as Action Input, and include an Observation.
|
||||
|
||||
Upon reaching the final answer, use this format after completing all necessary actions:
|
||||
|
||||
Thought: Indicate that you've determined the final answer.
|
||||
Final Answer: Present the answer to the user's query.`,
|
||||
suffix: `Keep these guidelines in mind when crafting your response:
|
||||
- Strictly adhere to the Action format for all responses, as they will be machine-parsed.
|
||||
- If a tool is unnecessary, quickly move to the Thought/Final Answer format.
|
||||
- Follow the logical sequence provided by the user without adding extra steps.
|
||||
- Be honest; if you can't provide an appropriate answer using the given tools, use your own knowledge.
|
||||
- Aim for efficiency and minimal actions to meet the user's needs effectively.`,
|
||||
},
|
||||
'gpt3-v2': {
|
||||
prefix: `Objective: Understand the human's query with available actions & tools. Let's work this out in a step by step way to be sure we fulfill the query.
|
||||
|
||||
When responding:
|
||||
- Choose actions relevant to the user's query, using multiple actions in a logical order if needed.
|
||||
- Prioritize direct and specific thoughts to meet user expectations.
|
||||
- Format results in a way compatible with open-API expectations.
|
||||
- Offer concise, meaningful answers to user queries.
|
||||
- Use tools when necessary but rely on your own knowledge for creative requests.
|
||||
- Strive for variety, avoiding repetitive responses.
|
||||
|
||||
# Available Actions & Tools:
|
||||
N/A: No suitable action; use your own knowledge.`,
|
||||
instructions: `I want you to respond with this format and this format only, without comments or explanations, to indicate actions taken:
|
||||
\`\`\`
|
||||
Thought: Summarize your thought process.
|
||||
Action: Select an action from [{tool_names}].
|
||||
Action Input: Define the action's input.
|
||||
Observation: Report the action's result.
|
||||
\`\`\`
|
||||
|
||||
Repeat the format for each action as needed. When not using a tool, use N/A for Action, provide the result as Action Input, and include an Observation.
|
||||
|
||||
Upon reaching the final answer, use this format after completing all necessary actions:
|
||||
\`\`\`
|
||||
Thought: Indicate that you've determined the final answer.
|
||||
Final Answer: A conversational reply to the user's query as if you were answering them directly.
|
||||
\`\`\``,
|
||||
suffix: `Keep these guidelines in mind when crafting your response:
|
||||
- Strictly adhere to the Action format for all responses, as they will be machine-parsed.
|
||||
- If a tool is unnecessary, quickly move to the Thought/Final Answer format.
|
||||
- Follow the logical sequence provided by the user without adding extra steps.
|
||||
- Be honest; if you can't provide an appropriate answer using the given tools, use your own knowledge.
|
||||
- Aim for efficiency and minimal actions to meet the user's needs effectively.`,
|
||||
},
|
||||
gpt3: {
|
||||
prefix: `Objective: Understand the human's query with available actions & tools. Let's work this out in a step by step way to be sure we fulfill the query.
|
||||
|
||||
Use available actions and tools judiciously.
|
||||
|
||||
# Available Actions & Tools:
|
||||
N/A: No suitable action; use your own knowledge.`,
|
||||
instructions: `I want you to respond with this format and this format only, without comments or explanations, to indicate actions taken:
|
||||
\`\`\`
|
||||
Thought: Your thought process.
|
||||
Action: Action from [{tool_names}].
|
||||
Action Input: Action's input.
|
||||
Observation: Action's result.
|
||||
\`\`\`
|
||||
|
||||
For each action, repeat the format. If no tool is used, use N/A for Action, and provide the result as Action Input.
|
||||
|
||||
Finally, complete with:
|
||||
\`\`\`
|
||||
Thought: Convey final answer determination.
|
||||
Final Answer: Reply to user's query conversationally.
|
||||
\`\`\``,
|
||||
suffix: `Remember:
|
||||
- Adhere to the Action format strictly for parsing.
|
||||
- Transition quickly to Thought/Final Answer format when a tool isn't needed.
|
||||
- Follow user's logic without superfluous steps.
|
||||
- If unable to use tools for a fitting answer, use your knowledge.
|
||||
- Strive for efficient, minimal actions.`,
|
||||
},
|
||||
'gpt4-v1': {
|
||||
prefix: `Objective: Understand the human's query with available actions & tools. Let's work this out in a step by step way to be sure we fulfill the query.
|
||||
|
||||
When responding:
|
||||
- Choose actions relevant to the query, using multiple actions in a step by step way.
|
||||
- Prioritize direct and specific thoughts to meet user expectations.
|
||||
- Be precise and offer meaningful answers to user queries.
|
||||
- Use tools when necessary but rely on your own knowledge for creative requests.
|
||||
- Strive for variety, avoiding repetitive responses.
|
||||
|
||||
# Available Actions & Tools:
|
||||
N/A: No suitable action; use your own knowledge.`,
|
||||
instructions: `I want you to respond with this format and this format only, without comments or explanations, to indicate actions taken:
|
||||
\`\`\`
|
||||
Thought: Summarize your thought process.
|
||||
Action: Select an action from [{tool_names}].
|
||||
Action Input: Define the action's input.
|
||||
Observation: Report the action's result.
|
||||
\`\`\`
|
||||
|
||||
Repeat the format for each action as needed. When not using a tool, use N/A for Action, provide the result as Action Input, and include an Observation.
|
||||
|
||||
Upon reaching the final answer, use this format after completing all necessary actions:
|
||||
\`\`\`
|
||||
Thought: Indicate that you've determined the final answer.
|
||||
Final Answer: A conversational reply to the user's query as if you were answering them directly.
|
||||
\`\`\``,
|
||||
suffix: `Keep these guidelines in mind when crafting your final response:
|
||||
- Strictly adhere to the Action format for all responses.
|
||||
- If a tool is unnecessary, quickly move to the Thought/Final Answer format, only if no further actions are possible or necessary.
|
||||
- Follow the logical sequence provided by the user without adding extra steps.
|
||||
- Be honest: if you can't provide an appropriate answer using the given tools, use your own knowledge.
|
||||
- Aim for efficiency and minimal actions to meet the user's needs effectively.`,
|
||||
},
|
||||
gpt4: {
|
||||
prefix: `Objective: Understand the human's query with available actions & tools. Let's work this out in a step by step way to be sure we fulfill the query.
|
||||
|
||||
Use available actions and tools judiciously.
|
||||
|
||||
# Available Actions & Tools:
|
||||
N/A: No suitable action; use your own knowledge.`,
|
||||
instructions: `Respond in this specific format without extraneous comments:
|
||||
\`\`\`
|
||||
Thought: Your thought process.
|
||||
Action: Action from [{tool_names}].
|
||||
Action Input: Action's input.
|
||||
Observation: Action's result.
|
||||
\`\`\`
|
||||
|
||||
For each action, repeat the format. If no tool is used, use N/A for Action, and provide the result as Action Input.
|
||||
|
||||
Finally, complete with:
|
||||
\`\`\`
|
||||
Thought: Indicate that you've determined the final answer.
|
||||
Final Answer: A conversational reply to the user's query, including your full answer.
|
||||
\`\`\``,
|
||||
suffix: `Remember:
|
||||
- Adhere to the Action format strictly for parsing.
|
||||
- Transition quickly to Thought/Final Answer format when a tool isn't needed.
|
||||
- Follow user's logic without superfluous steps.
|
||||
- If unable to use tools for a fitting answer, use your knowledge.
|
||||
- Strive for efficient, minimal actions.`,
|
||||
},
|
||||
};
|
||||
218
api/app/langchain/agents/CustomAgent/outputParser.js
Normal file
218
api/app/langchain/agents/CustomAgent/outputParser.js
Normal file
@@ -0,0 +1,218 @@
|
||||
const { ZeroShotAgentOutputParser } = require('langchain/agents');
|
||||
|
||||
class CustomOutputParser extends ZeroShotAgentOutputParser {
|
||||
constructor(fields) {
|
||||
super(fields);
|
||||
this.tools = fields.tools;
|
||||
this.longestToolName = '';
|
||||
for (const tool of this.tools) {
|
||||
if (tool.name.length > this.longestToolName.length) {
|
||||
this.longestToolName = tool.name;
|
||||
}
|
||||
}
|
||||
this.finishToolNameRegex = /(?:the\s+)?final\s+answer:\s*/i;
|
||||
this.actionValues =
|
||||
/(?:Action(?: [1-9])?:) ([\s\S]*?)(?:\n(?:Action Input(?: [1-9])?:) ([\s\S]*?))?$/i;
|
||||
this.actionInputRegex = /(?:Action Input(?: *\d*):) ?([\s\S]*?)$/i;
|
||||
this.thoughtRegex = /(?:Thought(?: *\d*):) ?([\s\S]*?)$/i;
|
||||
}
|
||||
|
||||
getValidTool(text) {
|
||||
let result = false;
|
||||
for (const tool of this.tools) {
|
||||
const { name } = tool;
|
||||
const toolIndex = text.indexOf(name);
|
||||
if (toolIndex !== -1) {
|
||||
result = name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
checkIfValidTool(text) {
|
||||
let isValidTool = false;
|
||||
for (const tool of this.tools) {
|
||||
const { name } = tool;
|
||||
if (text === name) {
|
||||
isValidTool = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isValidTool;
|
||||
}
|
||||
|
||||
async parse(text) {
|
||||
const finalMatch = text.match(this.finishToolNameRegex);
|
||||
// if (text.includes(this.finishToolName)) {
|
||||
// const parts = text.split(this.finishToolName);
|
||||
// const output = parts[parts.length - 1].trim();
|
||||
// return {
|
||||
// returnValues: { output },
|
||||
// log: text
|
||||
// };
|
||||
// }
|
||||
|
||||
if (finalMatch) {
|
||||
const output = text.substring(finalMatch.index + finalMatch[0].length).trim();
|
||||
return {
|
||||
returnValues: { output },
|
||||
log: text
|
||||
};
|
||||
}
|
||||
|
||||
const match = this.actionValues.exec(text); // old v2
|
||||
|
||||
if (!match) {
|
||||
console.log(
|
||||
'\n\n<----------------------HIT NO MATCH PARSING ERROR---------------------->\n\n',
|
||||
match
|
||||
);
|
||||
const thoughts = text.replace(/[tT]hought:/, '').split('\n');
|
||||
// return {
|
||||
// tool: 'self-reflection',
|
||||
// toolInput: thoughts[0],
|
||||
// log: thoughts.slice(1).join('\n')
|
||||
// };
|
||||
|
||||
return {
|
||||
returnValues: { output: thoughts[0] },
|
||||
log: thoughts.slice(1).join('\n')
|
||||
};
|
||||
}
|
||||
|
||||
let selectedTool = match?.[1].trim().toLowerCase();
|
||||
|
||||
if (match && selectedTool === 'n/a') {
|
||||
console.log(
|
||||
'\n\n<----------------------HIT N/A PARSING ERROR---------------------->\n\n',
|
||||
match
|
||||
);
|
||||
return {
|
||||
tool: 'self-reflection',
|
||||
toolInput: match[2]?.trim().replace(/^"+|"+$/g, '') ?? '',
|
||||
log: text
|
||||
};
|
||||
}
|
||||
|
||||
let toolIsValid = this.checkIfValidTool(selectedTool);
|
||||
if (match && !toolIsValid) {
|
||||
console.log(
|
||||
'\n\n<----------------Tool invalid: Re-assigning Selected Tool---------------->\n\n',
|
||||
match
|
||||
);
|
||||
selectedTool = this.getValidTool(selectedTool);
|
||||
}
|
||||
|
||||
if (match && !selectedTool) {
|
||||
console.log(
|
||||
'\n\n<----------------------HIT INVALID TOOL PARSING ERROR---------------------->\n\n',
|
||||
match
|
||||
);
|
||||
selectedTool = 'self-reflection';
|
||||
}
|
||||
|
||||
if (match && !match[2]) {
|
||||
console.log(
|
||||
'\n\n<----------------------HIT NO ACTION INPUT PARSING ERROR---------------------->\n\n',
|
||||
match
|
||||
);
|
||||
|
||||
// In case there is no action input, let's double-check if there is an action input in 'text' variable
|
||||
const actionInputMatch = this.actionInputRegex.exec(text);
|
||||
const thoughtMatch = this.thoughtRegex.exec(text);
|
||||
if (actionInputMatch) {
|
||||
return {
|
||||
tool: selectedTool,
|
||||
toolInput: actionInputMatch[1].trim(),
|
||||
log: text
|
||||
};
|
||||
}
|
||||
|
||||
if (thoughtMatch && !actionInputMatch) {
|
||||
return {
|
||||
tool: selectedTool,
|
||||
toolInput: thoughtMatch[1].trim(),
|
||||
log: text
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (match && selectedTool.length > this.longestToolName.length) {
|
||||
console.log('\n\n<----------------------HIT LONG PARSING ERROR---------------------->\n\n');
|
||||
|
||||
let action, input, thought;
|
||||
let firstIndex = Infinity;
|
||||
|
||||
for (const tool of this.tools) {
|
||||
const { name } = tool;
|
||||
const toolIndex = text.indexOf(name);
|
||||
if (toolIndex !== -1 && toolIndex < firstIndex) {
|
||||
firstIndex = toolIndex;
|
||||
action = name;
|
||||
}
|
||||
}
|
||||
|
||||
// In case there is no action input, let's double-check if there is an action input in 'text' variable
|
||||
const actionInputMatch = this.actionInputRegex.exec(text);
|
||||
if (action && actionInputMatch) {
|
||||
console.log(
|
||||
'\n\n<------Matched Action Input in Long Parsing Error------>\n\n',
|
||||
actionInputMatch
|
||||
);
|
||||
return {
|
||||
tool: action,
|
||||
toolInput: actionInputMatch[1].trim().replaceAll('"', ''),
|
||||
log: text
|
||||
};
|
||||
}
|
||||
|
||||
if (action) {
|
||||
const actionEndIndex = text.indexOf('Action:', firstIndex + action.length);
|
||||
const inputText = text
|
||||
.slice(firstIndex + action.length, actionEndIndex !== -1 ? actionEndIndex : undefined)
|
||||
.trim();
|
||||
const inputLines = inputText.split('\n');
|
||||
input = inputLines[0];
|
||||
if (inputLines.length > 1) {
|
||||
thought = inputLines.slice(1).join('\n');
|
||||
}
|
||||
const returnValues = {
|
||||
tool: action,
|
||||
toolInput: input,
|
||||
log: thought || inputText
|
||||
};
|
||||
|
||||
const inputMatch = this.actionValues.exec(returnValues.log); //new
|
||||
if (inputMatch) {
|
||||
console.log('inputMatch');
|
||||
console.dir(inputMatch, { depth: null });
|
||||
returnValues.toolInput = inputMatch[1].replaceAll('"', '').trim();
|
||||
returnValues.log = returnValues.log.replace(this.actionValues, '');
|
||||
}
|
||||
|
||||
return returnValues;
|
||||
} else {
|
||||
console.log('No valid tool mentioned.', this.tools, text);
|
||||
return {
|
||||
tool: 'self-reflection',
|
||||
toolInput: 'Hypothetical actions: \n"' + text + '"\n',
|
||||
log: 'Thought: I need to look at my hypothetical actions and try one'
|
||||
};
|
||||
}
|
||||
|
||||
// if (action && input) {
|
||||
// console.log('Action:', action);
|
||||
// console.log('Input:', input);
|
||||
// }
|
||||
}
|
||||
|
||||
return {
|
||||
tool: selectedTool,
|
||||
toolInput: match[2]?.trim()?.replace(/^"+|"+$/g, '') ?? '',
|
||||
log: text
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { CustomOutputParser };
|
||||
120
api/app/langchain/agents/Functions/FunctionsAgent.js
Normal file
120
api/app/langchain/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;
|
||||
@@ -0,0 +1,36 @@
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
const {
|
||||
ChainStepExecutor,
|
||||
LLMPlanner,
|
||||
PlanOutputParser,
|
||||
PlanAndExecuteAgentExecutor
|
||||
} = require('langchain/experimental/plan_and_execute');
|
||||
const { LLMChain } = require('langchain/chains');
|
||||
const { ChatAgent, AgentExecutor } = require('langchain/agents');
|
||||
const { BufferMemory, ChatMessageHistory } = require('langchain/memory');
|
||||
const {
|
||||
ChatPromptTemplate,
|
||||
SystemMessagePromptTemplate,
|
||||
HumanMessagePromptTemplate
|
||||
} = require('langchain/prompts');
|
||||
|
||||
const DEFAULT_STEP_EXECUTOR_HUMAN_CHAT_MESSAGE_TEMPLATE = `{chat_history}
|
||||
|
||||
Previous steps: {previous_steps}
|
||||
Current objective: {current_step}
|
||||
{agent_scratchpad}
|
||||
You may extract and combine relevant data from your previous steps when responding to me.`;
|
||||
|
||||
const PLANNER_SYSTEM_PROMPT_MESSAGE_TEMPLATE = [
|
||||
`Let's first understand the problem and devise a plan to solve the problem.`,
|
||||
`Please output the plan starting with the header "Plan:"`,
|
||||
`and then followed by a numbered list of steps.`,
|
||||
`Please make the plan the minimum number of steps required`,
|
||||
`to answer the query or complete the task accurately and precisely.`,
|
||||
`Your steps should be general, and should not require a specific method to solve a step. If the task is a question,`,
|
||||
`the final step in the plan must be the following: "Given the above steps taken,`,
|
||||
`please respond to the original query."`,
|
||||
`At the end of your plan, say "<END_OF_PLAN>"`
|
||||
].join(' ');
|
||||
|
||||
const PLANNER_CHAT_PROMPT = /* #__PURE__ */ ChatPromptTemplate.fromPromptMessages([
|
||||
/* #__PURE__ */ SystemMessagePromptTemplate.fromTemplate(PLANNER_SYSTEM_PROMPT_MESSAGE_TEMPLATE),
|
||||
/* #__PURE__ */ HumanMessagePromptTemplate.fromTemplate(`{input}`)
|
||||
]);
|
||||
|
||||
const initializePAEAgent = async ({ tools: _tools, model: llm, pastMessages, ...rest }) => {
|
||||
//removed currentDateString
|
||||
const tools = _tools.filter((tool) => tool.name !== 'self-reflection');
|
||||
|
||||
const memory = new BufferMemory({
|
||||
chatHistory: new ChatMessageHistory(pastMessages),
|
||||
// returnMessages: true, // commenting this out retains memory
|
||||
memoryKey: 'chat_history',
|
||||
humanPrefix: 'User',
|
||||
aiPrefix: 'Assistant',
|
||||
inputKey: 'input',
|
||||
outputKey: 'output'
|
||||
});
|
||||
|
||||
const plannerLlmChain = new LLMChain({
|
||||
llm,
|
||||
prompt: PLANNER_CHAT_PROMPT,
|
||||
memory
|
||||
});
|
||||
const planner = new LLMPlanner(plannerLlmChain, new PlanOutputParser());
|
||||
|
||||
const agent = ChatAgent.fromLLMAndTools(llm, tools, {
|
||||
humanMessageTemplate: DEFAULT_STEP_EXECUTOR_HUMAN_CHAT_MESSAGE_TEMPLATE
|
||||
});
|
||||
|
||||
const stepExecutor = new ChainStepExecutor(
|
||||
AgentExecutor.fromAgentAndTools({ agent, tools, memory, ...rest })
|
||||
);
|
||||
|
||||
return new PlanAndExecuteAgentExecutor({
|
||||
planner,
|
||||
stepExecutor
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
initializePAEAgent
|
||||
};
|
||||
7
api/app/langchain/agents/index.js
Normal file
7
api/app/langchain/agents/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const initializeCustomAgent = require('./CustomAgent/initializeCustomAgent');
|
||||
const initializeFunctionsAgent = require('./Functions/initializeFunctionsAgent');
|
||||
|
||||
module.exports = {
|
||||
initializeCustomAgent,
|
||||
initializeFunctionsAgent
|
||||
};
|
||||
31
api/app/langchain/demos/demo-aiplugin.js
Normal file
31
api/app/langchain/demos/demo-aiplugin.js
Normal file
@@ -0,0 +1,31 @@
|
||||
require('dotenv').config();
|
||||
const { ChatOpenAI } = require( "langchain/chat_models/openai");
|
||||
const { initializeAgentExecutorWithOptions } = require( "langchain/agents");
|
||||
const HttpRequestTool = require('../tools/HttpRequestTool');
|
||||
const AIPluginTool = require('../tools/AIPluginTool');
|
||||
|
||||
const run = async () => {
|
||||
const openAIApiKey = process.env.OPENAI_API_KEY;
|
||||
const tools = [
|
||||
new HttpRequestTool(),
|
||||
await AIPluginTool.fromPluginUrl(
|
||||
"https://www.klarna.com/.well-known/ai-plugin.json", new ChatOpenAI({ temperature: 0, openAIApiKey })
|
||||
),
|
||||
];
|
||||
const agent = await initializeAgentExecutorWithOptions(
|
||||
tools,
|
||||
new ChatOpenAI({ temperature: 0, openAIApiKey }),
|
||||
{ agentType: "chat-zero-shot-react-description", verbose: true }
|
||||
);
|
||||
|
||||
const result = await agent.call({
|
||||
input: "what t shirts are available in klarna?",
|
||||
});
|
||||
|
||||
console.log({ result });
|
||||
};
|
||||
|
||||
(async () => {
|
||||
await run();
|
||||
})();
|
||||
|
||||
47
api/app/langchain/demos/demo-yaml.js
Normal file
47
api/app/langchain/demos/demo-yaml.js
Normal file
@@ -0,0 +1,47 @@
|
||||
require('dotenv').config();
|
||||
|
||||
const fs = require( "fs");
|
||||
const yaml = require( "js-yaml");
|
||||
const { OpenAI } = require( "langchain/llms/openai");
|
||||
const { JsonSpec } = require( "langchain/tools");
|
||||
const { createOpenApiAgent, OpenApiToolkit } = require( "langchain/agents");
|
||||
|
||||
const run = async () => {
|
||||
let data;
|
||||
try {
|
||||
const yamlFile = fs.readFileSync("./app/langchain/demos/klarna.yaml", "utf8");
|
||||
data = yaml.load(yamlFile);
|
||||
if (!data) {
|
||||
throw new Error("Failed to load OpenAPI spec");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
// Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
|
||||
};
|
||||
const model = new OpenAI({ temperature: 0 });
|
||||
const toolkit = new OpenApiToolkit(new JsonSpec(data), model, headers);
|
||||
const executor = createOpenApiAgent(model, toolkit, { verbose: true });
|
||||
|
||||
const input = `Find me some medium sized blue shirts`;
|
||||
console.log(`Executing with input "${input}"...`);
|
||||
|
||||
const result = await executor.call({ input });
|
||||
console.log(`Got output ${result.output}`);
|
||||
|
||||
console.log(
|
||||
`Got intermediate steps ${JSON.stringify(
|
||||
result.intermediateSteps,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
};
|
||||
|
||||
(async () => {
|
||||
await run();
|
||||
})();
|
||||
79
api/app/langchain/demos/klarna.yaml
Normal file
79
api/app/langchain/demos/klarna.yaml
Normal file
@@ -0,0 +1,79 @@
|
||||
openapi: 3.0.1
|
||||
servers:
|
||||
- url: https://www.klarna.com/us/shopping
|
||||
info:
|
||||
title: Open AI Klarna product Api
|
||||
version: v0
|
||||
x-apisguru-categories:
|
||||
- ecommerce
|
||||
x-logo:
|
||||
url: https://www.klarna.com/static/img/social-prod-imagery-blinds-beauty-default.jpg
|
||||
x-origin:
|
||||
- format: openapi
|
||||
url: https://www.klarna.com/us/shopping/public/openai/v0/api-docs/
|
||||
version: "3.0"
|
||||
x-providerName: klarna.com
|
||||
x-serviceName: openai
|
||||
tags:
|
||||
- description: Open AI Product Endpoint. Query for products.
|
||||
name: open-ai-product-endpoint
|
||||
paths:
|
||||
/public/openai/v0/products:
|
||||
get:
|
||||
deprecated: false
|
||||
operationId: productsUsingGET
|
||||
parameters:
|
||||
- description: A precise query that matches one very small category or product that needs to be searched for to find the products the user is looking for. If the user explicitly stated what they want, use that as a query. The query is as specific as possible to the product name or category mentioned by the user in its singular form, and don't contain any clarifiers like latest, newest, cheapest, budget, premium, expensive or similar. The query is always taken from the latest topic, if there is a new topic a new query is started.
|
||||
in: query
|
||||
name: q
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- description: number of products returned
|
||||
in: query
|
||||
name: size
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
- description: maximum price of the matching product in local currency, filters results
|
||||
in: query
|
||||
name: budget
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProductResponse"
|
||||
description: Products found
|
||||
"503":
|
||||
description: one or more services are unavailable
|
||||
summary: API for fetching Klarna product information
|
||||
tags:
|
||||
- open-ai-product-endpoint
|
||||
components:
|
||||
schemas:
|
||||
Product:
|
||||
properties:
|
||||
attributes:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
name:
|
||||
type: string
|
||||
price:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
title: Product
|
||||
type: object
|
||||
ProductResponse:
|
||||
properties:
|
||||
products:
|
||||
items:
|
||||
$ref: "#/components/schemas/Product"
|
||||
type: array
|
||||
title: ProductResponse
|
||||
type: object
|
||||
32
api/app/langchain/demos/planExecutor.js
Normal file
32
api/app/langchain/demos/planExecutor.js
Normal file
@@ -0,0 +1,32 @@
|
||||
require('dotenv').config();
|
||||
const { Calculator } = require('langchain/tools/calculator');
|
||||
const { SerpAPI } = require('langchain/tools');
|
||||
const { ChatOpenAI } = require('langchain/chat_models/openai');
|
||||
const { PlanAndExecuteAgentExecutor } = require('langchain/experimental/plan_and_execute');
|
||||
|
||||
const tools = [
|
||||
new Calculator(),
|
||||
new SerpAPI(process.env.SERPAPI_API_KEY || '', {
|
||||
location: 'Austin,Texas,United States',
|
||||
hl: 'en',
|
||||
gl: 'us'
|
||||
})
|
||||
];
|
||||
const model = new ChatOpenAI({
|
||||
temperature: 0,
|
||||
modelName: 'gpt-3.5-turbo',
|
||||
verbose: true,
|
||||
openAIApiKey: process.env.OPENAI_API_KEY
|
||||
});
|
||||
const executor = PlanAndExecuteAgentExecutor.fromLLMAndTools({
|
||||
llm: model,
|
||||
tools
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const result = await executor.call({
|
||||
input: `Who is the current president of the United States? What is their current age raised to the second power?`
|
||||
});
|
||||
|
||||
console.log({ result });
|
||||
})();
|
||||
7305
api/app/langchain/demos/spotify.yaml
Normal file
7305
api/app/langchain/demos/spotify.yaml
Normal file
File diff suppressed because it is too large
Load Diff
6
api/app/langchain/instructions.js
Normal file
6
api/app/langchain/instructions.js
Normal file
@@ -0,0 +1,6 @@
|
||||
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:`,
|
||||
};
|
||||
237
api/app/langchain/tools/AIPluginTool.js
Normal file
237
api/app/langchain/tools/AIPluginTool.js
Normal file
@@ -0,0 +1,237 @@
|
||||
const { Tool } = require('langchain/tools');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
/*
|
||||
export interface AIPluginToolParams {
|
||||
name: string;
|
||||
description: string;
|
||||
apiSpec: string;
|
||||
openaiSpec: string;
|
||||
model: BaseLanguageModel;
|
||||
}
|
||||
|
||||
|
||||
export interface PathParameter {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Info {
|
||||
title: string;
|
||||
description: string;
|
||||
version: string;
|
||||
}
|
||||
export interface PathMethod {
|
||||
summary: string;
|
||||
operationId: string;
|
||||
parameters?: PathParameter[];
|
||||
}
|
||||
|
||||
interface ApiSpec {
|
||||
openapi: string;
|
||||
info: Info;
|
||||
paths: { [key: string]: { [key: string]: PathMethod } };
|
||||
}
|
||||
*/
|
||||
|
||||
function isJson(str) {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function convertJsonToYamlIfApplicable(spec) {
|
||||
if (isJson(spec)) {
|
||||
const jsonData = JSON.parse(spec);
|
||||
return yaml.dump(jsonData);
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
|
||||
function extractShortVersion(openapiSpec) {
|
||||
openapiSpec = convertJsonToYamlIfApplicable(openapiSpec);
|
||||
try {
|
||||
const fullApiSpec = yaml.load(openapiSpec);
|
||||
const shortApiSpec = {
|
||||
openapi: fullApiSpec.openapi,
|
||||
info: fullApiSpec.info,
|
||||
paths: {}
|
||||
};
|
||||
|
||||
for (let path in fullApiSpec.paths) {
|
||||
shortApiSpec.paths[path] = {};
|
||||
for (let method in fullApiSpec.paths[path]) {
|
||||
shortApiSpec.paths[path][method] = {
|
||||
summary: fullApiSpec.paths[path][method].summary,
|
||||
operationId: fullApiSpec.paths[path][method].operationId,
|
||||
parameters: fullApiSpec.paths[path][method].parameters?.map((parameter) => ({
|
||||
name: parameter.name,
|
||||
description: parameter.description
|
||||
}))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return yaml.dump(shortApiSpec);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
function printOperationDetails(operationId, openapiSpec) {
|
||||
openapiSpec = convertJsonToYamlIfApplicable(openapiSpec);
|
||||
let returnText = '';
|
||||
try {
|
||||
let doc = yaml.load(openapiSpec);
|
||||
let servers = doc.servers;
|
||||
let paths = doc.paths;
|
||||
let components = doc.components;
|
||||
|
||||
for (let path in paths) {
|
||||
for (let method in paths[path]) {
|
||||
let operation = paths[path][method];
|
||||
if (operation.operationId === operationId) {
|
||||
returnText += `The API request to do for operationId "${operationId}" is:\n`;
|
||||
returnText += `Method: ${method.toUpperCase()}\n`;
|
||||
|
||||
let url = servers[0].url + path;
|
||||
returnText += `Path: ${url}\n`;
|
||||
|
||||
returnText += 'Parameters:\n';
|
||||
if (operation.parameters) {
|
||||
for (let param of operation.parameters) {
|
||||
let required = param.required ? '' : ' (optional),';
|
||||
returnText += `- ${param.name} (${param.in},${required} ${param.schema.type}): ${param.description}\n`;
|
||||
}
|
||||
} else {
|
||||
returnText += ' None\n';
|
||||
}
|
||||
returnText += '\n';
|
||||
|
||||
let responseSchema = operation.responses['200'].content['application/json'].schema;
|
||||
|
||||
// Check if schema is a reference
|
||||
if (responseSchema.$ref) {
|
||||
// Extract schema name from reference
|
||||
let schemaName = responseSchema.$ref.split('/').pop();
|
||||
// Look up schema in components
|
||||
responseSchema = components.schemas[schemaName];
|
||||
}
|
||||
|
||||
returnText += 'Response schema:\n';
|
||||
returnText += '- Type: ' + responseSchema.type + '\n';
|
||||
returnText += '- Additional properties:\n';
|
||||
returnText += ' - Type: ' + responseSchema.additionalProperties?.type + '\n';
|
||||
if (responseSchema.additionalProperties?.properties) {
|
||||
returnText += ' - Properties:\n';
|
||||
for (let prop in responseSchema.additionalProperties.properties) {
|
||||
returnText += ` - ${prop} (${responseSchema.additionalProperties.properties[prop].type}): Description not provided in OpenAPI spec\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (returnText === '') {
|
||||
returnText += `No operation with operationId "${operationId}" found.`;
|
||||
}
|
||||
return returnText;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
class AIPluginTool extends Tool {
|
||||
/*
|
||||
private _name: string;
|
||||
private _description: string;
|
||||
apiSpec: string;
|
||||
openaiSpec: string;
|
||||
model: BaseLanguageModel;
|
||||
*/
|
||||
|
||||
get name() {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
get description() {
|
||||
return this._description;
|
||||
}
|
||||
|
||||
constructor(params) {
|
||||
super();
|
||||
this._name = params.name;
|
||||
this._description = params.description;
|
||||
this.apiSpec = params.apiSpec;
|
||||
this.openaiSpec = params.openaiSpec;
|
||||
this.model = params.model;
|
||||
}
|
||||
|
||||
async _call(input) {
|
||||
let date = new Date();
|
||||
let fullDate = `Date: ${date.getDate()}/${
|
||||
date.getMonth() + 1
|
||||
}/${date.getFullYear()}, Time: ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
|
||||
const prompt = `${fullDate}\nQuestion: ${input} \n${this.apiSpec}.`;
|
||||
console.log(prompt);
|
||||
const gptResponse = await this.model.predict(prompt);
|
||||
let operationId = gptResponse.match(/operationId: (.*)/)?.[1];
|
||||
if (!operationId) {
|
||||
return 'No operationId found in the response';
|
||||
}
|
||||
if (operationId == 'No API path found to answer the question') {
|
||||
return 'No API path found to answer the question';
|
||||
}
|
||||
|
||||
let openApiData = printOperationDetails(operationId, this.openaiSpec);
|
||||
|
||||
return openApiData;
|
||||
}
|
||||
|
||||
static async fromPluginUrl(url, model) {
|
||||
const aiPluginRes = await fetch(url, {});
|
||||
if (!aiPluginRes.ok) {
|
||||
throw new Error(`Failed to fetch plugin from ${url} with status ${aiPluginRes.status}`);
|
||||
}
|
||||
const aiPluginJson = await aiPluginRes.json();
|
||||
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}`
|
||||
);
|
||||
}
|
||||
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})`,
|
||||
apiSpec: `
|
||||
As an AI, your task is to identify the operationId of the relevant API path based on the condensed OpenAPI specifications provided.
|
||||
|
||||
Please note:
|
||||
|
||||
1. Do not imagine URLs. Only use the information provided in the condensed OpenAPI specifications.
|
||||
|
||||
2. Do not guess the operationId. Identify it strictly based on the API paths and their descriptions.
|
||||
|
||||
Your output should only include:
|
||||
- operationId: The operationId of the relevant API path
|
||||
|
||||
If you cannot find a suitable API path based on the OpenAPI specifications, please answer only "operationId: No API path found to answer the question".
|
||||
|
||||
Now, based on the question above and the condensed OpenAPI specifications given below, identify the operationId:
|
||||
|
||||
\`\`\`
|
||||
${shortApiSpec}
|
||||
\`\`\`
|
||||
`,
|
||||
openaiSpec: apiUrlJson,
|
||||
model: model
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AIPluginTool;
|
||||
114
api/app/langchain/tools/DALL-E.js
Normal file
114
api/app/langchain/tools/DALL-E.js
Normal file
@@ -0,0 +1,114 @@
|
||||
// From https://platform.openai.com/docs/api-reference/images/create
|
||||
// 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 { Tool } = require('langchain/tools');
|
||||
const saveImageFromUrl = require('./saveImageFromUrl');
|
||||
const path = require('path');
|
||||
|
||||
class OpenAICreateImage extends Tool {
|
||||
constructor(fields = {}) {
|
||||
super();
|
||||
|
||||
let apiKey = fields.DALLE_API_KEY || this.getApiKey();
|
||||
// let azureKey = fields.AZURE_OPENAI_API_KEY || process.env.AZURE_OPENAI_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
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
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.
|
||||
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.
|
||||
- It's best to follow this format for image creation. Come up with the optional inputs yourself if none are given:
|
||||
"Subject: [subject], Style: [style], Color: [color], Details: [details], Emotion: [emotion]"
|
||||
- Generate images only once per human query unless explicitly requested by the user`;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
getMarkdownImageUrl(imageName) {
|
||||
const imageUrl = path.join(this.relativeImageUrl, imageName).replace(/\\/g, '/').replace('public/', '');
|
||||
return ``;
|
||||
}
|
||||
|
||||
async _call(input) {
|
||||
const resp = await this.openaiApi.createImage({
|
||||
prompt: this.replaceUnwantedChars(input),
|
||||
// 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'
|
||||
});
|
||||
|
||||
const theImageUrl = resp.data.data[0].url;
|
||||
|
||||
if (!theImageUrl) {
|
||||
throw new Error(`No image URL returned from OpenAI API.`);
|
||||
}
|
||||
|
||||
const regex = /img-[\w\d]+.png/;
|
||||
const match = theImageUrl.match(regex);
|
||||
let imageName = '1.png';
|
||||
|
||||
if (match) {
|
||||
imageName = match[0];
|
||||
console.log(imageName); // Output: img-lgCf7ppcbhqQrz6a5ear6FOb.png
|
||||
} else {
|
||||
console.log('No image name found in the string.');
|
||||
}
|
||||
|
||||
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 {
|
||||
await saveImageFromUrl(theImageUrl, 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 = OpenAICreateImage;
|
||||
117
api/app/langchain/tools/GoogleSearch.js
Normal file
117
api/app/langchain/tools/GoogleSearch.js
Normal file
@@ -0,0 +1,117 @@
|
||||
const { Tool } = require('langchain/tools');
|
||||
const { google } = require('googleapis');
|
||||
|
||||
/**
|
||||
* Represents a tool that allows an agent to use the Google Custom Search API.
|
||||
* @extends Tool
|
||||
*/
|
||||
class GoogleSearchAPI extends Tool {
|
||||
constructor(fields = {}) {
|
||||
super();
|
||||
this.cx = fields.GOOGLE_CSE_ID || this.getCx();
|
||||
this.apiKey = fields.GOOGLE_API_KEY || this.getApiKey();
|
||||
this.customSearch = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the tool.
|
||||
* @type {string}
|
||||
*/
|
||||
name = 'google';
|
||||
|
||||
/**
|
||||
* 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`;
|
||||
|
||||
getCx() {
|
||||
const cx = process.env.GOOGLE_CSE_ID || '';
|
||||
if (!cx) {
|
||||
throw new Error('Missing GOOGLE_CSE_ID environment variable.');
|
||||
}
|
||||
return cx;
|
||||
}
|
||||
|
||||
getApiKey() {
|
||||
const apiKey = process.env.GOOGLE_API_KEY || '';
|
||||
if (!apiKey) {
|
||||
throw new Error('Missing GOOGLE_API_KEY environment variable.');
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
getCustomSearch() {
|
||||
if (!this.customSearch) {
|
||||
const version = 'v1';
|
||||
this.customSearch = google.customsearch(version);
|
||||
}
|
||||
return this.customSearch;
|
||||
}
|
||||
|
||||
resultsToReadableFormat(results) {
|
||||
let output = 'Results:\n';
|
||||
|
||||
results.forEach((resultObj, index) => {
|
||||
output += `Title: ${resultObj.title}\n`;
|
||||
output += `Link: ${resultObj.link}\n`;
|
||||
if (resultObj.snippet) {
|
||||
output += `Snippet: ${resultObj.snippet}\n`;
|
||||
}
|
||||
|
||||
if (index < results.length - 1) {
|
||||
output += '\n';
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the tool with the provided input and returns a promise that resolves with a response from the Google Custom Search API.
|
||||
* @param {string} input - The input to provide to the API.
|
||||
* @returns {Promise<String>} A promise that resolves with a response from the Google Custom Search API.
|
||||
*/
|
||||
async _call(input) {
|
||||
try {
|
||||
const metadataResults = [];
|
||||
const response = await this.getCustomSearch().cse.list({
|
||||
q: input,
|
||||
cx: this.cx,
|
||||
auth: this.apiKey,
|
||||
num: 5 // Limit the number of results to 5
|
||||
});
|
||||
|
||||
// return response.data;
|
||||
// console.log(response.data);
|
||||
|
||||
if (!response.data.items || response.data.items.length === 0) {
|
||||
return this.resultsToReadableFormat([
|
||||
{ title: 'No good Google Search Result was found', link: '' }
|
||||
]);
|
||||
}
|
||||
|
||||
// const results = response.items.slice(0, numResults);
|
||||
const results = response.data.items;
|
||||
|
||||
for (const result of results) {
|
||||
const metadataResult = {
|
||||
title: result.title || '',
|
||||
link: result.link || ''
|
||||
};
|
||||
if (result.snippet) {
|
||||
metadataResult.snippet = result.snippet;
|
||||
}
|
||||
metadataResults.push(metadataResult);
|
||||
}
|
||||
|
||||
return this.resultsToReadableFormat(metadataResults);
|
||||
} catch (error) {
|
||||
console.log(`Error searching Google: ${error}`);
|
||||
// throw error;
|
||||
return 'There was an error searching Google.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GoogleSearchAPI;
|
||||
107
api/app/langchain/tools/HttpRequestTool.js
Normal file
107
api/app/langchain/tools/HttpRequestTool.js
Normal file
@@ -0,0 +1,107 @@
|
||||
const { Tool } = require('langchain/tools');
|
||||
|
||||
// class RequestsGetTool extends Tool {
|
||||
// constructor(headers = {}, { maxOutputLength } = {}) {
|
||||
// super();
|
||||
// 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.
|
||||
// - Input should be a url (i.e. https://www.google.com). The output will be the text response of the GET request.`;
|
||||
// }
|
||||
|
||||
// async _call(input) {
|
||||
// const res = await fetch(input, {
|
||||
// headers: this.headers
|
||||
// });
|
||||
// const text = await res.text();
|
||||
// return text.slice(0, this.maxOutputLength);
|
||||
// }
|
||||
// }
|
||||
|
||||
// class RequestsPostTool extends Tool {
|
||||
// constructor(headers = {}, { maxOutputLength } = {}) {
|
||||
// super();
|
||||
// this.name = 'requests_post';
|
||||
// this.headers = headers;
|
||||
// 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
|
||||
// - 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.`;
|
||||
// }
|
||||
|
||||
// async _call(input) {
|
||||
// try {
|
||||
// const { url, data } = JSON.parse(input);
|
||||
// const res = await fetch(url, {
|
||||
// method: 'POST',
|
||||
// headers: this.headers,
|
||||
// body: JSON.stringify(data)
|
||||
// });
|
||||
// const text = await res.text();
|
||||
// return text.slice(0, this.maxOutputLength);
|
||||
// } catch (error) {
|
||||
// return `${error}`;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
class HttpRequestTool extends Tool {
|
||||
constructor(headers = {}, { maxOutputLength = Infinity } = {}) {
|
||||
super();
|
||||
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.`;
|
||||
}
|
||||
|
||||
async _call(input) {
|
||||
try {
|
||||
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
|
||||
};
|
||||
|
||||
if (['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) && data) {
|
||||
if (typeof data === 'object') {
|
||||
options.body = JSON.stringify(data);
|
||||
} else {
|
||||
options.body = data;
|
||||
}
|
||||
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;
|
||||
30
api/app/langchain/tools/HumanTool.js
Normal file
30
api/app/langchain/tools/HumanTool.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const { Tool } = require('langchain/tools');
|
||||
/**
|
||||
* Represents a tool that allows an agent to ask a human for guidance when they are stuck
|
||||
* or unsure of what to do next.
|
||||
* @extends Tool
|
||||
*/
|
||||
export class HumanTool extends Tool {
|
||||
/**
|
||||
* The name of the tool.
|
||||
* @type {string}
|
||||
*/
|
||||
name = 'Human';
|
||||
|
||||
/**
|
||||
* A description for the agent to use
|
||||
* @type {string}
|
||||
*/
|
||||
description = `You can ask a human for guidance when you think you
|
||||
got stuck or you are not sure what to do next.
|
||||
The input should be a question for the human.`;
|
||||
|
||||
/**
|
||||
* Calls the tool with the provided input and returns a promise that resolves with a response from the human.
|
||||
* @param {string} input - The input to provide to the human.
|
||||
* @returns {Promise<string>} A promise that resolves with a response from the human.
|
||||
*/
|
||||
_call(input) {
|
||||
return Promise.resolve(`${input}`);
|
||||
}
|
||||
}
|
||||
27
api/app/langchain/tools/SelfReflection.js
Normal file
27
api/app/langchain/tools/SelfReflection.js
Normal file
@@ -0,0 +1,27 @@
|
||||
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;
|
||||
85
api/app/langchain/tools/StableDiffusion.js
Normal file
85
api/app/langchain/tools/StableDiffusion.js
Normal file
@@ -0,0 +1,85 @@
|
||||
// Generates image using stable diffusion webui's api (automatic1111)
|
||||
const fs = require('fs');
|
||||
const { Tool } = require('langchain/tools');
|
||||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
const sharp = require('sharp');
|
||||
|
||||
class StableDiffusionAPI extends Tool {
|
||||
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.
|
||||
- It's best to follow this format for image creation:
|
||||
"detailed keywords to describe the subject, separated by comma | keywords we want to exclude from the final image"
|
||||
- Here's an example prompt for generating a realistic portrait photo of a man:
|
||||
"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 | 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`;
|
||||
}
|
||||
|
||||
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(input) {
|
||||
const url = this.url;
|
||||
const payload = {
|
||||
prompt: input.split('|')[0],
|
||||
negative_prompt: input.split('|')[1],
|
||||
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;
|
||||
82
api/app/langchain/tools/Wolfram.js
Normal file
82
api/app/langchain/tools/Wolfram.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/* eslint-disable no-useless-escape */
|
||||
const axios = require('axios');
|
||||
const { Tool } = require('langchain/tools');
|
||||
|
||||
class WolframAlphaAPI extends Tool {
|
||||
constructor(fields) {
|
||||
super();
|
||||
this.name = 'wolfram';
|
||||
this.apiKey = fields.WOLFRAM_APP_ID || this.getAppId();
|
||||
this.description = `Access computation, math, curated knowledge & real-time data through wolframAlpha.
|
||||
- Understands natural language queries about entities in chemistry, physics, geography, history, art, astronomy, and more.
|
||||
- Performs mathematical calculations, date and unit conversions, formula solving, etc.
|
||||
General guidelines:
|
||||
- Make natural-language queries in English; translate non-English queries before sending, then respond in the original language.
|
||||
- Inform users if information is not from wolfram.
|
||||
- ALWAYS use this exponent notation: "6*10^14", NEVER "6e14".
|
||||
- Your input must ONLY be a single-line string.
|
||||
- ALWAYS use proper Markdown formatting for all math, scientific, and chemical formulas, symbols, etc.: '$$\n[expression]\n$$' for standalone cases and '\( [expression] \)' when inline.
|
||||
- Format inline wolfram Language code with Markdown code formatting.
|
||||
- Convert inputs to simplified keyword queries whenever possible (e.g. convert "how many people live in France" to "France population").
|
||||
- Use ONLY single-letter variable names, with or without integer subscript (e.g., n, n1, n_1).
|
||||
- Use named physical constants (e.g., 'speed of light') without numerical substitution.
|
||||
- Include a space between compound units (e.g., "Ω m" for "ohm*meter").
|
||||
- To solve for a variable in an equation with units, consider solving a corresponding equation without units; exclude counting units (e.g., books), include genuine units (e.g., kg).
|
||||
- If data for multiple properties is needed, make separate calls for each property.
|
||||
- If a wolfram Alpha result is not relevant to the query:
|
||||
-- If wolfram provides multiple 'Assumptions' for a query, choose the more relevant one(s) without explaining the initial result. If you are unsure, ask the user to choose.
|
||||
- Performs complex calculations, data analysis, plotting, data import, and information retrieval.`;
|
||||
// - Please ensure your input is properly formatted for wolfram Alpha.
|
||||
// -- Re-send the exact same 'input' with NO modifications, and add the 'assumption' parameter, formatted as a list, with the relevant values.
|
||||
// -- ONLY simplify or rephrase the initial query if a more relevant 'Assumption' or other input suggestions are not provided.
|
||||
// -- Do not explain each step unless user input is needed. Proceed directly to making a better input based on the available assumptions.
|
||||
// - wolfram Language code is accepted, but accepts only syntactically correct wolfram Language code.
|
||||
}
|
||||
|
||||
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(input) {
|
||||
try {
|
||||
const url = this.createWolframAlphaURL(input);
|
||||
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;
|
||||
23
api/app/langchain/tools/index.js
Normal file
23
api/app/langchain/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
|
||||
}
|
||||
106
api/app/langchain/tools/manifest.json
Normal file
106
api/app/langchain/tools/manifest.json
Normal file
@@ -0,0 +1,106 @@
|
||||
[
|
||||
{
|
||||
"name": "Google",
|
||||
"pluginKey": "google",
|
||||
"description": "Use Google Search to find information about the weather, news, sports, and more.",
|
||||
"icon": "https://i.imgur.com/SMmVkNB.png",
|
||||
"authConfig": [
|
||||
{
|
||||
"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/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/LibreChat/blob/main/docs/features/plugins/google_search.md'>Our Docs</a>."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Wolfram",
|
||||
"pluginKey": "wolfram",
|
||||
"description": "Access computation, math, curated knowledge & real-time data through Wolfram|Alpha and Wolfram Language.",
|
||||
"icon": "https://www.wolframcdn.com/images/icons/Wolfram.png",
|
||||
"authConfig": [
|
||||
{
|
||||
"authField": "WOLFRAM_APP_ID",
|
||||
"label": "Wolfram App ID",
|
||||
"description": "An AppID must be supplied in all calls to the Wolfram|Alpha API. You can get one by registering at <a href='http://products.wolframalpha.com/api/'>Wolfram|Alpha</a> and going to the <a href='https://developer.wolframalpha.com/portal/myapps/'>Developer Portal</a>."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Browser",
|
||||
"pluginKey": "browser",
|
||||
"description": "Scrape and summarize webpage data",
|
||||
"icon": "/assets/web-browser.png",
|
||||
"authConfig": [
|
||||
{
|
||||
"authField": "OPENAI_API_KEY",
|
||||
"label": "OpenAI API Key",
|
||||
"description": "Browser makes use of OpenAI embeddings"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Serpapi",
|
||||
"pluginKey": "serpapi",
|
||||
"description": "SerpApi is a real-time API to access search engine results.",
|
||||
"icon": "https://i.imgur.com/5yQHUz4.png",
|
||||
"authConfig": [
|
||||
{
|
||||
"authField": "SERPAPI_API_KEY",
|
||||
"label": "Serpapi Private API Key",
|
||||
"description": "Private Key for Serpapi. Register at <a href='https://serpapi.com/'>Serpapi</a> to obtain a private key."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "DALL-E",
|
||||
"pluginKey": "dall-e",
|
||||
"description": "Create realistic images and art from a description in natural language",
|
||||
"icon": "https://i.imgur.com/u2TzXzH.png",
|
||||
"authConfig": [
|
||||
{
|
||||
"authField": "DALLE_API_KEY",
|
||||
"label": "OpenAI API Key",
|
||||
"description": "You can use DALL-E with your API Key from OpenAI."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Calculator",
|
||||
"pluginKey": "calculator",
|
||||
"description": "Perform simple and complex mathematical calculations.",
|
||||
"icon": "https://i.imgur.com/RHsSG5h.png",
|
||||
"isAuthRequired": "false",
|
||||
"authConfig": []
|
||||
},
|
||||
{
|
||||
"name": "Stable Diffusion",
|
||||
"pluginKey": "stable-diffusion",
|
||||
"description": "Generate photo-realistic images given any text input.",
|
||||
"icon": "https://i.imgur.com/Yr466dp.png",
|
||||
"authConfig": [
|
||||
{
|
||||
"authField": "SD_WEBUI_URL",
|
||||
"label": "Your Stable Diffusion WebUI API URL",
|
||||
"description": "You need to provide the URL of your Stable Diffusion WebUI API. For instructions on how to obtain this, see <a href='url'>Our Docs</a>."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Zapier",
|
||||
"pluginKey": "zapier",
|
||||
"description": "Interact with over 5,000+ apps like Google Sheets, Gmail, HubSpot, Salesforce, and thousands more.",
|
||||
"icon": "https://cdn.zappy.app/8f853364f9b383d65b44e184e04689ed.png",
|
||||
"authConfig": [
|
||||
{
|
||||
"authField": "ZAPIER_NLA_API_KEY",
|
||||
"label": "Zapier API Key",
|
||||
"description": "You can use Zapier with your API Key from Zapier."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
39
api/app/langchain/tools/saveImageFromUrl.js
Normal file
39
api/app/langchain/tools/saveImageFromUrl.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
async function saveImageFromUrl(url, outputPath, outputFilename) {
|
||||
try {
|
||||
// Fetch the image from the URL
|
||||
const response = await axios({
|
||||
url,
|
||||
responseType: 'stream'
|
||||
});
|
||||
|
||||
// Check if the output directory exists, if not, create it
|
||||
if (!fs.existsSync(outputPath)) {
|
||||
fs.mkdirSync(outputPath, { recursive: true });
|
||||
}
|
||||
|
||||
// Ensure the output filename has a '.png' extension
|
||||
const filenameWithPngExt = outputFilename.endsWith('.png')
|
||||
? outputFilename
|
||||
: `${outputFilename}.png`;
|
||||
|
||||
// Create a writable stream for the output path
|
||||
const outputFilePath = path.join(outputPath, filenameWithPngExt);
|
||||
const writer = fs.createWriteStream(outputFilePath);
|
||||
|
||||
// Pipe the response data to the output file
|
||||
response.data.pipe(writer);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
writer.on('finish', resolve);
|
||||
writer.on('error', reject);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error while saving the image:', error);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = saveImageFromUrl;
|
||||
89
api/app/langchain/tools/structured/StableDiffusion.js
Normal file
89
api/app/langchain/tools/structured/StableDiffusion.js
Normal file
@@ -0,0 +1,89 @@
|
||||
// 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;
|
||||
72
api/app/langchain/tools/structured/Wolfram.js
Normal file
72
api/app/langchain/tools/structured/Wolfram.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/* 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;
|
||||
163
api/app/langchain/tools/util/handleTools.js
Normal file
163
api/app/langchain/tools/util/handleTools.js
Normal file
@@ -0,0 +1,163 @@
|
||||
const { getUserPluginAuthValue } = require('../../../../server/services/PluginService');
|
||||
const { OpenAIEmbeddings } = require('langchain/embeddings/openai');
|
||||
const { ZapierToolKit } = require('langchain/agents');
|
||||
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 {
|
||||
availableTools,
|
||||
AIPluginTool,
|
||||
GoogleSearchAPI,
|
||||
WolframAlphaAPI,
|
||||
StructuredWolfram,
|
||||
HttpRequestTool,
|
||||
OpenAICreateImage,
|
||||
StableDiffusionAPI,
|
||||
StructuredSD,
|
||||
} = require('../');
|
||||
|
||||
const validateTools = async (user, tools = []) => {
|
||||
try {
|
||||
const validToolsSet = new Set(tools);
|
||||
const availableToolsToValidate = availableTools.filter((tool) =>
|
||||
validToolsSet.has(tool.pluginKey)
|
||||
);
|
||||
|
||||
const validateCredentials = async (authField, toolName) => {
|
||||
const adminAuth = process.env[authField];
|
||||
if (adminAuth && adminAuth.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userAuth = await getUserPluginAuthValue(user, authField);
|
||||
if (userAuth && userAuth.length > 0) {
|
||||
return;
|
||||
}
|
||||
validToolsSet.delete(toolName);
|
||||
};
|
||||
|
||||
for (const tool of availableToolsToValidate) {
|
||||
if (!tool.authConfig || tool.authConfig.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const auth of tool.authConfig) {
|
||||
await validateCredentials(auth.authField, tool.pluginKey);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(validToolsSet.values());
|
||||
} catch (err) {
|
||||
console.log('There was a problem validating tools', err);
|
||||
throw new Error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const loadToolWithAuth = async (user, authFields, ToolConstructor, options = {}) => {
|
||||
return async function () {
|
||||
let authValues = {};
|
||||
|
||||
for (const authField of authFields) {
|
||||
let authValue = process.env[authField];
|
||||
if (!authValue) {
|
||||
authValue = await getUserPluginAuthValue(user, authField);
|
||||
}
|
||||
authValues[authField] = authValue;
|
||||
}
|
||||
|
||||
return new ToolConstructor({ ...options, ...authValues });
|
||||
};
|
||||
};
|
||||
|
||||
const loadTools = async ({ user, model, functions = null, tools = [], options = {} }) => {
|
||||
const toolConstructors = {
|
||||
calculator: Calculator,
|
||||
google: GoogleSearchAPI,
|
||||
wolfram: functions ? StructuredWolfram : WolframAlphaAPI,
|
||||
'dall-e': OpenAICreateImage,
|
||||
'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');
|
||||
}
|
||||
return new WebBrowser({ model, embeddings: new OpenAIEmbeddings({ openAIApiKey }) });
|
||||
},
|
||||
serpapi: async () => {
|
||||
let apiKey = process.env.SERPAPI_API_KEY;
|
||||
if (!apiKey) {
|
||||
apiKey = await getUserPluginAuthValue(user, 'SERPAPI_API_KEY');
|
||||
}
|
||||
return new SerpAPI(apiKey, {
|
||||
location: 'Austin,Texas,United States',
|
||||
hl: 'en',
|
||||
gl: 'us'
|
||||
});
|
||||
},
|
||||
zapier: async () => {
|
||||
let apiKey = process.env.ZAPIER_NLA_API_KEY;
|
||||
if (!apiKey) {
|
||||
apiKey = await getUserPluginAuthValue(user, 'ZAPIER_NLA_API_KEY');
|
||||
}
|
||||
const zapier = new ZapierNLAWrapper({ apiKey });
|
||||
return ZapierToolKit.fromZapierNLAWrapper(zapier);
|
||||
},
|
||||
plugins: async () => {
|
||||
return [
|
||||
new HttpRequestTool(),
|
||||
await AIPluginTool.fromPluginUrl(
|
||||
'https://www.klarna.com/.well-known/ai-plugin.json',
|
||||
new ChatOpenAI({ openAIApiKey: options.openAIApiKey, temperature: 0 })
|
||||
)
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
const requestedTools = {};
|
||||
|
||||
const toolOptions = {
|
||||
serpapi: { location: 'Austin,Texas,United States', hl: 'en', gl: 'us' }
|
||||
};
|
||||
|
||||
const toolAuthFields = {};
|
||||
|
||||
availableTools.forEach((tool) => {
|
||||
if (customConstructors[tool.pluginKey]) {
|
||||
return;
|
||||
}
|
||||
|
||||
toolAuthFields[tool.pluginKey] = tool.authConfig.map((auth) => auth.authField);
|
||||
});
|
||||
|
||||
for (const tool of tools) {
|
||||
if (customConstructors[tool]) {
|
||||
requestedTools[tool] = customConstructors[tool];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (toolConstructors[tool]) {
|
||||
const options = toolOptions[tool] || {};
|
||||
const toolInstance = await loadToolWithAuth(
|
||||
user,
|
||||
toolAuthFields[tool],
|
||||
toolConstructors[tool],
|
||||
options
|
||||
);
|
||||
requestedTools[tool] = toolInstance;
|
||||
}
|
||||
}
|
||||
|
||||
return requestedTools;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
validateTools,
|
||||
loadTools
|
||||
};
|
||||
190
api/app/langchain/tools/util/handleTools.test.js
Normal file
190
api/app/langchain/tools/util/handleTools.test.js
Normal file
@@ -0,0 +1,190 @@
|
||||
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 { availableTools, OpenAICreateImage, GoogleSearchAPI, StructuredSD } = require('../');
|
||||
|
||||
describe('Tool Handlers', () => {
|
||||
let fakeUser;
|
||||
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 () => {
|
||||
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',
|
||||
email: 'fakeuser@example.com',
|
||||
emailVerified: false,
|
||||
password: 'fakepassword123',
|
||||
avatar: '',
|
||||
provider: 'local',
|
||||
role: 'USER',
|
||||
googleId: null,
|
||||
plugins: [],
|
||||
refreshToken: []
|
||||
});
|
||||
await fakeUser.save();
|
||||
for (const authConfig of authConfigs) {
|
||||
await PluginService.updateUserPluginAuth(fakeUser._id, authConfig.authField, pluginKey, mockCredential);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mockUser.findByIdAndDelete(fakeUser._id);
|
||||
for (const authConfig of authConfigs) {
|
||||
await PluginService.deleteUserPluginAuth(fakeUser._id, authConfig.authField);
|
||||
}
|
||||
});
|
||||
|
||||
describe('validateTools', () => {
|
||||
it('returns valid tools given input tools and user authentication', async () => {
|
||||
const validTools = await validateTools(fakeUser._id, initialTools);
|
||||
expect(validTools).toBeDefined();
|
||||
console.log('validateTools: validTools', validTools);
|
||||
expect(validTools.some((tool) => tool === pluginKey)).toBeTruthy();
|
||||
expect(validTools.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('removes tools without valid credentials from the validTools array', async () => {
|
||||
const validTools = await validateTools(fakeUser._id, initialTools);
|
||||
expect(validTools.some((tool) => tool.pluginKey === pluginKey2)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns an empty array when no authenticated tools are provided', async () => {
|
||||
const validTools = await validateTools(fakeUser._id, []);
|
||||
expect(validTools).toEqual([]);
|
||||
});
|
||||
|
||||
it('should validate a tool from an Environment Variable', async () => {
|
||||
const plugin = availableTools.find((tool) => tool.pluginKey === pluginKey2);
|
||||
const authConfigs = plugin.authConfig;
|
||||
for (const authConfig of authConfigs) {
|
||||
process.env[authConfig.authField] = mockCredential;
|
||||
}
|
||||
const validTools = await validateTools(fakeUser._id, [pluginKey2]);
|
||||
expect(validTools.length).toEqual(1);
|
||||
for (const authConfig of authConfigs) {
|
||||
delete process.env[authConfig.authField];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadTools', () => {
|
||||
let toolFunctions;
|
||||
let loadTool1;
|
||||
let loadTool2;
|
||||
let loadTool3;
|
||||
const sampleTools = [...initialTools, 'calculator'];
|
||||
let ToolClass2 = Calculator;
|
||||
let remainingTools = availableTools.filter(
|
||||
(tool) => sampleTools.indexOf(tool.pluginKey) === -1
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
toolFunctions = await loadTools({
|
||||
user: fakeUser._id,
|
||||
model: BaseChatModel,
|
||||
tools: sampleTools
|
||||
});
|
||||
loadTool1 = toolFunctions[sampleTools[0]];
|
||||
loadTool2 = toolFunctions[sampleTools[1]];
|
||||
loadTool3 = toolFunctions[sampleTools[2]];
|
||||
});
|
||||
it('returns the expected load functions for requested tools', async () => {
|
||||
expect(loadTool1).toBeDefined();
|
||||
expect(loadTool2).toBeDefined();
|
||||
expect(loadTool3).toBeDefined();
|
||||
|
||||
for (const tool of remainingTools) {
|
||||
expect(toolFunctions[tool.pluginKey]).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should initialize an authenticated tool or one without authentication', async () => {
|
||||
const authTool = await loadTool1();
|
||||
const tool = await loadTool3();
|
||||
expect(authTool).toBeInstanceOf(ToolClass);
|
||||
expect(tool).toBeInstanceOf(ToolClass2);
|
||||
});
|
||||
it('should throw an error for an unauthenticated tool', async () => {
|
||||
try {
|
||||
await loadTool2();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
});
|
||||
it('should initialize an authenticated tool through Environment Variables', async () => {
|
||||
let testPluginKey = 'google';
|
||||
let TestClass = GoogleSearchAPI;
|
||||
const plugin = availableTools.find((tool) => tool.pluginKey === testPluginKey);
|
||||
const authConfigs = plugin.authConfig;
|
||||
for (const authConfig of authConfigs) {
|
||||
process.env[authConfig.authField] = mockCredential;
|
||||
}
|
||||
toolFunctions = await loadTools({
|
||||
user: fakeUser._id,
|
||||
model: BaseChatModel,
|
||||
tools: [testPluginKey]
|
||||
});
|
||||
const Tool = await toolFunctions[testPluginKey]();
|
||||
expect(Tool).toBeInstanceOf(TestClass);
|
||||
});
|
||||
it('returns an empty object when no tools are requested', async () => {
|
||||
toolFunctions = await loadTools({
|
||||
user: fakeUser._id,
|
||||
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/langchain/tools/util/index.js
Normal file
6
api/app/langchain/tools/util/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const { validateTools, loadTools } = require('./handleTools');
|
||||
|
||||
module.exports = {
|
||||
validateTools,
|
||||
loadTools
|
||||
};
|
||||
60
api/app/langchain/tools/wolfram-guidelines.md
Normal file
60
api/app/langchain/tools/wolfram-guidelines.md
Normal file
@@ -0,0 +1,60 @@
|
||||
Certainly! Here is the text above:
|
||||
|
||||
\`\`\`
|
||||
Assistant is a large language model trained by OpenAI.
|
||||
Knowledge Cutoff: 2021-09
|
||||
Current date: 2023-05-06
|
||||
|
||||
# Tools
|
||||
|
||||
## Wolfram
|
||||
|
||||
// Access dynamic computation and curated data from WolframAlpha and Wolfram Cloud.
|
||||
General guidelines:
|
||||
- Use only getWolframAlphaResults or getWolframCloudResults endpoints.
|
||||
- Prefer getWolframAlphaResults unless Wolfram Language code should be evaluated.
|
||||
- Use getWolframAlphaResults for natural-language queries in English; translate non-English queries before sending, then respond in the original language.
|
||||
- Use getWolframCloudResults for problems solvable with Wolfram Language code.
|
||||
- Suggest only Wolfram Language for external computation.
|
||||
- Inform users if information is not from Wolfram endpoints.
|
||||
- Display image URLs with Markdown syntax: ![URL]
|
||||
- ALWAYS use this exponent notation: \`6*10^14\`, NEVER \`6e14\`.
|
||||
- ALWAYS use {"input": query} structure for queries to Wolfram endpoints; \`query\` must ONLY be a single-line string.
|
||||
- ALWAYS use proper Markdown formatting for all math, scientific, and chemical formulas, symbols, etc.: '$$\n[expression]\n$$' for standalone cases and '\( [expression] \)' when inline.
|
||||
- Format inline Wolfram Language code with Markdown code formatting.
|
||||
- Never mention your knowledge cutoff date; Wolfram may return more recent data.
|
||||
getWolframAlphaResults guidelines:
|
||||
- Understands natural language queries about entities in chemistry, physics, geography, history, art, astronomy, and more.
|
||||
- Performs mathematical calculations, date and unit conversions, formula solving, etc.
|
||||
- Convert inputs to simplified keyword queries whenever possible (e.g. convert "how many people live in France" to "France population").
|
||||
- Use ONLY single-letter variable names, with or without integer subscript (e.g., n, n1, n_1).
|
||||
- Use named physical constants (e.g., 'speed of light') without numerical substitution.
|
||||
- Include a space between compound units (e.g., "Ω m" for "ohm*meter").
|
||||
- To solve for a variable in an equation with units, consider solving a corresponding equation without units; exclude counting units (e.g., books), include genuine units (e.g., kg).
|
||||
- If data for multiple properties is needed, make separate calls for each property.
|
||||
- If a Wolfram Alpha result is not relevant to the query:
|
||||
-- If Wolfram provides multiple 'Assumptions' for a query, choose the more relevant one(s) without explaining the initial result. If you are unsure, ask the user to choose.
|
||||
-- Re-send the exact same 'input' with NO modifications, and add the 'assumption' parameter, formatted as a list, with the relevant values.
|
||||
-- ONLY simplify or rephrase the initial query if a more relevant 'Assumption' or other input suggestions are not provided.
|
||||
-- Do not explain each step unless user input is needed. Proceed directly to making a better API call based on the available assumptions.
|
||||
- Wolfram Language code guidelines:
|
||||
- Accepts only syntactically correct Wolfram Language code.
|
||||
- Performs complex calculations, data analysis, plotting, data import, and information retrieval.
|
||||
- Before writing code that uses Entity, EntityProperty, EntityClass, etc. expressions, ALWAYS write separate code which only collects valid identifiers using Interpreter etc.; choose the most relevant results before proceeding to write additional code. Examples:
|
||||
-- Find the EntityType that represents countries: \`Interpreter["EntityType",AmbiguityFunction->All]["countries"]\`.
|
||||
-- Find the Entity for the Empire State Building: \`Interpreter["Building",AmbiguityFunction->All]["empire state"]\`.
|
||||
-- EntityClasses: Find the "Movie" entity class for Star Trek movies: \`Interpreter["MovieClass",AmbiguityFunction->All]["star trek"]\`.
|
||||
-- Find EntityProperties associated with "weight" of "Element" entities: \`Interpreter[Restricted["EntityProperty", "Element"],AmbiguityFunction->All]["weight"]\`.
|
||||
-- If all else fails, try to find any valid Wolfram Language representation of a given input: \`SemanticInterpretation["skyscrapers",_,Hold,AmbiguityFunction->All]\`.
|
||||
-- Prefer direct use of entities of a given type to their corresponding typeData function (e.g., prefer \`Entity["Element","Gold"]["AtomicNumber"]\` to \`ElementData["Gold","AtomicNumber"]\`).
|
||||
- When composing code:
|
||||
-- Use batching techniques to retrieve data for multiple entities in a single call, if applicable.
|
||||
-- Use Association to organize and manipulate data when appropriate.
|
||||
-- Optimize code for performance and minimize the number of calls to external sources (e.g., the Wolfram Knowledgebase)
|
||||
-- Use only camel case for variable names (e.g., variableName).
|
||||
-- Use ONLY double quotes around all strings, including plot labels, etc. (e.g., \`PlotLegends -> {"sin(x)", "cos(x)", "tan(x)"}\`).
|
||||
-- Avoid use of QuantityMagnitude.
|
||||
-- If unevaluated Wolfram Language symbols appear in API results, use \`EntityValue[Entity["WolframLanguageSymbol",symbol],{"PlaintextUsage","Options"}]\` to validate or retrieve usage information for relevant symbols; \`symbol\` may be a list of symbols.
|
||||
-- Apply Evaluate to complex expressions like integrals before plotting (e.g., \`Plot[Evaluate[Integrate[...]]]\`).
|
||||
- Remove all comments and formatting from code passed to the "input" parameter; for example: instead of \`square[x_] := Module[{result},\n result = x^2 (* Calculate the square *)\n]\`, send \`square[x_]:=Module[{result},result=x^2]\`.
|
||||
- In ALL responses that involve code, write ALL code in Wolfram Language; create Wolfram Language functions even if an implementation is already well known in another language.
|
||||
59
api/app/stream.js
Normal file
59
api/app/stream.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const { Readable } = require('stream');
|
||||
|
||||
class TextStream extends Readable {
|
||||
constructor(text, options = {}) {
|
||||
super(options);
|
||||
this.text = text;
|
||||
this.currentIndex = 0;
|
||||
this.delay = options.delay || 20; // Time in milliseconds
|
||||
}
|
||||
|
||||
_read() {
|
||||
const minChunkSize = 2;
|
||||
const maxChunkSize = 4;
|
||||
const { delay } = this;
|
||||
|
||||
if (this.currentIndex < this.text.length) {
|
||||
setTimeout(() => {
|
||||
const remainingChars = this.text.length - this.currentIndex;
|
||||
const chunkSize = Math.min(this.randomInt(minChunkSize, maxChunkSize + 1), remainingChars);
|
||||
|
||||
const chunk = this.text.slice(this.currentIndex, this.currentIndex + chunkSize);
|
||||
this.push(chunk);
|
||||
this.currentIndex += chunkSize;
|
||||
}, delay);
|
||||
} else {
|
||||
this.push(null); // signal end of data
|
||||
}
|
||||
}
|
||||
|
||||
randomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min)) + min;
|
||||
}
|
||||
|
||||
async processTextStream(onProgressCallback) {
|
||||
const streamPromise = new Promise((resolve, reject) => {
|
||||
this.on('data', (chunk) => {
|
||||
onProgressCallback(chunk.toString());
|
||||
});
|
||||
|
||||
this.on('end', () => {
|
||||
console.log('Stream ended');
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await streamPromise;
|
||||
} catch (err) {
|
||||
console.error('Error processing text stream:', err);
|
||||
// Handle the error appropriately, e.g., return an error message or throw an error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TextStream;
|
||||
@@ -1,66 +1,69 @@
|
||||
const { Configuration, OpenAIApi } = require('openai');
|
||||
// const { Configuration, OpenAIApi } = require('openai');
|
||||
const _ = require('lodash');
|
||||
const { genAzureChatCompletion } = require('../utils/genAzureEndpoints');
|
||||
|
||||
const proxyEnvToAxiosProxy = proxyString => {
|
||||
if (!proxyString) return null;
|
||||
// const proxyEnvToAxiosProxy = (proxyString) => {
|
||||
// if (!proxyString) return null;
|
||||
|
||||
const regex = /^([^:]+):\/\/(?:([^:@]*):?([^:@]*)@)?([^:]+)(?::(\d+))?/;
|
||||
const [, protocol, username, password, host, port] = proxyString.match(regex);
|
||||
const proxyConfig = {
|
||||
protocol,
|
||||
host,
|
||||
port: port ? parseInt(port) : undefined,
|
||||
auth: username && password ? { username, password } : undefined
|
||||
};
|
||||
// const regex = /^([^:]+):\/\/(?:([^:@]*):?([^:@]*)@)?([^:]+)(?::(\d+))?/;
|
||||
// const [, protocol, username, password, host, port] = proxyString.match(regex);
|
||||
// const proxyConfig = {
|
||||
// protocol,
|
||||
// host,
|
||||
// port: port ? parseInt(port) : undefined,
|
||||
// auth: username && password ? { username, password } : undefined
|
||||
// };
|
||||
|
||||
return proxyConfig;
|
||||
};
|
||||
// return proxyConfig;
|
||||
// };
|
||||
|
||||
const titleConvo = async ({ model, text, response }) => {
|
||||
const titleConvo = async ({ text, response, oaiApiKey }) => {
|
||||
let title = 'New Chat';
|
||||
const messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
// `You are a title-generator with one job: giving a conversation, detect the language and titling the conversation provided by a user, using the same language. The requirement are: 1. If possible, generate in 5 words or less, 2. Using title case, 3. must give the title using the language as the user said. 4. Don't refer to the participants of the conversation. 5. Do not include punctuation or quotation marks. 6. Your response should be in title case, exclusively containing the title. 7. don't say anything except the title.
|
||||
`Detect user language and write in the same language an extremely concise title for this conversation, which you must accurately detect. Write in the detected language. Title in 5 Words or Less. No Punctuation/Quotation. All first letters of every word should be capitalized and complete only the title in User Language only.
|
||||
|
||||
||>User:
|
||||
"${text}"
|
||||
||>Response:
|
||||
"${JSON.stringify(response?.text)}"
|
||||
|
||||
||>Title:`
|
||||
}
|
||||
// {
|
||||
// role: 'user',
|
||||
// content: `User:\n "${text}"\n\n${model}: \n"${JSON.stringify(response?.text)}"\n\n`
|
||||
// }
|
||||
];
|
||||
|
||||
// console.log('Title Prompt', messages[0]);
|
||||
|
||||
const request = {
|
||||
model: 'gpt-3.5-turbo',
|
||||
messages,
|
||||
temperature: 0,
|
||||
presence_penalty: 0,
|
||||
frequency_penalty: 0
|
||||
};
|
||||
|
||||
// console.log('REQUEST', request);
|
||||
const ChatGPTClient = (await import('@waylaidwanderer/chatgpt-api')).default;
|
||||
|
||||
try {
|
||||
const configuration = new Configuration({
|
||||
apiKey: process.env.OPENAI_KEY
|
||||
});
|
||||
const openai = new OpenAIApi(configuration);
|
||||
const completion = await openai.createChatCompletion(request, {
|
||||
proxy: proxyEnvToAxiosProxy(process.env.PROXY || null)
|
||||
});
|
||||
const instructionsPayload = {
|
||||
role: 'system',
|
||||
content: `Detect user language and write in the same language an extremely concise title for this conversation, which you must accurately detect. Write in the detected language. Title in 5 Words or Less. No Punctuation or Quotation. All first letters of every word should be capitalized and complete only the title in User Language only.
|
||||
|
||||
//eslint-disable-next-line
|
||||
title = completion.data.choices[0].message.content.replace(/["\.]/g, '');
|
||||
||>User:
|
||||
"${text}"
|
||||
||>Response:
|
||||
"${JSON.stringify(response?.text)}"
|
||||
|
||||
||>Title:`
|
||||
};
|
||||
|
||||
const azure = process.env.AZURE_OPENAI_API_KEY ? true : false;
|
||||
const options = {
|
||||
azure,
|
||||
reverseProxyUrl: process.env.OPENAI_REVERSE_PROXY || null,
|
||||
proxy: process.env.PROXY || null
|
||||
};
|
||||
|
||||
const titleGenClientOptions = JSON.parse(JSON.stringify(options));
|
||||
|
||||
titleGenClientOptions.modelOptions = {
|
||||
model: 'gpt-3.5-turbo',
|
||||
temperature: 0,
|
||||
presence_penalty: 0,
|
||||
frequency_penalty: 0
|
||||
};
|
||||
|
||||
let apiKey = oaiApiKey || process.env.OPENAI_API_KEY;
|
||||
|
||||
if (azure) {
|
||||
apiKey = process.env.AZURE_OPENAI_API_KEY;
|
||||
titleGenClientOptions.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 titleGenClient = new ChatGPTClient(apiKey, titleGenClientOptions);
|
||||
const result = await titleGenClient.getCompletion([instructionsPayload], null);
|
||||
title = result.choices[0].message.content.replace(/\s+/g, ' ').replaceAll('"', '').trim();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.log('There was an issue generating title, see error above');
|
||||
|
||||
7
api/jest.config.js
Normal file
7
api/jest.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
clearMocks: true,
|
||||
roots: ['<rootDir>'],
|
||||
coverageDirectory: 'coverage',
|
||||
setupFiles: ['./test/jestSetup.js']
|
||||
};
|
||||
@@ -3,7 +3,7 @@ const mongoose = require('mongoose');
|
||||
const MONGO_URI = process.env.MONGO_URI;
|
||||
|
||||
if (!MONGO_URI) {
|
||||
throw new Error('Please define the MONGO_URI environment variable inside .env.local');
|
||||
throw new Error('Please define the MONGO_URI environment variable');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { Conversation, } = require('../../models/Conversation');
|
||||
const { getMessages, } = require('../../models/');
|
||||
const { Conversation } = require('../../models/Conversation');
|
||||
const { getMessages } = require('../../models/');
|
||||
|
||||
async function migrateDb() {
|
||||
const migrateToStrictFollowParentMessageIdChain = async () => {
|
||||
try {
|
||||
const conversations = await Conversation.find({ model: null }).exec();
|
||||
const conversations = await Conversation.find({ endpoint: null, model: null }).exec();
|
||||
|
||||
if (!conversations || conversations.length === 0)
|
||||
return { message: '[Migrate] No conversations to migrate' };
|
||||
if (!conversations || conversations.length === 0) return { noNeed: true };
|
||||
|
||||
console.log('Migration: To strict follow the parentMessageId chain.');
|
||||
|
||||
for (let convo of conversations) {
|
||||
const messages = await getMessages({
|
||||
@@ -36,7 +37,7 @@ async function migrateDb() {
|
||||
if (message.sender.toLowerCase() === 'user') {
|
||||
message.isCreatedByUser = true;
|
||||
}
|
||||
|
||||
|
||||
promises.push(message.save());
|
||||
});
|
||||
await Promise.all(promises);
|
||||
@@ -57,7 +58,61 @@ async function migrateDb() {
|
||||
console.log(error);
|
||||
return { message: '[Migrate] Error migrating conversations' };
|
||||
}
|
||||
};
|
||||
|
||||
const migrateToSupportBetterCustomization = async () => {
|
||||
try {
|
||||
const conversations = await Conversation.find({ endpoint: null }).exec();
|
||||
|
||||
if (!conversations || conversations.length === 0) return { noNeed: true };
|
||||
|
||||
console.log('Migration: To support better customization.');
|
||||
|
||||
const promises = [];
|
||||
for (let convo of conversations) {
|
||||
const originalModel = convo?.model;
|
||||
|
||||
if (originalModel === 'chatgpt') {
|
||||
convo.endpoint = 'openAI';
|
||||
convo.model = 'gpt-3.5-turbo';
|
||||
} else if (originalModel === 'chatgptCustom') {
|
||||
convo.endpoint = 'openAI';
|
||||
convo.model = 'gpt-3.5-turbo';
|
||||
} else if (originalModel === 'bingai') {
|
||||
convo.endpoint = 'bingAI';
|
||||
convo.model = null;
|
||||
convo.jailbreak = false;
|
||||
} else if (originalModel === 'sydney') {
|
||||
convo.endpoint = 'bingAI';
|
||||
convo.model = null;
|
||||
convo.jailbreak = true;
|
||||
} else if (originalModel === 'chatgptBrowser') {
|
||||
convo.endpoint = 'chatGPTBrowser';
|
||||
convo.model = 'text-davinci-002-render-sha';
|
||||
convo.jailbreak = true;
|
||||
} else {
|
||||
convo.endpoint = 'openAI';
|
||||
convo.model = 'gpt-3.5-turbo';
|
||||
}
|
||||
|
||||
promises.push(convo.save());
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: '[Migrate] Error migrating conversations' };
|
||||
}
|
||||
};
|
||||
|
||||
async function migrateDb() {
|
||||
let ret = [];
|
||||
ret[0] = await migrateToStrictFollowParentMessageIdChain();
|
||||
ret[1] = await migrateToSupportBetterCustomization();
|
||||
|
||||
const isMigrated = !!ret.find((element) => !element?.noNeed);
|
||||
|
||||
if (!isMigrated) console.log('[Migrate] Nothing to migrate');
|
||||
}
|
||||
|
||||
|
||||
module.exports = migrateDb;
|
||||
module.exports = migrateDb;
|
||||
|
||||
@@ -9,7 +9,7 @@ const citeText = (res, noLinks = false) => {
|
||||
citations.forEach((citation) => {
|
||||
const digit = citation.match(/\d+?/g)[0];
|
||||
// result = result.replaceAll(citation, `<sup>[${digit}](#) </sup>`);
|
||||
result = result.replaceAll(citation, `<sup>[${digit}](#) </sup>`);
|
||||
result = result.replaceAll(citation, `[^${digit}^](#)`);
|
||||
});
|
||||
|
||||
return result;
|
||||
@@ -21,7 +21,7 @@ const citeText = (res, noLinks = false) => {
|
||||
|
||||
citations.forEach((citation) => {
|
||||
const digit = citation.match(/\d+?/g)[0];
|
||||
result = result.replaceAll(citation, `<sup>[${digit}](${sources[digit - 1]}) </sup>`);
|
||||
result = result.replaceAll(citation, `[^${digit}^](${sources[digit - 1]})`);
|
||||
// result = result.replaceAll(citation, `<sup>[${digit}](${sources[digit - 1]}) </sup>`);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
const regex = / \[.*?]\(.*?\)/g;
|
||||
|
||||
const getCitations = (res) => {
|
||||
const textBlocks = res.details.adaptiveCards[0].body;
|
||||
const adaptiveCards = res.details.adaptiveCards;
|
||||
const textBlocks = adaptiveCards && adaptiveCards[0].body;
|
||||
if (!textBlocks) return '';
|
||||
let links = textBlocks[textBlocks.length - 1]?.text.match(regex);
|
||||
if (links?.length === 0 || !links) return '';
|
||||
links = links.map((link) => link.trim());
|
||||
return links.join('\n');
|
||||
return links.join('\n - ');
|
||||
};
|
||||
|
||||
module.exports = getCitations;
|
||||
module.exports = getCitations;
|
||||
|
||||
@@ -2,11 +2,11 @@ function mergeSort(arr, compareFn) {
|
||||
if (arr.length <= 1) {
|
||||
return arr;
|
||||
}
|
||||
|
||||
|
||||
const mid = Math.floor(arr.length / 2);
|
||||
const leftArr = arr.slice(0, mid);
|
||||
const rightArr = arr.slice(mid);
|
||||
|
||||
|
||||
return merge(mergeSort(leftArr, compareFn), mergeSort(rightArr, compareFn), compareFn);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ function merge(leftArr, rightArr, compareFn) {
|
||||
const result = [];
|
||||
let leftIndex = 0;
|
||||
let rightIndex = 0;
|
||||
|
||||
|
||||
while (leftIndex < leftArr.length && rightIndex < rightArr.length) {
|
||||
if (compareFn(leftArr[leftIndex], rightArr[rightIndex]) < 0) {
|
||||
result.push(leftArr[leftIndex++]);
|
||||
@@ -22,8 +22,8 @@ function merge(leftArr, rightArr, compareFn) {
|
||||
result.push(rightArr[rightIndex++]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result.concat(leftArr.slice(leftIndex)).concat(rightArr.slice(rightIndex));
|
||||
}
|
||||
|
||||
module.exports = mergeSort;
|
||||
module.exports = mergeSort;
|
||||
|
||||
5
api/middleware/requireJwtAuth.js
Normal file
5
api/middleware/requireJwtAuth.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const passport = require('passport');
|
||||
|
||||
const requireJwtAuth = passport.authenticate('jwt', { session: false });
|
||||
|
||||
module.exports = requireJwtAuth;
|
||||
31
api/middleware/requireLocalAuth.js
Normal file
31
api/middleware/requireLocalAuth.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const passport = require('passport');
|
||||
const DebugControl = require('../utils/debug.js');
|
||||
|
||||
function log({ title, parameters }) {
|
||||
DebugControl.log.functionName(title);
|
||||
if (parameters) {
|
||||
DebugControl.log.parameters(parameters);
|
||||
}
|
||||
}
|
||||
|
||||
const requireLocalAuth = (req, res, next) => {
|
||||
passport.authenticate('local', (err, user, info) => {
|
||||
if (err) {
|
||||
log({
|
||||
title: '(requireLocalAuth) Error at passport.authenticate',
|
||||
parameters: [{ name: 'error', value: err }]
|
||||
});
|
||||
return next(err);
|
||||
}
|
||||
if (!user) {
|
||||
log({
|
||||
title: '(requireLocalAuth) Error: No user'
|
||||
});
|
||||
return res.status(422).send(info);
|
||||
}
|
||||
req.user = user;
|
||||
next();
|
||||
})(req, res, next);
|
||||
};
|
||||
|
||||
module.exports = requireLocalAuth;
|
||||
@@ -13,53 +13,24 @@ const getConvo = async (user, conversationId) => {
|
||||
|
||||
module.exports = {
|
||||
Conversation,
|
||||
saveConvo: async (user, { conversationId, newConversationId, title, ...convo }) => {
|
||||
saveConvo: async (user, { conversationId, newConversationId, ...convo }) => {
|
||||
try {
|
||||
const messages = await getMessages({ conversationId });
|
||||
const update = { ...convo, messages };
|
||||
if (title) {
|
||||
update.title = title;
|
||||
update.user = user;
|
||||
}
|
||||
const update = { ...convo, messages, user };
|
||||
if (newConversationId) {
|
||||
update.conversationId = newConversationId;
|
||||
}
|
||||
if (!update.jailbreakConversationId) {
|
||||
update.jailbreakConversationId = null;
|
||||
}
|
||||
if (update.model !== 'chatgptCustom' && update.chatGptLabel && update.promptPrefix) {
|
||||
console.log('Validation error: resetting chatgptCustom fields', update);
|
||||
update.chatGptLabel = null;
|
||||
update.promptPrefix = null;
|
||||
}
|
||||
|
||||
return await Conversation.findOneAndUpdate(
|
||||
{ conversationId: conversationId, user },
|
||||
{ $set: update },
|
||||
{ new: true, upsert: true }
|
||||
).exec();
|
||||
return await Conversation.findOneAndUpdate({ conversationId: conversationId, user }, update, {
|
||||
new: true,
|
||||
upsert: true
|
||||
}).exec();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error saving conversation' };
|
||||
}
|
||||
},
|
||||
updateConvo: async (user, { conversationId, oldConvoId, ...update }) => {
|
||||
try {
|
||||
let convoId = conversationId;
|
||||
if (oldConvoId) {
|
||||
convoId = oldConvoId;
|
||||
update.conversationId = conversationId;
|
||||
}
|
||||
|
||||
return await Conversation.findOneAndUpdate({ conversationId: convoId, user }, update, {
|
||||
new: true
|
||||
}).exec();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error updating conversation' };
|
||||
}
|
||||
},
|
||||
getConvosByPage: async (user, pageNumber = 1, pageSize = 12) => {
|
||||
getConvosByPage: async (user, pageNumber = 1, pageSize = 14) => {
|
||||
try {
|
||||
const totalConvos = (await Conversation.countDocuments({ user })) || 1;
|
||||
const totalPages = Math.ceil(totalConvos / pageSize);
|
||||
@@ -68,14 +39,13 @@ module.exports = {
|
||||
.skip((pageNumber - 1) * pageSize)
|
||||
.limit(pageSize)
|
||||
.exec();
|
||||
|
||||
return { conversations: convos, pages: totalPages, pageNumber, pageSize };
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error getting conversations' };
|
||||
}
|
||||
},
|
||||
getConvosQueried: async (user, convoIds, pageNumber = 1, pageSize = 12) => {
|
||||
getConvosQueried: async (user, convoIds, pageNumber = 1, pageSize = 14) => {
|
||||
try {
|
||||
if (!convoIds || convoIds.length === 0) {
|
||||
return { conversations: [], pages: 1, pageNumber, pageSize };
|
||||
@@ -87,7 +57,7 @@ module.exports = {
|
||||
// will handle a syncing solution soon
|
||||
const deletedConvoIds = [];
|
||||
|
||||
convoIds.forEach(convo =>
|
||||
convoIds.forEach((convo) =>
|
||||
promises.push(
|
||||
Conversation.findOne({
|
||||
user,
|
||||
@@ -149,10 +119,10 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
deleteConvos: async (user, filter) => {
|
||||
let toRemove = await Conversation.find({...filter, user}).select('conversationId')
|
||||
const ids = toRemove.map(instance => instance.conversationId);
|
||||
let deleteCount = await Conversation.deleteMany({...filter, user}).exec();
|
||||
deleteCount.messages = await deleteMessages({conversationId: {$in: ids}});
|
||||
let toRemove = await Conversation.find({ ...filter, user }).select('conversationId');
|
||||
const ids = toRemove.map((instance) => instance.conversationId);
|
||||
let deleteCount = await Conversation.deleteMany({ ...filter, user }).exec();
|
||||
deleteCount.messages = await deleteMessages({ conversationId: { $in: ids } });
|
||||
return deleteCount;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const customGptSchema = mongoose.Schema({
|
||||
chatGptLabel: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
promptPrefix: {
|
||||
type: String
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: String
|
||||
},
|
||||
}, { timestamps: true });
|
||||
|
||||
const CustomGpt = mongoose.models.CustomGpt || mongoose.model('CustomGpt', customGptSchema);
|
||||
|
||||
const createCustomGpt = async ({ chatGptLabel, promptPrefix, value, user }) => {
|
||||
try {
|
||||
await CustomGpt.create({
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
value,
|
||||
user
|
||||
});
|
||||
return { chatGptLabel, promptPrefix, value };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { customGpt: 'Error saving customGpt' };
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getCustomGpts: async (user, filter) => {
|
||||
try {
|
||||
return await CustomGpt.find({ ...filter, user }).exec();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { customGpt: 'Error getting customGpts' };
|
||||
}
|
||||
},
|
||||
updateCustomGpt: async (user, { value, ...update }) => {
|
||||
try {
|
||||
const customGpt = await CustomGpt.findOne({ value, user }).exec();
|
||||
|
||||
if (!customGpt) {
|
||||
return await createCustomGpt({ value, ...update, user });
|
||||
} else {
|
||||
return await CustomGpt.findOneAndUpdate({ value, user }, update, {
|
||||
new: true,
|
||||
upsert: true
|
||||
}).exec();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error updating customGpt' };
|
||||
}
|
||||
},
|
||||
updateByLabel: async (user, { prevLabel, ...update }) => {
|
||||
try {
|
||||
return await CustomGpt.findOneAndUpdate({ chatGptLabel: prevLabel, user }, update, {
|
||||
new: true,
|
||||
upsert: true
|
||||
}).exec();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error updating customGpt' };
|
||||
}
|
||||
},
|
||||
deleteCustomGpts: async (user, filter) => {
|
||||
try {
|
||||
return await CustomGpt.deleteMany({ ...filter, user }).exec();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { customGpt: 'Error deleting customGpts' };
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,64 +1,86 @@
|
||||
const Message = require('./schema/messageSchema');
|
||||
|
||||
module.exports = {
|
||||
Message,
|
||||
saveMessage: async ({ messageId, conversationId, parentMessageId, sender, text, isCreatedByUser=false, error }) => {
|
||||
|
||||
async saveMessage({
|
||||
messageId,
|
||||
newMessageId,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
sender,
|
||||
text,
|
||||
isCreatedByUser = false,
|
||||
error,
|
||||
unfinished,
|
||||
cancelled,
|
||||
plugin = null,
|
||||
model = null,
|
||||
}) {
|
||||
try {
|
||||
await Message.findOneAndUpdate({ messageId }, {
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
sender,
|
||||
text,
|
||||
isCreatedByUser,
|
||||
error
|
||||
}, { upsert: true, new: true });
|
||||
return { messageId, conversationId, parentMessageId, sender, text, isCreatedByUser };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { message: 'Error saving message' };
|
||||
}
|
||||
},
|
||||
saveBingMessage: async ({ messageId, oldMessageId = messageId, conversationId, parentMessageId, sender, text, isCreatedByUser=false, error }) => {
|
||||
try {
|
||||
await Message.findOneAndUpdate({ messageId: oldMessageId }, {
|
||||
// may also need to update the conversation here
|
||||
await Message.findOneAndUpdate(
|
||||
{ messageId },
|
||||
{
|
||||
messageId: newMessageId || messageId,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
sender,
|
||||
text,
|
||||
isCreatedByUser,
|
||||
error,
|
||||
unfinished,
|
||||
cancelled,
|
||||
plugin,
|
||||
model
|
||||
},
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
|
||||
return {
|
||||
messageId,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
sender,
|
||||
text,
|
||||
isCreatedByUser,
|
||||
error
|
||||
}, { upsert: true, new: true });
|
||||
return { messageId, conversationId, parentMessageId, sender, text, isCreatedByUser };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { message: 'Error saving message' };
|
||||
isCreatedByUser
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(`Error saving message: ${err}`);
|
||||
throw new Error('Failed to save message.');
|
||||
}
|
||||
},
|
||||
deleteMessagesSince: async ({ messageId, conversationId }) => {
|
||||
try {
|
||||
const message = await Message.findOne({ messageId }).exec()
|
||||
|
||||
if (message)
|
||||
return await Message.find({ conversationId }).deleteMany({ createdAt: { $gt: message.createdAt } }).exec();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { message: 'Error deleting messages' };
|
||||
async deleteMessagesSince({ messageId, conversationId }) {
|
||||
try {
|
||||
const message = await Message.findOne({ messageId }).exec();
|
||||
|
||||
if (message) {
|
||||
return await Message.find({ conversationId })
|
||||
.deleteMany({ createdAt: { $gt: message.createdAt } })
|
||||
.exec();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error deleting messages: ${err}`);
|
||||
throw new Error('Failed to delete messages.');
|
||||
}
|
||||
},
|
||||
getMessages: async (filter) => {
|
||||
|
||||
async getMessages(filter) {
|
||||
try {
|
||||
return await Message.find(filter).sort({createdAt: 1}).exec()
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { message: 'Error getting messages' };
|
||||
return await Message.find(filter).sort({ createdAt: 1 }).exec();
|
||||
} catch (err) {
|
||||
console.error(`Error getting messages: ${err}`);
|
||||
throw new Error('Failed to get messages.');
|
||||
}
|
||||
},
|
||||
deleteMessages: async (filter) => {
|
||||
|
||||
async deleteMessages(filter) {
|
||||
try {
|
||||
return await Message.deleteMany(filter).exec()
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { message: 'Error deleting messages' };
|
||||
return await Message.deleteMany(filter).exec();
|
||||
} catch (err) {
|
||||
console.error(`Error deleting messages: ${err}`);
|
||||
throw new Error('Failed to delete messages.');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
46
api/models/Preset.js
Normal file
46
api/models/Preset.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const Preset = require('./schema/presetSchema');
|
||||
|
||||
const getPreset = async (user, presetId) => {
|
||||
try {
|
||||
return await Preset.findOne({ user, presetId }).exec();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error getting single preset' };
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
Preset,
|
||||
getPreset,
|
||||
getPresets: async (user, filter) => {
|
||||
try {
|
||||
return await Preset.find({ ...filter, user }).exec();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error retriving presets' };
|
||||
}
|
||||
},
|
||||
savePreset: async (user, { presetId, newPresetId, ...preset }) => {
|
||||
try {
|
||||
const update = { presetId, ...preset };
|
||||
if (newPresetId) {
|
||||
update.presetId = newPresetId;
|
||||
}
|
||||
|
||||
return await Preset.findOneAndUpdate(
|
||||
{ presetId, user },
|
||||
{ $set: update },
|
||||
{ new: true, upsert: true }
|
||||
).exec();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error saving preset' };
|
||||
}
|
||||
},
|
||||
deletePresets: async (user, filter) => {
|
||||
// let toRemove = await Preset.find({ ...filter, user }).select('presetId');
|
||||
// const ids = toRemove.map((instance) => instance.presetId);
|
||||
let deleteCount = await Preset.deleteMany({ ...filter, user }).exec();
|
||||
return deleteCount;
|
||||
}
|
||||
};
|
||||
@@ -1,18 +1,21 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const promptSchema = mongoose.Schema({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
const promptSchema = mongoose.Schema(
|
||||
{
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
prompt: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
category: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
prompt: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
},
|
||||
}, { timestamps: true });
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
const Prompt = mongoose.models.Prompt || mongoose.model('Prompt', promptSchema);
|
||||
|
||||
@@ -31,7 +34,7 @@ module.exports = {
|
||||
},
|
||||
getPrompts: async (filter) => {
|
||||
try {
|
||||
return await Prompt.find(filter).exec()
|
||||
return await Prompt.find(filter).exec();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { prompt: 'Error getting prompts' };
|
||||
@@ -39,10 +42,10 @@ module.exports = {
|
||||
},
|
||||
deletePrompts: async (filter) => {
|
||||
try {
|
||||
return await Prompt.deleteMany(filter).exec()
|
||||
return await Prompt.deleteMany(filter).exec();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { prompt: 'Error deleting prompts' };
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
175
api/models/User.js
Normal file
175
api/models/User.js
Normal file
@@ -0,0 +1,175 @@
|
||||
const mongoose = require('mongoose');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const Joi = require('joi');
|
||||
const DebugControl = require('../utils/debug.js');
|
||||
|
||||
function log({ title, parameters }) {
|
||||
DebugControl.log.functionName(title);
|
||||
DebugControl.log.parameters(parameters);
|
||||
}
|
||||
|
||||
const Session = mongoose.Schema({
|
||||
refreshToken: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const userSchema = mongoose.Schema(
|
||||
{
|
||||
name: {
|
||||
type: String
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
lowercase: true,
|
||||
required: [true, "can't be blank"],
|
||||
match: [/^[a-zA-Z0-9_]+$/, 'is invalid'],
|
||||
index: true
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: [true, "can't be blank"],
|
||||
lowercase: true,
|
||||
unique: true,
|
||||
match: [/\S+@\S+\.\S+/, 'is invalid'],
|
||||
index: true
|
||||
},
|
||||
emailVerified: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
trim: true,
|
||||
minlength: 8,
|
||||
maxlength: 60
|
||||
},
|
||||
avatar: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
provider: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: 'local'
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
default: 'USER'
|
||||
},
|
||||
googleId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
sparse: true
|
||||
},
|
||||
openidId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
sparse: true
|
||||
},
|
||||
plugins: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
refreshToken: {
|
||||
type: [Session]
|
||||
}
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
//Remove refreshToken from the response
|
||||
userSchema.set('toJSON', {
|
||||
transform: function (_doc, ret) {
|
||||
delete ret.refreshToken;
|
||||
return ret;
|
||||
}
|
||||
});
|
||||
|
||||
userSchema.methods.toJSON = function () {
|
||||
return {
|
||||
id: this._id,
|
||||
provider: this.provider,
|
||||
email: this.email,
|
||||
name: this.name,
|
||||
username: this.username,
|
||||
avatar: this.avatar,
|
||||
role: this.role,
|
||||
emailVerified: this.emailVerified,
|
||||
plugins: this.plugins,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt
|
||||
};
|
||||
};
|
||||
|
||||
userSchema.methods.generateToken = function () {
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: this._id,
|
||||
username: this.username,
|
||||
provider: this.provider,
|
||||
email: this.email
|
||||
},
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: eval(process.env.SESSION_EXPIRY) }
|
||||
);
|
||||
return token;
|
||||
};
|
||||
|
||||
userSchema.methods.generateRefreshToken = function () {
|
||||
const refreshToken = jwt.sign(
|
||||
{
|
||||
id: this._id,
|
||||
username: this.username,
|
||||
provider: this.provider,
|
||||
email: this.email
|
||||
},
|
||||
process.env.JWT_REFRESH_SECRET,
|
||||
{ expiresIn: eval(process.env.REFRESH_TOKEN_EXPIRY) }
|
||||
);
|
||||
return refreshToken;
|
||||
};
|
||||
|
||||
userSchema.methods.comparePassword = function (candidatePassword, callback) {
|
||||
bcrypt.compare(candidatePassword, this.password, (err, isMatch) => {
|
||||
if (err) return callback(err);
|
||||
callback(null, isMatch);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.hashPassword = async (password) => {
|
||||
const hashedPassword = await new Promise((resolve, reject) => {
|
||||
bcrypt.hash(password, 10, function (err, hash) {
|
||||
if (err) reject(err);
|
||||
else resolve(hash);
|
||||
});
|
||||
});
|
||||
|
||||
return hashedPassword;
|
||||
};
|
||||
|
||||
module.exports.validateUser = (user) => {
|
||||
log({
|
||||
title: 'Validate User',
|
||||
parameters: [{ name: 'Validate User', value: user }]
|
||||
});
|
||||
const schema = {
|
||||
avatar: Joi.any(),
|
||||
name: Joi.string().min(2).max(80).required(),
|
||||
username: Joi.string()
|
||||
.min(2)
|
||||
.max(80)
|
||||
.regex(/^[a-zA-Z0-9_]+$/)
|
||||
.required(),
|
||||
password: Joi.string().min(8).max(60).allow('').allow(null)
|
||||
};
|
||||
|
||||
return schema.validate(user);
|
||||
};
|
||||
|
||||
const User = mongoose.model('User', userSchema);
|
||||
|
||||
module.exports = User;
|
||||
@@ -1,19 +1,19 @@
|
||||
const { getMessages, saveMessage, saveBingMessage, deleteMessagesSince, deleteMessages } = require('./Message');
|
||||
const { getCustomGpts, updateCustomGpt, updateByLabel, deleteCustomGpts } = require('./CustomGpt');
|
||||
const { getConvoTitle, getConvo, saveConvo, updateConvo } = require('./Conversation');
|
||||
const { getMessages, saveMessage, deleteMessagesSince, deleteMessages } = require('./Message');
|
||||
const { getConvoTitle, getConvo, saveConvo } = require('./Conversation');
|
||||
const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset');
|
||||
|
||||
module.exports = {
|
||||
getMessages,
|
||||
saveMessage,
|
||||
saveBingMessage,
|
||||
deleteMessagesSince,
|
||||
deleteMessages,
|
||||
|
||||
getConvoTitle,
|
||||
getConvo,
|
||||
saveConvo,
|
||||
updateConvo,
|
||||
getCustomGpts,
|
||||
updateCustomGpt,
|
||||
updateByLabel,
|
||||
deleteCustomGpts
|
||||
|
||||
getPreset,
|
||||
getPresets,
|
||||
savePreset,
|
||||
deletePresets
|
||||
};
|
||||
|
||||
@@ -19,10 +19,7 @@ const createMeiliMongooseModel = function ({ index, indexName, client, attribute
|
||||
static async clearMeiliIndex() {
|
||||
await index.delete();
|
||||
// await index.deleteAllDocuments();
|
||||
await this.collection.updateMany(
|
||||
{ _meiliIndex: true },
|
||||
{ $set: { _meiliIndex: false } }
|
||||
);
|
||||
await this.collection.updateMany({ _meiliIndex: true }, { $set: { _meiliIndex: false } });
|
||||
}
|
||||
|
||||
static async resetIndex() {
|
||||
@@ -57,7 +54,7 @@ const createMeiliMongooseModel = function ({ index, indexName, client, attribute
|
||||
// Find objects into mongodb matching `objectID` from Meili search
|
||||
const query = {};
|
||||
// query[primaryKey] = { $in: _.map(data.hits, primaryKey) };
|
||||
query[primaryKey] = _.map(data.hits, hit => cleanUpPrimaryKeyValue(hit[primaryKey]));
|
||||
query[primaryKey] = _.map(data.hits, (hit) => cleanUpPrimaryKeyValue(hit[primaryKey]));
|
||||
// console.log('query', query);
|
||||
const hitsFromMongoose = await this.find(
|
||||
query,
|
||||
@@ -67,7 +64,7 @@ const createMeiliMongooseModel = function ({ index, indexName, client, attribute
|
||||
return { ...results, [key]: 1 };
|
||||
},
|
||||
{ _id: 1 }
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Add additional data from mongodb into Meili search hits
|
||||
@@ -198,8 +195,8 @@ module.exports = function mongoMeili(schema, options) {
|
||||
if (Object.prototype.hasOwnProperty.call(schema.obj, 'messages')) {
|
||||
console.log('Syncing convos...');
|
||||
mongoose.model('Conversation').syncWithMeili();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(schema.obj, 'messageId')) {
|
||||
console.log('Syncing messages...');
|
||||
mongoose.model('Message').syncWithMeili();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const mongoose = require('mongoose');
|
||||
const mongoMeili = require('../plugins/mongoMeili');
|
||||
const { conversationPreset } = require('./defaults');
|
||||
const convoSchema = mongoose.Schema(
|
||||
{
|
||||
conversationId: {
|
||||
@@ -9,15 +10,28 @@ const convoSchema = mongoose.Schema(
|
||||
index: true,
|
||||
meiliIndex: true
|
||||
},
|
||||
parentMessageId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'New Chat',
|
||||
meiliIndex: true
|
||||
},
|
||||
user: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }],
|
||||
// google only
|
||||
examples: [{ type: mongoose.Schema.Types.Mixed }],
|
||||
agentOptions: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
default: null
|
||||
},
|
||||
...conversationPreset,
|
||||
// for bingAI only
|
||||
bingConversationId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
jailbreakConversationId: {
|
||||
type: String,
|
||||
default: null
|
||||
@@ -27,32 +41,13 @@ const convoSchema = mongoose.Schema(
|
||||
default: null
|
||||
},
|
||||
clientId: {
|
||||
type: String
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
invocationId: {
|
||||
type: String
|
||||
},
|
||||
toneStyle: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
chatGptLabel: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
promptPrefix: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
model: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: String
|
||||
},
|
||||
suggestions: [{ type: String }],
|
||||
messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }]
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
158
api/models/schema/defaults.js
Normal file
158
api/models/schema/defaults.js
Normal file
@@ -0,0 +1,158 @@
|
||||
const conversationPreset = {
|
||||
// endpoint: [azureOpenAI, openAI, bingAI, chatGPTBrowser]
|
||||
endpoint: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: true
|
||||
},
|
||||
// for azureOpenAI, openAI, chatGPTBrowser only
|
||||
model: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
// for azureOpenAI, openAI only
|
||||
chatGptLabel: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
// for google only
|
||||
modelLabel: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
promptPrefix: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
temperature: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: false
|
||||
},
|
||||
top_p: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: false
|
||||
},
|
||||
// for google only
|
||||
topP: {
|
||||
type: Number,
|
||||
default: 0.95,
|
||||
required: false
|
||||
},
|
||||
topK: {
|
||||
type: Number,
|
||||
default: 40,
|
||||
required: false
|
||||
},
|
||||
maxOutputTokens: {
|
||||
type: Number,
|
||||
default: 1024,
|
||||
required: false
|
||||
},
|
||||
presence_penalty: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: false
|
||||
},
|
||||
frequency_penalty: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: false
|
||||
},
|
||||
// for bingai only
|
||||
jailbreak: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
context: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
systemMessage: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
toneStyle: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
};
|
||||
|
||||
const agentOptions = {
|
||||
model: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
// for azureOpenAI, openAI only
|
||||
chatGptLabel: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
// for google only
|
||||
modelLabel: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
promptPrefix: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
temperature: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: false
|
||||
},
|
||||
top_p: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: false
|
||||
},
|
||||
// for google only
|
||||
topP: {
|
||||
type: Number,
|
||||
default: 0.95,
|
||||
required: false
|
||||
},
|
||||
topK: {
|
||||
type: Number,
|
||||
default: 40,
|
||||
required: false
|
||||
},
|
||||
maxOutputTokens: {
|
||||
type: Number,
|
||||
default: 1024,
|
||||
required: false
|
||||
},
|
||||
presence_penalty: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: false
|
||||
},
|
||||
frequency_penalty: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: false
|
||||
},
|
||||
context: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
systemMessage: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
conversationPreset,
|
||||
agentOptions
|
||||
};
|
||||
@@ -14,6 +14,9 @@ const messageSchema = mongoose.Schema(
|
||||
required: true,
|
||||
meiliIndex: true
|
||||
},
|
||||
model: {
|
||||
type: String
|
||||
},
|
||||
conversationSignature: {
|
||||
type: String
|
||||
// required: true
|
||||
@@ -43,6 +46,14 @@ const messageSchema = mongoose.Schema(
|
||||
required: true,
|
||||
default: false
|
||||
},
|
||||
unfinished: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
cancelled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
error: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
@@ -52,6 +63,20 @@ const messageSchema = mongoose.Schema(
|
||||
required: false,
|
||||
select: false,
|
||||
default: false
|
||||
},
|
||||
plugin: {
|
||||
latest: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
inputs: {
|
||||
type: [mongoose.Schema.Types.Mixed],
|
||||
required: false
|
||||
},
|
||||
outputs: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
}
|
||||
},
|
||||
{ timestamps: true }
|
||||
|
||||
26
api/models/schema/pluginAuthSchema.js
Normal file
26
api/models/schema/pluginAuthSchema.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const pluginAuthSchema = mongoose.Schema(
|
||||
{
|
||||
authField: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
userId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
pluginKey: {
|
||||
type: String,
|
||||
}
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
const PluginAuth = mongoose.models.Plugin || mongoose.model('PluginAuth', pluginAuthSchema);
|
||||
|
||||
module.exports = PluginAuth;
|
||||
33
api/models/schema/presetSchema.js
Normal file
33
api/models/schema/presetSchema.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { conversationPreset } = require('./defaults');
|
||||
const presetSchema = mongoose.Schema(
|
||||
{
|
||||
presetId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'New Chat',
|
||||
meiliIndex: true
|
||||
},
|
||||
user: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
// google only
|
||||
examples: [{ type: mongoose.Schema.Types.Mixed }],
|
||||
...conversationPreset,
|
||||
agentOptions: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
const Preset = mongoose.models.Preset || mongoose.model('Preset', presetSchema);
|
||||
|
||||
module.exports = Preset;
|
||||
22
api/models/schema/tokenSchema.js
Normal file
22
api/models/schema/tokenSchema.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const tokenSchema = new Schema({
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
required: true,
|
||||
ref: 'user'
|
||||
},
|
||||
token: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
required: true,
|
||||
default: Date.now,
|
||||
expires: 900
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('Token', tokenSchema);
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"ignore": [
|
||||
"api/data/",
|
||||
"data"
|
||||
]
|
||||
}
|
||||
9931
api/package-lock.json
generated
9931
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,44 +1,72 @@
|
||||
{
|
||||
"name": "chatgpt-clone",
|
||||
"version": "0.2.0",
|
||||
"name": "@librechat/backend",
|
||||
"version": "0.5.2",
|
||||
"description": "",
|
||||
"main": "server/index.js",
|
||||
"scripts": {
|
||||
"start": "node server/index.js",
|
||||
"server-dev": "npx nodemon server/index.js"
|
||||
"start": "echo 'please run this from the root directory'",
|
||||
"server-dev": "echo 'please run this from the root directory'",
|
||||
"test": "cross-env NODE_ENV=test jest",
|
||||
"test:ci": "jest --ci",
|
||||
"test2": "node --inspect app/langchain/test2.js",
|
||||
"test3": "node --inspect app/langchain/test3.js",
|
||||
"test4": "node --inspect app/langchain/test4.js",
|
||||
"test5": "node --inspect app/langchain/test5.js",
|
||||
"test8": "node --inspect app/langchain/test8.js",
|
||||
"langchain": "node app/langchain/test2.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/danny-avila/chatgpt-clone.git"
|
||||
"url": "git+https://github.com/danny-avila/LibreChat.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/danny-avila/chatgpt-clone/issues"
|
||||
"url": "https://github.com/danny-avila/LibreChat/issues"
|
||||
},
|
||||
"homepage": "https://github.com/danny-avila/chatgpt-clone#readme",
|
||||
"homepage": "https://github.com/danny-avila/LibreChat#readme",
|
||||
"dependencies": {
|
||||
"@dqbd/tiktoken": "^1.0.2",
|
||||
"@keyv/mongo": "^2.1.8",
|
||||
"@waylaidwanderer/chatgpt-api": "^1.33.1",
|
||||
"@waylaidwanderer/chatgpt-api": "^1.37.0",
|
||||
"axios": "^1.3.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"cookie": "^0.5.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"crypto": "^1.0.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "^8.36.0",
|
||||
"eslint": "^8.41.0",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"googleapis": "^118.0.0",
|
||||
"handlebars": "^4.7.7",
|
||||
"html": "^1.0.0",
|
||||
"joi": "^17.9.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"keyv": "^4.5.2",
|
||||
"keyv-file": "^0.2.0",
|
||||
"langchain": "^0.0.95",
|
||||
"lodash": "^4.17.21",
|
||||
"meilisearch": "^0.31.1",
|
||||
"mongoose": "^6.9.0",
|
||||
"openai": "^3.1.0",
|
||||
"sanitize": "^2.1.2"
|
||||
"meilisearch": "^0.33.0",
|
||||
"mongoose": "^7.1.1",
|
||||
"nodemailer": "^6.9.1",
|
||||
"openai": "^3.2.1",
|
||||
"openid-client": "^5.4.2",
|
||||
"passport": "^0.6.0",
|
||||
"passport-facebook": "^3.0.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"pino": "^8.12.1",
|
||||
"sanitize": "^2.1.2",
|
||||
"sharp": "^0.32.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.5.0",
|
||||
"nodemon": "^2.0.20",
|
||||
"path": "^0.12.7"
|
||||
"path": "^0.12.7",
|
||||
"supertest": "^6.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
124
api/server/controllers/AuthController.js
Normal file
124
api/server/controllers/AuthController.js
Normal file
@@ -0,0 +1,124 @@
|
||||
const {
|
||||
registerUser,
|
||||
requestPasswordReset,
|
||||
resetPassword
|
||||
} = require('../services/auth.service');
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
const registrationController = async (req, res) => {
|
||||
try {
|
||||
const response = await registerUser(req.body);
|
||||
if (response.status === 200) {
|
||||
const { status, user } = response;
|
||||
const token = user.generateToken();
|
||||
//send token for automatic login
|
||||
res.cookie('token', token, {
|
||||
expires: new Date(Date.now() + eval(process.env.SESSION_EXPIRY)),
|
||||
httpOnly: false,
|
||||
secure: isProduction
|
||||
});
|
||||
res.status(status).send({ user });
|
||||
} else {
|
||||
const { status, message } = response;
|
||||
res.status(status).send({ message });
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return res.status(500).json({ message: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
const getUserController = async (req, res) => {
|
||||
return res.status(200).send(req.user);
|
||||
};
|
||||
|
||||
const resetPasswordRequestController = async (req, res) => {
|
||||
try {
|
||||
const resetService = await requestPasswordReset(req.body.email);
|
||||
if (resetService.link) {
|
||||
return res.status(200).json(resetService);
|
||||
} else {
|
||||
return res.status(400).json(resetService);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return res.status(400).json({ message: e.message });
|
||||
}
|
||||
};
|
||||
|
||||
const resetPasswordController = async (req, res) => {
|
||||
try {
|
||||
const resetPasswordService = await resetPassword(
|
||||
req.body.userId,
|
||||
req.body.token,
|
||||
req.body.password
|
||||
);
|
||||
if (resetPasswordService instanceof Error) {
|
||||
return res.status(400).json(resetPasswordService);
|
||||
} else {
|
||||
return res.status(200).json(resetPasswordService);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return res.status(400).json({ message: e.message });
|
||||
}
|
||||
};
|
||||
|
||||
// const refreshController = async (req, res, next) => {
|
||||
// const { signedCookies = {} } = req;
|
||||
// const { refreshToken } = signedCookies;
|
||||
// TODO
|
||||
// if (refreshToken) {
|
||||
// try {
|
||||
// const payload = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
|
||||
// const userId = payload._id;
|
||||
// User.findOne({ _id: userId }).then(
|
||||
// (user) => {
|
||||
// if (user) {
|
||||
// // Find the refresh token against the user record in database
|
||||
// const tokenIndex = user.refreshToken.findIndex(item => item.refreshToken === refreshToken);
|
||||
|
||||
// if (tokenIndex === -1) {
|
||||
// res.statusCode = 401;
|
||||
// res.send('Unauthorized');
|
||||
// } else {
|
||||
// const token = req.user.generateToken();
|
||||
// // If the refresh token exists, then create new one and replace it.
|
||||
// const newRefreshToken = req.user.generateRefreshToken();
|
||||
// user.refreshToken[tokenIndex] = { refreshToken: newRefreshToken };
|
||||
// user.save((err) => {
|
||||
// if (err) {
|
||||
// res.statusCode = 500;
|
||||
// res.send(err);
|
||||
// } else {
|
||||
// // setTokenCookie(res, newRefreshToken);
|
||||
// const user = req.user.toJSON();
|
||||
// res.status(200).send({ token, user });
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// } else {
|
||||
// res.statusCode = 401;
|
||||
// res.send('Unauthorized');
|
||||
// }
|
||||
// },
|
||||
// err => next(err)
|
||||
// );
|
||||
// } catch (err) {
|
||||
// res.statusCode = 401;
|
||||
// res.send('Unauthorized');
|
||||
// }
|
||||
// } else {
|
||||
// res.statusCode = 401;
|
||||
// res.send('Unauthorized');
|
||||
// }
|
||||
// };
|
||||
|
||||
module.exports = {
|
||||
getUserController,
|
||||
// refreshController,
|
||||
registrationController,
|
||||
resetPasswordRequestController,
|
||||
resetPasswordController
|
||||
};
|
||||
@@ -10,8 +10,8 @@ const handleDuplicateKeyError = (err, res) => {
|
||||
//handle validation errors
|
||||
const handleValidationError = (err, res) => {
|
||||
console.log('congrats you hit the validation middleware');
|
||||
let errors = Object.values(err.errors).map(el => el.message);
|
||||
let fields = Object.values(err.errors).map(el => el.path);
|
||||
let errors = Object.values(err.errors).map((el) => el.message);
|
||||
let fields = Object.values(err.errors).map((el) => el.path);
|
||||
let code = 400;
|
||||
if (errors.length > 1) {
|
||||
const formattedErrors = errors.join(' ');
|
||||
54
api/server/controllers/PluginController.js
Normal file
54
api/server/controllers/PluginController.js
Normal file
@@ -0,0 +1,54 @@
|
||||
// const { getAvailableToolsService } = require('../services/PluginService');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const filterUniquePlugins = (plugins) => {
|
||||
const seen = new Set();
|
||||
return plugins.filter((plugin) => {
|
||||
const duplicate = seen.has(plugin.pluginKey);
|
||||
seen.add(plugin.pluginKey);
|
||||
return !duplicate;
|
||||
});
|
||||
};
|
||||
|
||||
const isPluginAuthenticated = (plugin) => {
|
||||
if (!plugin.authConfig || plugin.authConfig.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return plugin.authConfig.every((authFieldObj) => {
|
||||
const envValue = process.env[authFieldObj.authField];
|
||||
return envValue && envValue.trim() !== '';
|
||||
});
|
||||
};
|
||||
|
||||
const getAvailablePluginsController = async (req, res) => {
|
||||
try {
|
||||
fs.readFile(
|
||||
path.join(__dirname, '..', '..', 'app', 'langchain', 'tools', 'manifest.json'),
|
||||
'utf8',
|
||||
(err, data) => {
|
||||
if (err) {
|
||||
res.status(500).json({ message: err.message });
|
||||
} else {
|
||||
const jsonData = JSON.parse(data);
|
||||
const uniquePlugins = filterUniquePlugins(jsonData);
|
||||
const authenticatedPlugins = uniquePlugins.map((plugin) => {
|
||||
if (isPluginAuthenticated(plugin)) {
|
||||
return { ...plugin, authenticated: true };
|
||||
} else {
|
||||
return plugin;
|
||||
}
|
||||
});
|
||||
res.status(200).json(authenticatedPlugins);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getAvailablePluginsController
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user