Compare commits
56 Commits
feature/tu
...
v0.5.2
| 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 |
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
@@ -0,0 +1,76 @@
|
||||
version: '3.4'
|
||||
|
||||
services:
|
||||
app:
|
||||
# container_name: LibreChat_dev
|
||||
image: node:19-alpine
|
||||
# Using a Dockerfile is optional, but included for completeness.
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile
|
||||
# # [Optional] You can use build args to set options. e.g. 'VARIANT' below affects the image in the Dockerfile
|
||||
# args:
|
||||
# VARIANT: buster
|
||||
network_mode: "host"
|
||||
# ports:
|
||||
# - 3080:3080 # Change it to 9000:3080 to use nginx
|
||||
extra_hosts: # if you are running APIs on docker you need access to, you will need to uncomment this line and next
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
volumes:
|
||||
# # This is where VS Code should expect to find your project's source code and the value of "workspaceFolder" in .devcontainer/devcontainer.json
|
||||
- ..:/workspace:cached
|
||||
# # - /app/client/node_modules
|
||||
# # - ./api:/app/api
|
||||
# # - ./.env:/app/.env
|
||||
# # - ./.env.development:/app/.env.development
|
||||
# # - ./.env.production:/app/.env.production
|
||||
# # - /app/api/node_modules
|
||||
|
||||
# # Uncomment the next line to use Docker from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker-compose for details.
|
||||
# # - /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
# Runs app on the same network as the service container, allows "forwardPorts" in devcontainer.json function.
|
||||
# network_mode: service:another-service
|
||||
|
||||
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
|
||||
# Uncomment the next line to use a non-root user for all processes - See https://aka.ms/vscode-remote/containers/non-root for details.
|
||||
# user: vscode
|
||||
|
||||
# Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust.
|
||||
# cap_add:
|
||||
# - SYS_PTRACE
|
||||
# security_opt:
|
||||
# - seccomp:unconfined
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: /bin/sh -c "while sleep 1000; do :; done"
|
||||
|
||||
mongodb:
|
||||
container_name: chat-mongodb
|
||||
network_mode: "host"
|
||||
# ports:
|
||||
# - 27018:27017
|
||||
image: mongo
|
||||
# restart: always
|
||||
volumes:
|
||||
- ./data-node:/data/db
|
||||
command: mongod --noauth
|
||||
meilisearch:
|
||||
container_name: chat-meilisearch
|
||||
image: getmeili/meilisearch:v1.0
|
||||
network_mode: "host"
|
||||
# ports:
|
||||
# - 7700:7700
|
||||
# env_file:
|
||||
# - .env
|
||||
environment:
|
||||
- SEARCH=false
|
||||
- MEILI_HOST=http://0.0.0.0:7700
|
||||
- MEILI_HTTP_ADDR=0.0.0.0:7700
|
||||
- MEILI_MASTER_KEY=5c71cf56d672d009e36070b5bc5e47b743535ae55c818ae3b735bb6ebfb4ba63
|
||||
volumes:
|
||||
- ./meili_data:/meili_data
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
**/node_modules
|
||||
api/.env
|
||||
client/dist/images
|
||||
data-node
|
||||
.env
|
||||
client/dist/images
|
||||
**/.env
|
||||
60
.env.example
@@ -2,6 +2,8 @@
|
||||
# Server configuration:
|
||||
##########################
|
||||
|
||||
APP_TITLE=LibreChat
|
||||
|
||||
# The server will listen to localhost:3080 by default. You can change the target IP as you want.
|
||||
# If you want to make this server available externally, for example to share the server with others
|
||||
# or expose this from a Docker container, set host to 0.0.0.0 or your external IP interface.
|
||||
@@ -25,12 +27,12 @@ MONGO_URI=mongodb://127.0.0.1:27017/LibreChat
|
||||
# Access key from OpenAI platform.
|
||||
# Leave it blank to disable this feature.
|
||||
# Set to "user_provided" to allow the user to provide their API key from the UI.
|
||||
OPENAI_API_KEY=user_provided
|
||||
OPENAI_API_KEY="user_provided"
|
||||
|
||||
# Identify the available models, separated by commas *without spaces*.
|
||||
# The first will be default.
|
||||
# Leave it blank to use internal settings.
|
||||
OPENAI_MODELS=gpt-3.5-turbo,gpt-3.5-turbo-0301,text-davinci-003,gpt-4,gpt-4-0314
|
||||
OPENAI_MODELS=gpt-3.5-turbo,gpt-3.5-turbo-16k,gpt-3.5-turbo-0301,text-davinci-003,gpt-4,gpt-4-0314,gpt-4-0613
|
||||
|
||||
# Reverse proxy settings for OpenAI:
|
||||
# https://github.com/waylaidwanderer/node-chatgpt-api#using-a-reverse-proxy
|
||||
@@ -98,6 +100,11 @@ CHATGPT_MODELS=text-davinci-002-render-sha,gpt-4
|
||||
# Plugins:
|
||||
#############################
|
||||
|
||||
# Identify the available models, separated by commas *without spaces*.
|
||||
# The first will be default.
|
||||
# Leave it blank to use internal settings.
|
||||
PLUGIN_MODELS=gpt-3.5-turbo,gpt-3.5-turbo-16k,gpt-3.5-turbo-0301,gpt-4,gpt-4-0314,gpt-4-0613
|
||||
|
||||
# For securely storing credentials, you need a fixed key and IV. You can set them here for prod and dev environments
|
||||
# If you don't set them, the app will crash on startup.
|
||||
# You need a 32-byte key (64 characters in hex) and 16-byte IV (32 characters in hex)
|
||||
@@ -109,13 +116,15 @@ CREDS_IV=e2341419ec3dd3d19b13a1a87fafcbfb
|
||||
|
||||
# AI-Assisted Google Search
|
||||
# This bot supports searching google for answers to your questions with assistance from GPT!
|
||||
# See detailed instructions here: https://github.com/danny-avila/chatgpt-clone/blob/main/docs/features/plugins/google_search.md
|
||||
# See detailed instructions here: https://github.com/danny-avila/LibreChat/blob/main/docs/features/plugins/google_search.md
|
||||
GOOGLE_API_KEY=
|
||||
GOOGLE_CSE_ID=
|
||||
|
||||
# StableDiffusion WebUI
|
||||
# This bot supports StableDiffusion WebUI, using it's API to generated requested images.
|
||||
SD_WEBUI_URL=http://0.0.0.0:7860
|
||||
# See detailed instructions here: https://github.com/danny-avila/LibreChat/blob/main/docs/features/plugins/stable_diffusion.md
|
||||
# Use "http://127.0.0.1:7860" with local install and "http://host.docker.internal:7860" for docker
|
||||
SD_WEBUI_URL=http://host.docker.internal:7860
|
||||
|
||||
##########################
|
||||
# PaLM (Google) Endpoint:
|
||||
@@ -142,7 +151,7 @@ PROXY=
|
||||
# ENABLING SEARCH MESSAGES/CONVOS
|
||||
# Requires the installation of the free self-hosted Meilisearch or a paid Remote Plan (Remote not tested)
|
||||
# The easiest setup for this is through docker-compose, which takes care of it for you.
|
||||
SEARCH=false
|
||||
SEARCH=true
|
||||
|
||||
# REQUIRED FOR SEARCH: MeiliSearch Host, mainly for the API server to connect to the search server.
|
||||
# Replace '0.0.0.0' with 'meilisearch' if serving MeiliSearch with docker-compose.
|
||||
@@ -164,6 +173,9 @@ MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFCt
|
||||
# User System:
|
||||
##########################
|
||||
|
||||
# Allow Public Registration
|
||||
ALLOW_REGISTRATION=true
|
||||
|
||||
# JWT Secrets
|
||||
JWT_SECRET=secret
|
||||
JWT_REFRESH_SECRET=secret
|
||||
@@ -175,6 +187,22 @@ 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
|
||||
@@ -183,24 +211,10 @@ 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 likely need to change DOMAIN_SERVER to localhost:3090 or place it in .env.development
|
||||
# Note:
|
||||
# Server = Backend
|
||||
# Client = Public (the client is the url you visit)
|
||||
# For the Google login to work in dev mode, you will need to change DOMAIN_SERVER to localhost:3090 or place it in .env.development
|
||||
|
||||
DOMAIN_CLIENT=http://localhost:3080
|
||||
DOMAIN_SERVER=http://localhost:3080
|
||||
|
||||
###########################
|
||||
# Frontend Configuration (Vite):
|
||||
###########################
|
||||
|
||||
# Custom app name, this text will be displayed in the landing page and the footer.
|
||||
VITE_APP_TITLE="LibreChat"
|
||||
|
||||
# Enable Social Login
|
||||
# This enables/disables the Login with Google button on the login page.
|
||||
# Set to true if you have registered the app with google cloud services
|
||||
# and have set the GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET above
|
||||
VITE_SHOW_GOOGLE_LOGIN_OPTION=false
|
||||
|
||||
# Allow Public Registration
|
||||
ALLOW_REGISTRATION=true
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
@@ -58,7 +58,7 @@ body:
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/chatgpt-clone/blob/main/documents/contributions/code_of_conduct.md)
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/CODE_OF_CONDUCT.md)
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
vendored
@@ -51,7 +51,7 @@ body:
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/chatgpt-clone/blob/main/documents/contributions/code_of_conduct.md)
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/CODE_OF_CONDUCT.md)
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
@@ -52,7 +52,7 @@ body:
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/chatgpt-clone/blob/main/documents/contributions/code_of_conduct.md)
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/CODE_OF_CONDUCT.md)
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
|
||||
17
.github/workflows/backend-review.yml
vendored
@@ -1,10 +1,15 @@
|
||||
|
||||
name: Backend Unit Tests
|
||||
on:
|
||||
push:
|
||||
branches: [feat/playwright-jest-cicd]
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- release/*
|
||||
pull_request:
|
||||
branches: [ feat/playwright-jest-cicd ]
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- release/*
|
||||
jobs:
|
||||
tests_Backend:
|
||||
name: Run Backend unit tests
|
||||
@@ -25,10 +30,10 @@ jobs:
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
run: npm ci
|
||||
|
||||
# - name: Install Linux X64 Sharp
|
||||
# run: npm install --platform=linux --arch=x64 --verbose sharp
|
||||
# run: npm install --platform=linux --arch=x64 --verbose sharp
|
||||
|
||||
- name: Run unit tests
|
||||
run: cd api && npm run test:ci
|
||||
run: cd api && npm run test:ci
|
||||
|
||||
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
|
||||
10
.github/workflows/frontend-review.yml
vendored
@@ -2,9 +2,15 @@
|
||||
name: Frontend Unit Tests
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- release/*
|
||||
pull_request:
|
||||
branches: [main, dev]
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- release/*
|
||||
jobs:
|
||||
tests_frontend:
|
||||
name: Run frontend unit tests
|
||||
|
||||
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
|
||||
1
.gitignore
vendored
@@ -39,7 +39,6 @@ meili_data/
|
||||
api/node_modules/
|
||||
client/node_modules/
|
||||
bower_components/
|
||||
.turbo
|
||||
|
||||
# Floobits
|
||||
.floo
|
||||
|
||||
12
Dockerfile
@@ -1,18 +1,14 @@
|
||||
# Base node image
|
||||
FROM node:19-alpine AS node
|
||||
|
||||
# Install curl for health check
|
||||
RUN apk --no-cache add curl
|
||||
|
||||
COPY . /app
|
||||
# Install dependencies
|
||||
WORKDIR /app
|
||||
RUN npm ci
|
||||
|
||||
# Frontend variables as build args
|
||||
ARG VITE_APP_TITLE
|
||||
ARG VITE_SHOW_GOOGLE_LOGIN_OPTION
|
||||
|
||||
# You will need to add your VITE variables to the docker-compose file
|
||||
ENV VITE_APP_TITLE=$VITE_APP_TITLE
|
||||
ENV VITE_SHOW_GOOGLE_LOGIN_OPTION=$VITE_SHOW_GOOGLE_LOGIN_OPTION
|
||||
|
||||
# React client build
|
||||
ENV NODE_OPTIONS="--max-old-space-size=2048"
|
||||
RUN npm run frontend
|
||||
|
||||
48
README.md
@@ -1,8 +1,8 @@
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/NGaa9RPCft">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/110412045/228325485-9d3e618f-a980-44fe-89e9-d6d39164680e.png">
|
||||
<img src="https://user-images.githubusercontent.com/110412045/228325485-9d3e618f-a980-44fe-89e9-d6d39164680e.png" height="128">
|
||||
<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">LibreChat</h1>
|
||||
</a>
|
||||
@@ -30,39 +30,20 @@ https://github.com/danny-avila/LibreChat/assets/110412045/c1eb0c0f-41f6-4335-b98
|
||||
- Response streaming identical to ChatGPT through server-sent events
|
||||
- UI from original ChatGPT, including Dark mode
|
||||
- AI model selection (through 5 endpoints: OpenAI API, BingAI, ChatGPT Browser, PaLM2, Plugins)
|
||||
- Create, Save, & Share custom presets - [More info on prompt presets here](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.3.0)
|
||||
- Create, Save, & Share custom presets - [More info on prompt presets here](https://github.com/danny-avila/LibreChat/releases/tag/v0.3.0)
|
||||
- Edit and Resubmit messages with conversation branching
|
||||
- Search all messages/conversations - [More info here](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.1.0)
|
||||
- Search all messages/conversations - [More info here](https://github.com/danny-avila/LibreChat/releases/tag/v0.1.0)
|
||||
- Plugins now available (including web access, image generation and more)
|
||||
|
||||
---
|
||||
# ⚠️ **Breaking Changes** ⚠️
|
||||
Note: These changes only apply to users who are updating from a previous version of the app.
|
||||
|
||||
- We have simplified the configuration process by using a single `.env` file in the root folder instead of separate `/api/.env` and `/client/.env` files.
|
||||
- If you had installed a previous version, you can run `npm run upgrade` to automatically copy the content of both files to the new `.env` file and backup the old ones in the root dir.
|
||||
- If you are installing the project for the first time, it's recommend you run the installation script `npm run install` to guide your local setup (otherwise continue to use docker)
|
||||
- The docker-compose file had some change. Review the [new docker instructions](docs\install\docker_install.md) to make sure you are setup properly. This is still the simplest and most effective method.
|
||||
- The upgrade script requires both `/api/.env` and `/client/.env` files to run properly. If you get an error about a missing client env file, just rename the `/client/.env.example` file to `/client/.env` and run the script again.
|
||||
- We have renamed the `OPENAI_KEY` variable to `OPENAI_API_KEY` to match the official documentation. The upgrade script should do this automatically for you, but please double-check that your key is correct in the new `.env` file.
|
||||
- After running the upgrade script, the `OPENAI_API_KEY` variable might be placed in a different section in the new `.env` file than before. This does not affect the functionality of the app, but if you want to keep it organized, you can look for it near the bottom of the file and move it to its usual section.
|
||||
|
||||
##
|
||||
|
||||
- For enhanced security, we are now asking for crypto keys for securely storing credentials in the `.env` file. Crypto keys are used to encrypt and decrypt sensitive data such as passwords and access keys. If you don't set them, the app will crash on startup.
|
||||
- You need to fill the following variables in the `.env` file with 32-byte (64 characters in hex) or 16-byte (32 characters in hex) values:
|
||||
- `CREDS_KEY` (32-byte)
|
||||
- `CREDS_IV` (16-byte)
|
||||
- `JWT_SECRET` (32-byte, optional but recommended)
|
||||
- You can use this replit to generate some crypto keys quickly: https://replit.com/@daavila/crypto#index.js
|
||||
- Make sure you keep your crypto keys safe and don't share them with anyone.
|
||||
|
||||
We apologize for any inconvenience caused by these changes. We hope you enjoy the new and improved version of our app!
|
||||
## ⚠️ [Breaking Changes as of v0.5.0](docs/general_info/breaking_changes.md#v050) ⚠️
|
||||
**Please read this before updating from a previous version**
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
- Keep up with the latest updates by visiting the releases page - [Releases](https://github.com/danny-avila/LibreChat/releases)
|
||||
Keep up with the latest updates by visiting the releases page - [Releases](https://github.com/danny-avila/LibreChat/releases)
|
||||
|
||||
---
|
||||
|
||||
@@ -71,7 +52,7 @@ We apologize for any inconvenience caused by these changes. We hope you enjoy th
|
||||
<details open>
|
||||
<summary><strong>Getting Started</strong></summary>
|
||||
|
||||
* [Docker Install](/docs/install/docker_install.md)
|
||||
* [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)
|
||||
@@ -105,7 +86,10 @@ We apologize for any inconvenience caused by these changes. We hope you enjoy th
|
||||
<details>
|
||||
<summary><strong>Cloud Deployment</strong></summary>
|
||||
|
||||
* [Hetzner](docs/deployment/hetzner_ubuntu.md)
|
||||
* [Heroku](docs/deployment/heroku.md)
|
||||
* [Linode](docs/deployment/linode.md)
|
||||
* [Cloudflare](docs/deployment/cloudflare.md)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -116,7 +100,7 @@ We apologize for any inconvenience caused by these changes. We hope you enjoy th
|
||||
* [Code Standards and Conventions](docs/contributions/coding_conventions.md)
|
||||
* [Testing](docs/contributions/testing.md)
|
||||
* [Security](SECURITY.md)
|
||||
* [Trello Board](https://trello.com/b/17z094kq/chatgpt-clone)
|
||||
* [Trello Board](https://trello.com/b/17z094kq/LibreChate)
|
||||
</details>
|
||||
|
||||
|
||||
@@ -124,13 +108,13 @@ We apologize for any inconvenience caused by these changes. We hope you enjoy th
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#danny-avila/chatgpt-clone&Date)
|
||||
[](https://star-history.com/#danny-avila/LibreChat&Date)
|
||||
|
||||
---
|
||||
|
||||
## Sponsors
|
||||
|
||||
Sponsored by <a href="https://github.com/DavidDev1334"><b>@DavidDev1334</b></a>, <a href="https://github.com/mjtechguy"><b>@mjtechguy</b></a>, <a href="https://github.com/Pharrcyde"><b>@Pharrcyde</b></a>, & <a href="https://github.com/fuegovic"><b>@fuegovic</b></a>
|
||||
Sponsored by <a href="https://github.com/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>
|
||||
|
||||
---
|
||||
|
||||
@@ -146,6 +130,6 @@ For new features, components, or extensions, please open an issue and discuss be
|
||||
|
||||
This project exists in its current state thanks to all the people who contribute
|
||||
---
|
||||
<a href="https://github.com/danny-avila/chatgpt-clone/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=danny-avila/chatgpt-clone" />
|
||||
<a href="https://github.com/danny-avila/LibreChat/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=danny-avila/LibreChat" />
|
||||
</a>
|
||||
|
||||
@@ -31,7 +31,19 @@ const askClient = async ({
|
||||
if (promptPrefix) {
|
||||
promptText = promptPrefix;
|
||||
}
|
||||
const maxContextTokens = model === 'gpt-4-32k' ? 32767 : model.startsWith('gpt-4') ? 8191 : 4095; // 1 less than maximum
|
||||
|
||||
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,
|
||||
|
||||
@@ -10,9 +10,10 @@ const TextStream = require('../stream');
|
||||
const { ChatOpenAI } = require('langchain/chat_models/openai');
|
||||
const { CallbackManager } = require('langchain/callbacks');
|
||||
const { HumanChatMessage, AIChatMessage } = require('langchain/schema');
|
||||
const { initializeCustomAgent } = require('./agents/CustomAgent/initializeCustomAgent');
|
||||
const { initializeCustomAgent, initializeFunctionsAgent } = require('./agents/');
|
||||
const { getMessages, saveMessage, saveConvo } = require('../../models');
|
||||
const { loadTools, SelfReflectionTool } = require('./tools');
|
||||
const { loadTools } = require('./tools/util');
|
||||
const { SelfReflectionTool } = require('./tools/');
|
||||
const {
|
||||
instructions,
|
||||
imageInstructions,
|
||||
@@ -50,7 +51,11 @@ class ChatAgent {
|
||||
let output = 'Internal thoughts & actions taken:\n"';
|
||||
let actions = input || this.actions;
|
||||
|
||||
if (actions[0]?.action) {
|
||||
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}`
|
||||
}));
|
||||
@@ -106,10 +111,10 @@ class ChatAgent {
|
||||
const preliminaryAnswer =
|
||||
result.output?.length > 0 ? `Preliminary Answer: "${result.output.trim()}"` : '';
|
||||
const prefix = preliminaryAnswer
|
||||
? `review and improve the answer you generated using plugins in response to the User Message below. The answer hasn't been sent to the user yet.`
|
||||
? `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 ChatGPT, ${prefix}${errorMessage}\n${internalActions}
|
||||
return `As a helpful AI Assistant, ${prefix}${errorMessage}\n${internalActions}
|
||||
${preliminaryAnswer}
|
||||
Reply conversationally to the User based on your ${
|
||||
preliminaryAnswer ? 'preliminary answer, ' : ''
|
||||
@@ -145,8 +150,7 @@ Only respond with your conversational reply to the following User Message:
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
this.agentOptions = this.options.agentOptions || {};
|
||||
this.agentIsGpt3 = this.agentOptions.model.startsWith('gpt-3');
|
||||
|
||||
const modelOptions = this.options.modelOptions || {};
|
||||
this.modelOptions = {
|
||||
...modelOptions,
|
||||
@@ -160,10 +164,27 @@ Only respond with your conversational reply to the following User Message:
|
||||
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');
|
||||
this.maxContextTokens = this.modelOptions.model === 'gpt-4-32k' ? 32767 : this.modelOptions.model.startsWith('gpt-4') ? 8191 : 4095,
|
||||
|
||||
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.
|
||||
@@ -180,7 +201,7 @@ Only respond with your conversational reply to the following User Message:
|
||||
}
|
||||
|
||||
this.userLabel = this.options.userLabel || 'User';
|
||||
this.chatGptLabel = this.options.chatGptLabel || 'ChatGPT';
|
||||
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,
|
||||
@@ -388,6 +409,26 @@ Only respond with your conversational reply to the following User Message:
|
||||
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,
|
||||
@@ -400,21 +441,7 @@ Only respond with your conversational reply to the following User Message:
|
||||
configOptions.basePath = this.langchainProxy;
|
||||
}
|
||||
|
||||
const model = this.azure
|
||||
? new ChatOpenAI({
|
||||
...this.azure,
|
||||
...modelOptions
|
||||
})
|
||||
: new ChatOpenAI(
|
||||
{
|
||||
openAIApiKey: this.openAIApiKey,
|
||||
...modelOptions
|
||||
},
|
||||
configOptions
|
||||
// {
|
||||
// basePath: 'http://localhost:8080/v1'
|
||||
// }
|
||||
);
|
||||
const model = this.createLLM(modelOptions, configOptions);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug(`<-----Agent Model: ${model.modelName} | Temp: ${model.temperature}----->`);
|
||||
@@ -424,6 +451,7 @@ Only respond with your conversational reply to the following User Message:
|
||||
user,
|
||||
model,
|
||||
tools: this.options.tools,
|
||||
functions: this.functionsAgent,
|
||||
options: {
|
||||
openAIApiKey: this.openAIApiKey
|
||||
}
|
||||
@@ -447,7 +475,7 @@ Only respond with your conversational reply to the following User Message:
|
||||
console.debug(this.tools.map((tool) => tool.name));
|
||||
}
|
||||
|
||||
if (this.tools.length > 0) {
|
||||
if (this.tools.length > 0 && !this.functionsAgent) {
|
||||
this.tools.push(new SelfReflectionTool({ message, isGpt3: false }));
|
||||
} else if (this.tools.length === 0) {
|
||||
return;
|
||||
@@ -466,7 +494,8 @@ Only respond with your conversational reply to the following User Message:
|
||||
};
|
||||
|
||||
// initialize agent
|
||||
this.executor = await initializeCustomAgent({
|
||||
const initializer = this.functionsAgent ? initializeFunctionsAgent : initializeCustomAgent;
|
||||
this.executor = await initializer({
|
||||
model,
|
||||
signal,
|
||||
tools: this.tools,
|
||||
@@ -517,7 +546,7 @@ Only respond with your conversational reply to the following User Message:
|
||||
return;
|
||||
}
|
||||
const token = this.isChatGptModel
|
||||
? progressMessage.choices[0].delta.content
|
||||
? progressMessage.choices?.[0]?.delta.content
|
||||
: progressMessage.choices[0].text;
|
||||
// first event's delta content is always undefined
|
||||
if (!token) {
|
||||
@@ -594,7 +623,7 @@ Only respond with your conversational reply to the following User Message:
|
||||
console.log('sendMessage', message, opts);
|
||||
|
||||
const user = opts.user || null;
|
||||
const { onAgentAction, onChainEnd, onProgress } = opts;
|
||||
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();
|
||||
@@ -658,11 +687,12 @@ Only respond with your conversational reply to the following User Message:
|
||||
return { ...responseMessage, ...this.result };
|
||||
}
|
||||
|
||||
if (!this.agentIsGpt3 && this.result.output) {
|
||||
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(onProgress);
|
||||
await textStream.processTextStream(opts.onProgress);
|
||||
return { ...responseMessage, ...this.result };
|
||||
}
|
||||
|
||||
@@ -685,6 +715,26 @@ Only respond with your conversational reply to the following User Message:
|
||||
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);
|
||||
@@ -808,8 +858,13 @@ Only respond with your conversational reply to the following User Message:
|
||||
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) {
|
||||
const result = [messagePayload, instructionsPayload];
|
||||
return result.filter((message) => message.content.length > 0);
|
||||
}
|
||||
|
||||
@@ -871,7 +926,7 @@ Only respond with your conversational reply to the following User Message:
|
||||
return orderedMessages.map((msg) => ({
|
||||
messageId: msg.messageId,
|
||||
parentMessageId: msg.parentMessageId,
|
||||
role: msg.isCreatedByUser ? 'User' : 'ChatGPT',
|
||||
role: msg.isCreatedByUser ? 'User' : 'Assistant',
|
||||
text: msg.text
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { HumanChatMessage, AIChatMessage } = require('langchain/schema');
|
||||
const ChatAgent = require('./ChatAgent');
|
||||
const connectDb = require('../../lib/db/connectDb');
|
||||
const Conversation = require('../../models/Conversation');
|
||||
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;
|
||||
@@ -13,26 +22,72 @@ describe('ChatAgent', () => {
|
||||
max_tokens: 2
|
||||
},
|
||||
agentOptions: {
|
||||
model: 'gpt-3.5-turbo',
|
||||
model: 'gpt-3.5-turbo'
|
||||
}
|
||||
};
|
||||
let parentMessageId;
|
||||
let conversationId;
|
||||
const fakeMessages = [];
|
||||
const userMessage = 'Hello, ChatGPT!';
|
||||
const apiKey = process.env.OPENAI_API_KEY;
|
||||
|
||||
beforeAll(async () => {
|
||||
await connectDb();
|
||||
});
|
||||
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([]);
|
||||
}
|
||||
|
||||
afterAll(async () => {
|
||||
// Delete the messages and conversation created by the test
|
||||
await Conversation.deleteConvos(null, { conversationId });
|
||||
await mongoose.connection.close();
|
||||
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', () => {
|
||||
|
||||
@@ -51,6 +51,4 @@ Query: {input}
|
||||
return AgentExecutor.fromAgentAndTools({ agent, tools, memory, ...rest });
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
initializeCustomAgent
|
||||
};
|
||||
module.exports = initializeCustomAgent;
|
||||
|
||||
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;
|
||||
|
||||
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
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
// To use this tool, you must pass in a configured OpenAIApi object.
|
||||
const fs = require('fs');
|
||||
const { Configuration, OpenAIApi } = require('openai');
|
||||
const { genAzureEndpoint } = require('../../../utils/genAzureEndpoints');
|
||||
// const { genAzureEndpoint } = require('../../../utils/genAzureEndpoints');
|
||||
const { Tool } = require('langchain/tools');
|
||||
const saveImageFromUrl = require('./saveImageFromUrl');
|
||||
const path = require('path');
|
||||
@@ -11,31 +11,31 @@ class OpenAICreateImage extends Tool {
|
||||
constructor(fields = {}) {
|
||||
super();
|
||||
|
||||
let apiKey = fields.OPENAI_API_KEY || process.env.OPENAI_API_KEY;
|
||||
let azureKey = fields.AZURE_OPENAI_API_KEY || process.env.AZURE_OPENAI_API_KEY;
|
||||
let apiKey = fields.DALLE_API_KEY || this.getApiKey();
|
||||
// let azureKey = fields.AZURE_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
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
// if (azureKey) {
|
||||
// apiKey = azureKey;
|
||||
// const azureConfig = {
|
||||
// apiKey,
|
||||
// azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME || fields.azureOpenAIApiInstanceName,
|
||||
// azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME || fields.azureOpenAIApiDeploymentName,
|
||||
// azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION || fields.azureOpenAIApiVersion
|
||||
// };
|
||||
// config = {
|
||||
// apiKey,
|
||||
// basePath: genAzureEndpoint({
|
||||
// ...azureConfig,
|
||||
// }),
|
||||
// baseOptions: {
|
||||
// headers: { 'api-key': apiKey },
|
||||
// params: {
|
||||
// 'api-version': azureConfig.azureOpenAIApiVersion // this might change. I got the current value from the sample code at https://oai.azure.com/portal/chat
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
this.openaiApi = new OpenAIApi(new Configuration(config));
|
||||
this.name = 'dall-e';
|
||||
this.description = `You can generate images with 'dall-e'. This tool is exclusively for visual content.
|
||||
@@ -46,11 +46,14 @@ Guidelines:
|
||||
"Subject: [subject], Style: [style], Color: [color], Details: [details], Emotion: [emotion]"
|
||||
- Generate images only once per human query unless explicitly requested by the user`;
|
||||
}
|
||||
// "Subject": "Mona Lisa",
|
||||
// "Style": "Chinese traditional painting",
|
||||
// "Color": "Mainly wash tones of ink, with small color blocks in some parts",
|
||||
// "Details": "Mona Lisa should have long hair, a silk dress, holding a fan. The background should have mountains and trees.",
|
||||
// "Emotion": "Serene and elegant"
|
||||
|
||||
getApiKey() {
|
||||
const apiKey = process.env.DALLE_API_KEY || '';
|
||||
if (!apiKey) {
|
||||
throw new Error('Missing DALLE_API_KEY environment variable.');
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
replaceUnwantedChars(inputString) {
|
||||
return inputString.replace(/\r\n|\r|\n/g, ' ').replace('"', '').trim();
|
||||
|
||||
@@ -1,10 +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');
|
||||
const { validateTools, loadTools } = require('./handleTools');
|
||||
|
||||
module.exports = {
|
||||
validateTools,
|
||||
loadTools,
|
||||
availableTools,
|
||||
GoogleSearchAPI,
|
||||
HttpRequestTool,
|
||||
AIPluginTool,
|
||||
OpenAICreateImage,
|
||||
StableDiffusionAPI,
|
||||
StructuredSD,
|
||||
WolframAlphaAPI,
|
||||
StructuredWolfram,
|
||||
SelfReflectionTool
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
{
|
||||
"authField": "GOOGLE_CSE_ID",
|
||||
"label": "Google CSE ID",
|
||||
"description": "This is your Google Custom Search Engine ID. For instructions on how to obtain this, see <a href='https://github.com/danny-avila/chatgpt-clone/blob/main/guides/GOOGLE_SEARCH.md'>Our Docs</a>."
|
||||
"description": "This is your Google Custom Search Engine ID. For instructions on how to obtain this, see <a href='https://github.com/danny-avila/LibreChat/blob/main/docs/features/plugins/google_search.md'>Our Docs</a>."
|
||||
},
|
||||
{
|
||||
"authField": "GOOGLE_API_KEY",
|
||||
"label": "Google API Key",
|
||||
"description": "This is your Google Custom Search API Key. For instructions on how to obtain this, see <a href='https://github.com/danny-avila/chatgpt-clone/blob/main/guides/GOOGLE_SEARCH.md'>Our Docs</a>."
|
||||
"description": "This is your Google Custom Search API Key. For instructions on how to obtain this, see <a href='https://github.com/danny-avila/LibreChat/blob/main/docs/features/plugins/google_search.md'>Our Docs</a>."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
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
@@ -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;
|
||||
@@ -1,3 +1,4 @@
|
||||
const { getUserPluginAuthValue } = require('../../../../server/services/PluginService');
|
||||
const { OpenAIEmbeddings } = require('langchain/embeddings/openai');
|
||||
const { ZapierToolKit } = require('langchain/agents');
|
||||
const {
|
||||
@@ -7,14 +8,17 @@ const {
|
||||
const { ChatOpenAI } = require('langchain/chat_models/openai');
|
||||
const { Calculator } = require('langchain/tools/calculator');
|
||||
const { WebBrowser } = require('langchain/tools/webbrowser');
|
||||
const GoogleSearchAPI = require('./GoogleSearch');
|
||||
const HttpRequestTool = require('./HttpRequestTool');
|
||||
const AIPluginTool = require('./AIPluginTool');
|
||||
const OpenAICreateImage = require('./DALL-E');
|
||||
const StableDiffusionAPI = require('./StableDiffusion');
|
||||
const WolframAlphaAPI = require('./Wolfram');
|
||||
const availableTools = require('./manifest.json');
|
||||
const { getUserPluginAuthValue } = require('../../../server/services/PluginService');
|
||||
const {
|
||||
availableTools,
|
||||
AIPluginTool,
|
||||
GoogleSearchAPI,
|
||||
WolframAlphaAPI,
|
||||
StructuredWolfram,
|
||||
HttpRequestTool,
|
||||
OpenAICreateImage,
|
||||
StableDiffusionAPI,
|
||||
StructuredSD,
|
||||
} = require('../');
|
||||
|
||||
const validateTools = async (user, tools = []) => {
|
||||
try {
|
||||
@@ -69,13 +73,13 @@ const loadToolWithAuth = async (user, authFields, ToolConstructor, options = {})
|
||||
};
|
||||
};
|
||||
|
||||
const loadTools = async ({ user, model, tools = [], options = {} }) => {
|
||||
const loadTools = async ({ user, model, functions = null, tools = [], options = {} }) => {
|
||||
const toolConstructors = {
|
||||
calculator: Calculator,
|
||||
google: GoogleSearchAPI,
|
||||
wolfram: WolframAlphaAPI,
|
||||
wolfram: functions ? StructuredWolfram : WolframAlphaAPI,
|
||||
'dall-e': OpenAICreateImage,
|
||||
'stable-diffusion': StableDiffusionAPI
|
||||
'stable-diffusion': functions ? StructuredSD : StableDiffusionAPI
|
||||
};
|
||||
|
||||
const customConstructors = {
|
||||
@@ -109,9 +113,10 @@ const loadTools = async ({ user, model, tools = [], options = {} }) => {
|
||||
return [
|
||||
new HttpRequestTool(),
|
||||
await AIPluginTool.fromPluginUrl(
|
||||
"https://www.klarna.com/.well-known/ai-plugin.json", new ChatOpenAI({ openAIApiKey: options.openAIApiKey, temperature: 0 })
|
||||
),
|
||||
]
|
||||
'https://www.klarna.com/.well-known/ai-plugin.json',
|
||||
new ChatOpenAI({ openAIApiKey: options.openAIApiKey, temperature: 0 })
|
||||
)
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
require('dotenv').config({ path: '../../../.env' });
|
||||
const mongoose = require('mongoose');
|
||||
const User = require('../../../models/User');
|
||||
const connectDb = require('../../../lib/db/connectDb');
|
||||
const { validateTools, loadTools, availableTools } = require('./index');
|
||||
const PluginService = require('../../../server/services/PluginService');
|
||||
const mockUser = {
|
||||
_id: 'fakeId',
|
||||
save: jest.fn(),
|
||||
findByIdAndDelete: jest.fn(),
|
||||
};
|
||||
|
||||
var mockPluginService = {
|
||||
updateUserPluginAuth: jest.fn(),
|
||||
deleteUserPluginAuth: jest.fn(),
|
||||
getUserPluginAuthValue: jest.fn()
|
||||
};
|
||||
|
||||
|
||||
jest.mock('../../../../models/User', () => {
|
||||
return function() {
|
||||
return mockUser;
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../server/services/PluginService', () => mockPluginService);
|
||||
|
||||
const User = require('../../../../models/User');
|
||||
const { validateTools, loadTools } = require('./');
|
||||
const PluginService = require('../../../../server/services/PluginService');
|
||||
const { BaseChatModel } = require('langchain/chat_models/openai');
|
||||
const { Calculator } = require('langchain/tools/calculator');
|
||||
const OpenAICreateImage = require('./DALL-E');
|
||||
const GoogleSearchAPI = require('./GoogleSearch');
|
||||
const { availableTools, OpenAICreateImage, GoogleSearchAPI, StructuredSD } = require('../');
|
||||
|
||||
describe('Tool Handlers', () => {
|
||||
let fakeUser;
|
||||
@@ -21,7 +37,16 @@ describe('Tool Handlers', () => {
|
||||
const authConfigs = mainPlugin.authConfig;
|
||||
|
||||
beforeAll(async () => {
|
||||
await connectDb();
|
||||
mockUser.save.mockResolvedValue(undefined);
|
||||
|
||||
const userAuthValues = {};
|
||||
mockPluginService.getUserPluginAuthValue.mockImplementation((userId, authField) => {
|
||||
return userAuthValues[`${userId}-${authField}`];
|
||||
});
|
||||
mockPluginService.updateUserPluginAuth.mockImplementation((userId, authField, _pluginKey, credential) => {
|
||||
userAuthValues[`${userId}-${authField}`] = credential;
|
||||
});
|
||||
|
||||
fakeUser = new User({
|
||||
name: 'Fake User',
|
||||
username: 'fakeuser',
|
||||
@@ -39,19 +64,13 @@ describe('Tool Handlers', () => {
|
||||
for (const authConfig of authConfigs) {
|
||||
await PluginService.updateUserPluginAuth(fakeUser._id, authConfig.authField, pluginKey, mockCredential);
|
||||
}
|
||||
});
|
||||
|
||||
// afterEach(async () => {
|
||||
// // Clean up any test-specific data.
|
||||
// });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Delete the fake user & plugin auth
|
||||
await User.findByIdAndDelete(fakeUser._id);
|
||||
await mockUser.findByIdAndDelete(fakeUser._id);
|
||||
for (const authConfig of authConfigs) {
|
||||
await PluginService.deleteUserPluginAuth(fakeUser._id, authConfig.authField);
|
||||
}
|
||||
await mongoose.connection.close();
|
||||
});
|
||||
|
||||
describe('validateTools', () => {
|
||||
@@ -128,6 +147,7 @@ describe('Tool Handlers', () => {
|
||||
try {
|
||||
await loadTool2();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
});
|
||||
@@ -154,5 +174,17 @@ describe('Tool Handlers', () => {
|
||||
});
|
||||
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
@@ -0,0 +1,6 @@
|
||||
const { validateTools, loadTools } = require('./handleTools');
|
||||
|
||||
module.exports = {
|
||||
validateTools,
|
||||
loadTools
|
||||
};
|
||||
@@ -65,6 +65,11 @@ const userSchema = mongoose.Schema(
|
||||
unique: true,
|
||||
sparse: true
|
||||
},
|
||||
openidId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
sparse: true
|
||||
},
|
||||
plugins: {
|
||||
type: Array,
|
||||
default: []
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"ignore": [
|
||||
"api/data/",
|
||||
"data"
|
||||
]
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
{
|
||||
"name": "chat-backend",
|
||||
"version": "0.5.0",
|
||||
"name": "@librechat/backend",
|
||||
"version": "0.5.2",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "echo 'please run this from the root directory'",
|
||||
"server-dev": "echo 'please run this from the root directory'",
|
||||
"watch": "cross-env NODE_ENV=development npx nodemon server/index.js",
|
||||
"test": "cross-env NODE_ENV=test jest",
|
||||
"test:ci": "jest --ci",
|
||||
"test2": "node --inspect app/langchain/test2.js",
|
||||
@@ -39,6 +38,7 @@
|
||||
"dotenv": "^16.0.3",
|
||||
"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",
|
||||
@@ -47,12 +47,13 @@
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"keyv": "^4.5.2",
|
||||
"keyv-file": "^0.2.0",
|
||||
"langchain": "^0.0.92",
|
||||
"langchain": "^0.0.95",
|
||||
"lodash": "^4.17.21",
|
||||
"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",
|
||||
@@ -65,6 +66,7 @@
|
||||
"devDependencies": {
|
||||
"jest": "^29.5.0",
|
||||
"nodemon": "^2.0.20",
|
||||
"path": "^0.12.7"
|
||||
"path": "^0.12.7",
|
||||
"supertest": "^6.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
const express = require('express');
|
||||
const session = require('express-session');
|
||||
const connectDb = require('../lib/db/connectDb');
|
||||
const migrateDb = require('../lib/db/migrateDb');
|
||||
const indexSync = require('../lib/db/indexSync');
|
||||
const path = require('path');
|
||||
const cors = require('cors');
|
||||
const routes = require('./routes');
|
||||
const errorController = require('./controllers/error.controller');
|
||||
const errorController = require('./controllers/ErrorController');
|
||||
const passport = require('passport');
|
||||
const port = process.env.PORT || 3080;
|
||||
const host = process.env.HOST || 'localhost';
|
||||
@@ -41,6 +42,15 @@ config.validate(); // Validate the config
|
||||
if (process.env.FACEBOOK_CLIENT_ID && process.env.FACEBOOK_CLIENT_SECRET) {
|
||||
require('../strategies/facebookStrategy');
|
||||
}
|
||||
if (process.env.OPENID_CLIENT_ID && process.env.OPENID_CLIENT_SECRET && process.env.OPENID_ISSUER && process.env.OPENID_SCOPE && process.env.OPENID_SESSION_SECRET) {
|
||||
app.use(session({
|
||||
secret: process.env.OPENID_SESSION_SECRET,
|
||||
resave: false,
|
||||
saveUninitialized: false
|
||||
}));
|
||||
app.use(passport.session());
|
||||
require('../strategies/openidStrategy');
|
||||
}
|
||||
app.use('/oauth', routes.oauth);
|
||||
// api endpoint
|
||||
app.use('/api/auth', routes.auth);
|
||||
@@ -54,6 +64,7 @@ config.validate(); // Validate the config
|
||||
app.use('/api/tokenizer', routes.tokenizer);
|
||||
app.use('/api/endpoints', routes.endpoints);
|
||||
app.use('/api/plugins', routes.plugins);
|
||||
app.use('/api/config', routes.config);
|
||||
|
||||
// static files
|
||||
app.get('/*', function (req, res) {
|
||||
|
||||
51
api/server/routes/__tests__/config.spec.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const request = require('supertest');
|
||||
const express = require('express');
|
||||
const routes = require('../');
|
||||
const app = express();
|
||||
app.use('/api/config', routes.config);
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.APP_TITLE;
|
||||
delete process.env.GOOGLE_CLIENT_ID;
|
||||
delete process.env.GOOGLE_CLIENT_SECRET;
|
||||
delete process.env.OPENID_CLIENT_ID;
|
||||
delete process.env.OPENID_CLIENT_SECRET;
|
||||
delete process.env.OPENID_ISSUER;
|
||||
delete process.env.OPENID_SESSION_SECRET;
|
||||
delete process.env.OPENID_BUTTON_LABEL;
|
||||
delete process.env.OPENID_AUTH_URL;
|
||||
delete process.env.DOMAIN_SERVER;
|
||||
delete process.env.ALLOW_REGISTRATION;
|
||||
});
|
||||
|
||||
//TODO: This works/passes locally but http request tests fail with 404 in CI. Need to figure out why.
|
||||
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
describe.skip('GET /', () => {
|
||||
it('should return 200 and the correct body', async () => {
|
||||
process.env.APP_TITLE = 'Test Title';
|
||||
process.env.GOOGLE_CLIENT_ID = 'Test Google Client Id';
|
||||
process.env.GOOGLE_CLIENT_SECRET = 'Test Google Client Secret';
|
||||
process.env.OPENID_CLIENT_ID= 'Test OpenID Id';
|
||||
process.env.OPENID_CLIENT_SECRET= 'Test OpenID Secret';
|
||||
process.env.OPENID_ISSUER= 'Test OpenID Issuer';
|
||||
process.env.OPENID_SESSION_SECRET= 'Test Secret';
|
||||
process.env.OPENID_BUTTON_LABEL= 'Test OpenID';
|
||||
process.env.OPENID_AUTH_URL= 'http://test-server.com';
|
||||
process.env.DOMAIN_SERVER = 'http://test-server.com';
|
||||
process.env.ALLOW_REGISTRATION = 'true';
|
||||
|
||||
const response = await request(app).get('/');
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body).toEqual({
|
||||
appTitle: 'Test Title',
|
||||
googleLoginEnabled: true,
|
||||
openidLoginEnabled: true,
|
||||
openidLabel: 'Test OpenID',
|
||||
openidImageUrl: 'http://test-server.com',
|
||||
serverDomain: 'http://test-server.com',
|
||||
registrationEnabled: 'true',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const router = express.Router();
|
||||
const { getChatGPTBrowserModels } = require('../endpoints');
|
||||
// const { getChatGPTBrowserModels } = require('../endpoints');
|
||||
const { browserClient } = require('../../../app/');
|
||||
const { saveMessage, getConvoTitle, saveConvo, getConvo } = require('../../../models');
|
||||
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
|
||||
@@ -38,9 +38,9 @@ router.post('/', requireJwtAuth, async (req, res) => {
|
||||
token: req.body?.token ?? null
|
||||
};
|
||||
|
||||
const availableModels = getChatGPTBrowserModels();
|
||||
if (availableModels.find((model) => model === endpointOption.model) === undefined)
|
||||
return handleError(res, { text: 'Illegal request: model' });
|
||||
// const availableModels = getChatGPTBrowserModels();
|
||||
// if (availableModels.find((model) => model === endpointOption.model) === undefined)
|
||||
// return handleError(res, { text: 'Illegal request: model' });
|
||||
|
||||
console.log('ask log', {
|
||||
userMessage,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { titleConvo } = require('../../../app/');
|
||||
const { getOpenAIModels } = require('../endpoints');
|
||||
// const { getOpenAIModels } = require('../endpoints');
|
||||
const ChatAgent = require('../../../app/langchain/ChatAgent');
|
||||
const { validateTools } = require('../../../app/langchain/tools');
|
||||
const { validateTools } = require('../../../app/langchain/tools/util');
|
||||
const { saveMessage, getConvoTitle, saveConvo, getConvo } = require('../../../models');
|
||||
const {
|
||||
handleError,
|
||||
@@ -39,8 +39,9 @@ router.post('/', requireJwtAuth, async (req, res) => {
|
||||
if (endpoint !== 'gptPlugins') return handleError(res, { text: 'Illegal request' });
|
||||
|
||||
const agentOptions = req.body?.agentOptions ?? {
|
||||
agent: 'functions',
|
||||
skipCompletion: true,
|
||||
model: 'gpt-3.5-turbo',
|
||||
// model: 'gpt-4', // for agent model
|
||||
temperature: 0,
|
||||
// top_p: 1,
|
||||
// presence_penalty: 0,
|
||||
@@ -60,20 +61,12 @@ router.post('/', requireJwtAuth, async (req, res) => {
|
||||
presence_penalty: req.body?.presence_penalty ?? 0,
|
||||
frequency_penalty: req.body?.frequency_penalty ?? 0
|
||||
},
|
||||
agentOptions
|
||||
agentOptions: {
|
||||
...agentOptions,
|
||||
// agent: 'functions'
|
||||
}
|
||||
};
|
||||
|
||||
const availableModels = getOpenAIModels();
|
||||
if (availableModels.find((model) => model === endpointOption.modelOptions.model) === undefined) {
|
||||
return handleError(res, { text: `Illegal request: model` });
|
||||
}
|
||||
|
||||
// console.log('ask log', {
|
||||
// text,
|
||||
// conversationId,
|
||||
// endpointOption
|
||||
// });
|
||||
|
||||
console.log('ask log');
|
||||
console.dir({ text, conversationId, endpointOption }, { depth: null });
|
||||
|
||||
@@ -225,6 +218,7 @@ const ask = async ({ text, endpointOption, parentMessageId = null, conversationI
|
||||
onAgentAction,
|
||||
onChainEnd,
|
||||
onStart,
|
||||
...endpointOption,
|
||||
onProgress: progressCallback.call(null, {
|
||||
res,
|
||||
text,
|
||||
|
||||
@@ -27,7 +27,7 @@ router.post('/', requireJwtAuth, async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
const availableModels = ['chat-bison', 'text-bison'];
|
||||
const availableModels = ['chat-bison', 'text-bison', 'codechat-bison'];
|
||||
if (availableModels.find((model) => model === endpointOption.modelOptions.model) === undefined) {
|
||||
return handleError(res, { text: `Illegal request: model` });
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const router = express.Router();
|
||||
const addToCache = require('./addToCache');
|
||||
const { getOpenAIModels } = require('../endpoints');
|
||||
// const { getOpenAIModels } = require('../endpoints');
|
||||
const { titleConvo, askClient } = require('../../../app/');
|
||||
const { saveMessage, getConvoTitle, saveConvo, getConvo } = require('../../../models');
|
||||
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
|
||||
@@ -63,9 +63,9 @@ router.post('/', requireJwtAuth, async (req, res) => {
|
||||
frequency_penalty: req.body?.frequency_penalty ?? 0
|
||||
};
|
||||
|
||||
const availableModels = getOpenAIModels();
|
||||
if (availableModels.find((model) => model === endpointOption.model) === undefined)
|
||||
return handleError(res, { text: 'Illegal request: model' });
|
||||
// const availableModels = getOpenAIModels();
|
||||
// if (availableModels.find((model) => model === endpointOption.model) === undefined)
|
||||
// return handleError(res, { text: 'Illegal request: model' });
|
||||
|
||||
console.log('ask log', {
|
||||
userMessage,
|
||||
|
||||
@@ -91,19 +91,22 @@ const handleText = async (response, bing = false) => {
|
||||
return text;
|
||||
};
|
||||
|
||||
const isObject = (item) => item && typeof item === 'object' && !Array.isArray(item);
|
||||
const getString = (input) => isObject(input) ? JSON.stringify(input) : input ;
|
||||
|
||||
function formatSteps(steps) {
|
||||
let output = '';
|
||||
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
const step = steps[i];
|
||||
const actionInput = step.action.toolInput;
|
||||
const actionInput = getString(step.action.toolInput);
|
||||
const observation = step.observation;
|
||||
|
||||
if (actionInput === 'N/A' || observation?.trim()?.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
output += `Input: ${actionInput}\nOutput: ${observation}`;
|
||||
output += `Input: ${actionInput}\nOutput: ${getString(observation)}`;
|
||||
|
||||
if (steps.length > 1 && i !== steps.length - 1) {
|
||||
output += '\n---\n';
|
||||
@@ -128,12 +131,14 @@ function formatAction(action) {
|
||||
|
||||
const formattedAction = {
|
||||
plugin: capitalizeWords(action.tool) || action.tool,
|
||||
input: action.toolInput,
|
||||
input: getString(action.toolInput),
|
||||
thought: action.log.includes('Thought: ')
|
||||
? action.log.split('\n')[0].replace('Thought: ', '')
|
||||
: action.log.split('\n')[0]
|
||||
};
|
||||
|
||||
formattedAction.thought = getString(formattedAction.thought);
|
||||
|
||||
if (action.tool.toLowerCase() === 'self-reflection' || formattedAction.plugin === 'N/A') {
|
||||
formattedAction.inputStr = `{\n\tthought: ${formattedAction.input}${
|
||||
!formattedAction.thought.includes(formattedAction.input)
|
||||
@@ -142,7 +147,9 @@ function formatAction(action) {
|
||||
}\n}`;
|
||||
formattedAction.inputStr = formattedAction.inputStr.replace('N/A - ', '');
|
||||
} else {
|
||||
formattedAction.inputStr = `{\n\tplugin: ${formattedAction.plugin}\n\tinput: ${formattedAction.input}\n\tthought: ${formattedAction.thought}\n}`;
|
||||
const hasThought = formattedAction.thought.length > 0;
|
||||
const thought = hasThought ? `\n\tthought: ${formattedAction.thought}` : '';
|
||||
formattedAction.inputStr = `{\n\tplugin: ${formattedAction.plugin}\n\tinput: ${formattedAction.input}\n${thought}}`;
|
||||
}
|
||||
|
||||
return formattedAction;
|
||||
|
||||
@@ -4,9 +4,9 @@ const {
|
||||
resetPasswordController,
|
||||
// refreshController,
|
||||
registrationController
|
||||
} = require('../controllers/auth.controller');
|
||||
const { loginController } = require('../controllers/auth/login.controller');
|
||||
const { logoutController } = require('../controllers/auth/logout.controller');
|
||||
} = require('../controllers/AuthController');
|
||||
const { loginController } = require('../controllers/auth/LoginController');
|
||||
const { logoutController } = require('../controllers/auth/LogoutController');
|
||||
const requireJwtAuth = require('../../middleware/requireJwtAuth');
|
||||
const requireLocalAuth = require('../../middleware/requireLocalAuth');
|
||||
|
||||
|
||||
24
api/server/routes/config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', async function (req, res) {
|
||||
try {
|
||||
const appTitle = process.env.APP_TITLE || 'LibreChat';
|
||||
const googleLoginEnabled = !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET;
|
||||
const openidLoginEnabled = !!process.env.OPENID_CLIENT_ID
|
||||
&& !!process.env.OPENID_CLIENT_SECRET
|
||||
&& !!process.env.OPENID_ISSUER
|
||||
&& !!process.env.OPENID_SESSION_SECRET;
|
||||
const openidLabel = process.env.OPENID_BUTTON_LABEL || 'Login with OpenID';
|
||||
const openidImageUrl = process.env.OPENID_IMAGE_URL;
|
||||
const serverDomain = process.env.DOMAIN_SERVER || 'http://localhost:3080';
|
||||
const registrationEnabled = process.env.ALLOW_REGISTRATION === 'true';
|
||||
|
||||
return res.status(200).send({appTitle, googleLoginEnabled, openidLoginEnabled, openidLabel, openidImageUrl, serverDomain, registrationEnabled});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return res.status(500).send({error: err.message});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -3,7 +3,7 @@ const router = express.Router();
|
||||
const { availableTools } = require('../../app/langchain/tools');
|
||||
|
||||
const getOpenAIModels = () => {
|
||||
let models = ['gpt-4', 'text-davinci-003', 'gpt-3.5-turbo', 'gpt-3.5-turbo-0301'];
|
||||
let models = ['gpt-4', 'gpt-4-0613', 'gpt-3.5-turbo', 'gpt-3.5-turbo-16k', 'gpt-3.5-turbo-0613', 'gpt-3.5-turbo-0301', 'text-davinci-003' ];
|
||||
if (process.env.OPENAI_MODELS) models = String(process.env.OPENAI_MODELS).split(',');
|
||||
|
||||
return models;
|
||||
@@ -16,6 +16,13 @@ const getChatGPTBrowserModels = () => {
|
||||
return models;
|
||||
};
|
||||
|
||||
const getPluginModels = () => {
|
||||
let models = ['gpt-4', 'gpt-4-0613', 'gpt-3.5-turbo', 'gpt-3.5-turbo-16k', 'gpt-3.5-turbo-0613', 'gpt-3.5-turbo-0301'];
|
||||
if (process.env.PLUGIN_MODELS) models = String(process.env.PLUGIN_MODELS).split(',');
|
||||
|
||||
return models;
|
||||
};
|
||||
|
||||
let i = 0;
|
||||
router.get('/', async function (req, res) {
|
||||
let key, palmUser;
|
||||
@@ -38,7 +45,7 @@ router.get('/', async function (req, res) {
|
||||
|
||||
const google =
|
||||
key || palmUser
|
||||
? { userProvide: palmUser, availableModels: ['chat-bison', 'text-bison'] }
|
||||
? { userProvide: palmUser, availableModels: ['chat-bison', 'text-bison', 'codechat-bison'] }
|
||||
: false;
|
||||
const azureOpenAI = !!process.env.AZURE_OPENAI_API_KEY;
|
||||
const apiKey = process.env.OPENAI_API_KEY || process.env.AZURE_OPENAI_API_KEY;
|
||||
@@ -46,7 +53,7 @@ router.get('/', async function (req, res) {
|
||||
? { availableModels: getOpenAIModels(), userProvide: apiKey === 'user_provided' }
|
||||
: false;
|
||||
const gptPlugins = apiKey
|
||||
? { availableModels: ['gpt-4', 'gpt-3.5-turbo', 'gpt-3.5-turbo-0301'], availableTools }
|
||||
? { availableModels: getPluginModels(), availableTools, availableAgents: ['classic', 'functions'] }
|
||||
: false;
|
||||
const bingAI = process.env.BINGAI_TOKEN
|
||||
? { userProvide: process.env.BINGAI_TOKEN == 'user_provided' }
|
||||
|
||||
@@ -10,6 +10,7 @@ const oauth = require('./oauth');
|
||||
const { router: endpoints } = require('./endpoints');
|
||||
const plugins = require('./plugins');
|
||||
const user = require('./user');
|
||||
const config = require('./config');
|
||||
|
||||
module.exports = {
|
||||
search,
|
||||
@@ -23,5 +24,6 @@ module.exports = {
|
||||
user,
|
||||
tokenizer,
|
||||
endpoints,
|
||||
plugins
|
||||
plugins,
|
||||
config
|
||||
};
|
||||
|
||||
@@ -62,4 +62,29 @@ router.get(
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/openid',
|
||||
passport.authenticate('openid', {
|
||||
session: false
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/openid/callback',
|
||||
passport.authenticate('openid', {
|
||||
failureRedirect: `${domains.client}/login`,
|
||||
failureMessage: true,
|
||||
session: false
|
||||
}),
|
||||
(req, res) => {
|
||||
const token = req.user.generateToken();
|
||||
res.cookie('token', token, {
|
||||
expires: new Date(Date.now() + eval(process.env.SESSION_EXPIRY)),
|
||||
httpOnly: false,
|
||||
secure: isProduction
|
||||
});
|
||||
res.redirect(domains.client);
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
123
api/strategies/openidStrategy.js
Normal file
@@ -0,0 +1,123 @@
|
||||
const passport = require('passport');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { Issuer, Strategy: OpenIDStrategy } = require('openid-client');
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const config = require('../../config/loader');
|
||||
const domains = config.domains;
|
||||
|
||||
const User = require('../models/User');
|
||||
|
||||
let crypto;
|
||||
try {
|
||||
crypto = require('node:crypto');
|
||||
} catch (err) {
|
||||
console.error('crypto support is disabled!');
|
||||
}
|
||||
|
||||
const downloadImage = async (url, imagePath, accessToken) => {
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
},
|
||||
responseType: 'arraybuffer'
|
||||
});
|
||||
|
||||
fs.mkdirSync(path.dirname(imagePath), { recursive: true });
|
||||
fs.writeFileSync(imagePath, response.data);
|
||||
|
||||
const fileName = path.basename(imagePath);
|
||||
|
||||
return `/images/openid/${fileName}`;
|
||||
} catch (error) {
|
||||
console.error(`Error downloading image at URL "${url}": ${error}`);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
Issuer.discover(process.env.OPENID_ISSUER)
|
||||
.then(issuer => {
|
||||
const client = new issuer.Client({
|
||||
client_id: process.env.OPENID_CLIENT_ID,
|
||||
client_secret: process.env.OPENID_CLIENT_SECRET,
|
||||
redirect_uris: [domains.server + process.env.OPENID_CALLBACK_URL]
|
||||
});
|
||||
|
||||
const openidLogin = new OpenIDStrategy(
|
||||
{
|
||||
client,
|
||||
params: {
|
||||
scope: process.env.OPENID_SCOPE
|
||||
}
|
||||
},
|
||||
async (tokenset, userinfo, done) => {
|
||||
try {
|
||||
let user = await User.findOne({ openidId: userinfo.sub });
|
||||
|
||||
if (!user) {
|
||||
user = await User.findOne({ email: userinfo.email });
|
||||
}
|
||||
|
||||
let fullName = '';
|
||||
if (userinfo.given_name && userinfo.family_name) {
|
||||
fullName = userinfo.given_name + ' ' + userinfo.family_name;
|
||||
} else if (userinfo.given_name) {
|
||||
fullName = userinfo.given_name;
|
||||
} else if (userinfo.family_name) {
|
||||
fullName = userinfo.family_name;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
user = new User({
|
||||
provider: 'openid',
|
||||
openidId: userinfo.sub,
|
||||
username: userinfo.given_name || '',
|
||||
email: userinfo.email || '',
|
||||
emailVerified: userinfo.email_verified || false,
|
||||
name: fullName
|
||||
});
|
||||
} else {
|
||||
user.provider = 'openid';
|
||||
user.openidId = userinfo.sub;
|
||||
user.username = userinfo.given_name || '';
|
||||
user.name = fullName;
|
||||
}
|
||||
|
||||
if (userinfo.picture) {
|
||||
const imageUrl = userinfo.picture;
|
||||
|
||||
let fileName;
|
||||
if (crypto) {
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(userinfo.sub);
|
||||
fileName = hash.digest('hex') + '.png';
|
||||
} else {
|
||||
fileName = userinfo.sub + '.png';
|
||||
}
|
||||
|
||||
const imagePath = path.join(__dirname, '..', '..', 'client', 'public', 'images', 'openid', fileName);
|
||||
|
||||
const imagePathOrEmpty = await downloadImage(imageUrl, imagePath, tokenset.access_token);
|
||||
|
||||
user.avatar = imagePathOrEmpty;
|
||||
} else {
|
||||
user.avatar = '';
|
||||
}
|
||||
|
||||
await user.save();
|
||||
|
||||
done(null, user);
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
passport.use('openid', openidLogin);
|
||||
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
12
client/env.d.ts
vendored
@@ -1,12 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_SERVER_URL_DEV: string;
|
||||
readonly VITE_SERVER_URL_PROD: string;
|
||||
readonly VITE_SHOW_GOOGLE_LOGIN_OPTION: string;
|
||||
readonly VITE_CLIENT_URL_DEV: string;
|
||||
readonly VITE_CLIENT_URL_PROD: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"name": "chat-frontend",
|
||||
"version": "0.5.0",
|
||||
"name": "@librechat/frontend",
|
||||
"version": "0.5.2",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production dotenv -e ../.env -- vite build",
|
||||
"watch": "cross-env NODE_ENV=production dotenv -e ../.env -- vite build --watch",
|
||||
"build:ci": "cross-env NODE_ENV=dev vite build --mode ci",
|
||||
"dev": "cross-env NODE_ENV=dev dotenv -e ../.env -- vite",
|
||||
"preview-prod": "cross-env NODE_ENV=dev dotenv -e ../.env -- vite preview",
|
||||
@@ -37,6 +36,7 @@
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.0",
|
||||
"@radix-ui/react-slider": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.3",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tanstack/react-query": "^4.28.0",
|
||||
|
||||
@@ -2,10 +2,11 @@ import { useEffect } from 'react';
|
||||
import LoginForm from './LoginForm';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { SHOW_GOOGLE_LOGIN_OPTION, ALLOW_REGISTRATION, DOMAIN_SERVER } from "~/utils/envConstants";
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
|
||||
function Login() {
|
||||
const { login, error, isAuthenticated } = useAuthContext();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -14,7 +15,6 @@ function Login() {
|
||||
navigate('/chat/new');
|
||||
}
|
||||
}, [isAuthenticated, navigate]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
||||
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
||||
@@ -29,7 +29,7 @@ function Login() {
|
||||
</div>
|
||||
)}
|
||||
<LoginForm onSubmit={login} />
|
||||
{ALLOW_REGISTRATION && (
|
||||
{startupConfig?.registrationEnabled && (
|
||||
<p className="my-4 text-center text-sm font-light text-gray-700">
|
||||
{' '}
|
||||
Don't have an account?{' '}
|
||||
@@ -38,7 +38,7 @@ function Login() {
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{SHOW_GOOGLE_LOGIN_OPTION && (
|
||||
{startupConfig?.googleLoginEnabled && (
|
||||
<>
|
||||
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
|
||||
<div className="absolute bg-white px-3 text-xs">Or</div>
|
||||
@@ -47,7 +47,7 @@ function Login() {
|
||||
<a
|
||||
aria-label="Login with Google"
|
||||
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
|
||||
href={`${DOMAIN_SERVER}/oauth/google`}
|
||||
href={`${startupConfig.serverDomain}/oauth/google`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -77,6 +77,31 @@ function Login() {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{startupConfig?.openidLoginEnabled && (
|
||||
<>
|
||||
<div className="mt-4 flex gap-x-2">
|
||||
<a
|
||||
aria-label="Login with OpenID"
|
||||
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
|
||||
href={`${startupConfig.serverDomain}/oauth/openid`}
|
||||
>
|
||||
{startupConfig.openidImageUrl ? (
|
||||
<img src={startupConfig.openidImageUrl} alt="OpenID Logo" className="h-5 w-5" />
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 448 512"
|
||||
id="openid"
|
||||
className="h-5 w-5"
|
||||
>
|
||||
<path d="M271.5 432l-68 32C88.5 453.7 0 392.5 0 318.2c0-71.5 82.5-131 191.7-144.3v43c-71.5 12.5-124 53-124 101.3 0 51 58.5 93.3 135.7 103v-340l68-33.2v384zM448 291l-131.3-28.5 36.8-20.7c-19.5-11.5-43.5-20-70-24.8v-43c46.2 5.5 87.7 19.5 120.3 39.3l35-19.8L448 291z"></path>
|
||||
</svg>
|
||||
)}
|
||||
<p>{startupConfig.openidLabel}</p>
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRegisterUserMutation, TRegisterUser } from '~/data-provider';
|
||||
import { SHOW_GOOGLE_LOGIN_OPTION, DOMAIN_SERVER } from '~/utils/envConstants';
|
||||
import { useRegisterUserMutation, TRegisterUser, useGetStartupConfig } from '~/data-provider';
|
||||
|
||||
function Registration() {
|
||||
const navigate = useNavigate();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -34,6 +34,12 @@ function Registration() {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (startupConfig?.registrationEnabled === false) {
|
||||
navigate('/login');
|
||||
}
|
||||
}, [startupConfig, navigate]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
||||
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
||||
@@ -266,7 +272,7 @@ function Registration() {
|
||||
Login
|
||||
</a>
|
||||
</p>
|
||||
{SHOW_GOOGLE_LOGIN_OPTION && (
|
||||
{startupConfig?.googleLoginEnabled && (
|
||||
<>
|
||||
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
|
||||
<div className="absolute bg-white px-3 text-xs">Or</div>
|
||||
@@ -275,7 +281,7 @@ function Registration() {
|
||||
<div className="mt-4 flex gap-x-2">
|
||||
<a
|
||||
aria-label="Login with Google"
|
||||
href={`${DOMAIN_SERVER}/oauth/google`}
|
||||
href={`${startupConfig.serverDomain}/oauth/google`}
|
||||
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
|
||||
>
|
||||
<svg
|
||||
@@ -306,6 +312,34 @@ function Registration() {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{startupConfig?.openidLoginEnabled && (
|
||||
<>
|
||||
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
|
||||
<div className="absolute bg-white px-3 text-xs">Or</div>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-x-2">
|
||||
<a
|
||||
aria-label="Login with OpenID"
|
||||
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
|
||||
href={`${startupConfig.serverDomain}/oauth/openid`}
|
||||
>
|
||||
{startupConfig.openidImageUrl ? (
|
||||
<img src={startupConfig.openidImageUrl} alt="OpenID Logo" className="h-5 w-5" />
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 448 512"
|
||||
id="openid"
|
||||
className="h-5 w-5"
|
||||
>
|
||||
<path d="M271.5 432l-68 32C88.5 453.7 0 392.5 0 318.2c0-71.5 82.5-131 191.7-144.3v43c-71.5 12.5-124 53-124 101.3 0 51 58.5 93.3 135.7 103v-340l68-33.2v384zM448 291l-131.3-28.5 36.8-20.7c-19.5-11.5-43.5-20-70-24.8v-43c46.2 5.5 87.7 19.5 120.3 39.3l35-19.8L448 291z"></path>
|
||||
</svg>
|
||||
)}
|
||||
<p>{startupConfig.openidLabel}</p>
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,12 +3,6 @@ import userEvent from '@testing-library/user-event';
|
||||
import Login from '../Login';
|
||||
import * as mockDataProvider from '~/data-provider';
|
||||
|
||||
jest.mock('~/utils/envConstants', () => ({
|
||||
DOMAIN_SERVER: 'mock-server',
|
||||
SHOW_GOOGLE_LOGIN_OPTION: true,
|
||||
ALLOW_REGISTRATION: true
|
||||
}));
|
||||
|
||||
jest.mock('~/data-provider');
|
||||
|
||||
const setup = ({
|
||||
@@ -23,6 +17,18 @@ const setup = ({
|
||||
mutate: jest.fn(),
|
||||
data: {},
|
||||
isSuccess: false
|
||||
},
|
||||
useGetStartupCongfigReturnValue = {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: {
|
||||
googleLoginEnabled: true,
|
||||
openidLoginEnabled: true,
|
||||
openidLabel: 'Test OpenID',
|
||||
openidImageUrl: 'http://test-server.com',
|
||||
registrationEnabled: true,
|
||||
serverDomain: 'mock-server'
|
||||
}
|
||||
}
|
||||
} = {}) => {
|
||||
const mockUseLoginUser = jest
|
||||
@@ -33,12 +39,16 @@ const setup = ({
|
||||
.spyOn(mockDataProvider, 'useGetUserQuery')
|
||||
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
|
||||
.mockReturnValue(useGetUserQueryReturnValue);
|
||||
const mockUseGetStartupConfig = jest
|
||||
.spyOn(mockDataProvider, 'useGetStartupConfig')
|
||||
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
|
||||
.mockReturnValue(useGetStartupCongfigReturnValue);
|
||||
const renderResult = render(<Login />);
|
||||
|
||||
return {
|
||||
...renderResult,
|
||||
mockUseLoginUser,
|
||||
mockUseGetUserQuery
|
||||
mockUseGetUserQuery,
|
||||
mockUseGetStartupConfig
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -3,11 +3,6 @@ import userEvent from '@testing-library/user-event';
|
||||
import Registration from '../Registration';
|
||||
import * as mockDataProvider from '~/data-provider';
|
||||
|
||||
jest.mock('~/utils/envConstants', () => ({
|
||||
DOMAIN_SERVER: 'mock-server',
|
||||
SHOW_GOOGLE_LOGIN_OPTION: true
|
||||
}));
|
||||
|
||||
jest.mock('~/data-provider');
|
||||
|
||||
const setup = ({
|
||||
@@ -22,6 +17,18 @@ const setup = ({
|
||||
mutate: jest.fn(),
|
||||
data: {},
|
||||
isSuccess: false
|
||||
},
|
||||
useGetStartupCongfigReturnValue = {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: {
|
||||
googleLoginEnabled: true,
|
||||
openidLoginEnabled: true,
|
||||
openidLabel: 'Test OpenID',
|
||||
openidImageUrl: 'http://test-server.com',
|
||||
registrationEnabled: true,
|
||||
serverDomain: 'mock-server'
|
||||
}
|
||||
}
|
||||
} = {}) => {
|
||||
const mockUseRegisterUserMutation = jest
|
||||
@@ -32,13 +39,18 @@ const setup = ({
|
||||
.spyOn(mockDataProvider, 'useGetUserQuery')
|
||||
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
|
||||
.mockReturnValue(useGetUserQueryReturnValue);
|
||||
const mockUseGetStartupConfig = jest
|
||||
.spyOn(mockDataProvider, 'useGetStartupConfig')
|
||||
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
|
||||
.mockReturnValue(useGetStartupCongfigReturnValue);
|
||||
|
||||
const renderResult = render(<Registration />);
|
||||
|
||||
return {
|
||||
...renderResult,
|
||||
mockUseRegisterUserMutation,
|
||||
mockUseGetUserQuery
|
||||
mockUseGetUserQuery,
|
||||
mockUseGetStartupConfig
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ function Settings(props) {
|
||||
}, [debouncedContext]);
|
||||
|
||||
return (
|
||||
<div className="max-h-[350px] overflow-y-auto">
|
||||
<div className="md:h-[350px] h-[490px] overflow-y-auto">
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
|
||||
@@ -168,9 +168,9 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTemplate
|
||||
title={`${title || 'Edit Preset'} - ${preset?.title}`}
|
||||
className="max-w-full sm:max-w-4xl"
|
||||
className="max-w-full sm:max-w-4xl h-[675px] "
|
||||
main={
|
||||
<div className="flex w-full flex-col items-center gap-2">
|
||||
<div className="flex w-full flex-col items-center gap-2 md:h-[475px]">
|
||||
<div className="grid w-full gap-6 sm:grid-cols-2">
|
||||
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
|
||||
<Label htmlFor="chatGptLabel" className="text-left text-sm font-medium">
|
||||
@@ -227,7 +227,7 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
|
||||
<div className="my-4 w-full border-t border-gray-300 dark:border-gray-500" />
|
||||
<div className="w-full p-0">
|
||||
{shouldShowSettings && <Settings preset={preset} setOption={setOption} />}
|
||||
{preset?.endpoint === 'google' && showExamples && (
|
||||
{preset?.endpoint === 'google' && showExamples && !preset?.model?.startsWith('codechat-') && (
|
||||
<Examples
|
||||
examples={preset.examples}
|
||||
setExample={setExample}
|
||||
@@ -238,6 +238,8 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
|
||||
)}
|
||||
{preset?.endpoint === 'gptPlugins' && showAgentSettings && (
|
||||
<AgentSettings
|
||||
agent={preset.agentOptions.agent}
|
||||
skipCompletion={preset.agentOptions.skipCompletion}
|
||||
model={preset.agentOptions.model}
|
||||
endpoint={preset.agentOptions.endpoint}
|
||||
temperature={preset.agentOptions.temperature}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button } from '../ui/Button.tsx';
|
||||
import CrossIcon from '../svg/CrossIcon';
|
||||
// import SaveIcon from '../svg/SaveIcon';
|
||||
import { Save } from 'lucide-react';
|
||||
import { cn } from '~/utils/';
|
||||
|
||||
function EndpointOptionsPopover({
|
||||
content,
|
||||
@@ -18,7 +19,7 @@ function EndpointOptionsPopover({
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
' endpointOptionsPopover-container absolute bottom-[-10px] flex w-full flex-col items-center md:px-4 z-50' +
|
||||
' endpointOptionsPopover-container absolute bottom-[-10px] flex w-full flex-col items-center md:px-4 z-0' +
|
||||
(visible ? ' show' : '')
|
||||
}
|
||||
>
|
||||
@@ -41,7 +42,7 @@ function EndpointOptionsPopover({
|
||||
{additionalButton && (
|
||||
<Button
|
||||
type="button"
|
||||
className="ml-1 h-auto justify-start bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black focus:ring-0 focus:ring-offset-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white dark:focus:outline-none dark:focus:ring-offset-0"
|
||||
className={cn(additionalButton.buttonClass, "ml-1 h-auto justify-start bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black focus:ring-0 focus:ring-offset-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white dark:focus:outline-none dark:focus:ring-offset-0")}
|
||||
onClick={additionalButton.handler}
|
||||
>
|
||||
{additionalButton.icon}
|
||||
|
||||
@@ -27,10 +27,8 @@ function Settings(props) {
|
||||
topP,
|
||||
topK,
|
||||
maxOutputTokens,
|
||||
setOption,
|
||||
edit = false
|
||||
setOption
|
||||
} = props;
|
||||
const maxHeight = edit ? 'max-h-[305px]' : 'max-h-[350px]';
|
||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||
|
||||
const setModel = setOption('model');
|
||||
@@ -43,8 +41,10 @@ function Settings(props) {
|
||||
|
||||
const models = endpointsConfig?.['google']?.['availableModels'] || [];
|
||||
|
||||
const codeChat = model.startsWith('codechat-');
|
||||
|
||||
return (
|
||||
<div className={`${maxHeight} overflow-y-auto`}>
|
||||
<div className={`md:h-[350px] h-[490px] overflow-y-auto`}>
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
@@ -55,43 +55,47 @@ function Settings(props) {
|
||||
disabled={readonly}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex w-full resize-none focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0'
|
||||
'flex w-full z-50 resize-none focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0'
|
||||
)}
|
||||
containerClassName="flex w-full resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="modelLabel" className="text-left text-sm font-medium">
|
||||
Custom Name <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<Input
|
||||
id="modelLabel"
|
||||
disabled={readonly}
|
||||
value={modelLabel || ''}
|
||||
onChange={(e) => setModelLabel(e.target.value || null)}
|
||||
placeholder="Set a custom name for PaLM2"
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex h-10 max-h-10 w-full resize-none px-3 py-2 focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="promptPrefix" className="text-left text-sm font-medium">
|
||||
Prompt Prefix <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id="promptPrefix"
|
||||
disabled={readonly}
|
||||
value={promptPrefix || ''}
|
||||
onChange={(e) => setPromptPrefix(e.target.value || null)}
|
||||
placeholder="Set custom instructions or context. Ignored if empty."
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex max-h-[300px] min-h-[100px] w-full resize-none px-3 py-2 '
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{!codeChat && (
|
||||
<>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="modelLabel" className="text-left text-sm font-medium">
|
||||
Custom Name <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<Input
|
||||
id="modelLabel"
|
||||
disabled={readonly}
|
||||
value={modelLabel || ''}
|
||||
onChange={(e) => setModelLabel(e.target.value || null)}
|
||||
placeholder="Set a custom name for PaLM2"
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex h-10 max-h-10 w-full resize-none px-3 py-2 focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="promptPrefix" className="text-left text-sm font-medium">
|
||||
Prompt Prefix <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id="promptPrefix"
|
||||
disabled={readonly}
|
||||
value={promptPrefix || ''}
|
||||
onChange={(e) => setPromptPrefix(e.target.value || null)}
|
||||
placeholder="Set custom instructions or context. Ignored if empty."
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex max-h-[300px] min-h-[100px] w-full resize-none px-3 py-2 '
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<HoverCard openDelay={300}>
|
||||
@@ -131,87 +135,91 @@ function Settings(props) {
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="temp" side="left" />
|
||||
</HoverCard>
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
|
||||
{!codeChat && (
|
||||
<>
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
|
||||
Top P <small className="opacity-40">(default: 0.95)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="top-p-int"
|
||||
disabled={readonly}
|
||||
value={topP}
|
||||
onChange={(value) => setTopP(value)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200'
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[topP]}
|
||||
onValueChange={(value) => setTopP(value[0])}
|
||||
doubleClickHandler={() => setTopP(1)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="topp" side="left" />
|
||||
</HoverCard>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="top-p-int"
|
||||
disabled={readonly}
|
||||
value={topP}
|
||||
onChange={(value) => setTopP(value)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200'
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[topP]}
|
||||
onValueChange={(value) => setTopP(value[0])}
|
||||
doubleClickHandler={() => setTopP(1)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="topp" side="left" />
|
||||
</HoverCard>
|
||||
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="top-k-int" className="text-left text-sm font-medium">
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="top-k-int" className="text-left text-sm font-medium">
|
||||
Top K <small className="opacity-40">(default: 40)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="top-k-int"
|
||||
disabled={readonly}
|
||||
value={topK}
|
||||
onChange={(value) => setTopK(value)}
|
||||
max={40}
|
||||
min={1}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200'
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[topK]}
|
||||
onValueChange={(value) => setTopK(value[0])}
|
||||
doubleClickHandler={() => setTopK(0)}
|
||||
max={40}
|
||||
min={1}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="topk" side="left" />
|
||||
</HoverCard>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="top-k-int"
|
||||
disabled={readonly}
|
||||
value={topK}
|
||||
onChange={(value) => setTopK(value)}
|
||||
max={40}
|
||||
min={1}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200'
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[topK]}
|
||||
onValueChange={(value) => setTopK(value[0])}
|
||||
doubleClickHandler={() => setTopK(0)}
|
||||
max={40}
|
||||
min={1}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="topk" side="left" />
|
||||
</HoverCard>
|
||||
|
||||
</>
|
||||
)}
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="max-tokens-int" className="text-left text-sm font-medium">
|
||||
Max Output Tokens <small className="opacity-40">(default: 1024)</small>
|
||||
Max Output Tokens <small className="opacity-40">(default: 1024)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="max-tokens-int"
|
||||
|
||||
@@ -43,7 +43,7 @@ function Settings(props) {
|
||||
const models = endpointsConfig?.[endpoint]?.['availableModels'] || [];
|
||||
|
||||
return (
|
||||
<div className="max-h-[350px] overflow-y-auto">
|
||||
<div className="md:h-[350px] h-[490px] overflow-y-auto">
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { cn } from '~/utils/';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import {
|
||||
Switch,
|
||||
SelectDropDown,
|
||||
Label,
|
||||
Slider,
|
||||
@@ -20,28 +21,32 @@ import store from '~/store';
|
||||
function Settings(props) {
|
||||
const {
|
||||
readonly,
|
||||
agent,
|
||||
skipCompletion,
|
||||
model,
|
||||
temperature,
|
||||
// topP,
|
||||
// freqP,
|
||||
// presP,
|
||||
setOption,
|
||||
// tools
|
||||
} = props;
|
||||
const endpoint = 'gptPlugins';
|
||||
|
||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||
const setModel = setOption('model');
|
||||
const setTemperature = setOption('temperature');
|
||||
// const setTopP = setOption('top_p');
|
||||
// const setFreqP = setOption('presence_penalty');
|
||||
// const setPresP = setOption('frequency_penalty');
|
||||
const setAgent = setOption('agent');
|
||||
const setSkipCompletion = setOption('skipCompletion');
|
||||
const onCheckedChangeAgent = (checked) => {
|
||||
setAgent(checked ? 'functions' : 'classic');
|
||||
};
|
||||
|
||||
const onCheckedChangeSkip = (checked) => {
|
||||
setSkipCompletion(checked);
|
||||
};
|
||||
|
||||
|
||||
// const toolsSelected = tools?.length > 0;
|
||||
const models = endpointsConfig?.[endpoint]?.['availableModels'] || [];
|
||||
|
||||
return (
|
||||
<div className="max-h-[350px] min-h-[305px] overflow-y-auto">
|
||||
<div className="md:h-[350px] h-[490px] overflow-y-auto">
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
@@ -58,6 +63,32 @@ function Settings(props) {
|
||||
containerClassName="flex w-full resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-2 grid-cols-2">
|
||||
<HoverCard openDelay={500}>
|
||||
<HoverCardTrigger className='w-[100px]'>
|
||||
<label
|
||||
htmlFor="functions-agent"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
|
||||
>
|
||||
<small>Use Functions</small>
|
||||
</label>
|
||||
<Switch id="functions-agent" checked={agent === 'functions'} onCheckedChange={onCheckedChangeAgent} disabled={readonly} className="mt-2 ml-4"/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="func" side="right" />
|
||||
</HoverCard>
|
||||
<HoverCard openDelay={500}>
|
||||
<HoverCardTrigger className='w-[100px] ml-[-60px]'>
|
||||
<label
|
||||
htmlFor="skip-completion"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
|
||||
>
|
||||
<small>Skip Completion</small>
|
||||
</label>
|
||||
<Switch id="skip-completion" checked={skipCompletion === true} onCheckedChange={onCheckedChangeSkip} disabled={readonly} className="mt-2 ml-4"/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="skip" side="right" />
|
||||
</HoverCard>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<HoverCard openDelay={300}>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { HoverCardPortal, HoverCardContent } from '~/components';
|
||||
|
||||
const types = {
|
||||
temp: 'Higher values = more random, while lower values = more focused and deterministic. We recommend altering this or Top P but not both.',
|
||||
func: 'Enable use of Plugins as OpenAI Functions',
|
||||
skip: 'Enable skipping the completion step, which reviews the final answer and generated steps',
|
||||
max: "The max tokens to generate. The total length of input tokens and generated tokens is limited by the model's context length.",
|
||||
topp: 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We recommend altering this or temperature but not both.',
|
||||
freq: "Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.",
|
||||
|
||||
@@ -47,7 +47,7 @@ function Settings(props) {
|
||||
const models = endpointsConfig?.[endpoint]?.['availableModels'] || [];
|
||||
|
||||
return (
|
||||
<div className="max-h-[350px] min-h-[305px] overflow-y-auto">
|
||||
<div className="md:h-[350px] h-[490px] overflow-y-auto">
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
|
||||
export default function Footer() {
|
||||
const { data: config } = useGetStartupConfig();
|
||||
return (
|
||||
<div className="hidden px-3 pb-1 pt-2 text-center text-xs text-black/50 dark:text-white/50 md:block md:px-4 md:pb-4 md:pt-3">
|
||||
<a
|
||||
@@ -9,7 +11,7 @@ export default function Footer() {
|
||||
rel="noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
{import.meta.env.VITE_APP_TITLE || 'LibreChat'}
|
||||
{config?.appTitle || 'LibreChat'}
|
||||
</a>
|
||||
. Serves and searches all conversations reliably. All AI convos under one house. Pay per call
|
||||
and not per month (cents compared to dollars).
|
||||
@@ -93,6 +93,7 @@ function GoogleOptions() {
|
||||
const cardStyle =
|
||||
'transition-colors shadow-md rounded-md min-w-[75px] font-normal bg-white border-black/10 hover:border-black/10 focus:border-black/10 dark:border-black/10 dark:hover:border-black/10 dark:focus:border-black/10 border dark:bg-gray-700 text-black dark:text-white';
|
||||
|
||||
const isCodeChat = model?.startsWith('codechat-');
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -126,7 +127,7 @@ function GoogleOptions() {
|
||||
<EndpointOptionsPopover
|
||||
content={
|
||||
<div className="px-4 py-4">
|
||||
{showExamples ? (
|
||||
{showExamples && !isCodeChat ? (
|
||||
<Examples
|
||||
examples={examples}
|
||||
setExample={setExample}
|
||||
@@ -152,6 +153,7 @@ function GoogleOptions() {
|
||||
switchToSimpleMode={switchToSimpleMode}
|
||||
additionalButton={{
|
||||
label: (showExamples ? 'Hide' : 'Show') + ' Examples',
|
||||
buttonClass: isCodeChat ? 'disabled' : '',
|
||||
handler: triggerExamples,
|
||||
icon: <MessagesSquared className="mr-1 w-[14px]" />
|
||||
}}
|
||||
|
||||
@@ -50,8 +50,8 @@ export default function PresetItem({ preset = {}, value, onChangePreset, onDelet
|
||||
className="group dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
{icon}
|
||||
{preset?.title}
|
||||
<small className="ml-2">({getPresetTitle()})</small>
|
||||
<small className="text-[11px]">{preset?.title}</small>
|
||||
<small className="ml-2 text-[10px]">({getPresetTitle()})</small>
|
||||
<div className="flex w-4 flex-1" />
|
||||
<button
|
||||
className="invisible m-0 mr-1 rounded-md p-2 text-gray-400 hover:text-gray-700 group-hover:visible dark:text-gray-400 dark:hover:text-gray-200 "
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function PresetItems({ presets, onSelect, onChangePreset, onDelet
|
||||
<>
|
||||
{presets.map((preset) => (
|
||||
<PresetItem
|
||||
key={preset?.presetId}
|
||||
key={preset?.presetId ?? Math.random()}
|
||||
value={preset}
|
||||
onSelect={onSelect}
|
||||
onChangePreset={onChangePreset}
|
||||
|
||||
@@ -153,7 +153,7 @@ export default function NewConversationMenu() {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="min-w-[300px] dark:bg-gray-900 z-[100]"
|
||||
className="w-96 dark:bg-gray-900 z-[100]"
|
||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
||||
>
|
||||
<DropdownMenuLabel
|
||||
@@ -217,7 +217,7 @@ export default function NewConversationMenu() {
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuRadioGroup
|
||||
onValueChange={onSelectPreset}
|
||||
className={cn('overflow-y-auto', showEndpoints ? 'max-h-[180px]' : 'max-h-[315px]')}
|
||||
className={cn('overflow-y-auto overflow-x-hidden', showEndpoints ? 'max-h-[210px]' : 'max-h-[315px]')}
|
||||
>
|
||||
{showPresets &&
|
||||
(presets.length ? (
|
||||
|
||||
@@ -187,6 +187,8 @@ function PluginsOptions() {
|
||||
<div className="px-4 py-4">
|
||||
{showAgentSettings ? (
|
||||
<AgentSettings
|
||||
agent={agentOptions.agent}
|
||||
skipCompletion={agentOptions.skipCompletion}
|
||||
model={agentOptions.model}
|
||||
endpoint={agentOptions.endpoint}
|
||||
temperature={agentOptions.temperature}
|
||||
|
||||
@@ -26,48 +26,25 @@ export default function SubmitButton({
|
||||
setSetTokenDialogOpen(true);
|
||||
};
|
||||
|
||||
if (isSubmitting)
|
||||
if (isSubmitting) {
|
||||
return (
|
||||
<button
|
||||
onClick={handleStopGenerating}
|
||||
type="button"
|
||||
className="group absolute bottom-0 right-0 flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500"
|
||||
className="group absolute bottom-0 right-0 z-[101] flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500"
|
||||
>
|
||||
<div className="m-1 mr-0 rounded-md p-2 pb-[10px] pt-[10px] group-hover:bg-gray-100 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-900 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
|
||||
<StopGeneratingIcon />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
// // previous three dot animation
|
||||
// return (
|
||||
// <button
|
||||
// className="absolute bottom-0 right-1 h-[100%] w-[40px] rounded-md p-1 text-gray-500 hover:bg-gray-100 disabled:hover:bg-transparent dark:hover:bg-gray-900 dark:hover:text-gray-400 dark:disabled:hover:bg-transparent md:right-2"
|
||||
// disabled
|
||||
// >
|
||||
// <div className="text-2xl">
|
||||
// <span style={{ maxWidth: 5.5, display: 'inline-grid' }}>·</span>
|
||||
// <span
|
||||
// className="blink"
|
||||
// style={{ maxWidth: 5.5, display: 'inline-grid' }}
|
||||
// >
|
||||
// ·
|
||||
// </span>
|
||||
// <span
|
||||
// className="blink2"
|
||||
// style={{ maxWidth: 5.5, display: 'inline-grid' }}
|
||||
// >
|
||||
// ·
|
||||
// </span>
|
||||
// </div>
|
||||
// </button>
|
||||
// );
|
||||
else if (!isTokenProvided && endpoint !== 'openAI') {
|
||||
} else if (!isTokenProvided && endpoint !== 'openAI') {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={setToken}
|
||||
type="button"
|
||||
className="group absolute bottom-0 right-0 flex h-[100%] w-auto items-center justify-center bg-transparent p-1 text-gray-500"
|
||||
className="group absolute bottom-0 right-0 z-[101] flex h-[100%] w-auto items-center justify-center bg-transparent p-1 text-gray-500"
|
||||
>
|
||||
<div className="m-1 mr-0 rounded-md p-2 pb-[10px] pt-[10px] align-middle text-xs group-hover:bg-gray-100 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-900 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
|
||||
<Settings className="mr-1 inline-block w-[18px]" />
|
||||
@@ -81,14 +58,14 @@ export default function SubmitButton({
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else
|
||||
} else {
|
||||
return (
|
||||
<button
|
||||
onClick={clickHandler}
|
||||
disabled={disabled}
|
||||
className="group absolute bottom-0 right-0 flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500"
|
||||
className="group absolute bottom-0 right-0 z-[101] flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500"
|
||||
>
|
||||
<div className="m-1 mr-0 rounded-md pt-[11px] pb-[9px] pl-[9.5px] pr-[7px] group-hover:bg-gray-100 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-900 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
|
||||
<div className="m-1 mr-0 rounded-md pb-[9px] pl-[9.5px] pr-[7px] pt-[11px] group-hover:bg-gray-100 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-900 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
@@ -107,6 +84,7 @@ export default function SubmitButton({
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -42,7 +42,7 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-[999] grid w-full gap-4 rounded-b-lg bg-white pb-6 sm:rounded-lg md:w-[680px]',
|
||||
'animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-[999] grid w-full gap-4 rounded-b-lg bg-white pb-6 sm:rounded-lg md:w-[680px] overflow-y-auto',
|
||||
'dark:bg-slate-900',
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import React from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import useDocumentTitle from '~/hooks/useDocumentTitle';
|
||||
import SunIcon from '../svg/SunIcon';
|
||||
import LightningIcon from '../svg/LightningIcon';
|
||||
import CautionIcon from '../svg/CautionIcon';
|
||||
import store from '~/store';
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
|
||||
export default function Landing() {
|
||||
const { data: config } = useGetStartupConfig();
|
||||
const setText = useSetRecoilState(store.text);
|
||||
const conversation = useRecoilValue(store.conversation);
|
||||
// @ts-ignore TODO: Fix anti-pattern - requires refactoring conversation store
|
||||
const { title = 'New Chat' } = conversation || {};
|
||||
|
||||
useDocumentTitle(title);
|
||||
|
||||
const clickHandler = (e) => {
|
||||
const clickHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
const { innerText } = e.target;
|
||||
const { innerText } = e.target as HTMLButtonElement;
|
||||
const quote = innerText.split('"')[1].trim();
|
||||
setText(quote);
|
||||
};
|
||||
@@ -26,7 +30,7 @@ export default function Landing() {
|
||||
id="landing-title"
|
||||
className="mb-10 ml-auto mr-auto mt-6 flex items-center justify-center gap-2 text-center text-4xl font-semibold sm:mb-16 md:mt-[10vh]"
|
||||
>
|
||||
{import.meta.env.VITE_APP_TITLE || 'LibreChat'}
|
||||
{config?.appTitle || 'LibreChat'}
|
||||
</h1>
|
||||
<div className="items-start gap-3.5 text-center md:flex">
|
||||
<div className="mb-8 flex flex-1 flex-col gap-3.5 md:mb-auto">
|
||||
27
client/src/components/ui/Switch.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-green-600 data-[state=unchecked]:bg-gray-200",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-white shadow-[0_1px_2px_rgba(0,0,0,0.45)] transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
@@ -11,6 +11,7 @@ export * from './Landing';
|
||||
export * from './ModelSelect';
|
||||
export * from './Prompt';
|
||||
export * from './Slider';
|
||||
export * from './Switch';
|
||||
export * from './Tabs';
|
||||
export * from './Templates';
|
||||
export * from './Textarea';
|
||||
|
||||
@@ -89,3 +89,7 @@ export const resetPassword = () => {
|
||||
export const plugins = () => {
|
||||
return '/api/plugins';
|
||||
};
|
||||
|
||||
export const config = () => {
|
||||
return '/api/config';
|
||||
}
|
||||
|
||||
@@ -111,3 +111,7 @@ export const getAvailablePlugins = (): Promise<t.TPlugin[]> => {
|
||||
export const updateUserPlugins = (payload: t.TUpdateUserPlugins) => {
|
||||
return request.post(endpoints.userPlugins(), payload);
|
||||
};
|
||||
|
||||
export const getStartupConfig = (): Promise<t.TStartupConfig> => {
|
||||
return request.get(endpoints.config());
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ export enum QueryKeys {
|
||||
presets = 'presets',
|
||||
searchResults = 'searchResults',
|
||||
tokenCount = 'tokenCount',
|
||||
availablePlugins = 'availablePlugins'
|
||||
availablePlugins = 'availablePlugins',
|
||||
startupConfig = 'startupConfig',
|
||||
}
|
||||
|
||||
export const useAbortRequestWithMessage = (): UseMutationResult<
|
||||
@@ -336,3 +337,11 @@ export const useUpdateUserPluginsMutation = (): UseMutationResult<
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetStartupConfig = (): QueryObserverResult<t.TStartupConfig> => {
|
||||
return useQuery<t.TStartupConfig>([QueryKeys.startupConfig], () => dataService.getStartupConfig(), {
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -233,3 +233,13 @@ export type TResetPassword = {
|
||||
token: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type TStartupConfig = {
|
||||
appTitle: boolean;
|
||||
googleLoginEnabled: boolean;
|
||||
openidLoginEnabled: boolean;
|
||||
openidLabel: string;
|
||||
openidImageUrl: string;
|
||||
serverDomain: string;
|
||||
registrationEnabled: boolean;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,11 @@ import Messages from '../components/Messages';
|
||||
import TextChat from '../components/Input';
|
||||
|
||||
import store from '~/store';
|
||||
import { useGetMessagesByConvoId, useGetConversationByIdMutation } from '~/data-provider';
|
||||
import {
|
||||
useGetMessagesByConvoId,
|
||||
useGetConversationByIdMutation,
|
||||
useGetStartupConfig
|
||||
} from '~/data-provider';
|
||||
|
||||
export default function Chat() {
|
||||
const searchQuery = useRecoilValue(store.searchQuery);
|
||||
@@ -21,6 +25,7 @@ export default function Chat() {
|
||||
//disabled by default, we only enable it when messagesTree is null
|
||||
const messagesQuery = useGetMessagesByConvoId(conversationId, { enabled: false });
|
||||
const getConversationMutation = useGetConversationByIdMutation(conversationId);
|
||||
const { data: config } = useGetStartupConfig();
|
||||
|
||||
// when conversation changed or conversationId (in url) changed
|
||||
useEffect(() => {
|
||||
@@ -53,8 +58,8 @@ export default function Chat() {
|
||||
// conversationId (in url) should always follow conversation?.conversationId, unless conversation is null
|
||||
navigate(`/chat/${conversation?.conversationId}`);
|
||||
}
|
||||
document.title = conversation?.title || import.meta.env.VITE_APP_TITLE || 'Chat';
|
||||
}, [conversation, conversationId]);
|
||||
document.title = conversation?.title || config?.appTitle || 'Chat';
|
||||
}, [conversation, conversationId, config]);
|
||||
|
||||
useEffect(() => {
|
||||
if (messagesTree === null && conversation?.conversationId) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import Search from './Search';
|
||||
import { Login, Registration, RequestPasswordReset, ResetPassword } from '../components/Auth';
|
||||
import { AuthContextProvider } from '../hooks/AuthContext';
|
||||
import ApiErrorWatcher from '../components/Auth/ApiErrorWatcher';
|
||||
import { ALLOW_REGISTRATION } from '../utils/envConstants';
|
||||
|
||||
const AuthLayout = () => (
|
||||
<AuthContextProvider>
|
||||
@@ -17,7 +16,7 @@ const AuthLayout = () => (
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: 'register',
|
||||
element: ALLOW_REGISTRATION ? <Registration /> : <Navigate to="/login" replace={true} />
|
||||
element: <Registration />
|
||||
},
|
||||
{
|
||||
path: 'forgot-password',
|
||||
|
||||
@@ -51,6 +51,8 @@ const cleanupPreset = ({ preset: _preset, endpointsConfig = {} }) => {
|
||||
};
|
||||
} else if (endpoint === 'gptPlugins') {
|
||||
const agentOptions = _preset?.agentOptions ?? {
|
||||
agent: 'functions',
|
||||
skipCompletion: true,
|
||||
model: 'gpt-3.5-turbo',
|
||||
temperature: 0,
|
||||
// top_p: 1,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
const ALLOW_REGISTRATION = import.meta.env.ALLOW_REGISTRATION === 'true';
|
||||
const DOMAIN_SERVER = import.meta.env.DOMAIN_SERVER;
|
||||
const SHOW_GOOGLE_LOGIN_OPTION = import.meta.env.VITE_SHOW_GOOGLE_LOGIN_OPTION === 'true';
|
||||
|
||||
export {
|
||||
ALLOW_REGISTRATION,
|
||||
DOMAIN_SERVER,
|
||||
SHOW_GOOGLE_LOGIN_OPTION
|
||||
};
|
||||
@@ -67,6 +67,8 @@ const buildDefaultConversation = ({
|
||||
};
|
||||
} else if (endpoint === 'gptPlugins') {
|
||||
const agentOptions = lastConversationSetup?.agentOptions ?? {
|
||||
agent: 'functions',
|
||||
skipCompletion: true,
|
||||
model: 'gpt-3.5-turbo',
|
||||
temperature: 0,
|
||||
// top_p: 1,
|
||||
|
||||
@@ -88,6 +88,8 @@ const useMessageHandler = () => {
|
||||
responseSender = 'ChatGPT';
|
||||
} else if (endpoint === 'gptPlugins') {
|
||||
const agentOptions = currentConversation?.agentOptions ?? {
|
||||
agent: 'functions',
|
||||
skipCompletion: true,
|
||||
model: 'gpt-3.5-turbo',
|
||||
temperature: 0,
|
||||
// top_p: 1,
|
||||
|
||||
135
config/create-user.js
Normal file
@@ -0,0 +1,135 @@
|
||||
const connectDb = require("@librechat/backend/lib/db/connectDb");
|
||||
const migrateDb = require("@librechat/backend/lib/db/migrateDb");
|
||||
const { registerUser } = require("@librechat/backend/server/services/auth.service");
|
||||
const { askQuestion } = require("./helpers");
|
||||
const User = require("@librechat/backend/models/User");
|
||||
|
||||
const silentExit = (code = 0) => {
|
||||
console.log = () => {};
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
/**
|
||||
* Connect to the database
|
||||
* - If it takes a while, we'll warn the user
|
||||
*/
|
||||
// Warn the user if this is taking a while
|
||||
let timeout = setTimeout(() => {
|
||||
console.orange('This is taking a while... You may need to check your connection if this fails.');
|
||||
timeout = setTimeout(() => {
|
||||
console.orange('Still going... Might as well assume the connection failed...');
|
||||
timeout = setTimeout(() => {
|
||||
console.orange('Error incoming in 3... 2... 1...');
|
||||
}, 13000);
|
||||
}, 10000);
|
||||
}, 5000);
|
||||
// Attempt to connect to the database
|
||||
try {
|
||||
console.orange('Warming up the engines...')
|
||||
await connectDb();
|
||||
clearTimeout(timeout);
|
||||
await migrateDb();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
silentExit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the welcome / help menu
|
||||
*/
|
||||
console.purple('--------------------------')
|
||||
console.purple('Create a new user account!')
|
||||
console.purple('--------------------------')
|
||||
// If we don't have enough arguments, show the help menu
|
||||
if (process.argv.length < 5) {
|
||||
console.orange('Usage: npm run create-user <email> <name> <username>')
|
||||
console.orange('Note: if you do not pass in the arguments, you will be prompted for them.')
|
||||
console.orange('If you really need to pass in the password, you can do so as the 4th argument (not recommended for security).')
|
||||
console.purple('--------------------------')
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the variables we need and get the arguments if they were passed in
|
||||
*/
|
||||
let email = '';
|
||||
let password = '';
|
||||
let name = '';
|
||||
let username = '';
|
||||
// If we have the right number of arguments, lets use them
|
||||
if (process.argv.length >= 4) {
|
||||
email = process.argv[2];
|
||||
name = process.argv[3];
|
||||
|
||||
if (process.argv.length >= 5) {
|
||||
username = process.argv[4];
|
||||
}
|
||||
if (process.argv.length >= 6) {
|
||||
console.red('Warning: password passed in as argument, this is not secure!');
|
||||
password = process.argv[5];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If we don't have the right number of arguments, lets prompt the user for them
|
||||
*/
|
||||
if (!email) {
|
||||
email = await askQuestion('Email:');
|
||||
}
|
||||
// Validate the email
|
||||
if (!email.includes('@')) {
|
||||
console.red('Error: Invalid email address!');
|
||||
silentExit(1);
|
||||
}
|
||||
|
||||
const defaultName = email.split('@')[0];
|
||||
if (!name) {
|
||||
name = await askQuestion('Name: (default is: ' + defaultName + ')');
|
||||
if (!name) {
|
||||
name = defaultName;
|
||||
}
|
||||
}
|
||||
if (!username) {
|
||||
username = await askQuestion('Username: (default is: ' + defaultName + ')');
|
||||
if (!username) {
|
||||
username = defaultName;
|
||||
}
|
||||
}
|
||||
if (!password) {
|
||||
password = await askQuestion('Password: (leave blank, to generate one)');
|
||||
if (!password) {
|
||||
// Make it a random password, length 18
|
||||
password = Math.random().toString(36).slice(-18);
|
||||
console.orange('Your password is: ' + password);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the user doesn't already exist
|
||||
const userExists = await User.findOne({ $or: [{ email }, { username }] });
|
||||
if (userExists) {
|
||||
console.red('Error: A user with that email or username already exists!');
|
||||
silentExit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Now that we have all the variables we need, lets create the user
|
||||
*/
|
||||
const user = { email, password, name, username, confirm_password: password };
|
||||
let result;
|
||||
try {
|
||||
result = await registerUser(user);
|
||||
} catch (error) {
|
||||
console.red('Error: ' + error.message);
|
||||
silentExit(1);
|
||||
}
|
||||
|
||||
// Check the result
|
||||
if (result.status !== 200) {
|
||||
console.red('Error: ' + result.message);
|
||||
silentExit(1);
|
||||
}
|
||||
|
||||
// Done!
|
||||
console.green("User created successfully!")
|
||||
silentExit(0);
|
||||
})();
|
||||
34
config/helpers.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Helper functions
|
||||
* This allows us to give the console some colour when running in a terminal
|
||||
*/
|
||||
const readline = require("readline");
|
||||
|
||||
const askQuestion = (query) => {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
return new Promise((resolve) =>
|
||||
rl.question("\x1b[36m" + query + "\n> " + "\x1b[0m", (ans) => {
|
||||
rl.close();
|
||||
resolve(ans);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// Set the console colours
|
||||
console.orange = (msg) => console.log('\x1b[33m%s\x1b[0m', msg);
|
||||
console.green = (msg) => console.log('\x1b[32m%s\x1b[0m', msg);
|
||||
console.red = (msg) => console.log('\x1b[31m%s\x1b[0m', msg);
|
||||
console.blue = (msg) => console.log('\x1b[34m%s\x1b[0m', msg);
|
||||
console.purple = (msg) => console.log('\x1b[35m%s\x1b[0m', msg);
|
||||
console.cyan = (msg) => console.log('\x1b[36m%s\x1b[0m', msg);
|
||||
console.yellow = (msg) => console.log('\x1b[33m%s\x1b[0m', msg);
|
||||
console.white = (msg) => console.log('\x1b[37m%s\x1b[0m', msg);
|
||||
console.gray = (msg) => console.log('\x1b[90m%s\x1b[0m', msg);
|
||||
|
||||
module.exports = {
|
||||
askQuestion,
|
||||
}
|
||||
@@ -2,8 +2,14 @@
|
||||
* Install script: WIP
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const readline = require('readline');
|
||||
const { exit } = require('process');
|
||||
const { askQuestion } = require('./helpers');
|
||||
|
||||
// If we are not in a TTY, lets exit
|
||||
if (!process.stdin.isTTY) {
|
||||
console.log('Note: we are not in a TTY, skipping install script.')
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Save the original console.log function
|
||||
const originalConsoleWarn = console.warn;
|
||||
@@ -13,22 +19,29 @@ console.warn = originalConsoleWarn;
|
||||
|
||||
const rootEnvPath = loader.resolve('.env');
|
||||
|
||||
// Skip if the env file exists
|
||||
if (fs.existsSync(rootEnvPath)) {
|
||||
console.info('Note: it looks like we\'ve already run the first install, skipping env changes.');
|
||||
// lets close this script without causing an error
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Run the upgrade script if the legacy api/env file exists
|
||||
// Todo: remove this in a future version
|
||||
if (fs.existsSync(loader.resolve('api/.env'))) {
|
||||
console.warn('Upgrade script has yet to run, lets do that!');
|
||||
require('./upgrade');
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Check the example file exists
|
||||
if (!fs.existsSync(rootEnvPath + '.example')) {
|
||||
console.red('It looks like the example env file is missing, please complete setup manually.');
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Copy the example file
|
||||
fs.copyFileSync(rootEnvPath + '.example', rootEnvPath);
|
||||
|
||||
// Lets update the secure keys!
|
||||
// Update the secure keys!
|
||||
loader.addSecureEnvVar(rootEnvPath, 'CREDS_KEY', 32);
|
||||
loader.addSecureEnvVar(rootEnvPath, 'CREDS_IV', 16);
|
||||
loader.addSecureEnvVar(rootEnvPath, 'JWT_SECRET', 32);
|
||||
@@ -37,49 +50,17 @@ loader.addSecureEnvVar(rootEnvPath, 'MEILI_MASTER_KEY', 32);
|
||||
// Init env
|
||||
let env = {};
|
||||
|
||||
// Function to ask for user input
|
||||
const askQuestion = (query) => {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
return new Promise((resolve) =>
|
||||
rl.question(query, (ans) => {
|
||||
rl.close();
|
||||
resolve(ans);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
(async () => {
|
||||
// If the terminal accepts questions, lets ask for the env vars
|
||||
if (!process.stdin.isTTY) {
|
||||
// We could use this to pass in env vars but its untested
|
||||
/*if (process.argv.length > 2) {
|
||||
console.log('Using passed in env vars');
|
||||
process.argv.slice(2).forEach((arg) => {
|
||||
const [key, value] = arg.split('=');
|
||||
env[key] = value;
|
||||
});
|
||||
// Lets colour the console
|
||||
console.purple('=== LibreChat First Install ===');
|
||||
console.blue('Note: Leave blank to use the default value.');
|
||||
console.log(''); // New line
|
||||
|
||||
// Write the env file
|
||||
loader.writeEnvFile(rootEnvPath, env);
|
||||
console.log('Env file written successfully!');
|
||||
exit(0);
|
||||
}*/
|
||||
console.log('This terminal does not accept user input, skipping env setup.');
|
||||
exit(0);
|
||||
}
|
||||
|
||||
console.log('Welcome to the ChatGPT Clone install script!');
|
||||
console.log('Please answer the following questions to setup your environment.');
|
||||
// Ask for the app title
|
||||
const title = await askQuestion(
|
||||
'Enter the app title (default: "LibreChat"): '
|
||||
);
|
||||
env['VITE_APP_TITLE'] = title || 'LibreChat';
|
||||
env['APP_TITLE'] = title || 'LibreChat';
|
||||
|
||||
// Ask for OPENAI_API_KEY
|
||||
const key = await askQuestion(
|
||||
@@ -97,11 +78,33 @@ const askQuestion = (query) => {
|
||||
env['OPENAI_MODELS'] = "gpt-3.5-turbo,gpt-3.5-turbo-0301,text-davinci-003"
|
||||
}
|
||||
|
||||
// Ask about mongodb
|
||||
const mongodb = await askQuestion(
|
||||
'What is your mongodb url? (default: mongodb://127.0.0.1:27017/LibreChat)'
|
||||
);
|
||||
env['MONGO_URI'] = mongodb || 'mongodb://127.0.0.1:27017/LibreChat';
|
||||
// Very basic check to make sure they entered a url
|
||||
if (!env['MONGO_URI'].includes('://')) {
|
||||
console.orange('Warning: Your mongodb url looks incorrect, please double check it in the `.env` file.');
|
||||
}
|
||||
|
||||
// Lets ask about open registration
|
||||
const openReg = await askQuestion(
|
||||
'Do you want to allow user registration (y/n)? Default: y'
|
||||
);
|
||||
if (openReg === 'n' || openReg === 'no') {
|
||||
env['ALLOW_REGISTRATION'] = 'false';
|
||||
// Lets tell them about how to create an account:
|
||||
console.red('Note: You can create an account by running: `npm run create-user <email> <name> <username>`');
|
||||
// sleep for 1 second so they can read this
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
// Update the env file
|
||||
loader.writeEnvFile(rootEnvPath, env);
|
||||
|
||||
// We can ask for more here if we want
|
||||
|
||||
console.log('Environment setup complete.');
|
||||
console.log(''); // New line
|
||||
console.green('Success! Please read our docs if you need help setting up the rest of the app.');
|
||||
console.log(''); // New line
|
||||
})();
|
||||
|
||||
|
||||
12
config/prepare.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const { exec } = require('child_process');
|
||||
|
||||
if (process.env.NODE_ENV !== 'CI') {
|
||||
exec('npx husky install', (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`exec error: ${error}`);
|
||||
return;
|
||||
}
|
||||
console.log(`stdout: ${stdout}`);
|
||||
console.error(`stderr: ${stderr}`);
|
||||
});
|
||||
}
|
||||
@@ -95,6 +95,7 @@ const removeEnvs = {
|
||||
'SERVER_URL_PROD': 'remove',
|
||||
'JWT_SECRET_DEV': 'remove', // Lets regen
|
||||
'JWT_SECRET_PROD': 'remove', // Lets regen
|
||||
'VITE_APP_TITLE': 'remove',
|
||||
// Comments to remove:
|
||||
'#JWT:': 'remove',
|
||||
'# Add a secure secret for production if deploying to live domain.': 'remove',
|
||||
@@ -120,11 +121,10 @@ loader.addSecureEnvVar(rootEnvPath, 'JWT_SECRET', 32);
|
||||
// Lets update the openai key name, not the best spot in the env file but who cares ¯\_(ツ)_/¯
|
||||
loader.writeEnvFile(rootEnvPath, {'OPENAI_API_KEY': initEnv['OPENAI_KEY']})
|
||||
|
||||
// TODO: we need to copy over the value of: VITE_SHOW_GOOGLE_LOGIN_OPTION & VITE_APP_TITLE
|
||||
// TODO: we need to copy over the value of: APP_TITLE
|
||||
fs.appendFileSync(rootEnvPath, '\n\n##########################\n# Frontend Vite Variables:\n##########################\n');
|
||||
const frontend = {
|
||||
'VITE_APP_TITLE': initEnv['VITE_APP_TITLE'] || '"LibreChat"',
|
||||
'VITE_SHOW_GOOGLE_LOGIN_OPTION': initEnv['VITE_SHOW_GOOGLE_LOGIN_OPTION'] || 'false',
|
||||
'APP_TITLE': initEnv['VITE_APP_TITLE'] || '"LibreChat"',
|
||||
'ALLOW_REGISTRATION': 'true'
|
||||
}
|
||||
loader.writeEnvFile(rootEnvPath, frontend)
|
||||
|
||||
@@ -19,17 +19,14 @@ services:
|
||||
- 3080:3080 # Change it to 9000:3080 to use nginx
|
||||
depends_on:
|
||||
- mongodb
|
||||
image: node # Comment this & uncomment below to build from docker hub image
|
||||
build:
|
||||
context: .
|
||||
target: node
|
||||
args:
|
||||
VITE_APP_TITLE: LibreChat # default, change to your desired app name
|
||||
VITE_SHOW_GOOGLE_LOGIN_OPTION: false # default, change to true if you have google auth setup
|
||||
image: librechat # Comment this & uncomment below to build from docker hub image
|
||||
build: # ^------
|
||||
context: . # ^------
|
||||
target: node # ^------v
|
||||
# image: chatgptclone/app:latest # Uncomment this & comment above to build from docker hub image
|
||||
restart: always
|
||||
# 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"
|
||||
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"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
@@ -46,6 +43,7 @@ services:
|
||||
- ./.env.development:/app/.env.development
|
||||
- ./.env.production:/app/.env.production
|
||||
- /app/api/node_modules
|
||||
- ./images:/app/client/public/images
|
||||
mongodb:
|
||||
container_name: chat-mongodb
|
||||
ports:
|
||||
|
||||
BIN
docs/assets/1-cloudflare.png
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
docs/assets/1-linode.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/assets/2-cloudflare.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
docs/assets/2-linode.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
docs/assets/Cloudflare-logo.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/assets/linode-logo.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
docs/assets/logo.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
@@ -97,12 +97,8 @@ Defines Mongoose models to represent data entities and their relationships.
|
||||
|
||||
### Data Services
|
||||
|
||||
Use the conventions found in the `data-provider` directory for handling data services. For more information, see [this article](https://www.danorlandoblog.com/chatgpt-clone-data-services-with-react-query/) which describes the methodology used.
|
||||
Use the conventions found in the `data-provider` directory for handling data services. For more information, see [this article](https://www.danorlandoblog.com/building-data-services-for-librechat-with-react-query/) which describes the methodology used.
|
||||
|
||||
### State Management
|
||||
|
||||
Use [Recoil](https://recoiljs.org/) for state management, but *DO NOT pollute the global state with unnecessary data*. Instead, use local state or props for data that is only used within a component or passed down from parent to child.
|
||||
|
||||
---
|
||||
|
||||
## [Go Back to ReadMe](../../README.md)
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
- For new features, create new documentation and place it in the appropriate folder(s)
|
||||
- If the feature adds new functionality, it should be added to the feature section of the main Readme
|
||||
- When you create a new document, do not forget to add it to the table of content
|
||||
- Add a shortcut that point back to the [README.MD](../../README.md) in the bottom of new documents (look at other docs for example)
|
||||
- Use `#` / `##` / `###` for the different section of the doc
|
||||
- Do not add unrelated information to an existing document, create a new one if needed
|
||||
- For incremental updates, you need to update the main **README.MD**
|
||||
|
||||
---
|
||||
|
||||
## [Go Back to ReadMe](../../README.md)
|
||||
|
||||
@@ -64,6 +64,3 @@ If everything goes well, you should see a `passed` message.
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/22865959/235321489-9be48fd6-77d4-4e21-97ad-0254e140b934.png">
|
||||
|
||||
---
|
||||
|
||||
## [Go Back to ReadMe](../../README.md)
|
||||
|
||||