Compare commits

4 Commits

Author SHA1 Message Date
ItzCrazyKns
2f20d845c8 feat(image-search): Use baseURL from env 2024-04-17 20:33:29 +05:30
ItzCrazyKns
7a7eafb8e7 Merge branch 'master' into feat/ollama-support 2024-04-17 19:58:53 +05:30
ItzCrazyKns
043f66b767 feat(readme): Update installation steps 2024-04-17 19:42:08 +05:30
ItzCrazyKns
811822c03b feat(agents): use ollama models 2024-04-17 10:22:20 +05:30
52 changed files with 857 additions and 2348 deletions

5
.env.example Normal file
View File

@ -0,0 +1,5 @@
PORT=3001
OLLAMA_URL=http://localhost:11434 # url of the ollama server
SIMILARITY_MEASURE=cosine # cosine or dot
SEARXNG_API_URL= # no need to fill this if using docker
MODEL_NAME=llama2

View File

@ -4,6 +4,7 @@ about: Create an issue to help us fix bugs
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
@ -11,7 +12,6 @@ A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'

View File

@ -4,4 +4,7 @@ about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''
---

View File

@ -4,6 +4,7 @@ about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**

3
.gitignore vendored
View File

@ -19,9 +19,6 @@ yarn-error.log
.env.test.local
.env.production.local
# Config files
config.toml
# Log files
logs/
*.log

View File

@ -1,38 +0,0 @@
# 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

View File

@ -9,14 +9,16 @@ Perplexica's design consists of two main domains:
- **Frontend (`ui` directory)**: This is a Next.js application holding all user interface components. It's a self-contained environment that manages everything the user interacts with.
- **Backend (root and `src` directory)**: The backend logic is situated in the `src` folder, but the root directory holds the main `package.json` for backend dependency management.
Both the root directory (for backend configurations outside `src`) and the `ui` folder come with an `.env.example` file. These are templates for environment variables that you need to set up manually for the application to run correctly.
## Setting Up Your Environment
Before diving into coding, setting up your local environment is key. Here's what you need to do:
### Backend
1. In the root directory, locate the `sample.config.toml` file.
2. Rename it to `config.toml` and fill in the necessary configuration fields specific to the backend.
1. In the root directory, locate the `.env.example` file.
2. Rename it to `.env` and fill in the necessary environment variables specific to the backend.
3. Run `npm install` to install dependencies.
4. Use `npm run dev` to start the backend in development mode.

View File

@ -10,38 +10,34 @@
- [Installation](#installation)
- [Getting Started with Docker (Recommended)](#getting-started-with-docker-recommended)
- [Non-Docker Installation](#non-docker-installation)
- [One-Click Deployment](#one-click-deployment)
- [Upcoming Features](#upcoming-features)
- [Support Us](#support-us)
- [Contribution](#contribution)
- [Help and Support](#help-and-support)
- [Acknowledgements](#acknowledgements)
## Overview
Perplexica is an open-source AI-powered searching tool or an AI-powered search engine that goes deep into the internet to find answers. Inspired by Perplexity AI, it's an open-source option that not just searches the web but understands your questions. It uses advanced machine learning algorithms like similarity searching and embeddings to refine results and provides clear answers with sources cited.
Using SearxNG to stay current and fully open source, Perplexica ensures you always get the most up-to-date information without compromising your privacy.
Want to know more about its architecture and how it works? You can read it [here](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/architecture/README.md).
## Preview
![video-preview](.assets/perplexica-preview.gif)
## Features
- **Local LLMs**: You can make use local LLMs such as Llama3 and Mixtral using Ollama.
- **Two Main Modes:**
- **Copilot Mode:** (In development) Boosts search by generating different queries to find more relevant internet sources. Like normal search instead of just using the context by SearxNG, it visits the top matches and tries to find relevant sources to the user's query directly from the page.
- **Normal Mode:** Processes your query and performs a web search.
- **Focus Modes:** Special modes to better answer specific types of questions. Perplexica currently has 6 focus modes:
- **All Mode:** Searches the entire web to find the best results.
- **Writing Assistant Mode:** Helpful for writing tasks that does not require searching the web.
- **Academic Search Mode:** Finds articles and papers, ideal for academic research.
- **YouTube Search Mode:** Finds YouTube videos based on the search query.
- **Wolfram Alpha Search Mode:** Answers queries that need calculations or data analysis using Wolfram Alpha.
- **Reddit Search Mode:** Searches Reddit for discussions and opinions related to the query.
- **Current Information:** Some search tools might give you outdated info because they use data from crawling bots and convert them into embeddings and store them in a index. Unlike them, Perplexica uses SearxNG, a metasearch engine to get the results and rerank and get the most relevant source out of it, ensuring you always get the latest information without the overhead of daily data updates.
1. **All Mode:** Searches the entire web to find the best results.
2. **Writing Assistant Mode:** Helpful for writing tasks that does not require searching the web.
3. **Academic Search Mode:** Finds articles and papers, ideal for academic research.
4. **YouTube Search Mode:** Finds YouTube videos based on the search query.
5. **Wolfram Alpha Search Mode:** Answers queries that need calculations or data analysis using Wolfram Alpha.
6. **Reddit Search Mode:** Searches Reddit for discussions and opinions related to the query.
- **Current Information:** Some search tools might give you outdated info because they use data from crawling bots and convert them into embeddings and store them in a index (its like converting the web into embeddings which is quite expensive.). Unlike them, Perplexica uses SearxNG, a metasearch engine to get the results and rerank and get the most relevent source out of it, ensuring you always get the latest information without the overhead of daily data updates.
It has many more features like image and video search. Some of the planned features are mentioned in [upcoming features](#upcoming-features).
@ -55,50 +51,41 @@ There are mainly 2 ways of installing Perplexica - With Docker, Without Docker.
2. Clone the Perplexica repository:
```bash
git clone https://github.com/ItzCrazyKns/Perplexica.git
git clone -b feat/ollama-support https://github.com/ItzCrazyKns/Perplexica.git
```
3. After cloning, navigate to the directory containing the project files.
4. Rename the `sample.config.toml` file to `config.toml`. For Docker setups, you need only fill in the following fields:
4. Rename the `.env.example` file to `.env`. For Docker setups, you need only fill in the following fields:
- `OPENAI`: Your OpenAI API key. **You only need to fill this if you wish to use OpenAI's models**.
- `OLLAMA`: Your Ollama API URL. You should enter it as `http://host.docker.internal:PORT_NUMBER`. If you installed Ollama on port 11434, use `http://host.docker.internal:11434`. For other ports, adjust accordingly. **You need to fill this if you wish to use Ollama's models instead of OpenAI's**.
- `GROQ`: Your Groq API key. **You only need to fill this if you wish to use Groq's hosted models**
**Note**: You can change these after starting Perplexica from the settings dialog.
- `SIMILARITY_MEASURE`: The similarity measure to use (This is filled by default; you can leave it as is if you are unsure about it.)
- `OLLAMA_URL` (It should be the URL where Ollama is running; it is also filled by default but you need to replace it if your Ollama URL is different.)
- `MODEL_NAME` (This is filled by default; you can change it if you want to use a different model.)
- `SIMILARITY_MEASURE` (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:
```bash
docker compose up -d
docker compose up
```
6. Wait a few minutes for the setup to complete. You can access Perplexica at http://localhost:3000 in your web browser.
**Note**: After the containers are built, you can start Perplexica directly from Docker without having to open a terminal.
**Note**: Once the terminal is stopped, Perplexica will also stop. To restart it, you will need to open Docker Desktop and run Perplexica again.
### Non-Docker Installation
1. Clone the repository and rename the `sample.config.toml` file to `config.toml` in the root directory. Ensure you complete all required fields in this file.
2. Rename the `.env.example` file to `.env` in the `ui` folder and fill in all necessary fields.
3. After populating the configuration and environment files, run `npm i` in both the `ui` folder and the root directory.
4. Install the dependencies and then execute `npm run build` in both the `ui` folder and the root directory.
5. Finally, start both the frontend and the backend by running `npm run start` in both the `ui` folder and the root directory.
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.
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.
**Note**: Using Docker is recommended as it simplifies the setup process, especially for managing environment variables and dependencies.
## One-Click Deployment
[![Deploy to RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploylobe.svg)](https://repocloud.io/details/?app_id=267)
## Upcoming Features
- [ ] Finalizing Copilot Mode
- [x] Add settings page
- [x] Adding support for local LLMs
- [ ] Adding support for multiple local LLMs and LLM providers such as Anthropic, Google, etc.
- [ ] Adding Discover and History Saving features
- [x] Introducing various Focus Modes
@ -110,8 +97,8 @@ If you find Perplexica useful, consider giving us a star on GitHub. This helps m
Perplexica is built on the idea that AI and large language models should be easy for everyone to use. If you find bugs or have ideas, please share them in via GitHub Issues. For more information on contributing to Perplexica you can read the [CONTRIBUTING.md](CONTRIBUTING.md) file to learn more about Perplexica and how you can contribute to it.
## Help and Support
## Acknowledgements
If you have any questions or feedback, please feel free to reach out to us. You can create an issue on GitHub or join our Discord server. There, you can connect with other users, share your experiences and reviews, and receive more personalized help. [Click here](https://discord.gg/EFwsmQDgAu) to join the Discord server. To discuss matters outside of regular support, feel free to contact me on Discord at `itzcrazykns`.
Inspired by Perplexity AI, Perplexica aims to provide a similar service but always up-to-date and fully open source, thanks to SearxNG.
Thank you for exploring Perplexica, the AI-powered search engine designed to enhance your search experience. We are constantly working to improve Perplexica and expand its capabilities. We value your feedback and contributions which help us make Perplexica even better. Don't forget to check back for updates and new features!
If you have any queries you can reach me via my Discord - `itzcrazykns`. Thanks for checking out Perplexica.

View File

@ -1,17 +1,16 @@
FROM node:alpine
ARG SEARXNG_API_URL
ENV SEARXNG_API_URL=${SEARXNG_API_URL}
WORKDIR /home/perplexica
COPY src /home/perplexica/src
COPY tsconfig.json /home/perplexica/
COPY config.toml /home/perplexica/
COPY .env /home/perplexica/
COPY package.json /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 build

View File

@ -9,7 +9,6 @@ services:
- 4000:8080
networks:
- perplexica-network
perplexica-backend:
build:
context: .

View File

@ -1,11 +0,0 @@
## Perplexica's Architecture
Perplexica's architecture consists of the following key components:
1. **User Interface**: A web-based interface that allows users to interact with Perplexica for searching images, videos, and much more.
2. **Agent/Chains**: These components predict Perplexica's next actions, understand user queries, and decide whether a web search is necessary.
3. **SearXNG**: A metadata search engine used by Perplexica to search the web for sources.
4. **LLMs (Large Language Models)**: Utilized by agents and chains for tasks like understanding content, writing responses, and citing sources. Examples include Claude, GPTs, etc.
5. **Embedding Models**: To improve the accuracy of search results, embedding models re-rank the results using similarity search algorithms such as cosine similarity and dot product distance.
For a more detailed explanation of how these components work together, see [WORKING.md](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/architecture/WORKING.md).

View File

@ -1,19 +0,0 @@
## How does Perplexica work?
Curious about how Perplexica works? Don't worry, we'll cover it here. Before we begin, make sure you've read about the architecture of Perplexica to ensure you understand what it's made up of. Haven't read it? You can read it [here](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/architecture/README.md).
We'll understand how Perplexica works by taking an example of a scenario where a user asks: "How does an A.C. work?". We'll break down the process into steps to make it easier to understand. The steps are as follows:
1. The message is sent via WS to the backend server where it invokes the chain. The chain will depend on your focus mode. For this example, let's assume we use the "webSearch" focus mode.
2. The chain is now invoked; first, the message is passed to another chain where it first predicts (using the chat history and the question) whether there is a need for sources or searching the web. If there is, it will generate a query (in accordance with the chat history) for searching the web that we'll take up later. If not, the chain will end there, and then the answer generator chain, also known as the response generator, will be started.
3. The query returned by the first chain is passed to SearXNG to search the web for information.
4. After the information is retrieved, it is based on keyword-based search. We then convert the information into embeddings and the query as well, then we perform a similarity search to find the most relevant sources to answer the query.
5. After all this is done, the sources are passed to the response generator. This chain takes all the chat history, the query, and the sources. It generates a response that is streamed to the UI.
### How are the answers cited?
The LLMs are prompted to do so. We've prompted them so well that they cite the answers themselves, and using some UI magic, we display it to the user.
### Image and Video Search
Image and video searches are conducted in a similar manner. A query is always generated first, then we search the web for images and videos that match the query. These results are then returned to the user.

View File

@ -1,12 +1,12 @@
{
"name": "perplexica-backend",
"version": "1.2.0",
"version": "1.0.0",
"license": "MIT",
"author": "ItzCrazyKns",
"scripts": {
"start": "node dist/app.js",
"start": "node --env-file=.env dist/app.js",
"build": "tsc",
"dev": "nodemon src/app.ts",
"dev": "nodemon -r dotenv/config src/app.ts",
"format": "prettier . --check",
"format:write": "prettier . --write"
},
@ -14,13 +14,11 @@
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/readable-stream": "^4.0.11",
"nodemon": "^3.1.0",
"prettier": "^3.2.5",
"ts-node": "^10.9.2",
"typescript": "^5.4.3"
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"@langchain/openai": "^0.0.25",
"axios": "^1.6.8",
"compute-cosine-similarity": "^1.1.0",
@ -29,8 +27,6 @@
"dotenv": "^16.4.5",
"express": "^4.19.2",
"langchain": "^0.1.30",
"react-text-to-speech": "^0.14.5",
"winston": "^3.13.0",
"ws": "^8.16.0",
"zod": "^3.22.4"
}

View File

@ -1,11 +0,0 @@
[GENERAL]
PORT = 3001 # Port to run the server on
SIMILARITY_MEASURE = "cosine" # "cosine" or "dot"
[API_KEYS]
OPENAI = "" # OpenAI API key - sk-1234567890abcdef1234567890abcdef
GROQ = "" # Groq API key - gsk_1234567890abcdef1234567890abcdef
[API_ENDPOINTS]
SEARXNG = "http://localhost:32768" # SearxNG API URL
OLLAMA = "" # Ollama API URL - http://host.docker.internal:11434

View File

@ -1071,6 +1071,25 @@ engines:
require_api_key: false
results: HTML
- name: azlyrics
shortcut: lyrics
engine: xpath
timeout: 4.0
disabled: true
categories: [music, lyrics]
paging: true
search_url: https://search.azlyrics.com/search.php?q={query}&w=lyrics&p={pageno}
url_xpath: //td[@class="text-left visitedlyr"]/a/@href
title_xpath: //span/b/text()
content_xpath: //td[@class="text-left visitedlyr"]/a/small
about:
website: https://azlyrics.com
wikidata_id: Q66372542
official_api_documentation:
use_official_api: false
require_api_key: false
results: HTML
- name: mastodon users
engine: mastodon
mastodon_type: accounts
@ -1550,6 +1569,11 @@ engines:
shortcut: scc
disabled: true
- name: framalibre
engine: framalibre
shortcut: frl
disabled: true
# - name: searx
# engine: searx_engine
# shortcut: se

View File

@ -9,16 +9,33 @@ import {
RunnableMap,
RunnableLambda,
} from '@langchain/core/runnables';
import { ChatOllama } from '@langchain/community/chat_models/ollama';
import { Ollama } from '@langchain/community/llms/ollama';
import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { Document } from '@langchain/core/documents';
import { searchSearxng } from '../lib/searxng';
import { searchSearxng } from '../core/searxng';
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 eventEmitter from 'events';
import computeSimilarity from '../utils/computeSimilarity';
import logger from '../utils/logger';
const chatLLM = new ChatOllama({
baseUrl: process.env.OLLAMA_URL,
model: process.env.MODEL_NAME,
temperature: 0.7,
});
const llm = new Ollama({
temperature: 0,
model: process.env.MODEL_NAME,
baseUrl: process.env.OLLAMA_URL,
});
const embeddings = new OllamaEmbeddings({
model: process.env.MODEL_NAME,
baseUrl: process.env.OLLAMA_URL,
});
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.
@ -42,7 +59,7 @@ Rephrased question:
`;
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 'Academic', 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 'Acadedemic', 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).
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.
@ -97,139 +114,122 @@ const handleStream = async (
}
};
const processDocs = async (docs: Document[]) => {
return docs
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
.join('\n');
};
const rerankDocs = async ({
query,
docs,
}: {
query: string;
docs: Document[];
}) => {
if (docs.length === 0) {
return docs;
}
const docsWithContent = docs.filter(
(doc) => doc.pageContent && doc.pageContent.length > 0,
);
const docEmbeddings = await embeddings.embedDocuments(
docsWithContent.map((doc) => doc.pageContent),
);
const queryEmbedding = await embeddings.embedQuery(query);
const similarity = docEmbeddings.map((docEmbedding, i) => {
const sim = computeSimilarity(queryEmbedding, docEmbedding);
return {
index: i,
similarity: sim,
};
});
const sortedDocs = similarity
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 15)
.map((sim) => docsWithContent[sim.index]);
return sortedDocs;
};
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',
'internetarchivescholar',
'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[]) => {
return docs
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
.join('\n');
};
const rerankDocs = async ({
query,
docs,
}: {
query: string;
docs: Document[];
}) => {
if (docs.length === 0) {
return docs;
const basicAcademicSearchRetrieverChain = RunnableSequence.from([
PromptTemplate.fromTemplate(basicAcademicSearchRetrieverPrompt),
llm,
strParser,
RunnableLambda.from(async (input: string) => {
if (input === 'not_needed') {
return { query: '', docs: [] };
}
const docsWithContent = docs.filter(
(doc) => doc.pageContent && doc.pageContent.length > 0,
);
const [docEmbeddings, queryEmbedding] = await Promise.all([
embeddings.embedDocuments(docsWithContent.map((doc) => doc.pageContent)),
embeddings.embedQuery(query),
]);
const similarity = docEmbeddings.map((docEmbedding, i) => {
const sim = computeSimilarity(queryEmbedding, docEmbedding);
return {
index: i,
similarity: sim,
};
const res = await searchSearxng(input, {
language: 'en',
engines: [
'arxiv',
'google_scholar',
'internet_archive_scholar',
'pubmed',
],
});
const sortedDocs = similarity
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 15)
.map((sim) => docsWithContent[sim.index]);
return sortedDocs;
};
return RunnableSequence.from([
RunnableMap.from({
query: (input: BasicChainInput) => input.query,
chat_history: (input: BasicChainInput) => input.chat_history,
context: RunnableSequence.from([
(input) => ({
query: input.query,
chat_history: formatChatHistoryAsString(input.chat_history),
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 }),
},
}),
basicAcademicSearchRetrieverChain
.pipe(rerankDocs)
.withConfig({
runName: 'FinalSourceRetriever',
})
.pipe(processDocs),
]),
}),
ChatPromptTemplate.fromMessages([
['system', basicAcademicSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]),
llm,
strParser,
]).withConfig({
runName: 'FinalResponseGenerator',
});
};
);
const basicAcademicSearch = (
query: string,
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
) => {
return { query: input, docs: documents };
}),
]);
const basicAcademicSearchAnsweringChain = RunnableSequence.from([
RunnableMap.from({
query: (input: BasicChainInput) => input.query,
chat_history: (input: BasicChainInput) => input.chat_history,
context: RunnableSequence.from([
(input) => ({
query: input.query,
chat_history: formatChatHistoryAsString(input.chat_history),
}),
basicAcademicSearchRetrieverChain
.pipe(rerankDocs)
.withConfig({
runName: 'FinalSourceRetriever',
})
.pipe(processDocs),
]),
}),
ChatPromptTemplate.fromMessages([
['system', basicAcademicSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]),
chatLLM,
strParser,
]).withConfig({
runName: 'FinalResponseGenerator',
});
const basicAcademicSearch = (query: string, history: BaseMessage[]) => {
const emitter = new eventEmitter();
try {
const basicAcademicSearchAnsweringChain =
createBasicAcademicSearchAnsweringChain(llm, embeddings);
const stream = basicAcademicSearchAnsweringChain.streamEvents(
{
chat_history: history,
@ -246,19 +246,14 @@ const basicAcademicSearch = (
'error',
JSON.stringify({ data: 'An error has occurred please try again later' }),
);
logger.error(`Error in academic search: ${err}`);
console.error(err);
}
return emitter;
};
const handleAcademicSearch = (
message: string,
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
) => {
const emitter = basicAcademicSearch(message, history, llm, embeddings);
const handleAcademicSearch = (message: string, history: BaseMessage[]) => {
const emitter = basicAcademicSearch(message, history);
return emitter;
};

View File

@ -4,11 +4,17 @@ import {
RunnableLambda,
} from '@langchain/core/runnables';
import { PromptTemplate } from '@langchain/core/prompts';
import { Ollama } from '@langchain/community/llms/ollama';
import formatChatHistoryAsString from '../utils/formatHistory';
import { BaseMessage } from '@langchain/core/messages';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { searchSearxng } from '../lib/searxng';
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { searchSearxng } from '../core/searxng';
const llm = new Ollama({
temperature: 0,
model: process.env.MODEL_NAME,
baseUrl: process.env.OLLAMA_URL,
});
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.
@ -38,47 +44,38 @@ type ImageSearchChainInput = {
const strParser = new StringOutputParser();
const createImageSearchChain = (llm: BaseChatModel) => {
return RunnableSequence.from([
RunnableMap.from({
chat_history: (input: ImageSearchChainInput) => {
return formatChatHistoryAsString(input.chat_history);
},
query: (input: ImageSearchChainInput) => {
return input.query;
},
}),
PromptTemplate.fromTemplate(imageSearchChainPrompt),
llm,
strParser,
RunnableLambda.from(async (input: string) => {
const res = await searchSearxng(input, {
engines: ['bing images', 'google images'],
});
const imageSearchChain = RunnableSequence.from([
RunnableMap.from({
chat_history: (input: ImageSearchChainInput) => {
return formatChatHistoryAsString(input.chat_history);
},
query: (input: ImageSearchChainInput) => {
return input.query;
},
}),
PromptTemplate.fromTemplate(imageSearchChainPrompt),
llm,
strParser,
RunnableLambda.from(async (input: string) => {
const res = await searchSearxng(input, {
categories: ['images'],
engines: ['bing_images', 'google_images'],
});
const images = [];
const images = [];
res.results.forEach((result) => {
if (result.img_src && result.url && result.title) {
images.push({
img_src: result.img_src,
url: result.url,
title: result.title,
});
}
});
res.results.forEach((result) => {
if (result.img_src && result.url && result.title) {
images.push({
img_src: result.img_src,
url: result.url,
title: result.title,
});
}
});
return images.slice(0, 10);
}),
]);
};
return images.slice(0, 10);
}),
]);
const handleImageSearch = (
input: ImageSearchChainInput,
llm: BaseChatModel,
) => {
const imageSearchChain = createImageSearchChain(llm);
return imageSearchChain.invoke(input);
};
export default handleImageSearch;
export default imageSearchChain;

View File

@ -9,16 +9,33 @@ import {
RunnableMap,
RunnableLambda,
} from '@langchain/core/runnables';
import { ChatOllama } from '@langchain/community/chat_models/ollama';
import { Ollama } from '@langchain/community/llms/ollama';
import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { Document } from '@langchain/core/documents';
import { searchSearxng } from '../lib/searxng';
import { searchSearxng } from '../core/searxng';
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 eventEmitter from 'events';
import computeSimilarity from '../utils/computeSimilarity';
import logger from '../utils/logger';
const chatLLM = new ChatOllama({
baseUrl: process.env.OLLAMA_URL,
model: process.env.MODEL_NAME,
temperature: 0.7,
});
const llm = new Ollama({
temperature: 0,
model: process.env.MODEL_NAME,
baseUrl: process.env.OLLAMA_URL,
});
const embeddings = new OllamaEmbeddings({
model: process.env.MODEL_NAME,
baseUrl: process.env.OLLAMA_URL,
});
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.
@ -97,134 +114,118 @@ const handleStream = async (
}
};
const processDocs = async (docs: Document[]) => {
return docs
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
.join('\n');
};
const rerankDocs = async ({
query,
docs,
}: {
query: string;
docs: Document[];
}) => {
if (docs.length === 0) {
return docs;
}
const docsWithContent = docs.filter(
(doc) => doc.pageContent && doc.pageContent.length > 0,
);
const docEmbeddings = await embeddings.embedDocuments(
docsWithContent.map((doc) => doc.pageContent),
);
const queryEmbedding = await embeddings.embedQuery(query);
const similarity = docEmbeddings.map((docEmbedding, i) => {
const sim = computeSimilarity(queryEmbedding, docEmbedding);
return {
index: i,
similarity: sim,
};
});
const sortedDocs = similarity
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 15)
.filter((sim) => sim.similarity > 0.3)
.map((sim) => docsWithContent[sim.index]);
return sortedDocs;
};
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[]) => {
return docs
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
.join('\n');
};
const rerankDocs = async ({
query,
docs,
}: {
query: string;
docs: Document[];
}) => {
if (docs.length === 0) {
return docs;
const basicRedditSearchRetrieverChain = RunnableSequence.from([
PromptTemplate.fromTemplate(basicRedditSearchRetrieverPrompt),
llm,
strParser,
RunnableLambda.from(async (input: string) => {
if (input === 'not_needed') {
return { query: '', docs: [] };
}
const docsWithContent = docs.filter(
(doc) => doc.pageContent && doc.pageContent.length > 0,
);
const [docEmbeddings, queryEmbedding] = await Promise.all([
embeddings.embedDocuments(docsWithContent.map((doc) => doc.pageContent)),
embeddings.embedQuery(query),
]);
const similarity = docEmbeddings.map((docEmbedding, i) => {
const sim = computeSimilarity(queryEmbedding, docEmbedding);
return {
index: i,
similarity: sim,
};
const res = await searchSearxng(input, {
language: 'en',
engines: ['reddit'],
});
const sortedDocs = similarity
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 15)
.filter((sim) => sim.similarity > 0.3)
.map((sim) => docsWithContent[sim.index]);
return sortedDocs;
};
return RunnableSequence.from([
RunnableMap.from({
query: (input: BasicChainInput) => input.query,
chat_history: (input: BasicChainInput) => input.chat_history,
context: RunnableSequence.from([
(input) => ({
query: input.query,
chat_history: formatChatHistoryAsString(input.chat_history),
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 }),
},
}),
basicRedditSearchRetrieverChain
.pipe(rerankDocs)
.withConfig({
runName: 'FinalSourceRetriever',
})
.pipe(processDocs),
]),
}),
ChatPromptTemplate.fromMessages([
['system', basicRedditSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]),
llm,
strParser,
]).withConfig({
runName: 'FinalResponseGenerator',
});
};
);
const basicRedditSearch = (
query: string,
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
) => {
return { query: input, docs: documents };
}),
]);
const basicRedditSearchAnsweringChain = RunnableSequence.from([
RunnableMap.from({
query: (input: BasicChainInput) => input.query,
chat_history: (input: BasicChainInput) => input.chat_history,
context: RunnableSequence.from([
(input) => ({
query: input.query,
chat_history: formatChatHistoryAsString(input.chat_history),
}),
basicRedditSearchRetrieverChain
.pipe(rerankDocs)
.withConfig({
runName: 'FinalSourceRetriever',
})
.pipe(processDocs),
]),
}),
ChatPromptTemplate.fromMessages([
['system', basicRedditSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]),
chatLLM,
strParser,
]).withConfig({
runName: 'FinalResponseGenerator',
});
const basicRedditSearch = (query: string, history: BaseMessage[]) => {
const emitter = new eventEmitter();
try {
const basicRedditSearchAnsweringChain =
createBasicRedditSearchAnsweringChain(llm, embeddings);
const stream = basicRedditSearchAnsweringChain.streamEvents(
{
chat_history: history,
@ -241,19 +242,14 @@ const basicRedditSearch = (
'error',
JSON.stringify({ data: 'An error has occurred please try again later' }),
);
logger.error(`Error in RedditSearch: ${err}`);
console.error(err);
}
return emitter;
};
const handleRedditSearch = (
message: string,
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
) => {
const emitter = basicRedditSearch(message, history, llm, embeddings);
const handleRedditSearch = (message: string, history: BaseMessage[]) => {
const emitter = basicRedditSearch(message, history);
return emitter;
};

View File

@ -1,90 +0,0 @@
import {
RunnableSequence,
RunnableMap,
RunnableLambda,
} from '@langchain/core/runnables';
import { PromptTemplate } from '@langchain/core/prompts';
import formatChatHistoryAsString from '../utils/formatHistory';
import { BaseMessage } from '@langchain/core/messages';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { searchSearxng } from '../lib/searxng';
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
const VideoSearchChainPrompt = `
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 Youtube for videos.
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
Example:
1. Follow up question: How does a car work?
Rephrased: How does a car work?
2. Follow up question: What is the theory of relativity?
Rephrased: What is theory of relativity
3. Follow up question: How does an AC work?
Rephrased: How does an AC work
Conversation:
{chat_history}
Follow up question: {query}
Rephrased question:
`;
type VideoSearchChainInput = {
chat_history: BaseMessage[];
query: string;
};
const strParser = new StringOutputParser();
const createVideoSearchChain = (llm: BaseChatModel) => {
return RunnableSequence.from([
RunnableMap.from({
chat_history: (input: VideoSearchChainInput) => {
return formatChatHistoryAsString(input.chat_history);
},
query: (input: VideoSearchChainInput) => {
return input.query;
},
}),
PromptTemplate.fromTemplate(VideoSearchChainPrompt),
llm,
strParser,
RunnableLambda.from(async (input: string) => {
const res = await searchSearxng(input, {
engines: ['youtube'],
});
const videos = [];
res.results.forEach((result) => {
if (
result.thumbnail &&
result.url &&
result.title &&
result.iframe_src
) {
videos.push({
img_src: result.thumbnail,
url: result.url,
title: result.title,
iframe_src: result.iframe_src,
});
}
});
return videos.slice(0, 10);
}),
]);
};
const handleVideoSearch = (
input: VideoSearchChainInput,
llm: BaseChatModel,
) => {
const VideoSearchChain = createVideoSearchChain(llm);
return VideoSearchChain.invoke(input);
};
export default handleVideoSearch;

View File

@ -9,16 +9,33 @@ import {
RunnableMap,
RunnableLambda,
} from '@langchain/core/runnables';
import { ChatOllama } from '@langchain/community/chat_models/ollama';
import { Ollama } from '@langchain/community/llms/ollama';
import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { Document } from '@langchain/core/documents';
import { searchSearxng } from '../lib/searxng';
import { searchSearxng } from '../core/searxng';
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 eventEmitter from 'events';
import computeSimilarity from '../utils/computeSimilarity';
import logger from '../utils/logger';
const chatLLM = new ChatOllama({
baseUrl: process.env.OLLAMA_URL,
model: process.env.MODEL_NAME,
temperature: 0.7,
});
const llm = new Ollama({
temperature: 0,
model: process.env.MODEL_NAME,
baseUrl: process.env.OLLAMA_URL,
});
const embeddings = new OllamaEmbeddings({
model: process.env.MODEL_NAME,
baseUrl: process.env.OLLAMA_URL,
});
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.
@ -97,135 +114,117 @@ const handleStream = async (
}
};
const processDocs = async (docs: Document[]) => {
return docs
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
.join('\n');
};
const rerankDocs = async ({
query,
docs,
}: {
query: string;
docs: Document[];
}) => {
if (docs.length === 0) {
return docs;
}
const docsWithContent = docs.filter(
(doc) => doc.pageContent && doc.pageContent.length > 0,
);
const docEmbeddings = await embeddings.embedDocuments(
docsWithContent.map((doc) => doc.pageContent),
);
const queryEmbedding = await embeddings.embedQuery(query);
const similarity = docEmbeddings.map((docEmbedding, i) => {
const sim = computeSimilarity(queryEmbedding, docEmbedding);
return {
index: i,
similarity: sim,
};
});
const sortedDocs = similarity
.sort((a, b) => b.similarity - a.similarity)
.filter((sim) => sim.similarity > 0.5)
.slice(0, 15)
.map((sim) => docsWithContent[sim.index]);
return sortedDocs;
};
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[]) => {
return docs
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
.join('\n');
};
const rerankDocs = async ({
query,
docs,
}: {
query: string;
docs: Document[];
}) => {
if (docs.length === 0) {
return docs;
const basicWebSearchRetrieverChain = RunnableSequence.from([
PromptTemplate.fromTemplate(basicSearchRetrieverPrompt),
llm,
strParser,
RunnableLambda.from(async (input: string) => {
if (input === 'not_needed') {
return { query: '', docs: [] };
}
const docsWithContent = docs.filter(
(doc) => doc.pageContent && doc.pageContent.length > 0,
);
const [docEmbeddings, queryEmbedding] = await Promise.all([
embeddings.embedDocuments(docsWithContent.map((doc) => doc.pageContent)),
embeddings.embedQuery(query),
]);
const similarity = docEmbeddings.map((docEmbedding, i) => {
const sim = computeSimilarity(queryEmbedding, docEmbedding);
return {
index: i,
similarity: sim,
};
const res = await searchSearxng(input, {
language: 'en',
});
const sortedDocs = similarity
.sort((a, b) => b.similarity - a.similarity)
.filter((sim) => sim.similarity > 0.5)
.slice(0, 15)
.map((sim) => docsWithContent[sim.index]);
return sortedDocs;
};
return RunnableSequence.from([
RunnableMap.from({
query: (input: BasicChainInput) => input.query,
chat_history: (input: BasicChainInput) => input.chat_history,
context: RunnableSequence.from([
(input) => ({
query: input.query,
chat_history: formatChatHistoryAsString(input.chat_history),
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 }),
},
}),
basicWebSearchRetrieverChain
.pipe(rerankDocs)
.withConfig({
runName: 'FinalSourceRetriever',
})
.pipe(processDocs),
]),
}),
ChatPromptTemplate.fromMessages([
['system', basicWebSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]),
llm,
strParser,
]).withConfig({
runName: 'FinalResponseGenerator',
});
};
);
const basicWebSearch = (
query: string,
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
) => {
return { query: input, docs: documents };
}),
]);
const basicWebSearchAnsweringChain = RunnableSequence.from([
RunnableMap.from({
query: (input: BasicChainInput) => input.query,
chat_history: (input: BasicChainInput) => input.chat_history,
context: RunnableSequence.from([
(input) => ({
query: input.query,
chat_history: formatChatHistoryAsString(input.chat_history),
}),
basicWebSearchRetrieverChain
.pipe(rerankDocs)
.withConfig({
runName: 'FinalSourceRetriever',
})
.pipe(processDocs),
]),
}),
ChatPromptTemplate.fromMessages([
['system', basicWebSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]),
chatLLM,
strParser,
]).withConfig({
runName: 'FinalResponseGenerator',
});
const basicWebSearch = (query: string, history: BaseMessage[]) => {
const emitter = new eventEmitter();
try {
const basicWebSearchAnsweringChain = createBasicWebSearchAnsweringChain(
llm,
embeddings,
);
const stream = basicWebSearchAnsweringChain.streamEvents(
{
chat_history: history,
@ -242,19 +241,14 @@ const basicWebSearch = (
'error',
JSON.stringify({ data: 'An error has occurred please try again later' }),
);
logger.error(`Error in websearch: ${err}`);
console.error(err);
}
return emitter;
};
const handleWebSearch = (
message: string,
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
) => {
const emitter = basicWebSearch(message, history, llm, embeddings);
const handleWebSearch = (message: string, history: BaseMessage[]) => {
const emitter = basicWebSearch(message, history);
return emitter;
};

View File

@ -9,15 +9,26 @@ import {
RunnableMap,
RunnableLambda,
} from '@langchain/core/runnables';
import { ChatOllama } from '@langchain/community/chat_models/ollama';
import { Ollama } from '@langchain/community/llms/ollama';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { Document } from '@langchain/core/documents';
import { searchSearxng } from '../lib/searxng';
import { searchSearxng } from '../core/searxng';
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 eventEmitter from 'events';
import logger from '../utils/logger';
const chatLLM = new ChatOllama({
baseUrl: process.env.OLLAMA_URL,
model: process.env.MODEL_NAME,
temperature: 0.7,
});
const llm = new Ollama({
temperature: 0,
model: process.env.MODEL_NAME,
baseUrl: process.env.OLLAMA_URL,
});
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.
@ -96,94 +107,81 @@ const handleStream = async (
}
};
const processDocs = async (docs: Document[]) => {
return docs
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
.join('\n');
};
type BasicChainInput = {
chat_history: BaseMessage[];
query: string;
};
const createBasicWolframAlphaSearchRetrieverChain = (llm: BaseChatModel) => {
return RunnableSequence.from([
PromptTemplate.fromTemplate(basicWolframAlphaSearchRetrieverPrompt),
llm,
strParser,
RunnableLambda.from(async (input: string) => {
if (input === 'not_needed') {
return { query: '', docs: [] };
}
const basicWolframAlphaSearchRetrieverChain = RunnableSequence.from([
PromptTemplate.fromTemplate(basicWolframAlphaSearchRetrieverPrompt),
llm,
strParser,
RunnableLambda.from(async (input: string) => {
if (input === 'not_needed') {
return { query: '', docs: [] };
}
const res = await searchSearxng(input, {
language: 'en',
engines: ['wolframalpha'],
});
const res = await searchSearxng(input, {
language: 'en',
engines: ['wolframalpha'],
});
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 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({
query: (input: BasicChainInput) => input.query,
chat_history: (input: BasicChainInput) => input.chat_history,
context: RunnableSequence.from([
(input) => ({
query: input.query,
chat_history: formatChatHistoryAsString(input.chat_history),
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 }),
},
}),
basicWolframAlphaSearchRetrieverChain
.pipe(({ query, docs }) => {
return docs;
})
.withConfig({
runName: 'FinalSourceRetriever',
})
.pipe(processDocs),
]),
}),
ChatPromptTemplate.fromMessages([
['system', basicWolframAlphaSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]),
llm,
strParser,
]).withConfig({
runName: 'FinalResponseGenerator',
});
};
);
const basicWolframAlphaSearch = (
query: string,
history: BaseMessage[],
llm: BaseChatModel,
) => {
return { query: input, docs: documents };
}),
]);
const basicWolframAlphaSearchAnsweringChain = RunnableSequence.from([
RunnableMap.from({
query: (input: BasicChainInput) => input.query,
chat_history: (input: BasicChainInput) => input.chat_history,
context: RunnableSequence.from([
(input) => ({
query: input.query,
chat_history: formatChatHistoryAsString(input.chat_history),
}),
basicWolframAlphaSearchRetrieverChain
.pipe(({ query, docs }) => {
return docs;
})
.withConfig({
runName: 'FinalSourceRetriever',
})
.pipe(processDocs),
]),
}),
ChatPromptTemplate.fromMessages([
['system', basicWolframAlphaSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]),
chatLLM,
strParser,
]).withConfig({
runName: 'FinalResponseGenerator',
});
const basicWolframAlphaSearch = (query: string, history: BaseMessage[]) => {
const emitter = new eventEmitter();
try {
const basicWolframAlphaSearchAnsweringChain =
createBasicWolframAlphaSearchAnsweringChain(llm);
const stream = basicWolframAlphaSearchAnsweringChain.streamEvents(
{
chat_history: history,
@ -200,19 +198,14 @@ const basicWolframAlphaSearch = (
'error',
JSON.stringify({ data: 'An error has occurred please try again later' }),
);
logger.error(`Error in WolframAlphaSearch: ${err}`);
console.error(err);
}
return emitter;
};
const handleWolframAlphaSearch = (
message: string,
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
) => {
const emitter = basicWolframAlphaSearch(message, history, llm);
const handleWolframAlphaSearch = (message: string, history: BaseMessage[]) => {
const emitter = basicWolframAlphaSearch(message, history);
return emitter;
};

View File

@ -4,12 +4,16 @@ import {
MessagesPlaceholder,
} from '@langchain/core/prompts';
import { RunnableSequence } from '@langchain/core/runnables';
import { ChatOllama } from '@langchain/community/chat_models/ollama';
import { StringOutputParser } from '@langchain/core/output_parsers';
import type { StreamEvent } from '@langchain/core/tracers/log_stream';
import eventEmitter from 'events';
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import type { Embeddings } from '@langchain/core/embeddings';
import logger from '../utils/logger';
const chatLLM = new ChatOllama({
baseUrl: process.env.OLLAMA_URL,
model: process.env.MODEL_NAME,
temperature: 0.7,
});
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.
@ -41,30 +45,22 @@ const handleStream = async (
}
};
const createWritingAssistantChain = (llm: BaseChatModel) => {
return RunnableSequence.from([
ChatPromptTemplate.fromMessages([
['system', writingAssistantPrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]),
llm,
strParser,
]).withConfig({
runName: 'FinalResponseGenerator',
});
};
const writingAssistantChain = RunnableSequence.from([
ChatPromptTemplate.fromMessages([
['system', writingAssistantPrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]),
chatLLM,
strParser,
]).withConfig({
runName: 'FinalResponseGenerator',
});
const handleWritingAssistant = (
query: string,
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
) => {
const handleWritingAssistant = (query: string, history: BaseMessage[]) => {
const emitter = new eventEmitter();
try {
const writingAssistantChain = createWritingAssistantChain(llm);
const stream = writingAssistantChain.streamEvents(
{
chat_history: history,
@ -81,7 +77,7 @@ const handleWritingAssistant = (
'error',
JSON.stringify({ data: 'An error has occurred please try again later' }),
);
logger.error(`Error in writing assistant: ${err}`);
console.error(err);
}
return emitter;

View File

@ -9,16 +9,33 @@ import {
RunnableMap,
RunnableLambda,
} from '@langchain/core/runnables';
import { ChatOllama } from '@langchain/community/chat_models/ollama';
import { Ollama } from '@langchain/community/llms/ollama';
import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { Document } from '@langchain/core/documents';
import { searchSearxng } from '../lib/searxng';
import { searchSearxng } from '../core/searxng';
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 eventEmitter from 'events';
import computeSimilarity from '../utils/computeSimilarity';
import logger from '../utils/logger';
const chatLLM = new ChatOllama({
baseUrl: process.env.OLLAMA_URL,
model: process.env.MODEL_NAME,
temperature: 0.7,
});
const llm = new Ollama({
temperature: 0,
model: process.env.MODEL_NAME,
baseUrl: process.env.OLLAMA_URL,
});
const embeddings = new OllamaEmbeddings({
model: process.env.MODEL_NAME,
baseUrl: process.env.OLLAMA_URL,
});
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.
@ -97,135 +114,118 @@ const handleStream = async (
}
};
const processDocs = async (docs: Document[]) => {
return docs
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
.join('\n');
};
const rerankDocs = async ({
query,
docs,
}: {
query: string;
docs: Document[];
}) => {
if (docs.length === 0) {
return docs;
}
const docsWithContent = docs.filter(
(doc) => doc.pageContent && doc.pageContent.length > 0,
);
const docEmbeddings = await embeddings.embedDocuments(
docsWithContent.map((doc) => doc.pageContent),
);
const queryEmbedding = await embeddings.embedQuery(query);
const similarity = docEmbeddings.map((docEmbedding, i) => {
const sim = computeSimilarity(queryEmbedding, docEmbedding);
return {
index: i,
similarity: sim,
};
});
const sortedDocs = similarity
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 15)
.filter((sim) => sim.similarity > 0.3)
.map((sim) => docsWithContent[sim.index]);
return sortedDocs;
};
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[]) => {
return docs
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
.join('\n');
};
const rerankDocs = async ({
query,
docs,
}: {
query: string;
docs: Document[];
}) => {
if (docs.length === 0) {
return docs;
const basicYoutubeSearchRetrieverChain = RunnableSequence.from([
PromptTemplate.fromTemplate(basicYoutubeSearchRetrieverPrompt),
llm,
strParser,
RunnableLambda.from(async (input: string) => {
if (input === 'not_needed') {
return { query: '', docs: [] };
}
const docsWithContent = docs.filter(
(doc) => doc.pageContent && doc.pageContent.length > 0,
);
const [docEmbeddings, queryEmbedding] = await Promise.all([
embeddings.embedDocuments(docsWithContent.map((doc) => doc.pageContent)),
embeddings.embedQuery(query),
]);
const similarity = docEmbeddings.map((docEmbedding, i) => {
const sim = computeSimilarity(queryEmbedding, docEmbedding);
return {
index: i,
similarity: sim,
};
const res = await searchSearxng(input, {
language: 'en',
engines: ['youtube'],
});
const sortedDocs = similarity
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 15)
.filter((sim) => sim.similarity > 0.3)
.map((sim) => docsWithContent[sim.index]);
return sortedDocs;
};
return RunnableSequence.from([
RunnableMap.from({
query: (input: BasicChainInput) => input.query,
chat_history: (input: BasicChainInput) => input.chat_history,
context: RunnableSequence.from([
(input) => ({
query: input.query,
chat_history: formatChatHistoryAsString(input.chat_history),
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 }),
},
}),
basicYoutubeSearchRetrieverChain
.pipe(rerankDocs)
.withConfig({
runName: 'FinalSourceRetriever',
})
.pipe(processDocs),
]),
}),
ChatPromptTemplate.fromMessages([
['system', basicYoutubeSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]),
llm,
strParser,
]).withConfig({
runName: 'FinalResponseGenerator',
});
};
);
const basicYoutubeSearch = (
query: string,
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
) => {
return { query: input, docs: documents };
}),
]);
const basicYoutubeSearchAnsweringChain = RunnableSequence.from([
RunnableMap.from({
query: (input: BasicChainInput) => input.query,
chat_history: (input: BasicChainInput) => input.chat_history,
context: RunnableSequence.from([
(input) => ({
query: input.query,
chat_history: formatChatHistoryAsString(input.chat_history),
}),
basicYoutubeSearchRetrieverChain
.pipe(rerankDocs)
.withConfig({
runName: 'FinalSourceRetriever',
})
.pipe(processDocs),
]),
}),
ChatPromptTemplate.fromMessages([
['system', basicYoutubeSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]),
chatLLM,
strParser,
]).withConfig({
runName: 'FinalResponseGenerator',
});
const basicYoutubeSearch = (query: string, history: BaseMessage[]) => {
const emitter = new eventEmitter();
try {
const basicYoutubeSearchAnsweringChain =
createBasicYoutubeSearchAnsweringChain(llm, embeddings);
const stream = basicYoutubeSearchAnsweringChain.streamEvents(
{
chat_history: history,
@ -242,19 +242,14 @@ const basicYoutubeSearch = (
'error',
JSON.stringify({ data: 'An error has occurred please try again later' }),
);
logger.error(`Error in youtube search: ${err}`);
console.error(err);
}
return emitter;
};
const handleYoutubeSearch = (
message: string,
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
) => {
const emitter = basicYoutubeSearch(message, history, llm, embeddings);
const handleYoutubeSearch = (message: string, history: BaseMessage[]) => {
const emitter = basicYoutubeSearch(message, history);
return emitter;
};

View File

@ -3,10 +3,6 @@ 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);
@ -23,8 +19,8 @@ app.get('/api', (_, res) => {
res.status(200).json({ status: 'ok' });
});
server.listen(port, () => {
logger.info(`Server is running on port ${port}`);
server.listen(process.env.PORT!, () => {
console.log(`API server started on port ${process.env.PORT}`);
});
startWebSocketServer(server);

View File

@ -1,69 +0,0 @@
import fs from 'fs';
import path from 'path';
import toml from '@iarna/toml';
const configFileName = 'config.toml';
interface Config {
GENERAL: {
PORT: number;
SIMILARITY_MEASURE: string;
};
API_KEYS: {
OPENAI: string;
GROQ: string;
};
API_ENDPOINTS: {
SEARXNG: string;
OLLAMA: string;
};
}
type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>;
};
const loadConfig = () =>
toml.parse(
fs.readFileSync(path.join(__dirname, `../${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 getGroqApiKey = () => loadConfig().API_KEYS.GROQ;
export const getSearxngApiEndpoint = () => loadConfig().API_ENDPOINTS.SEARXNG;
export const getOllamaApiEndpoint = () => loadConfig().API_ENDPOINTS.OLLAMA;
export const updateConfig = (config: RecursivePartial<Config>) => {
const currentConfig = loadConfig();
for (const key in currentConfig) {
if (!config[key]) config[key] = {};
if (typeof currentConfig[key] === 'object' && currentConfig[key] !== null) {
for (const nestedKey in currentConfig[key]) {
if (
!config[key][nestedKey] &&
currentConfig[key][nestedKey] &&
config[key][nestedKey] !== ''
) {
config[key][nestedKey] = currentConfig[key][nestedKey];
}
}
} else if (currentConfig[key] && config[key] !== '') {
config[key] = currentConfig[key];
}
}
fs.writeFileSync(
path.join(__dirname, `../${configFileName}`),
toml.stringify(config),
);
};

69
src/core/agentPicker.ts Normal file
View File

@ -0,0 +1,69 @@
import { z } from 'zod';
import { OpenAI } from '@langchain/openai';
import { RunnableSequence } from '@langchain/core/runnables';
import { StructuredOutputParser } from 'langchain/output_parsers';
import { PromptTemplate } from '@langchain/core/prompts';
const availableAgents = [
{
name: 'webSearch',
description:
'It is expert is searching the web for information and answer user queries',
},
/* {
name: 'academicSearch',
description:
'It is expert is searching the academic databases for information and answer user queries. It is particularly good at finding research papers and articles on topics like science, engineering, and technology. Use this instead of wolframAlphaSearch if the user query is not mathematical or scientific in nature',
},
{
name: 'youtubeSearch',
description:
'This model is expert at finding videos on youtube based on user queries',
},
{
name: 'wolframAlphaSearch',
description:
'This model is expert at finding answers to mathematical and scientific questions based on user queries.',
},
{
name: 'redditSearch',
description:
'This model is expert at finding posts and discussions on reddit based on user queries',
},
{
name: 'writingAssistant',
description:
'If there is no need for searching, this model is expert at generating text based on user queries',
}, */
];
const parser = StructuredOutputParser.fromZodSchema(
z.object({
agent: z.string().describe('The name of the selected agent'),
}),
);
const prompt = `
You are an AI model who is expert at finding suitable agents for user queries. The available agents are:
${availableAgents.map((agent) => `- ${agent.name}: ${agent.description}`).join('\n')}
Your task is to find the most suitable agent for the following query: {query}
{format_instructions}
`;
const chain = RunnableSequence.from([
PromptTemplate.fromTemplate(prompt),
new OpenAI({ temperature: 0 }),
parser,
]);
const pickSuitableAgent = async (query: string) => {
const res = await chain.invoke({
query,
format_instructions: parser.getFormatInstructions(),
});
return res.agent;
};
export default pickSuitableAgent;

View File

@ -1,5 +1,4 @@
import axios from 'axios';
import { getSearxngApiEndpoint } from '../config';
interface SearxngSearchOptions {
categories?: string[];
@ -13,19 +12,15 @@ interface SearxngSearchResult {
url: string;
img_src?: string;
thumbnail_src?: string;
thumbnail?: string;
content?: string;
author?: string;
iframe_src?: string;
}
export const searchSearxng = async (
query: string,
opts?: SearxngSearchOptions,
) => {
const searxngURL = getSearxngApiEndpoint();
const url = new URL(`${searxngURL}/search?format=json`);
const url = new URL(`${process.env.SEARXNG_API_URL}/search?format=json`);
url.searchParams.append('q', query);
if (opts) {

View File

@ -1,126 +0,0 @@
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
import { ChatOllama } from '@langchain/community/chat_models/ollama';
import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama';
import {
getGroqApiKey,
getOllamaApiEndpoint,
getOpenaiApiKey,
} from '../config';
import logger from '../utils/logger';
export const getAvailableProviders = async () => {
const openAIApiKey = getOpenaiApiKey();
const groqApiKey = getGroqApiKey();
const ollamaEndpoint = getOllamaApiEndpoint();
const models = {};
if (openAIApiKey) {
try {
models['openai'] = {
'GPT-3.5 turbo': new ChatOpenAI({
openAIApiKey,
modelName: 'gpt-3.5-turbo',
temperature: 0.7,
}),
'GPT-4': new ChatOpenAI({
openAIApiKey,
modelName: 'gpt-4',
temperature: 0.7,
}),
'GPT-4 turbo': new ChatOpenAI({
openAIApiKey,
modelName: 'gpt-4-turbo',
temperature: 0.7,
}),
embeddings: new OpenAIEmbeddings({
openAIApiKey,
modelName: 'text-embedding-3-large',
}),
};
} catch (err) {
logger.error(`Error loading OpenAI models: ${err}`);
}
}
if (groqApiKey) {
try {
models['groq'] = {
'LLaMA3 8b': new ChatOpenAI(
{
openAIApiKey: groqApiKey,
modelName: 'llama3-8b-8192',
temperature: 0.7,
},
{
baseURL: 'https://api.groq.com/openai/v1',
},
),
'LLaMA3 70b': new ChatOpenAI(
{
openAIApiKey: groqApiKey,
modelName: 'llama3-70b-8192',
temperature: 0.7,
},
{
baseURL: 'https://api.groq.com/openai/v1',
},
),
'Mixtral 8x7b': new ChatOpenAI(
{
openAIApiKey: groqApiKey,
modelName: 'mixtral-8x7b-32768',
temperature: 0.7,
},
{
baseURL: 'https://api.groq.com/openai/v1',
},
),
'Gemma 7b': new ChatOpenAI(
{
openAIApiKey: groqApiKey,
modelName: 'gemma-7b-it',
temperature: 0.7,
},
{
baseURL: 'https://api.groq.com/openai/v1',
},
),
embeddings: new OpenAIEmbeddings({
openAIApiKey: openAIApiKey,
modelName: 'text-embedding-3-large',
}),
};
} catch (err) {
logger.error(`Error loading Groq models: ${err}`);
}
}
if (ollamaEndpoint) {
try {
const response = await fetch(`${ollamaEndpoint}/api/tags`);
const { models: ollamaModels } = (await response.json()) as any;
models['ollama'] = ollamaModels.reduce((acc, model) => {
acc[model.model] = new ChatOllama({
baseUrl: ollamaEndpoint,
model: model.model,
temperature: 0.7,
});
return acc;
}, {});
if (Object.keys(models['ollama']).length > 0) {
models['ollama']['embeddings'] = new OllamaEmbeddings({
baseUrl: ollamaEndpoint,
model: models['ollama'][Object.keys(models['ollama'])[0]].model,
});
}
} catch (err) {
logger.error(`Error loading Ollama models: ${err}`);
}
}
return models;
};

View File

@ -1,52 +0,0 @@
import express from 'express';
import { getAvailableProviders } from '../lib/providers';
import {
getGroqApiKey,
getOllamaApiEndpoint,
getOpenaiApiKey,
updateConfig,
} from '../config';
const router = express.Router();
router.get('/', async (_, res) => {
const config = {};
const providers = await getAvailableProviders();
for (const provider in providers) {
delete providers[provider]['embeddings'];
}
config['providers'] = {};
for (const provider in providers) {
config['providers'][provider] = Object.keys(providers[provider]);
}
config['openaiApiKey'] = getOpenaiApiKey();
config['ollamaApiUrl'] = getOllamaApiEndpoint();
config['groqApiKey'] = getGroqApiKey();
res.status(200).json(config);
});
router.post('/', async (req, res) => {
const config = req.body;
const updatedConfig = {
API_KEYS: {
OPENAI: config.openaiApiKey,
GROQ: config.groqApiKey,
},
API_ENDPOINTS: {
OLLAMA: config.ollamaApiUrl,
},
};
updateConfig(updatedConfig);
res.status(200).json({ message: 'Config updated' });
});
export default router;

View File

@ -1,45 +1,21 @@
import express from 'express';
import handleImageSearch from '../agents/imageSearchAgent';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { getAvailableProviders } from '../lib/providers';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
import logger from '../utils/logger';
import imageSearchChain from '../agents/imageSearchAgent';
const router = express.Router();
router.post('/', async (req, res) => {
try {
let { query, chat_history, chat_model_provider, chat_model } = req.body;
const { query, chat_history } = req.body;
chat_history = chat_history.map((msg: any) => {
if (msg.role === 'user') {
return new HumanMessage(msg.content);
} else if (msg.role === 'assistant') {
return new AIMessage(msg.content);
}
const images = await imageSearchChain.invoke({
query,
chat_history,
});
const chatModels = await getAvailableProviders();
const provider = chat_model_provider || Object.keys(chatModels)[0];
const chatModel = chat_model || Object.keys(chatModels[provider])[0];
let llm: BaseChatModel | undefined;
if (chatModels[provider] && chatModels[provider][chatModel]) {
llm = chatModels[provider][chatModel] as BaseChatModel | undefined;
}
if (!llm) {
res.status(500).json({ message: 'Invalid LLM model selected' });
return;
}
const images = await handleImageSearch({ query, chat_history }, 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}`);
console.log(err.message);
}
});

View File

@ -1,14 +1,8 @@
import express from 'express';
import imagesRouter from './images';
import videosRouter from './videos';
import configRouter from './config';
import modelsRouter from './models';
const router = express.Router();
router.use('/images', imagesRouter);
router.use('/videos', videosRouter);
router.use('/config', configRouter);
router.use('/models', modelsRouter);
export default router;

View File

@ -1,18 +0,0 @@
import express from 'express';
import logger from '../utils/logger';
import { getAvailableProviders } from '../lib/providers';
const router = express.Router();
router.get('/', async (req, res) => {
try {
const providers = await getAvailableProviders();
res.status(200).json({ providers });
} catch (err) {
res.status(500).json({ message: 'An error has occurred.' });
logger.error(err.message);
}
});
export default router;

View File

@ -1,46 +0,0 @@
import express from 'express';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { getAvailableProviders } from '../lib/providers';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
import logger from '../utils/logger';
import handleVideoSearch from '../agents/videoSearchAgent';
const router = express.Router();
router.post('/', async (req, res) => {
try {
let { query, chat_history, chat_model_provider, chat_model } = req.body;
chat_history = chat_history.map((msg: any) => {
if (msg.role === 'user') {
return new HumanMessage(msg.content);
} else if (msg.role === 'assistant') {
return new AIMessage(msg.content);
}
});
const chatModels = await getAvailableProviders();
const provider = chat_model_provider || Object.keys(chatModels)[0];
const chatModel = chat_model || Object.keys(chatModels[provider])[0];
let llm: BaseChatModel | undefined;
if (chatModels[provider] && chatModels[provider][chatModel]) {
llm = chatModels[provider][chatModel] as BaseChatModel | undefined;
}
if (!llm) {
res.status(500).json({ message: 'Invalid LLM model selected' });
return;
}
const videos = await handleVideoSearch({ chat_history, 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;

View File

@ -1,13 +1,10 @@
import dot from 'compute-dot';
import cosineSimilarity from 'compute-cosine-similarity';
import { getSimilarityMeasure } from '../config';
const computeSimilarity = (x: number[], y: number[]): number => {
const similarityMeasure = getSimilarityMeasure();
if (similarityMeasure === 'cosine') {
if (process.env.SIMILARITY_MEASURE === 'cosine') {
return cosineSimilarity(x, y);
} else if (similarityMeasure === 'dot') {
} else if (process.env.SIMILARITY_MEASURE === 'dot') {
return dot(x, y);
}

View File

@ -1,22 +0,0 @@
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;

View File

@ -1,47 +1,11 @@
import { WebSocket } from 'ws';
import { handleMessage } from './messageHandler';
import { getAvailableProviders } 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';
export const handleConnection = async (
ws: WebSocket,
request: IncomingMessage,
) => {
const searchParams = new URL(request.url, `http://${request.headers.host}`)
.searchParams;
const models = await getAvailableProviders();
const provider =
searchParams.get('chatModelProvider') || Object.keys(models)[0];
const chatModel =
searchParams.get('chatModel') || Object.keys(models[provider])[0];
let llm: BaseChatModel | undefined;
let embeddings: Embeddings | undefined;
if (models[provider] && models[provider][chatModel]) {
llm = models[provider][chatModel] as BaseChatModel | undefined;
embeddings = models[provider].embeddings as Embeddings | undefined;
}
if (!llm || !embeddings) {
ws.send(
JSON.stringify({
type: 'error',
data: 'Invalid LLM or embeddings model selected',
}),
);
ws.close();
}
export const handleConnection = (ws: WebSocket) => {
ws.on(
'message',
async (message) =>
await handleMessage(message.toString(), ws, llm, embeddings),
async (message) => await handleMessage(message.toString(), ws),
);
ws.on('close', () => logger.debug('Connection closed'));
ws.on('close', () => console.log('Connection closed'));
};

View File

@ -6,9 +6,6 @@ import handleWritingAssistant from '../agents/writingAssistant';
import handleWolframAlphaSearch from '../agents/wolframAlphaSearchAgent';
import handleYoutubeSearch from '../agents/youtubeSearchAgent';
import handleRedditSearch from '../agents/redditSearchAgent';
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import type { Embeddings } from '@langchain/core/embeddings';
import logger from '../utils/logger';
type Message = {
type: string;
@ -61,12 +58,7 @@ const handleEmitterEvents = (
});
};
export const handleMessage = async (
message: string,
ws: WebSocket,
llm: BaseChatModel,
embeddings: Embeddings,
) => {
export const handleMessage = async (message: string, ws: WebSocket) => {
try {
const parsedMessage = JSON.parse(message) as Message;
const id = Math.random().toString(36).substring(7);
@ -91,19 +83,14 @@ export const handleMessage = async (
if (parsedMessage.type === 'message') {
const handler = searchHandlers[parsedMessage.focusMode];
if (handler) {
const emitter = handler(
parsedMessage.content,
history,
llm,
embeddings,
);
const emitter = handler(parsedMessage.content, history);
handleEmitterEvents(emitter, ws, id);
} else {
ws.send(JSON.stringify({ type: 'error', data: 'Invalid focus mode' }));
}
}
} catch (err) {
} catch (error) {
console.error('Failed to handle message', error);
ws.send(JSON.stringify({ type: 'error', data: 'Invalid message format' }));
logger.error(`Failed to handle message: ${err}`);
}
};

View File

@ -1,16 +1,15 @@
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);
wss.on('connection', (ws) => {
handleConnection(ws);
});
logger.info(`WebSocket server started on port ${port}`);
console.log(`WebSocket server started on port ${process.env.PORT}`);
};

View File

@ -77,7 +77,7 @@ const Chat = ({
className="bottom-24 lg:bottom-10 fixed z-40"
style={{ width: dividerWidth }}
>
<MessageInput loading={loading} sendMessage={sendMessage} />
<MessageInput sendMessage={sendMessage} />
</div>
)}
</div>

View File

@ -19,42 +19,14 @@ const useSocket = (url: string) => {
useEffect(() => {
if (!ws) {
const connectWs = async () => {
let chatModel = localStorage.getItem('chatModel');
let chatModelProvider = localStorage.getItem('chatModelProvider');
if (!chatModel || !chatModelProvider) {
const chatModelProviders = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/models`,
).then(async (res) => (await res.json())['providers']);
if (
!chatModelProviders ||
Object.keys(chatModelProviders).length === 0
)
return console.error('No chat models available');
chatModelProvider = Object.keys(chatModelProviders)[0];
chatModel = Object.keys(chatModelProviders[chatModelProvider])[0];
localStorage.setItem('chatModel', chatModel!);
localStorage.setItem('chatModelProvider', chatModelProvider);
}
const ws = new WebSocket(
`${url}?chatModel=${chatModel}&chatModelProvider=${chatModelProvider}`,
);
ws.onopen = () => {
console.log('[DEBUG] open');
setWs(ws);
};
const ws = new WebSocket(url);
ws.onopen = () => {
console.log('[DEBUG] open');
setWs(ws);
};
connectWs();
}
return () => {
1;
ws?.close();
console.log('[DEBUG] closed');
};
@ -180,7 +152,7 @@ const ChatWindow = () => {
sendMessage(message.content);
};
return ws ? (
return (
<div>
{messages.length > 0 ? (
<>
@ -201,25 +173,6 @@ const ChatWindow = () => {
/>
)}
</div>
) : (
<div className="flex flex-row items-center justify-center min-h-screen">
<svg
aria-hidden="true"
className="w-8 h-8 text-[#202020] animate-spin fill-[#ffffff3b]"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
);
};

View File

@ -1,17 +1,21 @@
'use client';
/* eslint-disable @next/next/no-img-element */
import React, { MutableRefObject, useEffect, useState } from 'react';
import { Message } from './ChatWindow';
import { cn } from '@/lib/utils';
import { BookCopy, Disc3, Share, Volume2, StopCircle } from 'lucide-react';
import {
BookCopy,
Disc3,
FilePen,
PlusIcon,
Share,
ThumbsDown,
VideoIcon,
} from 'lucide-react';
import Markdown from 'markdown-to-jsx';
import Copy from './MessageActions/Copy';
import Rewrite from './MessageActions/Rewrite';
import MessageSources from './MessageSources';
import SearchImages from './SearchImages';
import SearchVideos from './SearchVideos';
import { useSpeech } from 'react-text-to-speech';
const MessageBox = ({
message,
@ -31,7 +35,6 @@ const MessageBox = ({
rewrite: (messageId: string) => void;
}) => {
const [parsedMessage, setParsedMessage] = useState(message.content);
const [speechMessage, setSpeechMessage] = useState(message.content);
useEffect(() => {
if (
@ -41,8 +44,6 @@ const MessageBox = ({
) {
const regex = /\[(\d+)\]/g;
setSpeechMessage(message.content.replace(regex, ''));
return setParsedMessage(
message.content.replace(
regex,
@ -54,8 +55,6 @@ const MessageBox = ({
setParsedMessage(message.content);
}, [message.content, message.sources, message.role]);
const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
return (
<div>
{message.role === 'user' && (
@ -105,21 +104,11 @@ const MessageBox = ({
</div>
<div className="flex flex-row items-center space-x-1">
<Copy initialMessage={message.content} message={message} />
<button
onClick={() => {
if (speechStatus === 'started') {
stop();
} else {
start();
}
}}
className="p-2 text-white/70 rounded-xl hover:bg-[#1c1c1c] transition duration-200 hover:text-white"
>
{speechStatus === 'started' ? (
<StopCircle size={18} />
) : (
<Volume2 size={18} />
)}
<button className="p-2 text-white/70 rounded-xl hover:bg-[#1c1c1c] transition duration-200 hover:text-white">
<FilePen size={18} />
</button>
<button className="p-2 text-white/70 rounded-xl hover:bg-[#1c1c1c] transition duration-200 hover:text-white">
<ThumbsDown size={18} />
</button>
</div>
</div>
@ -127,14 +116,14 @@ const MessageBox = ({
</div>
</div>
<div className="lg:sticky lg:top-20 flex flex-col items-center space-y-3 w-full lg:w-3/12 z-30 h-full pb-4">
<SearchImages
query={history[messageIndex - 1].content}
chat_history={history.slice(0, messageIndex - 1)}
/>
<SearchVideos
chat_history={history.slice(0, messageIndex - 1)}
query={history[messageIndex - 1].content}
/>
<SearchImages query={history[messageIndex - 1].content} />
<div className="border border-dashed border-[#1C1C1C] px-4 py-2 flex flex-row items-center justify-between rounded-lg text-white text-sm w-full">
<div className="flex flex-row items-center space-x-2">
<VideoIcon size={17} />
<p>Search videos</p>
</div>
<PlusIcon className="text-[#24A0ED]" size={17} />
</div>
</div>
</div>
)}

View File

@ -6,10 +6,8 @@ import { Attach, CopilotToggle } from './MessageInputActions';
const MessageInput = ({
sendMessage,
loading,
}: {
sendMessage: (message: string) => void;
loading: boolean;
}) => {
const [copilotEnabled, setCopilotEnabled] = useState(false);
const [message, setMessage] = useState('');
@ -27,13 +25,12 @@ const MessageInput = ({
return (
<form
onSubmit={(e) => {
if (loading) return;
e.preventDefault();
sendMessage(message);
setMessage('');
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && !loading) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage(message);
setMessage('');
@ -61,7 +58,7 @@ const MessageInput = ({
setCopilotEnabled={setCopilotEnabled}
/>
<button
disabled={message.trim().length === 0 || loading}
disabled={message.trim().length === 0}
className="bg-[#24A0ED] text-white disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#ececec21] rounded-full p-2"
>
<ArrowUp className="bg-background" size={17} />
@ -77,7 +74,7 @@ const MessageInput = ({
setCopilotEnabled={setCopilotEnabled}
/>
<button
disabled={message.trim().length === 0 || loading}
disabled={message.trim().length === 0}
className="bg-[#24A0ED] text-white disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#ececec21] rounded-full p-2"
>
<ArrowUp className="bg-background" size={17} />

View File

@ -109,7 +109,7 @@ export const Focus = ({
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute z-10 w-full">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-1 bg-[#0A0A0A] border rounded-lg border-[#1c1c1c] w-full p-2 max-h-[200px] md:max-h-none overflow-y-auto">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-1 bg-[#0A0A0A] border rounded-lg border-[#1c1c1c] w-full p-2">
{focusModes.map((mode, i) => (
<Popover.Button
onClick={() => setFocusMode(mode.key)}

View File

@ -1,20 +1,21 @@
/* eslint-disable @next/next/no-img-element */
import { cn } from '@/lib/utils';
import { Dialog, Transition } from '@headlessui/react';
import { Document } from '@langchain/core/documents';
import Link from 'next/link';
import { Fragment, useState } from 'react';
const MessageSources = ({ sources }: { sources: Document[] }) => {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const closeModal = () => {
function closeModal() {
setIsDialogOpen(false);
document.body.classList.remove('overflow-hidden-scrollable');
};
}
const openModal = () => {
function openModal() {
setIsDialogOpen(true);
document.body.classList.add('overflow-hidden-scrollable');
};
}
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-2">

View File

@ -38,12 +38,12 @@ const Navbar = ({ messages }: { messages: Message[] }) => {
}, []);
return (
<div className="fixed z-40 top-0 left-0 right-0 px-4 lg:pl-[104px] lg:pr-6 lg:px-8 flex flex-row items-center justify-between w-full py-4 text-sm text-white/70 border-b bg-[#0A0A0A] border-[#1C1C1C]">
<div className="fixed z-40 top-0 left-0 right-0 px-4 lg:pl-32 lg:pr-4 lg:px-8 flex flex-row items-center justify-between w-full py-4 text-sm text-white/70 border-b bg-[#0A0A0A] border-[#1C1C1C]">
<Edit
size={17}
className="active:scale-95 transition duration-100 cursor-pointer lg:hidden"
/>
<div className="hidden lg:flex flex-row items-center justify-center space-x-2">
<div className="hidden lg:flex flex-row items-center space-x-2">
<Clock size={17} />
<p className="text-xs">{timeAgo} ago</p>
</div>

View File

@ -3,7 +3,6 @@ import { ImagesIcon, PlusIcon } from 'lucide-react';
import { useState } from 'react';
import Lightbox from 'yet-another-react-lightbox';
import 'yet-another-react-lightbox/styles.css';
import { Message } from './ChatWindow';
type Image = {
url: string;
@ -11,13 +10,7 @@ type Image = {
title: string;
};
const SearchImages = ({
query,
chat_history,
}: {
query: string;
chat_history: Message[];
}) => {
const SearchImages = ({ query }: { query: string }) => {
const [images, setImages] = useState<Image[] | null>(null);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
@ -29,10 +22,6 @@ const SearchImages = ({
<button
onClick={async () => {
setLoading(true);
const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel');
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/images`,
{
@ -42,9 +31,7 @@ const SearchImages = ({
},
body: JSON.stringify({
query: query,
chat_history: chat_history,
chat_model_provider: chatModelProvider,
chat_model: chatModel,
chat_history: [],
}),
},
);
@ -72,7 +59,7 @@ const SearchImages = ({
</button>
)}
{loading && (
<div className="grid grid-cols-2 gap-2">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-2">
{[...Array(4)].map((_, i) => (
<div
key={i}
@ -98,7 +85,7 @@ const SearchImages = ({
key={i}
src={image.img_src}
alt={image.title}
className="h-full w-full aspect-video object-cover rounded-lg transition duration-200 active:scale-95 hover:scale-[1.02] cursor-zoom-in"
className="h-full w-full aspect-video object-cover rounded-lg transition duration-200 active:scale-95 cursor-pointer"
/>
))
: images.map((image, i) => (
@ -114,13 +101,13 @@ const SearchImages = ({
key={i}
src={image.img_src}
alt={image.title}
className="h-full w-full aspect-video object-cover rounded-lg transition duration-200 active:scale-95 hover:scale-[1.02] cursor-zoom-in"
className="h-full w-full aspect-video object-cover rounded-lg transition duration-200 active:scale-95 cursor-pointer"
/>
))}
{images.length > 4 && (
<button
onClick={() => setOpen(true)}
className="bg-[#111111] hover:bg-[#1c1c1c] transition duration-200 active:scale-95 hover:scale-[1.02] h-auto w-full rounded-lg flex flex-col justify-between text-white p-2"
className="bg-[#111111] hover:bg-[#1c1c1c] transition duration-200 active:scale-95 h-auto w-full rounded-lg flex flex-col justify-between text-white p-2"
>
<div className="flex flex-row items-center space-x-1">
{images.slice(3, 6).map((image, i) => (
@ -133,7 +120,7 @@ const SearchImages = ({
))}
</div>
<p className="text-white/70 text-xs">
View {images.length - 3} more
View {images.slice(0, 2).length} more
</p>
</button>
)}

View File

@ -1,196 +0,0 @@
/* eslint-disable @next/next/no-img-element */
import { PlayCircle, PlayIcon, PlusIcon, VideoIcon } from 'lucide-react';
import { useState } from 'react';
import Lightbox, { GenericSlide, VideoSlide } from 'yet-another-react-lightbox';
import 'yet-another-react-lightbox/styles.css';
import { Message } from './ChatWindow';
type Video = {
url: string;
img_src: string;
title: string;
iframe_src: string;
};
declare module 'yet-another-react-lightbox' {
export interface VideoSlide extends GenericSlide {
type: 'video-slide';
src: string;
iframe_src: string;
}
interface SlideTypes {
'video-slide': VideoSlide;
}
}
const Searchvideos = ({
query,
chat_history,
}: {
query: string;
chat_history: Message[];
}) => {
const [videos, setVideos] = useState<Video[] | null>(null);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [slides, setSlides] = useState<VideoSlide[]>([]);
return (
<>
{!loading && videos === null && (
<button
onClick={async () => {
setLoading(true);
const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel');
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/videos`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: query,
chat_history: chat_history,
chat_model_provider: chatModelProvider,
chat_model: chatModel,
}),
},
);
const data = await res.json();
const videos = data.videos;
setVideos(videos);
setSlides(
videos.map((video: Video) => {
return {
type: 'video-slide',
iframe_src: video.iframe_src,
src: video.img_src,
};
}),
);
setLoading(false);
}}
className="border border-dashed border-[#1C1C1C] hover:bg-[#1c1c1c] active:scale-95 duration-200 transition px-4 py-2 flex flex-row items-center justify-between rounded-lg text-white text-sm w-full"
>
<div className="flex flex-row items-center space-x-2">
<VideoIcon size={17} />
<p>Search videos</p>
</div>
<PlusIcon className="text-[#24A0ED]" size={17} />
</button>
)}
{loading && (
<div className="grid grid-cols-2 gap-2">
{[...Array(4)].map((_, i) => (
<div
key={i}
className="bg-[#1C1C1C] h-32 w-full rounded-lg animate-pulse aspect-video object-cover"
/>
))}
</div>
)}
{videos !== null && videos.length > 0 && (
<>
<div className="grid grid-cols-2 gap-2">
{videos.length > 4
? videos.slice(0, 3).map((video, i) => (
<div
onClick={() => {
setOpen(true);
setSlides([
slides[i],
...slides.slice(0, i),
...slides.slice(i + 1),
]);
}}
className="relative transition duration-200 active:scale-95 hover:scale-[1.02] cursor-pointer"
key={i}
>
<img
src={video.img_src}
alt={video.title}
className="relative h-full w-full aspect-video object-cover rounded-lg"
/>
<div className="absolute bg-black/70 text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
<PlayCircle size={15} />
<p className="text-xs">Video</p>
</div>
</div>
))
: videos.map((video, i) => (
<div
onClick={() => {
setOpen(true);
setSlides([
slides[i],
...slides.slice(0, i),
...slides.slice(i + 1),
]);
}}
className="relative transition duration-200 active:scale-95 hover:scale-[1.02] cursor-pointer"
key={i}
>
<img
src={video.img_src}
alt={video.title}
className="relative h-full w-full aspect-video object-cover rounded-lg"
/>
<div className="absolute bg-black/70 text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
<PlayCircle size={15} />
<p className="text-xs">Video</p>
</div>
</div>
))}
{videos.length > 4 && (
<button
onClick={() => setOpen(true)}
className="bg-[#111111] hover:bg-[#1c1c1c] transition duration-200 active:scale-95 hover:scale-[1.02] h-auto w-full rounded-lg flex flex-col justify-between text-white p-2"
>
<div className="flex flex-row items-center space-x-1">
{videos.slice(3, 6).map((video, i) => (
<img
key={i}
src={video.img_src}
alt={video.title}
className="h-6 w-12 rounded-md lg:h-3 lg:w-6 lg:rounded-sm aspect-video object-cover"
/>
))}
</div>
<p className="text-white/70 text-xs">
View {videos.length - 3} more
</p>
</button>
)}
</div>
<Lightbox
open={open}
close={() => setOpen(false)}
slides={slides}
render={{
slide: ({ slide }) =>
slide.type === 'video-slide' ? (
<div className="h-full w-full flex flex-row items-center justify-center">
<iframe
src={slide.iframe_src}
className="aspect-video max-h-[95vh] w-[95vw] rounded-2xl md:w-[80vw]"
allowFullScreen
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
/>
</div>
) : null,
}}
/>
</>
)}
</>
);
};
export default Searchvideos;

View File

@ -1,248 +0,0 @@
import { Dialog, Transition } from '@headlessui/react';
import { CloudUpload, RefreshCcw, RefreshCw } from 'lucide-react';
import React, { Fragment, useEffect, useState } from 'react';
interface SettingsType {
providers: {
[key: string]: string[];
};
openaiApiKey: string;
groqApiKey: string;
ollamaApiUrl: string;
}
const SettingsDialog = ({
isOpen,
setIsOpen,
}: {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
}) => {
const [config, setConfig] = useState<SettingsType | null>(null);
const [selectedChatModelProvider, setSelectedChatModelProvider] = useState<
string | null
>(null);
const [selectedChatModel, setSelectedChatModel] = useState<string | null>(
null,
);
const [isLoading, setIsLoading] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
useEffect(() => {
if (isOpen) {
const fetchConfig = async () => {
setIsLoading(true);
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`);
const data = await res.json();
setConfig(data);
setIsLoading(false);
};
fetchConfig();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
useEffect(() => {
setSelectedChatModelProvider(localStorage.getItem('chatModelProvider'));
setSelectedChatModel(localStorage.getItem('chatModel'));
}, []);
const handleSubmit = async () => {
setIsUpdating(true);
try {
await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(config),
});
localStorage.setItem('chatModelProvider', selectedChatModelProvider!);
localStorage.setItem('chatModel', selectedChatModel!);
} catch (err) {
console.log(err);
} finally {
setIsUpdating(false);
setIsOpen(false);
window.location.reload();
}
};
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog
as="div"
className="relative z-50"
onClose={() => setIsOpen(false)}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black/50" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-200"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-100"
leaveFrom="opacity-100 scale-200"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-md transform rounded-2xl bg-[#111111] border border-[#1c1c1c] p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title className="text-xl font-medium leading-6 text-white">
Settings
</Dialog.Title>
{config && !isLoading && (
<div className="flex flex-col space-y-4 mt-6">
{config.providers && (
<div className="flex flex-col space-y-1">
<p className="text-white/70 text-sm">
Chat model Provider
</p>
<select
onChange={(e) => {
setSelectedChatModelProvider(e.target.value);
setSelectedChatModel(
config.providers[e.target.value][0],
);
}}
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
>
{Object.keys(config.providers).map((provider) => (
<option
key={provider}
value={provider}
selected={provider === selectedChatModelProvider}
>
{provider.charAt(0).toUpperCase() +
provider.slice(1)}
</option>
))}
</select>
</div>
)}
{selectedChatModelProvider && (
<div className="flex flex-col space-y-1">
<p className="text-white/70 text-sm">Chat Model</p>
<select
onChange={(e) => setSelectedChatModel(e.target.value)}
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
>
{config.providers[selectedChatModelProvider] ? (
config.providers[selectedChatModelProvider].length >
0 ? (
config.providers[selectedChatModelProvider].map(
(model) => (
<option
key={model}
value={model}
selected={model === selectedChatModel}
>
{model}
</option>
),
)
) : (
<option value="" disabled selected>
No models available
</option>
)
) : (
<option value="" disabled selected>
Invalid provider, please check backend logs
</option>
)}
</select>
</div>
)}
<div className="flex flex-col space-y-1">
<p className="text-white/70 text-sm">OpenAI API Key</p>
<input
type="text"
placeholder="OpenAI API Key"
defaultValue={config.openaiApiKey}
onChange={(e) =>
setConfig({
...config,
openaiApiKey: e.target.value,
})
}
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-white/70 text-sm">Ollama API URL</p>
<input
type="text"
placeholder="Ollama API URL"
defaultValue={config.ollamaApiUrl}
onChange={(e) =>
setConfig({
...config,
ollamaApiUrl: e.target.value,
})
}
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-white/70 text-sm">GROQ API Key</p>
<input
type="text"
placeholder="GROQ API Key"
defaultValue={config.groqApiKey}
onChange={(e) =>
setConfig({
...config,
groqApiKey: e.target.value,
})
}
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
/>
</div>
</div>
)}
{isLoading && (
<div className="w-full flex items-center justify-center mt-6 text-white/70 py-6">
<RefreshCcw className="animate-spin" />
</div>
)}
<div className="w-full mt-6 space-y-2">
<p className="text-xs text-white/50">
We&apos;ll refresh the page after updating the settings.
</p>
<button
onClick={handleSubmit}
className="bg-[#24A0ED] flex flex-row items-center space-x-2 text-white disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#ececec21] rounded-full px-4 py-2"
disabled={isLoading || isUpdating}
>
{isUpdating ? (
<RefreshCw size={20} className="animate-spin" />
) : (
<CloudUpload size={20} />
)}
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
);
};
export default SettingsDialog;

View File

@ -1,19 +1,16 @@
'use client';
import { cn } from '@/lib/utils';
import { BookOpenText, Home, Search, SquarePen, Settings } from 'lucide-react';
import { BookOpenText, Home, Search, SquarePen } from 'lucide-react';
import { SiGithub } from '@icons-pack/react-simple-icons';
import Link from 'next/link';
import { useSelectedLayoutSegments } from 'next/navigation';
import React, { Fragment, useState } from 'react';
import React from 'react';
import Layout from './Layout';
import { Dialog, Transition } from '@headlessui/react';
import SettingsDialog from './SettingsDialog';
const Sidebar = ({ children }: { children: React.ReactNode }) => {
const segments = useSelectedLayoutSegments();
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const navLinks = [
{
icon: Home,
@ -59,14 +56,16 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
</Link>
))}
</div>
<Settings
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
className="text-white cursor-pointer"
/>
<SettingsDialog
isOpen={isSettingsOpen}
setIsOpen={setIsSettingsOpen}
/>
<Link
href="https://github.com/ItzCrazyKns/Perplexica"
className="flex flex-col items-center text-center justify-center"
>
<SiGithub
className="text-white"
onPointerEnterCapture={undefined}
onPointerLeaveCapture={undefined}
/>
</Link>
</div>
</div>

View File

@ -1,6 +1,6 @@
{
"name": "perplexica-frontend",
"version": "1.2.0",
"version": "1.0.0",
"license": "MIT",
"author": "ItzCrazyKns",
"scripts": {
@ -22,7 +22,6 @@
"next": "14.1.4",
"react": "^18",
"react-dom": "^18",
"react-speech-kit": "^3.0.1",
"react-textarea-autosize": "^8.5.3",
"tailwind-merge": "^2.2.2",
"yet-another-react-lightbox": "^3.17.2",

View File

@ -2632,11 +2632,6 @@ react-is@^16.13.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-speech-kit@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/react-speech-kit/-/react-speech-kit-3.0.1.tgz#8bd936adfe064be1c5a07e2992dfdfd772e80d14"
integrity sha512-MXNOciISanhmnxpHJkBOev3M3NPDpW1T7nTc/eGw5pO9cXpoUccRxZkmr/IlpTPbPEneDNeTmbwri/YweyctZg==
react-textarea-autosize@^8.5.3:
version "8.5.3"
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz#d1e9fe760178413891484847d3378706052dd409"
@ -2850,7 +2845,6 @@ streamsearch@^1.1.0:
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
name string-width-cjs
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==

428
yarn.lock
View File

@ -17,11 +17,6 @@
node-fetch "^2.6.7"
web-streams-polyfill "^3.2.1"
"@colors/colors@1.6.0", "@colors/colors@^1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0"
integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==
"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
@ -29,20 +24,6 @@
dependencies:
"@jridgewell/trace-mapping" "0.3.9"
"@dabh/diagnostics@^2.0.2":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a"
integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==
dependencies:
colorspace "1.1.x"
enabled "2.0.x"
kuler "^2.0.0"
"@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":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
@ -236,21 +217,11 @@
"@types/node" "*"
"@types/send" "*"
"@types/triple-beam@^1.3.2":
version "1.3.5"
resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c"
integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==
"@types/uuid@^9.0.1":
version "9.0.8"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba"
integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
abort-controller@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
@ -288,14 +259,6 @@ ansi-styles@^5.0.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
anymatch@~3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
@ -311,11 +274,6 @@ array-flatten@1.1.1:
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
async@^3.2.3:
version "3.2.5"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66"
integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@ -330,11 +288,6 @@ axios@^1.6.8:
form-data "^4.0.0"
proxy-from-env "^1.1.0"
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base-64@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb"
@ -345,7 +298,7 @@ base64-js@^1.5.1:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
binary-extensions@^2.0.0, binary-extensions@^2.2.0:
binary-extensions@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
@ -373,21 +326,6 @@ body-parser@1.20.2:
type-is "~1.6.18"
unpipe "1.0.0"
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
dependencies:
fill-range "^7.0.1"
bytes@3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
@ -414,62 +352,6 @@ charenc@0.0.2:
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==
chokidar@^3.5.2:
version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
glob-parent "~5.1.2"
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
readdirp "~3.6.0"
optionalDependencies:
fsevents "~2.3.2"
color-convert@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
dependencies:
color-name "1.1.3"
color-name@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
color-name@^1.0.0:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-string@^1.6.0:
version "1.9.1"
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
dependencies:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
color@^3.1.3:
version "3.2.1"
resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164"
integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==
dependencies:
color-convert "^1.9.3"
color-string "^1.6.0"
colorspace@1.1.x:
version "1.1.4"
resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243"
integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==
dependencies:
color "^3.1.3"
text-hex "1.0.x"
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@ -508,11 +390,6 @@ compute-l2norm@^1.1.0:
validate.io-array "^1.0.3"
validate.io-function "^1.0.2"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
content-disposition@0.5.4:
version "0.5.4"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
@ -560,13 +437,6 @@ debug@2.6.9:
dependencies:
ms "2.0.0"
debug@^4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
decamelize@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@ -619,11 +489,6 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
enabled@2.0.x:
version "2.0.0"
resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
@ -703,18 +568,6 @@ express@^4.19.2:
utils-merge "1.0.1"
vary "~1.1.2"
fecha@^4.2.0:
version "4.2.3"
resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd"
integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
dependencies:
to-regex-range "^5.0.1"
finalhandler@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32"
@ -733,11 +586,6 @@ flat@^5.0.2:
resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241"
integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
fn.name@1.x.x:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
follow-redirects@^1.15.6:
version "1.15.6"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
@ -775,11 +623,6 @@ fresh@0.5.2:
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
@ -796,13 +639,6 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
has-symbols "^1.0.3"
hasown "^2.0.0"
glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
gopd@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
@ -810,11 +646,6 @@ gopd@^1.0.1:
dependencies:
get-intrinsic "^1.1.3"
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
has-property-descriptors@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
@ -864,12 +695,7 @@ iconv-lite@0.4.24:
dependencies:
safer-buffer ">= 2.1.2 < 3"
ignore-by-default@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==
inherits@2.0.4, inherits@^2.0.3:
inherits@2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@ -884,45 +710,11 @@ is-any-array@^2.0.0:
resolved "https://registry.yarnpkg.com/is-any-array/-/is-any-array-2.0.1.tgz#9233242a9c098220290aa2ec28f82ca7fa79899e"
integrity sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ==
is-arrayish@^0.3.1:
version "0.3.2"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-buffer@~1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-glob@^4.0.1, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-stream@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
js-tiktoken@^1.0.7, js-tiktoken@^1.0.8:
version "1.0.10"
resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.10.tgz#2b343ec169399dcee8f9ef9807dbd4fafd3b30dc"
@ -942,11 +734,6 @@ jsonpointer@^5.0.1:
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559"
integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==
kuler@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
langchain@^0.1.30:
version "0.1.30"
resolved "https://registry.yarnpkg.com/langchain/-/langchain-0.1.30.tgz#e1adb3f1849fcd5c596c668300afd5dc8cb37a97"
@ -986,25 +773,6 @@ langsmith@~0.1.1, langsmith@~0.1.7:
p-retry "4"
uuid "^9.0.0"
logform@^2.3.2, logform@^2.4.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/logform/-/logform-2.6.0.tgz#8c82a983f05d6eaeb2d75e3decae7a768b2bf9b5"
integrity sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==
dependencies:
"@colors/colors" "1.6.0"
"@types/triple-beam" "^1.3.2"
fecha "^4.2.0"
ms "^2.1.1"
safe-stable-stringify "^2.3.1"
triple-beam "^1.3.0"
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
make-error@^1.1.1:
version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
@ -1051,13 +819,6 @@ mime@1.6.0:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
dependencies:
brace-expansion "^1.1.7"
ml-array-mean@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/ml-array-mean/-/ml-array-mean-1.1.6.tgz#d951a700dc8e3a17b3e0a583c2c64abd0c619c56"
@ -1099,12 +860,7 @@ ms@2.0.0:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
ms@2.1.3, ms@^2.0.0, ms@^2.1.1:
ms@2.1.3, ms@^2.0.0:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@ -1126,34 +882,6 @@ node-fetch@^2.6.7:
dependencies:
whatwg-url "^5.0.0"
nodemon@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.0.tgz#ff7394f2450eb6a5e96fe4180acd5176b29799c9"
integrity sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==
dependencies:
chokidar "^3.5.2"
debug "^4"
ignore-by-default "^1.0.1"
minimatch "^3.1.2"
pstree.remy "^1.1.8"
semver "^7.5.3"
simple-update-notifier "^2.0.0"
supports-color "^5.5.0"
touch "^3.1.0"
undefsafe "^2.0.5"
nopt@~1.0.10:
version "1.0.10"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee"
integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==
dependencies:
abbrev "1"
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
num-sort@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/num-sort/-/num-sort-2.1.0.tgz#1cbb37aed071329fdf41151258bc011898577a9b"
@ -1176,13 +904,6 @@ on-finished@2.4.1:
dependencies:
ee-first "1.1.1"
one-time@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45"
integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==
dependencies:
fn.name "1.x.x"
openai@^4.26.0:
version "4.31.0"
resolved "https://registry.yarnpkg.com/openai/-/openai-4.31.0.tgz#5d96045c4eb244fa21f0fff0981043a2c9f09e8c"
@ -1241,11 +962,6 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
picomatch@^2.0.4, picomatch@^2.2.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
prettier@^3.2.5:
version "3.2.5"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368"
@ -1264,11 +980,6 @@ proxy-from-env@^1.1.0:
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
pstree.remy@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a"
integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==
qs@6.11.0:
version "6.11.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
@ -1291,33 +1002,12 @@ raw-body@2.5.2:
iconv-lite "0.4.24"
unpipe "1.0.0"
react-text-to-speech@^0.14.5:
version "0.14.5"
resolved "https://registry.yarnpkg.com/react-text-to-speech/-/react-text-to-speech-0.14.5.tgz#f918786ab283311535682011045bd49777193300"
integrity sha512-3brr/IrK/5YTtOZSTo+Y8b+dnWelzfZiDZvkXnOct1e7O7fgA/h9bYAVrtwSRo/VxKfdw+wh6glkj6M0mlQuQQ==
readable-stream@^3.4.0, readable-stream@^3.6.0:
version "3.6.2"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
dependencies:
inherits "^2.0.3"
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
retry@^0.13.1:
version "0.13.1"
resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658"
integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==
safe-buffer@5.2.1, safe-buffer@~5.2.0:
safe-buffer@5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@ -1327,23 +1017,11 @@ safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-stable-stringify@^2.3.1:
version "2.4.3"
resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886"
integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
semver@^7.5.3:
version "7.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d"
integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==
dependencies:
lru-cache "^6.0.0"
send@0.18.0:
version "0.18.0"
resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
@ -1400,78 +1078,21 @@ side-channel@^1.0.4:
get-intrinsic "^1.2.4"
object-inspect "^1.13.1"
simple-swizzle@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==
dependencies:
is-arrayish "^0.3.1"
simple-update-notifier@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb"
integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==
dependencies:
semver "^7.5.3"
stack-trace@0.0.x:
version "0.0.10"
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==
statuses@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
dependencies:
safe-buffer "~5.2.0"
supports-color@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
dependencies:
has-flag "^3.0.0"
text-hex@1.0.x:
version "1.0.0"
resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"
integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
toidentifier@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
touch@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==
dependencies:
nopt "~1.0.10"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
triple-beam@^1.3.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984"
integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==
ts-node@^10.9.2:
version "10.9.2"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
@ -1504,11 +1125,6 @@ typescript@^5.4.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.3.tgz#5c6fedd4c87bee01cd7a528a30145521f8e0feff"
integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==
undefsafe@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==
undici-types@~5.26.4:
version "5.26.5"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
@ -1519,11 +1135,6 @@ unpipe@1.0.0, unpipe@~1.0.0:
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
util-deprecate@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
@ -1577,42 +1188,11 @@ whatwg-url@^5.0.0:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
winston-transport@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.7.0.tgz#e302e6889e6ccb7f383b926df6936a5b781bd1f0"
integrity sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==
dependencies:
logform "^2.3.2"
readable-stream "^3.6.0"
triple-beam "^1.3.0"
winston@^3.13.0:
version "3.13.0"
resolved "https://registry.yarnpkg.com/winston/-/winston-3.13.0.tgz#e76c0d722f78e04838158c61adc1287201de7ce3"
integrity sha512-rwidmA1w3SE4j0E5MuIufFhyJPBDG7Nu71RkZor1p2+qHvJSZ9GYDA81AyleQcZbh/+V6HjeBdfnTZJm9rSeQQ==
dependencies:
"@colors/colors" "^1.6.0"
"@dabh/diagnostics" "^2.0.2"
async "^3.2.3"
is-stream "^2.0.0"
logform "^2.4.0"
one-time "^1.0.0"
readable-stream "^3.4.0"
safe-stable-stringify "^2.3.1"
stack-trace "0.0.x"
triple-beam "^1.3.0"
winston-transport "^4.7.0"
ws@^8.16.0:
version "8.16.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4"
integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yaml@^2.2.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.1.tgz#2e57e0b5e995292c25c75d2658f0664765210eed"