feat(config): Use toml instead of env
This commit is contained in:
parent
dd1ce4e324
commit
c6a5790d33
|
@ -1,5 +0,0 @@
|
||||||
PORT=3001
|
|
||||||
OPENAI_API_KEY=
|
|
||||||
SIMILARITY_MEASURE=cosine # cosine or dot
|
|
||||||
SEARXNG_API_URL= # no need to fill this if using docker
|
|
||||||
MODEL_NAME=gpt-3.5-turbo
|
|
|
@ -4,7 +4,6 @@ about: Create an issue to help us fix bugs
|
||||||
title: ''
|
title: ''
|
||||||
labels: bug
|
labels: bug
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
|
@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
|
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
|
|
|
@ -4,7 +4,4 @@ about: Describe this issue template's purpose here.
|
||||||
title: ''
|
title: ''
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ about: Suggest an idea for this project
|
||||||
title: ''
|
title: ''
|
||||||
labels: enhancement
|
labels: enhancement
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
|
|
@ -19,6 +19,9 @@ yarn-error.log
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
|
|
||||||
|
# Config files
|
||||||
|
config.toml
|
||||||
|
|
||||||
# Log files
|
# Log files
|
||||||
logs/
|
logs/
|
||||||
*.log
|
*.log
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Ignore all files in the node_modules directory
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Ignore all files in the .next directory (Next.js build output)
|
||||||
|
.next
|
||||||
|
|
||||||
|
# Ignore all files in the .out directory (TypeScript build output)
|
||||||
|
.out
|
||||||
|
|
||||||
|
# Ignore all files in the .cache directory (Prettier cache)
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Ignore all files in the .vscode directory (Visual Studio Code settings)
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Ignore all files in the .idea directory (IntelliJ IDEA settings)
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Ignore all files in the dist directory (build output)
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Ignore all files in the build directory (build output)
|
||||||
|
build
|
||||||
|
|
||||||
|
# Ignore all files in the coverage directory (test coverage reports)
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# Ignore all files with the .log extension
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Ignore all files with the .tmp extension
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# Ignore all files with the .swp extension
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# Ignore all files with the .DS_Store extension (macOS specific)
|
||||||
|
.DS_Store
|
|
@ -56,10 +56,10 @@ There are mainly 2 ways of installing Perplexica - With Docker, Without Docker.
|
||||||
|
|
||||||
3. After cloning, navigate to the directory containing the project files.
|
3. After cloning, navigate to the directory containing the project files.
|
||||||
|
|
||||||
4. Rename the `.env.example` file to `.env`. For Docker setups, you need only fill in the following fields:
|
4. Rename the `sample.config.toml` file to `config.toml`. For Docker setups, you need only fill in the following fields:
|
||||||
|
|
||||||
- `OPENAI_API_KEY`
|
- `OPENAI`: Your OpenAI API key.
|
||||||
- `SIMILARITY_MEASURE` (This is filled by default; you can leave it as is if you are unsure about it.)
|
- `SIMILARITY_MEASURE`: The similarity measure to use (This is filled by default; you can leave it as is if you are unsure about it.)
|
||||||
|
|
||||||
5. Ensure you are in the directory containing the `docker-compose.yaml` file and execute:
|
5. Ensure you are in the directory containing the `docker-compose.yaml` file and execute:
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ There are mainly 2 ways of installing Perplexica - With Docker, Without Docker.
|
||||||
|
|
||||||
For setups without Docker:
|
For setups without Docker:
|
||||||
|
|
||||||
1. Follow the initial steps to clone the repository and rename the `.env.example` file to `.env` in the root directory. You will need to fill in all the fields in this file.
|
1. Follow the initial steps to clone the repository and rename the `sample.config.toml` file to `config.toml` in the root directory. You will need to fill in all the fields in this file.
|
||||||
2. Additionally, rename the `.env.example` file to `.env` in the `ui` folder and complete all fields.
|
2. Additionally, rename the `.env.example` file to `.env` in the `ui` folder and complete all fields.
|
||||||
3. The non-Docker setup requires manual configuration of both the backend and frontend.
|
3. The non-Docker setup requires manual configuration of both the backend and frontend.
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
FROM node:alpine
|
FROM node:alpine
|
||||||
|
|
||||||
ARG SEARXNG_API_URL
|
ARG SEARXNG_API_URL
|
||||||
ENV SEARXNG_API_URL=${SEARXNG_API_URL}
|
|
||||||
|
|
||||||
WORKDIR /home/perplexica
|
WORKDIR /home/perplexica
|
||||||
|
|
||||||
COPY src /home/perplexica/src
|
COPY src /home/perplexica/src
|
||||||
COPY tsconfig.json /home/perplexica/
|
COPY tsconfig.json /home/perplexica/
|
||||||
COPY .env /home/perplexica/
|
COPY config.toml /home/perplexica/
|
||||||
COPY package.json /home/perplexica/
|
COPY package.json /home/perplexica/
|
||||||
COPY yarn.lock /home/perplexica/
|
COPY yarn.lock /home/perplexica/
|
||||||
|
|
||||||
|
RUN sed -i "s|SEARXNG = \".*\"|SEARXNG = \"${SEARXNG_API_URL}\"|g" /home/perplexica/config.toml
|
||||||
|
|
||||||
RUN yarn install
|
RUN yarn install
|
||||||
RUN yarn build
|
RUN yarn build
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "ItzCrazyKns",
|
"author": "ItzCrazyKns",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --env-file=.env dist/app.js",
|
"start": "node dist/app.js",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"dev": "nodemon -r dotenv/config src/app.ts",
|
"dev": "nodemon src/app.ts",
|
||||||
"format": "prettier . --check",
|
"format": "prettier . --check",
|
||||||
"format:write": "prettier . --write"
|
"format:write": "prettier . --write"
|
||||||
},
|
},
|
||||||
|
@ -19,6 +19,7 @@
|
||||||
"typescript": "^5.4.3"
|
"typescript": "^5.4.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@iarna/toml": "^2.2.5",
|
||||||
"@langchain/openai": "^0.0.25",
|
"@langchain/openai": "^0.0.25",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"compute-cosine-similarity": "^1.1.0",
|
"compute-cosine-similarity": "^1.1.0",
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
[GENERAL]
|
||||||
|
PORT = 3001 # Port to run the server on
|
||||||
|
SIMILARITY_MEASURE = "cosine" # "cosine" or "dot"
|
||||||
|
|
||||||
|
[API_KEYS]
|
||||||
|
OPENAI = "sk-1234567890abcdef1234567890abcdef" # OpenAI API key
|
||||||
|
|
||||||
|
[API_ENDPOINTS]
|
||||||
|
SEARXNG = "http://localhost:32768" # SearxNG API ULR
|
|
@ -9,24 +9,16 @@ import {
|
||||||
RunnableMap,
|
RunnableMap,
|
||||||
RunnableLambda,
|
RunnableLambda,
|
||||||
} from '@langchain/core/runnables';
|
} from '@langchain/core/runnables';
|
||||||
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
|
|
||||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
import { StringOutputParser } from '@langchain/core/output_parsers';
|
||||||
import { Document } from '@langchain/core/documents';
|
import { Document } from '@langchain/core/documents';
|
||||||
import { searchSearxng } from '../core/searxng';
|
import { searchSearxng } from '../core/searxng';
|
||||||
import type { StreamEvent } from '@langchain/core/tracers/log_stream';
|
import type { StreamEvent } from '@langchain/core/tracers/log_stream';
|
||||||
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
import type { Embeddings } from '@langchain/core/embeddings';
|
||||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
import formatChatHistoryAsString from '../utils/formatHistory';
|
||||||
import eventEmitter from 'events';
|
import eventEmitter from 'events';
|
||||||
import computeSimilarity from '../utils/computeSimilarity';
|
import computeSimilarity from '../utils/computeSimilarity';
|
||||||
|
|
||||||
const llm = new ChatOpenAI({
|
|
||||||
modelName: process.env.MODEL_NAME,
|
|
||||||
temperature: 0.7,
|
|
||||||
});
|
|
||||||
|
|
||||||
const embeddings = new OpenAIEmbeddings({
|
|
||||||
modelName: 'text-embedding-3-large',
|
|
||||||
});
|
|
||||||
|
|
||||||
const basicAcademicSearchRetrieverPrompt = `
|
const basicAcademicSearchRetrieverPrompt = `
|
||||||
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the LLM to search the web for information.
|
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the LLM to search the web for information.
|
||||||
If it is a writing task or a simple hi, hello rather than a question, you need to return \`not_needed\` as the response.
|
If it is a writing task or a simple hi, hello rather than a question, you need to return \`not_needed\` as the response.
|
||||||
|
@ -49,7 +41,7 @@ Rephrased question:
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const basicAcademicSearchResponsePrompt = `
|
const basicAcademicSearchResponsePrompt = `
|
||||||
You are Perplexica, an AI model who is expert at searching the web and answering user's queries. You are set on focus mode 'Acadedemic', this means you will be searching for academic papers and articles on the web.
|
You are Perplexica, an AI model who is expert at searching the web and answering user's queries. You are set on focus mode 'Academic', this means you will be searching for academic papers and articles on the web.
|
||||||
|
|
||||||
Generate a response that is informative and relevant to the user's query based on provided context (the context consits of search results containg a brief description of the content of that page).
|
Generate a response that is informative and relevant to the user's query based on provided context (the context consits of search results containg a brief description of the content of that page).
|
||||||
You must use this context to answer the user's query in the best way possible. Use an unbaised and journalistic tone in your response. Do not repeat the text.
|
You must use this context to answer the user's query in the best way possible. Use an unbaised and journalistic tone in your response. Do not repeat the text.
|
||||||
|
@ -104,6 +96,55 @@ const handleStream = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BasicChainInput = {
|
||||||
|
chat_history: BaseMessage[];
|
||||||
|
query: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBasicAcademicSearchRetrieverChain = (llm: BaseChatModel) => {
|
||||||
|
return RunnableSequence.from([
|
||||||
|
PromptTemplate.fromTemplate(basicAcademicSearchRetrieverPrompt),
|
||||||
|
llm,
|
||||||
|
strParser,
|
||||||
|
RunnableLambda.from(async (input: string) => {
|
||||||
|
if (input === 'not_needed') {
|
||||||
|
return { query: '', docs: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await searchSearxng(input, {
|
||||||
|
language: 'en',
|
||||||
|
engines: [
|
||||||
|
'arxiv',
|
||||||
|
'google_scholar',
|
||||||
|
'internet_archive_scholar',
|
||||||
|
'pubmed',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const documents = res.results.map(
|
||||||
|
(result) =>
|
||||||
|
new Document({
|
||||||
|
pageContent: result.content,
|
||||||
|
metadata: {
|
||||||
|
title: result.title,
|
||||||
|
url: result.url,
|
||||||
|
...(result.img_src && { img_src: result.img_src }),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { query: input, docs: documents };
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBasicAcademicSearchAnsweringChain = (
|
||||||
|
llm: BaseChatModel,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
) => {
|
||||||
|
const basicAcademicSearchRetrieverChain =
|
||||||
|
createBasicAcademicSearchRetrieverChain(llm);
|
||||||
|
|
||||||
const processDocs = async (docs: Document[]) => {
|
const processDocs = async (docs: Document[]) => {
|
||||||
return docs
|
return docs
|
||||||
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
|
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
|
||||||
|
@ -148,47 +189,7 @@ const rerankDocs = async ({
|
||||||
return sortedDocs;
|
return sortedDocs;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BasicChainInput = {
|
return RunnableSequence.from([
|
||||||
chat_history: BaseMessage[];
|
|
||||||
query: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const basicAcademicSearchRetrieverChain = RunnableSequence.from([
|
|
||||||
PromptTemplate.fromTemplate(basicAcademicSearchRetrieverPrompt),
|
|
||||||
llm,
|
|
||||||
strParser,
|
|
||||||
RunnableLambda.from(async (input: string) => {
|
|
||||||
if (input === 'not_needed') {
|
|
||||||
return { query: '', docs: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await searchSearxng(input, {
|
|
||||||
language: 'en',
|
|
||||||
engines: [
|
|
||||||
'arxiv',
|
|
||||||
'google_scholar',
|
|
||||||
'internet_archive_scholar',
|
|
||||||
'pubmed',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const documents = res.results.map(
|
|
||||||
(result) =>
|
|
||||||
new Document({
|
|
||||||
pageContent: result.content,
|
|
||||||
metadata: {
|
|
||||||
title: result.title,
|
|
||||||
url: result.url,
|
|
||||||
...(result.img_src && { img_src: result.img_src }),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return { query: input, docs: documents };
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const basicAcademicSearchAnsweringChain = RunnableSequence.from([
|
|
||||||
RunnableMap.from({
|
RunnableMap.from({
|
||||||
query: (input: BasicChainInput) => input.query,
|
query: (input: BasicChainInput) => input.query,
|
||||||
chat_history: (input: BasicChainInput) => input.chat_history,
|
chat_history: (input: BasicChainInput) => input.chat_history,
|
||||||
|
@ -215,11 +216,20 @@ const basicAcademicSearchAnsweringChain = RunnableSequence.from([
|
||||||
]).withConfig({
|
]).withConfig({
|
||||||
runName: 'FinalResponseGenerator',
|
runName: 'FinalResponseGenerator',
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const basicAcademicSearch = (query: string, history: BaseMessage[]) => {
|
const basicAcademicSearch = (
|
||||||
|
query: string,
|
||||||
|
history: BaseMessage[],
|
||||||
|
llm: BaseChatModel,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
) => {
|
||||||
const emitter = new eventEmitter();
|
const emitter = new eventEmitter();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const basicAcademicSearchAnsweringChain =
|
||||||
|
createBasicAcademicSearchAnsweringChain(llm, embeddings);
|
||||||
|
|
||||||
const stream = basicAcademicSearchAnsweringChain.streamEvents(
|
const stream = basicAcademicSearchAnsweringChain.streamEvents(
|
||||||
{
|
{
|
||||||
chat_history: history,
|
chat_history: history,
|
||||||
|
@ -242,8 +252,13 @@ const basicAcademicSearch = (query: string, history: BaseMessage[]) => {
|
||||||
return emitter;
|
return emitter;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAcademicSearch = (message: string, history: BaseMessage[]) => {
|
const handleAcademicSearch = (
|
||||||
const emitter = basicAcademicSearch(message, history);
|
message: string,
|
||||||
|
history: BaseMessage[],
|
||||||
|
llm: BaseChatModel,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
) => {
|
||||||
|
const emitter = basicAcademicSearch(message, history, llm, embeddings);
|
||||||
return emitter;
|
return emitter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,16 +4,11 @@ import {
|
||||||
RunnableLambda,
|
RunnableLambda,
|
||||||
} from '@langchain/core/runnables';
|
} from '@langchain/core/runnables';
|
||||||
import { PromptTemplate } from '@langchain/core/prompts';
|
import { PromptTemplate } from '@langchain/core/prompts';
|
||||||
import { ChatOpenAI } from '@langchain/openai';
|
|
||||||
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 '../core/searxng';
|
import { searchSearxng } from '../core/searxng';
|
||||||
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
const llm = new ChatOpenAI({
|
|
||||||
modelName: process.env.MODEL_NAME,
|
|
||||||
temperature: 0.7,
|
|
||||||
});
|
|
||||||
|
|
||||||
const imageSearchChainPrompt = `
|
const imageSearchChainPrompt = `
|
||||||
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search the web for images.
|
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search the web for images.
|
||||||
|
@ -43,7 +38,8 @@ type ImageSearchChainInput = {
|
||||||
|
|
||||||
const strParser = new StringOutputParser();
|
const strParser = new StringOutputParser();
|
||||||
|
|
||||||
const imageSearchChain = RunnableSequence.from([
|
const createImageSearchChain = (llm: BaseChatModel) => {
|
||||||
|
return RunnableSequence.from([
|
||||||
RunnableMap.from({
|
RunnableMap.from({
|
||||||
chat_history: (input: ImageSearchChainInput) => {
|
chat_history: (input: ImageSearchChainInput) => {
|
||||||
return formatChatHistoryAsString(input.chat_history);
|
return formatChatHistoryAsString(input.chat_history);
|
||||||
|
@ -76,5 +72,14 @@ const imageSearchChain = RunnableSequence.from([
|
||||||
return images.slice(0, 10);
|
return images.slice(0, 10);
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
export default imageSearchChain;
|
const handleImageSearch = (
|
||||||
|
input: ImageSearchChainInput,
|
||||||
|
llm: BaseChatModel,
|
||||||
|
) => {
|
||||||
|
const imageSearchChain = createImageSearchChain(llm);
|
||||||
|
return imageSearchChain.invoke(input);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handleImageSearch;
|
||||||
|
|
|
@ -9,24 +9,16 @@ import {
|
||||||
RunnableMap,
|
RunnableMap,
|
||||||
RunnableLambda,
|
RunnableLambda,
|
||||||
} from '@langchain/core/runnables';
|
} from '@langchain/core/runnables';
|
||||||
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
|
|
||||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
import { StringOutputParser } from '@langchain/core/output_parsers';
|
||||||
import { Document } from '@langchain/core/documents';
|
import { Document } from '@langchain/core/documents';
|
||||||
import { searchSearxng } from '../core/searxng';
|
import { searchSearxng } from '../core/searxng';
|
||||||
import type { StreamEvent } from '@langchain/core/tracers/log_stream';
|
import type { StreamEvent } from '@langchain/core/tracers/log_stream';
|
||||||
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
import type { Embeddings } from '@langchain/core/embeddings';
|
||||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
import formatChatHistoryAsString from '../utils/formatHistory';
|
||||||
import eventEmitter from 'events';
|
import eventEmitter from 'events';
|
||||||
import computeSimilarity from '../utils/computeSimilarity';
|
import computeSimilarity from '../utils/computeSimilarity';
|
||||||
|
|
||||||
const llm = new ChatOpenAI({
|
|
||||||
modelName: process.env.MODEL_NAME,
|
|
||||||
temperature: 0.7,
|
|
||||||
});
|
|
||||||
|
|
||||||
const embeddings = new OpenAIEmbeddings({
|
|
||||||
modelName: 'text-embedding-3-large',
|
|
||||||
});
|
|
||||||
|
|
||||||
const basicRedditSearchRetrieverPrompt = `
|
const basicRedditSearchRetrieverPrompt = `
|
||||||
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the LLM to search the web for information.
|
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the LLM to search the web for information.
|
||||||
If it is a writing task or a simple hi, hello rather than a question, you need to return \`not_needed\` as the response.
|
If it is a writing task or a simple hi, hello rather than a question, you need to return \`not_needed\` as the response.
|
||||||
|
@ -104,6 +96,50 @@ const handleStream = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BasicChainInput = {
|
||||||
|
chat_history: BaseMessage[];
|
||||||
|
query: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBasicRedditSearchRetrieverChain = (llm: BaseChatModel) => {
|
||||||
|
return RunnableSequence.from([
|
||||||
|
PromptTemplate.fromTemplate(basicRedditSearchRetrieverPrompt),
|
||||||
|
llm,
|
||||||
|
strParser,
|
||||||
|
RunnableLambda.from(async (input: string) => {
|
||||||
|
if (input === 'not_needed') {
|
||||||
|
return { query: '', docs: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await searchSearxng(input, {
|
||||||
|
language: 'en',
|
||||||
|
engines: ['reddit'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const documents = res.results.map(
|
||||||
|
(result) =>
|
||||||
|
new Document({
|
||||||
|
pageContent: result.content ? result.content : result.title,
|
||||||
|
metadata: {
|
||||||
|
title: result.title,
|
||||||
|
url: result.url,
|
||||||
|
...(result.img_src && { img_src: result.img_src }),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { query: input, docs: documents };
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBasicRedditSearchAnsweringChain = (
|
||||||
|
llm: BaseChatModel,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
) => {
|
||||||
|
const basicRedditSearchRetrieverChain =
|
||||||
|
createBasicRedditSearchRetrieverChain(llm);
|
||||||
|
|
||||||
const processDocs = async (docs: Document[]) => {
|
const processDocs = async (docs: Document[]) => {
|
||||||
return docs
|
return docs
|
||||||
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
|
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
|
||||||
|
@ -149,42 +185,7 @@ const rerankDocs = async ({
|
||||||
return sortedDocs;
|
return sortedDocs;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BasicChainInput = {
|
return RunnableSequence.from([
|
||||||
chat_history: BaseMessage[];
|
|
||||||
query: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const basicRedditSearchRetrieverChain = RunnableSequence.from([
|
|
||||||
PromptTemplate.fromTemplate(basicRedditSearchRetrieverPrompt),
|
|
||||||
llm,
|
|
||||||
strParser,
|
|
||||||
RunnableLambda.from(async (input: string) => {
|
|
||||||
if (input === 'not_needed') {
|
|
||||||
return { query: '', docs: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await searchSearxng(input, {
|
|
||||||
language: 'en',
|
|
||||||
engines: ['reddit'],
|
|
||||||
});
|
|
||||||
|
|
||||||
const documents = res.results.map(
|
|
||||||
(result) =>
|
|
||||||
new Document({
|
|
||||||
pageContent: result.content ? result.content : result.title,
|
|
||||||
metadata: {
|
|
||||||
title: result.title,
|
|
||||||
url: result.url,
|
|
||||||
...(result.img_src && { img_src: result.img_src }),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return { query: input, docs: documents };
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const basicRedditSearchAnsweringChain = RunnableSequence.from([
|
|
||||||
RunnableMap.from({
|
RunnableMap.from({
|
||||||
query: (input: BasicChainInput) => input.query,
|
query: (input: BasicChainInput) => input.query,
|
||||||
chat_history: (input: BasicChainInput) => input.chat_history,
|
chat_history: (input: BasicChainInput) => input.chat_history,
|
||||||
|
@ -211,11 +212,19 @@ const basicRedditSearchAnsweringChain = RunnableSequence.from([
|
||||||
]).withConfig({
|
]).withConfig({
|
||||||
runName: 'FinalResponseGenerator',
|
runName: 'FinalResponseGenerator',
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const basicRedditSearch = (query: string, history: BaseMessage[]) => {
|
const basicRedditSearch = (
|
||||||
|
query: string,
|
||||||
|
history: BaseMessage[],
|
||||||
|
llm: BaseChatModel,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
) => {
|
||||||
const emitter = new eventEmitter();
|
const emitter = new eventEmitter();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const basicRedditSearchAnsweringChain =
|
||||||
|
createBasicRedditSearchAnsweringChain(llm, embeddings);
|
||||||
const stream = basicRedditSearchAnsweringChain.streamEvents(
|
const stream = basicRedditSearchAnsweringChain.streamEvents(
|
||||||
{
|
{
|
||||||
chat_history: history,
|
chat_history: history,
|
||||||
|
@ -238,8 +247,13 @@ const basicRedditSearch = (query: string, history: BaseMessage[]) => {
|
||||||
return emitter;
|
return emitter;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRedditSearch = (message: string, history: BaseMessage[]) => {
|
const handleRedditSearch = (
|
||||||
const emitter = basicRedditSearch(message, history);
|
message: string,
|
||||||
|
history: BaseMessage[],
|
||||||
|
llm: BaseChatModel,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
) => {
|
||||||
|
const emitter = basicRedditSearch(message, history, llm, embeddings);
|
||||||
return emitter;
|
return emitter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -9,24 +9,16 @@ import {
|
||||||
RunnableMap,
|
RunnableMap,
|
||||||
RunnableLambda,
|
RunnableLambda,
|
||||||
} from '@langchain/core/runnables';
|
} from '@langchain/core/runnables';
|
||||||
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
|
|
||||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
import { StringOutputParser } from '@langchain/core/output_parsers';
|
||||||
import { Document } from '@langchain/core/documents';
|
import { Document } from '@langchain/core/documents';
|
||||||
import { searchSearxng } from '../core/searxng';
|
import { searchSearxng } from '../core/searxng';
|
||||||
import type { StreamEvent } from '@langchain/core/tracers/log_stream';
|
import type { StreamEvent } from '@langchain/core/tracers/log_stream';
|
||||||
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
import type { Embeddings } from '@langchain/core/embeddings';
|
||||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
import formatChatHistoryAsString from '../utils/formatHistory';
|
||||||
import eventEmitter from 'events';
|
import eventEmitter from 'events';
|
||||||
import computeSimilarity from '../utils/computeSimilarity';
|
import computeSimilarity from '../utils/computeSimilarity';
|
||||||
|
|
||||||
const llm = new ChatOpenAI({
|
|
||||||
modelName: process.env.MODEL_NAME,
|
|
||||||
temperature: 0.7,
|
|
||||||
});
|
|
||||||
|
|
||||||
const embeddings = new OpenAIEmbeddings({
|
|
||||||
modelName: 'text-embedding-3-large',
|
|
||||||
});
|
|
||||||
|
|
||||||
const basicSearchRetrieverPrompt = `
|
const basicSearchRetrieverPrompt = `
|
||||||
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the LLM to search the web for information.
|
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the LLM to search the web for information.
|
||||||
If it is a writing task or a simple hi, hello rather than a question, you need to return \`not_needed\` as the response.
|
If it is a writing task or a simple hi, hello rather than a question, you need to return \`not_needed\` as the response.
|
||||||
|
@ -104,6 +96,48 @@ const handleStream = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BasicChainInput = {
|
||||||
|
chat_history: BaseMessage[];
|
||||||
|
query: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBasicWebSearchRetrieverChain = (llm: BaseChatModel) => {
|
||||||
|
return RunnableSequence.from([
|
||||||
|
PromptTemplate.fromTemplate(basicSearchRetrieverPrompt),
|
||||||
|
llm,
|
||||||
|
strParser,
|
||||||
|
RunnableLambda.from(async (input: string) => {
|
||||||
|
if (input === 'not_needed') {
|
||||||
|
return { query: '', docs: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await searchSearxng(input, {
|
||||||
|
language: 'en',
|
||||||
|
});
|
||||||
|
|
||||||
|
const documents = res.results.map(
|
||||||
|
(result) =>
|
||||||
|
new Document({
|
||||||
|
pageContent: result.content,
|
||||||
|
metadata: {
|
||||||
|
title: result.title,
|
||||||
|
url: result.url,
|
||||||
|
...(result.img_src && { img_src: result.img_src }),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { query: input, docs: documents };
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBasicWebSearchAnsweringChain = (
|
||||||
|
llm: BaseChatModel,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
) => {
|
||||||
|
const basicWebSearchRetrieverChain = createBasicWebSearchRetrieverChain(llm);
|
||||||
|
|
||||||
const processDocs = async (docs: Document[]) => {
|
const processDocs = async (docs: Document[]) => {
|
||||||
return docs
|
return docs
|
||||||
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
|
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
|
||||||
|
@ -149,41 +183,7 @@ const rerankDocs = async ({
|
||||||
return sortedDocs;
|
return sortedDocs;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BasicChainInput = {
|
return RunnableSequence.from([
|
||||||
chat_history: BaseMessage[];
|
|
||||||
query: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const basicWebSearchRetrieverChain = RunnableSequence.from([
|
|
||||||
PromptTemplate.fromTemplate(basicSearchRetrieverPrompt),
|
|
||||||
llm,
|
|
||||||
strParser,
|
|
||||||
RunnableLambda.from(async (input: string) => {
|
|
||||||
if (input === 'not_needed') {
|
|
||||||
return { query: '', docs: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await searchSearxng(input, {
|
|
||||||
language: 'en',
|
|
||||||
});
|
|
||||||
|
|
||||||
const documents = res.results.map(
|
|
||||||
(result) =>
|
|
||||||
new Document({
|
|
||||||
pageContent: result.content,
|
|
||||||
metadata: {
|
|
||||||
title: result.title,
|
|
||||||
url: result.url,
|
|
||||||
...(result.img_src && { img_src: result.img_src }),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return { query: input, docs: documents };
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const basicWebSearchAnsweringChain = RunnableSequence.from([
|
|
||||||
RunnableMap.from({
|
RunnableMap.from({
|
||||||
query: (input: BasicChainInput) => input.query,
|
query: (input: BasicChainInput) => input.query,
|
||||||
chat_history: (input: BasicChainInput) => input.chat_history,
|
chat_history: (input: BasicChainInput) => input.chat_history,
|
||||||
|
@ -210,11 +210,22 @@ const basicWebSearchAnsweringChain = RunnableSequence.from([
|
||||||
]).withConfig({
|
]).withConfig({
|
||||||
runName: 'FinalResponseGenerator',
|
runName: 'FinalResponseGenerator',
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const basicWebSearch = (query: string, history: BaseMessage[]) => {
|
const basicWebSearch = (
|
||||||
|
query: string,
|
||||||
|
history: BaseMessage[],
|
||||||
|
llm: BaseChatModel,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
) => {
|
||||||
const emitter = new eventEmitter();
|
const emitter = new eventEmitter();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const basicWebSearchAnsweringChain = createBasicWebSearchAnsweringChain(
|
||||||
|
llm,
|
||||||
|
embeddings,
|
||||||
|
);
|
||||||
|
|
||||||
const stream = basicWebSearchAnsweringChain.streamEvents(
|
const stream = basicWebSearchAnsweringChain.streamEvents(
|
||||||
{
|
{
|
||||||
chat_history: history,
|
chat_history: history,
|
||||||
|
@ -237,8 +248,13 @@ const basicWebSearch = (query: string, history: BaseMessage[]) => {
|
||||||
return emitter;
|
return emitter;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleWebSearch = (message: string, history: BaseMessage[]) => {
|
const handleWebSearch = (
|
||||||
const emitter = basicWebSearch(message, history);
|
message: string,
|
||||||
|
history: BaseMessage[],
|
||||||
|
llm: BaseChatModel,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
) => {
|
||||||
|
const emitter = basicWebSearch(message, history, llm, embeddings);
|
||||||
return emitter;
|
return emitter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -9,19 +9,15 @@ import {
|
||||||
RunnableMap,
|
RunnableMap,
|
||||||
RunnableLambda,
|
RunnableLambda,
|
||||||
} from '@langchain/core/runnables';
|
} from '@langchain/core/runnables';
|
||||||
import { ChatOpenAI, OpenAI } from '@langchain/openai';
|
|
||||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
import { StringOutputParser } from '@langchain/core/output_parsers';
|
||||||
import { Document } from '@langchain/core/documents';
|
import { Document } from '@langchain/core/documents';
|
||||||
import { searchSearxng } from '../core/searxng';
|
import { searchSearxng } from '../core/searxng';
|
||||||
import type { StreamEvent } from '@langchain/core/tracers/log_stream';
|
import type { StreamEvent } from '@langchain/core/tracers/log_stream';
|
||||||
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
import type { Embeddings } from '@langchain/core/embeddings';
|
||||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
import formatChatHistoryAsString from '../utils/formatHistory';
|
||||||
import eventEmitter from 'events';
|
import eventEmitter from 'events';
|
||||||
|
|
||||||
const llm = new ChatOpenAI({
|
|
||||||
modelName: process.env.MODEL_NAME,
|
|
||||||
temperature: 0.7,
|
|
||||||
});
|
|
||||||
|
|
||||||
const basicWolframAlphaSearchRetrieverPrompt = `
|
const basicWolframAlphaSearchRetrieverPrompt = `
|
||||||
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the LLM to search the web for information.
|
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the LLM to search the web for information.
|
||||||
If it is a writing task or a simple hi, hello rather than a question, you need to return \`not_needed\` as the response.
|
If it is a writing task or a simple hi, hello rather than a question, you need to return \`not_needed\` as the response.
|
||||||
|
@ -99,18 +95,13 @@ const handleStream = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const processDocs = async (docs: Document[]) => {
|
|
||||||
return docs
|
|
||||||
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
|
|
||||||
.join('\n');
|
|
||||||
};
|
|
||||||
|
|
||||||
type BasicChainInput = {
|
type BasicChainInput = {
|
||||||
chat_history: BaseMessage[];
|
chat_history: BaseMessage[];
|
||||||
query: string;
|
query: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const basicWolframAlphaSearchRetrieverChain = RunnableSequence.from([
|
const createBasicWolframAlphaSearchRetrieverChain = (llm: BaseChatModel) => {
|
||||||
|
return RunnableSequence.from([
|
||||||
PromptTemplate.fromTemplate(basicWolframAlphaSearchRetrieverPrompt),
|
PromptTemplate.fromTemplate(basicWolframAlphaSearchRetrieverPrompt),
|
||||||
llm,
|
llm,
|
||||||
strParser,
|
strParser,
|
||||||
|
@ -139,8 +130,19 @@ const basicWolframAlphaSearchRetrieverChain = RunnableSequence.from([
|
||||||
return { query: input, docs: documents };
|
return { query: input, docs: documents };
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
const basicWolframAlphaSearchAnsweringChain = RunnableSequence.from([
|
const createBasicWolframAlphaSearchAnsweringChain = (llm: BaseChatModel) => {
|
||||||
|
const basicWolframAlphaSearchRetrieverChain =
|
||||||
|
createBasicWolframAlphaSearchRetrieverChain(llm);
|
||||||
|
|
||||||
|
const processDocs = (docs: Document[]) => {
|
||||||
|
return docs
|
||||||
|
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
|
||||||
|
.join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
return RunnableSequence.from([
|
||||||
RunnableMap.from({
|
RunnableMap.from({
|
||||||
query: (input: BasicChainInput) => input.query,
|
query: (input: BasicChainInput) => input.query,
|
||||||
chat_history: (input: BasicChainInput) => input.chat_history,
|
chat_history: (input: BasicChainInput) => input.chat_history,
|
||||||
|
@ -169,11 +171,18 @@ const basicWolframAlphaSearchAnsweringChain = RunnableSequence.from([
|
||||||
]).withConfig({
|
]).withConfig({
|
||||||
runName: 'FinalResponseGenerator',
|
runName: 'FinalResponseGenerator',
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const basicWolframAlphaSearch = (query: string, history: BaseMessage[]) => {
|
const basicWolframAlphaSearch = (
|
||||||
|
query: string,
|
||||||
|
history: BaseMessage[],
|
||||||
|
llm: BaseChatModel,
|
||||||
|
) => {
|
||||||
const emitter = new eventEmitter();
|
const emitter = new eventEmitter();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const basicWolframAlphaSearchAnsweringChain =
|
||||||
|
createBasicWolframAlphaSearchAnsweringChain(llm);
|
||||||
const stream = basicWolframAlphaSearchAnsweringChain.streamEvents(
|
const stream = basicWolframAlphaSearchAnsweringChain.streamEvents(
|
||||||
{
|
{
|
||||||
chat_history: history,
|
chat_history: history,
|
||||||
|
@ -196,8 +205,13 @@ const basicWolframAlphaSearch = (query: string, history: BaseMessage[]) => {
|
||||||
return emitter;
|
return emitter;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleWolframAlphaSearch = (message: string, history: BaseMessage[]) => {
|
const handleWolframAlphaSearch = (
|
||||||
const emitter = basicWolframAlphaSearch(message, history);
|
message: string,
|
||||||
|
history: BaseMessage[],
|
||||||
|
llm: BaseChatModel,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
) => {
|
||||||
|
const emitter = basicWolframAlphaSearch(message, history, llm);
|
||||||
return emitter;
|
return emitter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,15 +4,11 @@ import {
|
||||||
MessagesPlaceholder,
|
MessagesPlaceholder,
|
||||||
} from '@langchain/core/prompts';
|
} from '@langchain/core/prompts';
|
||||||
import { RunnableSequence } from '@langchain/core/runnables';
|
import { RunnableSequence } from '@langchain/core/runnables';
|
||||||
import { ChatOpenAI } from '@langchain/openai';
|
|
||||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
import { StringOutputParser } from '@langchain/core/output_parsers';
|
||||||
import type { StreamEvent } from '@langchain/core/tracers/log_stream';
|
import type { StreamEvent } from '@langchain/core/tracers/log_stream';
|
||||||
import eventEmitter from 'events';
|
import eventEmitter from 'events';
|
||||||
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
const llm = new ChatOpenAI({
|
import type { Embeddings } from '@langchain/core/embeddings';
|
||||||
modelName: process.env.MODEL_NAME,
|
|
||||||
temperature: 0.7,
|
|
||||||
});
|
|
||||||
|
|
||||||
const writingAssistantPrompt = `
|
const writingAssistantPrompt = `
|
||||||
You are Perplexica, an AI model who is expert at searching the web and answering user's queries. You are currently set on focus mode 'Writing Assistant', this means you will be helping the user write a response to a given query.
|
You are Perplexica, an AI model who is expert at searching the web and answering user's queries. You are currently set on focus mode 'Writing Assistant', this means you will be helping the user write a response to a given query.
|
||||||
|
@ -44,7 +40,8 @@ const handleStream = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const writingAssistantChain = RunnableSequence.from([
|
const createWritingAssistantChain = (llm: BaseChatModel) => {
|
||||||
|
return RunnableSequence.from([
|
||||||
ChatPromptTemplate.fromMessages([
|
ChatPromptTemplate.fromMessages([
|
||||||
['system', writingAssistantPrompt],
|
['system', writingAssistantPrompt],
|
||||||
new MessagesPlaceholder('chat_history'),
|
new MessagesPlaceholder('chat_history'),
|
||||||
|
@ -55,11 +52,18 @@ const writingAssistantChain = RunnableSequence.from([
|
||||||
]).withConfig({
|
]).withConfig({
|
||||||
runName: 'FinalResponseGenerator',
|
runName: 'FinalResponseGenerator',
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleWritingAssistant = (query: string, history: BaseMessage[]) => {
|
const handleWritingAssistant = (
|
||||||
|
query: string,
|
||||||
|
history: BaseMessage[],
|
||||||
|
llm: BaseChatModel,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
) => {
|
||||||
const emitter = new eventEmitter();
|
const emitter = new eventEmitter();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const writingAssistantChain = createWritingAssistantChain(llm);
|
||||||
const stream = writingAssistantChain.streamEvents(
|
const stream = writingAssistantChain.streamEvents(
|
||||||
{
|
{
|
||||||
chat_history: history,
|
chat_history: history,
|
||||||
|
|
|
@ -9,24 +9,16 @@ import {
|
||||||
RunnableMap,
|
RunnableMap,
|
||||||
RunnableLambda,
|
RunnableLambda,
|
||||||
} from '@langchain/core/runnables';
|
} from '@langchain/core/runnables';
|
||||||
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
|
|
||||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
import { StringOutputParser } from '@langchain/core/output_parsers';
|
||||||
import { Document } from '@langchain/core/documents';
|
import { Document } from '@langchain/core/documents';
|
||||||
import { searchSearxng } from '../core/searxng';
|
import { searchSearxng } from '../core/searxng';
|
||||||
import type { StreamEvent } from '@langchain/core/tracers/log_stream';
|
import type { StreamEvent } from '@langchain/core/tracers/log_stream';
|
||||||
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
import type { Embeddings } from '@langchain/core/embeddings';
|
||||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
import formatChatHistoryAsString from '../utils/formatHistory';
|
||||||
import eventEmitter from 'events';
|
import eventEmitter from 'events';
|
||||||
import computeSimilarity from '../utils/computeSimilarity';
|
import computeSimilarity from '../utils/computeSimilarity';
|
||||||
|
|
||||||
const llm = new ChatOpenAI({
|
|
||||||
modelName: process.env.MODEL_NAME,
|
|
||||||
temperature: 0.7,
|
|
||||||
});
|
|
||||||
|
|
||||||
const embeddings = new OpenAIEmbeddings({
|
|
||||||
modelName: 'text-embedding-3-large',
|
|
||||||
});
|
|
||||||
|
|
||||||
const basicYoutubeSearchRetrieverPrompt = `
|
const basicYoutubeSearchRetrieverPrompt = `
|
||||||
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the LLM to search the web for information.
|
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the LLM to search the web for information.
|
||||||
If it is a writing task or a simple hi, hello rather than a question, you need to return \`not_needed\` as the response.
|
If it is a writing task or a simple hi, hello rather than a question, you need to return \`not_needed\` as the response.
|
||||||
|
@ -104,6 +96,50 @@ const handleStream = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BasicChainInput = {
|
||||||
|
chat_history: BaseMessage[];
|
||||||
|
query: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBasicYoutubeSearchRetrieverChain = (llm: BaseChatModel) => {
|
||||||
|
return RunnableSequence.from([
|
||||||
|
PromptTemplate.fromTemplate(basicYoutubeSearchRetrieverPrompt),
|
||||||
|
llm,
|
||||||
|
strParser,
|
||||||
|
RunnableLambda.from(async (input: string) => {
|
||||||
|
if (input === 'not_needed') {
|
||||||
|
return { query: '', docs: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await searchSearxng(input, {
|
||||||
|
language: 'en',
|
||||||
|
engines: ['youtube'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const documents = res.results.map(
|
||||||
|
(result) =>
|
||||||
|
new Document({
|
||||||
|
pageContent: result.content ? result.content : result.title,
|
||||||
|
metadata: {
|
||||||
|
title: result.title,
|
||||||
|
url: result.url,
|
||||||
|
...(result.img_src && { img_src: result.img_src }),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { query: input, docs: documents };
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBasicYoutubeSearchAnsweringChain = (
|
||||||
|
llm: BaseChatModel,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
) => {
|
||||||
|
const basicYoutubeSearchRetrieverChain =
|
||||||
|
createBasicYoutubeSearchRetrieverChain(llm);
|
||||||
|
|
||||||
const processDocs = async (docs: Document[]) => {
|
const processDocs = async (docs: Document[]) => {
|
||||||
return docs
|
return docs
|
||||||
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
|
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
|
||||||
|
@ -149,42 +185,7 @@ const rerankDocs = async ({
|
||||||
return sortedDocs;
|
return sortedDocs;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BasicChainInput = {
|
return RunnableSequence.from([
|
||||||
chat_history: BaseMessage[];
|
|
||||||
query: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const basicYoutubeSearchRetrieverChain = RunnableSequence.from([
|
|
||||||
PromptTemplate.fromTemplate(basicYoutubeSearchRetrieverPrompt),
|
|
||||||
llm,
|
|
||||||
strParser,
|
|
||||||
RunnableLambda.from(async (input: string) => {
|
|
||||||
if (input === 'not_needed') {
|
|
||||||
return { query: '', docs: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await searchSearxng(input, {
|
|
||||||
language: 'en',
|
|
||||||
engines: ['youtube'],
|
|
||||||
});
|
|
||||||
|
|
||||||
const documents = res.results.map(
|
|
||||||
(result) =>
|
|
||||||
new Document({
|
|
||||||
pageContent: result.content ? result.content : result.title,
|
|
||||||
metadata: {
|
|
||||||
title: result.title,
|
|
||||||
url: result.url,
|
|
||||||
...(result.img_src && { img_src: result.img_src }),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return { query: input, docs: documents };
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const basicYoutubeSearchAnsweringChain = RunnableSequence.from([
|
|
||||||
RunnableMap.from({
|
RunnableMap.from({
|
||||||
query: (input: BasicChainInput) => input.query,
|
query: (input: BasicChainInput) => input.query,
|
||||||
chat_history: (input: BasicChainInput) => input.chat_history,
|
chat_history: (input: BasicChainInput) => input.chat_history,
|
||||||
|
@ -211,11 +212,20 @@ const basicYoutubeSearchAnsweringChain = RunnableSequence.from([
|
||||||
]).withConfig({
|
]).withConfig({
|
||||||
runName: 'FinalResponseGenerator',
|
runName: 'FinalResponseGenerator',
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const basicYoutubeSearch = (query: string, history: BaseMessage[]) => {
|
const basicYoutubeSearch = (
|
||||||
|
query: string,
|
||||||
|
history: BaseMessage[],
|
||||||
|
llm: BaseChatModel,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
) => {
|
||||||
const emitter = new eventEmitter();
|
const emitter = new eventEmitter();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const basicYoutubeSearchAnsweringChain =
|
||||||
|
createBasicYoutubeSearchAnsweringChain(llm, embeddings);
|
||||||
|
|
||||||
const stream = basicYoutubeSearchAnsweringChain.streamEvents(
|
const stream = basicYoutubeSearchAnsweringChain.streamEvents(
|
||||||
{
|
{
|
||||||
chat_history: history,
|
chat_history: history,
|
||||||
|
@ -238,8 +248,13 @@ const basicYoutubeSearch = (query: string, history: BaseMessage[]) => {
|
||||||
return emitter;
|
return emitter;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleYoutubeSearch = (message: string, history: BaseMessage[]) => {
|
const handleYoutubeSearch = (
|
||||||
const emitter = basicYoutubeSearch(message, history);
|
message: string,
|
||||||
|
history: BaseMessage[],
|
||||||
|
llm: BaseChatModel,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
) => {
|
||||||
|
const emitter = basicYoutubeSearch(message, history, llm, embeddings);
|
||||||
return emitter;
|
return emitter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,9 @@ import express from 'express';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
|
import { getPort } from './config';
|
||||||
|
|
||||||
|
const port = getPort();
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
|
@ -19,8 +22,8 @@ app.get('/api', (_, res) => {
|
||||||
res.status(200).json({ status: 'ok' });
|
res.status(200).json({ status: 'ok' });
|
||||||
});
|
});
|
||||||
|
|
||||||
server.listen(process.env.PORT!, () => {
|
server.listen(port, () => {
|
||||||
console.log(`API server started on port ${process.env.PORT}`);
|
console.log(`API server started on port ${port}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
startWebSocketServer(server);
|
startWebSocketServer(server);
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import toml, { JsonMap } from '@iarna/toml';
|
||||||
|
|
||||||
|
const configFileName = 'config.toml';
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
GENERAL: {
|
||||||
|
PORT: number;
|
||||||
|
SIMILARITY_MEASURE: string;
|
||||||
|
};
|
||||||
|
API_KEYS: {
|
||||||
|
OPENAI: string;
|
||||||
|
};
|
||||||
|
API_ENDPOINTS: {
|
||||||
|
SEARXNG: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadConfig = () =>
|
||||||
|
toml.parse(
|
||||||
|
fs.readFileSync(path.join(process.cwd(), `${configFileName}`), 'utf-8'),
|
||||||
|
) as any as Config;
|
||||||
|
|
||||||
|
export const getPort = () => loadConfig().GENERAL.PORT;
|
||||||
|
|
||||||
|
export const getSimilarityMeasure = () =>
|
||||||
|
loadConfig().GENERAL.SIMILARITY_MEASURE;
|
||||||
|
|
||||||
|
export const getOpenaiApiKey = () => loadConfig().API_KEYS.OPENAI;
|
||||||
|
|
||||||
|
export const getSearxngApiEndpoint = () => loadConfig().API_ENDPOINTS.SEARXNG;
|
|
@ -1,4 +1,5 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { getSearxngApiEndpoint } from '../config';
|
||||||
|
|
||||||
interface SearxngSearchOptions {
|
interface SearxngSearchOptions {
|
||||||
categories?: string[];
|
categories?: string[];
|
||||||
|
@ -20,7 +21,9 @@ export const searchSearxng = async (
|
||||||
query: string,
|
query: string,
|
||||||
opts?: SearxngSearchOptions,
|
opts?: SearxngSearchOptions,
|
||||||
) => {
|
) => {
|
||||||
const url = new URL(`${process.env.SEARXNG_API_URL}/search?format=json`);
|
const searxngURL = getSearxngApiEndpoint();
|
||||||
|
|
||||||
|
const url = new URL(`${searxngURL}/search?format=json`);
|
||||||
url.searchParams.append('q', query);
|
url.searchParams.append('q', query);
|
||||||
|
|
||||||
if (opts) {
|
if (opts) {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import imageSearchChain from '../agents/imageSearchAgent';
|
import handleImageSearch from '../agents/imageSearchAgent';
|
||||||
|
import { ChatOpenAI } from '@langchain/openai';
|
||||||
|
import { getOpenaiApiKey } from '../config';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
@ -7,11 +9,13 @@ router.post('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { query, chat_history } = req.body;
|
const { query, chat_history } = req.body;
|
||||||
|
|
||||||
const images = await imageSearchChain.invoke({
|
const llm = new ChatOpenAI({
|
||||||
query,
|
temperature: 0.7,
|
||||||
chat_history,
|
openAIApiKey: getOpenaiApiKey(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const images = await handleImageSearch({ query, chat_history }, llm);
|
||||||
|
|
||||||
res.status(200).json({ images });
|
res.status(200).json({ images });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ message: 'An error has occurred.' });
|
res.status(500).json({ message: 'An error has occurred.' });
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import dot from 'compute-dot';
|
import dot from 'compute-dot';
|
||||||
import cosineSimilarity from 'compute-cosine-similarity';
|
import cosineSimilarity from 'compute-cosine-similarity';
|
||||||
|
import { getSimilarityMeasure } from '../config';
|
||||||
|
|
||||||
const computeSimilarity = (x: number[], y: number[]): number => {
|
const computeSimilarity = (x: number[], y: number[]): number => {
|
||||||
if (process.env.SIMILARITY_MEASURE === 'cosine') {
|
const similarityMeasure = getSimilarityMeasure();
|
||||||
|
|
||||||
|
if (similarityMeasure === 'cosine') {
|
||||||
return cosineSimilarity(x, y);
|
return cosineSimilarity(x, y);
|
||||||
} else if (process.env.SIMILARITY_MEASURE === 'dot') {
|
} else if (similarityMeasure === 'dot') {
|
||||||
return dot(x, y);
|
return dot(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,23 @@
|
||||||
import { WebSocket } from 'ws';
|
import { WebSocket } from 'ws';
|
||||||
import { handleMessage } from './messageHandler';
|
import { handleMessage } from './messageHandler';
|
||||||
|
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
|
||||||
|
import { getOpenaiApiKey } from '../config';
|
||||||
|
|
||||||
export const handleConnection = (ws: WebSocket) => {
|
export const handleConnection = (ws: WebSocket) => {
|
||||||
|
const llm = new ChatOpenAI({
|
||||||
|
temperature: 0.7,
|
||||||
|
openAIApiKey: getOpenaiApiKey(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const embeddings = new OpenAIEmbeddings({
|
||||||
|
openAIApiKey: getOpenaiApiKey(),
|
||||||
|
modelName: 'text-embedding-3-large',
|
||||||
|
});
|
||||||
|
|
||||||
ws.on(
|
ws.on(
|
||||||
'message',
|
'message',
|
||||||
async (message) => await handleMessage(message.toString(), ws),
|
async (message) =>
|
||||||
|
await handleMessage(message.toString(), ws, llm, embeddings),
|
||||||
);
|
);
|
||||||
|
|
||||||
ws.on('close', () => console.log('Connection closed'));
|
ws.on('close', () => console.log('Connection closed'));
|
||||||
|
|
|
@ -6,6 +6,8 @@ import handleWritingAssistant from '../agents/writingAssistant';
|
||||||
import handleWolframAlphaSearch from '../agents/wolframAlphaSearchAgent';
|
import handleWolframAlphaSearch from '../agents/wolframAlphaSearchAgent';
|
||||||
import handleYoutubeSearch from '../agents/youtubeSearchAgent';
|
import handleYoutubeSearch from '../agents/youtubeSearchAgent';
|
||||||
import handleRedditSearch from '../agents/redditSearchAgent';
|
import handleRedditSearch from '../agents/redditSearchAgent';
|
||||||
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
import type { Embeddings } from '@langchain/core/embeddings';
|
||||||
|
|
||||||
type Message = {
|
type Message = {
|
||||||
type: string;
|
type: string;
|
||||||
|
@ -58,7 +60,12 @@ const handleEmitterEvents = (
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleMessage = async (message: string, ws: WebSocket) => {
|
export const handleMessage = async (
|
||||||
|
message: string,
|
||||||
|
ws: WebSocket,
|
||||||
|
llm: BaseChatModel,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const parsedMessage = JSON.parse(message) as Message;
|
const parsedMessage = JSON.parse(message) as Message;
|
||||||
const id = Math.random().toString(36).substring(7);
|
const id = Math.random().toString(36).substring(7);
|
||||||
|
@ -83,7 +90,12 @@ export const handleMessage = async (message: string, ws: WebSocket) => {
|
||||||
if (parsedMessage.type === 'message') {
|
if (parsedMessage.type === 'message') {
|
||||||
const handler = searchHandlers[parsedMessage.focusMode];
|
const handler = searchHandlers[parsedMessage.focusMode];
|
||||||
if (handler) {
|
if (handler) {
|
||||||
const emitter = handler(parsedMessage.content, history);
|
const emitter = handler(
|
||||||
|
parsedMessage.content,
|
||||||
|
history,
|
||||||
|
llm,
|
||||||
|
embeddings,
|
||||||
|
);
|
||||||
handleEmitterEvents(emitter, ws, id);
|
handleEmitterEvents(emitter, ws, id);
|
||||||
} else {
|
} else {
|
||||||
ws.send(JSON.stringify({ type: 'error', data: 'Invalid focus mode' }));
|
ws.send(JSON.stringify({ type: 'error', data: 'Invalid focus mode' }));
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import { WebSocketServer } from 'ws';
|
import { WebSocketServer } from 'ws';
|
||||||
import { handleConnection } from './connectionManager';
|
import { handleConnection } from './connectionManager';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
|
import { getPort } from '../config';
|
||||||
|
|
||||||
export const initServer = (
|
export const initServer = (
|
||||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||||
) => {
|
) => {
|
||||||
|
const port = getPort();
|
||||||
const wss = new WebSocketServer({ server });
|
const wss = new WebSocketServer({ server });
|
||||||
|
|
||||||
wss.on('connection', (ws) => {
|
wss.on('connection', (ws) => {
|
||||||
handleConnection(ws);
|
handleConnection(ws);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`WebSocket server started on port ${process.env.PORT}`);
|
console.log(`WebSocket server started on port ${port}`);
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,6 +24,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@jridgewell/trace-mapping" "0.3.9"
|
"@jridgewell/trace-mapping" "0.3.9"
|
||||||
|
|
||||||
|
"@iarna/toml@^2.2.5":
|
||||||
|
version "2.2.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c"
|
||||||
|
integrity sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==
|
||||||
|
|
||||||
"@jridgewell/resolve-uri@^3.0.3":
|
"@jridgewell/resolve-uri@^3.0.3":
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
|
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
|
||||||
|
|
Loading…
Reference in New Issue