Merge branch 'master' into login_by_social_providers
32
README.md
@ -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
|
||||
|
214
cloud-deployments/k8/manifest.yaml
Normal 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
|
@ -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],
|
||||
|
@ -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})`
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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}.` : "") + //
|
||||
|
@ -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;
|
||||
|
159
collector/utils/extensions/WebsiteDepth/index.js
Normal 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;
|
@ -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=
|
||||
|
@ -1,4 +1,8 @@
|
||||
<!doctype html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</head>
|
||||
<html lang="en">
|
||||
|
||||
<body>
|
||||
|
@ -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
|
||||
|
@ -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
@ -9,10 +9,8 @@ lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
lib
|
||||
dist-ssr
|
||||
*.local
|
||||
!frontend/components/lib
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
After Width: | Height: | Size: 9.5 KiB |
@ -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"} />
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
148
frontend/src/components/LLMSelection/LiteLLMOptions/index.jsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
))}
|
||||
|
@ -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>
|
||||
|
@ -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 <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 <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 <10Mb.
|
||||
<br />
|
||||
<br />
|
||||
<i>
|
||||
This model will automatically download on the first use. (1.56GB)
|
||||
</i>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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 && (
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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`}
|
||||
|
@ -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);
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -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 };
|
||||
}
|
||||
|
BIN
frontend/src/media/embeddingprovider/voyageai.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
frontend/src/media/llmprovider/litellm.png
Normal file
After Width: | Height: | Size: 49 KiB |
@ -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 }) {
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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");
|
||||
|
@ -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 />
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 }) {
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 46 KiB |
@ -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 }])
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
After Width: | Height: | Size: 73 KiB |
@ -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({
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 "random" or dynamic your chat
|
||||
responses will be.
|
||||
This setting controls how "creative" 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}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
@ -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}
|
||||
|
@ -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
@ -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
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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({
|
||||
|
@ -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",
|
||||
|
@ -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({
|
||||
|
@ -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 };
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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 [];
|
||||
|
@ -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",
|
||||
|
@ -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'/>`
|
||||
}
|
||||
});
|
@ -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: [{
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 = []) {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
135
server/utils/AiProviders/liteLLM/index.js
Normal 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,
|
||||
};
|