Merge branch 'master' into login_by_social_providers

This commit is contained in:
Giovanni Borgogno 2024-05-30 17:25:33 -03:00 committed by GitHub
commit f37385b8ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
168 changed files with 4604 additions and 1045 deletions

View File

@ -67,9 +67,9 @@ Some cool features of AnythingLLM
- Extremely efficient cost-saving measures for managing very large documents. You'll never pay to embed a massive document or transcript more than once. 90% more cost effective than other document chatbot solutions.
- Full Developer API for custom integrations!
### Supported LLMs, Embedders, Transcriptions models, and Vector Databases
### Supported LLMs, Embedder Models, Speech models, and Vector Databases
**Supported LLMs:**
**Language Learning Models:**
- [Any open-source llama.cpp compatible model](/server/storage/models/README.md#text-generation-llm-selection)
- [OpenAI](https://openai.com)
@ -88,9 +88,10 @@ Some cool features of AnythingLLM
- [Groq](https://groq.com/)
- [Cohere](https://cohere.com/)
- [KoboldCPP](https://github.com/LostRuins/koboldcpp)
- [LiteLLM](https://github.com/BerriAI/litellm)
- [Text Generation Web UI](https://github.com/oobabooga/text-generation-webui)
**Supported Embedding models:**
**Embedder models:**
- [AnythingLLM Native Embedder](/server/storage/models/README.md) (default)
- [OpenAI](https://openai.com)
@ -100,12 +101,22 @@ Some cool features of AnythingLLM
- [LM Studio (all)](https://lmstudio.ai)
- [Cohere](https://cohere.com/)
**Supported Transcription models:**
**Audio Transcription models:**
- [AnythingLLM Built-in](https://github.com/Mintplex-Labs/anything-llm/tree/master/server/storage/models#audiovideo-transcription) (default)
- [OpenAI](https://openai.com/)
**Supported Vector Databases:**
**TTS (text-to-speech) support:**
- Native Browser Built-in (default)
- [OpenAI TTS](https://platform.openai.com/docs/guides/text-to-speech/voice-options)
- [ElevenLabs](https://elevenlabs.io/)
**STT (speech-to-text) support:**
- Native Browser Built-in (default)
**Vector Databases:**
- [LanceDB](https://github.com/lancedb/lancedb) (default)
- [Astra DB](https://www.datastax.com/products/datastax-astra)
@ -122,8 +133,9 @@ This monorepo consists of three main sections:
- `frontend`: A viteJS + React frontend that you can run to easily create and manage all your content the LLM can use.
- `server`: A NodeJS express server to handle all the interactions and do all the vectorDB management and LLM interactions.
- `docker`: Docker instructions and build process + information for building from source.
- `collector`: NodeJS express server that process and parses documents from the UI.
- `docker`: Docker instructions and build process + information for building from source.
- `embed`: Code specifically for generation of the [embed widget](./embed/README.md).
## 🛳 Self Hosting
@ -132,9 +144,9 @@ Mintplex Labs & the community maintain a number of deployment methods, scripts,
|----------------------------------------|----:|-----|---------------|------------|
| [![Deploy on Docker][docker-btn]][docker-deploy] | [![Deploy on AWS][aws-btn]][aws-deploy] | [![Deploy on GCP][gcp-btn]][gcp-deploy] | [![Deploy on DigitalOcean][do-btn]][do-deploy] | [![Deploy on Render.com][render-btn]][render-deploy] |
| Railway |
| --------------------------------------------------- |
| [![Deploy on Railway][railway-btn]][railway-deploy] |
| Railway | RepoCloud |
| --- | --- |
| [![Deploy on Railway][railway-btn]][railway-deploy] | [![Deploy on RepoCloud][repocloud-btn]][repocloud-deploy] |
[or set up a production AnythingLLM instance without Docker →](./BARE_METAL.md)
@ -223,3 +235,5 @@ This project is [MIT](./LICENSE) licensed.
[render-deploy]: https://render.com/deploy?repo=https://github.com/Mintplex-Labs/anything-llm&branch=render
[railway-btn]: https://railway.app/button.svg
[railway-deploy]: https://railway.app/template/HNSCS1?referralCode=WFgJkn
[repocloud-btn]: https://d16t0pc4846x52.cloudfront.net/deploylobe.svg
[repocloud-deploy]: https://repocloud.io/details/?app_id=276

View File

@ -0,0 +1,214 @@
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: anything-llm-volume
annotations:
pv.beta.kubernetes.io/uid: "1000"
pv.beta.kubernetes.io/gid: "1000"
spec:
storageClassName: gp2
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
awsElasticBlockStore:
# This is the volume UUID from AWS EC2 EBS Volumes list.
volumeID: "{{ anythingllm_awsElasticBlockStore_volumeID }}"
fsType: ext4
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- us-east-1c
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: anything-llm-volume-claim
namespace: "{{ namespace }}"
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: anything-llm
namespace: "{{ namespace }}"
labels:
anything-llm: "true"
spec:
selector:
matchLabels:
k8s-app: anything-llm
replicas: 1
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 0%
maxUnavailable: 100%
template:
metadata:
labels:
anything-llm: "true"
k8s-app: anything-llm
app.kubernetes.io/name: anything-llm
app.kubernetes.io/part-of: anything-llm
annotations:
prometheus.io/scrape: "true"
prometheus.io/path: /metrics
prometheus.io/port: "9090"
spec:
serviceAccountName: "default"
terminationGracePeriodSeconds: 10
securityContext:
fsGroup: 1000
runAsNonRoot: true
runAsGroup: 1000
runAsUser: 1000
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- us-east-1c
containers:
- name: anything-llm
resources:
limits:
memory: "1Gi"
cpu: "500m"
requests:
memory: "512Mi"
cpu: "250m"
imagePullPolicy: IfNotPresent
image: "mintplexlabs/anythingllm:render"
securityContext:
allowPrivilegeEscalation: true
capabilities:
add:
- SYS_ADMIN
runAsNonRoot: true
runAsGroup: 1000
runAsUser: 1000
command:
# Specify a command to override the Dockerfile's ENTRYPOINT.
- /bin/bash
- -c
- |
set -x -e
sleep 3
echo "AWS_REGION: $AWS_REGION"
echo "SERVER_PORT: $SERVER_PORT"
echo "NODE_ENV: $NODE_ENV"
echo "STORAGE_DIR: $STORAGE_DIR"
{
cd /app/server/ &&
npx prisma generate --schema=./prisma/schema.prisma &&
npx prisma migrate deploy --schema=./prisma/schema.prisma &&
node /app/server/index.js
echo "Server process exited with status $?"
} &
{
node /app/collector/index.js
echo "Collector process exited with status $?"
} &
wait -n
exit $?
readinessProbe:
httpGet:
path: /v1/api/health
port: 8888
initialDelaySeconds: 15
periodSeconds: 5
successThreshold: 2
livenessProbe:
httpGet:
path: /v1/api/health
port: 8888
initialDelaySeconds: 15
periodSeconds: 5
failureThreshold: 3
env:
- name: AWS_REGION
value: "{{ aws_region }}"
- name: AWS_ACCESS_KEY_ID
value: "{{ aws_access_id }}"
- name: AWS_SECRET_ACCESS_KEY
value: "{{ aws_access_secret }}"
- name: SERVER_PORT
value: "3001"
- name: JWT_SECRET
value: "my-random-string-for-seeding" # Please generate random string at least 12 chars long.
- name: STORAGE_DIR
value: "/storage"
- name: NODE_ENV
value: "production"
- name: UID
value: "1000"
- name: GID
value: "1000"
volumeMounts:
- name: anything-llm-server-storage-volume-mount
mountPath: /storage
volumes:
- name: anything-llm-server-storage-volume-mount
persistentVolumeClaim:
claimName: anything-llm-volume-claim
---
# This serves the UI and the backend.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: anything-llm-ingress
namespace: "{{ namespace }}"
annotations:
external-dns.alpha.kubernetes.io/hostname: "{{ namespace }}-chat.{{ base_domain }}"
kubernetes.io/ingress.class: "internal-ingress"
nginx.ingress.kubernetes.io/rewrite-target: /
ingress.kubernetes.io/ssl-redirect: "false"
spec:
rules:
- host: "{{ namespace }}-chat.{{ base_domain }}"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: anything-llm-svc
port:
number: 3001
tls: # < placing a host in the TLS config will indicate a cert should be created
- hosts:
- "{{ namespace }}-chat.{{ base_domain }}"
secretName: letsencrypt-prod
---
apiVersion: v1
kind: Service
metadata:
labels:
kubernetes.io/name: anything-llm
name: anything-llm-svc
namespace: "{{ namespace }}"
spec:
ports:
# "port" is external port, and "targetPort" is internal.
- port: 3301
targetPort: 3001
name: traffic
- port: 9090
targetPort: 9090
name: metrics
selector:
k8s-app: anything-llm

View File

@ -1,5 +1,6 @@
const { verifyPayloadIntegrity } = require("../middleware/verifyIntegrity");
const { reqBody } = require("../utils/http");
const { validURL } = require("../utils/url");
function extensions(app) {
if (!app) return;
@ -86,6 +87,25 @@ function extensions(app) {
}
);
app.post(
"/ext/website-depth",
[verifyPayloadIntegrity],
async function (request, response) {
try {
const websiteDepth = require("../utils/extensions/WebsiteDepth");
const { url, depth = 1, maxLinks = 20 } = reqBody(request);
if (!validURL(url)) return { success: false, reason: "Not a valid URL." };
const scrapedData = await websiteDepth(url, depth, maxLinks);
response.status(200).json({ success: true, data: scrapedData });
} catch (e) {
console.error(e);
response.status(400).json({ success: false, reason: e.message });
}
return;
}
);
app.post(
"/ext/confluence",
[verifyPayloadIntegrity],

View File

@ -1,19 +1,23 @@
const fs = require("fs");
const path = require("path");
const { v4 } = require("uuid");
const defaultWhisper = "Xenova/whisper-small"; // Model Card: https://huggingface.co/Xenova/whisper-small
const fileSize = {
"Xenova/whisper-small": "250mb",
"Xenova/whisper-large": "1.56GB",
};
class LocalWhisper {
constructor() {
// Model Card: https://huggingface.co/Xenova/whisper-small
this.model = "Xenova/whisper-small";
constructor({ options }) {
this.model = options?.WhisperModelPref ?? defaultWhisper;
this.fileSize = fileSize[this.model];
this.cacheDir = path.resolve(
process.env.STORAGE_DIR
? path.resolve(process.env.STORAGE_DIR, `models`)
: path.resolve(__dirname, `../../../server/storage/models`)
);
this.modelPath = path.resolve(this.cacheDir, "Xenova", "whisper-small");
this.modelPath = path.resolve(this.cacheDir, ...this.model.split("/"));
// Make directory when it does not exist in existing installations
if (!fs.existsSync(this.cacheDir))
fs.mkdirSync(this.cacheDir, { recursive: true });
@ -104,7 +108,7 @@ class LocalWhisper {
async client() {
if (!fs.existsSync(this.modelPath)) {
this.#log(
`The native whisper model has never been run and will be downloaded right now. Subsequent runs will be faster. (~250MB)`
`The native whisper model has never been run and will be downloaded right now. Subsequent runs will be faster. (~${this.fileSize})`
);
}

View File

@ -12,18 +12,23 @@ const {
function validSpaceUrl(spaceUrl = "") {
// Atlassian default URL match
const atlassianPattern = new UrlPattern(
"https\\://(:subdomain).atlassian.net/wiki/spaces/(:spaceKey)/*"
"https\\://(:subdomain).atlassian.net/wiki/spaces/(:spaceKey)*"
);
const atlassianMatch = atlassianPattern.match(spaceUrl);
if (atlassianMatch) {
return { valid: true, result: atlassianMatch };
}
// Custom Confluence URL match
const customPattern = new UrlPattern(
"https\\://(:subdomain.):domain.:tld/wiki/spaces/(:spaceKey)/*"
);
const customMatch = customPattern.match(spaceUrl);
let customMatch = null;
[
"https\\://(:subdomain.):domain.:tld/wiki/spaces/(:spaceKey)*", // Custom Confluence space
"https\\://(:subdomain.):domain.:tld/display/(:spaceKey)*", // Custom Confluence space + Human-readable space tag.
].forEach((matchPattern) => {
if (!!customMatch) return;
const pattern = new UrlPattern(matchPattern);
customMatch = pattern.match(spaceUrl);
});
if (customMatch) {
customMatch.customDomain =
(customMatch.subdomain ? `${customMatch.subdomain}.` : "") + //

View File

@ -14,7 +14,11 @@ class RepoLoader {
#validGithubUrl() {
const UrlPattern = require("url-pattern");
const pattern = new UrlPattern(
"https\\://github.com/(:author)/(:project(*))"
"https\\://github.com/(:author)/(:project(*))",
{
// fixes project names with special characters (.github)
segmentValueCharset: "a-zA-Z0-9-._~%/+",
}
);
const match = pattern.match(this.repo);
if (!match) return false;

View File

@ -0,0 +1,159 @@
const { v4 } = require("uuid");
const {
PuppeteerWebBaseLoader,
} = require("langchain/document_loaders/web/puppeteer");
const { default: slugify } = require("slugify");
const { parse } = require("node-html-parser");
const { writeToServerDocuments } = require("../../files");
const { tokenizeString } = require("../../tokenizer");
const path = require("path");
const fs = require("fs");
async function discoverLinks(startUrl, depth = 1, maxLinks = 20) {
const baseUrl = new URL(startUrl).origin;
const discoveredLinks = new Set();
const pendingLinks = [startUrl];
let currentLevel = 0;
depth = depth < 1 ? 1 : depth;
maxLinks = maxLinks < 1 ? 1 : maxLinks;
// Check depth and if there are any links left to scrape
while (currentLevel < depth && pendingLinks.length > 0) {
const newLinks = await getPageLinks(pendingLinks[0], baseUrl);
pendingLinks.shift();
for (const link of newLinks) {
if (!discoveredLinks.has(link)) {
discoveredLinks.add(link);
pendingLinks.push(link);
}
// Exit out if we reach maxLinks
if (discoveredLinks.size >= maxLinks) {
return Array.from(discoveredLinks).slice(0, maxLinks);
}
}
if (pendingLinks.length === 0) {
currentLevel++;
}
}
return Array.from(discoveredLinks);
}
async function getPageLinks(url, baseUrl) {
try {
const loader = new PuppeteerWebBaseLoader(url, {
launchOptions: { headless: "new" },
gotoOptions: { waitUntil: "domcontentloaded" },
});
const docs = await loader.load();
const html = docs[0].pageContent;
const links = extractLinks(html, baseUrl);
return links;
} catch (error) {
console.error(`Failed to get page links from ${url}.`, error);
return [];
}
}
function extractLinks(html, baseUrl) {
const root = parse(html);
const links = root.querySelectorAll("a");
const extractedLinks = new Set();
for (const link of links) {
const href = link.getAttribute("href");
if (href) {
const absoluteUrl = new URL(href, baseUrl).href;
if (absoluteUrl.startsWith(baseUrl)) {
extractedLinks.add(absoluteUrl);
}
}
}
return Array.from(extractedLinks);
}
async function bulkScrapePages(links, outFolderPath) {
const scrapedData = [];
for (let i = 0; i < links.length; i++) {
const link = links[i];
console.log(`Scraping ${i + 1}/${links.length}: ${link}`);
try {
const loader = new PuppeteerWebBaseLoader(link, {
launchOptions: { headless: "new" },
gotoOptions: { waitUntil: "domcontentloaded" },
async evaluate(page, browser) {
const result = await page.evaluate(() => document.body.innerText);
await browser.close();
return result;
},
});
const docs = await loader.load();
const content = docs[0].pageContent;
if (!content.length) {
console.warn(`Empty content for ${link}. Skipping.`);
continue;
}
const url = new URL(link);
const filename = (url.host + "-" + url.pathname).replace(".", "_");
const data = {
id: v4(),
url: "file://" + slugify(filename) + ".html",
title: slugify(filename) + ".html",
docAuthor: "no author found",
description: "No description found.",
docSource: "URL link uploaded by the user.",
chunkSource: `link://${link}`,
published: new Date().toLocaleString(),
wordCount: content.split(" ").length,
pageContent: content,
token_count_estimate: tokenizeString(content).length,
};
writeToServerDocuments(data, data.title, outFolderPath);
scrapedData.push(data);
console.log(`Successfully scraped ${link}.`);
} catch (error) {
console.error(`Failed to scrape ${link}.`, error);
}
}
return scrapedData;
}
async function websiteScraper(startUrl, depth = 1, maxLinks = 20) {
const websiteName = new URL(startUrl).hostname;
const outFolder = slugify(
`${slugify(websiteName)}-${v4().slice(0, 4)}`
).toLowerCase();
const outFolderPath =
process.env.NODE_ENV === "development"
? path.resolve(
__dirname,
`../../../../server/storage/documents/${outFolder}`
)
: path.resolve(process.env.STORAGE_DIR, `documents/${outFolder}`);
console.log("Discovering links...");
const linksToScrape = await discoverLinks(startUrl, depth, maxLinks);
console.log(`Found ${linksToScrape.length} links to scrape.`);
if (!fs.existsSync(outFolderPath))
fs.mkdirSync(outFolderPath, { recursive: true });
console.log("Starting bulk scraping...");
const scrapedData = await bulkScrapePages(linksToScrape, outFolderPath);
console.log(`Scraped ${scrapedData.length} pages.`);
return scrapedData;
}
module.exports = websiteScraper;

View File

@ -82,6 +82,12 @@ GID='1000'
# GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT=4096
# GENERIC_OPEN_AI_API_KEY=sk-123abc
# LLM_PROVIDER='litellm'
# LITE_LLM_MODEL_PREF='gpt-3.5-turbo'
# LITE_LLM_MODEL_TOKEN_LIMIT=4096
# LITE_LLM_BASE_PATH='http://127.0.0.1:4000'
# LITE_LLM_API_KEY='sk-123abc'
# LLM_PROVIDER='cohere'
# COHERE_API_KEY=
# COHERE_MODEL_PREF='command-r'
@ -118,6 +124,10 @@ GID='1000'
# COHERE_API_KEY=
# EMBEDDING_MODEL_PREF='embed-english-v3.0'
# EMBEDDING_ENGINE='voyageai'
# VOYAGEAI_API_KEY=
# EMBEDDING_MODEL_PREF='voyage-large-2-instruct'
###########################################
######## Vector Database Selection ########
###########################################
@ -224,9 +234,11 @@ GID='1000'
#------ Serper.dev ----------- https://serper.dev/
# AGENT_SERPER_DEV_KEY=
#------ Bing Search ----------- https://portal.azure.com/
# AGENT_BING_SEARCH_API_KEY=
###########################################
######## SOCIAL PROVIDERS KEYS ###############
###########################################
# GOOGLE_AUTH_CLIENT_ID=
# GOOGLE_AUTH_CLIENT_ID=

View File

@ -1,4 +1,8 @@
<!doctype html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<html lang="en">
<body>

View File

@ -28,18 +28,21 @@ export default function App() {
const position = embedSettings.position || "bottom-right";
const windowWidth = embedSettings.windowWidth
? `md:max-w-[${embedSettings.windowWidth}]`
: "md:max-w-[400px]";
? `max-w-[${embedSettings.windowWidth}]`
: "max-w-[400px]";
const windowHeight = embedSettings.windowHeight
? `md:max-h-[${embedSettings.windowHeight}]`
: "md:max-h-[700px]";
? `max-h-[${embedSettings.windowHeight}]`
: "max-h-[700px]";
return (
<>
<Head />
<div className={`fixed inset-0 z-50 ${isChatOpen ? "block" : "hidden"}`}>
<div
id="anything-llm-embed-chat-container"
className={`fixed inset-0 z-50 ${isChatOpen ? "block" : "hidden"}`}
>
<div
className={`${windowHeight} ${windowWidth} h-full w-full bg-white md:fixed md:bottom-0 md:right-0 md:mb-4 md:mr-4 md:rounded-2xl md:border md:border-gray-300 md:shadow-[0_4px_14px_rgba(0,0,0,0.25)] ${positionClasses[position]}`}
className={`${windowHeight} ${windowWidth} h-full w-full bg-white fixed bottom-0 right-0 mb-4 md:mr-4 rounded-2xl border border-gray-300 shadow-[0_4px_14px_rgba(0,0,0,0.25)] ${positionClasses[position]}`}
id="anything-llm-chat"
>
{isChatOpen && (
@ -53,6 +56,7 @@ export default function App() {
</div>
{!isChatOpen && (
<div
id="anything-llm-embed-chat-button-container"
className={`fixed bottom-0 ${positionClasses[position]} mb-4 z-50`}
>
<OpenButton

View File

@ -23,6 +23,7 @@ export default function OpenButton({ settings, isOpen, toggleOpen }) {
: CHAT_ICONS.plus;
return (
<button
id="anything-llm-embed-chat-button"
onClick={toggleOpen}
className={`flex items-center justify-center p-4 rounded-full bg-[${settings.buttonColor}] text-white text-2xl`}
aria-label="Toggle Menu"

2
frontend/.gitignore vendored
View File

@ -9,10 +9,8 @@ lerna-debug.log*
node_modules
dist
lib
dist-ssr
*.local
!frontend/components/lib
# Editor directories and files
.vscode/*

File diff suppressed because one or more lines are too long

View File

@ -1,27 +1,41 @@
import { createContext, useEffect, useState } from "react";
import AnythingLLM from "./media/logo/anything-llm.png";
import DefaultLoginLogo from "./media/illustrations/login-logo.svg";
import System from "./models/system";
export const LogoContext = createContext();
export function LogoProvider({ children }) {
const [logo, setLogo] = useState("");
const [loginLogo, setLoginLogo] = useState("");
const [isCustomLogo, setIsCustomLogo] = useState(false);
useEffect(() => {
async function fetchInstanceLogo() {
try {
const logoURL = await System.fetchLogo();
logoURL ? setLogo(logoURL) : setLogo(AnythingLLM);
const { isCustomLogo, logoURL } = await System.fetchLogo();
if (logoURL) {
setLogo(logoURL);
setLoginLogo(isCustomLogo ? logoURL : DefaultLoginLogo);
setIsCustomLogo(isCustomLogo);
} else {
setLogo(AnythingLLM);
setLoginLogo(DefaultLoginLogo);
setIsCustomLogo(false);
}
} catch (err) {
setLogo(AnythingLLM);
setLoginLogo(DefaultLoginLogo);
setIsCustomLogo(false);
console.error("Failed to fetch logo:", err);
}
}
fetchInstanceLogo();
}, []);
return (
<LogoContext.Provider value={{ logo, setLogo }}>
<LogoContext.Provider value={{ logo, setLogo, loginLogo, isCustomLogo }}>
{children}
</LogoContext.Provider>
);

View File

@ -9,9 +9,7 @@ export default function ChatBubble({ message, type, popMsg }) {
return (
<div className={`flex justify-center items-end w-full ${backgroundColor}`}>
<div
className={`py-8 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
>
<div className={`py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}>
<div className="flex gap-x-5">
<Jazzicon
size={36}

View File

@ -1,10 +1,12 @@
import Github from "./github.svg";
import YouTube from "./youtube.svg";
import Link from "./link.svg";
import Confluence from "./confluence.jpeg";
const ConnectorImages = {
github: Github,
youtube: YouTube,
websiteDepth: Link,
confluence: Confluence,
};

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -43,7 +43,7 @@ export default function DefaultChatContainer() {
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR} md:mt-0 mt-[40px]`}
>
<div
className={`pt-10 pb-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
className={`pt-10 pb-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
@ -67,7 +67,7 @@ export default function DefaultChatContainer() {
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
>
<div
className={`pb-4 pt-2 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
className={`pb-4 pt-2 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
@ -90,7 +90,7 @@ export default function DefaultChatContainer() {
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
>
<div
className={`pt-2 pb-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
className={`pt-2 pb-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
@ -124,7 +124,7 @@ export default function DefaultChatContainer() {
className={`flex justify-center items-end w-full ${USER_BACKGROUND_COLOR}`}
>
<div
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon
@ -148,7 +148,7 @@ export default function DefaultChatContainer() {
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
>
<div
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
@ -185,7 +185,7 @@ export default function DefaultChatContainer() {
className={`flex justify-center items-end w-full ${USER_BACKGROUND_COLOR}`}
>
<div
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon
@ -210,7 +210,7 @@ export default function DefaultChatContainer() {
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
>
<div
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
@ -248,7 +248,7 @@ export default function DefaultChatContainer() {
className={`flex justify-center items-end w-full ${USER_BACKGROUND_COLOR}`}
>
<div
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon
@ -272,7 +272,7 @@ export default function DefaultChatContainer() {
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
>
<div
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />

View File

@ -0,0 +1,50 @@
export default function VoyageAiOptions({ settings }) {
return (
<div className="w-full flex flex-col gap-y-4">
<div className="w-full flex items-center gap-4">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
API Key
</label>
<input
type="password"
name="VoyageAiApiKey"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="Voyage AI API Key"
defaultValue={settings?.VoyageAiApiKey ? "*".repeat(20) : ""}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Model Preference
</label>
<select
name="EmbeddingModelPref"
required={true}
defaultValue={settings?.EmbeddingModelPref}
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
<optgroup label="Available embedding models">
{[
"voyage-large-2-instruct",
"voyage-law-2",
"voyage-code-2",
"voyage-large-2",
"voyage-2",
].map((model) => {
return (
<option key={model} value={model}>
{model}
</option>
);
})}
</optgroup>
</select>
</div>
</div>
</div>
);
}

View File

@ -14,6 +14,8 @@ import {
import React, { useEffect, useState } from "react";
import SettingsButton from "../SettingsButton";
import { isMobile } from "react-device-detect";
import { Tooltip } from "react-tooltip";
import { v4 } from "uuid";
export const MAX_ICONS = 3;
export const ICON_COMPONENTS = {
@ -47,36 +49,48 @@ export default function Footer() {
return (
<div className="flex justify-center mb-2">
<div className="flex space-x-4">
<a
href={paths.github()}
target="_blank"
rel="noreferrer"
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
aria-label="Find us on Github"
>
<GithubLogo weight="fill" className="h-5 w-5 " />
</a>
<a
href={paths.docs()}
target="_blank"
rel="noreferrer"
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
aria-label="Docs"
>
<BookOpen weight="fill" className="h-5 w-5 " />
</a>
<a
href={paths.discord()}
target="_blank"
rel="noreferrer"
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
aria-label="Join our Discord server"
>
<DiscordLogo
weight="fill"
className="h-5 w-5 stroke-slate-200 group-hover:stroke-slate-200"
/>
</a>
<ToolTipWrapper id="open-github">
<a
href={paths.github()}
target="_blank"
rel="noreferrer"
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
aria-label="Find us on Github"
data-tooltip-id="open-github"
data-tooltip-content="View source code on Github"
>
<GithubLogo weight="fill" className="h-5 w-5 " />
</a>
</ToolTipWrapper>
<ToolTipWrapper id="open-documentation">
<a
href={paths.docs()}
target="_blank"
rel="noreferrer"
className="w-fit transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
aria-label="Docs"
data-tooltip-id="open-documentation"
data-tooltip-content="Open AnythingLLM help docs"
>
<BookOpen weight="fill" className="h-5 w-5 " />
</a>
</ToolTipWrapper>
<ToolTipWrapper id="open-discord">
<a
href={paths.discord()}
target="_blank"
rel="noreferrer"
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
aria-label="Join our Discord server"
data-tooltip-id="open-discord"
data-tooltip-content="Join the AnythingLLM Discord"
>
<DiscordLogo
weight="fill"
className="h-5 w-5 stroke-slate-200 group-hover:stroke-slate-200"
/>
</a>
</ToolTipWrapper>
{!isMobile && <SettingsButton />}
</div>
</div>
@ -94,10 +108,13 @@ export default function Footer() {
rel="noreferrer"
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
{React.createElement(ICON_COMPONENTS[item.icon], {
weight: "fill",
className: "h-5 w-5",
})}
{React.createElement(
ICON_COMPONENTS?.[item.icon] ?? ICON_COMPONENTS.Info,
{
weight: "fill",
className: "h-5 w-5",
}
)}
</a>
))}
{!isMobile && <SettingsButton />}
@ -105,3 +122,17 @@ export default function Footer() {
</div>
);
}
export function ToolTipWrapper({ id = v4(), children }) {
return (
<div className="flex w-fit">
{children}
<Tooltip
id={id}
place="top"
delayShow={300}
className="tooltip !text-xs z-99"
/>
</div>
);
}

View File

@ -19,25 +19,52 @@ export default function GeminiLLMOptions({ settings }) {
</div>
{!settings?.credentialsOnly && (
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Chat Model Selection
</label>
<select
name="GeminiLLMModelPref"
defaultValue={settings?.GeminiLLMModelPref || "gemini-pro"}
required={true}
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
{["gemini-pro", "gemini-1.5-pro-latest"].map((model) => {
return (
<option key={model} value={model}>
{model}
</option>
);
})}
</select>
</div>
<>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Chat Model Selection
</label>
<select
name="GeminiLLMModelPref"
defaultValue={settings?.GeminiLLMModelPref || "gemini-pro"}
required={true}
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
{[
"gemini-pro",
"gemini-1.0-pro",
"gemini-1.5-pro-latest",
"gemini-1.5-flash-latest",
].map((model) => {
return (
<option key={model} value={model}>
{model}
</option>
);
})}
</select>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Safety Setting
</label>
<select
name="GeminiSafetySetting"
defaultValue={
settings?.GeminiSafetySetting || "BLOCK_MEDIUM_AND_ABOVE"
}
required={true}
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
<option value="BLOCK_NONE">None</option>
<option value="BLOCK_ONLY_HIGH">Block few</option>
<option value="BLOCK_MEDIUM_AND_ABOVE">
Block some (default)
</option>
<option value="BLOCK_LOW_AND_ABOVE">Block most</option>
</select>
</div>
</>
)}
</div>
</div>

View File

@ -1,80 +1,84 @@
export default function GenericOpenAiOptions({ settings }) {
return (
<div className="flex gap-4 flex-wrap">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Base URL
</label>
<input
type="url"
name="GenericOpenAiBasePath"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="eg: https://proxy.openai.com"
defaultValue={settings?.GenericOpenAiBasePath}
required={true}
autoComplete="off"
spellCheck={false}
/>
<div className="flex flex-col gap-y-4">
<div className="flex gap-4 flex-wrap">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Base URL
</label>
<input
type="url"
name="GenericOpenAiBasePath"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="eg: https://proxy.openai.com"
defaultValue={settings?.GenericOpenAiBasePath}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
API Key
</label>
<input
type="password"
name="GenericOpenAiKey"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="Generic service API Key"
defaultValue={settings?.GenericOpenAiKey ? "*".repeat(20) : ""}
required={false}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Chat Model Name
</label>
<input
type="text"
name="GenericOpenAiModelPref"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="Model id used for chat requests"
defaultValue={settings?.GenericOpenAiModelPref}
required={true}
autoComplete="off"
/>
</div>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
API Key
</label>
<input
type="password"
name="GenericOpenAiKey"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="Generic service API Key"
defaultValue={settings?.GenericOpenAiKey ? "*".repeat(20) : ""}
required={false}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Chat Model Name
</label>
<input
type="text"
name="GenericOpenAiModelPref"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="Model id used for chat requests"
defaultValue={settings?.GenericOpenAiModelPref}
required={true}
autoComplete="off"
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Token context window
</label>
<input
type="number"
name="GenericOpenAiTokenLimit"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="Content window limit (eg: 4096)"
min={1}
onScroll={(e) => e.target.blur()}
defaultValue={settings?.GenericOpenAiTokenLimit}
required={true}
autoComplete="off"
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Max Tokens
</label>
<input
type="number"
name="GenericOpenAiMaxTokens"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="Max tokens per request (eg: 1024)"
min={1}
defaultValue={settings?.GenericOpenAiMaxTokens || 1024}
required={true}
autoComplete="off"
/>
<div className="flex gap-x-4 flex-wrap">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Token context window
</label>
<input
type="number"
name="GenericOpenAiTokenLimit"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="Content window limit (eg: 4096)"
min={1}
onScroll={(e) => e.target.blur()}
defaultValue={settings?.GenericOpenAiTokenLimit}
required={true}
autoComplete="off"
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Max Tokens
</label>
<input
type="number"
name="GenericOpenAiMaxTokens"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="Max tokens per request (eg: 1024)"
min={1}
defaultValue={settings?.GenericOpenAiMaxTokens || 1024}
required={true}
autoComplete="off"
/>
</div>
</div>
</div>
);

View File

@ -0,0 +1,148 @@
import { useEffect, useState } from "react";
import System from "@/models/system";
export default function LiteLLMOptions({ settings }) {
const [basePathValue, setBasePathValue] = useState(settings?.LiteLLMBasePath);
const [basePath, setBasePath] = useState(settings?.LiteLLMBasePath);
const [apiKeyValue, setApiKeyValue] = useState(settings?.LiteLLMAPIKey);
const [apiKey, setApiKey] = useState(settings?.LiteLLMAPIKey);
return (
<div className="w-full flex flex-col gap-y-4">
<div className="w-full flex items-center gap-4">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Base URL
</label>
<input
type="url"
name="LiteLLMBasePath"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="http://127.0.0.1:4000"
defaultValue={settings?.LiteLLMBasePath}
required={true}
autoComplete="off"
spellCheck={false}
onChange={(e) => setBasePathValue(e.target.value)}
onBlur={() => setBasePath(basePathValue)}
/>
</div>
<LiteLLMModelSelection
settings={settings}
basePath={basePath}
apiKey={apiKey}
/>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Token context window
</label>
<input
type="number"
name="LiteLLMTokenLimit"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="4096"
min={1}
onScroll={(e) => e.target.blur()}
defaultValue={settings?.LiteLLMTokenLimit}
required={true}
autoComplete="off"
/>
</div>
</div>
<div className="w-full flex items-center gap-4">
<div className="flex flex-col w-60">
<div className="flex flex-col gap-y-1 mb-4">
<label className="text-white text-sm font-semibold flex items-center gap-x-2">
API Key <p className="!text-xs !italic !font-thin">optional</p>
</label>
</div>
<input
type="password"
name="LiteLLMAPIKey"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="sk-mysecretkey"
defaultValue={settings?.LiteLLMAPIKey ? "*".repeat(20) : ""}
autoComplete="off"
spellCheck={false}
onChange={(e) => setApiKeyValue(e.target.value)}
onBlur={() => setApiKey(apiKeyValue)}
/>
</div>
</div>
</div>
);
}
function LiteLLMModelSelection({ settings, basePath = null, apiKey = null }) {
const [customModels, setCustomModels] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function findCustomModels() {
if (!basePath) {
setCustomModels([]);
setLoading(false);
return;
}
setLoading(true);
const { models } = await System.customModels(
"litellm",
typeof apiKey === "boolean" ? null : apiKey,
basePath
);
setCustomModels(models || []);
setLoading(false);
}
findCustomModels();
}, [basePath, apiKey]);
if (loading || customModels.length == 0) {
return (
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Chat Model Selection
</label>
<select
name="LiteLLMModelPref"
disabled={true}
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
<option disabled={true} selected={true}>
{basePath?.includes("/v1")
? "-- loading available models --"
: "-- waiting for URL --"}
</option>
</select>
</div>
);
}
return (
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Chat Model Selection
</label>
<select
name="LiteLLMModelPref"
required={true}
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
{customModels.length > 0 && (
<optgroup label="Your loaded models">
{customModels.map((model) => {
return (
<option
key={model.id}
value={model.id}
selected={settings.LiteLLMModelPref === model.id}
>
{model.id}
</option>
);
})}
</optgroup>
)}
</select>
</div>
);
}

View File

@ -3,7 +3,7 @@ import System from "@/models/system";
import showToast from "@/utils/toast";
import pluralize from "pluralize";
import { TagsInput } from "react-tag-input-component";
import { Warning } from "@phosphor-icons/react";
import { Info, Warning } from "@phosphor-icons/react";
import { Tooltip } from "react-tooltip";
const DEFAULT_BRANCHES = ["main", "master"];
@ -92,45 +92,7 @@ export default function GithubOptions() {
<p className="font-bold text-white">Github Access Token</p>{" "}
<p className="text-xs text-white/50 font-light flex items-center">
optional
{!accessToken && (
<Warning
size={14}
className="ml-1 text-orange-500 cursor-pointer"
data-tooltip-id="access-token-tooltip"
data-tooltip-place="right"
/>
)}
<Tooltip
delayHide={300}
id="access-token-tooltip"
className="max-w-xs"
clickable={true}
>
<p className="text-sm">
Without a{" "}
<a
href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens"
rel="noreferrer"
target="_blank"
className="underline"
onClick={(e) => e.stopPropagation()}
>
Personal Access Token
</a>
, the GitHub API may limit the number of files that
can be collected due to rate limits. You can{" "}
<a
href="https://github.com/settings/personal-access-tokens/new"
rel="noreferrer"
target="_blank"
className="underline"
onClick={(e) => e.stopPropagation()}
>
create a temporary Access Token
</a>{" "}
to avoid this issue.
</p>
</Tooltip>
<PATTooltip accessToken={accessToken} />
</p>
</label>
<p className="text-xs font-normal text-white/50">
@ -180,6 +142,7 @@ export default function GithubOptions() {
</div>
<div className="flex flex-col gap-y-2 w-full pr-10">
<PATAlert accessToken={accessToken} />
<button
type="submit"
disabled={loading}
@ -269,3 +232,78 @@ function GitHubBranchSelection({ repo, accessToken }) {
</div>
);
}
function PATAlert({ accessToken }) {
if (!!accessToken) return null;
return (
<div className="flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2">
<div className="gap-x-2 flex items-center">
<Info className="shrink-0" size={25} />
<p className="text-sm">
Without filling out the <b>Github Access Token</b> this data connector
will only be able to collect the <b>top-level</b> files of the repo
due to GitHub's public API rate-limits.
<br />
<br />
<a
href="https://github.com/settings/personal-access-tokens/new"
rel="noreferrer"
target="_blank"
className="underline"
onClick={(e) => e.stopPropagation()}
>
{" "}
Get a free Personal Access Token with a GitHub account here.
</a>
</p>
</div>
</div>
);
}
function PATTooltip({ accessToken }) {
if (!!accessToken) return null;
return (
<>
{!accessToken && (
<Warning
size={14}
className="ml-1 text-orange-500 cursor-pointer"
data-tooltip-id="access-token-tooltip"
data-tooltip-place="right"
/>
)}
<Tooltip
delayHide={300}
id="access-token-tooltip"
className="max-w-xs"
clickable={true}
>
<p className="text-sm">
Without a{" "}
<a
href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens"
rel="noreferrer"
target="_blank"
className="underline"
onClick={(e) => e.stopPropagation()}
>
Personal Access Token
</a>
, the GitHub API may limit the number of files that can be collected
due to rate limits. You can{" "}
<a
href="https://github.com/settings/personal-access-tokens/new"
rel="noreferrer"
target="_blank"
className="underline"
onClick={(e) => e.stopPropagation()}
>
create a temporary Access Token
</a>{" "}
to avoid this issue.
</p>
</Tooltip>
</>
);
}

View File

@ -0,0 +1,134 @@
import React, { useState } from "react";
import System from "@/models/system";
import showToast from "@/utils/toast";
import pluralize from "pluralize";
export default function WebsiteDepthOptions() {
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
const form = new FormData(e.target);
try {
setLoading(true);
showToast("Scraping website - this may take a while.", "info", {
clear: true,
autoClose: false,
});
const { data, error } = await System.dataConnectors.websiteDepth.scrape({
url: form.get("url"),
depth: parseInt(form.get("depth")),
maxLinks: parseInt(form.get("maxLinks")),
});
if (!!error) {
showToast(error, "error", { clear: true });
setLoading(false);
return;
}
showToast(
`Successfully scraped ${data.length} ${pluralize(
"page",
data.length
)}!`,
"success",
{ clear: true }
);
e.target.reset();
setLoading(false);
} catch (e) {
console.error(e);
showToast(e.message, "error", { clear: true });
setLoading(false);
}
};
return (
<div className="flex w-full">
<div className="flex flex-col w-full px-1 md:pb-6 pb-16">
<form className="w-full" onSubmit={handleSubmit}>
<div className="w-full flex flex-col py-2">
<div className="w-full flex flex-col gap-4">
<div className="flex flex-col pr-10">
<div className="flex flex-col gap-y-1 mb-4">
<label className="text-white text-sm font-bold">
Website URL
</label>
<p className="text-xs font-normal text-white/50">
URL of the website you want to scrape.
</p>
</div>
<input
type="url"
name="url"
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="https://example.com"
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col pr-10">
<div className="flex flex-col gap-y-1 mb-4">
<label className="text-white text-sm font-bold">Depth</label>
<p className="text-xs font-normal text-white/50">
This is the number of child-links that the worker should
follow from the origin URL.
</p>
</div>
<input
type="number"
name="depth"
min="1"
max="5"
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
required={true}
defaultValue="1"
/>
</div>
<div className="flex flex-col pr-10">
<div className="flex flex-col gap-y-1 mb-4">
<label className="text-white text-sm font-bold">
Max Links
</label>
<p className="text-xs font-normal text-white/50">
Maximum number of links to scrape.
</p>
</div>
<input
type="number"
name="maxLinks"
min="1"
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
required={true}
defaultValue="20"
/>
</div>
</div>
</div>
<div className="flex flex-col gap-y-2 w-full pr-10">
<button
type="submit"
disabled={loading}
className={`mt-2 w-full ${
loading ? "cursor-not-allowed animate-pulse" : ""
} justify-center border border-slate-200 px-4 py-2 rounded-lg text-[#222628] text-sm font-bold items-center flex gap-x-2 bg-slate-200 hover:bg-slate-300 hover:text-slate-800 disabled:bg-slate-300 disabled:cursor-not-allowed`}
>
{loading ? "Scraping website..." : "Submit"}
</button>
{loading && (
<p className="text-xs text-white/50">
Once complete, all scraped pages will be available for embedding
into workspaces in the document picker.
</p>
)}
</div>
</form>
</div>
</div>
);
}

View File

@ -5,6 +5,7 @@ import YoutubeOptions from "./Connectors/Youtube";
import ConfluenceOptions from "./Connectors/Confluence";
import { useState } from "react";
import ConnectorOption from "./ConnectorOption";
import WebsiteDepthOptions from "./Connectors/WebsiteDepth";
export const DATA_CONNECTORS = {
github: {
@ -21,6 +22,12 @@ export const DATA_CONNECTORS = {
"Import the transcription of an entire YouTube video from a link.",
options: <YoutubeOptions />,
},
"website-depth": {
name: "Bulk Link Scraper",
image: ConnectorImages.websiteDepth,
description: "Scrape a website and its sub-links up to a certain depth.",
options: <WebsiteDepthOptions />,
},
confluence: {
name: "Confluence",
image: ConnectorImages.confluence,

View File

@ -169,6 +169,7 @@ export default function MultiUserAuth() {
const [token, setToken] = useState(null);
const [showRecoveryForm, setShowRecoveryForm] = useState(false);
const [showResetPasswordForm, setShowResetPasswordForm] = useState(false);
const [customAppName, setCustomAppName] = useState(null);
const {
isOpen: isRecoveryCodeModalOpen,
@ -251,6 +252,15 @@ export default function MultiUserAuth() {
}
}, [downloadComplete, user, token]);
useEffect(() => {
const fetchCustomAppName = async () => {
const { appName } = await System.fetchCustomAppName();
setCustomAppName(appName || "");
setLoading(false);
};
fetchCustomAppName();
}, []);
if (showRecoveryForm) {
return (
<RecoveryForm
@ -273,11 +283,11 @@ export default function MultiUserAuth() {
Welcome to
</h3>
<p className="text-4xl md:text-2xl font-bold bg-gradient-to-r from-[#75D6FF] via-[#FFFFFF] to-[#FFFFFF] bg-clip-text text-transparent">
AnythingLLM
{customAppName || "AnythingLLM"}
</p>
</div>
<p className="text-sm text-white/90 text-center">
Sign in to your AnythingLLM account.
Sign in to your {customAppName || "AnythingLLM"} account.
</p>
</div>
</div>

View File

@ -1,7 +1,6 @@
import React, { useEffect, useState } from "react";
import System from "../../../models/system";
import { AUTH_TOKEN } from "../../../utils/constants";
import useLogo from "../../../hooks/useLogo";
import paths from "../../../utils/paths";
import ModalWrapper from "@/components/ModalWrapper";
import { useModal } from "@/hooks/useModal";
@ -10,10 +9,10 @@ import RecoveryCodeModal from "@/components/Modals/DisplayRecoveryCodeModal";
export default function SingleUserAuth() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const { logo: _initLogo } = useLogo();
const [recoveryCodes, setRecoveryCodes] = useState([]);
const [downloadComplete, setDownloadComplete] = useState(false);
const [token, setToken] = useState(null);
const [customAppName, setCustomAppName] = useState(null);
const {
isOpen: isRecoveryCodeModalOpen,
@ -57,6 +56,15 @@ export default function SingleUserAuth() {
}
}, [downloadComplete, token]);
useEffect(() => {
const fetchCustomAppName = async () => {
const { appName } = await System.fetchCustomAppName();
setCustomAppName(appName || "");
setLoading(false);
};
fetchCustomAppName();
}, []);
return (
<>
<form onSubmit={handleLogin}>
@ -68,11 +76,11 @@ export default function SingleUserAuth() {
Welcome to
</h3>
<p className="text-4xl md:text-2xl font-bold bg-gradient-to-r from-[#75D6FF] via-[#FFFFFF] to-[#FFFFFF] bg-clip-text text-transparent">
AnythingLLM
{customAppName || "AnythingLLM"}
</p>
</div>
<p className="text-sm text-white/90 text-center">
Sign in to your AnythingLLM instance.
Sign in to your {customAppName || "AnythingLLM"} instance.
</p>
</div>
</div>

View File

@ -9,10 +9,9 @@ import {
} from "../../../utils/constants";
import useLogo from "../../../hooks/useLogo";
import illustration from "@/media/illustrations/login-illustration.svg";
import loginLogo from "@/media/illustrations/login-logo.svg";
export default function PasswordModal({ mode = "single" }) {
const { logo: _initLogo } = useLogo();
const { loginLogo } = useLogo();
return (
<div className="fixed top-0 left-0 right-0 z-50 w-full overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] h-full bg-[#25272C] flex flex-col md:flex-row items-center justify-center">
<div
@ -37,10 +36,11 @@ export default function PasswordModal({ mode = "single" }) {
<div className="flex flex-col items-center justify-center h-full w-full md:w-1/2 z-50 relative">
<img
src={loginLogo}
className={`mb-8 w-[84px] h-[84px] absolute ${
mode === "single" ? "md:top-50" : "md:top-36"
} top-44 z-30`}
alt="logo"
alt="Logo"
className={`hidden md:flex rounded-2xl w-fit m-4 z-30 ${
mode === "single" ? "md:top-[170px]" : "md:top-36"
} absolute max-h-[65px] md:bg-login-gradient md:shadow-[0_4px_14px_rgba(0,0,0,0.25)]`}
style={{ objectFit: "contain" }}
/>
{mode === "single" ? <SingleUserAuth /> : <MultiUserAuth />}
</div>

View File

@ -3,6 +3,7 @@ import paths from "@/utils/paths";
import { ArrowUUpLeft, Wrench } from "@phosphor-icons/react";
import { Link } from "react-router-dom";
import { useMatch } from "react-router-dom";
import { ToolTipWrapper } from "../Footer";
export default function SettingsButton() {
const isInSettings = !!useMatch("/settings/*");
@ -12,22 +13,32 @@ export default function SettingsButton() {
if (isInSettings)
return (
<Link
to={paths.home()}
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
aria-label="Home"
>
<ArrowUUpLeft className="h-5 w-5" weight="fill" />
</Link>
<ToolTipWrapper id="go-home">
<Link
to={paths.home()}
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
aria-label="Home"
data-tooltip-id="go-home"
data-tooltip-content="Back to workspaces"
>
<ArrowUUpLeft className="h-5 w-5" weight="fill" />
</Link>
</ToolTipWrapper>
);
return (
<Link
to={!!user?.role ? paths.settings.system() : paths.settings.appearance()}
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
aria-label="Settings"
>
<Wrench className="h-5 w-5" weight="fill" />
</Link>
<ToolTipWrapper id="open-settings">
<Link
to={
!!user?.role ? paths.settings.system() : paths.settings.appearance()
}
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
aria-label="Settings"
data-tooltip-id="open-settings"
data-tooltip-content="Open settings"
>
<Wrench className="h-5 w-5" weight="fill" />
</Link>
</ToolTipWrapper>
);
}

View File

@ -329,7 +329,7 @@ const SidebarOptions = ({ user = null }) => (
<Option
href={paths.settings.embedSetup()}
childLinks={[paths.settings.embedChats()]}
btnText="Embedded Chat"
btnText="Chat Embed Widgets"
icon={<CodeBlock className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
@ -338,7 +338,7 @@ const SidebarOptions = ({ user = null }) => (
<>
<Option
href={paths.settings.embedChats()}
btnText="Embedded Chat History"
btnText="Chat Embed History"
icon={<Barcode className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}

View File

@ -22,11 +22,17 @@ export default function ThreadContainer({ workspace }) {
fetchThreads();
}, [workspace.slug]);
// Enable toggling of meta-key (ctrl on win and cmd/fn on others)
// Enable toggling of bulk-deletion by holding meta-key (ctrl on win and cmd/fn on others)
useEffect(() => {
const handleKeyDown = (event) => {
if (["Control", "Meta"].includes(event.key)) {
setCtrlPressed((prev) => !prev);
setCtrlPressed(true);
}
};
const handleKeyUp = (event) => {
if (["Control", "Meta"].includes(event.key)) {
setCtrlPressed(false);
// when toggling, unset bulk progress so
// previously marked threads that were never deleted
// come back to life.
@ -37,9 +43,13 @@ export default function ThreadContainer({ workspace }) {
);
}
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
};
}, []);
@ -56,7 +66,6 @@ export default function ThreadContainer({ workspace }) {
const slugs = threads.filter((t) => t.deleted === true).map((t) => t.slug);
await Workspace.threads.deleteBulk(workspace.slug, slugs);
setThreads((prev) => prev.filter((t) => !t.deleted));
setCtrlPressed(false);
};
function removeThread(threadId) {
@ -89,6 +98,7 @@ export default function ThreadContainer({ workspace }) {
)
? threads.findIndex((thread) => thread?.slug === threadSlug) + 1
: 0;
return (
<div className="flex flex-col" role="list" aria-label="Threads">
<ThreadItem

View File

@ -84,6 +84,7 @@ function ElevenLabsModelSelection({ apiKey, settings }) {
<select
name="TTSElevenLabsVoiceModel"
required={true}
defaultValue={settings?.TTSElevenLabsVoiceModel}
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
{Object.keys(groupedModels)
@ -91,11 +92,7 @@ function ElevenLabsModelSelection({ apiKey, settings }) {
.map((organization) => (
<optgroup key={organization} label={organization}>
{groupedModels[organization].map((model) => (
<option
key={model.id}
value={model.id}
selected={settings?.OpenAiModelPref === model.id}
>
<option key={model.id} value={model.id}>
{model.name}
</option>
))}

View File

@ -35,7 +35,11 @@ export default function OpenAiTextToSpeechOptions({ settings }) {
>
{["alloy", "echo", "fable", "onyx", "nova", "shimmer"].map(
(voice) => {
return <option value={voice}>{toProperCase(voice)}</option>;
return (
<option key={voice} value={voice}>
{toProperCase(voice)}
</option>
);
}
)}
</select>

View File

@ -1,38 +1,89 @@
import { Gauge } from "@phosphor-icons/react";
export default function NativeTranscriptionOptions() {
import { useState } from "react";
export default function NativeTranscriptionOptions({ settings }) {
const [model, setModel] = useState(settings?.WhisperModelPref);
return (
<div className="w-full flex flex-col gap-y-4">
<div className="flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2">
<div className="gap-x-2 flex items-center">
<Gauge size={25} />
<p className="text-sm">
Using the local whisper model on machines with limited RAM or CPU
can stall AnythingLLM when processing media files.
<br />
We recommend at least 2GB of RAM and upload files &lt;10Mb.
<br />
<br />
<i>
The built-in model will automatically download on the first use.
</i>
</p>
</div>
</div>
<LocalWarning model={model} />
<div className="w-full flex items-center gap-4">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Model Selection
</label>
<select
disabled={true}
name="WhisperModelPref"
defaultValue={model}
onChange={(e) => setModel(e.target.value)}
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
<option disabled={true} selected={true}>
Xenova/whisper-small
</option>
{["Xenova/whisper-small", "Xenova/whisper-large"].map(
(value, i) => {
return (
<option key={i} value={value}>
{value}
</option>
);
}
)}
</select>
</div>
</div>
</div>
);
}
function LocalWarning({ model }) {
switch (model) {
case "Xenova/whisper-small":
return <WhisperSmall />;
case "Xenova/whisper-large":
return <WhisperLarge />;
default:
return <WhisperSmall />;
}
}
function WhisperSmall() {
return (
<div className="flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2">
<div className="gap-x-2 flex items-center">
<Gauge size={25} />
<p className="text-sm">
Running the <b>whisper-small</b> model on a machine with limited RAM
or CPU can stall AnythingLLM when processing media files.
<br />
We recommend at least 2GB of RAM and upload files &lt;10Mb.
<br />
<br />
<i>
This model will automatically download on the first use. (250mb)
</i>
</p>
</div>
</div>
);
}
function WhisperLarge() {
return (
<div className="flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2">
<div className="gap-x-2 flex items-center">
<Gauge size={25} />
<p className="text-sm">
Using the <b>whisper-large</b> model on machines with limited RAM or
CPU can stall AnythingLLM when processing media files. This model is
substantially larger than the whisper-small.
<br />
We recommend at least 8GB of RAM and upload files &lt;10Mb.
<br />
<br />
<i>
This model will automatically download on the first use. (1.56GB)
</i>
</p>
</div>
</div>
);
}

View File

@ -368,7 +368,7 @@ export function Chartable({ props, workspace }) {
if (!!props.chatId) {
return (
<div className="flex justify-center items-end w-full">
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className="flex gap-x-5">
<WorkspaceProfileImage workspace={workspace} />
<div className="relative w-full">
@ -389,7 +389,7 @@ export function Chartable({ props, workspace }) {
return (
<div className="flex justify-center items-end w-full">
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className="relative w-full">
<DownloadGraph onClick={handleDownload} />
<div ref={ref}>{renderChart()}</div>

View File

@ -115,6 +115,11 @@ function SkeletonLine() {
);
}
function omitChunkHeader(text) {
if (!text.startsWith("<document_metadata>")) return text;
return text.split("</document_metadata>")[1].trim();
}
function CitationDetailModal({ source, onClose }) {
const { references, title, chunks } = source;
const { isUrl, text: webpageUrl, href: linkTo } = parseChunkSource(source);
@ -167,7 +172,7 @@ function CitationDetailModal({ source, onClose }) {
<div key={idx} className="pt-6 text-white">
<div className="flex flex-col w-full justify-start pb-6 gap-y-1">
<p className="text-white whitespace-pre-line">
{HTMLDecode(text)}
{HTMLDecode(omitChunkHeader(text))}
</p>
{!!score && (

View File

@ -32,14 +32,13 @@ const Actions = ({
<div className="flex w-full justify-between items-center">
<div className="flex justify-start items-center gap-x-4">
<CopyMessage message={message} />
{isLastMessage &&
!message?.includes("Workspace chat memory was reset!") && (
<RegenerateMessage
regenerateMessage={regenerateMessage}
slug={slug}
chatId={chatId}
/>
)}
{isLastMessage && (
<RegenerateMessage
regenerateMessage={regenerateMessage}
slug={slug}
chatId={chatId}
/>
)}
{chatId && (
<>
<FeedbackButton
@ -127,6 +126,7 @@ function CopyMessage({ message }) {
}
function RegenerateMessage({ regenerateMessage, chatId }) {
if (!chatId) return null;
return (
<div className="mt-3 relative">
<button

View File

@ -29,9 +29,7 @@ const HistoricalMessage = ({
role === "user" ? USER_BACKGROUND_COLOR : AI_BACKGROUND_COLOR
}`}
>
<div
className={`py-8 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
>
<div className={`py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}>
<div className="flex gap-x-5">
<ProfileImage role={role} workspace={workspace} />
{error ? (
@ -57,7 +55,7 @@ const HistoricalMessage = ({
<div className="flex gap-x-5">
<div className="relative w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden" />
<Actions
message={DOMPurify.sanitize(message)}
message={message}
feedbackScore={feedbackScore}
chatId={chatId}
slug={workspace?.slug}

View File

@ -21,7 +21,7 @@ const PromptReply = ({
<div
className={`flex justify-center items-end w-full ${assistantBackgroundColor}`}
>
<div className="py-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
<div className="py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className="flex gap-x-5">
<WorkspaceProfileImage workspace={workspace} />
<div className="mt-3 ml-5 dot-falling"></div>
@ -36,7 +36,7 @@ const PromptReply = ({
<div
className={`flex justify-center items-end w-full ${assistantBackgroundColor}`}
>
<div className="py-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
<div className="py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className="flex gap-x-5">
<WorkspaceProfileImage workspace={workspace} />
<span
@ -57,7 +57,7 @@ const PromptReply = ({
key={uuid}
className={`flex justify-center items-end w-full ${assistantBackgroundColor}`}
>
<div className="py-6 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
<div className="py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className="flex gap-x-5">
<WorkspaceProfileImage workspace={workspace} />
<span

View File

@ -198,7 +198,7 @@ export default function ChatHistory({
function StatusResponse({ props }) {
return (
<div className="flex justify-center items-end w-full">
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className="flex gap-x-5">
<span
className={`text-xs inline-block p-2 rounded-lg text-white/60 font-mono whitespace-pre-line`}

View File

@ -31,10 +31,7 @@ export default function EditPresetModal({
};
const handleDelete = async () => {
const confirmDelete = window.confirm(
"Are you sure you want to delete this preset?"
);
if (!confirmDelete) return;
if (!window.confirm("Are you sure you want to delete this preset?")) return;
setDeleting(true);
await onDelete(preset.id);

View File

@ -3,7 +3,6 @@ import SlashCommandsButton, {
SlashCommands,
useSlashCommands,
} from "./SlashCommands";
import { isMobile } from "react-device-detect";
import debounce from "lodash.debounce";
import { PaperPlaneRight } from "@phosphor-icons/react";
import StopGenerationButton from "./StopGenerationButton";
@ -13,6 +12,7 @@ import AvailableAgentsButton, {
} from "./AgentMenu";
import TextSizeButton from "./TextSizeMenu";
import SpeechToText from "./SpeechToText";
import { Tooltip } from "react-tooltip";
export const PROMPT_INPUT_EVENT = "set_prompt_input";
export default function PromptInput({
@ -83,7 +83,6 @@ export default function PromptInput({
};
const adjustTextArea = (event) => {
if (isMobile) return false;
const element = event.target;
element.style.height = "auto";
element.style.height = `${element.scrollHeight}px`;
@ -130,20 +129,31 @@ export default function PromptInput({
adjustTextArea(e);
}}
value={promptInput}
className="cursor-text max-h-[100px] md:min-h-[40px] mx-2 md:mx-0 py-2 w-full text-[16px] md:text-md text-white bg-transparent placeholder:text-white/60 resize-none active:outline-none focus:outline-none flex-grow"
className="cursor-text max-h-[50vh] md:max-h-[350px] md:min-h-[40px] mx-2 md:mx-0 py-2 w-full text-[16px] md:text-md text-white bg-transparent placeholder:text-white/60 resize-none active:outline-none focus:outline-none flex-grow"
placeholder={"Send a message"}
/>
{buttonDisabled ? (
<StopGenerationButton />
) : (
<button
ref={formRef}
type="submit"
className="inline-flex justify-center rounded-2xl cursor-pointer text-white/60 hover:text-white group ml-4"
>
<PaperPlaneRight className="w-7 h-7 my-3" weight="fill" />
<span className="sr-only">Send message</span>
</button>
<>
<button
ref={formRef}
type="submit"
className="inline-flex justify-center rounded-2xl cursor-pointer text-white/60 hover:text-white group ml-4"
data-tooltip-id="send-prompt"
data-tooltip-content="Send prompt message to workspace"
aria-label="Send prompt message to workspace"
>
<PaperPlaneRight className="w-7 h-7 my-3" weight="fill" />
<span className="sr-only">Send message</span>
</button>
<Tooltip
id="send-prompt"
place="bottom"
delayShow={300}
className="tooltip !text-xs z-99"
/>
</>
)}
</div>
<div className="flex justify-between py-3.5">

View File

@ -8,7 +8,7 @@ export default function LoadingChat() {
return (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll"
className="p-4 transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<Skeleton.default
height="100px"
@ -16,7 +16,7 @@ export default function LoadingChat() {
highlightColor={highlightColor}
baseColor={baseColor}
count={1}
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
className="max-w-full md:max-w-[80%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
containerClassName="flex justify-start"
/>
<Skeleton.default
@ -25,7 +25,7 @@ export default function LoadingChat() {
baseColor={baseColor}
highlightColor={highlightColor}
count={1}
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
className="max-w-full md:max-w-[80%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
containerClassName="flex justify-end"
/>
<Skeleton.default
@ -34,7 +34,7 @@ export default function LoadingChat() {
baseColor={baseColor}
highlightColor={highlightColor}
count={1}
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
className="max-w-full md:max-w-[80%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
containerClassName="flex justify-start"
/>
<Skeleton.default
@ -43,7 +43,7 @@ export default function LoadingChat() {
baseColor={baseColor}
highlightColor={highlightColor}
count={1}
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
className="max-w-full md:max-w-[80%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
containerClassName="flex justify-end"
/>
<Skeleton.default
@ -52,7 +52,7 @@ export default function LoadingChat() {
baseColor={baseColor}
highlightColor={highlightColor}
count={1}
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
className="max-w-full md:max-w-[80%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
containerClassName="flex justify-start"
/>
</div>

View File

@ -10,7 +10,12 @@ export const DISABLED_PROVIDERS = [
];
const PROVIDER_DEFAULT_MODELS = {
openai: [],
gemini: ["gemini-pro", "gemini-1.5-pro-latest"],
gemini: [
"gemini-pro",
"gemini-1.0-pro",
"gemini-1.5-pro-latest",
"gemini-1.5-flash-latest",
],
anthropic: [
"claude-instant-1.2",
"claude-2.0",

View File

@ -2,6 +2,6 @@ import { useContext } from "react";
import { LogoContext } from "../LogoContext";
export default function useLogo() {
const { logo, setLogo } = useContext(LogoContext);
return { logo, setLogo };
const { logo, setLogo, loginLogo, isCustomLogo } = useContext(LogoContext);
return { logo, setLogo, loginLogo, isCustomLogo };
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -60,6 +60,24 @@ const DataConnector = {
});
},
},
websiteDepth: {
scrape: async ({ url, depth, maxLinks }) => {
return await fetch(`${API_BASE}/ext/website-depth`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({ url, depth, maxLinks }),
})
.then((res) => res.json())
.then((res) => {
if (!res.success) throw new Error(res.reason);
return { data: res.data, error: null };
})
.catch((e) => {
console.error(e);
return { data: null, error: e.message };
});
},
},
confluence: {
collect: async function ({ pageUrl, username, accessToken }) {

View File

@ -6,6 +6,7 @@ const System = {
cacheKeys: {
footerIcons: "anythingllm_footer_links",
supportEmail: "anythingllm_support_email",
customAppName: "anythingllm_custom_app_name",
},
ping: async function () {
return await fetch(`${API_BASE}/ping`)
@ -319,19 +320,58 @@ const System = {
);
return { email: supportEmail, error: null };
},
fetchCustomAppName: async function () {
const cache = window.localStorage.getItem(this.cacheKeys.customAppName);
const { appName, lastFetched } = cache
? safeJsonParse(cache, { appName: "", lastFetched: 0 })
: { appName: "", lastFetched: 0 };
if (!!appName && Date.now() - lastFetched < 3_600_000)
return { appName: appName, error: null };
const { customAppName, error } = await fetch(
`${API_BASE}/system/custom-app-name`,
{
method: "GET",
cache: "no-cache",
headers: baseHeaders(),
}
)
.then((res) => res.json())
.catch((e) => {
console.log(e);
return { customAppName: "", error: e.message };
});
if (!customAppName || !!error) {
window.localStorage.removeItem(this.cacheKeys.customAppName);
return { appName: "", error: null };
}
window.localStorage.setItem(
this.cacheKeys.customAppName,
JSON.stringify({ appName: customAppName, lastFetched: Date.now() })
);
return { appName: customAppName, error: null };
},
fetchLogo: async function () {
return await fetch(`${API_BASE}/system/logo`, {
method: "GET",
cache: "no-cache",
})
.then((res) => {
if (res.ok && res.status !== 204) return res.blob();
.then(async (res) => {
if (res.ok && res.status !== 204) {
const isCustomLogo = res.headers.get("X-Is-Custom-Logo") === "true";
const blob = await res.blob();
const logoURL = URL.createObjectURL(blob);
return { isCustomLogo, logoURL };
}
throw new Error("Failed to fetch logo!");
})
.then((blob) => URL.createObjectURL(blob))
.catch((e) => {
console.log(e);
return null;
return { isCustomLogo: false, logoURL: null };
});
},
fetchPfp: async function (id) {

View File

@ -0,0 +1,100 @@
import Admin from "@/models/admin";
import System from "@/models/system";
import showToast from "@/utils/toast";
import { useEffect, useState } from "react";
export default function CustomAppName() {
const [loading, setLoading] = useState(true);
const [hasChanges, setHasChanges] = useState(false);
const [customAppName, setCustomAppName] = useState("");
const [originalAppName, setOriginalAppName] = useState("");
const [canCustomize, setCanCustomize] = useState(false);
useEffect(() => {
const fetchInitialParams = async () => {
const settings = await System.keys();
if (!settings?.MultiUserMode && !settings?.RequiresAuth) {
setCanCustomize(false);
return false;
}
const { appName } = await System.fetchCustomAppName();
setCustomAppName(appName || "");
setOriginalAppName(appName || "");
setCanCustomize(true);
setLoading(false);
};
fetchInitialParams();
}, []);
const updateCustomAppName = async (e, newValue = null) => {
e.preventDefault();
let custom_app_name = newValue;
if (newValue === null) {
const form = new FormData(e.target);
custom_app_name = form.get("customAppName");
}
const { success, error } = await Admin.updateSystemPreferences({
custom_app_name,
});
if (!success) {
showToast(`Failed to update custom app name: ${error}`, "error");
return;
} else {
showToast("Successfully updated custom app name.", "success");
window.localStorage.removeItem(System.cacheKeys.customAppName);
setCustomAppName(custom_app_name);
setOriginalAppName(custom_app_name);
setHasChanges(false);
}
};
const handleChange = (e) => {
setCustomAppName(e.target.value);
setHasChanges(true);
};
if (!canCustomize || loading) return null;
return (
<form className="mb-6" onSubmit={updateCustomAppName}>
<div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white">
Custom App Name
</h2>
<p className="text-xs leading-[18px] font-base text-white/60">
Set a custom app name that is displayed on the login page.
</p>
</div>
<div className="flex items-center gap-x-4">
<input
name="customAppName"
type="text"
className="bg-zinc-900 mt-3 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 max-w-[275px] placeholder:text-white/20"
placeholder="AnythingLLM"
required={true}
autoComplete="off"
onChange={handleChange}
value={customAppName}
/>
{originalAppName !== "" && (
<button
type="button"
onClick={(e) => updateCustomAppName(e, "")}
className="mt-4 text-white text-base font-medium hover:text-opacity-60"
>
Clear
</button>
)}
</div>
{hasChanges && (
<button
type="submit"
className="transition-all mt-6 w-fit duration-300 border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Save
</button>
)}
</form>
);
}

View File

@ -2,7 +2,6 @@ import useLogo from "@/hooks/useLogo";
import System from "@/models/system";
import showToast from "@/utils/toast";
import { useEffect, useRef, useState } from "react";
import AnythingLLM from "@/media/logo/anything-llm.png";
import { Plus } from "@phosphor-icons/react";
export default function CustomLogo() {
@ -36,7 +35,7 @@ export default function CustomLogo() {
return;
}
const logoURL = await System.fetchLogo();
const { logoURL } = await System.fetchLogo();
_setLogo(logoURL);
showToast("Image uploaded successfully.", "success");
@ -51,13 +50,13 @@ export default function CustomLogo() {
if (!success) {
console.error("Failed to remove logo:", error);
showToast(`Failed to remove logo: ${error}`, "error");
const logoURL = await System.fetchLogo();
const { logoURL } = await System.fetchLogo();
setLogo(logoURL);
setIsDefaultLogo(false);
return;
}
const logoURL = await System.fetchLogo();
const { logoURL } = await System.fetchLogo();
_setLogo(logoURL);
showToast("Image successfully removed.", "success");

View File

@ -4,6 +4,7 @@ import FooterCustomization from "./FooterCustomization";
import SupportEmail from "./SupportEmail";
import CustomLogo from "./CustomLogo";
import CustomMessages from "./CustomMessages";
import CustomAppName from "./CustomAppName";
export default function Appearance() {
return (
@ -25,6 +26,7 @@ export default function Appearance() {
</p>
</div>
<CustomLogo />
<CustomAppName />
<CustomMessages />
<FooterCustomization />
<SupportEmail />

View File

@ -47,7 +47,7 @@ export default function TextToSpeechProvider({ settings }) {
const searchInputRef = useRef(null);
const handleSubmit = async (e) => {
e.preventDefault();
e?.preventDefault();
const form = e.target;
const data = { TextToSpeechProvider: selectedProvider };
const formData = new FormData(form);
@ -110,10 +110,7 @@ export default function TextToSpeechProvider({ settings }) {
</div>
<div className="w-full justify-end flex">
{hasChanges && (
<CTAButton
onClick={() => handleSubmit()}
className="mt-3 mr-0 -mb-14 z-10"
>
<CTAButton className="mt-3 mr-0 -mb-14 z-10">
{saving ? "Saving..." : "Save changes"}
</CTAButton>
)}

View File

@ -7,7 +7,7 @@ import useQuery from "@/hooks/useQuery";
import ChatRow from "./ChatRow";
import showToast from "@/utils/toast";
import System from "@/models/system";
import { CaretDown, Download } from "@phosphor-icons/react";
import { CaretDown, Download, Trash } from "@phosphor-icons/react";
import { saveAs } from "file-saver";
const exportOptions = {
@ -49,6 +49,12 @@ export default function WorkspaceChats() {
const [showMenu, setShowMenu] = useState(false);
const menuRef = useRef();
const openMenuButton = useRef();
const query = useQuery();
const [loading, setLoading] = useState(true);
const [chats, setChats] = useState([]);
const [offset, setOffset] = useState(Number(query.get("offset") || 0));
const [canNext, setCanNext] = useState(false);
const handleDumpChats = async (exportType) => {
const chats = await System.exportChats(exportType);
if (!!chats) {
@ -62,6 +68,18 @@ export default function WorkspaceChats() {
}
};
const handleClearAllChats = async () => {
if (
!window.confirm(
`Are you sure you want to clear all chats?\n\nThis action is irreversible.`
)
)
return false;
await System.deleteChat(-1);
setChats([]);
showToast("Cleared all chats.", "success");
};
const toggleMenu = () => {
setShowMenu(!showMenu);
};
@ -83,6 +101,16 @@ export default function WorkspaceChats() {
};
}, []);
useEffect(() => {
async function fetchChats() {
const { chats: _chats, hasPages = false } = await System.chats(offset);
setChats(_chats);
setCanNext(hasPages);
setLoading(false);
}
fetchChats();
}, [offset]);
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
@ -100,7 +128,7 @@ export default function WorkspaceChats() {
<button
ref={openMenuButton}
onClick={toggleMenu}
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
className="flex items-center gap-x-2 px-4 py-1 rounded-lg bg-[#46C8FF] hover:text-white text-xs font-semibold hover:bg-[#2C2F36] shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit"
>
<Download size={18} weight="bold" />
Export
@ -128,26 +156,43 @@ export default function WorkspaceChats() {
</div>
</div>
</div>
{chats.length > 0 && (
<button
onClick={handleClearAllChats}
className="flex items-center gap-x-2 px-4 py-1 border hover:border-transparent border-white/40 text-white/40 rounded-lg bg-transparent hover:text-white text-xs font-semibold hover:bg-red-500 shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit"
>
<Trash size={18} weight="bold" />
Clear Chats
</button>
)}
</div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
These are all the recorded chats and messages that have been sent
by users ordered by their creation date.
</p>
</div>
<ChatsContainer />
<ChatsContainer
loading={loading}
chats={chats}
setChats={setChats}
offset={offset}
setOffset={setOffset}
canNext={canNext}
/>
</div>
</div>
</div>
);
}
function ChatsContainer() {
const query = useQuery();
const [loading, setLoading] = useState(true);
const [chats, setChats] = useState([]);
const [offset, setOffset] = useState(Number(query.get("offset") || 0));
const [canNext, setCanNext] = useState(false);
function ChatsContainer({
loading,
chats,
setChats,
offset,
setOffset,
canNext,
}) {
const handlePrevious = () => {
setOffset(Math.max(offset - 1, 0));
};
@ -155,20 +200,11 @@ function ChatsContainer() {
setOffset(offset + 1);
};
const handleDeleteChat = (chatId) => {
const handleDeleteChat = async (chatId) => {
await System.deleteChat(chatId);
setChats((prevChats) => prevChats.filter((chat) => chat.id !== chatId));
};
useEffect(() => {
async function fetchChats() {
const { chats: _chats, hasPages = false } = await System.chats(offset);
setChats(_chats);
setCanNext(hasPages);
setLoading(false);
}
fetchChats();
}, [offset]);
if (loading) {
return (
<Skeleton.default

View File

@ -10,6 +10,8 @@ import LocalAiLogo from "@/media/llmprovider/localai.png";
import OllamaLogo from "@/media/llmprovider/ollama.png";
import LMStudioLogo from "@/media/llmprovider/lmstudio.png";
import CohereLogo from "@/media/llmprovider/cohere.png";
import VoyageAiLogo from "@/media/embeddingprovider/voyageai.png";
import PreLoader from "@/components/Preloader";
import ChangeWarningModal from "@/components/ChangeWarning";
import OpenAiOptions from "@/components/EmbeddingSelection/OpenAiOptions";
@ -19,6 +21,7 @@ import NativeEmbeddingOptions from "@/components/EmbeddingSelection/NativeEmbedd
import OllamaEmbeddingOptions from "@/components/EmbeddingSelection/OllamaOptions";
import LMStudioEmbeddingOptions from "@/components/EmbeddingSelection/LMStudioOptions";
import CohereEmbeddingOptions from "@/components/EmbeddingSelection/CohereOptions";
import VoyageAiOptions from "@/components/EmbeddingSelection/VoyageAiOptions";
import EmbedderItem from "@/components/EmbeddingSelection/EmbedderItem";
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
@ -78,6 +81,13 @@ const EMBEDDERS = [
options: (settings) => <CohereEmbeddingOptions settings={settings} />,
description: "Run powerful embedding models from Cohere.",
},
{
name: "Voyage AI",
value: "voyageai",
logo: VoyageAiLogo,
options: (settings) => <VoyageAiOptions settings={settings} />,
description: "Run powerful embedding models from Voyage AI.",
},
];
export default function GeneralEmbeddingPreference() {

View File

@ -21,6 +21,7 @@ import GroqLogo from "@/media/llmprovider/groq.png";
import KoboldCPPLogo from "@/media/llmprovider/koboldcpp.png";
import TextGenWebUILogo from "@/media/llmprovider/text-generation-webui.png";
import CohereLogo from "@/media/llmprovider/cohere.png";
import LiteLLMLogo from "@/media/llmprovider/litellm.png";
import PreLoader from "@/components/Preloader";
import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions";
import GenericOpenAiOptions from "@/components/LLMSelection/GenericOpenAiOptions";
@ -38,12 +39,13 @@ import PerplexityOptions from "@/components/LLMSelection/PerplexityOptions";
import OpenRouterOptions from "@/components/LLMSelection/OpenRouterOptions";
import GroqAiOptions from "@/components/LLMSelection/GroqAiOptions";
import CohereAiOptions from "@/components/LLMSelection/CohereAiOptions";
import KoboldCPPOptions from "@/components/LLMSelection/KoboldCPPOptions";
import TextGenWebUIOptions from "@/components/LLMSelection/TextGenWebUIOptions";
import LiteLLMOptions from "@/components/LLMSelection/LiteLLMOptions";
import LLMItem from "@/components/LLMSelection/LLMItem";
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
import CTAButton from "@/components/lib/CTAButton";
import KoboldCPPOptions from "@/components/LLMSelection/KoboldCPPOptions";
import TextGenWebUIOptions from "@/components/LLMSelection/TextGenWebUIOptions";
export const AVAILABLE_LLM_PROVIDERS = [
{
@ -186,6 +188,14 @@ export const AVAILABLE_LLM_PROVIDERS = [
description: "Run Cohere's powerful Command models.",
requiredConfig: ["CohereApiKey"],
},
{
name: "LiteLLM",
value: "litellm",
logo: LiteLLMLogo,
options: (settings) => <LiteLLMOptions settings={settings} />,
description: "Run LiteLLM's OpenAI compatible proxy for various LLMs.",
requiredConfig: ["LiteLLMBasePath"],
},
{
name: "Generic OpenAI",
value: "generic-openai",
@ -384,16 +394,17 @@ export default function GeneralLLMPreference() {
>
<div className="flex gap-x-4 items-center">
<img
src={selectedLLMObject.logo}
alt={`${selectedLLMObject.name} logo`}
src={selectedLLMObject?.logo || AnythingLLMIcon}
alt={`${selectedLLMObject?.name} logo`}
className="w-10 h-10 rounded-md"
/>
<div className="flex flex-col text-left">
<div className="text-sm font-semibold text-white">
{selectedLLMObject.name}
{selectedLLMObject?.name || "None selected"}
</div>
<div className="mt-1 text-xs text-[#D2D5DB]">
{selectedLLMObject.description}
{selectedLLMObject?.description ||
"You need to select an LLM"}
</div>
</div>
</div>

View File

@ -12,6 +12,23 @@ import LLMItem from "@/components/LLMSelection/LLMItem";
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
import CTAButton from "@/components/lib/CTAButton";
const PROVIDERS = [
{
name: "OpenAI",
value: "openai",
logo: OpenAiLogo,
options: (settings) => <OpenAiWhisperOptions settings={settings} />,
description: "Leverage the OpenAI Whisper-large model using your API key.",
},
{
name: "AnythingLLM Built-In",
value: "local",
logo: AnythingLLMIcon,
options: (settings) => <NativeTranscriptionOptions settings={settings} />,
description: "Run a built-in whisper model on this instance privately.",
},
];
export default function TranscriptionModelPreference() {
const [saving, setSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
@ -68,24 +85,6 @@ export default function TranscriptionModelPreference() {
fetchKeys();
}, []);
const PROVIDERS = [
{
name: "OpenAI",
value: "openai",
logo: OpenAiLogo,
options: <OpenAiWhisperOptions settings={settings} />,
description:
"Leverage the OpenAI Whisper-large model using your API key.",
},
{
name: "AnythingLLM Built-In",
value: "local",
logo: AnythingLLMIcon,
options: <NativeTranscriptionOptions settings={settings} />,
description: "Run a built-in whisper model on this instance privately.",
},
];
useEffect(() => {
const filtered = PROVIDERS.filter((provider) =>
provider.name.toLowerCase().includes(searchQuery.toLowerCase())
@ -228,7 +227,7 @@ export default function TranscriptionModelPreference() {
{selectedProvider &&
PROVIDERS.find(
(provider) => provider.value === selectedProvider
)?.options}
)?.options(settings)}
</div>
</div>
</form>

View File

@ -17,6 +17,8 @@ import OpenRouterLogo from "@/media/llmprovider/openrouter.jpeg";
import GroqLogo from "@/media/llmprovider/groq.png";
import KoboldCPPLogo from "@/media/llmprovider/koboldcpp.png";
import TextGenWebUILogo from "@/media/llmprovider/text-generation-webui.png";
import LiteLLMLogo from "@/media/llmprovider/litellm.png";
import CohereLogo from "@/media/llmprovider/cohere.png";
import ZillizLogo from "@/media/vectordbs/zilliz.png";
import AstraDBLogo from "@/media/vectordbs/astraDB.png";
@ -26,6 +28,8 @@ import LanceDbLogo from "@/media/vectordbs/lancedb.png";
import WeaviateLogo from "@/media/vectordbs/weaviate.png";
import QDrantLogo from "@/media/vectordbs/qdrant.png";
import MilvusLogo from "@/media/vectordbs/milvus.png";
import VoyageAiLogo from "@/media/embeddingprovider/voyageai.png";
import React, { useState, useEffect } from "react";
import paths from "@/utils/paths";
import { useNavigate } from "react-router-dom";
@ -168,6 +172,13 @@ export const LLM_SELECTION_PRIVACY = {
],
logo: CohereLogo,
},
litellm: {
name: "LiteLLM",
description: [
"Your model and chats are only accessible on the server running LiteLLM",
],
logo: LiteLLMLogo,
},
};
export const VECTOR_DB_PRIVACY = {
@ -283,6 +294,13 @@ export const EMBEDDING_ENGINE_PRIVACY = {
],
logo: CohereLogo,
},
voyageai: {
name: "Voyage AI",
description: [
"Data sent to Voyage AI's servers is shared according to the terms of service of voyageai.com.",
],
logo: VoyageAiLogo,
},
};
export default function DataHandling({ setHeader, setForwardBtn, setBackBtn }) {

View File

@ -17,6 +17,8 @@ import OpenRouterLogo from "@/media/llmprovider/openrouter.jpeg";
import GroqLogo from "@/media/llmprovider/groq.png";
import KoboldCPPLogo from "@/media/llmprovider/koboldcpp.png";
import TextGenWebUILogo from "@/media/llmprovider/text-generation-webui.png";
import LiteLLMLogo from "@/media/llmprovider/litellm.png";
import CohereLogo from "@/media/llmprovider/cohere.png";
import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions";
import GenericOpenAiOptions from "@/components/LLMSelection/GenericOpenAiOptions";
@ -34,14 +36,15 @@ import PerplexityOptions from "@/components/LLMSelection/PerplexityOptions";
import OpenRouterOptions from "@/components/LLMSelection/OpenRouterOptions";
import GroqAiOptions from "@/components/LLMSelection/GroqAiOptions";
import CohereAiOptions from "@/components/LLMSelection/CohereAiOptions";
import KoboldCPPOptions from "@/components/LLMSelection/KoboldCPPOptions";
import TextGenWebUIOptions from "@/components/LLMSelection/TextGenWebUIOptions";
import LiteLLMOptions from "@/components/LLMSelection/LiteLLMOptions";
import LLMItem from "@/components/LLMSelection/LLMItem";
import System from "@/models/system";
import paths from "@/utils/paths";
import showToast from "@/utils/toast";
import { useNavigate } from "react-router-dom";
import KoboldCPPOptions from "@/components/LLMSelection/KoboldCPPOptions";
import TextGenWebUIOptions from "@/components/LLMSelection/TextGenWebUIOptions";
const TITLE = "LLM Preference";
const DESCRIPTION =
@ -164,6 +167,13 @@ const LLMS = [
options: (settings) => <CohereAiOptions settings={settings} />,
description: "Run Cohere's powerful Command models.",
},
{
name: "LiteLLM",
value: "litellm",
logo: LiteLLMLogo,
options: (settings) => <LiteLLMOptions settings={settings} />,
description: "Run LiteLLM's OpenAI compatible proxy for various LLMs.",
},
{
name: "Generic OpenAI",
value: "generic-openai",

View File

@ -0,0 +1,47 @@
import PostgreSQLLogo from "./icons/postgresql.png";
import MySQLLogo from "./icons/mysql.png";
import MSSQLLogo from "./icons/mssql.png";
import { X } from "@phosphor-icons/react";
export const DB_LOGOS = {
postgresql: PostgreSQLLogo,
mysql: MySQLLogo,
"sql-server": MSSQLLogo,
};
export default function DBConnection({ connection, onRemove }) {
const { database_id, engine } = connection;
function removeConfirmation() {
if (
!window.confirm(
`Delete ${database_id} from the list of available SQL connections? This cannot be undone.`
)
) {
return false;
}
onRemove(database_id);
}
return (
<div className="flex gap-x-4 items-center">
<img
src={DB_LOGOS?.[engine] ?? null}
alt={`${engine} logo`}
className="w-10 h-10 rounded-md"
/>
<div className="flex w-full items-center justify-between">
<div className="flex flex-col">
<div className="text-sm font-semibold text-white">{database_id}</div>
<div className="mt-1 text-xs text-[#D2D5DB]">{engine}</div>
</div>
<button
type="button"
onClick={removeConfirmation}
className="border-none text-white/40 hover:text-red-500"
>
<X size={24} />
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,271 @@
import { useState } from "react";
import { createPortal } from "react-dom";
import ModalWrapper from "@/components/ModalWrapper";
import { WarningOctagon, X } from "@phosphor-icons/react";
import { DB_LOGOS } from "./DBConnection";
function assembleConnectionString({
engine,
username = "",
password = "",
host = "",
port = "",
database = "",
}) {
if ([username, password, host, database].every((i) => !!i) === false)
return `Please fill out all the fields above.`;
switch (engine) {
case "postgresql":
return `postgres://${username}:${password}@${host}:${port}/${database}`;
case "mysql":
return `mysql://${username}:${password}@${host}:${port}/${database}`;
case "sql-server":
return `mssql://${username}:${password}@${host}:${port}/${database}`;
default:
return null;
}
}
const DEFAULT_ENGINE = "postgresql";
const DEFAULT_CONFIG = {
username: null,
password: null,
host: null,
port: null,
database: null,
};
export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) {
const [engine, setEngine] = useState(DEFAULT_ENGINE);
const [config, setConfig] = useState(DEFAULT_CONFIG);
if (!isOpen) return null;
function handleClose() {
setEngine(DEFAULT_ENGINE);
setConfig(DEFAULT_CONFIG);
closeModal();
}
function onFormChange() {
const form = new FormData(document.getElementById("sql-connection-form"));
setConfig({
username: form.get("username").trim(),
password: form.get("password"),
host: form.get("host").trim(),
port: form.get("port").trim(),
database: form.get("database").trim(),
});
}
async function handleUpdate(e) {
e.preventDefault();
e.stopPropagation();
const form = new FormData(e.target);
onSubmit({
engine,
database_id: form.get("name"),
connectionString: assembleConnectionString({ engine, ...config }),
});
handleClose();
return false;
}
// Cannot do nested forms, it will cause all sorts of issues, so we portal this out
// to the parent container form so we don't have nested forms.
return createPortal(
<ModalWrapper isOpen={isOpen}>
<div className="relative w-1/3 max-h-full ">
<div className="relative bg-main-gradient rounded-xl shadow-[0_4px_14px_rgba(0,0,0,0.25)] max-h-[90vh] overflow-y-scroll no-scroll">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white">
New SQL Connection
</h3>
<button
onClick={handleClose}
type="button"
className="border-none transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
data-modal-hide="staticModal"
>
<X className="text-gray-300 text-lg" />
</button>
</div>
<form
id="sql-connection-form"
onSubmit={handleUpdate}
onChange={onFormChange}
>
<div className="py-[17px] px-[20px] flex flex-col gap-y-6">
<p className="text-sm text-white">
Add the connection information for your database below and it
will be available for future SQL agent calls.
</p>
<div className="flex flex-col w-full">
<div className="border border-red-800 bg-zinc-800 p-4 rounded-lg flex items-center gap-x-2 text-sm text-red-400">
<WarningOctagon size={28} className="shrink-0" />
<p>
<b>WARNING:</b> The SQL agent has been <i>instructed</i> to
only perform non-modifying queries. This <b>does not</b>{" "}
prevent a hallucination from still deleting data. Only
connect with a user who has <b>READ_ONLY</b> permissions.
</p>
</div>
<label className="text-white text-sm font-semibold block my-4">
Select your SQL engine
</label>
<div className="flex w-full flex-wrap gap-x-4">
<DBEngine
provider="postgresql"
active={engine === "postgresql"}
onClick={() => setEngine("postgresql")}
/>
<DBEngine
provider="mysql"
active={engine === "mysql"}
onClick={() => setEngine("mysql")}
/>
<DBEngine
provider="sql-server"
active={engine === "sql-server"}
onClick={() => setEngine("sql-server")}
/>
</div>
</div>
<div className="flex flex-col w-full">
<label className="text-white text-sm font-semibold block mb-4">
Connection name
</label>
<input
type="text"
name="name"
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="a unique name to identify this SQL connection"
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex gap-x-2">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Database user
</label>
<input
type="text"
name="username"
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="root"
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Database user password
</label>
<input
type="text"
name="password"
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="password123"
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
</div>
<div className="flex gap-x-2">
<div className="flex flex-col w-full">
<label className="text-white text-sm font-semibold block mb-4">
Server endpoint
</label>
<input
type="text"
name="host"
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="the hostname or endpoint for your database"
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-30">
<label className="text-white text-sm font-semibold block mb-4">
Port
</label>
<input
type="text"
name="port"
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="3306"
required={false}
autoComplete="off"
spellCheck={false}
/>
</div>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Database
</label>
<input
type="text"
name="database"
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="the database the agent will interact with"
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<p className="text-white/40 text-sm">
{assembleConnectionString({ engine, ...config })}
</p>
</div>
<div className="flex w-full justify-between items-center p-3 space-x-2 border-t rounded-b border-gray-500/50">
<button
type="button"
onClick={handleClose}
className="border-none text-xs px-2 py-1 font-semibold rounded-lg bg-white hover:bg-transparent border-2 border-transparent hover:border-white hover:text-white h-[32px] w-fit -mr-8 whitespace-nowrap shadow-[0_4px_14px_rgba(0,0,0,0.25)]"
>
Cancel
</button>
<button
type="submit"
form="sql-connection-form"
className="border-none text-xs px-2 py-1 font-semibold rounded-lg bg-[#46C8FF] hover:bg-[#2C2F36] border-2 border-transparent hover:border-[#46C8FF] hover:text-white h-[32px] w-fit -mr-8 whitespace-nowrap shadow-[0_4px_14px_rgba(0,0,0,0.25)]"
>
Save connection
</button>
</div>
</form>
</div>
</div>
</ModalWrapper>,
document.getElementById("workspace-agent-settings-container")
);
}
function DBEngine({ provider, active, onClick }) {
return (
<button
type="button"
onClick={onClick}
className={`flex flex-col p-4 border border-white/40 bg-zinc-800 rounded-lg w-fit hover:bg-zinc-700 ${
active ? "!bg-blue-500/50" : ""
}`}
>
<img
src={DB_LOGOS[provider]}
className="h-[100px] rounded-md"
alt="PostgreSQL"
/>
</button>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -0,0 +1,109 @@
import React, { useState } from "react";
import DBConnection from "./DBConnection";
import { Plus } from "@phosphor-icons/react";
import NewSQLConnection from "./NewConnectionModal";
import { useModal } from "@/hooks/useModal";
export default function AgentSQLConnectorSelection({
skill,
settings,
toggleSkill,
enabled = false,
}) {
const { isOpen, openModal, closeModal } = useModal();
const [connections, setConnections] = useState(
settings?.preferences?.agent_sql_connections || []
);
return (
<>
<div className="border-b border-white/40 pb-4">
<div className="flex flex-col">
<div className="flex w-full justify-between items-center">
<label htmlFor="name" className="block input-label">
SQL Agent
</label>
<label className="border-none relative inline-flex cursor-pointer items-center mt-2">
<input
type="checkbox"
className="peer sr-only"
checked={enabled}
onClick={() => toggleSkill(skill)}
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div>
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
</label>
</div>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
Enable your agent to be able to leverage SQL to answer you questions
by connecting to various SQL database providers.
</p>
</div>
{enabled && (
<>
<input
name="system::agent_sql_connections"
type="hidden"
value={JSON.stringify(connections)}
/>
<input
type="hidden"
value={JSON.stringify(
connections.filter((conn) => conn.action !== "remove")
)}
/>
<div className="flex flex-col mt-2 gap-y-2">
<p className="text-white font-semibold text-sm">
Your database connections
</p>
<div className="flex flex-col gap-y-3">
{connections
.filter((connection) => connection.action !== "remove")
.map((connection) => (
<DBConnection
key={connection.database_id}
connection={connection}
onRemove={(databaseId) => {
setConnections((prev) =>
prev.map((conn) => {
if (conn.database_id === databaseId)
return { ...conn, action: "remove" };
return conn;
})
);
}}
/>
))}
<button
type="button"
onClick={openModal}
className="w-fit relative flex h-[40px] items-center border-none hover:bg-slate-600/20 rounded-lg"
>
<div className="flex w-full gap-x-2 items-center p-4">
<div className="bg-zinc-600 p-2 rounded-lg h-[24px] w-[24px] flex items-center justify-center">
<Plus
weight="bold"
size={14}
className="shrink-0 text-slate-100"
/>
</div>
<p className="text-left text-slate-100 text-sm">
New SQL connection
</p>
</div>
</button>
</div>
</div>
</>
)}
</div>
<NewSQLConnection
isOpen={isOpen}
closeModal={closeModal}
onSubmit={(newDb) =>
setConnections((prev) => [...prev, { action: "add", ...newDb }])
}
/>
</>
);
}

View File

@ -6,6 +6,7 @@ export function GoogleSearchOptions({ settings }) {
<a
href="https://programmablesearchengine.google.com/controlpanel/create"
target="_blank"
rel="noreferrer"
className="text-blue-300 underline"
>
from Google here.
@ -57,6 +58,7 @@ export function SerperDotDevOptions({ settings }) {
<a
href="https://serper.dev"
target="_blank"
rel="noreferrer"
className="text-blue-300 underline"
>
from Serper.dev.
@ -82,3 +84,66 @@ export function SerperDotDevOptions({ settings }) {
</>
);
}
export function BingSearchOptions({ settings }) {
return (
<>
<p className="text-sm text-white/60 my-2">
You can get a Bing Web Search API subscription key{" "}
<a
href="https://portal.azure.com/"
target="_blank"
rel="noreferrer"
className="text-blue-300 underline"
>
from the Azure portal.
</a>
</p>
<div className="flex gap-x-4">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
API Key
</label>
<input
type="password"
name="env::AgentBingSearchApiKey"
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="Bing Web Search API Key"
defaultValue={settings?.AgentBingSearchApiKey ? "*".repeat(20) : ""}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
</div>
<p className="text-sm text-white/60 my-2">
To set up a Bing Web Search API subscription:
</p>
<ol className="list-decimal text-sm text-white/60 ml-6">
<li>
Go to the Azure portal:{" "}
<a
href="https://portal.azure.com/"
target="_blank"
rel="noreferrer"
className="text-blue-300 underline"
>
https://portal.azure.com/
</a>
</li>
<li>Create a new Azure account or sign in with an existing one.</li>
<li>
Navigate to the "Create a resource" section and search for "Bing
Search v7".
</li>
<li>
Select the "Bing Search v7" resource and create a new subscription.
</li>
<li>
Choose the pricing tier that suits your needs (free tier available).
</li>
<li>Obtain the API key for your Bing Web Search subscription.</li>
</ol>
</>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@ -2,11 +2,13 @@ import React, { useEffect, useRef, useState } from "react";
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
import GoogleSearchIcon from "./icons/google.png";
import SerperDotDevIcon from "./icons/serper.png";
import BingSearchIcon from "./icons/bing.png";
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
import SearchProviderItem from "./SearchProviderItem";
import {
SerperDotDevOptions,
GoogleSearchOptions,
BingSearchOptions,
} from "./SearchProviderOptions";
const SEARCH_PROVIDERS = [
@ -34,6 +36,14 @@ const SEARCH_PROVIDERS = [
description:
"Serper.dev web-search. Free account with a 2,500 calls, but then paid.",
},
{
name: "Bing Search",
value: "bing-search",
logo: BingSearchIcon,
options: (settings) => <BingSearchOptions settings={settings} />,
description:
"Web search powered by the Bing Search API. Free for 1000 queries per month.",
},
];
export default function AgentWebSearchSelection({

View File

@ -5,6 +5,7 @@ import { castToType } from "@/utils/types";
import { useEffect, useRef, useState } from "react";
import AgentLLMSelection from "./AgentLLMSelection";
import AgentWebSearchSelection from "./WebSearchSelection";
import AgentSQLConnectorSelection from "./SQLConnectorSelection";
import GenericSkill from "./GenericSkill";
import Admin from "@/models/admin";
import * as Skeleton from "react-loading-skeleton";
@ -205,6 +206,12 @@ function AvailableAgentSkills({ skills, settings, toggleAgentSkill }) {
toggleSkill={toggleAgentSkill}
enabled={skills.includes("web-browsing")}
/>
<AgentSQLConnectorSelection
skill="sql-agent"
settings={settings}
toggleSkill={toggleAgentSkill}
enabled={skills.includes("sql-agent")}
/>
</div>
</div>
);

View File

@ -20,19 +20,23 @@ export default function ChatTemperatureSettings({
LLM Temperature
</label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
This setting controls how &quot;random&quot; or dynamic your chat
responses will be.
This setting controls how &quot;creative&quot; your LLM responses will
be.
<br />
The higher the number (1.0 maximum) the more random and incoherent.
The higher the number the more creative. For some models this can lead
to incoherent responses when set too high.
<br />
<i>Recommended: {defaults.temp}</i>
<br />
<i>
Most LLMs have various acceptable ranges of valid values. Consult
your LLM provider for that information.
</i>
</p>
</div>
<input
name="openAiTemp"
type="number"
min={0.0}
max={1.0}
step={0.1}
onWheel={(e) => e.target.blur()}
defaultValue={workspace?.openAiTemp ?? defaults.temp}

View File

@ -2,7 +2,6 @@ import Workspace from "@/models/workspace";
import { castToType } from "@/utils/types";
import showToast from "@/utils/toast";
import { useEffect, useRef, useState } from "react";
import VectorCount from "./VectorCount";
import WorkspaceName from "./WorkspaceName";
import SuggestedChatMessages from "./SuggestedChatMessages";
import DeleteWorkspace from "./DeleteWorkspace";
@ -51,7 +50,6 @@ export default function GeneralInfo({ slug }) {
onSubmit={handleUpdate}
className="w-1/2 flex flex-col gap-y-6"
>
<VectorCount reload={true} workspace={workspace} />
<WorkspaceName
key={workspace.slug}
workspace={workspace}

View File

@ -28,9 +28,6 @@ export default function VectorCount({ reload, workspace }) {
return (
<div>
<h3 className="input-label">Number of vectors</h3>
<p className="text-white text-opacity-60 text-xs font-medium py-1">
Total number of vectors in your vector database.
</p>
<p className="text-white text-opacity-60 text-sm font-medium">
{totalVectors}
</p>

View File

@ -6,6 +6,7 @@ import VectorDBIdentifier from "./VectorDBIdentifier";
import MaxContextSnippets from "./MaxContextSnippets";
import DocumentSimilarityThreshold from "./DocumentSimilarityThreshold";
import ResetDatabase from "./ResetDatabase";
import VectorCount from "./VectorCount";
export default function VectorDatabase({ workspace }) {
const [hasChanges, setHasChanges] = useState(false);
@ -38,7 +39,10 @@ export default function VectorDatabase({ workspace }) {
onSubmit={handleUpdate}
className="w-1/2 flex flex-col gap-y-6"
>
<VectorDBIdentifier workspace={workspace} />
<div className="flex items-start gap-x-5">
<VectorDBIdentifier workspace={workspace} />
<VectorCount reload={true} workspace={workspace} />
</div>
<MaxContextSnippets workspace={workspace} setHasChanges={setHasChanges} />
<DocumentSimilarityThreshold
workspace={workspace}

View File

@ -79,6 +79,12 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea
# GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT=4096
# GENERIC_OPEN_AI_API_KEY=sk-123abc
# LLM_PROVIDER='litellm'
# LITE_LLM_MODEL_PREF='gpt-3.5-turbo'
# LITE_LLM_MODEL_TOKEN_LIMIT=4096
# LITE_LLM_BASE_PATH='http://127.0.0.1:4000'
# LITE_LLM_API_KEY='sk-123abc'
# LLM_PROVIDER='cohere'
# COHERE_API_KEY=
# COHERE_MODEL_PREF='command-r'
@ -115,6 +121,10 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea
# COHERE_API_KEY=
# EMBEDDING_MODEL_PREF='embed-english-v3.0'
# EMBEDDING_ENGINE='voyageai'
# VOYAGEAI_API_KEY=
# EMBEDDING_MODEL_PREF='voyage-large-2-instruct'
###########################################
######## Vector Database Selection ########
###########################################
@ -220,8 +230,11 @@ TTS_PROVIDER="native"
#------ Serper.dev ----------- https://serper.dev/
# AGENT_SERPER_DEV_KEY=
#------ Bing Search ----------- https://portal.azure.com/
# AGENT_BING_SEARCH_API_KEY=
###########################################
######## SOCIAL PROVIDERS KEYS ###############
###########################################
# GOOGLE_AUTH_CLIENT_ID=
# GOOGLE_AUTH_CLIENT_ID=

5
server/.gitignore vendored
View File

@ -18,4 +18,7 @@ public/
# For legacy copies of repo
documents
vector-cache
yarn-error.log
yarn-error.log
# Local SSL Certs for HTTPS
sslcert

View File

@ -33,10 +33,7 @@ function adminEndpoints(app) {
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (_request, response) => {
try {
const users = (await User.where()).map((user) => {
const { password, ...rest } = user;
return rest;
});
const users = await User.where();
response.status(200).json({ users });
} catch (e) {
console.error(e);
@ -350,6 +347,8 @@ function adminEndpoints(app) {
agent_search_provider:
(await SystemSettings.get({ label: "agent_search_provider" }))
?.value || null,
agent_sql_connections:
await SystemSettings.brief.agent_sql_connections(),
default_agent_skills:
safeJsonParse(
(await SystemSettings.get({ label: "default_agent_skills" }))
@ -362,6 +361,9 @@ function adminEndpoints(app) {
allowed_domain: (
await SystemSettings.get({ label: "allowed_domain" })
)?.value,
custom_app_name:
(await SystemSettings.get({ label: "custom_app_name" }))?.value ||
null,
};
response.status(200).json({ settings });
} catch (e) {

View File

@ -73,10 +73,7 @@ function apiAdminEndpoints(app) {
return;
}
const users = (await User.where()).map((user) => {
const { password, ...rest } = user;
return rest;
});
const users = await User.where();
response.status(200).json({ users });
} catch (e) {
console.error(e);

View File

@ -5,6 +5,7 @@ const {
viewLocalFiles,
findDocumentInDocuments,
normalizePath,
isWithin,
} = require("../../../utils/files");
const { reqBody } = require("../../../utils/http");
const { EventLogs } = require("../../../models/eventLogs");
@ -603,6 +604,8 @@ function apiDocumentEndpoints(app) {
try {
const { name } = reqBody(request);
const storagePath = path.join(documentsPath, normalizePath(name));
if (!isWithin(path.resolve(documentsPath), path.resolve(storagePath)))
throw new Error("Invalid path name");
if (fs.existsSync(storagePath)) {
response.status(500).json({

View File

@ -447,6 +447,76 @@ function apiWorkspaceEndpoints(app) {
}
);
app.post(
"/v1/workspace/:slug/update-pin",
[validApiKey],
async (request, response) => {
/*
#swagger.tags = ['Workspaces']
#swagger.description = 'Add or remove pin from a document in a workspace by its unique slug.'
#swagger.path = '/workspace/{slug}/update-pin'
#swagger.parameters['slug'] = {
in: 'path',
description: 'Unique slug of workspace to find',
required: true,
type: 'string'
}
#swagger.requestBody = {
description: 'JSON object with the document path and pin status to update.',
required: true,
type: 'object',
content: {
"application/json": {
example: {
docPath: "custom-documents/my-pdf.pdf-hash.json",
pinStatus: true
}
}
}
}
#swagger.responses[200] = {
description: 'OK',
content: {
"application/json": {
schema: {
type: 'object',
example: {
message: 'Pin status updated successfully'
}
}
}
}
}
#swagger.responses[404] = {
description: 'Document not found'
}
#swagger.responses[500] = {
description: 'Internal Server Error'
}
*/
try {
const { slug = null } = request.params;
const { docPath, pinStatus = false } = reqBody(request);
const workspace = await Workspace.get({ slug });
const document = await Document.get({
workspaceId: workspace.id,
docpath: docPath,
});
if (!document) return response.sendStatus(404).end();
await Document.update(document.id, { pinned: pinStatus });
return response
.status(200)
.json({ message: "Pin status updated successfully" })
.end();
} catch (error) {
console.error("Error processing the pin status update:", error);
return response.status(500).end();
}
}
);
app.post(
"/v1/workspace/:slug/chat",
[validApiKey],
@ -533,6 +603,7 @@ function apiWorkspaceEndpoints(app) {
});
response.status(200).json({ ...result });
} catch (e) {
console.log(e.message, e);
response.status(500).json({
id: uuidv4(),
type: "abort",
@ -655,7 +726,7 @@ function apiWorkspaceEndpoints(app) {
});
response.end();
} catch (e) {
console.error(e);
console.log(e.message, e);
writeResponseChunk(response, {
id: uuidv4(),
type: "abort",

View File

@ -1,5 +1,5 @@
const { Document } = require("../models/documents");
const { normalizePath, documentsPath } = require("../utils/files");
const { normalizePath, documentsPath, isWithin } = require("../utils/files");
const { reqBody } = require("../utils/http");
const {
flexUserRoleValid,
@ -18,6 +18,8 @@ function documentEndpoints(app) {
try {
const { name } = reqBody(request);
const storagePath = path.join(documentsPath, normalizePath(name));
if (!isWithin(path.resolve(documentsPath), path.resolve(storagePath)))
throw new Error("Invalid folder name.");
if (fs.existsSync(storagePath)) {
response.status(500).json({

View File

@ -93,6 +93,27 @@ function extensionEndpoints(app) {
}
}
);
app.post(
"/ext/website-depth",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
try {
const responseFromProcessor =
await new CollectorApi().forwardExtensionRequest({
endpoint: "/ext/website-depth",
method: "POST",
body: request.body,
});
await Telemetry.sendTelemetry("extension_invoked", {
type: "website_depth",
});
response.status(200).json(responseFromProcessor);
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
}
module.exports = { extensionEndpoints };

View File

@ -1,7 +1,7 @@
process.env.NODE_ENV === "development"
? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` })
: require("dotenv").config();
const { viewLocalFiles, normalizePath } = require("../utils/files");
const { viewLocalFiles, normalizePath, isWithin } = require("../utils/files");
const { purgeDocument, purgeFolder } = require("../utils/files/purgeDocument");
const { getVectorDbClass } = require("../utils/helpers");
const { updateENV, dumpENV } = require("../utils/helpers/updateENV");
@ -111,7 +111,7 @@ function systemEndpoints(app) {
if (await SystemSettings.isMultiUserMode()) {
const { username, password } = reqBody(request);
const existingUser = await User.get({ username: String(username) });
const existingUser = await User._get({ username: String(username) });
if (!existingUser) {
await EventLogs.logEvent(
@ -207,7 +207,7 @@ function systemEndpoints(app) {
// Return recovery codes to frontend
response.status(200).json({
valid: true,
user: existingUser,
user: User.filterFields(existingUser),
token: makeJWT(
{ id: existingUser.id, username: existingUser.username },
"30d"
@ -220,7 +220,7 @@ function systemEndpoints(app) {
response.status(200).json({
valid: true,
user: existingUser,
user: User.filterFields(existingUser),
token: makeJWT(
{ id: existingUser.id, username: existingUser.username },
"30d"
@ -571,14 +571,22 @@ function systemEndpoints(app) {
return;
}
let error = null;
const { usePassword, newPassword } = reqBody(request);
const { error } = await updateENV(
{
AuthToken: usePassword ? newPassword : "",
JWTSecret: usePassword ? v4() : "",
},
true
);
if (!usePassword) {
// Password is being disabled so directly unset everything to bypass validation.
process.env.AUTH_TOKEN = "";
process.env.JWT_SECRET = "";
} else {
error = await updateENV(
{
AuthToken: newPassword,
JWTSecret: v4(),
},
true
)?.error;
}
if (process.env.NODE_ENV === "production") await dumpENV();
response.status(200).json({ success: !error, error });
} catch (e) {
@ -653,17 +661,24 @@ function systemEndpoints(app) {
const defaultFilename = getDefaultFilename();
const logoPath = await determineLogoFilepath(defaultFilename);
const { found, buffer, size, mime } = fetchLogo(logoPath);
if (!found) {
response.sendStatus(204).end();
return;
}
const currentLogoFilename = await SystemSettings.currentLogoFilename();
response.writeHead(200, {
"Access-Control-Expose-Headers":
"Content-Disposition,X-Is-Custom-Logo,Content-Type,Content-Length",
"Content-Type": mime || "image/png",
"Content-Disposition": `attachment; filename=${path.basename(
logoPath
)}`,
"Content-Length": size,
"X-Is-Custom-Logo":
currentLogoFilename !== null &&
currentLogoFilename !== defaultFilename,
});
response.end(Buffer.from(buffer, "base64"));
return;
@ -700,6 +715,22 @@ function systemEndpoints(app) {
}
});
// No middleware protection in order to get this on the login page
app.get("/system/custom-app-name", async (_, response) => {
try {
const customAppName =
(
await SystemSettings.get({
label: "custom_app_name",
})
)?.value ?? null;
response.status(200).json({ customAppName: customAppName });
} catch (error) {
console.error("Error fetching custom app name:", error);
response.status(500).json({ message: "Internal server error" });
}
});
app.get(
"/system/pfp/:id",
[validatedRequest, flexUserRoleValid([ROLES.all])],
@ -749,11 +780,13 @@ function systemEndpoints(app) {
const userRecord = await User.get({ id: user.id });
const oldPfpFilename = userRecord.pfpFilename;
if (oldPfpFilename) {
const storagePath = path.join(__dirname, "../storage/assets/pfp");
const oldPfpPath = path.join(
__dirname,
`../storage/assets/pfp/${normalizePath(userRecord.pfpFilename)}`
storagePath,
normalizePath(userRecord.pfpFilename)
);
if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))
throw new Error("Invalid path name");
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
}
@ -782,13 +815,14 @@ function systemEndpoints(app) {
const userRecord = await User.get({ id: user.id });
const oldPfpFilename = userRecord.pfpFilename;
console.log("oldPfpFilename", oldPfpFilename);
if (oldPfpFilename) {
const storagePath = path.join(__dirname, "../storage/assets/pfp");
const oldPfpPath = path.join(
__dirname,
`../storage/assets/pfp/${normalizePath(oldPfpFilename)}`
storagePath,
normalizePath(oldPfpFilename)
);
if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))
throw new Error("Invalid path name");
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
}
@ -1111,7 +1145,9 @@ function systemEndpoints(app) {
async (request, response) => {
try {
const { id } = request.params;
await WorkspaceChats.delete({ id: Number(id) });
Number(id) === -1
? await WorkspaceChats.delete({}, true)
: await WorkspaceChats.delete({ id: Number(id) });
response.json({ success: true, error: null });
} catch (e) {
console.error(e);
@ -1159,7 +1195,7 @@ function systemEndpoints(app) {
const updates = {};
if (username) {
updates.username = String(username);
updates.username = User.validations.username(String(username));
}
if (password) {
updates.password = String(password);

View File

@ -6,7 +6,7 @@ const {
userFromSession,
safeJsonParse,
} = require("../utils/http");
const { normalizePath } = require("../utils/files");
const { normalizePath, isWithin } = require("../utils/files");
const { Workspace } = require("../models/workspace");
const { Document } = require("../models/documents");
const { DocumentVectors } = require("../models/vectors");
@ -111,39 +111,45 @@ function workspaceEndpoints(app) {
handleFileUpload,
],
async function (request, response) {
const Collector = new CollectorApi();
const { originalname } = request.file;
const processingOnline = await Collector.online();
try {
const Collector = new CollectorApi();
const { originalname } = request.file;
const processingOnline = await Collector.online();
if (!processingOnline) {
response
.status(500)
.json({
success: false,
error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,
})
.end();
return;
if (!processingOnline) {
response
.status(500)
.json({
success: false,
error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,
})
.end();
return;
}
const { success, reason } =
await Collector.processDocument(originalname);
if (!success) {
response.status(500).json({ success: false, error: reason }).end();
return;
}
Collector.log(
`Document ${originalname} uploaded processed and successfully. It is now available in documents.`
);
await Telemetry.sendTelemetry("document_uploaded");
await EventLogs.logEvent(
"document_uploaded",
{
documentName: originalname,
},
response.locals?.user?.id
);
response.status(200).json({ success: true, error: null });
} catch (e) {
console.log(e.message, e);
response.sendStatus(500).end();
}
const { success, reason } = await Collector.processDocument(originalname);
if (!success) {
response.status(500).json({ success: false, error: reason }).end();
return;
}
Collector.log(
`Document ${originalname} uploaded processed and successfully. It is now available in documents.`
);
await Telemetry.sendTelemetry("document_uploaded");
await EventLogs.logEvent(
"document_uploaded",
{
documentName: originalname,
},
response.locals?.user?.id
);
response.status(200).json({ success: true, error: null });
}
);
@ -151,37 +157,42 @@ function workspaceEndpoints(app) {
"/workspace/:slug/upload-link",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
const Collector = new CollectorApi();
const { link = "" } = reqBody(request);
const processingOnline = await Collector.online();
try {
const Collector = new CollectorApi();
const { link = "" } = reqBody(request);
const processingOnline = await Collector.online();
if (!processingOnline) {
response
.status(500)
.json({
success: false,
error: `Document processing API is not online. Link ${link} will not be processed automatically.`,
})
.end();
return;
if (!processingOnline) {
response
.status(500)
.json({
success: false,
error: `Document processing API is not online. Link ${link} will not be processed automatically.`,
})
.end();
return;
}
const { success, reason } = await Collector.processLink(link);
if (!success) {
response.status(500).json({ success: false, error: reason }).end();
return;
}
Collector.log(
`Link ${link} uploaded processed and successfully. It is now available in documents.`
);
await Telemetry.sendTelemetry("link_uploaded");
await EventLogs.logEvent(
"link_uploaded",
{ link },
response.locals?.user?.id
);
response.status(200).json({ success: true, error: null });
} catch (e) {
console.log(e.message, e);
response.sendStatus(500).end();
}
const { success, reason } = await Collector.processLink(link);
if (!success) {
response.status(500).json({ success: false, error: reason }).end();
return;
}
Collector.log(
`Link ${link} uploaded processed and successfully. It is now available in documents.`
);
await Telemetry.sendTelemetry("link_uploaded");
await EventLogs.logEvent(
"link_uploaded",
{ link },
response.locals?.user?.id
);
response.status(200).json({ success: true, error: null });
}
);
@ -618,13 +629,13 @@ function workspaceEndpoints(app) {
const oldPfpFilename = workspaceRecord.pfpFilename;
if (oldPfpFilename) {
const storagePath = path.join(__dirname, "../storage/assets/pfp");
const oldPfpPath = path.join(
__dirname,
`../storage/assets/pfp/${normalizePath(
workspaceRecord.pfpFilename
)}`
storagePath,
normalizePath(workspaceRecord.pfpFilename)
);
if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))
throw new Error("Invalid path name");
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
}
@ -659,11 +670,13 @@ function workspaceEndpoints(app) {
const oldPfpFilename = workspaceRecord.pfpFilename;
if (oldPfpFilename) {
const storagePath = path.join(__dirname, "../storage/assets/pfp");
const oldPfpPath = path.join(
__dirname,
`../storage/assets/pfp/${normalizePath(oldPfpFilename)}`
storagePath,
normalizePath(oldPfpFilename)
);
if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))
throw new Error("Invalid path name");
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
}

View File

@ -36,7 +36,12 @@ app.use(
})
);
require("express-ws")(app);
if (!!process.env.ENABLE_HTTPS) {
bootSSL(app, process.env.SERVER_PORT || 3001);
} else {
require("express-ws")(app); // load WebSockets in non-SSL mode.
}
app.use("/api", apiRouter);
systemEndpoints(apiRouter);
extensionEndpoints(apiRouter);
@ -109,8 +114,6 @@ app.all("*", function (_, response) {
response.sendStatus(404);
});
if (!!process.env.ENABLE_HTTPS) {
bootSSL(app, process.env.SERVER_PORT || 3001);
} else {
bootHTTP(app, process.env.SERVER_PORT || 3001);
}
// In non-https mode we need to boot at the end since the server has not yet
// started and is `.listen`ing.
if (!process.env.ENABLE_HTTPS) bootHTTP(app, process.env.SERVER_PORT || 3001);

View File

@ -2,8 +2,10 @@ process.env.NODE_ENV === "development"
? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` })
: require("dotenv").config();
const { isValidUrl } = require("../utils/http");
const { default: slugify } = require("slugify");
const { isValidUrl, safeJsonParse } = require("../utils/http");
const prisma = require("../utils/prisma");
const { v4 } = require("uuid");
function isNullOrNaN(value) {
if (value === null) return true;
@ -26,6 +28,8 @@ const SystemSettings = {
"default_agent_skills",
"users_can_login_with_google",
"allowed_domain",
"agent_sql_connections",
"custom_app_name",
],
validations: {
footer_data: (updates) => {
@ -67,7 +71,12 @@ const SystemSettings = {
},
agent_search_provider: (update) => {
try {
if (!["google-search-engine", "serper-dot-dev"].includes(update))
if (update === "none") return null;
if (
!["google-search-engine", "serper-dot-dev", "bing-search"].includes(
update
)
)
throw new Error("Invalid SERP provider.");
return String(update);
} catch (e) {
@ -87,6 +96,22 @@ const SystemSettings = {
return JSON.stringify([]);
}
},
agent_sql_connections: async (updates) => {
const existingConnections = safeJsonParse(
(await SystemSettings.get({ label: "agent_sql_connections" }))?.value,
[]
);
try {
const updatedConnections = mergeConnections(
existingConnections,
safeJsonParse(updates, [])
);
return JSON.stringify(updatedConnections);
} catch (e) {
console.error(`Failed to merge connections`);
return JSON.stringify(existingConnections ?? []);
}
},
},
currentSettings: async function () {
const { hasVectorCachedFiles } = require("../utils/files");
@ -132,6 +157,8 @@ const SystemSettings = {
// - then it can be shared.
// --------------------------------------------------------
WhisperProvider: process.env.WHISPER_PROVIDER || "local",
WhisperModelPref:
process.env.WHISPER_MODEL_PREF || "Xenova/whisper-small",
// --------------------------------------------------------
// TTS/STT Selection Settings & Configs
@ -150,6 +177,7 @@ const SystemSettings = {
AgentGoogleSearchEngineId: process.env.AGENT_GSE_CTX || null,
AgentGoogleSearchEngineKey: process.env.AGENT_GSE_KEY || null,
AgentSerperApiKey: process.env.AGENT_SERPER_DEV_KEY || null,
AgentBingSearchApiKey: process.env.AGENT_BING_SEARCH_API_KEY || null,
// --------------------------------------------------------
// Social Providers
@ -215,22 +243,30 @@ const SystemSettings = {
// that takes no user input for the keys being modified.
_updateSettings: async function (updates = {}) {
try {
const updatePromises = Object.keys(updates).map((key) => {
const validatedValue = this.validations.hasOwnProperty(key)
? this.validations[key](updates[key])
: updates[key];
const updatePromises = [];
for (const key of Object.keys(updates)) {
let validatedValue = updates[key];
if (this.validations.hasOwnProperty(key)) {
if (this.validations[key].constructor.name === "AsyncFunction") {
validatedValue = await this.validations[key](updates[key]);
} else {
validatedValue = this.validations[key](updates[key]);
}
}
return prisma.system_settings.upsert({
where: { label: key },
update: {
value: validatedValue === null ? null : String(validatedValue),
},
create: {
label: key,
value: validatedValue === null ? null : String(validatedValue),
},
});
});
updatePromises.push(
prisma.system_settings.upsert({
where: { label: key },
update: {
value: validatedValue === null ? null : String(validatedValue),
},
create: {
label: key,
value: validatedValue === null ? null : String(validatedValue),
},
})
);
}
await Promise.all(updatePromises);
return { success: true, error: null };
@ -335,6 +371,8 @@ const SystemSettings = {
// Gemini Keys
GeminiLLMApiKey: !!process.env.GEMINI_API_KEY,
GeminiLLMModelPref: process.env.GEMINI_LLM_MODEL_PREF || "gemini-pro",
GeminiSafetySetting:
process.env.GEMINI_SAFETY_SETTING || "BLOCK_MEDIUM_AND_ABOVE",
// LMStudio Keys
LMStudioBasePath: process.env.LMSTUDIO_BASE_PATH,
@ -391,6 +429,12 @@ const SystemSettings = {
TextGenWebUITokenLimit: process.env.TEXT_GEN_WEB_UI_MODEL_TOKEN_LIMIT,
TextGenWebUIAPIKey: !!process.env.TEXT_GEN_WEB_UI_API_KEY,
// LiteLLM Keys
LiteLLMModelPref: process.env.LITE_LLM_MODEL_PREF,
LiteLLMTokenLimit: process.env.LITE_LLM_MODEL_TOKEN_LIMIT,
LiteLLMBasePath: process.env.LITE_LLM_BASE_PATH,
LiteLLMApiKey: !!process.env.LITE_LLM_API_KEY,
// Generic OpenAI Keys
GenericOpenAiBasePath: process.env.GENERIC_OPEN_AI_BASE_PATH,
GenericOpenAiModelPref: process.env.GENERIC_OPEN_AI_MODEL_PREF,
@ -401,8 +445,63 @@ const SystemSettings = {
// Cohere API Keys
CohereApiKey: !!process.env.COHERE_API_KEY,
CohereModelPref: process.env.COHERE_MODEL_PREF,
// VoyageAi API Keys
VoyageAiApiKey: !!process.env.VOYAGEAI_API_KEY,
};
},
// For special retrieval of a key setting that does not expose any credential information
brief: {
agent_sql_connections: async function () {
const setting = await SystemSettings.get({
label: "agent_sql_connections",
});
if (!setting) return [];
return safeJsonParse(setting.value, []).map((dbConfig) => {
const { connectionString, ...rest } = dbConfig;
return rest;
});
},
},
};
function mergeConnections(existingConnections = [], updates = []) {
let updatedConnections = [...existingConnections];
const existingDbIds = existingConnections.map((conn) => conn.database_id);
// First remove all 'action:remove' candidates from existing connections.
const toRemove = updates
.filter((conn) => conn.action === "remove")
.map((conn) => conn.database_id);
updatedConnections = updatedConnections.filter(
(conn) => !toRemove.includes(conn.database_id)
);
// Next add all 'action:add' candidates into the updatedConnections; We DO NOT validate the connection strings.
// but we do validate their database_id is unique.
updates
.filter((conn) => conn.action === "add")
.forEach((update) => {
if (!update.connectionString) return; // invalid connection string
// Remap name to be unique to entire set.
if (existingDbIds.includes(update.database_id)) {
update.database_id = slugify(
`${update.database_id}-${v4().slice(0, 4)}`
);
} else {
update.database_id = slugify(update.database_id);
}
updatedConnections.push({
engine: update.engine,
database_id: update.database_id,
connectionString: update.connectionString,
});
});
return updatedConnections;
}
module.exports.SystemSettings = SystemSettings;

View File

@ -10,6 +10,20 @@ const User = {
"role",
"suspended",
],
validations: {
username: (newValue = "") => {
try {
if (String(newValue).length > 100)
throw new Error("Username cannot be longer than 100 characters");
if (String(newValue).length < 2)
throw new Error("Username must be at least 2 characters");
return String(newValue);
} catch (e) {
throw new Error(e.message);
}
},
},
// validations for the above writable fields.
castColumnValue: function (key, value) {
switch (key) {
@ -19,6 +33,12 @@ const User = {
return String(value);
}
},
filterFields: function (user = {}) {
const { password, ...rest } = user;
return { ...rest };
},
create: async function ({ username, password, role = "default" }) {
const passwordCheck = this.checkPasswordComplexity(password);
if (!passwordCheck.checkedOK) {
@ -30,12 +50,12 @@ const User = {
const hashedPassword = bcrypt.hashSync(password, 10);
const user = await prisma.users.create({
data: {
username,
username: this.validations.username(username),
password: hashedPassword,
role,
role: String(role),
},
});
return { user, error: null };
return { user: this.filterFields(user), error: null };
} catch (error) {
console.error("FAILED TO CREATE USER.", error.message);
return { user: null, error: error.message };
@ -84,7 +104,13 @@ const User = {
// and force-casts to the proper type;
Object.entries(updates).forEach(([key, value]) => {
if (this.writable.includes(key)) {
updates[key] = this.castColumnValue(key, value);
if (this.validations.hasOwnProperty(key)) {
updates[key] = this.validations[key](
this.castColumnValue(key, value)
);
} else {
updates[key] = this.castColumnValue(key, value);
}
return;
}
delete updates[key];
@ -142,6 +168,17 @@ const User = {
},
get: async function (clause = {}) {
try {
const user = await prisma.users.findFirst({ where: clause });
return user ? this.filterFields({ ...user }) : null;
} catch (error) {
console.error(error.message);
return null;
}
},
// Returns user object with all fields
_get: async function (clause = {}) {
try {
const user = await prisma.users.findFirst({ where: clause });
return user ? { ...user } : null;
@ -177,7 +214,7 @@ const User = {
where: clause,
...(limit !== null ? { take: limit } : {}),
});
return users;
return users.map((usr) => this.filterFields(usr));
} catch (error) {
console.error(error.message);
return [];

View File

@ -59,11 +59,14 @@
"langchain": "0.1.36",
"mime": "^3.0.0",
"moment": "^2.29.4",
"mssql": "^10.0.2",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.9.7",
"node-html-markdown": "^1.3.0",
"node-llama-cpp": "^2.8.0",
"ollama": "^0.5.0",
"openai": "4.38.5",
"pg": "^8.11.5",
"pinecone-client": "^1.1.0",
"pluralize": "^8.0.0",
"posthog-node": "^3.1.1",
@ -73,6 +76,7 @@
"sqlite3": "^5.1.6",
"swagger-autogen": "^2.23.5",
"swagger-ui-express": "^5.0.0",
"url-pattern": "^1.0.3",
"uuid": "^9.0.0",
"uuid-apikey": "^1.5.3",
"vectordb": "0.4.11",

View File

@ -19,10 +19,10 @@ function waitForElm(selector) {
}
// Force change the Swagger logo in the header
waitForElm('img[alt="Swagger UI"]').then((elm) => {
waitForElm('.topbar-wrapper').then((elm) => {
if (window.SWAGGER_DOCS_ENV === 'development') {
elm.src = 'http://localhost:3000/public/anything-llm.png'
elm.innerHTML = `<img href='${window.location.origin}' src='http://localhost:3000/public/anything-llm-light.png' width='200'/>`
} else {
elm.src = `${window.location.origin}/anything-llm.png`
elm.innerHTML = `<img href='${window.location.origin}' src='${window.location.origin}/anything-llm-light.png' width='200'/>`
}
});

View File

@ -40,6 +40,28 @@ const endpointsFiles = [
swaggerAutogen(outputFile, endpointsFiles, doc)
.then(({ data }) => {
// Remove Authorization parameters from arguments.
for (const path of Object.keys(data.paths)) {
if (data.paths[path].hasOwnProperty('get')) {
let parameters = data.paths[path].get?.parameters || [];
parameters = parameters.filter((arg) => arg.name !== 'Authorization');
data.paths[path].get.parameters = parameters;
}
if (data.paths[path].hasOwnProperty('post')) {
let parameters = data.paths[path].post?.parameters || [];
parameters = parameters.filter((arg) => arg.name !== 'Authorization');
data.paths[path].post.parameters = parameters;
}
if (data.paths[path].hasOwnProperty('delete')) {
let parameters = data.paths[path].delete?.parameters || [];
parameters = parameters.filter((arg) => arg.name !== 'Authorization');
data.paths[path].delete.parameters = parameters;
}
}
const openApiSpec = {
...data,
servers: [{

View File

@ -17,15 +17,7 @@
"Authentication"
],
"description": "Verify the attached Authentication header contains a valid API token.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "Valid auth token was found.",
@ -64,15 +56,7 @@
"Admin"
],
"description": "Check to see if the instance is in multi-user-mode first. Methods are disabled until multi user mode is enabled via the UI.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -111,15 +95,7 @@
"Admin"
],
"description": "Check to see if the instance is in multi-user-mode first. Methods are disabled until multi user mode is enabled via the UI.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -169,15 +145,7 @@
"Admin"
],
"description": "Create a new user with username and password. Methods are disabled until multi user mode is enabled via the UI.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -250,13 +218,6 @@
"type": "string"
},
"description": "id of the user in the database."
},
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
@ -326,13 +287,6 @@
"type": "string"
},
"description": "id of the user in the database."
},
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
@ -380,15 +334,7 @@
"Admin"
],
"description": "List all existing invitations to instance regardless of status. Methods are disabled until multi user mode is enabled via the UI.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -440,15 +386,7 @@
"Admin"
],
"description": "Create a new invite code for someone to use to register with instance. Methods are disabled until multi user mode is enabled via the UI.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -523,13 +461,6 @@
"type": "string"
},
"description": "id of the invite in the database."
},
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
@ -586,13 +517,6 @@
"type": "string"
},
"description": "id of the workspace in the database."
},
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
@ -657,15 +581,7 @@
"Admin"
],
"description": "All chats in the system ordered by most recent. Methods are disabled until multi user mode is enabled via the UI.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -720,15 +636,7 @@
"Admin"
],
"description": "Show all multi-user preferences for instance. Methods are disabled until multi user mode is enabled via the UI.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -775,15 +683,7 @@
"Admin"
],
"description": "Update multi-user preferences for instance. Methods are disabled until multi user mode is enabled via the UI.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -843,15 +743,7 @@
"Documents"
],
"description": "Upload a new file to AnythingLLM to be parsed and prepared for embedding.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -927,15 +819,7 @@
"Documents"
],
"description": "Upload a valid URL for AnythingLLM to scrape and prepare for embedding.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -1009,15 +893,7 @@
"Documents"
],
"description": "Upload a file by specifying its raw text content and metadata values without having to upload a file.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -1099,15 +975,7 @@
"Documents"
],
"description": "List of all locally-stored documents in instance",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -1162,15 +1030,7 @@
"Documents"
],
"description": "Check available filetypes and MIMEs that can be uploaded.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -1232,15 +1092,7 @@
"Documents"
],
"description": "Get the known available metadata schema for when doing a raw-text upload and the acceptable type of value for each key.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -1296,13 +1148,6 @@
"type": "string"
},
"description": "Unique document name to find (name in /documents)"
},
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
@ -1362,15 +1207,7 @@
"Documents"
],
"description": "Create a new folder inside the documents storage directory.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -1428,15 +1265,7 @@
"Documents"
],
"description": "Move files within the documents storage directory.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -1499,15 +1328,7 @@
"Workspaces"
],
"description": "Create a new workspace",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -1571,15 +1392,7 @@
"Workspaces"
],
"description": "List all current workspaces",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -1641,13 +1454,6 @@
"type": "string"
},
"description": "Unique slug of workspace to find"
},
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
@ -1708,13 +1514,6 @@
"type": "string"
},
"description": "Unique slug of workspace to delete"
},
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
@ -1760,13 +1559,6 @@
"type": "string"
},
"description": "Unique slug of workspace to find"
},
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
@ -1848,13 +1640,6 @@
"type": "string"
},
"description": "Unique slug of workspace to find"
},
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
@ -1925,13 +1710,6 @@
"type": "string"
},
"description": "Unique slug of workspace to find"
},
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
@ -2000,6 +1778,62 @@
}
}
},
"/workspace/{slug}/update-pin": {
"post": {
"tags": [
"Workspaces"
],
"description": "Add or remove pin from a document in a workspace by its unique slug.",
"parameters": [
{
"name": "slug",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "Unique slug of workspace to find"
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"message": "Pin status updated successfully"
}
}
}
}
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "Document not found"
},
"500": {
"description": "Internal Server Error"
}
},
"requestBody": {
"description": "JSON object with the document path and pin status to update.",
"required": true,
"type": "object",
"content": {
"application/json": {
"example": {
"docPath": "custom-documents/my-pdf.pdf-hash.json",
"pinStatus": true
}
}
}
}
}
},
"/v1/workspace/{slug}/chat": {
"post": {
"tags": [
@ -2014,13 +1848,6 @@
"schema": {
"type": "string"
}
},
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
@ -2098,13 +1925,6 @@
"schema": {
"type": "string"
}
},
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
@ -2189,6 +2009,7 @@
"System Settings"
],
"description": "Dump all settings to file storage",
"parameters": [],
"responses": {
"200": {
"description": "OK"
@ -2220,15 +2041,7 @@
"System Settings"
],
"description": "Get all current system settings that are defined.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -2276,15 +2089,7 @@
"System Settings"
],
"description": "Number of all vectors in connected vector database",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -2326,15 +2131,7 @@
"System Settings"
],
"description": "Update a system setting or preference.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK",
@ -2393,13 +2190,6 @@
],
"description": "Export all of the chats from the system in a known format. Output depends on the type sent. Will be send with the correct header for the output.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
},
{
"name": "type",
"in": "query",

View File

@ -3,6 +3,7 @@ const {
writeResponseChunk,
clientAbortedHandler,
} = require("../../helpers/chat/responses");
const { NativeEmbedder } = require("../../EmbeddingEngines/native");
class AnthropicLLM {
constructor(embedder = null, modelPreference = null) {
@ -23,11 +24,7 @@ class AnthropicLLM {
user: this.promptWindowLimit() * 0.7,
};
if (!embedder)
throw new Error(
"INVALID ANTHROPIC SETUP. No embedding engine has been set. Go to instance settings and set up an embedding interface to use Anthropic as your LLM."
);
this.embedder = embedder;
this.embedder = embedder ?? new NativeEmbedder();
this.defaultTemp = 0.7;
}

View File

@ -1,4 +1,4 @@
const { AzureOpenAiEmbedder } = require("../../EmbeddingEngines/azureOpenAi");
const { NativeEmbedder } = require("../../EmbeddingEngines/native");
const {
writeResponseChunk,
clientAbortedHandler,
@ -23,11 +23,7 @@ class AzureOpenAiLLM {
user: this.promptWindowLimit() * 0.7,
};
if (!embedder)
console.warn(
"No embedding provider defined for AzureOpenAiLLM - falling back to AzureOpenAiEmbedder for embedding!"
);
this.embedder = !embedder ? new AzureOpenAiEmbedder() : embedder;
this.embedder = embedder ?? new NativeEmbedder();
this.defaultTemp = 0.7;
}

View File

@ -19,7 +19,8 @@ class CohereLLM {
system: this.promptWindowLimit() * 0.15,
user: this.promptWindowLimit() * 0.7,
};
this.embedder = !!embedder ? embedder : new NativeEmbedder();
this.embedder = embedder ?? new NativeEmbedder();
}
#appendContext(contextTexts = []) {

View File

@ -1,3 +1,4 @@
const { NativeEmbedder } = require("../../EmbeddingEngines/native");
const {
writeResponseChunk,
clientAbortedHandler,
@ -16,8 +17,12 @@ class GeminiLLM {
this.gemini = genAI.getGenerativeModel(
{ model: this.model },
{
// Gemini-1.5-pro is only available on the v1beta API.
apiVersion: this.model === "gemini-1.5-pro-latest" ? "v1beta" : "v1",
// Gemini-1.5-pro and Gemini-1.5-flash are only available on the v1beta API.
apiVersion:
this.model === "gemini-1.5-pro-latest" ||
this.model === "gemini-1.5-flash-latest"
? "v1beta"
: "v1",
}
);
this.limits = {
@ -26,12 +31,9 @@ class GeminiLLM {
user: this.promptWindowLimit() * 0.7,
};
if (!embedder)
throw new Error(
"INVALID GEMINI LLM SETUP. No embedding engine has been set. Go to instance settings and set up an embedding interface to use Gemini as your LLM."
);
this.embedder = embedder;
this.embedder = embedder ?? new NativeEmbedder();
this.defaultTemp = 0.7; // not used for Gemini
this.safetyThreshold = this.#fetchSafetyThreshold();
}
#appendContext(contextTexts = []) {
@ -46,6 +48,41 @@ class GeminiLLM {
);
}
// BLOCK_NONE can be a special candidate for some fields
// https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/configure-safety-attributes#how_to_remove_automated_response_blocking_for_select_safety_attributes
// so if you are wondering why BLOCK_NONE still failed, the link above will explain why.
#fetchSafetyThreshold() {
const threshold =
process.env.GEMINI_SAFETY_SETTING ?? "BLOCK_MEDIUM_AND_ABOVE";
const safetyThresholds = [
"BLOCK_NONE",
"BLOCK_ONLY_HIGH",
"BLOCK_MEDIUM_AND_ABOVE",
"BLOCK_LOW_AND_ABOVE",
];
return safetyThresholds.includes(threshold)
? threshold
: "BLOCK_MEDIUM_AND_ABOVE";
}
#safetySettings() {
return [
{
category: "HARM_CATEGORY_HATE_SPEECH",
threshold: this.safetyThreshold,
},
{
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
threshold: this.safetyThreshold,
},
{ category: "HARM_CATEGORY_HARASSMENT", threshold: this.safetyThreshold },
{
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
threshold: this.safetyThreshold,
},
];
}
streamingEnabled() {
return "streamGetChatCompletion" in this;
}
@ -54,6 +91,10 @@ class GeminiLLM {
switch (this.model) {
case "gemini-pro":
return 30_720;
case "gemini-1.0-pro":
return 30_720;
case "gemini-1.5-flash-latest":
return 1_048_576;
case "gemini-1.5-pro-latest":
return 1_048_576;
default:
@ -62,7 +103,12 @@ class GeminiLLM {
}
isValidChatCompletionModel(modelName = "") {
const validModels = ["gemini-pro", "gemini-1.5-pro-latest"];
const validModels = [
"gemini-pro",
"gemini-1.0-pro",
"gemini-1.5-pro-latest",
"gemini-1.5-flash-latest",
];
return validModels.includes(modelName);
}
@ -146,6 +192,7 @@ class GeminiLLM {
)?.content;
const chatThread = this.gemini.startChat({
history: this.formatMessages(messages),
safetySettings: this.#safetySettings(),
});
const result = await chatThread.sendMessage(prompt);
const response = result.response;
@ -167,6 +214,7 @@ class GeminiLLM {
)?.content;
const chatThread = this.gemini.startChat({
history: this.formatMessages(messages),
safetySettings: this.#safetySettings(),
});
const responseStream = await chatThread.sendMessageStream(prompt);
if (!responseStream.stream)

View File

@ -2,6 +2,7 @@ const { NativeEmbedder } = require("../../EmbeddingEngines/native");
const {
handleDefaultStreamResponseV2,
} = require("../../helpers/chat/responses");
const { toValidNumber } = require("../../http");
class GenericOpenAiLLM {
constructor(embedder = null, modelPreference = null) {
@ -18,7 +19,9 @@ class GenericOpenAiLLM {
});
this.model =
modelPreference ?? process.env.GENERIC_OPEN_AI_MODEL_PREF ?? null;
this.maxTokens = process.env.GENERIC_OPEN_AI_MAX_TOKENS ?? 1024;
this.maxTokens = process.env.GENERIC_OPEN_AI_MAX_TOKENS
? toValidNumber(process.env.GENERIC_OPEN_AI_MAX_TOKENS, 1024)
: 1024;
if (!this.model)
throw new Error("GenericOpenAI must have a valid model set.");
this.limits = {
@ -27,11 +30,7 @@ class GenericOpenAiLLM {
user: this.promptWindowLimit() * 0.7,
};
if (!embedder)
console.warn(
"No embedding provider defined for GenericOpenAiLLM - falling back to NativeEmbedder for embedding!"
);
this.embedder = !embedder ? new NativeEmbedder() : embedder;
this.embedder = embedder ?? new NativeEmbedder();
this.defaultTemp = 0.7;
this.log(`Inference API: ${this.basePath} Model: ${this.model}`);
}
@ -98,7 +97,7 @@ class GenericOpenAiLLM {
max_tokens: this.maxTokens,
})
.catch((e) => {
throw new Error(e.response.data.error.message);
throw new Error(e.message);
});
if (!result.hasOwnProperty("choices") || result.choices.length === 0)

View File

@ -20,7 +20,7 @@ class GroqLLM {
user: this.promptWindowLimit() * 0.7,
};
this.embedder = !embedder ? new NativeEmbedder() : embedder;
this.embedder = embedder ?? new NativeEmbedder();
this.defaultTemp = 0.7;
}
@ -103,7 +103,7 @@ class GroqLLM {
temperature,
})
.catch((e) => {
throw new Error(e.response.data.error.message);
throw new Error(e.message);
});
if (!result.hasOwnProperty("choices") || result.choices.length === 0)

View File

@ -1,5 +1,4 @@
const { NativeEmbedder } = require("../../EmbeddingEngines/native");
const { OpenAiEmbedder } = require("../../EmbeddingEngines/openAi");
const {
handleDefaultStreamResponseV2,
} = require("../../helpers/chat/responses");
@ -26,11 +25,7 @@ class HuggingFaceLLM {
user: this.promptWindowLimit() * 0.7,
};
if (!embedder)
console.warn(
"No embedding provider defined for HuggingFaceLLM - falling back to Native for embedding!"
);
this.embedder = !embedder ? new OpenAiEmbedder() : new NativeEmbedder();
this.embedder = embedder ?? new NativeEmbedder();
this.defaultTemp = 0.2;
}

View File

@ -26,11 +26,7 @@ class KoboldCPPLLM {
user: this.promptWindowLimit() * 0.7,
};
if (!embedder)
console.warn(
"No embedding provider defined for KoboldCPPLLM - falling back to NativeEmbedder for embedding!"
);
this.embedder = !embedder ? new NativeEmbedder() : embedder;
this.embedder = embedder ?? new NativeEmbedder();
this.defaultTemp = 0.7;
this.log(`Inference API: ${this.basePath} Model: ${this.model}`);
}
@ -96,7 +92,7 @@ class KoboldCPPLLM {
temperature,
})
.catch((e) => {
throw new Error(e.response.data.error.message);
throw new Error(e.message);
});
if (!result.hasOwnProperty("choices") || result.choices.length === 0)

View File

@ -0,0 +1,135 @@
const { NativeEmbedder } = require("../../EmbeddingEngines/native");
const {
handleDefaultStreamResponseV2,
} = require("../../helpers/chat/responses");
class LiteLLM {
constructor(embedder = null, modelPreference = null) {
const { OpenAI: OpenAIApi } = require("openai");
if (!process.env.LITE_LLM_BASE_PATH)
throw new Error(
"LiteLLM must have a valid base path to use for the api."
);
this.basePath = process.env.LITE_LLM_BASE_PATH;
this.openai = new OpenAIApi({
baseURL: this.basePath,
apiKey: process.env.LITE_LLM_API_KEY ?? null,
});
this.model = modelPreference ?? process.env.LITE_LLM_MODEL_PREF ?? null;
this.maxTokens = process.env.LITE_LLM_MODEL_TOKEN_LIMIT ?? 1024;
if (!this.model) throw new Error("LiteLLM must have a valid model set.");
this.limits = {
history: this.promptWindowLimit() * 0.15,
system: this.promptWindowLimit() * 0.15,
user: this.promptWindowLimit() * 0.7,
};
this.embedder = embedder ?? new NativeEmbedder();
this.defaultTemp = 0.7;
this.log(`Inference API: ${this.basePath} Model: ${this.model}`);
}
log(text, ...args) {
console.log(`\x1b[36m[${this.constructor.name}]\x1b[0m ${text}`, ...args);
}
#appendContext(contextTexts = []) {
if (!contextTexts || !contextTexts.length) return "";
return (
"\nContext:\n" +
contextTexts
.map((text, i) => {
return `[CONTEXT ${i}]:\n${text}\n[END CONTEXT ${i}]\n\n`;
})
.join("")
);
}
streamingEnabled() {
return "streamGetChatCompletion" in this;
}
// Ensure the user set a value for the token limit
// and if undefined - assume 4096 window.
promptWindowLimit() {
const limit = process.env.LITE_LLM_MODEL_TOKEN_LIMIT || 4096;
if (!limit || isNaN(Number(limit)))
throw new Error("No token context limit was set.");
return Number(limit);
}
// Short circuit since we have no idea if the model is valid or not
// in pre-flight for generic endpoints
isValidChatCompletionModel(_modelName = "") {
return true;
}
constructPrompt({
systemPrompt = "",
contextTexts = [],
chatHistory = [],
userPrompt = "",
}) {
const prompt = {
role: "system",
content: `${systemPrompt}${this.#appendContext(contextTexts)}`,
};
return [prompt, ...chatHistory, { role: "user", content: userPrompt }];
}
async isSafe(_input = "") {
// Not implemented so must be stubbed
return { safe: true, reasons: [] };
}
async getChatCompletion(messages = null, { temperature = 0.7 }) {
const result = await this.openai.chat.completions
.create({
model: this.model,
messages,
temperature,
max_tokens: parseInt(this.maxTokens), // LiteLLM requires int
})
.catch((e) => {
throw new Error(e.message);
});
if (!result.hasOwnProperty("choices") || result.choices.length === 0)
return null;
return result.choices[0].message.content;
}
async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {
const streamRequest = await this.openai.chat.completions.create({
model: this.model,
stream: true,
messages,
temperature,
max_tokens: parseInt(this.maxTokens), // LiteLLM requires int
});
return streamRequest;
}
handleStream(response, stream, responseProps) {
return handleDefaultStreamResponseV2(response, stream, responseProps);
}
// Simple wrapper for dynamic embedder & normalize interface for all LLM implementations
async embedTextInput(textInput) {
return await this.embedder.embedTextInput(textInput);
}
async embedChunks(textChunks = []) {
return await this.embedder.embedChunks(textChunks);
}
async compressMessages(promptArgs = {}, rawHistory = []) {
const { messageArrayCompressor } = require("../../helpers/chat");
const messageArray = this.constructPrompt(promptArgs);
return await messageArrayCompressor(this, messageArray, rawHistory);
}
}
module.exports = {
LiteLLM,
};

Some files were not shown because too many files have changed in this diff Show More