mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-09-17 14:51:32 +00:00
Compare commits
8 Commits
403d13eb50
...
admin-pass
Author | SHA1 | Date | |
---|---|---|---|
|
a401e67d87 | ||
|
d95849e538 | ||
|
ec5e5b3893 | ||
|
639fbd7a15 | ||
|
a88104434d | ||
|
a1e0d368c6 | ||
|
5779701b7d | ||
|
fdfe8d1f41 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -4,9 +4,9 @@ npm-debug.log
|
|||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
|
||||||
# Build output
|
# Build output
|
||||||
.next/
|
/.next/
|
||||||
out/
|
/out/
|
||||||
dist/
|
/dist/
|
||||||
|
|
||||||
# IDE/Editor specific
|
# IDE/Editor specific
|
||||||
.vscode/
|
.vscode/
|
||||||
|
@@ -6,6 +6,7 @@ const config = {
|
|||||||
endOfLine: 'auto',
|
endOfLine: 'auto',
|
||||||
singleQuote: true,
|
singleQuote: true,
|
||||||
tabWidth: 2,
|
tabWidth: 2,
|
||||||
|
semi: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
@@ -1,20 +1,13 @@
|
|||||||
FROM node:20.18.0-alpine
|
FROM node:20.18.0-alpine
|
||||||
|
|
||||||
|
ARG NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001
|
||||||
|
ARG NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api
|
||||||
|
ENV NEXT_PUBLIC_WS_URL=${NEXT_PUBLIC_WS_URL}
|
||||||
|
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||||
|
|
||||||
WORKDIR /home/perplexica
|
WORKDIR /home/perplexica
|
||||||
|
|
||||||
COPY src /home/perplexica/src
|
COPY ui /home/perplexica/
|
||||||
COPY public /home/perplexica/public
|
|
||||||
COPY package.json /home/perplexica/package.json
|
|
||||||
COPY yarn.lock /home/perplexica/yarn.lock
|
|
||||||
COPY tsconfig.json /home/perplexica/tsconfig.json
|
|
||||||
COPY next.config.mjs /home/perplexica/next.config.mjs
|
|
||||||
COPY next-env.d.ts /home/perplexica/next-env.d.ts
|
|
||||||
COPY postcss.config.js /home/perplexica/postcss.config.js
|
|
||||||
COPY drizzle.config.ts /home/perplexica/drizzle.config.ts
|
|
||||||
COPY tailwind.config.ts /home/perplexica/tailwind.config.ts
|
|
||||||
|
|
||||||
RUN mkdir /home/perplexica/data
|
|
||||||
RUN mkdir /home/perplexica/uploads
|
|
||||||
|
|
||||||
RUN yarn install --frozen-lockfile
|
RUN yarn install --frozen-lockfile
|
||||||
RUN yarn build
|
RUN yarn build
|
||||||
|
17
backend.dockerfile
Normal file
17
backend.dockerfile
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
FROM node:18-slim
|
||||||
|
|
||||||
|
WORKDIR /home/perplexica
|
||||||
|
|
||||||
|
COPY src /home/perplexica/src
|
||||||
|
COPY tsconfig.json /home/perplexica/
|
||||||
|
COPY drizzle.config.ts /home/perplexica/
|
||||||
|
COPY package.json /home/perplexica/
|
||||||
|
COPY yarn.lock /home/perplexica/
|
||||||
|
|
||||||
|
RUN mkdir /home/perplexica/data
|
||||||
|
RUN mkdir /home/perplexica/uploads
|
||||||
|
|
||||||
|
RUN yarn install --frozen-lockfile --network-timeout 600000
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
CMD ["yarn", "start"]
|
@@ -9,20 +9,41 @@ services:
|
|||||||
- perplexica-network
|
- perplexica-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
app:
|
perplexica-backend:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: app.dockerfile
|
dockerfile: backend.dockerfile
|
||||||
|
image: itzcrazykns1337/perplexica-backend:main
|
||||||
environment:
|
environment:
|
||||||
- SEARXNG_API_URL=http://searxng:8080
|
- SEARXNG_API_URL=http://searxng:8080
|
||||||
|
depends_on:
|
||||||
|
- searxng
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3001:3001
|
||||||
networks:
|
|
||||||
- perplexica-network
|
|
||||||
volumes:
|
volumes:
|
||||||
- backend-dbstore:/home/perplexica/data
|
- backend-dbstore:/home/perplexica/data
|
||||||
- uploads:/home/perplexica/uploads
|
- uploads:/home/perplexica/uploads
|
||||||
- ./config.toml:/home/perplexica/config.toml
|
- ./config.toml:/home/perplexica/config.toml
|
||||||
|
extra_hosts:
|
||||||
|
- 'host.docker.internal:host-gateway'
|
||||||
|
networks:
|
||||||
|
- perplexica-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
perplexica-frontend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: app.dockerfile
|
||||||
|
args:
|
||||||
|
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api
|
||||||
|
- NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001
|
||||||
|
image: itzcrazykns1337/perplexica-frontend:main
|
||||||
|
depends_on:
|
||||||
|
- perplexica-backend
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
|
networks:
|
||||||
|
- perplexica-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
@@ -2,7 +2,7 @@ import { defineConfig } from 'drizzle-kit';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
dialect: 'sqlite',
|
dialect: 'sqlite',
|
||||||
schema: './src/lib/db/schema.ts',
|
schema: './src/db/schema.ts',
|
||||||
out: './drizzle',
|
out: './drizzle',
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: './data/db.sqlite',
|
url: './data/db.sqlite',
|
||||||
|
5
next-env.d.ts
vendored
5
next-env.d.ts
vendored
@@ -1,5 +0,0 @@
|
|||||||
/// <reference types="next" />
|
|
||||||
/// <reference types="next/image-types/global" />
|
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
91
package.json
91
package.json
@@ -1,62 +1,53 @@
|
|||||||
{
|
{
|
||||||
"name": "perplexica-frontend",
|
"name": "perplexica-backend",
|
||||||
"version": "1.10.0-rc3",
|
"version": "1.10.0-rc3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "ItzCrazyKns",
|
"author": "ItzCrazyKns",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"start": "npm run db:push && node dist/app.js",
|
||||||
"build": "next build",
|
"build": "tsc",
|
||||||
"start": "npm run db:push && next start",
|
"dev": "nodemon --ignore uploads/ src/app.ts ",
|
||||||
"lint": "next lint",
|
"db:push": "drizzle-kit push sqlite",
|
||||||
"format:write": "prettier . --write",
|
"format": "prettier . --check",
|
||||||
"db:push": "drizzle-kit push"
|
"format:write": "prettier . --write"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@headlessui/react": "^2.2.0",
|
|
||||||
"@iarna/toml": "^2.2.5",
|
|
||||||
"@icons-pack/react-simple-icons": "^12.3.0",
|
|
||||||
"@langchain/community": "^0.3.36",
|
|
||||||
"@langchain/core": "^0.3.42",
|
|
||||||
"@langchain/openai": "^0.0.25",
|
|
||||||
"@langchain/textsplitters": "^0.1.0",
|
|
||||||
"@tailwindcss/typography": "^0.5.12",
|
|
||||||
"axios": "^1.8.3",
|
|
||||||
"better-sqlite3": "^11.9.1",
|
|
||||||
"clsx": "^2.1.0",
|
|
||||||
"compute-cosine-similarity": "^1.1.0",
|
|
||||||
"compute-dot": "^1.1.0",
|
|
||||||
"drizzle-orm": "^0.40.1",
|
|
||||||
"html-to-text": "^9.0.5",
|
|
||||||
"langchain": "^0.1.30",
|
|
||||||
"lucide-react": "^0.363.0",
|
|
||||||
"markdown-to-jsx": "^7.7.2",
|
|
||||||
"next": "^15.2.2",
|
|
||||||
"next-themes": "^0.3.0",
|
|
||||||
"pdf-parse": "^1.1.1",
|
|
||||||
"react": "^18",
|
|
||||||
"react-dom": "^18",
|
|
||||||
"react-text-to-speech": "^0.14.5",
|
|
||||||
"react-textarea-autosize": "^8.5.3",
|
|
||||||
"sonner": "^1.4.41",
|
|
||||||
"tailwind-merge": "^2.2.2",
|
|
||||||
"winston": "^3.17.0",
|
|
||||||
"yet-another-react-lightbox": "^3.17.2",
|
|
||||||
"zod": "^3.22.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.12",
|
"@types/better-sqlite3": "^7.6.10",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
"@types/html-to-text": "^9.0.4",
|
"@types/html-to-text": "^9.0.4",
|
||||||
"@types/node": "^20",
|
"@types/multer": "^1.4.12",
|
||||||
"@types/pdf-parse": "^1.1.4",
|
"@types/pdf-parse": "^1.1.4",
|
||||||
"@types/react": "^18",
|
"@types/readable-stream": "^4.0.11",
|
||||||
"@types/react-dom": "^18",
|
"@types/ws": "^8.5.12",
|
||||||
"autoprefixer": "^10.0.1",
|
"drizzle-kit": "^0.22.7",
|
||||||
"drizzle-kit": "^0.30.5",
|
"nodemon": "^3.1.0",
|
||||||
"eslint": "^8",
|
|
||||||
"eslint-config-next": "14.1.4",
|
|
||||||
"postcss": "^8",
|
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"tailwindcss": "^3.3.0",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5"
|
"typescript": "^5.4.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@iarna/toml": "^2.2.5",
|
||||||
|
"@langchain/anthropic": "^0.2.3",
|
||||||
|
"@langchain/community": "^0.2.16",
|
||||||
|
"@langchain/openai": "^0.0.25",
|
||||||
|
"@langchain/google-genai": "^0.0.23",
|
||||||
|
"@xenova/transformers": "^2.17.1",
|
||||||
|
"axios": "^1.6.8",
|
||||||
|
"better-sqlite3": "^11.0.0",
|
||||||
|
"compute-cosine-similarity": "^1.1.0",
|
||||||
|
"compute-dot": "^1.1.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"drizzle-orm": "^0.31.2",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"html-to-text": "^9.0.5",
|
||||||
|
"langchain": "^0.1.30",
|
||||||
|
"mammoth": "^1.8.0",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"pdf-parse": "^1.1.1",
|
||||||
|
"winston": "^3.13.0",
|
||||||
|
"ws": "^8.17.1",
|
||||||
|
"zod": "^3.22.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
[GENERAL]
|
[GENERAL]
|
||||||
PORT = 3001 # Port to run the server on
|
PORT = 3001 # Port to run the server on
|
||||||
SIMILARITY_MEASURE = "cosine" # "cosine" or "dot"
|
SIMILARITY_MEASURE = "cosine" # "cosine" or "dot"
|
||||||
|
CONFIG_PASSWORD = "lorem_ipsum" # Password to access config
|
||||||
|
DISCOVER_ENABLED = true
|
||||||
|
LIBRARY_ENABLED = true
|
||||||
|
COPILOT_ENABLED = true
|
||||||
KEEP_ALIVE = "5m" # How long to keep Ollama models loaded into memory. (Instead of using -1 use "-1m")
|
KEEP_ALIVE = "5m" # How long to keep Ollama models loaded into memory. (Instead of using -1 use "-1m")
|
||||||
|
|
||||||
[MODELS.OPENAI]
|
[MODELS.OPENAI]
|
||||||
@@ -24,4 +28,4 @@ MODEL_NAME = ""
|
|||||||
API_URL = "" # Ollama API URL - http://host.docker.internal:11434
|
API_URL = "" # Ollama API URL - http://host.docker.internal:11434
|
||||||
|
|
||||||
[API_ENDPOINTS]
|
[API_ENDPOINTS]
|
||||||
SEARXNG = "" # SearxNG API URL - http://localhost:32768
|
SEARXNG = "http://localhost:32768" # SearxNG API URL
|
38
src/app.ts
Normal file
38
src/app.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { startWebSocketServer } from './websocket';
|
||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import http from 'http';
|
||||||
|
import routes from './routes';
|
||||||
|
import { getPort } from './config';
|
||||||
|
import logger from './utils/logger';
|
||||||
|
|
||||||
|
const port = getPort();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const server = http.createServer(app);
|
||||||
|
|
||||||
|
const corsOptions = {
|
||||||
|
origin: '*',
|
||||||
|
};
|
||||||
|
|
||||||
|
app.use(cors(corsOptions));
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
app.use('/api', routes);
|
||||||
|
app.get('/api', (_, res) => {
|
||||||
|
res.status(200).json({ status: 'ok' });
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(port, () => {
|
||||||
|
logger.info(`Server is running on port ${port}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
startWebSocketServer(server);
|
||||||
|
|
||||||
|
process.on('uncaughtException', (err, origin) => {
|
||||||
|
logger.error(`Uncaught Exception at ${origin}: ${err}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
logger.error(`Unhandled Rejection at: ${promise}, reason: ${reason}`);
|
||||||
|
});
|
@@ -1,360 +0,0 @@
|
|||||||
import prompts from '@/lib/prompts';
|
|
||||||
import MetaSearchAgent from '@/lib/search/metaSearchAgent';
|
|
||||||
import crypto from 'crypto';
|
|
||||||
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
|
|
||||||
import { EventEmitter } from 'stream';
|
|
||||||
import {
|
|
||||||
chatModelProviders,
|
|
||||||
embeddingModelProviders,
|
|
||||||
getAvailableChatModelProviders,
|
|
||||||
getAvailableEmbeddingModelProviders,
|
|
||||||
} from '@/lib/providers';
|
|
||||||
import db from '@/lib/db';
|
|
||||||
import { chats, messages as messagesSchema } from '@/lib/db/schema';
|
|
||||||
import { and, eq, gt } from 'drizzle-orm';
|
|
||||||
import { getFileDetails } from '@/lib/utils/files';
|
|
||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { ChatOpenAI } from '@langchain/openai';
|
|
||||||
import {
|
|
||||||
getCustomOpenaiApiKey,
|
|
||||||
getCustomOpenaiApiUrl,
|
|
||||||
getCustomOpenaiModelName,
|
|
||||||
} from '@/lib/config';
|
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
|
||||||
export const dynamic = 'force-dynamic';
|
|
||||||
|
|
||||||
const searchHandlers: Record<string, MetaSearchAgent> = {
|
|
||||||
webSearch: new MetaSearchAgent({
|
|
||||||
activeEngines: [],
|
|
||||||
queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
|
|
||||||
responsePrompt: prompts.webSearchResponsePrompt,
|
|
||||||
rerank: true,
|
|
||||||
rerankThreshold: 0.3,
|
|
||||||
searchWeb: true,
|
|
||||||
summarizer: true,
|
|
||||||
}),
|
|
||||||
academicSearch: new MetaSearchAgent({
|
|
||||||
activeEngines: ['arxiv', 'google scholar', 'pubmed'],
|
|
||||||
queryGeneratorPrompt: prompts.academicSearchRetrieverPrompt,
|
|
||||||
responsePrompt: prompts.academicSearchResponsePrompt,
|
|
||||||
rerank: true,
|
|
||||||
rerankThreshold: 0,
|
|
||||||
searchWeb: true,
|
|
||||||
summarizer: false,
|
|
||||||
}),
|
|
||||||
writingAssistant: new MetaSearchAgent({
|
|
||||||
activeEngines: [],
|
|
||||||
queryGeneratorPrompt: '',
|
|
||||||
responsePrompt: prompts.writingAssistantPrompt,
|
|
||||||
rerank: true,
|
|
||||||
rerankThreshold: 0,
|
|
||||||
searchWeb: false,
|
|
||||||
summarizer: false,
|
|
||||||
}),
|
|
||||||
wolframAlphaSearch: new MetaSearchAgent({
|
|
||||||
activeEngines: ['wolframalpha'],
|
|
||||||
queryGeneratorPrompt: prompts.wolframAlphaSearchRetrieverPrompt,
|
|
||||||
responsePrompt: prompts.wolframAlphaSearchResponsePrompt,
|
|
||||||
rerank: false,
|
|
||||||
rerankThreshold: 0,
|
|
||||||
searchWeb: true,
|
|
||||||
summarizer: false,
|
|
||||||
}),
|
|
||||||
youtubeSearch: new MetaSearchAgent({
|
|
||||||
activeEngines: ['youtube'],
|
|
||||||
queryGeneratorPrompt: prompts.youtubeSearchRetrieverPrompt,
|
|
||||||
responsePrompt: prompts.youtubeSearchResponsePrompt,
|
|
||||||
rerank: true,
|
|
||||||
rerankThreshold: 0.3,
|
|
||||||
searchWeb: true,
|
|
||||||
summarizer: false,
|
|
||||||
}),
|
|
||||||
redditSearch: new MetaSearchAgent({
|
|
||||||
activeEngines: ['reddit'],
|
|
||||||
queryGeneratorPrompt: prompts.redditSearchRetrieverPrompt,
|
|
||||||
responsePrompt: prompts.redditSearchResponsePrompt,
|
|
||||||
rerank: true,
|
|
||||||
rerankThreshold: 0.3,
|
|
||||||
searchWeb: true,
|
|
||||||
summarizer: false,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
type Message = {
|
|
||||||
messageId: string;
|
|
||||||
chatId: string;
|
|
||||||
content: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ChatModel = {
|
|
||||||
provider: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type EmbeddingModel = {
|
|
||||||
provider: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Body = {
|
|
||||||
message: Message;
|
|
||||||
optimizationMode: 'speed' | 'balanced' | 'quality';
|
|
||||||
focusMode: string;
|
|
||||||
history: Array<[string, string]>;
|
|
||||||
files: Array<string>;
|
|
||||||
chatModel: ChatModel;
|
|
||||||
embeddingModel: EmbeddingModel;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEmitterEvents = async (
|
|
||||||
stream: EventEmitter,
|
|
||||||
writer: WritableStreamDefaultWriter,
|
|
||||||
encoder: TextEncoder,
|
|
||||||
aiMessageId: string,
|
|
||||||
chatId: string,
|
|
||||||
) => {
|
|
||||||
let recievedMessage = '';
|
|
||||||
let sources: any[] = [];
|
|
||||||
|
|
||||||
stream.on('data', (data) => {
|
|
||||||
const parsedData = JSON.parse(data);
|
|
||||||
if (parsedData.type === 'response') {
|
|
||||||
writer.write(
|
|
||||||
encoder.encode(
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'message',
|
|
||||||
data: parsedData.data,
|
|
||||||
messageId: aiMessageId,
|
|
||||||
}) + '\n',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
recievedMessage += parsedData.data;
|
|
||||||
} else if (parsedData.type === 'sources') {
|
|
||||||
writer.write(
|
|
||||||
encoder.encode(
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'sources',
|
|
||||||
data: parsedData.data,
|
|
||||||
messageId: aiMessageId,
|
|
||||||
}) + '\n',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
sources = parsedData.data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
stream.on('end', () => {
|
|
||||||
writer.write(
|
|
||||||
encoder.encode(
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'messageEnd',
|
|
||||||
messageId: aiMessageId,
|
|
||||||
}) + '\n',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
writer.close();
|
|
||||||
|
|
||||||
db.insert(messagesSchema)
|
|
||||||
.values({
|
|
||||||
content: recievedMessage,
|
|
||||||
chatId: chatId,
|
|
||||||
messageId: aiMessageId,
|
|
||||||
role: 'assistant',
|
|
||||||
metadata: JSON.stringify({
|
|
||||||
createdAt: new Date(),
|
|
||||||
...(sources && sources.length > 0 && { sources }),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
});
|
|
||||||
stream.on('error', (data) => {
|
|
||||||
const parsedData = JSON.parse(data);
|
|
||||||
writer.write(
|
|
||||||
encoder.encode(
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'error',
|
|
||||||
data: parsedData.data,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
writer.close();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHistorySave = async (
|
|
||||||
message: Message,
|
|
||||||
humanMessageId: string,
|
|
||||||
focusMode: string,
|
|
||||||
files: string[],
|
|
||||||
) => {
|
|
||||||
const chat = await db.query.chats.findFirst({
|
|
||||||
where: eq(chats.id, message.chatId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!chat) {
|
|
||||||
await db
|
|
||||||
.insert(chats)
|
|
||||||
.values({
|
|
||||||
id: message.chatId,
|
|
||||||
title: message.content,
|
|
||||||
createdAt: new Date().toString(),
|
|
||||||
focusMode: focusMode,
|
|
||||||
files: files.map(getFileDetails),
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageExists = await db.query.messages.findFirst({
|
|
||||||
where: eq(messagesSchema.messageId, humanMessageId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!messageExists) {
|
|
||||||
await db
|
|
||||||
.insert(messagesSchema)
|
|
||||||
.values({
|
|
||||||
content: message.content,
|
|
||||||
chatId: message.chatId,
|
|
||||||
messageId: humanMessageId,
|
|
||||||
role: 'user',
|
|
||||||
metadata: JSON.stringify({
|
|
||||||
createdAt: new Date(),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
} else {
|
|
||||||
await db
|
|
||||||
.delete(messagesSchema)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
gt(messagesSchema.id, messageExists.id),
|
|
||||||
eq(messagesSchema.chatId, message.chatId),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const POST = async (req: Request) => {
|
|
||||||
try {
|
|
||||||
const body = (await req.json()) as Body;
|
|
||||||
const { message } = body;
|
|
||||||
|
|
||||||
if (message.content === '') {
|
|
||||||
return Response.json(
|
|
||||||
{
|
|
||||||
message: 'Please provide a message to process',
|
|
||||||
},
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [chatModelProviders, embeddingModelProviders] = await Promise.all([
|
|
||||||
getAvailableChatModelProviders(),
|
|
||||||
getAvailableEmbeddingModelProviders(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const chatModelProvider =
|
|
||||||
chatModelProviders[
|
|
||||||
body.chatModel?.provider || Object.keys(chatModelProviders)[0]
|
|
||||||
];
|
|
||||||
const chatModel =
|
|
||||||
chatModelProvider[
|
|
||||||
body.chatModel?.name || Object.keys(chatModelProvider)[0]
|
|
||||||
];
|
|
||||||
|
|
||||||
const embeddingProvider =
|
|
||||||
embeddingModelProviders[
|
|
||||||
body.embeddingModel?.provider || Object.keys(embeddingModelProviders)[0]
|
|
||||||
];
|
|
||||||
const embeddingModel =
|
|
||||||
embeddingProvider[
|
|
||||||
body.embeddingModel?.name || Object.keys(embeddingProvider)[0]
|
|
||||||
];
|
|
||||||
|
|
||||||
let llm: BaseChatModel | undefined;
|
|
||||||
let embedding = embeddingModel.model;
|
|
||||||
|
|
||||||
if (body.chatModel?.provider === 'custom_openai') {
|
|
||||||
llm = new ChatOpenAI({
|
|
||||||
openAIApiKey: getCustomOpenaiApiKey(),
|
|
||||||
modelName: getCustomOpenaiModelName(),
|
|
||||||
temperature: 0.7,
|
|
||||||
configuration: {
|
|
||||||
baseURL: getCustomOpenaiApiUrl(),
|
|
||||||
},
|
|
||||||
}) as unknown as BaseChatModel;
|
|
||||||
} else if (chatModelProvider && chatModel) {
|
|
||||||
llm = chatModel.model;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!llm) {
|
|
||||||
return Response.json({ error: 'Invalid chat model' }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!embedding) {
|
|
||||||
return Response.json(
|
|
||||||
{ error: 'Invalid embedding model' },
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const humanMessageId =
|
|
||||||
message.messageId ?? crypto.randomBytes(7).toString('hex');
|
|
||||||
const aiMessageId = crypto.randomBytes(7).toString('hex');
|
|
||||||
|
|
||||||
const history: BaseMessage[] = body.history.map((msg) => {
|
|
||||||
if (msg[0] === 'human') {
|
|
||||||
return new HumanMessage({
|
|
||||||
content: msg[1],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return new AIMessage({
|
|
||||||
content: msg[1],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handler = searchHandlers[body.focusMode];
|
|
||||||
|
|
||||||
if (!handler) {
|
|
||||||
return Response.json(
|
|
||||||
{
|
|
||||||
message: 'Invalid focus mode',
|
|
||||||
},
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const stream = await handler.searchAndAnswer(
|
|
||||||
message.content,
|
|
||||||
history,
|
|
||||||
llm,
|
|
||||||
embedding,
|
|
||||||
body.optimizationMode,
|
|
||||||
body.files,
|
|
||||||
);
|
|
||||||
|
|
||||||
const responseStream = new TransformStream();
|
|
||||||
const writer = responseStream.writable.getWriter();
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
|
|
||||||
handleEmitterEvents(stream, writer, encoder, aiMessageId, message.chatId);
|
|
||||||
handleHistorySave(message, humanMessageId, body.focusMode, body.files);
|
|
||||||
|
|
||||||
return new Response(responseStream.readable, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'text/event-stream',
|
|
||||||
Connection: 'keep-alive',
|
|
||||||
'Cache-Control': 'no-cache, no-transform',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('An error ocurred while processing chat request:', err);
|
|
||||||
return Response.json(
|
|
||||||
{ message: 'An error ocurred while processing chat request' },
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,69 +0,0 @@
|
|||||||
import db from '@/lib/db';
|
|
||||||
import { chats, messages } from '@/lib/db/schema';
|
|
||||||
import { eq } from 'drizzle-orm';
|
|
||||||
|
|
||||||
export const GET = async (
|
|
||||||
req: Request,
|
|
||||||
{ params }: { params: Promise<{ id: string }> },
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const { id } = await params;
|
|
||||||
|
|
||||||
const chatExists = await db.query.chats.findFirst({
|
|
||||||
where: eq(chats.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!chatExists) {
|
|
||||||
return Response.json({ message: 'Chat not found' }, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatMessages = await db.query.messages.findMany({
|
|
||||||
where: eq(messages.chatId, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
return Response.json(
|
|
||||||
{
|
|
||||||
chat: chatExists,
|
|
||||||
messages: chatMessages,
|
|
||||||
},
|
|
||||||
{ status: 200 },
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error in getting chat by id: ', err);
|
|
||||||
return Response.json(
|
|
||||||
{ message: 'An error has occurred.' },
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DELETE = async (
|
|
||||||
req: Request,
|
|
||||||
{ params }: { params: Promise<{ id: string }> },
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const { id } = await params;
|
|
||||||
|
|
||||||
const chatExists = await db.query.chats.findFirst({
|
|
||||||
where: eq(chats.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!chatExists) {
|
|
||||||
return Response.json({ message: 'Chat not found' }, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.delete(chats).where(eq(chats.id, id)).execute();
|
|
||||||
await db.delete(messages).where(eq(messages.chatId, id)).execute();
|
|
||||||
|
|
||||||
return Response.json(
|
|
||||||
{ message: 'Chat deleted successfully' },
|
|
||||||
{ status: 200 },
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error in deleting chat by id: ', err);
|
|
||||||
return Response.json(
|
|
||||||
{ message: 'An error has occurred.' },
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,15 +0,0 @@
|
|||||||
import db from '@/lib/db';
|
|
||||||
|
|
||||||
export const GET = async (req: Request) => {
|
|
||||||
try {
|
|
||||||
let chats = await db.query.chats.findMany();
|
|
||||||
chats = chats.reverse();
|
|
||||||
return Response.json({ chats: chats }, { status: 200 });
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error in getting chats: ', err);
|
|
||||||
return Response.json(
|
|
||||||
{ message: 'An error has occurred.' },
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,109 +0,0 @@
|
|||||||
import {
|
|
||||||
getAnthropicApiKey,
|
|
||||||
getCustomOpenaiApiKey,
|
|
||||||
getCustomOpenaiApiUrl,
|
|
||||||
getCustomOpenaiModelName,
|
|
||||||
getGeminiApiKey,
|
|
||||||
getGroqApiKey,
|
|
||||||
getOllamaApiEndpoint,
|
|
||||||
getOpenaiApiKey,
|
|
||||||
updateConfig,
|
|
||||||
} from '@/lib/config';
|
|
||||||
import {
|
|
||||||
getAvailableChatModelProviders,
|
|
||||||
getAvailableEmbeddingModelProviders,
|
|
||||||
} from '@/lib/providers';
|
|
||||||
|
|
||||||
export const GET = async (req: Request) => {
|
|
||||||
try {
|
|
||||||
const config: Record<string, any> = {};
|
|
||||||
|
|
||||||
const [chatModelProviders, embeddingModelProviders] = await Promise.all([
|
|
||||||
getAvailableChatModelProviders(),
|
|
||||||
getAvailableEmbeddingModelProviders(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
config['chatModelProviders'] = {};
|
|
||||||
config['embeddingModelProviders'] = {};
|
|
||||||
|
|
||||||
for (const provider in chatModelProviders) {
|
|
||||||
config['chatModelProviders'][provider] = Object.keys(
|
|
||||||
chatModelProviders[provider],
|
|
||||||
).map((model) => {
|
|
||||||
return {
|
|
||||||
name: model,
|
|
||||||
displayName: chatModelProviders[provider][model].displayName,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const provider in embeddingModelProviders) {
|
|
||||||
config['embeddingModelProviders'][provider] = Object.keys(
|
|
||||||
embeddingModelProviders[provider],
|
|
||||||
).map((model) => {
|
|
||||||
return {
|
|
||||||
name: model,
|
|
||||||
displayName: embeddingModelProviders[provider][model].displayName,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
config['openaiApiKey'] = getOpenaiApiKey();
|
|
||||||
config['ollamaApiUrl'] = getOllamaApiEndpoint();
|
|
||||||
config['anthropicApiKey'] = getAnthropicApiKey();
|
|
||||||
config['groqApiKey'] = getGroqApiKey();
|
|
||||||
config['geminiApiKey'] = getGeminiApiKey();
|
|
||||||
config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl();
|
|
||||||
config['customOpenaiApiKey'] = getCustomOpenaiApiKey();
|
|
||||||
config['customOpenaiModelName'] = getCustomOpenaiModelName();
|
|
||||||
|
|
||||||
return Response.json({ ...config }, { status: 200 });
|
|
||||||
} catch (err) {
|
|
||||||
console.error('An error ocurred while getting config:', err);
|
|
||||||
return Response.json(
|
|
||||||
{ message: 'An error ocurred while getting config' },
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const POST = async (req: Request) => {
|
|
||||||
try {
|
|
||||||
const config = await req.json();
|
|
||||||
|
|
||||||
const updatedConfig = {
|
|
||||||
MODELS: {
|
|
||||||
OPENAI: {
|
|
||||||
API_KEY: config.openaiApiKey,
|
|
||||||
},
|
|
||||||
GROQ: {
|
|
||||||
API_KEY: config.groqApiKey,
|
|
||||||
},
|
|
||||||
ANTHROPIC: {
|
|
||||||
API_KEY: config.anthropicApiKey,
|
|
||||||
},
|
|
||||||
GEMINI: {
|
|
||||||
API_KEY: config.geminiApiKey,
|
|
||||||
},
|
|
||||||
OLLAMA: {
|
|
||||||
API_URL: config.ollamaApiUrl,
|
|
||||||
},
|
|
||||||
CUSTOM_OPENAI: {
|
|
||||||
API_URL: config.customOpenaiApiUrl,
|
|
||||||
API_KEY: config.customOpenaiApiKey,
|
|
||||||
MODEL_NAME: config.customOpenaiModelName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
updateConfig(updatedConfig);
|
|
||||||
|
|
||||||
return Response.json({ message: 'Config updated' }, { status: 200 });
|
|
||||||
} catch (err) {
|
|
||||||
console.error('An error ocurred while updating config:', err);
|
|
||||||
return Response.json(
|
|
||||||
{ message: 'An error ocurred while updating config' },
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,61 +0,0 @@
|
|||||||
import { searchSearxng } from '@/lib/searxng';
|
|
||||||
|
|
||||||
const articleWebsites = [
|
|
||||||
'yahoo.com',
|
|
||||||
'www.exchangewire.com',
|
|
||||||
'businessinsider.com',
|
|
||||||
/* 'wired.com',
|
|
||||||
'mashable.com',
|
|
||||||
'theverge.com',
|
|
||||||
'gizmodo.com',
|
|
||||||
'cnet.com',
|
|
||||||
'venturebeat.com', */
|
|
||||||
];
|
|
||||||
|
|
||||||
const topics = ['AI', 'tech']; /* TODO: Add UI to customize this */
|
|
||||||
|
|
||||||
export const GET = async (req: Request) => {
|
|
||||||
try {
|
|
||||||
const data = (
|
|
||||||
await Promise.all([
|
|
||||||
...new Array(articleWebsites.length * topics.length)
|
|
||||||
.fill(0)
|
|
||||||
.map(async (_, i) => {
|
|
||||||
return (
|
|
||||||
await searchSearxng(
|
|
||||||
`site:${articleWebsites[i % articleWebsites.length]} ${
|
|
||||||
topics[i % topics.length]
|
|
||||||
}`,
|
|
||||||
{
|
|
||||||
engines: ['bing news'],
|
|
||||||
pageno: 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
).results;
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
.map((result) => result)
|
|
||||||
.flat()
|
|
||||||
.sort(() => Math.random() - 0.5);
|
|
||||||
|
|
||||||
return Response.json(
|
|
||||||
{
|
|
||||||
blogs: data,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`An error ocurred in discover route: ${err}`);
|
|
||||||
return Response.json(
|
|
||||||
{
|
|
||||||
message: 'An error has occurred',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 500,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,83 +0,0 @@
|
|||||||
import handleImageSearch from '@/lib/chains/imageSearchAgent';
|
|
||||||
import {
|
|
||||||
getCustomOpenaiApiKey,
|
|
||||||
getCustomOpenaiApiUrl,
|
|
||||||
getCustomOpenaiModelName,
|
|
||||||
} from '@/lib/config';
|
|
||||||
import { getAvailableChatModelProviders } from '@/lib/providers';
|
|
||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
|
|
||||||
import { ChatOpenAI } from '@langchain/openai';
|
|
||||||
|
|
||||||
interface ChatModel {
|
|
||||||
provider: string;
|
|
||||||
model: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ImageSearchBody {
|
|
||||||
query: string;
|
|
||||||
chatHistory: any[];
|
|
||||||
chatModel?: ChatModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const POST = async (req: Request) => {
|
|
||||||
try {
|
|
||||||
const body: ImageSearchBody = await req.json();
|
|
||||||
|
|
||||||
const chatHistory = body.chatHistory
|
|
||||||
.map((msg: any) => {
|
|
||||||
if (msg.role === 'user') {
|
|
||||||
return new HumanMessage(msg.content);
|
|
||||||
} else if (msg.role === 'assistant') {
|
|
||||||
return new AIMessage(msg.content);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((msg) => msg !== undefined) as BaseMessage[];
|
|
||||||
|
|
||||||
const chatModelProviders = await getAvailableChatModelProviders();
|
|
||||||
|
|
||||||
const chatModelProvider =
|
|
||||||
chatModelProviders[
|
|
||||||
body.chatModel?.provider || Object.keys(chatModelProviders)[0]
|
|
||||||
];
|
|
||||||
const chatModel =
|
|
||||||
chatModelProvider[
|
|
||||||
body.chatModel?.model || Object.keys(chatModelProvider)[0]
|
|
||||||
];
|
|
||||||
|
|
||||||
let llm: BaseChatModel | undefined;
|
|
||||||
|
|
||||||
if (body.chatModel?.provider === 'custom_openai') {
|
|
||||||
llm = new ChatOpenAI({
|
|
||||||
openAIApiKey: getCustomOpenaiApiKey(),
|
|
||||||
modelName: getCustomOpenaiModelName(),
|
|
||||||
temperature: 0.7,
|
|
||||||
configuration: {
|
|
||||||
baseURL: getCustomOpenaiApiUrl(),
|
|
||||||
},
|
|
||||||
}) as unknown as BaseChatModel;
|
|
||||||
} else if (chatModelProvider && chatModel) {
|
|
||||||
llm = chatModel.model;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!llm) {
|
|
||||||
return Response.json({ error: 'Invalid chat model' }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const images = await handleImageSearch(
|
|
||||||
{
|
|
||||||
chat_history: chatHistory,
|
|
||||||
query: body.query,
|
|
||||||
},
|
|
||||||
llm,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Response.json({ images }, { status: 200 });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`An error ocurred while searching images: ${err}`);
|
|
||||||
return Response.json(
|
|
||||||
{ message: 'An error ocurred while searching images' },
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,47 +0,0 @@
|
|||||||
import {
|
|
||||||
getAvailableChatModelProviders,
|
|
||||||
getAvailableEmbeddingModelProviders,
|
|
||||||
} from '@/lib/providers';
|
|
||||||
|
|
||||||
export const GET = async (req: Request) => {
|
|
||||||
try {
|
|
||||||
const [chatModelProviders, embeddingModelProviders] = await Promise.all([
|
|
||||||
getAvailableChatModelProviders(),
|
|
||||||
getAvailableEmbeddingModelProviders(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
Object.keys(chatModelProviders).forEach((provider) => {
|
|
||||||
Object.keys(chatModelProviders[provider]).forEach((model) => {
|
|
||||||
delete (chatModelProviders[provider][model] as { model?: unknown })
|
|
||||||
.model;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.keys(embeddingModelProviders).forEach((provider) => {
|
|
||||||
Object.keys(embeddingModelProviders[provider]).forEach((model) => {
|
|
||||||
delete (embeddingModelProviders[provider][model] as { model?: unknown })
|
|
||||||
.model;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return Response.json(
|
|
||||||
{
|
|
||||||
chatModelProviders,
|
|
||||||
embeddingModelProviders,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('An error ocurred while fetching models', err);
|
|
||||||
return Response.json(
|
|
||||||
{
|
|
||||||
message: 'An error has occurred.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 500,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,81 +0,0 @@
|
|||||||
import generateSuggestions from '@/lib/chains/suggestionGeneratorAgent';
|
|
||||||
import {
|
|
||||||
getCustomOpenaiApiKey,
|
|
||||||
getCustomOpenaiApiUrl,
|
|
||||||
getCustomOpenaiModelName,
|
|
||||||
} from '@/lib/config';
|
|
||||||
import { getAvailableChatModelProviders } from '@/lib/providers';
|
|
||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
|
|
||||||
import { ChatOpenAI } from '@langchain/openai';
|
|
||||||
|
|
||||||
interface ChatModel {
|
|
||||||
provider: string;
|
|
||||||
model: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SuggestionsGenerationBody {
|
|
||||||
chatHistory: any[];
|
|
||||||
chatModel?: ChatModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const POST = async (req: Request) => {
|
|
||||||
try {
|
|
||||||
const body: SuggestionsGenerationBody = await req.json();
|
|
||||||
|
|
||||||
const chatHistory = body.chatHistory
|
|
||||||
.map((msg: any) => {
|
|
||||||
if (msg.role === 'user') {
|
|
||||||
return new HumanMessage(msg.content);
|
|
||||||
} else if (msg.role === 'assistant') {
|
|
||||||
return new AIMessage(msg.content);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((msg) => msg !== undefined) as BaseMessage[];
|
|
||||||
|
|
||||||
const chatModelProviders = await getAvailableChatModelProviders();
|
|
||||||
|
|
||||||
const chatModelProvider =
|
|
||||||
chatModelProviders[
|
|
||||||
body.chatModel?.provider || Object.keys(chatModelProviders)[0]
|
|
||||||
];
|
|
||||||
const chatModel =
|
|
||||||
chatModelProvider[
|
|
||||||
body.chatModel?.model || Object.keys(chatModelProvider)[0]
|
|
||||||
];
|
|
||||||
|
|
||||||
let llm: BaseChatModel | undefined;
|
|
||||||
|
|
||||||
if (body.chatModel?.provider === 'custom_openai') {
|
|
||||||
llm = new ChatOpenAI({
|
|
||||||
openAIApiKey: getCustomOpenaiApiKey(),
|
|
||||||
modelName: getCustomOpenaiModelName(),
|
|
||||||
temperature: 0.7,
|
|
||||||
configuration: {
|
|
||||||
baseURL: getCustomOpenaiApiUrl(),
|
|
||||||
},
|
|
||||||
}) as unknown as BaseChatModel;
|
|
||||||
} else if (chatModelProvider && chatModel) {
|
|
||||||
llm = chatModel.model;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!llm) {
|
|
||||||
return Response.json({ error: 'Invalid chat model' }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const suggestions = await generateSuggestions(
|
|
||||||
{
|
|
||||||
chat_history: chatHistory,
|
|
||||||
},
|
|
||||||
llm,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Response.json({ suggestions }, { status: 200 });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`An error ocurred while generating suggestions: ${err}`);
|
|
||||||
return Response.json(
|
|
||||||
{ message: 'An error ocurred while generating suggestions' },
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,134 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import crypto from 'crypto';
|
|
||||||
import { getAvailableEmbeddingModelProviders } from '@/lib/providers';
|
|
||||||
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf';
|
|
||||||
import { DocxLoader } from '@langchain/community/document_loaders/fs/docx';
|
|
||||||
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
|
|
||||||
import { Document } from 'langchain/document';
|
|
||||||
|
|
||||||
interface FileRes {
|
|
||||||
fileName: string;
|
|
||||||
fileExtension: string;
|
|
||||||
fileId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadDir = path.join(process.cwd(), 'uploads');
|
|
||||||
|
|
||||||
if (!fs.existsSync(uploadDir)) {
|
|
||||||
fs.mkdirSync(uploadDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitter = new RecursiveCharacterTextSplitter({
|
|
||||||
chunkSize: 500,
|
|
||||||
chunkOverlap: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
|
||||||
try {
|
|
||||||
const formData = await req.formData();
|
|
||||||
|
|
||||||
const files = formData.getAll('files') as File[];
|
|
||||||
const embedding_model = formData.get('embedding_model');
|
|
||||||
const embedding_model_provider = formData.get('embedding_model_provider');
|
|
||||||
|
|
||||||
if (!embedding_model || !embedding_model_provider) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ message: 'Missing embedding model or provider' },
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const embeddingModels = await getAvailableEmbeddingModelProviders();
|
|
||||||
const provider =
|
|
||||||
embedding_model_provider ?? Object.keys(embeddingModels)[0];
|
|
||||||
const embeddingModel =
|
|
||||||
embedding_model ?? Object.keys(embeddingModels[provider as string])[0];
|
|
||||||
|
|
||||||
let embeddingsModel =
|
|
||||||
embeddingModels[provider as string]?.[embeddingModel as string]?.model;
|
|
||||||
if (!embeddingsModel) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ message: 'Invalid embedding model selected' },
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const processedFiles: FileRes[] = [];
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
files.map(async (file: any) => {
|
|
||||||
const fileExtension = file.name.split('.').pop();
|
|
||||||
if (!['pdf', 'docx', 'txt'].includes(fileExtension!)) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ message: 'File type not supported' },
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueFileName = `${crypto.randomBytes(16).toString('hex')}.${fileExtension}`;
|
|
||||||
const filePath = path.join(uploadDir, uniqueFileName);
|
|
||||||
|
|
||||||
const buffer = Buffer.from(await file.arrayBuffer());
|
|
||||||
fs.writeFileSync(filePath, new Uint8Array(buffer));
|
|
||||||
|
|
||||||
let docs: any[] = [];
|
|
||||||
if (fileExtension === 'pdf') {
|
|
||||||
const loader = new PDFLoader(filePath);
|
|
||||||
docs = await loader.load();
|
|
||||||
} else if (fileExtension === 'docx') {
|
|
||||||
const loader = new DocxLoader(filePath);
|
|
||||||
docs = await loader.load();
|
|
||||||
} else if (fileExtension === 'txt') {
|
|
||||||
const text = fs.readFileSync(filePath, 'utf-8');
|
|
||||||
docs = [
|
|
||||||
new Document({ pageContent: text, metadata: { title: file.name } }),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitted = await splitter.splitDocuments(docs);
|
|
||||||
|
|
||||||
const extractedDataPath = filePath.replace(/\.\w+$/, '-extracted.json');
|
|
||||||
fs.writeFileSync(
|
|
||||||
extractedDataPath,
|
|
||||||
JSON.stringify({
|
|
||||||
title: file.name,
|
|
||||||
contents: splitted.map((doc) => doc.pageContent),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const embeddings = await embeddingsModel.embedDocuments(
|
|
||||||
splitted.map((doc) => doc.pageContent),
|
|
||||||
);
|
|
||||||
const embeddingsDataPath = filePath.replace(
|
|
||||||
/\.\w+$/,
|
|
||||||
'-embeddings.json',
|
|
||||||
);
|
|
||||||
fs.writeFileSync(
|
|
||||||
embeddingsDataPath,
|
|
||||||
JSON.stringify({
|
|
||||||
title: file.name,
|
|
||||||
embeddings,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
processedFiles.push({
|
|
||||||
fileName: file.name,
|
|
||||||
fileExtension: fileExtension,
|
|
||||||
fileId: uniqueFileName.replace(/\.\w+$/, ''),
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
files: processedFiles,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error uploading file:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ message: 'An error has occurred.' },
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,83 +0,0 @@
|
|||||||
import handleVideoSearch from '@/lib/chains/videoSearchAgent';
|
|
||||||
import {
|
|
||||||
getCustomOpenaiApiKey,
|
|
||||||
getCustomOpenaiApiUrl,
|
|
||||||
getCustomOpenaiModelName,
|
|
||||||
} from '@/lib/config';
|
|
||||||
import { getAvailableChatModelProviders } from '@/lib/providers';
|
|
||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
|
|
||||||
import { ChatOpenAI } from '@langchain/openai';
|
|
||||||
|
|
||||||
interface ChatModel {
|
|
||||||
provider: string;
|
|
||||||
model: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VideoSearchBody {
|
|
||||||
query: string;
|
|
||||||
chatHistory: any[];
|
|
||||||
chatModel?: ChatModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const POST = async (req: Request) => {
|
|
||||||
try {
|
|
||||||
const body: VideoSearchBody = await req.json();
|
|
||||||
|
|
||||||
const chatHistory = body.chatHistory
|
|
||||||
.map((msg: any) => {
|
|
||||||
if (msg.role === 'user') {
|
|
||||||
return new HumanMessage(msg.content);
|
|
||||||
} else if (msg.role === 'assistant') {
|
|
||||||
return new AIMessage(msg.content);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((msg) => msg !== undefined) as BaseMessage[];
|
|
||||||
|
|
||||||
const chatModelProviders = await getAvailableChatModelProviders();
|
|
||||||
|
|
||||||
const chatModelProvider =
|
|
||||||
chatModelProviders[
|
|
||||||
body.chatModel?.provider || Object.keys(chatModelProviders)[0]
|
|
||||||
];
|
|
||||||
const chatModel =
|
|
||||||
chatModelProvider[
|
|
||||||
body.chatModel?.model || Object.keys(chatModelProvider)[0]
|
|
||||||
];
|
|
||||||
|
|
||||||
let llm: BaseChatModel | undefined;
|
|
||||||
|
|
||||||
if (body.chatModel?.provider === 'custom_openai') {
|
|
||||||
llm = new ChatOpenAI({
|
|
||||||
openAIApiKey: getCustomOpenaiApiKey(),
|
|
||||||
modelName: getCustomOpenaiModelName(),
|
|
||||||
temperature: 0.7,
|
|
||||||
configuration: {
|
|
||||||
baseURL: getCustomOpenaiApiUrl(),
|
|
||||||
},
|
|
||||||
}) as unknown as BaseChatModel;
|
|
||||||
} else if (chatModelProvider && chatModel) {
|
|
||||||
llm = chatModel.model;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!llm) {
|
|
||||||
return Response.json({ error: 'Invalid chat model' }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const videos = await handleVideoSearch(
|
|
||||||
{
|
|
||||||
chat_history: chatHistory,
|
|
||||||
query: body.query,
|
|
||||||
},
|
|
||||||
llm,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Response.json({ videos }, { status: 200 });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`An error ocurred while searching videos: ${err}`);
|
|
||||||
return Response.json(
|
|
||||||
{ message: 'An error ocurred while searching videos' },
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,9 +0,0 @@
|
|||||||
import ChatWindow from '@/components/ChatWindow';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
const Page = ({ params }: { params: Promise<{ chatId: string }> }) => {
|
|
||||||
const { chatId } = React.use(params);
|
|
||||||
return <ChatWindow id={chatId} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Page;
|
|
@@ -1,12 +0,0 @@
|
|||||||
import { Metadata } from 'next';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Library - Perplexica',
|
|
||||||
};
|
|
||||||
|
|
||||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
|
||||||
return <div>{children}</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Layout;
|
|
@@ -7,7 +7,7 @@ import { PromptTemplate } from '@langchain/core/prompts';
|
|||||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
import formatChatHistoryAsString from '../utils/formatHistory';
|
||||||
import { BaseMessage } from '@langchain/core/messages';
|
import { BaseMessage } from '@langchain/core/messages';
|
||||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
import { StringOutputParser } from '@langchain/core/output_parsers';
|
||||||
import { searchSearxng } from '../searxng';
|
import { searchSearxng } from '../lib/searxng';
|
||||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
|
||||||
const imageSearchChainPrompt = `
|
const imageSearchChainPrompt = `
|
||||||
@@ -36,12 +36,6 @@ type ImageSearchChainInput = {
|
|||||||
query: string;
|
query: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ImageSearchResult {
|
|
||||||
img_src: string;
|
|
||||||
url: string;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const strParser = new StringOutputParser();
|
const strParser = new StringOutputParser();
|
||||||
|
|
||||||
const createImageSearchChain = (llm: BaseChatModel) => {
|
const createImageSearchChain = (llm: BaseChatModel) => {
|
||||||
@@ -62,7 +56,7 @@ const createImageSearchChain = (llm: BaseChatModel) => {
|
|||||||
engines: ['bing images', 'google images'],
|
engines: ['bing images', 'google images'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const images: ImageSearchResult[] = [];
|
const images = [];
|
||||||
|
|
||||||
res.results.forEach((result) => {
|
res.results.forEach((result) => {
|
||||||
if (result.img_src && result.url && result.title) {
|
if (result.img_src && result.url && result.title) {
|
@@ -1,5 +1,5 @@
|
|||||||
import { RunnableSequence, RunnableMap } from '@langchain/core/runnables';
|
import { RunnableSequence, RunnableMap } from '@langchain/core/runnables';
|
||||||
import ListLineOutputParser from '../outputParsers/listLineOutputParser';
|
import ListLineOutputParser from '../lib/outputParsers/listLineOutputParser';
|
||||||
import { PromptTemplate } from '@langchain/core/prompts';
|
import { PromptTemplate } from '@langchain/core/prompts';
|
||||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
import formatChatHistoryAsString from '../utils/formatHistory';
|
||||||
import { BaseMessage } from '@langchain/core/messages';
|
import { BaseMessage } from '@langchain/core/messages';
|
@@ -7,7 +7,7 @@ import { PromptTemplate } from '@langchain/core/prompts';
|
|||||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
import formatChatHistoryAsString from '../utils/formatHistory';
|
||||||
import { BaseMessage } from '@langchain/core/messages';
|
import { BaseMessage } from '@langchain/core/messages';
|
||||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
import { StringOutputParser } from '@langchain/core/output_parsers';
|
||||||
import { searchSearxng } from '../searxng';
|
import { searchSearxng } from '../lib/searxng';
|
||||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
|
||||||
const VideoSearchChainPrompt = `
|
const VideoSearchChainPrompt = `
|
||||||
@@ -36,13 +36,6 @@ type VideoSearchChainInput = {
|
|||||||
query: string;
|
query: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface VideoSearchResult {
|
|
||||||
img_src: string;
|
|
||||||
url: string;
|
|
||||||
title: string;
|
|
||||||
iframe_src: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const strParser = new StringOutputParser();
|
const strParser = new StringOutputParser();
|
||||||
|
|
||||||
const createVideoSearchChain = (llm: BaseChatModel) => {
|
const createVideoSearchChain = (llm: BaseChatModel) => {
|
||||||
@@ -63,7 +56,7 @@ const createVideoSearchChain = (llm: BaseChatModel) => {
|
|||||||
engines: ['youtube'],
|
engines: ['youtube'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const videos: VideoSearchResult[] = [];
|
const videos = [];
|
||||||
|
|
||||||
res.results.forEach((result) => {
|
res.results.forEach((result) => {
|
||||||
if (
|
if (
|
@@ -1,101 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { BookOpenText, Home, Search, SquarePen, Settings } from 'lucide-react';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useSelectedLayoutSegments } from 'next/navigation';
|
|
||||||
import React, { useState, type ReactNode } from 'react';
|
|
||||||
import Layout from './Layout';
|
|
||||||
|
|
||||||
const VerticalIconContainer = ({ children }: { children: ReactNode }) => {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center gap-y-3 w-full">{children}</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Sidebar = ({ children }: { children: React.ReactNode }) => {
|
|
||||||
const segments = useSelectedLayoutSegments();
|
|
||||||
|
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
|
||||||
|
|
||||||
const navLinks = [
|
|
||||||
{
|
|
||||||
icon: Home,
|
|
||||||
href: '/',
|
|
||||||
active: segments.length === 0 || segments.includes('c'),
|
|
||||||
label: 'Home',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Search,
|
|
||||||
href: '/discover',
|
|
||||||
active: segments.includes('discover'),
|
|
||||||
label: 'Discover',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: BookOpenText,
|
|
||||||
href: '/library',
|
|
||||||
active: segments.includes('library'),
|
|
||||||
label: 'Library',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-20 lg:flex-col">
|
|
||||||
<div className="flex grow flex-col items-center justify-between gap-y-5 overflow-y-auto bg-light-secondary dark:bg-dark-secondary px-2 py-8">
|
|
||||||
<a href="/">
|
|
||||||
<SquarePen className="cursor-pointer" />
|
|
||||||
</a>
|
|
||||||
<VerticalIconContainer>
|
|
||||||
{navLinks.map((link, i) => (
|
|
||||||
<Link
|
|
||||||
key={i}
|
|
||||||
href={link.href}
|
|
||||||
className={cn(
|
|
||||||
'relative flex flex-row items-center justify-center cursor-pointer hover:bg-black/10 dark:hover:bg-white/10 duration-150 transition w-full py-2 rounded-lg',
|
|
||||||
link.active
|
|
||||||
? 'text-black dark:text-white'
|
|
||||||
: 'text-black/70 dark:text-white/70',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<link.icon />
|
|
||||||
{link.active && (
|
|
||||||
<div className="absolute right-0 -mr-2 h-full w-1 rounded-l-lg bg-black dark:bg-white" />
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</VerticalIconContainer>
|
|
||||||
|
|
||||||
<Link href="/settings">
|
|
||||||
<Settings className="cursor-pointer" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="fixed bottom-0 w-full z-50 flex flex-row items-center gap-x-6 bg-light-primary dark:bg-dark-primary px-4 py-4 shadow-sm lg:hidden">
|
|
||||||
{navLinks.map((link, i) => (
|
|
||||||
<Link
|
|
||||||
href={link.href}
|
|
||||||
key={i}
|
|
||||||
className={cn(
|
|
||||||
'relative flex flex-col items-center space-y-1 text-center w-full',
|
|
||||||
link.active
|
|
||||||
? 'text-black dark:text-white'
|
|
||||||
: 'text-black dark:text-white/70',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{link.active && (
|
|
||||||
<div className="absolute top-0 -mt-4 h-1 w-full rounded-b-lg bg-black dark:bg-white" />
|
|
||||||
)}
|
|
||||||
<link.icon />
|
|
||||||
<p className="text-xs">{link.label}</p>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Layout>{children}</Layout>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Sidebar;
|
|
@@ -8,6 +8,10 @@ interface Config {
|
|||||||
GENERAL: {
|
GENERAL: {
|
||||||
PORT: number;
|
PORT: number;
|
||||||
SIMILARITY_MEASURE: string;
|
SIMILARITY_MEASURE: string;
|
||||||
|
CONFIG_PASSWORD: string;
|
||||||
|
DISCOVER_ENABLED: boolean;
|
||||||
|
LIBRARY_ENABLED: boolean;
|
||||||
|
COPILOT_ENABLED: boolean;
|
||||||
KEEP_ALIVE: string;
|
KEEP_ALIVE: string;
|
||||||
};
|
};
|
||||||
MODELS: {
|
MODELS: {
|
||||||
@@ -43,7 +47,7 @@ type RecursivePartial<T> = {
|
|||||||
|
|
||||||
const loadConfig = () =>
|
const loadConfig = () =>
|
||||||
toml.parse(
|
toml.parse(
|
||||||
fs.readFileSync(path.join(process.cwd(), `${configFileName}`), 'utf-8'),
|
fs.readFileSync(path.join(__dirname, `../${configFileName}`), 'utf-8'),
|
||||||
) as any as Config;
|
) as any as Config;
|
||||||
|
|
||||||
export const getPort = () => loadConfig().GENERAL.PORT;
|
export const getPort = () => loadConfig().GENERAL.PORT;
|
||||||
@@ -51,6 +55,14 @@ export const getPort = () => loadConfig().GENERAL.PORT;
|
|||||||
export const getSimilarityMeasure = () =>
|
export const getSimilarityMeasure = () =>
|
||||||
loadConfig().GENERAL.SIMILARITY_MEASURE;
|
loadConfig().GENERAL.SIMILARITY_MEASURE;
|
||||||
|
|
||||||
|
export const getConfigPassword = () => loadConfig().GENERAL.CONFIG_PASSWORD;
|
||||||
|
|
||||||
|
export const isDiscoverEnabled = () => loadConfig().GENERAL.DISCOVER_ENABLED;
|
||||||
|
|
||||||
|
export const isLibraryEnabled = () => loadConfig().GENERAL.LIBRARY_ENABLED;
|
||||||
|
|
||||||
|
export const isCopilotEnabled = () => loadConfig().GENERAL.COPILOT_ENABLED;
|
||||||
|
|
||||||
export const getKeepAlive = () => loadConfig().GENERAL.KEEP_ALIVE;
|
export const getKeepAlive = () => loadConfig().GENERAL.KEEP_ALIVE;
|
||||||
|
|
||||||
export const getOpenaiApiKey = () => loadConfig().MODELS.OPENAI.API_KEY;
|
export const getOpenaiApiKey = () => loadConfig().MODELS.OPENAI.API_KEY;
|
||||||
@@ -62,7 +74,7 @@ export const getAnthropicApiKey = () => loadConfig().MODELS.ANTHROPIC.API_KEY;
|
|||||||
export const getGeminiApiKey = () => loadConfig().MODELS.GEMINI.API_KEY;
|
export const getGeminiApiKey = () => loadConfig().MODELS.GEMINI.API_KEY;
|
||||||
|
|
||||||
export const getSearxngApiEndpoint = () =>
|
export const getSearxngApiEndpoint = () =>
|
||||||
loadConfig().API_ENDPOINTS.SEARXNG || process.env.SEARXNG_API_URL;
|
process.env.SEARXNG_API_URL || loadConfig().API_ENDPOINTS.SEARXNG;
|
||||||
|
|
||||||
export const getOllamaApiEndpoint = () => loadConfig().MODELS.OLLAMA.API_URL;
|
export const getOllamaApiEndpoint = () => loadConfig().MODELS.OLLAMA.API_URL;
|
||||||
|
|
||||||
@@ -109,8 +121,9 @@ const mergeConfigs = (current: any, update: any): any => {
|
|||||||
export const updateConfig = (config: RecursivePartial<Config>) => {
|
export const updateConfig = (config: RecursivePartial<Config>) => {
|
||||||
const currentConfig = loadConfig();
|
const currentConfig = loadConfig();
|
||||||
const mergedConfig = mergeConfigs(currentConfig, config);
|
const mergedConfig = mergeConfigs(currentConfig, config);
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(path.join(process.cwd(), `${configFileName}`)),
|
path.join(__dirname, `../${configFileName}`),
|
||||||
toml.stringify(mergedConfig),
|
toml.stringify(mergedConfig),
|
||||||
);
|
);
|
||||||
};
|
};
|
@@ -1,9 +1,8 @@
|
|||||||
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
||||||
import Database from 'better-sqlite3';
|
import Database from 'better-sqlite3';
|
||||||
import * as schema from './schema';
|
import * as schema from './schema';
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
const sqlite = new Database(path.join(process.cwd(), 'data/db.sqlite'));
|
const sqlite = new Database('data/db.sqlite');
|
||||||
const db = drizzle(sqlite, {
|
const db = drizzle(sqlite, {
|
||||||
schema: schema,
|
schema: schema,
|
||||||
});
|
});
|
82
src/lib/huggingfaceTransformer.ts
Normal file
82
src/lib/huggingfaceTransformer.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { Embeddings, type EmbeddingsParams } from '@langchain/core/embeddings';
|
||||||
|
import { chunkArray } from '@langchain/core/utils/chunk_array';
|
||||||
|
|
||||||
|
export interface HuggingFaceTransformersEmbeddingsParams
|
||||||
|
extends EmbeddingsParams {
|
||||||
|
modelName: string;
|
||||||
|
|
||||||
|
model: string;
|
||||||
|
|
||||||
|
timeout?: number;
|
||||||
|
|
||||||
|
batchSize?: number;
|
||||||
|
|
||||||
|
stripNewLines?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HuggingFaceTransformersEmbeddings
|
||||||
|
extends Embeddings
|
||||||
|
implements HuggingFaceTransformersEmbeddingsParams
|
||||||
|
{
|
||||||
|
modelName = 'Xenova/all-MiniLM-L6-v2';
|
||||||
|
|
||||||
|
model = 'Xenova/all-MiniLM-L6-v2';
|
||||||
|
|
||||||
|
batchSize = 512;
|
||||||
|
|
||||||
|
stripNewLines = true;
|
||||||
|
|
||||||
|
timeout?: number;
|
||||||
|
|
||||||
|
private pipelinePromise: Promise<any>;
|
||||||
|
|
||||||
|
constructor(fields?: Partial<HuggingFaceTransformersEmbeddingsParams>) {
|
||||||
|
super(fields ?? {});
|
||||||
|
|
||||||
|
this.modelName = fields?.model ?? fields?.modelName ?? this.model;
|
||||||
|
this.model = this.modelName;
|
||||||
|
this.stripNewLines = fields?.stripNewLines ?? this.stripNewLines;
|
||||||
|
this.timeout = fields?.timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
async embedDocuments(texts: string[]): Promise<number[][]> {
|
||||||
|
const batches = chunkArray(
|
||||||
|
this.stripNewLines ? texts.map((t) => t.replace(/\n/g, ' ')) : texts,
|
||||||
|
this.batchSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
const batchRequests = batches.map((batch) => this.runEmbedding(batch));
|
||||||
|
const batchResponses = await Promise.all(batchRequests);
|
||||||
|
const embeddings: number[][] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < batchResponses.length; i += 1) {
|
||||||
|
const batchResponse = batchResponses[i];
|
||||||
|
for (let j = 0; j < batchResponse.length; j += 1) {
|
||||||
|
embeddings.push(batchResponse[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return embeddings;
|
||||||
|
}
|
||||||
|
|
||||||
|
async embedQuery(text: string): Promise<number[]> {
|
||||||
|
const data = await this.runEmbedding([
|
||||||
|
this.stripNewLines ? text.replace(/\n/g, ' ') : text,
|
||||||
|
]);
|
||||||
|
return data[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runEmbedding(texts: string[]) {
|
||||||
|
const { pipeline } = await import('@xenova/transformers');
|
||||||
|
|
||||||
|
const pipe = await (this.pipelinePromise ??= pipeline(
|
||||||
|
'feature-extraction',
|
||||||
|
this.model,
|
||||||
|
));
|
||||||
|
|
||||||
|
return this.caller.call(async () => {
|
||||||
|
const output = await pipe(texts, { pooling: 'mean', normalize: true });
|
||||||
|
return output.tolist();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -9,7 +9,7 @@ class LineOutputParser extends BaseOutputParser<string> {
|
|||||||
|
|
||||||
constructor(args?: LineOutputParserArgs) {
|
constructor(args?: LineOutputParserArgs) {
|
||||||
super();
|
super();
|
||||||
this.key = args?.key ?? this.key;
|
this.key = args.key ?? this.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
static lc_name() {
|
static lc_name() {
|
||||||
|
@@ -9,7 +9,7 @@ class LineListOutputParser extends BaseOutputParser<string[]> {
|
|||||||
|
|
||||||
constructor(args?: LineListOutputParserArgs) {
|
constructor(args?: LineListOutputParserArgs) {
|
||||||
super();
|
super();
|
||||||
this.key = args?.key ?? this.key;
|
this.key = args.key ?? this.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
static lc_name() {
|
static lc_name() {
|
||||||
|
@@ -1,38 +1,6 @@
|
|||||||
import { ChatOpenAI } from '@langchain/openai';
|
import { ChatAnthropic } from '@langchain/anthropic';
|
||||||
import { ChatModel } from '.';
|
import { getAnthropicApiKey } from '../../config';
|
||||||
import { getAnthropicApiKey } from '../config';
|
import logger from '../../utils/logger';
|
||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
|
|
||||||
const anthropicChatModels: Record<string, string>[] = [
|
|
||||||
{
|
|
||||||
displayName: 'Claude 3.7 Sonnet',
|
|
||||||
key: 'claude-3-7-sonnet-20250219',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Claude 3.5 Haiku',
|
|
||||||
key: 'claude-3-5-haiku-20241022',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Claude 3.5 Sonnet v2',
|
|
||||||
key: 'claude-3-5-sonnet-20241022',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Claude 3.5 Sonnet',
|
|
||||||
key: 'claude-3-5-sonnet-20240620',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Claude 3 Opus',
|
|
||||||
key: 'claude-3-opus-20240229',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Claude 3 Sonnet',
|
|
||||||
key: 'claude-3-sonnet-20240229',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Claude 3 Haiku',
|
|
||||||
key: 'claude-3-haiku-20240307',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const loadAnthropicChatModels = async () => {
|
export const loadAnthropicChatModels = async () => {
|
||||||
const anthropicApiKey = getAnthropicApiKey();
|
const anthropicApiKey = getAnthropicApiKey();
|
||||||
@@ -40,25 +8,52 @@ export const loadAnthropicChatModels = async () => {
|
|||||||
if (!anthropicApiKey) return {};
|
if (!anthropicApiKey) return {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const chatModels: Record<string, ChatModel> = {};
|
const chatModels = {
|
||||||
|
'claude-3-5-sonnet-20241022': {
|
||||||
anthropicChatModels.forEach((model) => {
|
displayName: 'Claude 3.5 Sonnet',
|
||||||
chatModels[model.key] = {
|
model: new ChatAnthropic({
|
||||||
displayName: model.displayName,
|
|
||||||
model: new ChatOpenAI({
|
|
||||||
openAIApiKey: anthropicApiKey,
|
|
||||||
modelName: model.key,
|
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
configuration: {
|
anthropicApiKey: anthropicApiKey,
|
||||||
baseURL: 'https://api.anthropic.com/v1/',
|
model: 'claude-3-5-sonnet-20241022',
|
||||||
},
|
}),
|
||||||
}) as unknown as BaseChatModel,
|
},
|
||||||
};
|
'claude-3-5-haiku-20241022': {
|
||||||
});
|
displayName: 'Claude 3.5 Haiku',
|
||||||
|
model: new ChatAnthropic({
|
||||||
|
temperature: 0.7,
|
||||||
|
anthropicApiKey: anthropicApiKey,
|
||||||
|
model: 'claude-3-5-haiku-20241022',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'claude-3-opus-20240229': {
|
||||||
|
displayName: 'Claude 3 Opus',
|
||||||
|
model: new ChatAnthropic({
|
||||||
|
temperature: 0.7,
|
||||||
|
anthropicApiKey: anthropicApiKey,
|
||||||
|
model: 'claude-3-opus-20240229',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'claude-3-sonnet-20240229': {
|
||||||
|
displayName: 'Claude 3 Sonnet',
|
||||||
|
model: new ChatAnthropic({
|
||||||
|
temperature: 0.7,
|
||||||
|
anthropicApiKey: anthropicApiKey,
|
||||||
|
model: 'claude-3-sonnet-20240229',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'claude-3-haiku-20240307': {
|
||||||
|
displayName: 'Claude 3 Haiku',
|
||||||
|
model: new ChatAnthropic({
|
||||||
|
temperature: 0.7,
|
||||||
|
anthropicApiKey: anthropicApiKey,
|
||||||
|
model: 'claude-3-haiku-20240307',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return chatModels;
|
return chatModels;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error loading Anthropic models: ${err}`);
|
logger.error(`Error loading Anthropic models: ${err}`);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -1,42 +1,9 @@
|
|||||||
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
|
import {
|
||||||
import { getGeminiApiKey } from '../config';
|
ChatGoogleGenerativeAI,
|
||||||
import { ChatModel, EmbeddingModel } from '.';
|
GoogleGenerativeAIEmbeddings,
|
||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
} from '@langchain/google-genai';
|
||||||
import { Embeddings } from '@langchain/core/embeddings';
|
import { getGeminiApiKey } from '../../config';
|
||||||
|
import logger from '../../utils/logger';
|
||||||
const geminiChatModels: Record<string, string>[] = [
|
|
||||||
{
|
|
||||||
displayName: 'Gemini 2.0 Flash',
|
|
||||||
key: 'gemini-2.0-flash',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Gemini 2.0 Flash-Lite',
|
|
||||||
key: 'gemini-2.0-flash-lite',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Gemini 2.0 Pro Experimental',
|
|
||||||
key: 'gemini-2.0-pro-exp-02-05',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Gemini 1.5 Flash',
|
|
||||||
key: 'gemini-1.5-flash',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Gemini 1.5 Flash-8B',
|
|
||||||
key: 'gemini-1.5-flash-8b',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Gemini 1.5 Pro',
|
|
||||||
key: 'gemini-1.5-pro',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const geminiEmbeddingModels: Record<string, string>[] = [
|
|
||||||
{
|
|
||||||
displayName: 'Gemini Embedding',
|
|
||||||
key: 'gemini-embedding-exp',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const loadGeminiChatModels = async () => {
|
export const loadGeminiChatModels = async () => {
|
||||||
const geminiApiKey = getGeminiApiKey();
|
const geminiApiKey = getGeminiApiKey();
|
||||||
@@ -44,53 +11,75 @@ export const loadGeminiChatModels = async () => {
|
|||||||
if (!geminiApiKey) return {};
|
if (!geminiApiKey) return {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const chatModels: Record<string, ChatModel> = {};
|
const chatModels = {
|
||||||
|
'gemini-1.5-flash': {
|
||||||
geminiChatModels.forEach((model) => {
|
displayName: 'Gemini 1.5 Flash',
|
||||||
chatModels[model.key] = {
|
model: new ChatGoogleGenerativeAI({
|
||||||
displayName: model.displayName,
|
modelName: 'gemini-1.5-flash',
|
||||||
model: new ChatOpenAI({
|
|
||||||
openAIApiKey: geminiApiKey,
|
|
||||||
modelName: model.key,
|
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
configuration: {
|
apiKey: geminiApiKey,
|
||||||
baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/',
|
}),
|
||||||
},
|
},
|
||||||
}) as unknown as BaseChatModel,
|
'gemini-1.5-flash-8b': {
|
||||||
};
|
displayName: 'Gemini 1.5 Flash 8B',
|
||||||
});
|
model: new ChatGoogleGenerativeAI({
|
||||||
|
modelName: 'gemini-1.5-flash-8b',
|
||||||
|
temperature: 0.7,
|
||||||
|
apiKey: geminiApiKey,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'gemini-1.5-pro': {
|
||||||
|
displayName: 'Gemini 1.5 Pro',
|
||||||
|
model: new ChatGoogleGenerativeAI({
|
||||||
|
modelName: 'gemini-1.5-pro',
|
||||||
|
temperature: 0.7,
|
||||||
|
apiKey: geminiApiKey,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'gemini-2.0-flash-exp': {
|
||||||
|
displayName: 'Gemini 2.0 Flash Exp',
|
||||||
|
model: new ChatGoogleGenerativeAI({
|
||||||
|
modelName: 'gemini-2.0-flash-exp',
|
||||||
|
temperature: 0.7,
|
||||||
|
apiKey: geminiApiKey,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'gemini-2.0-flash-thinking-exp-01-21': {
|
||||||
|
displayName: 'Gemini 2.0 Flash Thinking Exp 01-21',
|
||||||
|
model: new ChatGoogleGenerativeAI({
|
||||||
|
modelName: 'gemini-2.0-flash-thinking-exp-01-21',
|
||||||
|
temperature: 0.7,
|
||||||
|
apiKey: geminiApiKey,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return chatModels;
|
return chatModels;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error loading Gemini models: ${err}`);
|
logger.error(`Error loading Gemini models: ${err}`);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadGeminiEmbeddingModels = async () => {
|
export const loadGeminiEmbeddingsModels = async () => {
|
||||||
const geminiApiKey = getGeminiApiKey();
|
const geminiApiKey = getGeminiApiKey();
|
||||||
|
|
||||||
if (!geminiApiKey) return {};
|
if (!geminiApiKey) return {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const embeddingModels: Record<string, EmbeddingModel> = {};
|
const embeddingModels = {
|
||||||
|
'text-embedding-004': {
|
||||||
geminiEmbeddingModels.forEach((model) => {
|
displayName: 'Text Embedding',
|
||||||
embeddingModels[model.key] = {
|
model: new GoogleGenerativeAIEmbeddings({
|
||||||
displayName: model.displayName,
|
apiKey: geminiApiKey,
|
||||||
model: new OpenAIEmbeddings({
|
modelName: 'text-embedding-004',
|
||||||
openAIApiKey: geminiApiKey,
|
}),
|
||||||
modelName: model.key,
|
},
|
||||||
configuration: {
|
};
|
||||||
baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/',
|
|
||||||
},
|
|
||||||
}) as unknown as Embeddings,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return embeddingModels;
|
return embeddingModels;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error loading OpenAI embeddings models: ${err}`);
|
logger.error(`Error loading Gemini embeddings model: ${err}`);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -1,82 +1,6 @@
|
|||||||
import { ChatOpenAI } from '@langchain/openai';
|
import { ChatOpenAI } from '@langchain/openai';
|
||||||
import { getGroqApiKey } from '../config';
|
import { getGroqApiKey } from '../../config';
|
||||||
import { ChatModel } from '.';
|
import logger from '../../utils/logger';
|
||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
|
|
||||||
const groqChatModels: Record<string, string>[] = [
|
|
||||||
{
|
|
||||||
displayName: 'Gemma2 9B IT',
|
|
||||||
key: 'gemma2-9b-it',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Llama 3.3 70B Versatile',
|
|
||||||
key: 'llama-3.3-70b-versatile',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Llama 3.1 8B Instant',
|
|
||||||
key: 'llama-3.1-8b-instant',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Llama3 70B 8192',
|
|
||||||
key: 'llama3-70b-8192',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Llama3 8B 8192',
|
|
||||||
key: 'llama3-8b-8192',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Mixtral 8x7B 32768',
|
|
||||||
key: 'mixtral-8x7b-32768',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Qwen QWQ 32B (Preview)',
|
|
||||||
key: 'qwen-qwq-32b',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Mistral Saba 24B (Preview)',
|
|
||||||
key: 'mistral-saba-24b',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Qwen 2.5 Coder 32B (Preview)',
|
|
||||||
key: 'qwen-2.5-coder-32b',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Qwen 2.5 32B (Preview)',
|
|
||||||
key: 'qwen-2.5-32b',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'DeepSeek R1 Distill Qwen 32B (Preview)',
|
|
||||||
key: 'deepseek-r1-distill-qwen-32b',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'DeepSeek R1 Distill Llama 70B SpecDec (Preview)',
|
|
||||||
key: 'deepseek-r1-distill-llama-70b-specdec',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'DeepSeek R1 Distill Llama 70B (Preview)',
|
|
||||||
key: 'deepseek-r1-distill-llama-70b',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Llama 3.3 70B SpecDec (Preview)',
|
|
||||||
key: 'llama-3.3-70b-specdec',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Llama 3.2 1B Preview (Preview)',
|
|
||||||
key: 'llama-3.2-1b-preview',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Llama 3.2 3B Preview (Preview)',
|
|
||||||
key: 'llama-3.2-3b-preview',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Llama 3.2 11B Vision Preview (Preview)',
|
|
||||||
key: 'llama-3.2-11b-vision-preview',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Llama 3.2 90B Vision Preview (Preview)',
|
|
||||||
key: 'llama-3.2-90b-vision-preview',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const loadGroqChatModels = async () => {
|
export const loadGroqChatModels = async () => {
|
||||||
const groqApiKey = getGroqApiKey();
|
const groqApiKey = getGroqApiKey();
|
||||||
@@ -84,25 +8,129 @@ export const loadGroqChatModels = async () => {
|
|||||||
if (!groqApiKey) return {};
|
if (!groqApiKey) return {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const chatModels: Record<string, ChatModel> = {};
|
const chatModels = {
|
||||||
|
'llama-3.3-70b-versatile': {
|
||||||
groqChatModels.forEach((model) => {
|
displayName: 'Llama 3.3 70B',
|
||||||
chatModels[model.key] = {
|
model: new ChatOpenAI(
|
||||||
displayName: model.displayName,
|
{
|
||||||
model: new ChatOpenAI({
|
openAIApiKey: groqApiKey,
|
||||||
openAIApiKey: groqApiKey,
|
modelName: 'llama-3.3-70b-versatile',
|
||||||
modelName: model.key,
|
temperature: 0.7,
|
||||||
temperature: 0.7,
|
},
|
||||||
configuration: {
|
{
|
||||||
baseURL: 'https://api.groq.com/openai/v1',
|
baseURL: 'https://api.groq.com/openai/v1',
|
||||||
},
|
},
|
||||||
}) as unknown as BaseChatModel,
|
),
|
||||||
};
|
},
|
||||||
});
|
'llama-3.2-3b-preview': {
|
||||||
|
displayName: 'Llama 3.2 3B',
|
||||||
|
model: new ChatOpenAI(
|
||||||
|
{
|
||||||
|
openAIApiKey: groqApiKey,
|
||||||
|
modelName: 'llama-3.2-3b-preview',
|
||||||
|
temperature: 0.7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
baseURL: 'https://api.groq.com/openai/v1',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'llama-3.2-11b-vision-preview': {
|
||||||
|
displayName: 'Llama 3.2 11B Vision',
|
||||||
|
model: new ChatOpenAI(
|
||||||
|
{
|
||||||
|
openAIApiKey: groqApiKey,
|
||||||
|
modelName: 'llama-3.2-11b-vision-preview',
|
||||||
|
temperature: 0.7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
baseURL: 'https://api.groq.com/openai/v1',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'llama-3.2-90b-vision-preview': {
|
||||||
|
displayName: 'Llama 3.2 90B Vision',
|
||||||
|
model: new ChatOpenAI(
|
||||||
|
{
|
||||||
|
openAIApiKey: groqApiKey,
|
||||||
|
modelName: 'llama-3.2-90b-vision-preview',
|
||||||
|
temperature: 0.7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
baseURL: 'https://api.groq.com/openai/v1',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'llama-3.1-8b-instant': {
|
||||||
|
displayName: 'Llama 3.1 8B',
|
||||||
|
model: new ChatOpenAI(
|
||||||
|
{
|
||||||
|
openAIApiKey: groqApiKey,
|
||||||
|
modelName: 'llama-3.1-8b-instant',
|
||||||
|
temperature: 0.7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
baseURL: 'https://api.groq.com/openai/v1',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'llama3-8b-8192': {
|
||||||
|
displayName: 'LLaMA3 8B',
|
||||||
|
model: new ChatOpenAI(
|
||||||
|
{
|
||||||
|
openAIApiKey: groqApiKey,
|
||||||
|
modelName: 'llama3-8b-8192',
|
||||||
|
temperature: 0.7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
baseURL: 'https://api.groq.com/openai/v1',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'llama3-70b-8192': {
|
||||||
|
displayName: 'LLaMA3 70B',
|
||||||
|
model: new ChatOpenAI(
|
||||||
|
{
|
||||||
|
openAIApiKey: groqApiKey,
|
||||||
|
modelName: 'llama3-70b-8192',
|
||||||
|
temperature: 0.7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
baseURL: 'https://api.groq.com/openai/v1',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'mixtral-8x7b-32768': {
|
||||||
|
displayName: 'Mixtral 8x7B',
|
||||||
|
model: new ChatOpenAI(
|
||||||
|
{
|
||||||
|
openAIApiKey: groqApiKey,
|
||||||
|
modelName: 'mixtral-8x7b-32768',
|
||||||
|
temperature: 0.7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
baseURL: 'https://api.groq.com/openai/v1',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'gemma2-9b-it': {
|
||||||
|
displayName: 'Gemma2 9B',
|
||||||
|
model: new ChatOpenAI(
|
||||||
|
{
|
||||||
|
openAIApiKey: groqApiKey,
|
||||||
|
modelName: 'gemma2-9b-it',
|
||||||
|
temperature: 0.7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
baseURL: 'https://api.groq.com/openai/v1',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return chatModels;
|
return chatModels;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error loading Groq models: ${err}`);
|
logger.error(`Error loading Groq models: ${err}`);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -1,49 +1,33 @@
|
|||||||
import { Embeddings } from '@langchain/core/embeddings';
|
import { loadGroqChatModels } from './groq';
|
||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
import { loadOllamaChatModels, loadOllamaEmbeddingsModels } from './ollama';
|
||||||
import { loadOpenAIChatModels, loadOpenAIEmbeddingModels } from './openai';
|
import { loadOpenAIChatModels, loadOpenAIEmbeddingsModels } from './openai';
|
||||||
|
import { loadAnthropicChatModels } from './anthropic';
|
||||||
|
import { loadTransformersEmbeddingsModels } from './transformers';
|
||||||
|
import { loadGeminiChatModels, loadGeminiEmbeddingsModels } from './gemini';
|
||||||
import {
|
import {
|
||||||
getCustomOpenaiApiKey,
|
getCustomOpenaiApiKey,
|
||||||
getCustomOpenaiApiUrl,
|
getCustomOpenaiApiUrl,
|
||||||
getCustomOpenaiModelName,
|
getCustomOpenaiModelName,
|
||||||
} from '../config';
|
} from '../../config';
|
||||||
import { ChatOpenAI } from '@langchain/openai';
|
import { ChatOpenAI } from '@langchain/openai';
|
||||||
import { loadOllamaChatModels, loadOllamaEmbeddingModels } from './ollama';
|
|
||||||
import { loadGroqChatModels } from './groq';
|
|
||||||
import { loadAnthropicChatModels } from './anthropic';
|
|
||||||
import { loadGeminiChatModels, loadGeminiEmbeddingModels } from './gemini';
|
|
||||||
|
|
||||||
export interface ChatModel {
|
const chatModelProviders = {
|
||||||
displayName: string;
|
|
||||||
model: BaseChatModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EmbeddingModel {
|
|
||||||
displayName: string;
|
|
||||||
model: Embeddings;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const chatModelProviders: Record<
|
|
||||||
string,
|
|
||||||
() => Promise<Record<string, ChatModel>>
|
|
||||||
> = {
|
|
||||||
openai: loadOpenAIChatModels,
|
openai: loadOpenAIChatModels,
|
||||||
ollama: loadOllamaChatModels,
|
|
||||||
groq: loadGroqChatModels,
|
groq: loadGroqChatModels,
|
||||||
|
ollama: loadOllamaChatModels,
|
||||||
anthropic: loadAnthropicChatModels,
|
anthropic: loadAnthropicChatModels,
|
||||||
gemini: loadGeminiChatModels,
|
gemini: loadGeminiChatModels,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const embeddingModelProviders: Record<
|
const embeddingModelProviders = {
|
||||||
string,
|
openai: loadOpenAIEmbeddingsModels,
|
||||||
() => Promise<Record<string, EmbeddingModel>>
|
local: loadTransformersEmbeddingsModels,
|
||||||
> = {
|
ollama: loadOllamaEmbeddingsModels,
|
||||||
openai: loadOpenAIEmbeddingModels,
|
gemini: loadGeminiEmbeddingsModels,
|
||||||
ollama: loadOllamaEmbeddingModels,
|
|
||||||
gemini: loadGeminiEmbeddingModels,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAvailableChatModelProviders = async () => {
|
export const getAvailableChatModelProviders = async () => {
|
||||||
const models: Record<string, Record<string, ChatModel>> = {};
|
const models = {};
|
||||||
|
|
||||||
for (const provider in chatModelProviders) {
|
for (const provider in chatModelProviders) {
|
||||||
const providerModels = await chatModelProviders[provider]();
|
const providerModels = await chatModelProviders[provider]();
|
||||||
@@ -68,7 +52,7 @@ export const getAvailableChatModelProviders = async () => {
|
|||||||
configuration: {
|
configuration: {
|
||||||
baseURL: customOpenAiApiUrl,
|
baseURL: customOpenAiApiUrl,
|
||||||
},
|
},
|
||||||
}) as unknown as BaseChatModel,
|
}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
@@ -78,7 +62,7 @@ export const getAvailableChatModelProviders = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getAvailableEmbeddingModelProviders = async () => {
|
export const getAvailableEmbeddingModelProviders = async () => {
|
||||||
const models: Record<string, Record<string, EmbeddingModel>> = {};
|
const models = {};
|
||||||
|
|
||||||
for (const provider in embeddingModelProviders) {
|
for (const provider in embeddingModelProviders) {
|
||||||
const providerModels = await embeddingModelProviders[provider]();
|
const providerModels = await embeddingModelProviders[provider]();
|
||||||
|
@@ -1,73 +1,74 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import { getKeepAlive, getOllamaApiEndpoint } from '../config';
|
|
||||||
import { ChatModel, EmbeddingModel } from '.';
|
|
||||||
import { ChatOllama } from '@langchain/community/chat_models/ollama';
|
|
||||||
import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama';
|
import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama';
|
||||||
|
import { getKeepAlive, getOllamaApiEndpoint } from '../../config';
|
||||||
|
import logger from '../../utils/logger';
|
||||||
|
import { ChatOllama } from '@langchain/community/chat_models/ollama';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
export const loadOllamaChatModels = async () => {
|
export const loadOllamaChatModels = async () => {
|
||||||
const ollamaApiEndpoint = getOllamaApiEndpoint();
|
const ollamaEndpoint = getOllamaApiEndpoint();
|
||||||
|
const keepAlive = getKeepAlive();
|
||||||
|
|
||||||
if (!ollamaApiEndpoint) return {};
|
if (!ollamaEndpoint) return {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(`${ollamaApiEndpoint}/api/tags`, {
|
const response = await axios.get(`${ollamaEndpoint}/api/tags`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { models } = res.data;
|
const { models: ollamaModels } = response.data;
|
||||||
|
|
||||||
const chatModels: Record<string, ChatModel> = {};
|
const chatModels = ollamaModels.reduce((acc, model) => {
|
||||||
|
acc[model.model] = {
|
||||||
models.forEach((model: any) => {
|
|
||||||
chatModels[model.model] = {
|
|
||||||
displayName: model.name,
|
displayName: model.name,
|
||||||
model: new ChatOllama({
|
model: new ChatOllama({
|
||||||
baseUrl: ollamaApiEndpoint,
|
baseUrl: ollamaEndpoint,
|
||||||
model: model.model,
|
model: model.model,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
keepAlive: getKeepAlive(),
|
keepAlive: keepAlive,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
return chatModels;
|
return chatModels;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error loading Ollama models: ${err}`);
|
logger.error(`Error loading Ollama models: ${err}`);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadOllamaEmbeddingModels = async () => {
|
export const loadOllamaEmbeddingsModels = async () => {
|
||||||
const ollamaApiEndpoint = getOllamaApiEndpoint();
|
const ollamaEndpoint = getOllamaApiEndpoint();
|
||||||
|
|
||||||
if (!ollamaApiEndpoint) return {};
|
if (!ollamaEndpoint) return {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(`${ollamaApiEndpoint}/api/tags`, {
|
const response = await axios.get(`${ollamaEndpoint}/api/tags`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { models } = res.data;
|
const { models: ollamaModels } = response.data;
|
||||||
|
|
||||||
const embeddingModels: Record<string, EmbeddingModel> = {};
|
const embeddingsModels = ollamaModels.reduce((acc, model) => {
|
||||||
|
acc[model.model] = {
|
||||||
models.forEach((model: any) => {
|
|
||||||
embeddingModels[model.model] = {
|
|
||||||
displayName: model.name,
|
displayName: model.name,
|
||||||
model: new OllamaEmbeddings({
|
model: new OllamaEmbeddings({
|
||||||
baseUrl: ollamaApiEndpoint,
|
baseUrl: ollamaEndpoint,
|
||||||
model: model.model,
|
model: model.model,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
|
||||||
return embeddingModels;
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return embeddingsModels;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error loading Ollama embeddings models: ${err}`);
|
logger.error(`Error loading Ollama embeddings model: ${err}`);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -1,90 +1,89 @@
|
|||||||
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
|
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
|
||||||
import { getOpenaiApiKey } from '../config';
|
import { getOpenaiApiKey } from '../../config';
|
||||||
import { ChatModel, EmbeddingModel } from '.';
|
import logger from '../../utils/logger';
|
||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { Embeddings } from '@langchain/core/embeddings';
|
|
||||||
|
|
||||||
const openaiChatModels: Record<string, string>[] = [
|
|
||||||
{
|
|
||||||
displayName: 'GPT-3.5 Turbo',
|
|
||||||
key: 'gpt-3.5-turbo',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'GPT-4',
|
|
||||||
key: 'gpt-4',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'GPT-4 turbo',
|
|
||||||
key: 'gpt-4-turbo',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'GPT-4 omni',
|
|
||||||
key: 'gpt-4o',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'GPT-4 omni mini',
|
|
||||||
key: 'gpt-4o-mini',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const openaiEmbeddingModels: Record<string, string>[] = [
|
|
||||||
{
|
|
||||||
displayName: 'Text Embedding 3 Small',
|
|
||||||
key: 'text-embedding-3-small',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Text Embedding 3 Large',
|
|
||||||
key: 'text-embedding-3-large',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const loadOpenAIChatModels = async () => {
|
export const loadOpenAIChatModels = async () => {
|
||||||
const openaiApiKey = getOpenaiApiKey();
|
const openAIApiKey = getOpenaiApiKey();
|
||||||
|
|
||||||
if (!openaiApiKey) return {};
|
if (!openAIApiKey) return {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const chatModels: Record<string, ChatModel> = {};
|
const chatModels = {
|
||||||
|
'gpt-3.5-turbo': {
|
||||||
openaiChatModels.forEach((model) => {
|
displayName: 'GPT-3.5 Turbo',
|
||||||
chatModels[model.key] = {
|
|
||||||
displayName: model.displayName,
|
|
||||||
model: new ChatOpenAI({
|
model: new ChatOpenAI({
|
||||||
openAIApiKey: openaiApiKey,
|
openAIApiKey,
|
||||||
modelName: model.key,
|
modelName: 'gpt-3.5-turbo',
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
}) as unknown as BaseChatModel,
|
}),
|
||||||
};
|
},
|
||||||
});
|
'gpt-4': {
|
||||||
|
displayName: 'GPT-4',
|
||||||
|
model: new ChatOpenAI({
|
||||||
|
openAIApiKey,
|
||||||
|
modelName: 'gpt-4',
|
||||||
|
temperature: 0.7,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'gpt-4-turbo': {
|
||||||
|
displayName: 'GPT-4 turbo',
|
||||||
|
model: new ChatOpenAI({
|
||||||
|
openAIApiKey,
|
||||||
|
modelName: 'gpt-4-turbo',
|
||||||
|
temperature: 0.7,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'gpt-4o': {
|
||||||
|
displayName: 'GPT-4 omni',
|
||||||
|
model: new ChatOpenAI({
|
||||||
|
openAIApiKey,
|
||||||
|
modelName: 'gpt-4o',
|
||||||
|
temperature: 0.7,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'gpt-4o-mini': {
|
||||||
|
displayName: 'GPT-4 omni mini',
|
||||||
|
model: new ChatOpenAI({
|
||||||
|
openAIApiKey,
|
||||||
|
modelName: 'gpt-4o-mini',
|
||||||
|
temperature: 0.7,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return chatModels;
|
return chatModels;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error loading OpenAI models: ${err}`);
|
logger.error(`Error loading OpenAI models: ${err}`);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadOpenAIEmbeddingModels = async () => {
|
export const loadOpenAIEmbeddingsModels = async () => {
|
||||||
const openaiApiKey = getOpenaiApiKey();
|
const openAIApiKey = getOpenaiApiKey();
|
||||||
|
|
||||||
if (!openaiApiKey) return {};
|
if (!openAIApiKey) return {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const embeddingModels: Record<string, EmbeddingModel> = {};
|
const embeddingModels = {
|
||||||
|
'text-embedding-3-small': {
|
||||||
openaiEmbeddingModels.forEach((model) => {
|
displayName: 'Text Embedding 3 Small',
|
||||||
embeddingModels[model.key] = {
|
|
||||||
displayName: model.displayName,
|
|
||||||
model: new OpenAIEmbeddings({
|
model: new OpenAIEmbeddings({
|
||||||
openAIApiKey: openaiApiKey,
|
openAIApiKey,
|
||||||
modelName: model.key,
|
modelName: 'text-embedding-3-small',
|
||||||
}) as unknown as Embeddings,
|
}),
|
||||||
};
|
},
|
||||||
});
|
'text-embedding-3-large': {
|
||||||
|
displayName: 'Text Embedding 3 Large',
|
||||||
|
model: new OpenAIEmbeddings({
|
||||||
|
openAIApiKey,
|
||||||
|
modelName: 'text-embedding-3-large',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return embeddingModels;
|
return embeddingModels;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error loading OpenAI embeddings models: ${err}`);
|
logger.error(`Error loading OpenAI embeddings model: ${err}`);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
32
src/lib/providers/transformers.ts
Normal file
32
src/lib/providers/transformers.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import logger from '../../utils/logger';
|
||||||
|
import { HuggingFaceTransformersEmbeddings } from '../huggingfaceTransformer';
|
||||||
|
|
||||||
|
export const loadTransformersEmbeddingsModels = async () => {
|
||||||
|
try {
|
||||||
|
const embeddingModels = {
|
||||||
|
'xenova-bge-small-en-v1.5': {
|
||||||
|
displayName: 'BGE Small',
|
||||||
|
model: new HuggingFaceTransformersEmbeddings({
|
||||||
|
modelName: 'Xenova/bge-small-en-v1.5',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'xenova-gte-small': {
|
||||||
|
displayName: 'GTE Small',
|
||||||
|
model: new HuggingFaceTransformersEmbeddings({
|
||||||
|
modelName: 'Xenova/gte-small',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'xenova-bert-base-multilingual-uncased': {
|
||||||
|
displayName: 'Bert Multilingual',
|
||||||
|
model: new HuggingFaceTransformersEmbeddings({
|
||||||
|
modelName: 'Xenova/bert-base-multilingual-uncased',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return embeddingModels;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Error loading Transformers embeddings model: ${err}`);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
@@ -1,5 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { getSearxngApiEndpoint } from './config';
|
import { getSearxngApiEndpoint } from '../config';
|
||||||
|
|
||||||
interface SearxngSearchOptions {
|
interface SearxngSearchOptions {
|
||||||
categories?: string[];
|
categories?: string[];
|
||||||
@@ -30,12 +30,11 @@ export const searchSearxng = async (
|
|||||||
|
|
||||||
if (opts) {
|
if (opts) {
|
||||||
Object.keys(opts).forEach((key) => {
|
Object.keys(opts).forEach((key) => {
|
||||||
const value = opts[key as keyof SearxngSearchOptions];
|
if (Array.isArray(opts[key])) {
|
||||||
if (Array.isArray(value)) {
|
url.searchParams.append(key, opts[key].join(','));
|
||||||
url.searchParams.append(key, value.join(','));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
url.searchParams.append(key, value as string);
|
url.searchParams.append(key, opts[key]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
5
src/lib/types/compute-dot.d.ts
vendored
5
src/lib/types/compute-dot.d.ts
vendored
@@ -1,5 +0,0 @@
|
|||||||
declare function computeDot(vectorA: number[], vectorB: number[]): number;
|
|
||||||
|
|
||||||
declare module 'compute-dot' {
|
|
||||||
export default computeDot;
|
|
||||||
}
|
|
66
src/routes/chats.ts
Normal file
66
src/routes/chats.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
import db from '../db/index';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { chats, messages } from '../db/schema';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', async (_, res) => {
|
||||||
|
try {
|
||||||
|
let chats = await db.query.chats.findMany();
|
||||||
|
|
||||||
|
chats = chats.reverse();
|
||||||
|
|
||||||
|
return res.status(200).json({ chats: chats });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ message: 'An error has occurred.' });
|
||||||
|
logger.error(`Error in getting chats: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const chatExists = await db.query.chats.findFirst({
|
||||||
|
where: eq(chats.id, req.params.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!chatExists) {
|
||||||
|
return res.status(404).json({ message: 'Chat not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatMessages = await db.query.messages.findMany({
|
||||||
|
where: eq(messages.chatId, req.params.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({ chat: chatExists, messages: chatMessages });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ message: 'An error has occurred.' });
|
||||||
|
logger.error(`Error in getting chat: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete(`/:id`, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const chatExists = await db.query.chats.findFirst({
|
||||||
|
where: eq(chats.id, req.params.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!chatExists) {
|
||||||
|
return res.status(404).json({ message: 'Chat not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.delete(chats).where(eq(chats.id, req.params.id)).execute();
|
||||||
|
await db
|
||||||
|
.delete(messages)
|
||||||
|
.where(eq(messages.chatId, req.params.id))
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return res.status(200).json({ message: 'Chat deleted successfully' });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ message: 'An error has occurred.' });
|
||||||
|
logger.error(`Error in deleting chat: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
139
src/routes/config.ts
Normal file
139
src/routes/config.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import {
|
||||||
|
getAvailableChatModelProviders,
|
||||||
|
getAvailableEmbeddingModelProviders,
|
||||||
|
} from '../lib/providers';
|
||||||
|
import {
|
||||||
|
getGroqApiKey,
|
||||||
|
getOllamaApiEndpoint,
|
||||||
|
getAnthropicApiKey,
|
||||||
|
getGeminiApiKey,
|
||||||
|
getOpenaiApiKey,
|
||||||
|
updateConfig,
|
||||||
|
getConfigPassword,
|
||||||
|
isLibraryEnabled,
|
||||||
|
isCopilotEnabled,
|
||||||
|
isDiscoverEnabled,
|
||||||
|
getCustomOpenaiApiUrl,
|
||||||
|
getCustomOpenaiApiKey,
|
||||||
|
getCustomOpenaiModelName,
|
||||||
|
} from '../config';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const authHeader = req.headers['authorization']?.split(' ')[1];
|
||||||
|
const password = getConfigPassword();
|
||||||
|
|
||||||
|
if (authHeader !== password) {
|
||||||
|
res.status(401).json({ message: 'Unauthorized' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {};
|
||||||
|
|
||||||
|
const [chatModelProviders, embeddingModelProviders] = await Promise.all([
|
||||||
|
getAvailableChatModelProviders(),
|
||||||
|
getAvailableEmbeddingModelProviders(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
config['chatModelProviders'] = {};
|
||||||
|
config['embeddingModelProviders'] = {};
|
||||||
|
|
||||||
|
for (const provider in chatModelProviders) {
|
||||||
|
config['chatModelProviders'][provider] = Object.keys(
|
||||||
|
chatModelProviders[provider],
|
||||||
|
).map((model) => {
|
||||||
|
return {
|
||||||
|
name: model,
|
||||||
|
displayName: chatModelProviders[provider][model].displayName,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const provider in embeddingModelProviders) {
|
||||||
|
config['embeddingModelProviders'][provider] = Object.keys(
|
||||||
|
embeddingModelProviders[provider],
|
||||||
|
).map((model) => {
|
||||||
|
return {
|
||||||
|
name: model,
|
||||||
|
displayName: embeddingModelProviders[provider][model].displayName,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
config['openaiApiKey'] = getOpenaiApiKey();
|
||||||
|
config['ollamaApiUrl'] = getOllamaApiEndpoint();
|
||||||
|
config['anthropicApiKey'] = getAnthropicApiKey();
|
||||||
|
config['groqApiKey'] = getGroqApiKey();
|
||||||
|
config['geminiApiKey'] = getGeminiApiKey();
|
||||||
|
config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl();
|
||||||
|
config['customOpenaiApiKey'] = getCustomOpenaiApiKey();
|
||||||
|
config['customOpenaiModelName'] = getCustomOpenaiModelName();
|
||||||
|
|
||||||
|
res.status(200).json(config);
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(500).json({ message: 'An error has occurred.' });
|
||||||
|
logger.error(`Error getting config: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', async (req, res) => {
|
||||||
|
const authHeader = req.headers['authorization']?.split(' ')[1];
|
||||||
|
const password = getConfigPassword();
|
||||||
|
|
||||||
|
if (authHeader !== password) {
|
||||||
|
res.status(401).json({ message: 'Unauthorized' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = req.body;
|
||||||
|
|
||||||
|
const updatedConfig = {
|
||||||
|
GENERAL: {
|
||||||
|
DISCOVER_ENABLED: config.isDiscoverEnabled,
|
||||||
|
LIBRARY_ENABLED: config.isLibraryEnabled,
|
||||||
|
COPILOT_ENABLED: config.isCopilotEnabled,
|
||||||
|
},
|
||||||
|
MODELS: {
|
||||||
|
OPENAI: {
|
||||||
|
API_KEY: config.openaiApiKey,
|
||||||
|
},
|
||||||
|
GROQ: {
|
||||||
|
API_KEY: config.groqApiKey,
|
||||||
|
},
|
||||||
|
ANTHROPIC: {
|
||||||
|
API_KEY: config.anthropicApiKey,
|
||||||
|
},
|
||||||
|
GEMINI: {
|
||||||
|
API_KEY: config.geminiApiKey,
|
||||||
|
},
|
||||||
|
OLLAMA: {
|
||||||
|
API_URL: config.ollamaApiUrl,
|
||||||
|
},
|
||||||
|
CUSTOM_OPENAI: {
|
||||||
|
API_URL: config.customOpenaiApiUrl,
|
||||||
|
API_KEY: config.customOpenaiApiKey,
|
||||||
|
MODEL_NAME: config.customOpenaiModelName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
updateConfig(updatedConfig);
|
||||||
|
|
||||||
|
res.status(200).json({ message: 'Config updated' });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/preferences', (_, res) => {
|
||||||
|
const preferences = {
|
||||||
|
isLibraryEnabled: isLibraryEnabled(),
|
||||||
|
isCopilotEnabled: isCopilotEnabled(),
|
||||||
|
isDiscoverEnabled: isDiscoverEnabled(),
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(preferences);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
48
src/routes/discover.ts
Normal file
48
src/routes/discover.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { searchSearxng } from '../lib/searxng';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const data = (
|
||||||
|
await Promise.all([
|
||||||
|
searchSearxng('site:businessinsider.com AI', {
|
||||||
|
engines: ['bing news'],
|
||||||
|
pageno: 1,
|
||||||
|
}),
|
||||||
|
searchSearxng('site:www.exchangewire.com AI', {
|
||||||
|
engines: ['bing news'],
|
||||||
|
pageno: 1,
|
||||||
|
}),
|
||||||
|
searchSearxng('site:yahoo.com AI', {
|
||||||
|
engines: ['bing news'],
|
||||||
|
pageno: 1,
|
||||||
|
}),
|
||||||
|
searchSearxng('site:businessinsider.com tech', {
|
||||||
|
engines: ['bing news'],
|
||||||
|
pageno: 1,
|
||||||
|
}),
|
||||||
|
searchSearxng('site:www.exchangewire.com tech', {
|
||||||
|
engines: ['bing news'],
|
||||||
|
pageno: 1,
|
||||||
|
}),
|
||||||
|
searchSearxng('site:yahoo.com tech', {
|
||||||
|
engines: ['bing news'],
|
||||||
|
pageno: 1,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.map((result) => result.results)
|
||||||
|
.flat()
|
||||||
|
.sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
|
return res.json({ blogs: data });
|
||||||
|
} catch (err: any) {
|
||||||
|
logger.error(`Error in discover route: ${err.message}`);
|
||||||
|
return res.status(500).json({ message: 'An error has occurred' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
82
src/routes/images.ts
Normal file
82
src/routes/images.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import handleImageSearch from '../chains/imageSearchAgent';
|
||||||
|
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
import { getAvailableChatModelProviders } from '../lib/providers';
|
||||||
|
import { HumanMessage, AIMessage } from '@langchain/core/messages';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
import { ChatOpenAI } from '@langchain/openai';
|
||||||
|
import {
|
||||||
|
getCustomOpenaiApiKey,
|
||||||
|
getCustomOpenaiApiUrl,
|
||||||
|
getCustomOpenaiModelName,
|
||||||
|
} from '../config';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
interface ChatModel {
|
||||||
|
provider: string;
|
||||||
|
model: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImageSearchBody {
|
||||||
|
query: string;
|
||||||
|
chatHistory: any[];
|
||||||
|
chatModel?: ChatModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
let body: ImageSearchBody = req.body;
|
||||||
|
|
||||||
|
const chatHistory = body.chatHistory.map((msg: any) => {
|
||||||
|
if (msg.role === 'user') {
|
||||||
|
return new HumanMessage(msg.content);
|
||||||
|
} else if (msg.role === 'assistant') {
|
||||||
|
return new AIMessage(msg.content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatModelProviders = await getAvailableChatModelProviders();
|
||||||
|
|
||||||
|
const chatModelProvider =
|
||||||
|
body.chatModel?.provider || Object.keys(chatModelProviders)[0];
|
||||||
|
const chatModel =
|
||||||
|
body.chatModel?.model ||
|
||||||
|
Object.keys(chatModelProviders[chatModelProvider])[0];
|
||||||
|
|
||||||
|
let llm: BaseChatModel | undefined;
|
||||||
|
|
||||||
|
if (body.chatModel?.provider === 'custom_openai') {
|
||||||
|
llm = new ChatOpenAI({
|
||||||
|
modelName: getCustomOpenaiModelName(),
|
||||||
|
openAIApiKey: getCustomOpenaiApiKey(),
|
||||||
|
temperature: 0.7,
|
||||||
|
configuration: {
|
||||||
|
baseURL: getCustomOpenaiApiUrl(),
|
||||||
|
},
|
||||||
|
}) as unknown as BaseChatModel;
|
||||||
|
} else if (
|
||||||
|
chatModelProviders[chatModelProvider] &&
|
||||||
|
chatModelProviders[chatModelProvider][chatModel]
|
||||||
|
) {
|
||||||
|
llm = chatModelProviders[chatModelProvider][chatModel]
|
||||||
|
.model as unknown as BaseChatModel | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!llm) {
|
||||||
|
return res.status(400).json({ message: 'Invalid model selected' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const images = await handleImageSearch(
|
||||||
|
{ query: body.query, chat_history: chatHistory },
|
||||||
|
llm,
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(200).json({ images });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ message: 'An error has occurred.' });
|
||||||
|
logger.error(`Error in image search: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
24
src/routes/index.ts
Normal file
24
src/routes/index.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import imagesRouter from './images';
|
||||||
|
import videosRouter from './videos';
|
||||||
|
import configRouter from './config';
|
||||||
|
import modelsRouter from './models';
|
||||||
|
import suggestionsRouter from './suggestions';
|
||||||
|
import chatsRouter from './chats';
|
||||||
|
import searchRouter from './search';
|
||||||
|
import discoverRouter from './discover';
|
||||||
|
import uploadsRouter from './uploads';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.use('/images', imagesRouter);
|
||||||
|
router.use('/videos', videosRouter);
|
||||||
|
router.use('/config', configRouter);
|
||||||
|
router.use('/models', modelsRouter);
|
||||||
|
router.use('/suggestions', suggestionsRouter);
|
||||||
|
router.use('/chats', chatsRouter);
|
||||||
|
router.use('/search', searchRouter);
|
||||||
|
router.use('/discover', discoverRouter);
|
||||||
|
router.use('/uploads', uploadsRouter);
|
||||||
|
|
||||||
|
export default router;
|
59
src/routes/models.ts
Normal file
59
src/routes/models.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
import {
|
||||||
|
getAvailableChatModelProviders,
|
||||||
|
getAvailableEmbeddingModelProviders,
|
||||||
|
} from '../lib/providers';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const [chatModelProvidersRaw, embeddingModelProvidersRaw] =
|
||||||
|
await Promise.all([
|
||||||
|
getAvailableChatModelProviders(),
|
||||||
|
getAvailableEmbeddingModelProviders(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const chatModelProviders = {};
|
||||||
|
|
||||||
|
const chatModelProvidersKeys = Object.keys(chatModelProvidersRaw);
|
||||||
|
chatModelProvidersKeys.forEach((provider) => {
|
||||||
|
chatModelProviders[provider] = {};
|
||||||
|
const models = Object.keys(chatModelProvidersRaw[provider]);
|
||||||
|
models.forEach((model) => {
|
||||||
|
chatModelProviders[provider][model] = {};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const embeddingModelProviders = {};
|
||||||
|
|
||||||
|
const embeddingModelProvidersKeys = Object.keys(embeddingModelProvidersRaw);
|
||||||
|
embeddingModelProvidersKeys.forEach((provider) => {
|
||||||
|
embeddingModelProviders[provider] = {};
|
||||||
|
const models = Object.keys(embeddingModelProvidersRaw[provider]);
|
||||||
|
models.forEach((model) => {
|
||||||
|
embeddingModelProviders[provider][model] = {};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(chatModelProviders).forEach((provider) => {
|
||||||
|
Object.keys(chatModelProviders[provider]).forEach((model) => {
|
||||||
|
delete chatModelProviders[provider][model].model;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(embeddingModelProviders).forEach((provider) => {
|
||||||
|
Object.keys(embeddingModelProviders[provider]).forEach((model) => {
|
||||||
|
delete embeddingModelProviders[provider][model].model;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({ chatModelProviders, embeddingModelProviders });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ message: 'An error has occurred.' });
|
||||||
|
logger.error(err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
158
src/routes/search.ts
Normal file
158
src/routes/search.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
import type { Embeddings } from '@langchain/core/embeddings';
|
||||||
|
import { ChatOpenAI } from '@langchain/openai';
|
||||||
|
import {
|
||||||
|
getAvailableChatModelProviders,
|
||||||
|
getAvailableEmbeddingModelProviders,
|
||||||
|
} from '../lib/providers';
|
||||||
|
import { searchHandlers } from '../websocket/messageHandler';
|
||||||
|
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
|
||||||
|
import { MetaSearchAgentType } from '../search/metaSearchAgent';
|
||||||
|
import {
|
||||||
|
getCustomOpenaiApiKey,
|
||||||
|
getCustomOpenaiApiUrl,
|
||||||
|
getCustomOpenaiModelName,
|
||||||
|
} from '../config';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
interface chatModel {
|
||||||
|
provider: string;
|
||||||
|
model: string;
|
||||||
|
customOpenAIKey?: string;
|
||||||
|
customOpenAIBaseURL?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface embeddingModel {
|
||||||
|
provider: string;
|
||||||
|
model: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatRequestBody {
|
||||||
|
optimizationMode: 'speed' | 'balanced';
|
||||||
|
focusMode: string;
|
||||||
|
chatModel?: chatModel;
|
||||||
|
embeddingModel?: embeddingModel;
|
||||||
|
query: string;
|
||||||
|
history: Array<[string, string]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const body: ChatRequestBody = req.body;
|
||||||
|
|
||||||
|
if (!body.focusMode || !body.query) {
|
||||||
|
return res.status(400).json({ message: 'Missing focus mode or query' });
|
||||||
|
}
|
||||||
|
|
||||||
|
body.history = body.history || [];
|
||||||
|
body.optimizationMode = body.optimizationMode || 'balanced';
|
||||||
|
|
||||||
|
const history: BaseMessage[] = body.history.map((msg) => {
|
||||||
|
if (msg[0] === 'human') {
|
||||||
|
return new HumanMessage({
|
||||||
|
content: msg[1],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new AIMessage({
|
||||||
|
content: msg[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const [chatModelProviders, embeddingModelProviders] = await Promise.all([
|
||||||
|
getAvailableChatModelProviders(),
|
||||||
|
getAvailableEmbeddingModelProviders(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const chatModelProvider =
|
||||||
|
body.chatModel?.provider || Object.keys(chatModelProviders)[0];
|
||||||
|
const chatModel =
|
||||||
|
body.chatModel?.model ||
|
||||||
|
Object.keys(chatModelProviders[chatModelProvider])[0];
|
||||||
|
|
||||||
|
const embeddingModelProvider =
|
||||||
|
body.embeddingModel?.provider || Object.keys(embeddingModelProviders)[0];
|
||||||
|
const embeddingModel =
|
||||||
|
body.embeddingModel?.model ||
|
||||||
|
Object.keys(embeddingModelProviders[embeddingModelProvider])[0];
|
||||||
|
|
||||||
|
let llm: BaseChatModel | undefined;
|
||||||
|
let embeddings: Embeddings | undefined;
|
||||||
|
|
||||||
|
if (body.chatModel?.provider === 'custom_openai') {
|
||||||
|
llm = new ChatOpenAI({
|
||||||
|
modelName: body.chatModel?.model || getCustomOpenaiModelName(),
|
||||||
|
openAIApiKey:
|
||||||
|
body.chatModel?.customOpenAIKey || getCustomOpenaiApiKey(),
|
||||||
|
temperature: 0.7,
|
||||||
|
configuration: {
|
||||||
|
baseURL:
|
||||||
|
body.chatModel?.customOpenAIBaseURL || getCustomOpenaiApiUrl(),
|
||||||
|
},
|
||||||
|
}) as unknown as BaseChatModel;
|
||||||
|
} else if (
|
||||||
|
chatModelProviders[chatModelProvider] &&
|
||||||
|
chatModelProviders[chatModelProvider][chatModel]
|
||||||
|
) {
|
||||||
|
llm = chatModelProviders[chatModelProvider][chatModel]
|
||||||
|
.model as unknown as BaseChatModel | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
embeddingModelProviders[embeddingModelProvider] &&
|
||||||
|
embeddingModelProviders[embeddingModelProvider][embeddingModel]
|
||||||
|
) {
|
||||||
|
embeddings = embeddingModelProviders[embeddingModelProvider][
|
||||||
|
embeddingModel
|
||||||
|
].model as Embeddings | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!llm || !embeddings) {
|
||||||
|
return res.status(400).json({ message: 'Invalid model selected' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchHandler: MetaSearchAgentType = searchHandlers[body.focusMode];
|
||||||
|
|
||||||
|
if (!searchHandler) {
|
||||||
|
return res.status(400).json({ message: 'Invalid focus mode' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const emitter = await searchHandler.searchAndAnswer(
|
||||||
|
body.query,
|
||||||
|
history,
|
||||||
|
llm,
|
||||||
|
embeddings,
|
||||||
|
body.optimizationMode,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
let message = '';
|
||||||
|
let sources = [];
|
||||||
|
|
||||||
|
emitter.on('data', (data) => {
|
||||||
|
const parsedData = JSON.parse(data);
|
||||||
|
if (parsedData.type === 'response') {
|
||||||
|
message += parsedData.data;
|
||||||
|
} else if (parsedData.type === 'sources') {
|
||||||
|
sources = parsedData.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('end', () => {
|
||||||
|
res.status(200).json({ message, sources });
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('error', (data) => {
|
||||||
|
const parsedData = JSON.parse(data);
|
||||||
|
res.status(500).json({ message: parsedData.data });
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
logger.error(`Error in getting search results: ${err.message}`);
|
||||||
|
res.status(500).json({ message: 'An error has occurred.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
81
src/routes/suggestions.ts
Normal file
81
src/routes/suggestions.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import generateSuggestions from '../chains/suggestionGeneratorAgent';
|
||||||
|
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
import { getAvailableChatModelProviders } from '../lib/providers';
|
||||||
|
import { HumanMessage, AIMessage } from '@langchain/core/messages';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
import { ChatOpenAI } from '@langchain/openai';
|
||||||
|
import {
|
||||||
|
getCustomOpenaiApiKey,
|
||||||
|
getCustomOpenaiApiUrl,
|
||||||
|
getCustomOpenaiModelName,
|
||||||
|
} from '../config';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
interface ChatModel {
|
||||||
|
provider: string;
|
||||||
|
model: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SuggestionsBody {
|
||||||
|
chatHistory: any[];
|
||||||
|
chatModel?: ChatModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
let body: SuggestionsBody = req.body;
|
||||||
|
|
||||||
|
const chatHistory = body.chatHistory.map((msg: any) => {
|
||||||
|
if (msg.role === 'user') {
|
||||||
|
return new HumanMessage(msg.content);
|
||||||
|
} else if (msg.role === 'assistant') {
|
||||||
|
return new AIMessage(msg.content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatModelProviders = await getAvailableChatModelProviders();
|
||||||
|
|
||||||
|
const chatModelProvider =
|
||||||
|
body.chatModel?.provider || Object.keys(chatModelProviders)[0];
|
||||||
|
const chatModel =
|
||||||
|
body.chatModel?.model ||
|
||||||
|
Object.keys(chatModelProviders[chatModelProvider])[0];
|
||||||
|
|
||||||
|
let llm: BaseChatModel | undefined;
|
||||||
|
|
||||||
|
if (body.chatModel?.provider === 'custom_openai') {
|
||||||
|
llm = new ChatOpenAI({
|
||||||
|
modelName: getCustomOpenaiModelName(),
|
||||||
|
openAIApiKey: getCustomOpenaiApiKey(),
|
||||||
|
temperature: 0.7,
|
||||||
|
configuration: {
|
||||||
|
baseURL: getCustomOpenaiApiUrl(),
|
||||||
|
},
|
||||||
|
}) as unknown as BaseChatModel;
|
||||||
|
} else if (
|
||||||
|
chatModelProviders[chatModelProvider] &&
|
||||||
|
chatModelProviders[chatModelProvider][chatModel]
|
||||||
|
) {
|
||||||
|
llm = chatModelProviders[chatModelProvider][chatModel]
|
||||||
|
.model as unknown as BaseChatModel | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!llm) {
|
||||||
|
return res.status(400).json({ message: 'Invalid model selected' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const suggestions = await generateSuggestions(
|
||||||
|
{ chat_history: chatHistory },
|
||||||
|
llm,
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(200).json({ suggestions: suggestions });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ message: 'An error has occurred.' });
|
||||||
|
logger.error(`Error in generating suggestions: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
151
src/routes/uploads.ts
Normal file
151
src/routes/uploads.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
import multer from 'multer';
|
||||||
|
import path from 'path';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { Embeddings } from '@langchain/core/embeddings';
|
||||||
|
import { getAvailableEmbeddingModelProviders } from '../lib/providers';
|
||||||
|
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf';
|
||||||
|
import { DocxLoader } from '@langchain/community/document_loaders/fs/docx';
|
||||||
|
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
|
||||||
|
import { Document } from 'langchain/document';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const splitter = new RecursiveCharacterTextSplitter({
|
||||||
|
chunkSize: 500,
|
||||||
|
chunkOverlap: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: (req, file, cb) => {
|
||||||
|
cb(null, path.join(process.cwd(), './uploads'));
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
const splitedFileName = file.originalname.split('.');
|
||||||
|
const fileExtension = splitedFileName[splitedFileName.length - 1];
|
||||||
|
if (!['pdf', 'docx', 'txt'].includes(fileExtension)) {
|
||||||
|
return cb(new Error('File type is not supported'), '');
|
||||||
|
}
|
||||||
|
cb(null, `${crypto.randomBytes(16).toString('hex')}.${fileExtension}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const upload = multer({ storage });
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/',
|
||||||
|
upload.fields([
|
||||||
|
{ name: 'files' },
|
||||||
|
{ name: 'embedding_model', maxCount: 1 },
|
||||||
|
{ name: 'embedding_model_provider', maxCount: 1 },
|
||||||
|
]),
|
||||||
|
async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { embedding_model, embedding_model_provider } = req.body;
|
||||||
|
|
||||||
|
if (!embedding_model || !embedding_model_provider) {
|
||||||
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ message: 'Missing embedding model or provider' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const embeddingModels = await getAvailableEmbeddingModelProviders();
|
||||||
|
const provider =
|
||||||
|
embedding_model_provider ?? Object.keys(embeddingModels)[0];
|
||||||
|
const embeddingModel: Embeddings =
|
||||||
|
embedding_model ?? Object.keys(embeddingModels[provider])[0];
|
||||||
|
|
||||||
|
let embeddingsModel: Embeddings | undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
embeddingModels[provider] &&
|
||||||
|
embeddingModels[provider][embeddingModel]
|
||||||
|
) {
|
||||||
|
embeddingsModel = embeddingModels[provider][embeddingModel].model as
|
||||||
|
| Embeddings
|
||||||
|
| undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!embeddingsModel) {
|
||||||
|
res.status(400).json({ message: 'Invalid LLM model selected' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = req.files['files'] as Express.Multer.File[];
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
res.status(400).json({ message: 'No files uploaded' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
files.map(async (file) => {
|
||||||
|
let docs: Document[] = [];
|
||||||
|
|
||||||
|
if (file.mimetype === 'application/pdf') {
|
||||||
|
const loader = new PDFLoader(file.path);
|
||||||
|
docs = await loader.load();
|
||||||
|
} else if (
|
||||||
|
file.mimetype ===
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||||
|
) {
|
||||||
|
const loader = new DocxLoader(file.path);
|
||||||
|
docs = await loader.load();
|
||||||
|
} else if (file.mimetype === 'text/plain') {
|
||||||
|
const text = fs.readFileSync(file.path, 'utf-8');
|
||||||
|
docs = [
|
||||||
|
new Document({
|
||||||
|
pageContent: text,
|
||||||
|
metadata: {
|
||||||
|
title: file.originalname,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitted = await splitter.splitDocuments(docs);
|
||||||
|
|
||||||
|
const json = JSON.stringify({
|
||||||
|
title: file.originalname,
|
||||||
|
contents: splitted.map((doc) => doc.pageContent),
|
||||||
|
});
|
||||||
|
|
||||||
|
const pathToSave = file.path.replace(/\.\w+$/, '-extracted.json');
|
||||||
|
fs.writeFileSync(pathToSave, json);
|
||||||
|
|
||||||
|
const embeddings = await embeddingsModel.embedDocuments(
|
||||||
|
splitted.map((doc) => doc.pageContent),
|
||||||
|
);
|
||||||
|
|
||||||
|
const embeddingsJSON = JSON.stringify({
|
||||||
|
title: file.originalname,
|
||||||
|
embeddings: embeddings,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pathToSaveEmbeddings = file.path.replace(
|
||||||
|
/\.\w+$/,
|
||||||
|
'-embeddings.json',
|
||||||
|
);
|
||||||
|
fs.writeFileSync(pathToSaveEmbeddings, embeddingsJSON);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
files: files.map((file) => {
|
||||||
|
return {
|
||||||
|
fileName: file.originalname,
|
||||||
|
fileExtension: file.filename.split('.').pop(),
|
||||||
|
fileId: file.filename.replace(/\.\w+$/, ''),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
logger.error(`Error in uploading file results: ${err.message}`);
|
||||||
|
res.status(500).json({ message: 'An error has occurred.' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
82
src/routes/videos.ts
Normal file
82
src/routes/videos.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
import { getAvailableChatModelProviders } from '../lib/providers';
|
||||||
|
import { HumanMessage, AIMessage } from '@langchain/core/messages';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
import handleVideoSearch from '../chains/videoSearchAgent';
|
||||||
|
import { ChatOpenAI } from '@langchain/openai';
|
||||||
|
import {
|
||||||
|
getCustomOpenaiApiKey,
|
||||||
|
getCustomOpenaiApiUrl,
|
||||||
|
getCustomOpenaiModelName,
|
||||||
|
} from '../config';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
interface ChatModel {
|
||||||
|
provider: string;
|
||||||
|
model: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VideoSearchBody {
|
||||||
|
query: string;
|
||||||
|
chatHistory: any[];
|
||||||
|
chatModel?: ChatModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
let body: VideoSearchBody = req.body;
|
||||||
|
|
||||||
|
const chatHistory = body.chatHistory.map((msg: any) => {
|
||||||
|
if (msg.role === 'user') {
|
||||||
|
return new HumanMessage(msg.content);
|
||||||
|
} else if (msg.role === 'assistant') {
|
||||||
|
return new AIMessage(msg.content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatModelProviders = await getAvailableChatModelProviders();
|
||||||
|
|
||||||
|
const chatModelProvider =
|
||||||
|
body.chatModel?.provider || Object.keys(chatModelProviders)[0];
|
||||||
|
const chatModel =
|
||||||
|
body.chatModel?.model ||
|
||||||
|
Object.keys(chatModelProviders[chatModelProvider])[0];
|
||||||
|
|
||||||
|
let llm: BaseChatModel | undefined;
|
||||||
|
|
||||||
|
if (body.chatModel?.provider === 'custom_openai') {
|
||||||
|
llm = new ChatOpenAI({
|
||||||
|
modelName: getCustomOpenaiModelName(),
|
||||||
|
openAIApiKey: getCustomOpenaiApiKey(),
|
||||||
|
temperature: 0.7,
|
||||||
|
configuration: {
|
||||||
|
baseURL: getCustomOpenaiApiUrl(),
|
||||||
|
},
|
||||||
|
}) as unknown as BaseChatModel;
|
||||||
|
} else if (
|
||||||
|
chatModelProviders[chatModelProvider] &&
|
||||||
|
chatModelProviders[chatModelProvider][chatModel]
|
||||||
|
) {
|
||||||
|
llm = chatModelProviders[chatModelProvider][chatModel]
|
||||||
|
.model as unknown as BaseChatModel | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!llm) {
|
||||||
|
return res.status(400).json({ message: 'Invalid model selected' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const videos = await handleVideoSearch(
|
||||||
|
{ chat_history: chatHistory, query: body.query },
|
||||||
|
llm,
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(200).json({ videos });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ message: 'An error has occurred.' });
|
||||||
|
logger.error(`Error in video search: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
@@ -13,17 +13,18 @@ import {
|
|||||||
} from '@langchain/core/runnables';
|
} from '@langchain/core/runnables';
|
||||||
import { BaseMessage } from '@langchain/core/messages';
|
import { BaseMessage } from '@langchain/core/messages';
|
||||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
import { StringOutputParser } from '@langchain/core/output_parsers';
|
||||||
import LineListOutputParser from '../outputParsers/listLineOutputParser';
|
import LineListOutputParser from '../lib/outputParsers/listLineOutputParser';
|
||||||
import LineOutputParser from '../outputParsers/lineOutputParser';
|
import LineOutputParser from '../lib/outputParsers/lineOutputParser';
|
||||||
import { getDocumentsFromLinks } from '../utils/documents';
|
import { getDocumentsFromLinks } from '../utils/documents';
|
||||||
import { Document } from 'langchain/document';
|
import { Document } from 'langchain/document';
|
||||||
import { searchSearxng } from '../searxng';
|
import { searchSearxng } from '../lib/searxng';
|
||||||
import path from 'node:path';
|
import path from 'path';
|
||||||
import fs from 'node:fs';
|
import fs from 'fs';
|
||||||
import computeSimilarity from '../utils/computeSimilarity';
|
import computeSimilarity from '../utils/computeSimilarity';
|
||||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
import formatChatHistoryAsString from '../utils/formatHistory';
|
||||||
import eventEmitter from 'events';
|
import eventEmitter from 'events';
|
||||||
import { StreamEvent } from '@langchain/core/tracers/log_stream';
|
import { StreamEvent } from '@langchain/core/tracers/log_stream';
|
||||||
|
import { IterableReadableStream } from '@langchain/core/utils/stream';
|
||||||
|
|
||||||
export interface MetaSearchAgentType {
|
export interface MetaSearchAgentType {
|
||||||
searchAndAnswer: (
|
searchAndAnswer: (
|
||||||
@@ -89,7 +90,7 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|||||||
question = 'summarize';
|
question = 'summarize';
|
||||||
}
|
}
|
||||||
|
|
||||||
let docs: Document[] = [];
|
let docs = [];
|
||||||
|
|
||||||
const linkDocs = await getDocumentsFromLinks({ links });
|
const linkDocs = await getDocumentsFromLinks({ links });
|
||||||
|
|
||||||
@@ -310,7 +311,7 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|||||||
const embeddings = JSON.parse(fs.readFileSync(embeddingsPath, 'utf8'));
|
const embeddings = JSON.parse(fs.readFileSync(embeddingsPath, 'utf8'));
|
||||||
|
|
||||||
const fileSimilaritySearchObject = content.contents.map(
|
const fileSimilaritySearchObject = content.contents.map(
|
||||||
(c: string, i: number) => {
|
(c: string, i) => {
|
||||||
return {
|
return {
|
||||||
fileName: content.title,
|
fileName: content.title,
|
||||||
content: c,
|
content: c,
|
||||||
@@ -413,8 +414,6 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|||||||
|
|
||||||
return sortedDocs;
|
return sortedDocs;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private processDocs(docs: Document[]) {
|
private processDocs(docs: Document[]) {
|
||||||
@@ -427,7 +426,7 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async handleStream(
|
private async handleStream(
|
||||||
stream: AsyncGenerator<StreamEvent, any, any>,
|
stream: IterableReadableStream<StreamEvent>,
|
||||||
emitter: eventEmitter,
|
emitter: eventEmitter,
|
||||||
) {
|
) {
|
||||||
for await (const event of stream) {
|
for await (const event of stream) {
|
@@ -6,7 +6,7 @@ const computeSimilarity = (x: number[], y: number[]): number => {
|
|||||||
const similarityMeasure = getSimilarityMeasure();
|
const similarityMeasure = getSimilarityMeasure();
|
||||||
|
|
||||||
if (similarityMeasure === 'cosine') {
|
if (similarityMeasure === 'cosine') {
|
||||||
return cosineSimilarity(x, y) as number;
|
return cosineSimilarity(x, y);
|
||||||
} else if (similarityMeasure === 'dot') {
|
} else if (similarityMeasure === 'dot') {
|
||||||
return dot(x, y);
|
return dot(x, y);
|
||||||
}
|
}
|
@@ -3,6 +3,7 @@ import { htmlToText } from 'html-to-text';
|
|||||||
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
|
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
|
||||||
import { Document } from '@langchain/core/documents';
|
import { Document } from '@langchain/core/documents';
|
||||||
import pdfParse from 'pdf-parse';
|
import pdfParse from 'pdf-parse';
|
||||||
|
import logger from './logger';
|
||||||
|
|
||||||
export const getDocumentsFromLinks = async ({ links }: { links: string[] }) => {
|
export const getDocumentsFromLinks = async ({ links }: { links: string[] }) => {
|
||||||
const splitter = new RecursiveCharacterTextSplitter();
|
const splitter = new RecursiveCharacterTextSplitter();
|
||||||
@@ -78,13 +79,12 @@ export const getDocumentsFromLinks = async ({ links }: { links: string[] }) => {
|
|||||||
|
|
||||||
docs.push(...linkDocs);
|
docs.push(...linkDocs);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(
|
logger.error(
|
||||||
'An error occurred while getting documents from links: ',
|
`Error at generating documents from links: ${err.message}`,
|
||||||
err,
|
|
||||||
);
|
);
|
||||||
docs.push(
|
docs.push(
|
||||||
new Document({
|
new Document({
|
||||||
pageContent: `Failed to retrieve content from the link: ${err}`,
|
pageContent: `Failed to retrieve content from the link: ${err.message}`,
|
||||||
metadata: {
|
metadata: {
|
||||||
title: 'Failed to retrieve content',
|
title: 'Failed to retrieve content',
|
||||||
url: link,
|
url: link,
|
22
src/utils/logger.ts
Normal file
22
src/utils/logger.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import winston from 'winston';
|
||||||
|
|
||||||
|
const logger = winston.createLogger({
|
||||||
|
level: 'info',
|
||||||
|
transports: [
|
||||||
|
new winston.transports.Console({
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.colorize(),
|
||||||
|
winston.format.simple(),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: 'app.log',
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.timestamp(),
|
||||||
|
winston.format.json(),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default logger;
|
122
src/websocket/connectionManager.ts
Normal file
122
src/websocket/connectionManager.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { WebSocket } from 'ws';
|
||||||
|
import { handleMessage } from './messageHandler';
|
||||||
|
import {
|
||||||
|
getAvailableEmbeddingModelProviders,
|
||||||
|
getAvailableChatModelProviders,
|
||||||
|
} from '../lib/providers';
|
||||||
|
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
import type { Embeddings } from '@langchain/core/embeddings';
|
||||||
|
import type { IncomingMessage } from 'http';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
import { ChatOpenAI } from '@langchain/openai';
|
||||||
|
import {
|
||||||
|
getCustomOpenaiApiKey,
|
||||||
|
getCustomOpenaiApiUrl,
|
||||||
|
getCustomOpenaiModelName,
|
||||||
|
} from '../config';
|
||||||
|
|
||||||
|
export const handleConnection = async (
|
||||||
|
ws: WebSocket,
|
||||||
|
request: IncomingMessage,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const searchParams = new URL(request.url, `http://${request.headers.host}`)
|
||||||
|
.searchParams;
|
||||||
|
|
||||||
|
const [chatModelProviders, embeddingModelProviders] = await Promise.all([
|
||||||
|
getAvailableChatModelProviders(),
|
||||||
|
getAvailableEmbeddingModelProviders(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const chatModelProvider =
|
||||||
|
searchParams.get('chatModelProvider') ||
|
||||||
|
Object.keys(chatModelProviders)[0];
|
||||||
|
const chatModel =
|
||||||
|
searchParams.get('chatModel') ||
|
||||||
|
Object.keys(chatModelProviders[chatModelProvider])[0];
|
||||||
|
|
||||||
|
const embeddingModelProvider =
|
||||||
|
searchParams.get('embeddingModelProvider') ||
|
||||||
|
Object.keys(embeddingModelProviders)[0];
|
||||||
|
const embeddingModel =
|
||||||
|
searchParams.get('embeddingModel') ||
|
||||||
|
Object.keys(embeddingModelProviders[embeddingModelProvider])[0];
|
||||||
|
|
||||||
|
let llm: BaseChatModel | undefined;
|
||||||
|
let embeddings: Embeddings | undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
chatModelProviders[chatModelProvider] &&
|
||||||
|
chatModelProviders[chatModelProvider][chatModel] &&
|
||||||
|
chatModelProvider != 'custom_openai'
|
||||||
|
) {
|
||||||
|
llm = chatModelProviders[chatModelProvider][chatModel]
|
||||||
|
.model as unknown as BaseChatModel | undefined;
|
||||||
|
} else if (chatModelProvider == 'custom_openai') {
|
||||||
|
const customOpenaiApiKey = getCustomOpenaiApiKey();
|
||||||
|
const customOpenaiApiUrl = getCustomOpenaiApiUrl();
|
||||||
|
const customOpenaiModelName = getCustomOpenaiModelName();
|
||||||
|
|
||||||
|
if (customOpenaiApiKey && customOpenaiApiUrl && customOpenaiModelName) {
|
||||||
|
llm = new ChatOpenAI({
|
||||||
|
modelName: customOpenaiModelName,
|
||||||
|
openAIApiKey: customOpenaiApiKey,
|
||||||
|
temperature: 0.7,
|
||||||
|
configuration: {
|
||||||
|
baseURL: customOpenaiApiUrl,
|
||||||
|
},
|
||||||
|
}) as unknown as BaseChatModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
embeddingModelProviders[embeddingModelProvider] &&
|
||||||
|
embeddingModelProviders[embeddingModelProvider][embeddingModel]
|
||||||
|
) {
|
||||||
|
embeddings = embeddingModelProviders[embeddingModelProvider][
|
||||||
|
embeddingModel
|
||||||
|
].model as Embeddings | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!llm || !embeddings) {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'error',
|
||||||
|
data: 'Invalid LLM or embeddings model selected, please refresh the page and try again.',
|
||||||
|
key: 'INVALID_MODEL_SELECTED',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (ws.readyState === ws.OPEN) {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'signal',
|
||||||
|
data: 'open',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, 5);
|
||||||
|
|
||||||
|
ws.on(
|
||||||
|
'message',
|
||||||
|
async (message) =>
|
||||||
|
await handleMessage(message.toString(), ws, llm, embeddings),
|
||||||
|
);
|
||||||
|
|
||||||
|
ws.on('close', () => logger.debug('Connection closed'));
|
||||||
|
} catch (err) {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'error',
|
||||||
|
data: 'Internal server error.',
|
||||||
|
key: 'INTERNAL_SERVER_ERROR',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
ws.close();
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
};
|
8
src/websocket/index.ts
Normal file
8
src/websocket/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { initServer } from './websocketServer';
|
||||||
|
import http from 'http';
|
||||||
|
|
||||||
|
export const startWebSocketServer = (
|
||||||
|
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||||
|
) => {
|
||||||
|
initServer(server);
|
||||||
|
};
|
281
src/websocket/messageHandler.ts
Normal file
281
src/websocket/messageHandler.ts
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import { EventEmitter, WebSocket } from 'ws';
|
||||||
|
import { BaseMessage, AIMessage, HumanMessage } from '@langchain/core/messages';
|
||||||
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
import type { Embeddings } from '@langchain/core/embeddings';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
import db from '../db';
|
||||||
|
import { chats, messages as messagesSchema } from '../db/schema';
|
||||||
|
import { eq, gt, and } from 'drizzle-orm';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import { isLibraryEnabled } from '../config';
|
||||||
|
import { getFileDetails } from '../utils/files';
|
||||||
|
import MetaSearchAgent, {
|
||||||
|
MetaSearchAgentType,
|
||||||
|
} from '../search/metaSearchAgent';
|
||||||
|
import prompts from '../prompts';
|
||||||
|
|
||||||
|
type Message = {
|
||||||
|
messageId: string;
|
||||||
|
chatId: string;
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WSMessage = {
|
||||||
|
message: Message;
|
||||||
|
optimizationMode: 'speed' | 'balanced' | 'quality';
|
||||||
|
type: string;
|
||||||
|
focusMode: string;
|
||||||
|
history: Array<[string, string]>;
|
||||||
|
files: Array<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchHandlers = {
|
||||||
|
webSearch: new MetaSearchAgent({
|
||||||
|
activeEngines: [],
|
||||||
|
queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
|
||||||
|
responsePrompt: prompts.webSearchResponsePrompt,
|
||||||
|
rerank: true,
|
||||||
|
rerankThreshold: 0.3,
|
||||||
|
searchWeb: true,
|
||||||
|
summarizer: true,
|
||||||
|
}),
|
||||||
|
academicSearch: new MetaSearchAgent({
|
||||||
|
activeEngines: ['arxiv', 'google scholar', 'pubmed'],
|
||||||
|
queryGeneratorPrompt: prompts.academicSearchRetrieverPrompt,
|
||||||
|
responsePrompt: prompts.academicSearchResponsePrompt,
|
||||||
|
rerank: true,
|
||||||
|
rerankThreshold: 0,
|
||||||
|
searchWeb: true,
|
||||||
|
summarizer: false,
|
||||||
|
}),
|
||||||
|
writingAssistant: new MetaSearchAgent({
|
||||||
|
activeEngines: [],
|
||||||
|
queryGeneratorPrompt: '',
|
||||||
|
responsePrompt: prompts.writingAssistantPrompt,
|
||||||
|
rerank: true,
|
||||||
|
rerankThreshold: 0,
|
||||||
|
searchWeb: false,
|
||||||
|
summarizer: false,
|
||||||
|
}),
|
||||||
|
wolframAlphaSearch: new MetaSearchAgent({
|
||||||
|
activeEngines: ['wolframalpha'],
|
||||||
|
queryGeneratorPrompt: prompts.wolframAlphaSearchRetrieverPrompt,
|
||||||
|
responsePrompt: prompts.wolframAlphaSearchResponsePrompt,
|
||||||
|
rerank: false,
|
||||||
|
rerankThreshold: 0,
|
||||||
|
searchWeb: true,
|
||||||
|
summarizer: false,
|
||||||
|
}),
|
||||||
|
youtubeSearch: new MetaSearchAgent({
|
||||||
|
activeEngines: ['youtube'],
|
||||||
|
queryGeneratorPrompt: prompts.youtubeSearchRetrieverPrompt,
|
||||||
|
responsePrompt: prompts.youtubeSearchResponsePrompt,
|
||||||
|
rerank: true,
|
||||||
|
rerankThreshold: 0.3,
|
||||||
|
searchWeb: true,
|
||||||
|
summarizer: false,
|
||||||
|
}),
|
||||||
|
redditSearch: new MetaSearchAgent({
|
||||||
|
activeEngines: ['reddit'],
|
||||||
|
queryGeneratorPrompt: prompts.redditSearchRetrieverPrompt,
|
||||||
|
responsePrompt: prompts.redditSearchResponsePrompt,
|
||||||
|
rerank: true,
|
||||||
|
rerankThreshold: 0.3,
|
||||||
|
searchWeb: true,
|
||||||
|
summarizer: false,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEmitterEvents = (
|
||||||
|
emitter: EventEmitter,
|
||||||
|
ws: WebSocket,
|
||||||
|
messageId: string,
|
||||||
|
chatId: string,
|
||||||
|
) => {
|
||||||
|
let recievedMessage = '';
|
||||||
|
let sources = [];
|
||||||
|
|
||||||
|
const libraryEnabled = isLibraryEnabled();
|
||||||
|
|
||||||
|
emitter.on('data', (data) => {
|
||||||
|
const parsedData = JSON.parse(data);
|
||||||
|
if (parsedData.type === 'response') {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'message',
|
||||||
|
data: parsedData.data,
|
||||||
|
messageId: messageId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
recievedMessage += parsedData.data;
|
||||||
|
} else if (parsedData.type === 'sources') {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'sources',
|
||||||
|
data: parsedData.data,
|
||||||
|
messageId: messageId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
sources = parsedData.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
emitter.on('end', () => {
|
||||||
|
ws.send(JSON.stringify({ type: 'messageEnd', messageId: messageId }));
|
||||||
|
|
||||||
|
if (libraryEnabled) {
|
||||||
|
db.insert(messagesSchema)
|
||||||
|
.values({
|
||||||
|
content: recievedMessage,
|
||||||
|
chatId: chatId,
|
||||||
|
messageId: messageId,
|
||||||
|
role: 'assistant',
|
||||||
|
metadata: JSON.stringify({
|
||||||
|
createdAt: new Date(),
|
||||||
|
...(sources && sources.length > 0 && { sources }),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
emitter.on('error', (data) => {
|
||||||
|
const parsedData = JSON.parse(data);
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'error',
|
||||||
|
data: parsedData.data,
|
||||||
|
key: 'CHAIN_ERROR',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleMessage = async (
|
||||||
|
message: string,
|
||||||
|
ws: WebSocket,
|
||||||
|
llm: BaseChatModel,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const parsedWSMessage = JSON.parse(message) as WSMessage;
|
||||||
|
const parsedMessage = parsedWSMessage.message;
|
||||||
|
|
||||||
|
if (parsedWSMessage.files.length > 0) {
|
||||||
|
/* TODO: Implement uploads in other classes/single meta class system*/
|
||||||
|
parsedWSMessage.focusMode = 'webSearch';
|
||||||
|
}
|
||||||
|
|
||||||
|
const humanMessageId =
|
||||||
|
parsedMessage.messageId ?? crypto.randomBytes(7).toString('hex');
|
||||||
|
const aiMessageId = crypto.randomBytes(7).toString('hex');
|
||||||
|
|
||||||
|
if (!parsedMessage.content)
|
||||||
|
return ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'error',
|
||||||
|
data: 'Invalid message format',
|
||||||
|
key: 'INVALID_FORMAT',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const history: BaseMessage[] = parsedWSMessage.history.map((msg) => {
|
||||||
|
if (msg[0] === 'human') {
|
||||||
|
return new HumanMessage({
|
||||||
|
content: msg[1],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new AIMessage({
|
||||||
|
content: msg[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parsedWSMessage.type === 'message') {
|
||||||
|
const handler: MetaSearchAgentType =
|
||||||
|
searchHandlers[parsedWSMessage.focusMode];
|
||||||
|
|
||||||
|
const libraryEnabled = isLibraryEnabled();
|
||||||
|
|
||||||
|
if (handler) {
|
||||||
|
try {
|
||||||
|
const emitter = await handler.searchAndAnswer(
|
||||||
|
parsedMessage.content,
|
||||||
|
history,
|
||||||
|
llm,
|
||||||
|
embeddings,
|
||||||
|
parsedWSMessage.optimizationMode,
|
||||||
|
parsedWSMessage.files,
|
||||||
|
);
|
||||||
|
|
||||||
|
handleEmitterEvents(emitter, ws, aiMessageId, parsedMessage.chatId);
|
||||||
|
|
||||||
|
if (libraryEnabled) {
|
||||||
|
const chat = await db.query.chats.findFirst({
|
||||||
|
where: eq(chats.id, parsedMessage.chatId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!chat) {
|
||||||
|
await db
|
||||||
|
.insert(chats)
|
||||||
|
.values({
|
||||||
|
id: parsedMessage.chatId,
|
||||||
|
title: parsedMessage.content,
|
||||||
|
createdAt: new Date().toString(),
|
||||||
|
focusMode: parsedWSMessage.focusMode,
|
||||||
|
files: parsedWSMessage.files.map(getFileDetails),
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageExists = await db.query.messages.findFirst({
|
||||||
|
where: eq(messagesSchema.messageId, humanMessageId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!messageExists) {
|
||||||
|
await db
|
||||||
|
.insert(messagesSchema)
|
||||||
|
.values({
|
||||||
|
content: parsedMessage.content,
|
||||||
|
chatId: parsedMessage.chatId,
|
||||||
|
messageId: humanMessageId,
|
||||||
|
role: 'user',
|
||||||
|
metadata: JSON.stringify({
|
||||||
|
createdAt: new Date(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
} else {
|
||||||
|
await db
|
||||||
|
.delete(messagesSchema)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
gt(messagesSchema.id, messageExists.id),
|
||||||
|
eq(messagesSchema.chatId, parsedMessage.chatId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'error',
|
||||||
|
data: 'Invalid focus mode',
|
||||||
|
key: 'INVALID_FOCUS_MODE',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'error',
|
||||||
|
data: 'Invalid message format',
|
||||||
|
key: 'INVALID_FORMAT',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
logger.error(`Failed to handle message: ${err}`);
|
||||||
|
}
|
||||||
|
};
|
16
src/websocket/websocketServer.ts
Normal file
16
src/websocket/websocketServer.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { WebSocketServer } from 'ws';
|
||||||
|
import { handleConnection } from './connectionManager';
|
||||||
|
import http from 'http';
|
||||||
|
import { getPort } from '../config';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
|
export const initServer = (
|
||||||
|
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||||
|
) => {
|
||||||
|
const port = getPort();
|
||||||
|
const wss = new WebSocketServer({ server });
|
||||||
|
|
||||||
|
wss.on('connection', handleConnection);
|
||||||
|
|
||||||
|
logger.info(`WebSocket server started on port ${port}`);
|
||||||
|
};
|
@@ -1,27 +1,18 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["ESNext"],
|
||||||
"allowJs": true,
|
"module": "Node16",
|
||||||
"skipLibCheck": true,
|
"moduleResolution": "Node16",
|
||||||
"strict": true,
|
"target": "ESNext",
|
||||||
"noEmit": true,
|
"outDir": "dist",
|
||||||
|
"sourceMap": false,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"experimentalDecorators": true,
|
||||||
"moduleResolution": "bundler",
|
"emitDecoratorMetadata": true,
|
||||||
"resolveJsonModule": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"isolatedModules": true,
|
"skipLibCheck": true,
|
||||||
"jsx": "preserve",
|
"skipDefaultLibCheck": true
|
||||||
"incremental": true,
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "next"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
},
|
|
||||||
"target": "ES2017"
|
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["src"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules", "**/*.spec.ts"]
|
||||||
}
|
}
|
||||||
|
2
ui/.env.example
Normal file
2
ui/.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
NEXT_PUBLIC_WS_URL=ws://localhost:3001
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:3001/api
|
34
ui/.gitignore
vendored
Normal file
34
ui/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
11
ui/.prettierrc.js
Normal file
11
ui/.prettierrc.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/** @type {import("prettier").Config} */
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
printWidth: 80,
|
||||||
|
trailingComma: 'all',
|
||||||
|
endOfLine: 'auto',
|
||||||
|
singleQuote: true,
|
||||||
|
tabWidth: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
7
ui/app/c/[chatId]/page.tsx
Normal file
7
ui/app/c/[chatId]/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import ChatWindow from '@/components/ChatWindow';
|
||||||
|
|
||||||
|
const Page = ({ params }: { params: { chatId: string } }) => {
|
||||||
|
return <ChatWindow id={params.chatId} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
@@ -19,7 +19,7 @@ const Page = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/discover`, {
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/discover`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
36
ui/app/library/layout.tsx
Normal file
36
ui/app/library/layout.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Library - Perplexica',
|
||||||
|
};
|
||||||
|
|
||||||
|
const Layout = async ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const res = await fetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/config/preferences`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
const { isLibraryEnabled } = data;
|
||||||
|
|
||||||
|
if (!isLibraryEnabled) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row items-center justify-center min-h-screen w-full">
|
||||||
|
<p className="text-lg dark:text-white/70 text-black/70">
|
||||||
|
Library is disabled
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>{children}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import DeleteChat from '@/components/DeleteChat';
|
import DeleteChat from '@/components/DeleteChat';
|
||||||
import { cn, formatTimeDifference } from '@/lib/utils';
|
import { formatTimeDifference } from '@/lib/utils';
|
||||||
import { BookOpenText, ClockIcon, Delete, ScanEye } from 'lucide-react';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { BookOpenText, ClockIcon } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ const Page = () => {
|
|||||||
const fetchChats = async () => {
|
const fetchChats = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const res = await fetch(`/api/chats`, {
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/chats`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
@@ -113,12 +113,90 @@ const Page = () => {
|
|||||||
const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false);
|
const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false);
|
||||||
const [savingStates, setSavingStates] = useState<Record<string, boolean>>({});
|
const [savingStates, setSavingStates] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [passwordSubmitted, setPasswordSubmitted] = useState(false);
|
||||||
|
const [isPasswordValid, setIsPasswordValid] = useState(true);
|
||||||
|
|
||||||
|
const handlePasswordSubmit = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setPasswordSubmitted(true);
|
||||||
|
|
||||||
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${password}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 401) {
|
||||||
|
setIsPasswordValid(false);
|
||||||
|
setPasswordSubmitted(false);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setIsPasswordValid(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await res.json()) as SettingsType;
|
||||||
|
setConfig(data);
|
||||||
|
|
||||||
|
const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {});
|
||||||
|
const embeddingModelProvidersKeys = Object.keys(
|
||||||
|
data.embeddingModelProviders || {},
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultChatModelProvider =
|
||||||
|
chatModelProvidersKeys.length > 0 ? chatModelProvidersKeys[0] : '';
|
||||||
|
const defaultEmbeddingModelProvider =
|
||||||
|
embeddingModelProvidersKeys.length > 0
|
||||||
|
? embeddingModelProvidersKeys[0]
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const chatModelProvider =
|
||||||
|
localStorage.getItem('chatModelProvider') ||
|
||||||
|
defaultChatModelProvider ||
|
||||||
|
'';
|
||||||
|
const chatModel =
|
||||||
|
localStorage.getItem('chatModel') ||
|
||||||
|
(data.chatModelProviders &&
|
||||||
|
data.chatModelProviders[chatModelProvider]?.length > 0
|
||||||
|
? data.chatModelProviders[chatModelProvider][0].name
|
||||||
|
: undefined) ||
|
||||||
|
'';
|
||||||
|
const embeddingModelProvider =
|
||||||
|
localStorage.getItem('embeddingModelProvider') ||
|
||||||
|
defaultEmbeddingModelProvider ||
|
||||||
|
'';
|
||||||
|
const embeddingModel =
|
||||||
|
localStorage.getItem('embeddingModel') ||
|
||||||
|
(data.embeddingModelProviders &&
|
||||||
|
data.embeddingModelProviders[embeddingModelProvider]?.[0].name) ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
setSelectedChatModelProvider(chatModelProvider);
|
||||||
|
setSelectedChatModel(chatModel);
|
||||||
|
setSelectedEmbeddingModelProvider(embeddingModelProvider);
|
||||||
|
setSelectedEmbeddingModel(embeddingModel);
|
||||||
|
setChatModels(data.chatModelProviders || {});
|
||||||
|
setEmbeddingModels(data.embeddingModelProviders || {});
|
||||||
|
|
||||||
|
setAutomaticImageSearch(
|
||||||
|
localStorage.getItem('autoImageSearch') === 'true',
|
||||||
|
);
|
||||||
|
setAutomaticVideoSearch(
|
||||||
|
localStorage.getItem('autoVideoSearch') === 'true',
|
||||||
|
);
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchConfig = async () => {
|
const fetchConfig = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const res = await fetch(`/api/config`, {
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${password}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -187,13 +265,21 @@ const Page = () => {
|
|||||||
[key]: value,
|
[key]: value,
|
||||||
} as SettingsType;
|
} as SettingsType;
|
||||||
|
|
||||||
const response = await fetch(`/api/config`, {
|
const response = await fetch(
|
||||||
method: 'POST',
|
`${process.env.NEXT_PUBLIC_API_URL}/config`,
|
||||||
headers: {
|
{
|
||||||
'Content-Type': 'application/json',
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${password}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(updatedConfig),
|
||||||
},
|
},
|
||||||
body: JSON.stringify(updatedConfig),
|
);
|
||||||
});
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to update config');
|
throw new Error('Failed to update config');
|
||||||
@@ -205,9 +291,10 @@ const Page = () => {
|
|||||||
key.toLowerCase().includes('api') ||
|
key.toLowerCase().includes('api') ||
|
||||||
key.toLowerCase().includes('url')
|
key.toLowerCase().includes('url')
|
||||||
) {
|
) {
|
||||||
const res = await fetch(`/api/config`, {
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${password}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -373,6 +460,33 @@ const Page = () => {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
) : !passwordSubmitted ? (
|
||||||
|
<div className="flex flex-col max-w-md mx-auto mt-10 p-6 bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 rounded-2xl">
|
||||||
|
<h2 className="text-sm text-black/80 dark:text-white/80">
|
||||||
|
Enter the password to access the settings
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
className="mt-4"
|
||||||
|
disabled={isLoading}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!isPasswordValid && (
|
||||||
|
<p className="text-xs text-red-500 mt-2">
|
||||||
|
Password is incorrect
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={handlePasswordSubmit}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="bg-[#24A0ED] flex flex-row items-center text-xs mt-4 text-white disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#ececec21] rounded-full px-4 py-2"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
config && (
|
config && (
|
||||||
<div className="flex flex-col space-y-6 pb-28 lg:pb-8">
|
<div className="flex flex-col space-y-6 pb-28 lg:pb-8">
|
@@ -48,17 +48,11 @@ const Chat = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scroll = () => {
|
messageEnd.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
messageEnd.current?.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
};
|
|
||||||
|
|
||||||
if (messages.length === 1) {
|
if (messages.length === 1) {
|
||||||
document.title = `${messages[0].content.substring(0, 30)} - Perplexica`;
|
document.title = `${messages[0].content.substring(0, 30)} - Perplexica`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messages[messages.length - 1]?.role == 'user') {
|
|
||||||
scroll();
|
|
||||||
}
|
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
return (
|
return (
|
@@ -29,154 +29,280 @@ export interface File {
|
|||||||
fileId: string;
|
fileId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatModelProvider {
|
const useSocket = (
|
||||||
name: string;
|
url: string,
|
||||||
provider: string;
|
setIsWSReady: (ready: boolean) => void,
|
||||||
}
|
setError: (error: boolean) => void,
|
||||||
|
|
||||||
interface EmbeddingModelProvider {
|
|
||||||
name: string;
|
|
||||||
provider: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkConfig = async (
|
|
||||||
setChatModelProvider: (provider: ChatModelProvider) => void,
|
|
||||||
setEmbeddingModelProvider: (provider: EmbeddingModelProvider) => void,
|
|
||||||
setIsConfigReady: (ready: boolean) => void,
|
|
||||||
setHasError: (hasError: boolean) => void,
|
|
||||||
) => {
|
) => {
|
||||||
try {
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
let chatModel = localStorage.getItem('chatModel');
|
const reconnectTimeoutRef = useRef<NodeJS.Timeout>();
|
||||||
let chatModelProvider = localStorage.getItem('chatModelProvider');
|
const retryCountRef = useRef(0);
|
||||||
let embeddingModel = localStorage.getItem('embeddingModel');
|
const isCleaningUpRef = useRef(false);
|
||||||
let embeddingModelProvider = localStorage.getItem('embeddingModelProvider');
|
const MAX_RETRIES = 3;
|
||||||
|
const INITIAL_BACKOFF = 1000; // 1 second
|
||||||
|
const isConnectionErrorRef = useRef(false);
|
||||||
|
|
||||||
const autoImageSearch = localStorage.getItem('autoImageSearch');
|
const getBackoffDelay = (retryCount: number) => {
|
||||||
const autoVideoSearch = localStorage.getItem('autoVideoSearch');
|
return Math.min(INITIAL_BACKOFF * Math.pow(2, retryCount), 10000); // Cap at 10 seconds
|
||||||
|
};
|
||||||
|
|
||||||
if (!autoImageSearch) {
|
useEffect(() => {
|
||||||
localStorage.setItem('autoImageSearch', 'true');
|
const connectWs = async () => {
|
||||||
}
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||||
|
wsRef.current.close();
|
||||||
if (!autoVideoSearch) {
|
|
||||||
localStorage.setItem('autoVideoSearch', 'false');
|
|
||||||
}
|
|
||||||
|
|
||||||
const providers = await fetch(`/api/models`, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
}).then(async (res) => {
|
|
||||||
if (!res.ok)
|
|
||||||
throw new Error(
|
|
||||||
`Failed to fetch models: ${res.status} ${res.statusText}`,
|
|
||||||
);
|
|
||||||
return res.json();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
!chatModel ||
|
|
||||||
!chatModelProvider ||
|
|
||||||
!embeddingModel ||
|
|
||||||
!embeddingModelProvider
|
|
||||||
) {
|
|
||||||
if (!chatModel || !chatModelProvider) {
|
|
||||||
const chatModelProviders = providers.chatModelProviders;
|
|
||||||
|
|
||||||
chatModelProvider =
|
|
||||||
chatModelProvider || Object.keys(chatModelProviders)[0];
|
|
||||||
|
|
||||||
chatModel = Object.keys(chatModelProviders[chatModelProvider])[0];
|
|
||||||
|
|
||||||
if (!chatModelProviders || Object.keys(chatModelProviders).length === 0)
|
|
||||||
return toast.error('No chat models available');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!embeddingModel || !embeddingModelProvider) {
|
try {
|
||||||
const embeddingModelProviders = providers.embeddingModelProviders;
|
let chatModel = localStorage.getItem('chatModel');
|
||||||
|
let chatModelProvider = localStorage.getItem('chatModelProvider');
|
||||||
|
let embeddingModel = localStorage.getItem('embeddingModel');
|
||||||
|
let embeddingModelProvider = localStorage.getItem(
|
||||||
|
'embeddingModelProvider',
|
||||||
|
);
|
||||||
|
|
||||||
|
const autoImageSearch = localStorage.getItem('autoImageSearch');
|
||||||
|
const autoVideoSearch = localStorage.getItem('autoVideoSearch');
|
||||||
|
|
||||||
|
if (!autoImageSearch) {
|
||||||
|
localStorage.setItem('autoImageSearch', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!autoVideoSearch) {
|
||||||
|
localStorage.setItem('autoVideoSearch', 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
const providers = await fetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/models`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
).then(async (res) => {
|
||||||
|
if (!res.ok)
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch models: ${res.status} ${res.statusText}`,
|
||||||
|
);
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!embeddingModelProviders ||
|
!chatModel ||
|
||||||
Object.keys(embeddingModelProviders).length === 0
|
!chatModelProvider ||
|
||||||
)
|
!embeddingModel ||
|
||||||
return toast.error('No embedding models available');
|
!embeddingModelProvider
|
||||||
|
) {
|
||||||
|
if (!chatModel || !chatModelProvider) {
|
||||||
|
const chatModelProviders = providers.chatModelProviders;
|
||||||
|
|
||||||
embeddingModelProvider = Object.keys(embeddingModelProviders)[0];
|
chatModelProvider =
|
||||||
embeddingModel = Object.keys(
|
chatModelProvider || Object.keys(chatModelProviders)[0];
|
||||||
embeddingModelProviders[embeddingModelProvider],
|
|
||||||
)[0];
|
chatModel = Object.keys(chatModelProviders[chatModelProvider])[0];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!chatModelProviders ||
|
||||||
|
Object.keys(chatModelProviders).length === 0
|
||||||
|
)
|
||||||
|
return toast.error('No chat models available');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!embeddingModel || !embeddingModelProvider) {
|
||||||
|
const embeddingModelProviders = providers.embeddingModelProviders;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!embeddingModelProviders ||
|
||||||
|
Object.keys(embeddingModelProviders).length === 0
|
||||||
|
)
|
||||||
|
return toast.error('No embedding models available');
|
||||||
|
|
||||||
|
embeddingModelProvider = Object.keys(embeddingModelProviders)[0];
|
||||||
|
embeddingModel = Object.keys(
|
||||||
|
embeddingModelProviders[embeddingModelProvider],
|
||||||
|
)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('chatModel', chatModel!);
|
||||||
|
localStorage.setItem('chatModelProvider', chatModelProvider);
|
||||||
|
localStorage.setItem('embeddingModel', embeddingModel!);
|
||||||
|
localStorage.setItem(
|
||||||
|
'embeddingModelProvider',
|
||||||
|
embeddingModelProvider,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const chatModelProviders = providers.chatModelProviders;
|
||||||
|
const embeddingModelProviders = providers.embeddingModelProviders;
|
||||||
|
|
||||||
|
if (
|
||||||
|
Object.keys(chatModelProviders).length > 0 &&
|
||||||
|
!chatModelProviders[chatModelProvider]
|
||||||
|
) {
|
||||||
|
const chatModelProvidersKeys = Object.keys(chatModelProviders);
|
||||||
|
chatModelProvider =
|
||||||
|
chatModelProvidersKeys.find(
|
||||||
|
(key) => Object.keys(chatModelProviders[key]).length > 0,
|
||||||
|
) || chatModelProvidersKeys[0];
|
||||||
|
|
||||||
|
localStorage.setItem('chatModelProvider', chatModelProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
chatModelProvider &&
|
||||||
|
!chatModelProviders[chatModelProvider][chatModel]
|
||||||
|
) {
|
||||||
|
chatModel = Object.keys(
|
||||||
|
chatModelProviders[
|
||||||
|
Object.keys(chatModelProviders[chatModelProvider]).length > 0
|
||||||
|
? chatModelProvider
|
||||||
|
: Object.keys(chatModelProviders)[0]
|
||||||
|
],
|
||||||
|
)[0];
|
||||||
|
localStorage.setItem('chatModel', chatModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Object.keys(embeddingModelProviders).length > 0 &&
|
||||||
|
!embeddingModelProviders[embeddingModelProvider]
|
||||||
|
) {
|
||||||
|
embeddingModelProvider = Object.keys(embeddingModelProviders)[0];
|
||||||
|
localStorage.setItem(
|
||||||
|
'embeddingModelProvider',
|
||||||
|
embeddingModelProvider,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
embeddingModelProvider &&
|
||||||
|
!embeddingModelProviders[embeddingModelProvider][embeddingModel]
|
||||||
|
) {
|
||||||
|
embeddingModel = Object.keys(
|
||||||
|
embeddingModelProviders[embeddingModelProvider],
|
||||||
|
)[0];
|
||||||
|
localStorage.setItem('embeddingModel', embeddingModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsURL = new URL(url);
|
||||||
|
const searchParams = new URLSearchParams({});
|
||||||
|
|
||||||
|
searchParams.append('chatModel', chatModel!);
|
||||||
|
searchParams.append('chatModelProvider', chatModelProvider);
|
||||||
|
|
||||||
|
if (chatModelProvider === 'custom_openai') {
|
||||||
|
searchParams.append(
|
||||||
|
'openAIApiKey',
|
||||||
|
localStorage.getItem('openAIApiKey')!,
|
||||||
|
);
|
||||||
|
searchParams.append(
|
||||||
|
'openAIBaseURL',
|
||||||
|
localStorage.getItem('openAIBaseURL')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchParams.append('embeddingModel', embeddingModel!);
|
||||||
|
searchParams.append('embeddingModelProvider', embeddingModelProvider);
|
||||||
|
|
||||||
|
wsURL.search = searchParams.toString();
|
||||||
|
|
||||||
|
const ws = new WebSocket(wsURL.toString());
|
||||||
|
wsRef.current = ws;
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (ws.readyState !== 1) {
|
||||||
|
toast.error(
|
||||||
|
'Failed to connect to the server. Please try again later.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
ws.addEventListener('message', (e) => {
|
||||||
|
const data = JSON.parse(e.data);
|
||||||
|
if (data.type === 'signal' && data.data === 'open') {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (ws.readyState === 1) {
|
||||||
|
setIsWSReady(true);
|
||||||
|
setError(false);
|
||||||
|
if (retryCountRef.current > 0) {
|
||||||
|
toast.success('Connection restored.');
|
||||||
|
}
|
||||||
|
retryCountRef.current = 0;
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, 5);
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
console.debug(new Date(), 'ws:connected');
|
||||||
|
}
|
||||||
|
if (data.type === 'error') {
|
||||||
|
isConnectionErrorRef.current = true;
|
||||||
|
setError(true);
|
||||||
|
toast.error(data.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.onerror = () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
setIsWSReady(false);
|
||||||
|
toast.error('WebSocket connection error.');
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
setIsWSReady(false);
|
||||||
|
console.debug(new Date(), 'ws:disconnected');
|
||||||
|
if (!isCleaningUpRef.current && !isConnectionErrorRef.current) {
|
||||||
|
toast.error('Connection lost. Attempting to reconnect...');
|
||||||
|
attemptReconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.debug(new Date(), 'ws:error', error);
|
||||||
|
setIsWSReady(false);
|
||||||
|
attemptReconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const attemptReconnect = () => {
|
||||||
|
retryCountRef.current += 1;
|
||||||
|
|
||||||
|
if (retryCountRef.current > MAX_RETRIES) {
|
||||||
|
console.debug(new Date(), 'ws:max_retries');
|
||||||
|
setError(true);
|
||||||
|
toast.error(
|
||||||
|
'Unable to connect to server after multiple attempts. Please refresh the page to try again.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem('chatModel', chatModel!);
|
const backoffDelay = getBackoffDelay(retryCountRef.current);
|
||||||
localStorage.setItem('chatModelProvider', chatModelProvider);
|
console.debug(
|
||||||
localStorage.setItem('embeddingModel', embeddingModel!);
|
new Date(),
|
||||||
localStorage.setItem('embeddingModelProvider', embeddingModelProvider);
|
`ws:retry attempt=${retryCountRef.current}/${MAX_RETRIES} delay=${backoffDelay}ms`,
|
||||||
} else {
|
);
|
||||||
const chatModelProviders = providers.chatModelProviders;
|
|
||||||
const embeddingModelProviders = providers.embeddingModelProviders;
|
|
||||||
|
|
||||||
if (
|
if (reconnectTimeoutRef.current) {
|
||||||
Object.keys(chatModelProviders).length > 0 &&
|
clearTimeout(reconnectTimeoutRef.current);
|
||||||
!chatModelProviders[chatModelProvider]
|
|
||||||
) {
|
|
||||||
const chatModelProvidersKeys = Object.keys(chatModelProviders);
|
|
||||||
chatModelProvider =
|
|
||||||
chatModelProvidersKeys.find(
|
|
||||||
(key) => Object.keys(chatModelProviders[key]).length > 0,
|
|
||||||
) || chatModelProvidersKeys[0];
|
|
||||||
|
|
||||||
localStorage.setItem('chatModelProvider', chatModelProvider);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
reconnectTimeoutRef.current = setTimeout(() => {
|
||||||
chatModelProvider &&
|
connectWs();
|
||||||
!chatModelProviders[chatModelProvider][chatModel]
|
}, backoffDelay);
|
||||||
) {
|
};
|
||||||
chatModel = Object.keys(
|
|
||||||
chatModelProviders[
|
connectWs();
|
||||||
Object.keys(chatModelProviders[chatModelProvider]).length > 0
|
|
||||||
? chatModelProvider
|
return () => {
|
||||||
: Object.keys(chatModelProviders)[0]
|
if (reconnectTimeoutRef.current) {
|
||||||
],
|
clearTimeout(reconnectTimeoutRef.current);
|
||||||
)[0];
|
|
||||||
localStorage.setItem('chatModel', chatModel);
|
|
||||||
}
|
}
|
||||||
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||||
if (
|
wsRef.current.close();
|
||||||
Object.keys(embeddingModelProviders).length > 0 &&
|
isCleaningUpRef.current = true;
|
||||||
!embeddingModelProviders[embeddingModelProvider]
|
console.debug(new Date(), 'ws:cleanup');
|
||||||
) {
|
|
||||||
embeddingModelProvider = Object.keys(embeddingModelProviders)[0];
|
|
||||||
localStorage.setItem('embeddingModelProvider', embeddingModelProvider);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
}, [url, setIsWSReady, setError]);
|
||||||
|
|
||||||
if (
|
return wsRef.current;
|
||||||
embeddingModelProvider &&
|
|
||||||
!embeddingModelProviders[embeddingModelProvider][embeddingModel]
|
|
||||||
) {
|
|
||||||
embeddingModel = Object.keys(
|
|
||||||
embeddingModelProviders[embeddingModelProvider],
|
|
||||||
)[0];
|
|
||||||
localStorage.setItem('embeddingModel', embeddingModel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setChatModelProvider({
|
|
||||||
name: chatModel!,
|
|
||||||
provider: chatModelProvider,
|
|
||||||
});
|
|
||||||
|
|
||||||
setEmbeddingModelProvider({
|
|
||||||
name: embeddingModel!,
|
|
||||||
provider: embeddingModelProvider,
|
|
||||||
});
|
|
||||||
|
|
||||||
setIsConfigReady(true);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('An error occurred while checking the configuration:', err);
|
|
||||||
setIsConfigReady(false);
|
|
||||||
setHasError(true);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadMessages = async (
|
const loadMessages = async (
|
||||||
@@ -189,12 +315,15 @@ const loadMessages = async (
|
|||||||
setFiles: (files: File[]) => void,
|
setFiles: (files: File[]) => void,
|
||||||
setFileIds: (fileIds: string[]) => void,
|
setFileIds: (fileIds: string[]) => void,
|
||||||
) => {
|
) => {
|
||||||
const res = await fetch(`/api/chats/${chatId}`, {
|
const res = await fetch(
|
||||||
method: 'GET',
|
`${process.env.NEXT_PUBLIC_API_URL}/chats/${chatId}`,
|
||||||
headers: {
|
{
|
||||||
'Content-Type': 'application/json',
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
if (res.status === 404) {
|
if (res.status === 404) {
|
||||||
setNotFound(true);
|
setNotFound(true);
|
||||||
@@ -244,32 +373,15 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
const [chatId, setChatId] = useState<string | undefined>(id);
|
const [chatId, setChatId] = useState<string | undefined>(id);
|
||||||
const [newChatCreated, setNewChatCreated] = useState(false);
|
const [newChatCreated, setNewChatCreated] = useState(false);
|
||||||
|
|
||||||
const [chatModelProvider, setChatModelProvider] = useState<ChatModelProvider>(
|
|
||||||
{
|
|
||||||
name: '',
|
|
||||||
provider: '',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const [embeddingModelProvider, setEmbeddingModelProvider] =
|
|
||||||
useState<EmbeddingModelProvider>({
|
|
||||||
name: '',
|
|
||||||
provider: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isConfigReady, setIsConfigReady] = useState(false);
|
|
||||||
const [hasError, setHasError] = useState(false);
|
const [hasError, setHasError] = useState(false);
|
||||||
const [isReady, setIsReady] = useState(false);
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const [isWSReady, setIsWSReady] = useState(false);
|
||||||
checkConfig(
|
const ws = useSocket(
|
||||||
setChatModelProvider,
|
process.env.NEXT_PUBLIC_WS_URL!,
|
||||||
setEmbeddingModelProvider,
|
setIsWSReady,
|
||||||
setIsConfigReady,
|
setHasError,
|
||||||
setHasError,
|
);
|
||||||
);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [messageAppeared, setMessageAppeared] = useState(false);
|
const [messageAppeared, setMessageAppeared] = useState(false);
|
||||||
@@ -287,6 +399,8 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
|
|
||||||
const [notFound, setNotFound] = useState(false);
|
const [notFound, setNotFound] = useState(false);
|
||||||
|
|
||||||
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
chatId &&
|
chatId &&
|
||||||
@@ -312,6 +426,16 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (ws?.readyState === 1) {
|
||||||
|
ws.close();
|
||||||
|
console.debug(new Date(), 'ws:cleanup');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const messagesRef = useRef<Message[]>([]);
|
const messagesRef = useRef<Message[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -319,18 +443,18 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMessagesLoaded && isConfigReady) {
|
if (isMessagesLoaded && isWSReady) {
|
||||||
setIsReady(true);
|
setIsReady(true);
|
||||||
console.debug(new Date(), 'app:ready');
|
console.debug(new Date(), 'app:ready');
|
||||||
} else {
|
} else {
|
||||||
setIsReady(false);
|
setIsReady(false);
|
||||||
}
|
}
|
||||||
}, [isMessagesLoaded, isConfigReady]);
|
}, [isMessagesLoaded, isWSReady]);
|
||||||
|
|
||||||
const sendMessage = async (message: string, messageId?: string) => {
|
const sendMessage = async (message: string, messageId?: string) => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
if (!isConfigReady) {
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
toast.error('Cannot send message before the configuration is ready');
|
toast.error('Cannot send message while disconnected');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,27 +467,18 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
|
|
||||||
messageId = messageId ?? crypto.randomBytes(7).toString('hex');
|
messageId = messageId ?? crypto.randomBytes(7).toString('hex');
|
||||||
|
|
||||||
console.log(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
content: message,
|
type: 'message',
|
||||||
message: {
|
message: {
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
chatId: chatId!,
|
chatId: chatId!,
|
||||||
content: message,
|
content: message,
|
||||||
},
|
},
|
||||||
chatId: chatId!,
|
|
||||||
files: fileIds,
|
files: fileIds,
|
||||||
focusMode: focusMode,
|
focusMode: focusMode,
|
||||||
optimizationMode: optimizationMode,
|
optimizationMode: optimizationMode,
|
||||||
history: chatHistory,
|
history: [...chatHistory, ['human', message]],
|
||||||
chatModel: {
|
|
||||||
name: chatModelProvider.name,
|
|
||||||
provider: chatModelProvider.provider,
|
|
||||||
},
|
|
||||||
embeddingModel: {
|
|
||||||
name: embeddingModelProvider.name,
|
|
||||||
provider: embeddingModelProvider.provider,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -378,7 +493,9 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const messageHandler = async (data: any) => {
|
const messageHandler = async (e: MessageEvent) => {
|
||||||
|
const data = JSON.parse(e.data);
|
||||||
|
|
||||||
if (data.type === 'error') {
|
if (data.type === 'error') {
|
||||||
toast.error(data.data);
|
toast.error(data.data);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -441,6 +558,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
['assistant', recievedMessage],
|
['assistant', recievedMessage],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
ws?.removeEventListener('message', messageHandler);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
const lastMsg = messagesRef.current[messagesRef.current.length - 1];
|
const lastMsg = messagesRef.current[messagesRef.current.length - 1];
|
||||||
@@ -466,72 +584,16 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
const autoVideoSearch = localStorage.getItem('autoVideoSearch');
|
const autoVideoSearch = localStorage.getItem('autoVideoSearch');
|
||||||
|
|
||||||
if (autoImageSearch === 'true') {
|
if (autoImageSearch === 'true') {
|
||||||
document
|
document.getElementById('search-images')?.click();
|
||||||
.getElementById(`search-images-${lastMsg.messageId}`)
|
|
||||||
?.click();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (autoVideoSearch === 'true') {
|
if (autoVideoSearch === 'true') {
|
||||||
document
|
document.getElementById('search-videos')?.click();
|
||||||
.getElementById(`search-videos-${lastMsg.messageId}`)
|
|
||||||
?.click();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await fetch('/api/chat', {
|
ws?.addEventListener('message', messageHandler);
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
content: message,
|
|
||||||
message: {
|
|
||||||
messageId: messageId,
|
|
||||||
chatId: chatId!,
|
|
||||||
content: message,
|
|
||||||
},
|
|
||||||
chatId: chatId!,
|
|
||||||
files: fileIds,
|
|
||||||
focusMode: focusMode,
|
|
||||||
optimizationMode: optimizationMode,
|
|
||||||
history: chatHistory,
|
|
||||||
chatModel: {
|
|
||||||
name: chatModelProvider.name,
|
|
||||||
provider: chatModelProvider.provider,
|
|
||||||
},
|
|
||||||
embeddingModel: {
|
|
||||||
name: embeddingModelProvider.name,
|
|
||||||
provider: embeddingModelProvider.provider,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.body) throw new Error('No response body');
|
|
||||||
|
|
||||||
const reader = res.body?.getReader();
|
|
||||||
const decoder = new TextDecoder('utf-8');
|
|
||||||
|
|
||||||
let partialChunk = '';
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { value, done } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
|
|
||||||
partialChunk += decoder.decode(value, { stream: true });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const messages = partialChunk.split('\n');
|
|
||||||
for (const msg of messages) {
|
|
||||||
if (!msg.trim()) continue;
|
|
||||||
const json = JSON.parse(msg);
|
|
||||||
messageHandler(json);
|
|
||||||
}
|
|
||||||
partialChunk = '';
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Incomplete JSON, waiting for next chunk...');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const rewrite = (messageId: string) => {
|
const rewrite = (messageId: string) => {
|
||||||
@@ -552,11 +614,11 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isReady && initialMessage && isConfigReady) {
|
if (isReady && initialMessage && ws?.readyState === 1) {
|
||||||
sendMessage(initialMessage);
|
sendMessage(initialMessage);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isConfigReady, isReady, initialMessage]);
|
}, [ws?.readyState, isReady, initialMessage, isWSReady]);
|
||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
return (
|
return (
|
@@ -29,12 +29,15 @@ const DeleteChat = ({
|
|||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/chats/${chatId}`, {
|
const res = await fetch(
|
||||||
method: 'DELETE',
|
`${process.env.NEXT_PUBLIC_API_URL}/chats/${chatId}`,
|
||||||
headers: {
|
{
|
||||||
'Content-Type': 'application/json',
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
if (res.status != 200) {
|
if (res.status != 200) {
|
||||||
throw new Error('Failed to delete chat');
|
throw new Error('Failed to delete chat');
|
@@ -68,13 +68,7 @@ const MessageBox = ({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{message.role === 'user' && (
|
{message.role === 'user' && (
|
||||||
<div
|
<div className={cn('w-full', messageIndex === 0 ? 'pt-16' : 'pt-8', 'break-words')}>
|
||||||
className={cn(
|
|
||||||
'w-full',
|
|
||||||
messageIndex === 0 ? 'pt-16' : 'pt-8',
|
|
||||||
'break-words',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
|
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
|
||||||
{message.content}
|
{message.content}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -193,12 +187,10 @@ const MessageBox = ({
|
|||||||
<SearchImages
|
<SearchImages
|
||||||
query={history[messageIndex - 1].content}
|
query={history[messageIndex - 1].content}
|
||||||
chatHistory={history.slice(0, messageIndex - 1)}
|
chatHistory={history.slice(0, messageIndex - 1)}
|
||||||
messageId={message.messageId}
|
|
||||||
/>
|
/>
|
||||||
<SearchVideos
|
<SearchVideos
|
||||||
chatHistory={history.slice(0, messageIndex - 1)}
|
chatHistory={history.slice(0, messageIndex - 1)}
|
||||||
query={history[messageIndex - 1].content}
|
query={history[messageIndex - 1].content}
|
||||||
messageId={message.messageId}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@@ -41,7 +41,7 @@ const Attach = ({
|
|||||||
data.append('embedding_model_provider', embeddingModelProvider!);
|
data.append('embedding_model_provider', embeddingModelProvider!);
|
||||||
data.append('embedding_model', embeddingModel!);
|
data.append('embedding_model', embeddingModel!);
|
||||||
|
|
||||||
const res = await fetch(`/api/uploads`, {
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/uploads`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: data,
|
body: data,
|
||||||
});
|
});
|
@@ -39,7 +39,7 @@ const AttachSmall = ({
|
|||||||
data.append('embedding_model_provider', embeddingModelProvider!);
|
data.append('embedding_model_provider', embeddingModelProvider!);
|
||||||
data.append('embedding_model', embeddingModel!);
|
data.append('embedding_model', embeddingModel!);
|
||||||
|
|
||||||
const res = await fetch(`/api/uploads`, {
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/uploads`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: data,
|
body: data,
|
||||||
});
|
});
|
@@ -1,5 +1,6 @@
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Switch } from '@headlessui/react';
|
import { Switch } from '@headlessui/react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
const CopilotToggle = ({
|
const CopilotToggle = ({
|
||||||
copilotEnabled,
|
copilotEnabled,
|
||||||
@@ -8,11 +9,33 @@ const CopilotToggle = ({
|
|||||||
copilotEnabled: boolean;
|
copilotEnabled: boolean;
|
||||||
setCopilotEnabled: (enabled: boolean) => void;
|
setCopilotEnabled: (enabled: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const fetchAndSetCopilotEnabled = async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/config/preferences`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const preferences = await res.json();
|
||||||
|
|
||||||
|
setCopilotEnabled(preferences.isCopilotEnabled);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAndSetCopilotEnabled();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group flex flex-row items-center space-x-1 active:scale-95 duration-200 transition cursor-pointer">
|
<div className="group flex flex-row items-center space-x-1 active:scale-95 duration-200 transition cursor-pointer">
|
||||||
<Switch
|
<Switch
|
||||||
checked={copilotEnabled}
|
checked={copilotEnabled}
|
||||||
onChange={setCopilotEnabled}
|
onChange={setCopilotEnabled}
|
||||||
|
disabled={true}
|
||||||
className="bg-light-secondary dark:bg-dark-secondary border border-light-200/70 dark:border-dark-200 relative inline-flex h-5 w-10 sm:h-6 sm:w-11 items-center rounded-full"
|
className="bg-light-secondary dark:bg-dark-secondary border border-light-200/70 dark:border-dark-200 relative inline-flex h-5 w-10 sm:h-6 sm:w-11 items-center rounded-full"
|
||||||
>
|
>
|
||||||
<span className="sr-only">Copilot</span>
|
<span className="sr-only">Copilot</span>
|
@@ -45,13 +45,25 @@ const focusModes = [
|
|||||||
key: 'youtubeSearch',
|
key: 'youtubeSearch',
|
||||||
title: 'Youtube',
|
title: 'Youtube',
|
||||||
description: 'Search and watch videos',
|
description: 'Search and watch videos',
|
||||||
icon: <SiYoutube className="h-5 w-auto mr-0.5" />,
|
icon: (
|
||||||
|
<SiYoutube
|
||||||
|
className="h-5 w-auto mr-0.5"
|
||||||
|
onPointerEnterCapture={undefined}
|
||||||
|
onPointerLeaveCapture={undefined}
|
||||||
|
/>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'redditSearch',
|
key: 'redditSearch',
|
||||||
title: 'Reddit',
|
title: 'Reddit',
|
||||||
description: 'Search for discussions and opinions',
|
description: 'Search for discussions and opinions',
|
||||||
icon: <SiReddit className="h-5 w-auto mr-0.5" />,
|
icon: (
|
||||||
|
<SiReddit
|
||||||
|
className="h-5 w-auto mr-0.5"
|
||||||
|
onPointerEnterCapture={undefined}
|
||||||
|
onPointerLeaveCapture={undefined}
|
||||||
|
/>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@@ -69,15 +69,11 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
|
|||||||
<div className="flex flex-row items-center space-x-1">
|
<div className="flex flex-row items-center space-x-1">
|
||||||
{sources.slice(3, 6).map((source, i) => {
|
{sources.slice(3, 6).map((source, i) => {
|
||||||
return source.metadata.url === 'File' ? (
|
return source.metadata.url === 'File' ? (
|
||||||
<div
|
<div className="bg-dark-200 hover:bg-dark-100 transition duration-200 flex items-center justify-center w-6 h-6 rounded-full">
|
||||||
key={i}
|
|
||||||
className="bg-dark-200 hover:bg-dark-100 transition duration-200 flex items-center justify-center w-6 h-6 rounded-full"
|
|
||||||
>
|
|
||||||
<File size={12} className="text-white/70" />
|
<File size={12} className="text-white/70" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
key={i}
|
|
||||||
src={`https://s2.googleusercontent.com/s2/favicons?domain_url=${source.metadata.url}`}
|
src={`https://s2.googleusercontent.com/s2/favicons?domain_url=${source.metadata.url}`}
|
||||||
width={16}
|
width={16}
|
||||||
height={16}
|
height={16}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user