Upload 81 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .dockerignore +1 -0
- .github/ISSUE_TEMPLATE/bug_report.md +27 -0
- .github/ISSUE_TEMPLATE/custom.md +7 -0
- .github/ISSUE_TEMPLATE/feature_request.md +19 -0
- .gitignore +34 -0
- .prettierignore +38 -0
- .prettierrc.js +12 -0
- CONTRIBUTING.md +39 -0
- LICENSE +21 -0
- README.md +117 -11
- app.dockerfile +15 -0
- backend.dockerfile +18 -0
- config.toml +11 -0
- docker-compose.yaml +45 -0
- docs/architecture/README.md +11 -0
- docs/architecture/WORKING.md +19 -0
- package.json +36 -0
- searxng-settings.yml +2356 -0
- searxng.dockerfile +3 -0
- src/Perplexica - Shortcut.lnk +0 -0
- src/agents/academicSearchAgent.ts +265 -0
- src/agents/imageSearchAgent.ts +84 -0
- src/agents/redditSearchAgent.ts +260 -0
- src/agents/videoSearchAgent.ts +90 -0
- src/agents/webSearchAgent.ts +261 -0
- src/agents/wolframAlphaSearchAgent.ts +219 -0
- src/agents/writingAssistant.ts +90 -0
- src/agents/youtubeSearchAgent.ts +261 -0
- src/app.ts +30 -0
- src/config.ts +69 -0
- src/lib/providers.ts +157 -0
- src/lib/searxng.ts +47 -0
- src/routes/config.ts +63 -0
- src/routes/images.ts +46 -0
- src/routes/index.ts +14 -0
- src/routes/models.ts +24 -0
- src/routes/videos.ts +46 -0
- src/utils/computeSimilarity.ts +17 -0
- src/utils/formatHistory.ts +9 -0
- src/utils/logger.ts +22 -0
- src/websocket/connectionManager.ts +86 -0
- src/websocket/index.ts +8 -0
- src/websocket/messageHandler.ts +109 -0
- src/websocket/websocketServer.ts +16 -0
- tsconfig.json +17 -0
- ui/.env.example +2 -0
- ui/.eslintrc.json +3 -0
- ui/.gitignore +34 -0
- ui/.prettierrc.js +11 -0
- ui/app/discover/page.tsx +5 -0
.dockerignore
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
**/node_modules
|
.github/ISSUE_TEMPLATE/bug_report.md
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
name: Bug report
|
3 |
+
about: Create an issue to help us fix bugs
|
4 |
+
title: ''
|
5 |
+
labels: bug
|
6 |
+
assignees: ''
|
7 |
+
---
|
8 |
+
|
9 |
+
**Describe the bug**
|
10 |
+
A clear and concise description of what the bug is.
|
11 |
+
|
12 |
+
**To Reproduce**
|
13 |
+
Steps to reproduce the behavior:
|
14 |
+
|
15 |
+
1. Go to '...'
|
16 |
+
2. Click on '....'
|
17 |
+
3. Scroll down to '....'
|
18 |
+
4. See error
|
19 |
+
|
20 |
+
**Expected behavior**
|
21 |
+
A clear and concise description of what you expected to happen.
|
22 |
+
|
23 |
+
**Screenshots**
|
24 |
+
If applicable, add screenshots to help explain your problem.
|
25 |
+
|
26 |
+
**Additional context**
|
27 |
+
Add any other context about the problem here.
|
.github/ISSUE_TEMPLATE/custom.md
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
name: Custom issue template
|
3 |
+
about: Describe this issue template's purpose here.
|
4 |
+
title: ''
|
5 |
+
labels: ''
|
6 |
+
assignees: ''
|
7 |
+
---
|
.github/ISSUE_TEMPLATE/feature_request.md
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
name: Feature request
|
3 |
+
about: Suggest an idea for this project
|
4 |
+
title: ''
|
5 |
+
labels: enhancement
|
6 |
+
assignees: ''
|
7 |
+
---
|
8 |
+
|
9 |
+
**Is your feature request related to a problem? Please describe.**
|
10 |
+
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
11 |
+
|
12 |
+
**Describe the solution you'd like**
|
13 |
+
A clear and concise description of what you want to happen.
|
14 |
+
|
15 |
+
**Describe alternatives you've considered**
|
16 |
+
A clear and concise description of any alternative solutions or features you've considered.
|
17 |
+
|
18 |
+
**Additional context**
|
19 |
+
Add any other context or screenshots about the feature request here.
|
.gitignore
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Node.js
|
2 |
+
node_modules/
|
3 |
+
npm-debug.log
|
4 |
+
yarn-error.log
|
5 |
+
|
6 |
+
# Build output
|
7 |
+
/.next/
|
8 |
+
/out/
|
9 |
+
|
10 |
+
# IDE/Editor specific
|
11 |
+
.vscode/
|
12 |
+
.idea/
|
13 |
+
*.iml
|
14 |
+
|
15 |
+
# Environment variables
|
16 |
+
.env
|
17 |
+
.env.local
|
18 |
+
.env.development.local
|
19 |
+
.env.test.local
|
20 |
+
.env.production.local
|
21 |
+
|
22 |
+
# Config files
|
23 |
+
config.toml
|
24 |
+
|
25 |
+
# Log files
|
26 |
+
logs/
|
27 |
+
*.log
|
28 |
+
|
29 |
+
# Testing
|
30 |
+
/coverage/
|
31 |
+
|
32 |
+
# Miscellaneous
|
33 |
+
.DS_Store
|
34 |
+
Thumbs.db
|
.prettierignore
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Ignore all files in the node_modules directory
|
2 |
+
node_modules
|
3 |
+
|
4 |
+
# Ignore all files in the .next directory (Next.js build output)
|
5 |
+
.next
|
6 |
+
|
7 |
+
# Ignore all files in the .out directory (TypeScript build output)
|
8 |
+
.out
|
9 |
+
|
10 |
+
# Ignore all files in the .cache directory (Prettier cache)
|
11 |
+
.cache
|
12 |
+
|
13 |
+
# Ignore all files in the .vscode directory (Visual Studio Code settings)
|
14 |
+
.vscode
|
15 |
+
|
16 |
+
# Ignore all files in the .idea directory (IntelliJ IDEA settings)
|
17 |
+
.idea
|
18 |
+
|
19 |
+
# Ignore all files in the dist directory (build output)
|
20 |
+
dist
|
21 |
+
|
22 |
+
# Ignore all files in the build directory (build output)
|
23 |
+
build
|
24 |
+
|
25 |
+
# Ignore all files in the coverage directory (test coverage reports)
|
26 |
+
coverage
|
27 |
+
|
28 |
+
# Ignore all files with the .log extension
|
29 |
+
*.log
|
30 |
+
|
31 |
+
# Ignore all files with the .tmp extension
|
32 |
+
*.tmp
|
33 |
+
|
34 |
+
# Ignore all files with the .swp extension
|
35 |
+
*.swp
|
36 |
+
|
37 |
+
# Ignore all files with the .DS_Store extension (macOS specific)
|
38 |
+
.DS_Store
|
.prettierrc.js
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/** @type {import("prettier").Config} */
|
2 |
+
|
3 |
+
const config = {
|
4 |
+
printWidth: 80,
|
5 |
+
trailingComma: 'all',
|
6 |
+
endOfLine: 'auto',
|
7 |
+
singleQuote: true,
|
8 |
+
tabWidth: 2,
|
9 |
+
semi: true,
|
10 |
+
};
|
11 |
+
|
12 |
+
module.exports = config;
|
CONTRIBUTING.md
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# How to Contribute to Perplexica
|
2 |
+
|
3 |
+
Hey there, thanks for deciding to contribute to Perplexica. Anything you help with will support the development of Perplexica and will make it better. Let's walk you through the key aspects to ensure your contributions are effective and in harmony with the project's setup.
|
4 |
+
|
5 |
+
## Project Structure
|
6 |
+
|
7 |
+
Perplexica's design consists of two main domains:
|
8 |
+
|
9 |
+
- **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.
|
10 |
+
- **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.
|
11 |
+
|
12 |
+
## Setting Up Your Environment
|
13 |
+
|
14 |
+
Before diving into coding, setting up your local environment is key. Here's what you need to do:
|
15 |
+
|
16 |
+
### Backend
|
17 |
+
|
18 |
+
1. In the root directory, locate the `sample.config.toml` file.
|
19 |
+
2. Rename it to `config.toml` and fill in the necessary configuration fields specific to the backend.
|
20 |
+
3. Run `npm install` to install dependencies.
|
21 |
+
4. Use `npm run dev` to start the backend in development mode.
|
22 |
+
|
23 |
+
### Frontend
|
24 |
+
|
25 |
+
1. Navigate to the `ui` folder and repeat the process of renaming `.env.example` to `.env`, making sure to provide the frontend-specific variables.
|
26 |
+
2. Execute `npm install` within the `ui` directory to get the frontend dependencies ready.
|
27 |
+
3. Launch the frontend development server with `npm run dev`.
|
28 |
+
|
29 |
+
**Please note**: Docker configurations are present for setting up production environments, whereas `npm run dev` is used for development purposes.
|
30 |
+
|
31 |
+
## Coding and Contribution Practices
|
32 |
+
|
33 |
+
Before committing changes:
|
34 |
+
|
35 |
+
1. Ensure that your code functions correctly by thorough testing.
|
36 |
+
2. Always run `npm run format:write` to format your code according to the project's coding standards. This helps maintain consistency and code quality.
|
37 |
+
3. We currently do not have a code of conduct, but it is in the works. In the meantime, please be mindful of how you engage with the project and its community.
|
38 |
+
|
39 |
+
Following these steps will help maintain the integrity of Perplexica's codebase and facilitate a smoother integration of your valuable contributions. Thank you for your support and commitment to improving Perplexica.
|
LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MIT License
|
2 |
+
|
3 |
+
Copyright (c) 2024 ItzCrazyKns
|
4 |
+
|
5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6 |
+
of this software and associated documentation files (the "Software"), to deal
|
7 |
+
in the Software without restriction, including without limitation the rights
|
8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9 |
+
copies of the Software, and to permit persons to whom the Software is
|
10 |
+
furnished to do so, subject to the following conditions:
|
11 |
+
|
12 |
+
The above copyright notice and this permission notice shall be included in all
|
13 |
+
copies or substantial portions of the Software.
|
14 |
+
|
15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21 |
+
SOFTWARE.
|
README.md
CHANGED
@@ -1,11 +1,117 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 🚀 Perplexica - An AI-powered search engine 🔎 <!-- omit in toc -->
|
2 |
+
|
3 |
+

|
4 |
+
|
5 |
+
## Table of Contents <!-- omit in toc -->
|
6 |
+
|
7 |
+
- [Overview](#overview)
|
8 |
+
- [Preview](#preview)
|
9 |
+
- [Features](#features)
|
10 |
+
- [Installation](#installation)
|
11 |
+
- [Getting Started with Docker (Recommended)](#getting-started-with-docker-recommended)
|
12 |
+
- [Non-Docker Installation](#non-docker-installation)
|
13 |
+
- [One-Click Deployment](#one-click-deployment)
|
14 |
+
- [Upcoming Features](#upcoming-features)
|
15 |
+
- [Support Us](#support-us)
|
16 |
+
- [Contribution](#contribution)
|
17 |
+
- [Help and Support](#help-and-support)
|
18 |
+
|
19 |
+
## Overview
|
20 |
+
|
21 |
+
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.
|
22 |
+
|
23 |
+
Using SearxNG to stay current and fully open source, Perplexica ensures you always get the most up-to-date information without compromising your privacy.
|
24 |
+
|
25 |
+
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).
|
26 |
+
|
27 |
+
## Preview
|
28 |
+
|
29 |
+

|
30 |
+
|
31 |
+
## Features
|
32 |
+
|
33 |
+
- **Local LLMs**: You can make use local LLMs such as Llama3 and Mixtral using Ollama.
|
34 |
+
- **Two Main Modes:**
|
35 |
+
- **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.
|
36 |
+
- **Normal Mode:** Processes your query and performs a web search.
|
37 |
+
- **Focus Modes:** Special modes to better answer specific types of questions. Perplexica currently has 6 focus modes:
|
38 |
+
- **All Mode:** Searches the entire web to find the best results.
|
39 |
+
- **Writing Assistant Mode:** Helpful for writing tasks that does not require searching the web.
|
40 |
+
- **Academic Search Mode:** Finds articles and papers, ideal for academic research.
|
41 |
+
- **YouTube Search Mode:** Finds YouTube videos based on the search query.
|
42 |
+
- **Wolfram Alpha Search Mode:** Answers queries that need calculations or data analysis using Wolfram Alpha.
|
43 |
+
- **Reddit Search Mode:** Searches Reddit for discussions and opinions related to the query.
|
44 |
+
- **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.
|
45 |
+
|
46 |
+
It has many more features like image and video search. Some of the planned features are mentioned in [upcoming features](#upcoming-features).
|
47 |
+
|
48 |
+
## Installation
|
49 |
+
|
50 |
+
There are mainly 2 ways of installing Perplexica - With Docker, Without Docker. Using Docker is highly recommended.
|
51 |
+
|
52 |
+
### Getting Started with Docker (Recommended)
|
53 |
+
|
54 |
+
1. Ensure Docker is installed and running on your system.
|
55 |
+
2. Clone the Perplexica repository:
|
56 |
+
|
57 |
+
```bash
|
58 |
+
git clone https://github.com/ItzCrazyKns/Perplexica.git
|
59 |
+
```
|
60 |
+
|
61 |
+
3. After cloning, navigate to the directory containing the project files.
|
62 |
+
|
63 |
+
4. Rename the `sample.config.toml` file to `config.toml`. For Docker setups, you need only fill in the following fields:
|
64 |
+
|
65 |
+
- `OPENAI`: Your OpenAI API key. **You only need to fill this if you wish to use OpenAI's models**.
|
66 |
+
- `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**.
|
67 |
+
- `GROQ`: Your Groq API key. **You only need to fill this if you wish to use Groq's hosted models**
|
68 |
+
|
69 |
+
**Note**: You can change these after starting Perplexica from the settings dialog.
|
70 |
+
|
71 |
+
- `SIMILARITY_MEASURE`: The similarity measure to use (This is filled by default; you can leave it as is if you are unsure about it.)
|
72 |
+
|
73 |
+
5. Ensure you are in the directory containing the `docker-compose.yaml` file and execute:
|
74 |
+
|
75 |
+
```bash
|
76 |
+
docker compose up -d
|
77 |
+
```
|
78 |
+
|
79 |
+
6. Wait a few minutes for the setup to complete. You can access Perplexica at http://localhost:3000 in your web browser.
|
80 |
+
|
81 |
+
**Note**: After the containers are built, you can start Perplexica directly from Docker without having to open a terminal.
|
82 |
+
|
83 |
+
### Non-Docker Installation
|
84 |
+
|
85 |
+
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.
|
86 |
+
2. Rename the `.env.example` file to `.env` in the `ui` folder and fill in all necessary fields.
|
87 |
+
3. After populating the configuration and environment files, run `npm i` in both the `ui` folder and the root directory.
|
88 |
+
4. Install the dependencies and then execute `npm run build` in both the `ui` folder and the root directory.
|
89 |
+
5. Finally, start both the frontend and the backend by running `npm run start` in both the `ui` folder and the root directory.
|
90 |
+
|
91 |
+
**Note**: Using Docker is recommended as it simplifies the setup process, especially for managing environment variables and dependencies.
|
92 |
+
|
93 |
+
## One-Click Deployment
|
94 |
+
|
95 |
+
[](https://repocloud.io/details/?app_id=267)
|
96 |
+
|
97 |
+
## Upcoming Features
|
98 |
+
|
99 |
+
- [ ] Finalizing Copilot Mode
|
100 |
+
- [x] Add settings page
|
101 |
+
- [x] Adding support for local LLMs
|
102 |
+
- [ ] Adding Discover and History Saving features
|
103 |
+
- [x] Introducing various Focus Modes
|
104 |
+
|
105 |
+
## Support Us
|
106 |
+
|
107 |
+
If you find Perplexica useful, consider giving us a star on GitHub. This helps more people discover Perplexica and supports the development of new features. Your support is appreciated.
|
108 |
+
|
109 |
+
## Contribution
|
110 |
+
|
111 |
+
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.
|
112 |
+
|
113 |
+
## Help and Support
|
114 |
+
|
115 |
+
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`.
|
116 |
+
|
117 |
+
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!
|
app.dockerfile
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM node:alpine
|
2 |
+
|
3 |
+
ARG NEXT_PUBLIC_WS_URL
|
4 |
+
ARG NEXT_PUBLIC_API_URL
|
5 |
+
ENV NEXT_PUBLIC_WS_URL=${NEXT_PUBLIC_WS_URL}
|
6 |
+
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
7 |
+
|
8 |
+
WORKDIR /home/perplexica
|
9 |
+
|
10 |
+
COPY ui /home/perplexica/
|
11 |
+
|
12 |
+
RUN yarn install
|
13 |
+
RUN yarn build
|
14 |
+
|
15 |
+
CMD ["yarn", "start"]
|
backend.dockerfile
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM node:alpine
|
2 |
+
|
3 |
+
ARG SEARXNG_API_URL
|
4 |
+
|
5 |
+
WORKDIR /home/perplexica
|
6 |
+
|
7 |
+
COPY src /home/perplexica/src
|
8 |
+
COPY tsconfig.json /home/perplexica/
|
9 |
+
COPY config.toml /home/perplexica/
|
10 |
+
COPY package.json /home/perplexica/
|
11 |
+
COPY yarn.lock /home/perplexica/
|
12 |
+
|
13 |
+
RUN sed -i "s|SEARXNG = \".*\"|SEARXNG = \"${SEARXNG_API_URL}\"|g" /home/perplexica/config.toml
|
14 |
+
|
15 |
+
RUN yarn install
|
16 |
+
RUN yarn build
|
17 |
+
|
18 |
+
CMD ["yarn", "start"]
|
config.toml
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[GENERAL]
|
2 |
+
PORT = 3001 # Port to run the server on
|
3 |
+
SIMILARITY_MEASURE = "cosine" # "cosine" or "dot"
|
4 |
+
|
5 |
+
[API_KEYS]
|
6 |
+
OPENAI = "" # OpenAI API key - sk-1234567890abcdef1234567890abcdef
|
7 |
+
GROQ = "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"
|
8 |
+
|
9 |
+
[API_ENDPOINTS]
|
10 |
+
SEARXNG = "http://localhost:32768" # SearxNG API URL
|
11 |
+
OLLAMA = "" # Ollama API URL - http://host.docker.internal:11434
|
docker-compose.yaml
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
services:
|
2 |
+
searxng:
|
3 |
+
build:
|
4 |
+
context: .
|
5 |
+
dockerfile: searxng.dockerfile
|
6 |
+
expose:
|
7 |
+
- 4000
|
8 |
+
ports:
|
9 |
+
- 4000:8080
|
10 |
+
networks:
|
11 |
+
- perplexica-network
|
12 |
+
|
13 |
+
perplexica-backend:
|
14 |
+
build:
|
15 |
+
context: .
|
16 |
+
dockerfile: backend.dockerfile
|
17 |
+
args:
|
18 |
+
- SEARXNG_API_URL=http://searxng:8080
|
19 |
+
depends_on:
|
20 |
+
- searxng
|
21 |
+
expose:
|
22 |
+
- 3001
|
23 |
+
ports:
|
24 |
+
- 3001:3001
|
25 |
+
networks:
|
26 |
+
- perplexica-network
|
27 |
+
|
28 |
+
perplexica-frontend:
|
29 |
+
build:
|
30 |
+
context: .
|
31 |
+
dockerfile: app.dockerfile
|
32 |
+
args:
|
33 |
+
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api
|
34 |
+
- NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001
|
35 |
+
depends_on:
|
36 |
+
- perplexica-backend
|
37 |
+
expose:
|
38 |
+
- 3000
|
39 |
+
ports:
|
40 |
+
- 3000:3000
|
41 |
+
networks:
|
42 |
+
- perplexica-network
|
43 |
+
|
44 |
+
networks:
|
45 |
+
perplexica-network:
|
docs/architecture/README.md
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
## Perplexica's Architecture
|
2 |
+
|
3 |
+
Perplexica's architecture consists of the following key components:
|
4 |
+
|
5 |
+
1. **User Interface**: A web-based interface that allows users to interact with Perplexica for searching images, videos, and much more.
|
6 |
+
2. **Agent/Chains**: These components predict Perplexica's next actions, understand user queries, and decide whether a web search is necessary.
|
7 |
+
3. **SearXNG**: A metadata search engine used by Perplexica to search the web for sources.
|
8 |
+
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.
|
9 |
+
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.
|
10 |
+
|
11 |
+
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).
|
docs/architecture/WORKING.md
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
## How does Perplexica work?
|
2 |
+
|
3 |
+
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).
|
4 |
+
|
5 |
+
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:
|
6 |
+
|
7 |
+
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.
|
8 |
+
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.
|
9 |
+
3. The query returned by the first chain is passed to SearXNG to search the web for information.
|
10 |
+
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.
|
11 |
+
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.
|
12 |
+
|
13 |
+
### How are the answers cited?
|
14 |
+
|
15 |
+
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.
|
16 |
+
|
17 |
+
### Image and Video Search
|
18 |
+
|
19 |
+
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.
|
package.json
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "perplexica-backend",
|
3 |
+
"version": "1.3.1",
|
4 |
+
"license": "MIT",
|
5 |
+
"author": "ItzCrazyKns",
|
6 |
+
"scripts": {
|
7 |
+
"start": "node dist/app.js",
|
8 |
+
"build": "tsc",
|
9 |
+
"dev": "nodemon src/app.ts",
|
10 |
+
"format": "prettier . --check",
|
11 |
+
"format:write": "prettier . --write"
|
12 |
+
},
|
13 |
+
"devDependencies": {
|
14 |
+
"@types/cors": "^2.8.17",
|
15 |
+
"@types/express": "^4.17.21",
|
16 |
+
"@types/readable-stream": "^4.0.11",
|
17 |
+
"nodemon": "^3.1.0",
|
18 |
+
"prettier": "^3.2.5",
|
19 |
+
"ts-node": "^10.9.2",
|
20 |
+
"typescript": "^5.4.3"
|
21 |
+
},
|
22 |
+
"dependencies": {
|
23 |
+
"@iarna/toml": "^2.2.5",
|
24 |
+
"@langchain/openai": "^0.0.25",
|
25 |
+
"axios": "^1.6.8",
|
26 |
+
"compute-cosine-similarity": "^1.1.0",
|
27 |
+
"compute-dot": "^1.1.0",
|
28 |
+
"cors": "^2.8.5",
|
29 |
+
"dotenv": "^16.4.5",
|
30 |
+
"express": "^4.19.2",
|
31 |
+
"langchain": "^0.1.30",
|
32 |
+
"winston": "^3.13.0",
|
33 |
+
"ws": "^8.16.0",
|
34 |
+
"zod": "^3.22.4"
|
35 |
+
}
|
36 |
+
}
|
searxng-settings.yml
ADDED
@@ -0,0 +1,2356 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
general:
|
2 |
+
# Debug mode, only for development. Is overwritten by ${SEARXNG_DEBUG}
|
3 |
+
debug: false
|
4 |
+
# displayed name
|
5 |
+
instance_name: 'searxng'
|
6 |
+
# For example: https://example.com/privacy
|
7 |
+
privacypolicy_url: false
|
8 |
+
# use true to use your own donation page written in searx/info/en/donate.md
|
9 |
+
# use false to disable the donation link
|
10 |
+
donation_url: false
|
11 |
+
# mailto:[email protected]
|
12 |
+
contact_url: false
|
13 |
+
# record stats
|
14 |
+
enable_metrics: true
|
15 |
+
|
16 |
+
brand:
|
17 |
+
new_issue_url: https://github.com/searxng/searxng/issues/new
|
18 |
+
docs_url: https://docs.searxng.org/
|
19 |
+
public_instances: https://searx.space
|
20 |
+
wiki_url: https://github.com/searxng/searxng/wiki
|
21 |
+
issue_url: https://github.com/searxng/searxng/issues
|
22 |
+
# custom:
|
23 |
+
# maintainer: "Jon Doe"
|
24 |
+
# # Custom entries in the footer: [title]: [link]
|
25 |
+
# links:
|
26 |
+
# Uptime: https://uptime.searxng.org/history/darmarit-org
|
27 |
+
# About: "https://searxng.org"
|
28 |
+
|
29 |
+
search:
|
30 |
+
# Filter results. 0: None, 1: Moderate, 2: Strict
|
31 |
+
safe_search: 0
|
32 |
+
# Existing autocomplete backends: "dbpedia", "duckduckgo", "google", "yandex", "mwmbl",
|
33 |
+
# "seznam", "startpage", "stract", "swisscows", "qwant", "wikipedia" - leave blank to turn it off
|
34 |
+
# by default.
|
35 |
+
autocomplete: 'google'
|
36 |
+
# minimun characters to type before autocompleter starts
|
37 |
+
autocomplete_min: 4
|
38 |
+
# Default search language - leave blank to detect from browser information or
|
39 |
+
# use codes from 'languages.py'
|
40 |
+
default_lang: 'auto'
|
41 |
+
# max_page: 0 # if engine supports paging, 0 means unlimited numbers of pages
|
42 |
+
# Available languages
|
43 |
+
# languages:
|
44 |
+
# - all
|
45 |
+
# - en
|
46 |
+
# - en-US
|
47 |
+
# - de
|
48 |
+
# - it-IT
|
49 |
+
# - fr
|
50 |
+
# - fr-BE
|
51 |
+
# ban time in seconds after engine errors
|
52 |
+
ban_time_on_fail: 5
|
53 |
+
# max ban time in seconds after engine errors
|
54 |
+
max_ban_time_on_fail: 120
|
55 |
+
suspended_times:
|
56 |
+
# Engine suspension time after error (in seconds; set to 0 to disable)
|
57 |
+
# For error "Access denied" and "HTTP error [402, 403]"
|
58 |
+
SearxEngineAccessDenied: 86400
|
59 |
+
# For error "CAPTCHA"
|
60 |
+
SearxEngineCaptcha: 86400
|
61 |
+
# For error "Too many request" and "HTTP error 429"
|
62 |
+
SearxEngineTooManyRequests: 3600
|
63 |
+
# Cloudflare CAPTCHA
|
64 |
+
cf_SearxEngineCaptcha: 1296000
|
65 |
+
cf_SearxEngineAccessDenied: 86400
|
66 |
+
# ReCAPTCHA
|
67 |
+
recaptcha_SearxEngineCaptcha: 604800
|
68 |
+
|
69 |
+
# remove format to deny access, use lower case.
|
70 |
+
# formats: [html, csv, json, rss]
|
71 |
+
formats:
|
72 |
+
- html
|
73 |
+
- json
|
74 |
+
|
75 |
+
server:
|
76 |
+
# Is overwritten by ${SEARXNG_PORT} and ${SEARXNG_BIND_ADDRESS}
|
77 |
+
port: 8888
|
78 |
+
bind_address: '127.0.0.1'
|
79 |
+
# public URL of the instance, to ensure correct inbound links. Is overwritten
|
80 |
+
# by ${SEARXNG_URL}.
|
81 |
+
base_url: / # "http://example.com/location"
|
82 |
+
limiter: false # rate limit the number of request on the instance, block some bots
|
83 |
+
public_instance: false # enable features designed only for public instances
|
84 |
+
|
85 |
+
# If your instance owns a /etc/searxng/settings.yml file, then set the following
|
86 |
+
# values there.
|
87 |
+
|
88 |
+
secret_key: 'a2fb23f1b02e6ee83875b09826990de0f6bd908b6638e8c10277d415f6ab852b' # Is overwritten by ${SEARXNG_SECRET}
|
89 |
+
# Proxying image results through searx
|
90 |
+
image_proxy: false
|
91 |
+
# 1.0 and 1.1 are supported
|
92 |
+
http_protocol_version: '1.0'
|
93 |
+
# POST queries are more secure as they don't show up in history but may cause
|
94 |
+
# problems when using Firefox containers
|
95 |
+
method: 'POST'
|
96 |
+
default_http_headers:
|
97 |
+
X-Content-Type-Options: nosniff
|
98 |
+
X-Download-Options: noopen
|
99 |
+
X-Robots-Tag: noindex, nofollow
|
100 |
+
Referrer-Policy: no-referrer
|
101 |
+
|
102 |
+
redis:
|
103 |
+
# URL to connect redis database. Is overwritten by ${SEARXNG_REDIS_URL}.
|
104 |
+
# https://docs.searxng.org/admin/settings/settings_redis.html#settings-redis
|
105 |
+
url: false
|
106 |
+
|
107 |
+
ui:
|
108 |
+
# Custom static path - leave it blank if you didn't change
|
109 |
+
static_path: ''
|
110 |
+
static_use_hash: false
|
111 |
+
# Custom templates path - leave it blank if you didn't change
|
112 |
+
templates_path: ''
|
113 |
+
# query_in_title: When true, the result page's titles contains the query
|
114 |
+
# it decreases the privacy, since the browser can records the page titles.
|
115 |
+
query_in_title: false
|
116 |
+
# infinite_scroll: When true, automatically loads the next page when scrolling to bottom of the current page.
|
117 |
+
infinite_scroll: false
|
118 |
+
# ui theme
|
119 |
+
default_theme: simple
|
120 |
+
# center the results ?
|
121 |
+
center_alignment: false
|
122 |
+
# URL prefix of the internet archive, don't forget trailing slash (if needed).
|
123 |
+
# cache_url: "https://webcache.googleusercontent.com/search?q=cache:"
|
124 |
+
# Default interface locale - leave blank to detect from browser information or
|
125 |
+
# use codes from the 'locales' config section
|
126 |
+
default_locale: ''
|
127 |
+
# Open result links in a new tab by default
|
128 |
+
# results_on_new_tab: false
|
129 |
+
theme_args:
|
130 |
+
# style of simple theme: auto, light, dark
|
131 |
+
simple_style: auto
|
132 |
+
# Perform search immediately if a category selected.
|
133 |
+
# Disable to select multiple categories at once and start the search manually.
|
134 |
+
search_on_category_select: true
|
135 |
+
# Hotkeys: default or vim
|
136 |
+
hotkeys: default
|
137 |
+
|
138 |
+
# Lock arbitrary settings on the preferences page. To find the ID of the user
|
139 |
+
# setting you want to lock, check the ID of the form on the page "preferences".
|
140 |
+
#
|
141 |
+
# preferences:
|
142 |
+
# lock:
|
143 |
+
# - language
|
144 |
+
# - autocomplete
|
145 |
+
# - method
|
146 |
+
# - query_in_title
|
147 |
+
|
148 |
+
# searx supports result proxification using an external service:
|
149 |
+
# https://github.com/asciimoo/morty uncomment below section if you have running
|
150 |
+
# morty proxy the key is base64 encoded (keep the !!binary notation)
|
151 |
+
# Note: since commit af77ec3, morty accepts a base64 encoded key.
|
152 |
+
#
|
153 |
+
# result_proxy:
|
154 |
+
# url: http://127.0.0.1:3000/
|
155 |
+
# # the key is a base64 encoded string, the YAML !!binary prefix is optional
|
156 |
+
# key: !!binary "your_morty_proxy_key"
|
157 |
+
# # [true|false] enable the "proxy" button next to each result
|
158 |
+
# proxify_results: true
|
159 |
+
|
160 |
+
# communication with search engines
|
161 |
+
#
|
162 |
+
outgoing:
|
163 |
+
# default timeout in seconds, can be override by engine
|
164 |
+
request_timeout: 3.0
|
165 |
+
# the maximum timeout in seconds
|
166 |
+
# max_request_timeout: 10.0
|
167 |
+
# suffix of searx_useragent, could contain information like an email address
|
168 |
+
# to the administrator
|
169 |
+
useragent_suffix: ''
|
170 |
+
# The maximum number of concurrent connections that may be established.
|
171 |
+
pool_connections: 100
|
172 |
+
# Allow the connection pool to maintain keep-alive connections below this
|
173 |
+
# point.
|
174 |
+
pool_maxsize: 20
|
175 |
+
# See https://www.python-httpx.org/http2/
|
176 |
+
enable_http2: true
|
177 |
+
# uncomment below section if you want to use a custom server certificate
|
178 |
+
# see https://www.python-httpx.org/advanced/#changing-the-verification-defaults
|
179 |
+
# and https://www.python-httpx.org/compatibility/#ssl-configuration
|
180 |
+
# verify: ~/.mitmproxy/mitmproxy-ca-cert.cer
|
181 |
+
#
|
182 |
+
# uncomment below section if you want to use a proxyq see: SOCKS proxies
|
183 |
+
# https://2.python-requests.org/en/latest/user/advanced/#proxies
|
184 |
+
# are also supported: see
|
185 |
+
# https://2.python-requests.org/en/latest/user/advanced/#socks
|
186 |
+
#
|
187 |
+
# proxies:
|
188 |
+
# all://:
|
189 |
+
# - http://proxy1:8080
|
190 |
+
# - http://proxy2:8080
|
191 |
+
#
|
192 |
+
# using_tor_proxy: true
|
193 |
+
#
|
194 |
+
# Extra seconds to add in order to account for the time taken by the proxy
|
195 |
+
#
|
196 |
+
# extra_proxy_timeout: 10.0
|
197 |
+
#
|
198 |
+
# uncomment below section only if you have more than one network interface
|
199 |
+
# which can be the source of outgoing search requests
|
200 |
+
#
|
201 |
+
# source_ips:
|
202 |
+
# - 1.1.1.1
|
203 |
+
# - 1.1.1.2
|
204 |
+
# - fe80::/126
|
205 |
+
|
206 |
+
# External plugin configuration, for more details see
|
207 |
+
# https://docs.searxng.org/dev/plugins.html
|
208 |
+
#
|
209 |
+
# plugins:
|
210 |
+
# - plugin1
|
211 |
+
# - plugin2
|
212 |
+
# - ...
|
213 |
+
|
214 |
+
# Comment or un-comment plugin to activate / deactivate by default.
|
215 |
+
#
|
216 |
+
# enabled_plugins:
|
217 |
+
# # these plugins are enabled if nothing is configured ..
|
218 |
+
# - 'Hash plugin'
|
219 |
+
# - 'Self Information'
|
220 |
+
# - 'Tracker URL remover'
|
221 |
+
# - 'Ahmia blacklist' # activation depends on outgoing.using_tor_proxy
|
222 |
+
# # these plugins are disabled if nothing is configured ..
|
223 |
+
# - 'Hostname replace' # see hostname_replace configuration below
|
224 |
+
# - 'Open Access DOI rewrite'
|
225 |
+
# - 'Tor check plugin'
|
226 |
+
# # Read the docs before activate: auto-detection of the language could be
|
227 |
+
# # detrimental to users expectations / users can activate the plugin in the
|
228 |
+
# # preferences if they want.
|
229 |
+
# - 'Autodetect search language'
|
230 |
+
|
231 |
+
# Configuration of the "Hostname replace" plugin:
|
232 |
+
#
|
233 |
+
# hostname_replace:
|
234 |
+
# '(.*\.)?youtube\.com$': 'invidious.example.com'
|
235 |
+
# '(.*\.)?youtu\.be$': 'invidious.example.com'
|
236 |
+
# '(.*\.)?youtube-noocookie\.com$': 'yotter.example.com'
|
237 |
+
# '(.*\.)?reddit\.com$': 'teddit.example.com'
|
238 |
+
# '(.*\.)?redd\.it$': 'teddit.example.com'
|
239 |
+
# '(www\.)?twitter\.com$': 'nitter.example.com'
|
240 |
+
# # to remove matching host names from result list, set value to false
|
241 |
+
# 'spam\.example\.com': false
|
242 |
+
|
243 |
+
checker:
|
244 |
+
# disable checker when in debug mode
|
245 |
+
off_when_debug: true
|
246 |
+
|
247 |
+
# use "scheduling: false" to disable scheduling
|
248 |
+
# scheduling: interval or int
|
249 |
+
|
250 |
+
# to activate the scheduler:
|
251 |
+
# * uncomment "scheduling" section
|
252 |
+
# * add "cache2 = name=searxngcache,items=2000,blocks=2000,blocksize=4096,bitmap=1"
|
253 |
+
# to your uwsgi.ini
|
254 |
+
|
255 |
+
# scheduling:
|
256 |
+
# start_after: [300, 1800] # delay to start the first run of the checker
|
257 |
+
# every: [86400, 90000] # how often the checker runs
|
258 |
+
|
259 |
+
# additional tests: only for the YAML anchors (see the engines section)
|
260 |
+
#
|
261 |
+
additional_tests:
|
262 |
+
rosebud: &test_rosebud
|
263 |
+
matrix:
|
264 |
+
query: rosebud
|
265 |
+
lang: en
|
266 |
+
result_container:
|
267 |
+
- not_empty
|
268 |
+
- ['one_title_contains', 'citizen kane']
|
269 |
+
test:
|
270 |
+
- unique_results
|
271 |
+
|
272 |
+
android: &test_android
|
273 |
+
matrix:
|
274 |
+
query: ['android']
|
275 |
+
lang: ['en', 'de', 'fr', 'zh-CN']
|
276 |
+
result_container:
|
277 |
+
- not_empty
|
278 |
+
- ['one_title_contains', 'google']
|
279 |
+
test:
|
280 |
+
- unique_results
|
281 |
+
|
282 |
+
# tests: only for the YAML anchors (see the engines section)
|
283 |
+
tests:
|
284 |
+
infobox: &tests_infobox
|
285 |
+
infobox:
|
286 |
+
matrix:
|
287 |
+
query: ['linux', 'new york', 'bbc']
|
288 |
+
result_container:
|
289 |
+
- has_infobox
|
290 |
+
|
291 |
+
categories_as_tabs:
|
292 |
+
general:
|
293 |
+
images:
|
294 |
+
videos:
|
295 |
+
news:
|
296 |
+
map:
|
297 |
+
music:
|
298 |
+
it:
|
299 |
+
science:
|
300 |
+
files:
|
301 |
+
social media:
|
302 |
+
|
303 |
+
engines:
|
304 |
+
- name: 9gag
|
305 |
+
engine: 9gag
|
306 |
+
shortcut: 9g
|
307 |
+
disabled: true
|
308 |
+
|
309 |
+
- name: annas archive
|
310 |
+
engine: annas_archive
|
311 |
+
disabled: true
|
312 |
+
shortcut: aa
|
313 |
+
|
314 |
+
# - name: annas articles
|
315 |
+
# engine: annas_archive
|
316 |
+
# shortcut: aaa
|
317 |
+
# # https://docs.searxng.org/dev/engines/online/annas_archive.html
|
318 |
+
# aa_content: 'journal_article' # book_any .. magazine, standards_document
|
319 |
+
# aa_ext: 'pdf' # pdf, epub, ..
|
320 |
+
# aa_sort: 'newest' # newest, oldest, largest, smallest
|
321 |
+
|
322 |
+
- name: apk mirror
|
323 |
+
engine: apkmirror
|
324 |
+
timeout: 4.0
|
325 |
+
shortcut: apkm
|
326 |
+
disabled: true
|
327 |
+
|
328 |
+
- name: apple app store
|
329 |
+
engine: apple_app_store
|
330 |
+
shortcut: aps
|
331 |
+
disabled: true
|
332 |
+
|
333 |
+
# Requires Tor
|
334 |
+
- name: ahmia
|
335 |
+
engine: ahmia
|
336 |
+
categories: onions
|
337 |
+
enable_http: true
|
338 |
+
shortcut: ah
|
339 |
+
|
340 |
+
- name: anaconda
|
341 |
+
engine: xpath
|
342 |
+
paging: true
|
343 |
+
first_page_num: 0
|
344 |
+
search_url: https://anaconda.org/search?q={query}&page={pageno}
|
345 |
+
results_xpath: //tbody/tr
|
346 |
+
url_xpath: ./td/h5/a[last()]/@href
|
347 |
+
title_xpath: ./td/h5
|
348 |
+
content_xpath: ./td[h5]/text()
|
349 |
+
categories: it
|
350 |
+
timeout: 6.0
|
351 |
+
shortcut: conda
|
352 |
+
disabled: true
|
353 |
+
|
354 |
+
- name: arch linux wiki
|
355 |
+
engine: archlinux
|
356 |
+
shortcut: al
|
357 |
+
|
358 |
+
- name: artic
|
359 |
+
engine: artic
|
360 |
+
shortcut: arc
|
361 |
+
timeout: 4.0
|
362 |
+
|
363 |
+
- name: arxiv
|
364 |
+
engine: arxiv
|
365 |
+
shortcut: arx
|
366 |
+
timeout: 4.0
|
367 |
+
|
368 |
+
- name: ask
|
369 |
+
engine: ask
|
370 |
+
shortcut: ask
|
371 |
+
disabled: true
|
372 |
+
|
373 |
+
# tmp suspended: dh key too small
|
374 |
+
# - name: base
|
375 |
+
# engine: base
|
376 |
+
# shortcut: bs
|
377 |
+
|
378 |
+
- name: bandcamp
|
379 |
+
engine: bandcamp
|
380 |
+
shortcut: bc
|
381 |
+
categories: music
|
382 |
+
|
383 |
+
- name: wikipedia
|
384 |
+
engine: wikipedia
|
385 |
+
shortcut: wp
|
386 |
+
# add "list" to the array to get results in the results list
|
387 |
+
display_type: ['infobox']
|
388 |
+
base_url: 'https://{language}.wikipedia.org/'
|
389 |
+
categories: [general]
|
390 |
+
|
391 |
+
- name: bilibili
|
392 |
+
engine: bilibili
|
393 |
+
shortcut: bil
|
394 |
+
disabled: true
|
395 |
+
|
396 |
+
- name: bing
|
397 |
+
engine: bing
|
398 |
+
shortcut: bi
|
399 |
+
disabled: true
|
400 |
+
|
401 |
+
- name: bing images
|
402 |
+
engine: bing_images
|
403 |
+
shortcut: bii
|
404 |
+
|
405 |
+
- name: bing news
|
406 |
+
engine: bing_news
|
407 |
+
shortcut: bin
|
408 |
+
|
409 |
+
- name: bing videos
|
410 |
+
engine: bing_videos
|
411 |
+
shortcut: biv
|
412 |
+
|
413 |
+
- name: bitbucket
|
414 |
+
engine: xpath
|
415 |
+
paging: true
|
416 |
+
search_url: https://bitbucket.org/repo/all/{pageno}?name={query}
|
417 |
+
url_xpath: //article[@class="repo-summary"]//a[@class="repo-link"]/@href
|
418 |
+
title_xpath: //article[@class="repo-summary"]//a[@class="repo-link"]
|
419 |
+
content_xpath: //article[@class="repo-summary"]/p
|
420 |
+
categories: [it, repos]
|
421 |
+
timeout: 4.0
|
422 |
+
disabled: true
|
423 |
+
shortcut: bb
|
424 |
+
about:
|
425 |
+
website: https://bitbucket.org/
|
426 |
+
wikidata_id: Q2493781
|
427 |
+
official_api_documentation: https://developer.atlassian.com/bitbucket
|
428 |
+
use_official_api: false
|
429 |
+
require_api_key: false
|
430 |
+
results: HTML
|
431 |
+
|
432 |
+
- name: bpb
|
433 |
+
engine: bpb
|
434 |
+
shortcut: bpb
|
435 |
+
disabled: true
|
436 |
+
|
437 |
+
- name: btdigg
|
438 |
+
engine: btdigg
|
439 |
+
shortcut: bt
|
440 |
+
disabled: true
|
441 |
+
|
442 |
+
- name: ccc-tv
|
443 |
+
engine: xpath
|
444 |
+
paging: false
|
445 |
+
search_url: https://media.ccc.de/search/?q={query}
|
446 |
+
url_xpath: //div[@class="caption"]/h3/a/@href
|
447 |
+
title_xpath: //div[@class="caption"]/h3/a/text()
|
448 |
+
content_xpath: //div[@class="caption"]/h4/@title
|
449 |
+
categories: videos
|
450 |
+
disabled: true
|
451 |
+
shortcut: c3tv
|
452 |
+
about:
|
453 |
+
website: https://media.ccc.de/
|
454 |
+
wikidata_id: Q80729951
|
455 |
+
official_api_documentation: https://github.com/voc/voctoweb
|
456 |
+
use_official_api: false
|
457 |
+
require_api_key: false
|
458 |
+
results: HTML
|
459 |
+
# We don't set language: de here because media.ccc.de is not just
|
460 |
+
# for a German audience. It contains many English videos and many
|
461 |
+
# German videos have English subtitles.
|
462 |
+
|
463 |
+
- name: openverse
|
464 |
+
engine: openverse
|
465 |
+
categories: images
|
466 |
+
shortcut: opv
|
467 |
+
|
468 |
+
- name: chefkoch
|
469 |
+
engine: chefkoch
|
470 |
+
shortcut: chef
|
471 |
+
# to show premium or plus results too:
|
472 |
+
# skip_premium: false
|
473 |
+
|
474 |
+
# - name: core.ac.uk
|
475 |
+
# engine: core
|
476 |
+
# categories: science
|
477 |
+
# shortcut: cor
|
478 |
+
# # get your API key from: https://core.ac.uk/api-keys/register/
|
479 |
+
# api_key: 'unset'
|
480 |
+
|
481 |
+
- name: crossref
|
482 |
+
engine: crossref
|
483 |
+
shortcut: cr
|
484 |
+
timeout: 30
|
485 |
+
disabled: true
|
486 |
+
|
487 |
+
- name: crowdview
|
488 |
+
engine: json_engine
|
489 |
+
shortcut: cv
|
490 |
+
categories: general
|
491 |
+
paging: false
|
492 |
+
search_url: https://crowdview-next-js.onrender.com/api/search-v3?query={query}
|
493 |
+
results_query: results
|
494 |
+
url_query: link
|
495 |
+
title_query: title
|
496 |
+
content_query: snippet
|
497 |
+
disabled: true
|
498 |
+
about:
|
499 |
+
website: https://crowdview.ai/
|
500 |
+
|
501 |
+
- name: yep
|
502 |
+
engine: yep
|
503 |
+
shortcut: yep
|
504 |
+
categories: general
|
505 |
+
search_type: web
|
506 |
+
disabled: true
|
507 |
+
|
508 |
+
- name: yep images
|
509 |
+
engine: yep
|
510 |
+
shortcut: yepi
|
511 |
+
categories: images
|
512 |
+
search_type: images
|
513 |
+
disabled: true
|
514 |
+
|
515 |
+
- name: yep news
|
516 |
+
engine: yep
|
517 |
+
shortcut: yepn
|
518 |
+
categories: news
|
519 |
+
search_type: news
|
520 |
+
disabled: true
|
521 |
+
|
522 |
+
- name: curlie
|
523 |
+
engine: xpath
|
524 |
+
shortcut: cl
|
525 |
+
categories: general
|
526 |
+
disabled: true
|
527 |
+
paging: true
|
528 |
+
lang_all: ''
|
529 |
+
search_url: https://curlie.org/search?q={query}&lang={lang}&start={pageno}&stime=92452189
|
530 |
+
page_size: 20
|
531 |
+
results_xpath: //div[@id="site-list-content"]/div[@class="site-item"]
|
532 |
+
url_xpath: ./div[@class="title-and-desc"]/a/@href
|
533 |
+
title_xpath: ./div[@class="title-and-desc"]/a/div
|
534 |
+
content_xpath: ./div[@class="title-and-desc"]/div[@class="site-descr"]
|
535 |
+
about:
|
536 |
+
website: https://curlie.org/
|
537 |
+
wikidata_id: Q60715723
|
538 |
+
use_official_api: false
|
539 |
+
require_api_key: false
|
540 |
+
results: HTML
|
541 |
+
|
542 |
+
- name: currency
|
543 |
+
engine: currency_convert
|
544 |
+
categories: general
|
545 |
+
shortcut: cc
|
546 |
+
|
547 |
+
- name: bahnhof
|
548 |
+
engine: json_engine
|
549 |
+
search_url: https://www.bahnhof.de/api/stations/search/{query}
|
550 |
+
url_prefix: https://www.bahnhof.de/
|
551 |
+
url_query: slug
|
552 |
+
title_query: name
|
553 |
+
content_query: state
|
554 |
+
shortcut: bf
|
555 |
+
disabled: true
|
556 |
+
about:
|
557 |
+
website: https://www.bahn.de
|
558 |
+
wikidata_id: Q22811603
|
559 |
+
use_official_api: false
|
560 |
+
require_api_key: false
|
561 |
+
results: JSON
|
562 |
+
language: de
|
563 |
+
|
564 |
+
- name: deezer
|
565 |
+
engine: deezer
|
566 |
+
shortcut: dz
|
567 |
+
disabled: true
|
568 |
+
|
569 |
+
- name: destatis
|
570 |
+
engine: destatis
|
571 |
+
shortcut: destat
|
572 |
+
disabled: true
|
573 |
+
|
574 |
+
- name: deviantart
|
575 |
+
engine: deviantart
|
576 |
+
shortcut: da
|
577 |
+
timeout: 3.0
|
578 |
+
|
579 |
+
- name: ddg definitions
|
580 |
+
engine: duckduckgo_definitions
|
581 |
+
shortcut: ddd
|
582 |
+
weight: 2
|
583 |
+
disabled: true
|
584 |
+
tests: *tests_infobox
|
585 |
+
|
586 |
+
# cloudflare protected
|
587 |
+
# - name: digbt
|
588 |
+
# engine: digbt
|
589 |
+
# shortcut: dbt
|
590 |
+
# timeout: 6.0
|
591 |
+
# disabled: true
|
592 |
+
|
593 |
+
- name: docker hub
|
594 |
+
engine: docker_hub
|
595 |
+
shortcut: dh
|
596 |
+
categories: [it, packages]
|
597 |
+
|
598 |
+
- name: erowid
|
599 |
+
engine: xpath
|
600 |
+
paging: true
|
601 |
+
first_page_num: 0
|
602 |
+
page_size: 30
|
603 |
+
search_url: https://www.erowid.org/search.php?q={query}&s={pageno}
|
604 |
+
url_xpath: //dl[@class="results-list"]/dt[@class="result-title"]/a/@href
|
605 |
+
title_xpath: //dl[@class="results-list"]/dt[@class="result-title"]/a/text()
|
606 |
+
content_xpath: //dl[@class="results-list"]/dd[@class="result-details"]
|
607 |
+
categories: []
|
608 |
+
shortcut: ew
|
609 |
+
disabled: true
|
610 |
+
about:
|
611 |
+
website: https://www.erowid.org/
|
612 |
+
wikidata_id: Q1430691
|
613 |
+
official_api_documentation:
|
614 |
+
use_official_api: false
|
615 |
+
require_api_key: false
|
616 |
+
results: HTML
|
617 |
+
|
618 |
+
# - name: elasticsearch
|
619 |
+
# shortcut: es
|
620 |
+
# engine: elasticsearch
|
621 |
+
# base_url: http://localhost:9200
|
622 |
+
# username: elastic
|
623 |
+
# password: changeme
|
624 |
+
# index: my-index
|
625 |
+
# # available options: match, simple_query_string, term, terms, custom
|
626 |
+
# query_type: match
|
627 |
+
# # if query_type is set to custom, provide your query here
|
628 |
+
# #custom_query_json: {"query":{"match_all": {}}}
|
629 |
+
# #show_metadata: false
|
630 |
+
# disabled: true
|
631 |
+
|
632 |
+
- name: wikidata
|
633 |
+
engine: wikidata
|
634 |
+
shortcut: wd
|
635 |
+
timeout: 3.0
|
636 |
+
weight: 2
|
637 |
+
# add "list" to the array to get results in the results list
|
638 |
+
display_type: ['infobox']
|
639 |
+
tests: *tests_infobox
|
640 |
+
categories: [general]
|
641 |
+
|
642 |
+
- name: duckduckgo
|
643 |
+
engine: duckduckgo
|
644 |
+
shortcut: ddg
|
645 |
+
|
646 |
+
- name: duckduckgo images
|
647 |
+
engine: duckduckgo_extra
|
648 |
+
categories: [images, web]
|
649 |
+
ddg_category: images
|
650 |
+
shortcut: ddi
|
651 |
+
disabled: true
|
652 |
+
|
653 |
+
- name: duckduckgo videos
|
654 |
+
engine: duckduckgo_extra
|
655 |
+
categories: [videos, web]
|
656 |
+
ddg_category: videos
|
657 |
+
shortcut: ddv
|
658 |
+
disabled: true
|
659 |
+
|
660 |
+
- name: duckduckgo news
|
661 |
+
engine: duckduckgo_extra
|
662 |
+
categories: [news, web]
|
663 |
+
ddg_category: news
|
664 |
+
shortcut: ddn
|
665 |
+
disabled: true
|
666 |
+
|
667 |
+
- name: duckduckgo weather
|
668 |
+
engine: duckduckgo_weather
|
669 |
+
shortcut: ddw
|
670 |
+
disabled: true
|
671 |
+
|
672 |
+
- name: apple maps
|
673 |
+
engine: apple_maps
|
674 |
+
shortcut: apm
|
675 |
+
disabled: true
|
676 |
+
timeout: 5.0
|
677 |
+
|
678 |
+
- name: emojipedia
|
679 |
+
engine: emojipedia
|
680 |
+
timeout: 4.0
|
681 |
+
shortcut: em
|
682 |
+
disabled: true
|
683 |
+
|
684 |
+
- name: tineye
|
685 |
+
engine: tineye
|
686 |
+
shortcut: tin
|
687 |
+
timeout: 9.0
|
688 |
+
disabled: true
|
689 |
+
|
690 |
+
- name: etymonline
|
691 |
+
engine: xpath
|
692 |
+
paging: true
|
693 |
+
search_url: https://etymonline.com/search?page={pageno}&q={query}
|
694 |
+
url_xpath: //a[contains(@class, "word__name--")]/@href
|
695 |
+
title_xpath: //a[contains(@class, "word__name--")]
|
696 |
+
content_xpath: //section[contains(@class, "word__defination")]
|
697 |
+
first_page_num: 1
|
698 |
+
shortcut: et
|
699 |
+
categories: [dictionaries]
|
700 |
+
about:
|
701 |
+
website: https://www.etymonline.com/
|
702 |
+
wikidata_id: Q1188617
|
703 |
+
official_api_documentation:
|
704 |
+
use_official_api: false
|
705 |
+
require_api_key: false
|
706 |
+
results: HTML
|
707 |
+
|
708 |
+
# - name: ebay
|
709 |
+
# engine: ebay
|
710 |
+
# shortcut: eb
|
711 |
+
# base_url: 'https://www.ebay.com'
|
712 |
+
# disabled: true
|
713 |
+
# timeout: 5
|
714 |
+
|
715 |
+
- name: 1x
|
716 |
+
engine: www1x
|
717 |
+
shortcut: 1x
|
718 |
+
timeout: 3.0
|
719 |
+
disabled: true
|
720 |
+
|
721 |
+
- name: fdroid
|
722 |
+
engine: fdroid
|
723 |
+
shortcut: fd
|
724 |
+
disabled: true
|
725 |
+
|
726 |
+
- name: flickr
|
727 |
+
categories: images
|
728 |
+
shortcut: fl
|
729 |
+
# You can use the engine using the official stable API, but you need an API
|
730 |
+
# key, see: https://www.flickr.com/services/apps/create/
|
731 |
+
# engine: flickr
|
732 |
+
# api_key: 'apikey' # required!
|
733 |
+
# Or you can use the html non-stable engine, activated by default
|
734 |
+
engine: flickr_noapi
|
735 |
+
|
736 |
+
- name: free software directory
|
737 |
+
engine: mediawiki
|
738 |
+
shortcut: fsd
|
739 |
+
categories: [it, software wikis]
|
740 |
+
base_url: https://directory.fsf.org/
|
741 |
+
search_type: title
|
742 |
+
timeout: 5.0
|
743 |
+
disabled: true
|
744 |
+
about:
|
745 |
+
website: https://directory.fsf.org/
|
746 |
+
wikidata_id: Q2470288
|
747 |
+
|
748 |
+
# - name: freesound
|
749 |
+
# engine: freesound
|
750 |
+
# shortcut: fnd
|
751 |
+
# disabled: true
|
752 |
+
# timeout: 15.0
|
753 |
+
# API key required, see: https://freesound.org/docs/api/overview.html
|
754 |
+
# api_key: MyAPIkey
|
755 |
+
|
756 |
+
- name: frinkiac
|
757 |
+
engine: frinkiac
|
758 |
+
shortcut: frk
|
759 |
+
disabled: true
|
760 |
+
|
761 |
+
- name: fyyd
|
762 |
+
engine: fyyd
|
763 |
+
shortcut: fy
|
764 |
+
timeout: 8.0
|
765 |
+
disabled: true
|
766 |
+
|
767 |
+
- name: genius
|
768 |
+
engine: genius
|
769 |
+
shortcut: gen
|
770 |
+
|
771 |
+
- name: gentoo
|
772 |
+
engine: gentoo
|
773 |
+
shortcut: ge
|
774 |
+
timeout: 10.0
|
775 |
+
|
776 |
+
- name: gitlab
|
777 |
+
engine: json_engine
|
778 |
+
paging: true
|
779 |
+
search_url: https://gitlab.com/api/v4/projects?search={query}&page={pageno}
|
780 |
+
url_query: web_url
|
781 |
+
title_query: name_with_namespace
|
782 |
+
content_query: description
|
783 |
+
page_size: 20
|
784 |
+
categories: [it, repos]
|
785 |
+
shortcut: gl
|
786 |
+
timeout: 10.0
|
787 |
+
disabled: true
|
788 |
+
about:
|
789 |
+
website: https://about.gitlab.com/
|
790 |
+
wikidata_id: Q16639197
|
791 |
+
official_api_documentation: https://docs.gitlab.com/ee/api/
|
792 |
+
use_official_api: false
|
793 |
+
require_api_key: false
|
794 |
+
results: JSON
|
795 |
+
|
796 |
+
- name: github
|
797 |
+
engine: github
|
798 |
+
shortcut: gh
|
799 |
+
|
800 |
+
# This a Gitea service. If you would like to use a different instance,
|
801 |
+
# change codeberg.org to URL of the desired Gitea host. Or you can create a
|
802 |
+
# new engine by copying this and changing the name, shortcut and search_url.
|
803 |
+
|
804 |
+
- name: codeberg
|
805 |
+
engine: json_engine
|
806 |
+
search_url: https://codeberg.org/api/v1/repos/search?q={query}&limit=10
|
807 |
+
url_query: html_url
|
808 |
+
title_query: name
|
809 |
+
content_query: description
|
810 |
+
categories: [it, repos]
|
811 |
+
shortcut: cb
|
812 |
+
disabled: true
|
813 |
+
about:
|
814 |
+
website: https://codeberg.org/
|
815 |
+
wikidata_id:
|
816 |
+
official_api_documentation: https://try.gitea.io/api/swagger
|
817 |
+
use_official_api: false
|
818 |
+
require_api_key: false
|
819 |
+
results: JSON
|
820 |
+
|
821 |
+
- name: goodreads
|
822 |
+
engine: goodreads
|
823 |
+
shortcut: good
|
824 |
+
timeout: 4.0
|
825 |
+
disabled: true
|
826 |
+
|
827 |
+
- name: google
|
828 |
+
engine: google
|
829 |
+
shortcut: go
|
830 |
+
# additional_tests:
|
831 |
+
# android: *test_android
|
832 |
+
|
833 |
+
- name: google images
|
834 |
+
engine: google_images
|
835 |
+
shortcut: goi
|
836 |
+
# additional_tests:
|
837 |
+
# android: *test_android
|
838 |
+
# dali:
|
839 |
+
# matrix:
|
840 |
+
# query: ['Dali Christ']
|
841 |
+
# lang: ['en', 'de', 'fr', 'zh-CN']
|
842 |
+
# result_container:
|
843 |
+
# - ['one_title_contains', 'Salvador']
|
844 |
+
|
845 |
+
- name: google news
|
846 |
+
engine: google_news
|
847 |
+
shortcut: gon
|
848 |
+
# additional_tests:
|
849 |
+
# android: *test_android
|
850 |
+
|
851 |
+
- name: google videos
|
852 |
+
engine: google_videos
|
853 |
+
shortcut: gov
|
854 |
+
# additional_tests:
|
855 |
+
# android: *test_android
|
856 |
+
|
857 |
+
- name: google scholar
|
858 |
+
engine: google_scholar
|
859 |
+
shortcut: gos
|
860 |
+
|
861 |
+
- name: google play apps
|
862 |
+
engine: google_play
|
863 |
+
categories: [files, apps]
|
864 |
+
shortcut: gpa
|
865 |
+
play_categ: apps
|
866 |
+
disabled: true
|
867 |
+
|
868 |
+
- name: google play movies
|
869 |
+
engine: google_play
|
870 |
+
categories: videos
|
871 |
+
shortcut: gpm
|
872 |
+
play_categ: movies
|
873 |
+
disabled: true
|
874 |
+
|
875 |
+
- name: material icons
|
876 |
+
engine: material_icons
|
877 |
+
categories: images
|
878 |
+
shortcut: mi
|
879 |
+
disabled: true
|
880 |
+
|
881 |
+
- name: gpodder
|
882 |
+
engine: json_engine
|
883 |
+
shortcut: gpod
|
884 |
+
timeout: 4.0
|
885 |
+
paging: false
|
886 |
+
search_url: https://gpodder.net/search.json?q={query}
|
887 |
+
url_query: url
|
888 |
+
title_query: title
|
889 |
+
content_query: description
|
890 |
+
page_size: 19
|
891 |
+
categories: music
|
892 |
+
disabled: true
|
893 |
+
about:
|
894 |
+
website: https://gpodder.net
|
895 |
+
wikidata_id: Q3093354
|
896 |
+
official_api_documentation: https://gpoddernet.readthedocs.io/en/latest/api/
|
897 |
+
use_official_api: false
|
898 |
+
requires_api_key: false
|
899 |
+
results: JSON
|
900 |
+
|
901 |
+
- name: habrahabr
|
902 |
+
engine: xpath
|
903 |
+
paging: true
|
904 |
+
search_url: https://habr.com/en/search/page{pageno}/?q={query}
|
905 |
+
results_xpath: //article[contains(@class, "tm-articles-list__item")]
|
906 |
+
url_xpath: .//a[@class="tm-title__link"]/@href
|
907 |
+
title_xpath: .//a[@class="tm-title__link"]
|
908 |
+
content_xpath: .//div[contains(@class, "article-formatted-body")]
|
909 |
+
categories: it
|
910 |
+
timeout: 4.0
|
911 |
+
disabled: true
|
912 |
+
shortcut: habr
|
913 |
+
about:
|
914 |
+
website: https://habr.com/
|
915 |
+
wikidata_id: Q4494434
|
916 |
+
official_api_documentation: https://habr.com/en/docs/help/api/
|
917 |
+
use_official_api: false
|
918 |
+
require_api_key: false
|
919 |
+
results: HTML
|
920 |
+
|
921 |
+
- name: hackernews
|
922 |
+
engine: hackernews
|
923 |
+
shortcut: hn
|
924 |
+
disabled: true
|
925 |
+
|
926 |
+
- name: hoogle
|
927 |
+
engine: xpath
|
928 |
+
paging: true
|
929 |
+
search_url: https://hoogle.haskell.org/?hoogle={query}&start={pageno}
|
930 |
+
results_xpath: '//div[@class="result"]'
|
931 |
+
title_xpath: './/div[@class="ans"]//a'
|
932 |
+
url_xpath: './/div[@class="ans"]//a/@href'
|
933 |
+
content_xpath: './/div[@class="from"]'
|
934 |
+
page_size: 20
|
935 |
+
categories: [it, packages]
|
936 |
+
shortcut: ho
|
937 |
+
about:
|
938 |
+
website: https://hoogle.haskell.org/
|
939 |
+
wikidata_id: Q34010
|
940 |
+
official_api_documentation: https://hackage.haskell.org/api
|
941 |
+
use_official_api: false
|
942 |
+
require_api_key: false
|
943 |
+
results: JSON
|
944 |
+
|
945 |
+
- name: imdb
|
946 |
+
engine: imdb
|
947 |
+
shortcut: imdb
|
948 |
+
timeout: 6.0
|
949 |
+
disabled: true
|
950 |
+
|
951 |
+
- name: imgur
|
952 |
+
engine: imgur
|
953 |
+
shortcut: img
|
954 |
+
disabled: true
|
955 |
+
|
956 |
+
- name: ina
|
957 |
+
engine: ina
|
958 |
+
shortcut: in
|
959 |
+
timeout: 6.0
|
960 |
+
disabled: true
|
961 |
+
|
962 |
+
- name: invidious
|
963 |
+
engine: invidious
|
964 |
+
# Instanes will be selected randomly, see https://api.invidious.io/ for
|
965 |
+
# instances that are stable (good uptime) and close to you.
|
966 |
+
base_url:
|
967 |
+
- https://invidious.io.lol
|
968 |
+
- https://invidious.fdn.fr
|
969 |
+
- https://yt.artemislena.eu
|
970 |
+
- https://invidious.tiekoetter.com
|
971 |
+
- https://invidious.flokinet.to
|
972 |
+
- https://vid.puffyan.us
|
973 |
+
- https://invidious.privacydev.net
|
974 |
+
- https://inv.tux.pizza
|
975 |
+
shortcut: iv
|
976 |
+
timeout: 3.0
|
977 |
+
disabled: true
|
978 |
+
|
979 |
+
- name: jisho
|
980 |
+
engine: jisho
|
981 |
+
shortcut: js
|
982 |
+
timeout: 3.0
|
983 |
+
disabled: true
|
984 |
+
|
985 |
+
- name: kickass
|
986 |
+
engine: kickass
|
987 |
+
base_url:
|
988 |
+
- https://kickasstorrents.to
|
989 |
+
- https://kickasstorrents.cr
|
990 |
+
- https://kickasstorrent.cr
|
991 |
+
- https://kickass.sx
|
992 |
+
- https://kat.am
|
993 |
+
shortcut: kc
|
994 |
+
timeout: 4.0
|
995 |
+
|
996 |
+
- name: lemmy communities
|
997 |
+
engine: lemmy
|
998 |
+
lemmy_type: Communities
|
999 |
+
shortcut: leco
|
1000 |
+
|
1001 |
+
- name: lemmy users
|
1002 |
+
engine: lemmy
|
1003 |
+
network: lemmy communities
|
1004 |
+
lemmy_type: Users
|
1005 |
+
shortcut: leus
|
1006 |
+
|
1007 |
+
- name: lemmy posts
|
1008 |
+
engine: lemmy
|
1009 |
+
network: lemmy communities
|
1010 |
+
lemmy_type: Posts
|
1011 |
+
shortcut: lepo
|
1012 |
+
|
1013 |
+
- name: lemmy comments
|
1014 |
+
engine: lemmy
|
1015 |
+
network: lemmy communities
|
1016 |
+
lemmy_type: Comments
|
1017 |
+
shortcut: lecom
|
1018 |
+
|
1019 |
+
- name: library genesis
|
1020 |
+
engine: xpath
|
1021 |
+
# search_url: https://libgen.is/search.php?req={query}
|
1022 |
+
search_url: https://libgen.rs/search.php?req={query}
|
1023 |
+
url_xpath: //a[contains(@href,"book/index.php?md5")]/@href
|
1024 |
+
title_xpath: //a[contains(@href,"book/")]/text()[1]
|
1025 |
+
content_xpath: //td/a[1][contains(@href,"=author")]/text()
|
1026 |
+
categories: files
|
1027 |
+
timeout: 7.0
|
1028 |
+
disabled: true
|
1029 |
+
shortcut: lg
|
1030 |
+
about:
|
1031 |
+
website: https://libgen.fun/
|
1032 |
+
wikidata_id: Q22017206
|
1033 |
+
official_api_documentation:
|
1034 |
+
use_official_api: false
|
1035 |
+
require_api_key: false
|
1036 |
+
results: HTML
|
1037 |
+
|
1038 |
+
- name: z-library
|
1039 |
+
engine: zlibrary
|
1040 |
+
shortcut: zlib
|
1041 |
+
categories: files
|
1042 |
+
timeout: 7.0
|
1043 |
+
|
1044 |
+
- name: library of congress
|
1045 |
+
engine: loc
|
1046 |
+
shortcut: loc
|
1047 |
+
categories: images
|
1048 |
+
|
1049 |
+
- name: lingva
|
1050 |
+
engine: lingva
|
1051 |
+
shortcut: lv
|
1052 |
+
# set lingva instance in url, by default it will use the official instance
|
1053 |
+
# url: https://lingva.thedaviddelta.com
|
1054 |
+
|
1055 |
+
- name: lobste.rs
|
1056 |
+
engine: xpath
|
1057 |
+
search_url: https://lobste.rs/search?utf8=%E2%9C%93&q={query}&what=stories&order=relevance
|
1058 |
+
results_xpath: //li[contains(@class, "story")]
|
1059 |
+
url_xpath: .//a[@class="u-url"]/@href
|
1060 |
+
title_xpath: .//a[@class="u-url"]
|
1061 |
+
content_xpath: .//a[@class="domain"]
|
1062 |
+
categories: it
|
1063 |
+
shortcut: lo
|
1064 |
+
timeout: 5.0
|
1065 |
+
disabled: true
|
1066 |
+
about:
|
1067 |
+
website: https://lobste.rs/
|
1068 |
+
wikidata_id: Q60762874
|
1069 |
+
official_api_documentation:
|
1070 |
+
use_official_api: false
|
1071 |
+
require_api_key: false
|
1072 |
+
results: HTML
|
1073 |
+
|
1074 |
+
- name: mastodon users
|
1075 |
+
engine: mastodon
|
1076 |
+
mastodon_type: accounts
|
1077 |
+
base_url: https://mastodon.social
|
1078 |
+
shortcut: mau
|
1079 |
+
|
1080 |
+
- name: mastodon hashtags
|
1081 |
+
engine: mastodon
|
1082 |
+
mastodon_type: hashtags
|
1083 |
+
base_url: https://mastodon.social
|
1084 |
+
shortcut: mah
|
1085 |
+
|
1086 |
+
# - name: matrixrooms
|
1087 |
+
# engine: mrs
|
1088 |
+
# # https://docs.searxng.org/dev/engines/online/mrs.html
|
1089 |
+
# # base_url: https://mrs-api-host
|
1090 |
+
# shortcut: mtrx
|
1091 |
+
# disabled: true
|
1092 |
+
|
1093 |
+
- name: mdn
|
1094 |
+
shortcut: mdn
|
1095 |
+
engine: json_engine
|
1096 |
+
categories: [it]
|
1097 |
+
paging: true
|
1098 |
+
search_url: https://developer.mozilla.org/api/v1/search?q={query}&page={pageno}
|
1099 |
+
results_query: documents
|
1100 |
+
url_query: mdn_url
|
1101 |
+
url_prefix: https://developer.mozilla.org
|
1102 |
+
title_query: title
|
1103 |
+
content_query: summary
|
1104 |
+
about:
|
1105 |
+
website: https://developer.mozilla.org
|
1106 |
+
wikidata_id: Q3273508
|
1107 |
+
official_api_documentation: null
|
1108 |
+
use_official_api: false
|
1109 |
+
require_api_key: false
|
1110 |
+
results: JSON
|
1111 |
+
|
1112 |
+
- name: metacpan
|
1113 |
+
engine: metacpan
|
1114 |
+
shortcut: cpan
|
1115 |
+
disabled: true
|
1116 |
+
number_of_results: 20
|
1117 |
+
|
1118 |
+
# - name: meilisearch
|
1119 |
+
# engine: meilisearch
|
1120 |
+
# shortcut: mes
|
1121 |
+
# enable_http: true
|
1122 |
+
# base_url: http://localhost:7700
|
1123 |
+
# index: my-index
|
1124 |
+
|
1125 |
+
- name: mixcloud
|
1126 |
+
engine: mixcloud
|
1127 |
+
shortcut: mc
|
1128 |
+
|
1129 |
+
# MongoDB engine
|
1130 |
+
# Required dependency: pymongo
|
1131 |
+
# - name: mymongo
|
1132 |
+
# engine: mongodb
|
1133 |
+
# shortcut: md
|
1134 |
+
# exact_match_only: false
|
1135 |
+
# host: '127.0.0.1'
|
1136 |
+
# port: 27017
|
1137 |
+
# enable_http: true
|
1138 |
+
# results_per_page: 20
|
1139 |
+
# database: 'business'
|
1140 |
+
# collection: 'reviews' # name of the db collection
|
1141 |
+
# key: 'name' # key in the collection to search for
|
1142 |
+
|
1143 |
+
- name: mozhi
|
1144 |
+
engine: mozhi
|
1145 |
+
base_url:
|
1146 |
+
- https://mozhi.aryak.me
|
1147 |
+
- https://translate.bus-hit.me
|
1148 |
+
- https://nyc1.mz.ggtyler.dev
|
1149 |
+
# mozhi_engine: google - see https://mozhi.aryak.me for supported engines
|
1150 |
+
timeout: 4.0
|
1151 |
+
shortcut: mz
|
1152 |
+
disabled: true
|
1153 |
+
|
1154 |
+
- name: mwmbl
|
1155 |
+
engine: mwmbl
|
1156 |
+
# api_url: https://api.mwmbl.org
|
1157 |
+
shortcut: mwm
|
1158 |
+
disabled: true
|
1159 |
+
|
1160 |
+
- name: npm
|
1161 |
+
engine: json_engine
|
1162 |
+
paging: true
|
1163 |
+
first_page_num: 0
|
1164 |
+
search_url: https://api.npms.io/v2/search?q={query}&size=25&from={pageno}
|
1165 |
+
results_query: results
|
1166 |
+
url_query: package/links/npm
|
1167 |
+
title_query: package/name
|
1168 |
+
content_query: package/description
|
1169 |
+
page_size: 25
|
1170 |
+
categories: [it, packages]
|
1171 |
+
disabled: true
|
1172 |
+
timeout: 5.0
|
1173 |
+
shortcut: npm
|
1174 |
+
about:
|
1175 |
+
website: https://npms.io/
|
1176 |
+
wikidata_id: Q7067518
|
1177 |
+
official_api_documentation: https://api-docs.npms.io/
|
1178 |
+
use_official_api: false
|
1179 |
+
require_api_key: false
|
1180 |
+
results: JSON
|
1181 |
+
|
1182 |
+
- name: nyaa
|
1183 |
+
engine: nyaa
|
1184 |
+
shortcut: nt
|
1185 |
+
disabled: true
|
1186 |
+
|
1187 |
+
- name: mankier
|
1188 |
+
engine: json_engine
|
1189 |
+
search_url: https://www.mankier.com/api/v2/mans/?q={query}
|
1190 |
+
results_query: results
|
1191 |
+
url_query: url
|
1192 |
+
title_query: name
|
1193 |
+
content_query: description
|
1194 |
+
categories: it
|
1195 |
+
shortcut: man
|
1196 |
+
about:
|
1197 |
+
website: https://www.mankier.com/
|
1198 |
+
official_api_documentation: https://www.mankier.com/api
|
1199 |
+
use_official_api: true
|
1200 |
+
require_api_key: false
|
1201 |
+
results: JSON
|
1202 |
+
|
1203 |
+
- name: odysee
|
1204 |
+
engine: odysee
|
1205 |
+
shortcut: od
|
1206 |
+
disabled: true
|
1207 |
+
|
1208 |
+
- name: openairedatasets
|
1209 |
+
engine: json_engine
|
1210 |
+
paging: true
|
1211 |
+
search_url: https://api.openaire.eu/search/datasets?format=json&page={pageno}&size=10&title={query}
|
1212 |
+
results_query: response/results/result
|
1213 |
+
url_query: metadata/oaf:entity/oaf:result/children/instance/webresource/url/$
|
1214 |
+
title_query: metadata/oaf:entity/oaf:result/title/$
|
1215 |
+
content_query: metadata/oaf:entity/oaf:result/description/$
|
1216 |
+
content_html_to_text: true
|
1217 |
+
categories: 'science'
|
1218 |
+
shortcut: oad
|
1219 |
+
timeout: 5.0
|
1220 |
+
about:
|
1221 |
+
website: https://www.openaire.eu/
|
1222 |
+
wikidata_id: Q25106053
|
1223 |
+
official_api_documentation: https://api.openaire.eu/
|
1224 |
+
use_official_api: false
|
1225 |
+
require_api_key: false
|
1226 |
+
results: JSON
|
1227 |
+
|
1228 |
+
- name: openairepublications
|
1229 |
+
engine: json_engine
|
1230 |
+
paging: true
|
1231 |
+
search_url: https://api.openaire.eu/search/publications?format=json&page={pageno}&size=10&title={query}
|
1232 |
+
results_query: response/results/result
|
1233 |
+
url_query: metadata/oaf:entity/oaf:result/children/instance/webresource/url/$
|
1234 |
+
title_query: metadata/oaf:entity/oaf:result/title/$
|
1235 |
+
content_query: metadata/oaf:entity/oaf:result/description/$
|
1236 |
+
content_html_to_text: true
|
1237 |
+
categories: science
|
1238 |
+
shortcut: oap
|
1239 |
+
timeout: 5.0
|
1240 |
+
about:
|
1241 |
+
website: https://www.openaire.eu/
|
1242 |
+
wikidata_id: Q25106053
|
1243 |
+
official_api_documentation: https://api.openaire.eu/
|
1244 |
+
use_official_api: false
|
1245 |
+
require_api_key: false
|
1246 |
+
results: JSON
|
1247 |
+
|
1248 |
+
# - name: opensemanticsearch
|
1249 |
+
# engine: opensemantic
|
1250 |
+
# shortcut: oss
|
1251 |
+
# base_url: 'http://localhost:8983/solr/opensemanticsearch/'
|
1252 |
+
|
1253 |
+
- name: openstreetmap
|
1254 |
+
engine: openstreetmap
|
1255 |
+
shortcut: osm
|
1256 |
+
|
1257 |
+
- name: openrepos
|
1258 |
+
engine: xpath
|
1259 |
+
paging: true
|
1260 |
+
search_url: https://openrepos.net/search/node/{query}?page={pageno}
|
1261 |
+
url_xpath: //li[@class="search-result"]//h3[@class="title"]/a/@href
|
1262 |
+
title_xpath: //li[@class="search-result"]//h3[@class="title"]/a
|
1263 |
+
content_xpath: //li[@class="search-result"]//div[@class="search-snippet-info"]//p[@class="search-snippet"]
|
1264 |
+
categories: files
|
1265 |
+
timeout: 4.0
|
1266 |
+
disabled: true
|
1267 |
+
shortcut: or
|
1268 |
+
about:
|
1269 |
+
website: https://openrepos.net/
|
1270 |
+
wikidata_id:
|
1271 |
+
official_api_documentation:
|
1272 |
+
use_official_api: false
|
1273 |
+
require_api_key: false
|
1274 |
+
results: HTML
|
1275 |
+
|
1276 |
+
- name: packagist
|
1277 |
+
engine: json_engine
|
1278 |
+
paging: true
|
1279 |
+
search_url: https://packagist.org/search.json?q={query}&page={pageno}
|
1280 |
+
results_query: results
|
1281 |
+
url_query: url
|
1282 |
+
title_query: name
|
1283 |
+
content_query: description
|
1284 |
+
categories: [it, packages]
|
1285 |
+
disabled: true
|
1286 |
+
timeout: 5.0
|
1287 |
+
shortcut: pack
|
1288 |
+
about:
|
1289 |
+
website: https://packagist.org
|
1290 |
+
wikidata_id: Q108311377
|
1291 |
+
official_api_documentation: https://packagist.org/apidoc
|
1292 |
+
use_official_api: true
|
1293 |
+
require_api_key: false
|
1294 |
+
results: JSON
|
1295 |
+
|
1296 |
+
- name: pdbe
|
1297 |
+
engine: pdbe
|
1298 |
+
shortcut: pdb
|
1299 |
+
# Hide obsolete PDB entries. Default is not to hide obsolete structures
|
1300 |
+
# hide_obsolete: false
|
1301 |
+
|
1302 |
+
- name: photon
|
1303 |
+
engine: photon
|
1304 |
+
shortcut: ph
|
1305 |
+
|
1306 |
+
- name: pinterest
|
1307 |
+
engine: pinterest
|
1308 |
+
shortcut: pin
|
1309 |
+
|
1310 |
+
- name: piped
|
1311 |
+
engine: piped
|
1312 |
+
shortcut: ppd
|
1313 |
+
categories: videos
|
1314 |
+
piped_filter: videos
|
1315 |
+
timeout: 3.0
|
1316 |
+
|
1317 |
+
# URL to use as link and for embeds
|
1318 |
+
frontend_url: https://srv.piped.video
|
1319 |
+
# Instance will be selected randomly, for more see https://piped-instances.kavin.rocks/
|
1320 |
+
backend_url:
|
1321 |
+
- https://pipedapi.kavin.rocks
|
1322 |
+
- https://pipedapi-libre.kavin.rocks
|
1323 |
+
- https://pipedapi.adminforge.de
|
1324 |
+
|
1325 |
+
- name: piped.music
|
1326 |
+
engine: piped
|
1327 |
+
network: piped
|
1328 |
+
shortcut: ppdm
|
1329 |
+
categories: music
|
1330 |
+
piped_filter: music_songs
|
1331 |
+
timeout: 3.0
|
1332 |
+
|
1333 |
+
- name: piratebay
|
1334 |
+
engine: piratebay
|
1335 |
+
shortcut: tpb
|
1336 |
+
# You may need to change this URL to a proxy if piratebay is blocked in your
|
1337 |
+
# country
|
1338 |
+
url: https://thepiratebay.org/
|
1339 |
+
timeout: 3.0
|
1340 |
+
|
1341 |
+
- name: podcastindex
|
1342 |
+
engine: podcastindex
|
1343 |
+
shortcut: podcast
|
1344 |
+
|
1345 |
+
# Required dependency: psychopg2
|
1346 |
+
# - name: postgresql
|
1347 |
+
# engine: postgresql
|
1348 |
+
# database: postgres
|
1349 |
+
# username: postgres
|
1350 |
+
# password: postgres
|
1351 |
+
# limit: 10
|
1352 |
+
# query_str: 'SELECT * from my_table WHERE my_column = %(query)s'
|
1353 |
+
# shortcut : psql
|
1354 |
+
|
1355 |
+
- name: presearch
|
1356 |
+
engine: presearch
|
1357 |
+
search_type: search
|
1358 |
+
categories: [general, web]
|
1359 |
+
shortcut: ps
|
1360 |
+
timeout: 4.0
|
1361 |
+
disabled: true
|
1362 |
+
|
1363 |
+
- name: presearch images
|
1364 |
+
engine: presearch
|
1365 |
+
network: presearch
|
1366 |
+
search_type: images
|
1367 |
+
categories: [images, web]
|
1368 |
+
timeout: 4.0
|
1369 |
+
shortcut: psimg
|
1370 |
+
disabled: true
|
1371 |
+
|
1372 |
+
- name: presearch videos
|
1373 |
+
engine: presearch
|
1374 |
+
network: presearch
|
1375 |
+
search_type: videos
|
1376 |
+
categories: [general, web]
|
1377 |
+
timeout: 4.0
|
1378 |
+
shortcut: psvid
|
1379 |
+
disabled: true
|
1380 |
+
|
1381 |
+
- name: presearch news
|
1382 |
+
engine: presearch
|
1383 |
+
network: presearch
|
1384 |
+
search_type: news
|
1385 |
+
categories: [news, web]
|
1386 |
+
timeout: 4.0
|
1387 |
+
shortcut: psnews
|
1388 |
+
disabled: true
|
1389 |
+
|
1390 |
+
- name: pub.dev
|
1391 |
+
engine: xpath
|
1392 |
+
shortcut: pd
|
1393 |
+
search_url: https://pub.dev/packages?q={query}&page={pageno}
|
1394 |
+
paging: true
|
1395 |
+
results_xpath: //div[contains(@class,"packages-item")]
|
1396 |
+
url_xpath: ./div/h3/a/@href
|
1397 |
+
title_xpath: ./div/h3/a
|
1398 |
+
content_xpath: ./div/div/div[contains(@class,"packages-description")]/span
|
1399 |
+
categories: [packages, it]
|
1400 |
+
timeout: 3.0
|
1401 |
+
disabled: true
|
1402 |
+
first_page_num: 1
|
1403 |
+
about:
|
1404 |
+
website: https://pub.dev/
|
1405 |
+
official_api_documentation: https://pub.dev/help/api
|
1406 |
+
use_official_api: false
|
1407 |
+
require_api_key: false
|
1408 |
+
results: HTML
|
1409 |
+
|
1410 |
+
- name: pubmed
|
1411 |
+
engine: pubmed
|
1412 |
+
shortcut: pub
|
1413 |
+
timeout: 3.0
|
1414 |
+
|
1415 |
+
- name: pypi
|
1416 |
+
shortcut: pypi
|
1417 |
+
engine: xpath
|
1418 |
+
paging: true
|
1419 |
+
search_url: https://pypi.org/search/?q={query}&page={pageno}
|
1420 |
+
results_xpath: /html/body/main/div/div/div/form/div/ul/li/a[@class="package-snippet"]
|
1421 |
+
url_xpath: ./@href
|
1422 |
+
title_xpath: ./h3/span[@class="package-snippet__name"]
|
1423 |
+
content_xpath: ./p
|
1424 |
+
suggestion_xpath: /html/body/main/div/div/div/form/div/div[@class="callout-block"]/p/span/a[@class="link"]
|
1425 |
+
first_page_num: 1
|
1426 |
+
categories: [it, packages]
|
1427 |
+
about:
|
1428 |
+
website: https://pypi.org
|
1429 |
+
wikidata_id: Q2984686
|
1430 |
+
official_api_documentation: https://warehouse.readthedocs.io/api-reference/index.html
|
1431 |
+
use_official_api: false
|
1432 |
+
require_api_key: false
|
1433 |
+
results: HTML
|
1434 |
+
|
1435 |
+
- name: qwant
|
1436 |
+
qwant_categ: web
|
1437 |
+
engine: qwant
|
1438 |
+
shortcut: qw
|
1439 |
+
categories: [general, web]
|
1440 |
+
additional_tests:
|
1441 |
+
rosebud: *test_rosebud
|
1442 |
+
|
1443 |
+
- name: qwant news
|
1444 |
+
qwant_categ: news
|
1445 |
+
engine: qwant
|
1446 |
+
shortcut: qwn
|
1447 |
+
categories: news
|
1448 |
+
network: qwant
|
1449 |
+
|
1450 |
+
- name: qwant images
|
1451 |
+
qwant_categ: images
|
1452 |
+
engine: qwant
|
1453 |
+
shortcut: qwi
|
1454 |
+
categories: [images, web]
|
1455 |
+
network: qwant
|
1456 |
+
|
1457 |
+
- name: qwant videos
|
1458 |
+
qwant_categ: videos
|
1459 |
+
engine: qwant
|
1460 |
+
shortcut: qwv
|
1461 |
+
categories: [videos, web]
|
1462 |
+
network: qwant
|
1463 |
+
|
1464 |
+
# - name: library
|
1465 |
+
# engine: recoll
|
1466 |
+
# shortcut: lib
|
1467 |
+
# base_url: 'https://recoll.example.org/'
|
1468 |
+
# search_dir: ''
|
1469 |
+
# mount_prefix: /export
|
1470 |
+
# dl_prefix: 'https://download.example.org'
|
1471 |
+
# timeout: 30.0
|
1472 |
+
# categories: files
|
1473 |
+
# disabled: true
|
1474 |
+
|
1475 |
+
# - name: recoll library reference
|
1476 |
+
# engine: recoll
|
1477 |
+
# base_url: 'https://recoll.example.org/'
|
1478 |
+
# search_dir: reference
|
1479 |
+
# mount_prefix: /export
|
1480 |
+
# dl_prefix: 'https://download.example.org'
|
1481 |
+
# shortcut: libr
|
1482 |
+
# timeout: 30.0
|
1483 |
+
# categories: files
|
1484 |
+
# disabled: true
|
1485 |
+
|
1486 |
+
- name: radio browser
|
1487 |
+
engine: radio_browser
|
1488 |
+
shortcut: rb
|
1489 |
+
|
1490 |
+
- name: reddit
|
1491 |
+
engine: reddit
|
1492 |
+
shortcut: re
|
1493 |
+
page_size: 25
|
1494 |
+
|
1495 |
+
- name: rottentomatoes
|
1496 |
+
engine: rottentomatoes
|
1497 |
+
shortcut: rt
|
1498 |
+
disabled: true
|
1499 |
+
|
1500 |
+
# Required dependency: redis
|
1501 |
+
# - name: myredis
|
1502 |
+
# shortcut : rds
|
1503 |
+
# engine: redis_server
|
1504 |
+
# exact_match_only: false
|
1505 |
+
# host: '127.0.0.1'
|
1506 |
+
# port: 6379
|
1507 |
+
# enable_http: true
|
1508 |
+
# password: ''
|
1509 |
+
# db: 0
|
1510 |
+
|
1511 |
+
# tmp suspended: bad certificate
|
1512 |
+
# - name: scanr structures
|
1513 |
+
# shortcut: scs
|
1514 |
+
# engine: scanr_structures
|
1515 |
+
# disabled: true
|
1516 |
+
|
1517 |
+
- name: sepiasearch
|
1518 |
+
engine: sepiasearch
|
1519 |
+
shortcut: sep
|
1520 |
+
|
1521 |
+
- name: soundcloud
|
1522 |
+
engine: soundcloud
|
1523 |
+
shortcut: sc
|
1524 |
+
|
1525 |
+
- name: stackoverflow
|
1526 |
+
engine: stackexchange
|
1527 |
+
shortcut: st
|
1528 |
+
api_site: 'stackoverflow'
|
1529 |
+
categories: [it, q&a]
|
1530 |
+
|
1531 |
+
- name: askubuntu
|
1532 |
+
engine: stackexchange
|
1533 |
+
shortcut: ubuntu
|
1534 |
+
api_site: 'askubuntu'
|
1535 |
+
categories: [it, q&a]
|
1536 |
+
|
1537 |
+
- name: internetarchivescholar
|
1538 |
+
engine: internet_archive_scholar
|
1539 |
+
shortcut: ias
|
1540 |
+
timeout: 5.0
|
1541 |
+
|
1542 |
+
- name: superuser
|
1543 |
+
engine: stackexchange
|
1544 |
+
shortcut: su
|
1545 |
+
api_site: 'superuser'
|
1546 |
+
categories: [it, q&a]
|
1547 |
+
|
1548 |
+
- name: searchcode code
|
1549 |
+
engine: searchcode_code
|
1550 |
+
shortcut: scc
|
1551 |
+
disabled: true
|
1552 |
+
|
1553 |
+
# - name: searx
|
1554 |
+
# engine: searx_engine
|
1555 |
+
# shortcut: se
|
1556 |
+
# instance_urls :
|
1557 |
+
# - http://127.0.0.1:8888/
|
1558 |
+
# - ...
|
1559 |
+
# disabled: true
|
1560 |
+
|
1561 |
+
- name: semantic scholar
|
1562 |
+
engine: semantic_scholar
|
1563 |
+
disabled: true
|
1564 |
+
shortcut: se
|
1565 |
+
|
1566 |
+
# Spotify needs API credentials
|
1567 |
+
# - name: spotify
|
1568 |
+
# engine: spotify
|
1569 |
+
# shortcut: stf
|
1570 |
+
# api_client_id: *******
|
1571 |
+
# api_client_secret: *******
|
1572 |
+
|
1573 |
+
# - name: solr
|
1574 |
+
# engine: solr
|
1575 |
+
# shortcut: slr
|
1576 |
+
# base_url: http://localhost:8983
|
1577 |
+
# collection: collection_name
|
1578 |
+
# sort: '' # sorting: asc or desc
|
1579 |
+
# field_list: '' # comma separated list of field names to display on the UI
|
1580 |
+
# default_fields: '' # default field to query
|
1581 |
+
# query_fields: '' # query fields
|
1582 |
+
# enable_http: true
|
1583 |
+
|
1584 |
+
# - name: springer nature
|
1585 |
+
# engine: springer
|
1586 |
+
# # get your API key from: https://dev.springernature.com/signup
|
1587 |
+
# # working API key, for test & debug: "a69685087d07eca9f13db62f65b8f601"
|
1588 |
+
# api_key: 'unset'
|
1589 |
+
# shortcut: springer
|
1590 |
+
# timeout: 15.0
|
1591 |
+
|
1592 |
+
- name: startpage
|
1593 |
+
engine: startpage
|
1594 |
+
shortcut: sp
|
1595 |
+
timeout: 6.0
|
1596 |
+
disabled: true
|
1597 |
+
additional_tests:
|
1598 |
+
rosebud: *test_rosebud
|
1599 |
+
|
1600 |
+
- name: tokyotoshokan
|
1601 |
+
engine: tokyotoshokan
|
1602 |
+
shortcut: tt
|
1603 |
+
timeout: 6.0
|
1604 |
+
disabled: true
|
1605 |
+
|
1606 |
+
- name: solidtorrents
|
1607 |
+
engine: solidtorrents
|
1608 |
+
shortcut: solid
|
1609 |
+
timeout: 4.0
|
1610 |
+
base_url:
|
1611 |
+
- https://solidtorrents.to
|
1612 |
+
- https://bitsearch.to
|
1613 |
+
|
1614 |
+
# For this demo of the sqlite engine download:
|
1615 |
+
# https://liste.mediathekview.de/filmliste-v2.db.bz2
|
1616 |
+
# and unpack into searx/data/filmliste-v2.db
|
1617 |
+
# Query to test: "!demo concert"
|
1618 |
+
#
|
1619 |
+
# - name: demo
|
1620 |
+
# engine: sqlite
|
1621 |
+
# shortcut: demo
|
1622 |
+
# categories: general
|
1623 |
+
# result_template: default.html
|
1624 |
+
# database: searx/data/filmliste-v2.db
|
1625 |
+
# query_str: >-
|
1626 |
+
# SELECT title || ' (' || time(duration, 'unixepoch') || ')' AS title,
|
1627 |
+
# COALESCE( NULLIF(url_video_hd,''), NULLIF(url_video_sd,''), url_video) AS url,
|
1628 |
+
# description AS content
|
1629 |
+
# FROM film
|
1630 |
+
# WHERE title LIKE :wildcard OR description LIKE :wildcard
|
1631 |
+
# ORDER BY duration DESC
|
1632 |
+
|
1633 |
+
- name: tagesschau
|
1634 |
+
engine: tagesschau
|
1635 |
+
# when set to false, display URLs from Tagesschau, and not the actual source
|
1636 |
+
# (e.g. NDR, WDR, SWR, HR, ...)
|
1637 |
+
use_source_url: true
|
1638 |
+
shortcut: ts
|
1639 |
+
disabled: true
|
1640 |
+
|
1641 |
+
- name: tmdb
|
1642 |
+
engine: xpath
|
1643 |
+
paging: true
|
1644 |
+
categories: movies
|
1645 |
+
search_url: https://www.themoviedb.org/search?page={pageno}&query={query}
|
1646 |
+
results_xpath: //div[contains(@class,"movie") or contains(@class,"tv")]//div[contains(@class,"card")]
|
1647 |
+
url_xpath: .//div[contains(@class,"poster")]/a/@href
|
1648 |
+
thumbnail_xpath: .//img/@src
|
1649 |
+
title_xpath: .//div[contains(@class,"title")]//h2
|
1650 |
+
content_xpath: .//div[contains(@class,"overview")]
|
1651 |
+
shortcut: tm
|
1652 |
+
disabled: true
|
1653 |
+
|
1654 |
+
# Requires Tor
|
1655 |
+
- name: torch
|
1656 |
+
engine: xpath
|
1657 |
+
paging: true
|
1658 |
+
search_url: http://xmh57jrknzkhv6y3ls3ubitzfqnkrwxhopf5aygthi7d6rplyvk3noyd.onion/cgi-bin/omega/omega?P={query}&DEFAULTOP=and
|
1659 |
+
results_xpath: //table//tr
|
1660 |
+
url_xpath: ./td[2]/a
|
1661 |
+
title_xpath: ./td[2]/b
|
1662 |
+
content_xpath: ./td[2]/small
|
1663 |
+
categories: onions
|
1664 |
+
enable_http: true
|
1665 |
+
shortcut: tch
|
1666 |
+
|
1667 |
+
# torznab engine lets you query any torznab compatible indexer. Using this
|
1668 |
+
# engine in combination with Jackett opens the possibility to query a lot of
|
1669 |
+
# public and private indexers directly from SearXNG. More details at:
|
1670 |
+
# https://docs.searxng.org/dev/engines/online/torznab.html
|
1671 |
+
#
|
1672 |
+
# - name: Torznab EZTV
|
1673 |
+
# engine: torznab
|
1674 |
+
# shortcut: eztv
|
1675 |
+
# base_url: http://localhost:9117/api/v2.0/indexers/eztv/results/torznab
|
1676 |
+
# enable_http: true # if using localhost
|
1677 |
+
# api_key: xxxxxxxxxxxxxxx
|
1678 |
+
# show_magnet_links: true
|
1679 |
+
# show_torrent_files: false
|
1680 |
+
# # https://github.com/Jackett/Jackett/wiki/Jackett-Categories
|
1681 |
+
# torznab_categories: # optional
|
1682 |
+
# - 2000
|
1683 |
+
# - 5000
|
1684 |
+
|
1685 |
+
# tmp suspended - too slow, too many errors
|
1686 |
+
# - name: urbandictionary
|
1687 |
+
# engine : xpath
|
1688 |
+
# search_url : https://www.urbandictionary.com/define.php?term={query}
|
1689 |
+
# url_xpath : //*[@class="word"]/@href
|
1690 |
+
# title_xpath : //*[@class="def-header"]
|
1691 |
+
# content_xpath: //*[@class="meaning"]
|
1692 |
+
# shortcut: ud
|
1693 |
+
|
1694 |
+
- name: unsplash
|
1695 |
+
engine: unsplash
|
1696 |
+
shortcut: us
|
1697 |
+
|
1698 |
+
- name: yandex music
|
1699 |
+
engine: yandex_music
|
1700 |
+
shortcut: ydm
|
1701 |
+
disabled: true
|
1702 |
+
# https://yandex.com/support/music/access.html
|
1703 |
+
inactive: true
|
1704 |
+
|
1705 |
+
- name: yahoo
|
1706 |
+
engine: yahoo
|
1707 |
+
shortcut: yh
|
1708 |
+
disabled: true
|
1709 |
+
|
1710 |
+
- name: yahoo news
|
1711 |
+
engine: yahoo_news
|
1712 |
+
shortcut: yhn
|
1713 |
+
|
1714 |
+
- name: youtube
|
1715 |
+
shortcut: yt
|
1716 |
+
# You can use the engine using the official stable API, but you need an API
|
1717 |
+
# key See: https://console.developers.google.com/project
|
1718 |
+
#
|
1719 |
+
# engine: youtube_api
|
1720 |
+
# api_key: 'apikey' # required!
|
1721 |
+
#
|
1722 |
+
# Or you can use the html non-stable engine, activated by default
|
1723 |
+
engine: youtube_noapi
|
1724 |
+
|
1725 |
+
- name: dailymotion
|
1726 |
+
engine: dailymotion
|
1727 |
+
shortcut: dm
|
1728 |
+
|
1729 |
+
- name: vimeo
|
1730 |
+
engine: vimeo
|
1731 |
+
shortcut: vm
|
1732 |
+
|
1733 |
+
- name: wiby
|
1734 |
+
engine: json_engine
|
1735 |
+
paging: true
|
1736 |
+
search_url: https://wiby.me/json/?q={query}&p={pageno}
|
1737 |
+
url_query: URL
|
1738 |
+
title_query: Title
|
1739 |
+
content_query: Snippet
|
1740 |
+
categories: [general, web]
|
1741 |
+
shortcut: wib
|
1742 |
+
disabled: true
|
1743 |
+
about:
|
1744 |
+
website: https://wiby.me/
|
1745 |
+
|
1746 |
+
- name: alexandria
|
1747 |
+
engine: json_engine
|
1748 |
+
shortcut: alx
|
1749 |
+
categories: general
|
1750 |
+
paging: true
|
1751 |
+
search_url: https://api.alexandria.org/?a=1&q={query}&p={pageno}
|
1752 |
+
results_query: results
|
1753 |
+
title_query: title
|
1754 |
+
url_query: url
|
1755 |
+
content_query: snippet
|
1756 |
+
timeout: 1.5
|
1757 |
+
disabled: true
|
1758 |
+
about:
|
1759 |
+
website: https://alexandria.org/
|
1760 |
+
official_api_documentation: https://github.com/alexandria-org/alexandria-api/raw/master/README.md
|
1761 |
+
use_official_api: true
|
1762 |
+
require_api_key: false
|
1763 |
+
results: JSON
|
1764 |
+
|
1765 |
+
- name: wikibooks
|
1766 |
+
engine: mediawiki
|
1767 |
+
weight: 0.5
|
1768 |
+
shortcut: wb
|
1769 |
+
categories: [general, wikimedia]
|
1770 |
+
base_url: 'https://{language}.wikibooks.org/'
|
1771 |
+
search_type: text
|
1772 |
+
disabled: true
|
1773 |
+
about:
|
1774 |
+
website: https://www.wikibooks.org/
|
1775 |
+
wikidata_id: Q367
|
1776 |
+
|
1777 |
+
- name: wikinews
|
1778 |
+
engine: mediawiki
|
1779 |
+
shortcut: wn
|
1780 |
+
categories: [news, wikimedia]
|
1781 |
+
base_url: 'https://{language}.wikinews.org/'
|
1782 |
+
search_type: text
|
1783 |
+
srsort: create_timestamp_desc
|
1784 |
+
about:
|
1785 |
+
website: https://www.wikinews.org/
|
1786 |
+
wikidata_id: Q964
|
1787 |
+
|
1788 |
+
- name: wikiquote
|
1789 |
+
engine: mediawiki
|
1790 |
+
weight: 0.5
|
1791 |
+
shortcut: wq
|
1792 |
+
categories: [general, wikimedia]
|
1793 |
+
base_url: 'https://{language}.wikiquote.org/'
|
1794 |
+
search_type: text
|
1795 |
+
disabled: true
|
1796 |
+
additional_tests:
|
1797 |
+
rosebud: *test_rosebud
|
1798 |
+
about:
|
1799 |
+
website: https://www.wikiquote.org/
|
1800 |
+
wikidata_id: Q369
|
1801 |
+
|
1802 |
+
- name: wikisource
|
1803 |
+
engine: mediawiki
|
1804 |
+
weight: 0.5
|
1805 |
+
shortcut: ws
|
1806 |
+
categories: [general, wikimedia]
|
1807 |
+
base_url: 'https://{language}.wikisource.org/'
|
1808 |
+
search_type: text
|
1809 |
+
disabled: true
|
1810 |
+
about:
|
1811 |
+
website: https://www.wikisource.org/
|
1812 |
+
wikidata_id: Q263
|
1813 |
+
|
1814 |
+
- name: wikispecies
|
1815 |
+
engine: mediawiki
|
1816 |
+
shortcut: wsp
|
1817 |
+
categories: [general, science, wikimedia]
|
1818 |
+
base_url: 'https://species.wikimedia.org/'
|
1819 |
+
search_type: text
|
1820 |
+
disabled: true
|
1821 |
+
about:
|
1822 |
+
website: https://species.wikimedia.org/
|
1823 |
+
wikidata_id: Q13679
|
1824 |
+
|
1825 |
+
- name: wiktionary
|
1826 |
+
engine: mediawiki
|
1827 |
+
shortcut: wt
|
1828 |
+
categories: [dictionaries, wikimedia]
|
1829 |
+
base_url: 'https://{language}.wiktionary.org/'
|
1830 |
+
search_type: text
|
1831 |
+
about:
|
1832 |
+
website: https://www.wiktionary.org/
|
1833 |
+
wikidata_id: Q151
|
1834 |
+
|
1835 |
+
- name: wikiversity
|
1836 |
+
engine: mediawiki
|
1837 |
+
weight: 0.5
|
1838 |
+
shortcut: wv
|
1839 |
+
categories: [general, wikimedia]
|
1840 |
+
base_url: 'https://{language}.wikiversity.org/'
|
1841 |
+
search_type: text
|
1842 |
+
disabled: true
|
1843 |
+
about:
|
1844 |
+
website: https://www.wikiversity.org/
|
1845 |
+
wikidata_id: Q370
|
1846 |
+
|
1847 |
+
- name: wikivoyage
|
1848 |
+
engine: mediawiki
|
1849 |
+
weight: 0.5
|
1850 |
+
shortcut: wy
|
1851 |
+
categories: [general, wikimedia]
|
1852 |
+
base_url: 'https://{language}.wikivoyage.org/'
|
1853 |
+
search_type: text
|
1854 |
+
disabled: true
|
1855 |
+
about:
|
1856 |
+
website: https://www.wikivoyage.org/
|
1857 |
+
wikidata_id: Q373
|
1858 |
+
|
1859 |
+
- name: wikicommons.images
|
1860 |
+
engine: wikicommons
|
1861 |
+
shortcut: wc
|
1862 |
+
categories: images
|
1863 |
+
number_of_results: 10
|
1864 |
+
|
1865 |
+
- name: wolframalpha
|
1866 |
+
shortcut: wa
|
1867 |
+
# You can use the engine using the official stable API, but you need an API
|
1868 |
+
# key. See: https://products.wolframalpha.com/api/
|
1869 |
+
#
|
1870 |
+
# engine: wolframalpha_api
|
1871 |
+
# api_key: ''
|
1872 |
+
#
|
1873 |
+
# Or you can use the html non-stable engine, activated by default
|
1874 |
+
engine: wolframalpha_noapi
|
1875 |
+
timeout: 6.0
|
1876 |
+
categories: general
|
1877 |
+
disabled: false
|
1878 |
+
|
1879 |
+
- name: dictzone
|
1880 |
+
engine: dictzone
|
1881 |
+
shortcut: dc
|
1882 |
+
|
1883 |
+
- name: mymemory translated
|
1884 |
+
engine: translated
|
1885 |
+
shortcut: tl
|
1886 |
+
timeout: 5.0
|
1887 |
+
# You can use without an API key, but you are limited to 1000 words/day
|
1888 |
+
# See: https://mymemory.translated.net/doc/usagelimits.php
|
1889 |
+
# api_key: ''
|
1890 |
+
|
1891 |
+
# Required dependency: mysql-connector-python
|
1892 |
+
# - name: mysql
|
1893 |
+
# engine: mysql_server
|
1894 |
+
# database: mydatabase
|
1895 |
+
# username: user
|
1896 |
+
# password: pass
|
1897 |
+
# limit: 10
|
1898 |
+
# query_str: 'SELECT * from mytable WHERE fieldname=%(query)s'
|
1899 |
+
# shortcut: mysql
|
1900 |
+
|
1901 |
+
- name: 1337x
|
1902 |
+
engine: 1337x
|
1903 |
+
shortcut: 1337x
|
1904 |
+
disabled: true
|
1905 |
+
|
1906 |
+
- name: duden
|
1907 |
+
engine: duden
|
1908 |
+
shortcut: du
|
1909 |
+
disabled: true
|
1910 |
+
|
1911 |
+
- name: seznam
|
1912 |
+
shortcut: szn
|
1913 |
+
engine: seznam
|
1914 |
+
disabled: true
|
1915 |
+
|
1916 |
+
# - name: deepl
|
1917 |
+
# engine: deepl
|
1918 |
+
# shortcut: dpl
|
1919 |
+
# # You can use the engine using the official stable API, but you need an API key
|
1920 |
+
# # See: https://www.deepl.com/pro-api?cta=header-pro-api
|
1921 |
+
# api_key: '' # required!
|
1922 |
+
# timeout: 5.0
|
1923 |
+
# disabled: true
|
1924 |
+
|
1925 |
+
- name: mojeek
|
1926 |
+
shortcut: mjk
|
1927 |
+
engine: xpath
|
1928 |
+
paging: true
|
1929 |
+
categories: [general, web]
|
1930 |
+
search_url: https://www.mojeek.com/search?q={query}&s={pageno}&lang={lang}&lb={lang}
|
1931 |
+
results_xpath: //ul[@class="results-standard"]/li/a[@class="ob"]
|
1932 |
+
url_xpath: ./@href
|
1933 |
+
title_xpath: ../h2/a
|
1934 |
+
content_xpath: ..//p[@class="s"]
|
1935 |
+
suggestion_xpath: //div[@class="top-info"]/p[@class="top-info spell"]/em/a
|
1936 |
+
first_page_num: 0
|
1937 |
+
page_size: 10
|
1938 |
+
max_page: 100
|
1939 |
+
disabled: true
|
1940 |
+
about:
|
1941 |
+
website: https://www.mojeek.com/
|
1942 |
+
wikidata_id: Q60747299
|
1943 |
+
official_api_documentation: https://www.mojeek.com/services/api.html/
|
1944 |
+
use_official_api: false
|
1945 |
+
require_api_key: false
|
1946 |
+
results: HTML
|
1947 |
+
|
1948 |
+
- name: moviepilot
|
1949 |
+
engine: moviepilot
|
1950 |
+
shortcut: mp
|
1951 |
+
disabled: true
|
1952 |
+
|
1953 |
+
- name: naver
|
1954 |
+
shortcut: nvr
|
1955 |
+
categories: [general, web]
|
1956 |
+
engine: xpath
|
1957 |
+
paging: true
|
1958 |
+
search_url: https://search.naver.com/search.naver?where=webkr&sm=osp_hty&ie=UTF-8&query={query}&start={pageno}
|
1959 |
+
url_xpath: //a[@class="link_tit"]/@href
|
1960 |
+
title_xpath: //a[@class="link_tit"]
|
1961 |
+
content_xpath: //a[@class="total_dsc"]/div
|
1962 |
+
first_page_num: 1
|
1963 |
+
page_size: 10
|
1964 |
+
disabled: true
|
1965 |
+
about:
|
1966 |
+
website: https://www.naver.com/
|
1967 |
+
wikidata_id: Q485639
|
1968 |
+
official_api_documentation: https://developers.naver.com/docs/nmt/examples/
|
1969 |
+
use_official_api: false
|
1970 |
+
require_api_key: false
|
1971 |
+
results: HTML
|
1972 |
+
language: ko
|
1973 |
+
|
1974 |
+
- name: rubygems
|
1975 |
+
shortcut: rbg
|
1976 |
+
engine: xpath
|
1977 |
+
paging: true
|
1978 |
+
search_url: https://rubygems.org/search?page={pageno}&query={query}
|
1979 |
+
results_xpath: /html/body/main/div/a[@class="gems__gem"]
|
1980 |
+
url_xpath: ./@href
|
1981 |
+
title_xpath: ./span/h2
|
1982 |
+
content_xpath: ./span/p
|
1983 |
+
suggestion_xpath: /html/body/main/div/div[@class="search__suggestions"]/p/a
|
1984 |
+
first_page_num: 1
|
1985 |
+
categories: [it, packages]
|
1986 |
+
disabled: true
|
1987 |
+
about:
|
1988 |
+
website: https://rubygems.org/
|
1989 |
+
wikidata_id: Q1853420
|
1990 |
+
official_api_documentation: https://guides.rubygems.org/rubygems-org-api/
|
1991 |
+
use_official_api: false
|
1992 |
+
require_api_key: false
|
1993 |
+
results: HTML
|
1994 |
+
|
1995 |
+
- name: peertube
|
1996 |
+
engine: peertube
|
1997 |
+
shortcut: ptb
|
1998 |
+
paging: true
|
1999 |
+
# alternatives see: https://instances.joinpeertube.org/instances
|
2000 |
+
# base_url: https://tube.4aem.com
|
2001 |
+
categories: videos
|
2002 |
+
disabled: true
|
2003 |
+
timeout: 6.0
|
2004 |
+
|
2005 |
+
- name: mediathekviewweb
|
2006 |
+
engine: mediathekviewweb
|
2007 |
+
shortcut: mvw
|
2008 |
+
disabled: true
|
2009 |
+
|
2010 |
+
- name: yacy
|
2011 |
+
engine: yacy
|
2012 |
+
categories: general
|
2013 |
+
search_type: text
|
2014 |
+
base_url: https://yacy.searchlab.eu
|
2015 |
+
shortcut: ya
|
2016 |
+
disabled: true
|
2017 |
+
# required if you aren't using HTTPS for your local yacy instance
|
2018 |
+
# https://docs.searxng.org/dev/engines/online/yacy.html
|
2019 |
+
# enable_http: true
|
2020 |
+
# timeout: 3.0
|
2021 |
+
# search_mode: 'global'
|
2022 |
+
|
2023 |
+
- name: yacy images
|
2024 |
+
engine: yacy
|
2025 |
+
categories: images
|
2026 |
+
search_type: image
|
2027 |
+
base_url: https://yacy.searchlab.eu
|
2028 |
+
shortcut: yai
|
2029 |
+
disabled: true
|
2030 |
+
|
2031 |
+
- name: rumble
|
2032 |
+
engine: rumble
|
2033 |
+
shortcut: ru
|
2034 |
+
base_url: https://rumble.com/
|
2035 |
+
paging: true
|
2036 |
+
categories: videos
|
2037 |
+
disabled: true
|
2038 |
+
|
2039 |
+
- name: livespace
|
2040 |
+
engine: livespace
|
2041 |
+
shortcut: ls
|
2042 |
+
categories: videos
|
2043 |
+
disabled: true
|
2044 |
+
timeout: 5.0
|
2045 |
+
|
2046 |
+
- name: wordnik
|
2047 |
+
engine: wordnik
|
2048 |
+
shortcut: def
|
2049 |
+
base_url: https://www.wordnik.com/
|
2050 |
+
categories: [dictionaries]
|
2051 |
+
timeout: 5.0
|
2052 |
+
|
2053 |
+
- name: woxikon.de synonyme
|
2054 |
+
engine: xpath
|
2055 |
+
shortcut: woxi
|
2056 |
+
categories: [dictionaries]
|
2057 |
+
timeout: 5.0
|
2058 |
+
disabled: true
|
2059 |
+
search_url: https://synonyme.woxikon.de/synonyme/{query}.php
|
2060 |
+
url_xpath: //div[@class="upper-synonyms"]/a/@href
|
2061 |
+
content_xpath: //div[@class="synonyms-list-group"]
|
2062 |
+
title_xpath: //div[@class="upper-synonyms"]/a
|
2063 |
+
no_result_for_http_status: [404]
|
2064 |
+
about:
|
2065 |
+
website: https://www.woxikon.de/
|
2066 |
+
wikidata_id: # No Wikidata ID
|
2067 |
+
use_official_api: false
|
2068 |
+
require_api_key: false
|
2069 |
+
results: HTML
|
2070 |
+
language: de
|
2071 |
+
|
2072 |
+
- name: seekr news
|
2073 |
+
engine: seekr
|
2074 |
+
shortcut: senews
|
2075 |
+
categories: news
|
2076 |
+
seekr_category: news
|
2077 |
+
disabled: true
|
2078 |
+
|
2079 |
+
- name: seekr images
|
2080 |
+
engine: seekr
|
2081 |
+
network: seekr news
|
2082 |
+
shortcut: seimg
|
2083 |
+
categories: images
|
2084 |
+
seekr_category: images
|
2085 |
+
disabled: true
|
2086 |
+
|
2087 |
+
- name: seekr videos
|
2088 |
+
engine: seekr
|
2089 |
+
network: seekr news
|
2090 |
+
shortcut: sevid
|
2091 |
+
categories: videos
|
2092 |
+
seekr_category: videos
|
2093 |
+
disabled: true
|
2094 |
+
|
2095 |
+
- name: sjp.pwn
|
2096 |
+
engine: sjp
|
2097 |
+
shortcut: sjp
|
2098 |
+
base_url: https://sjp.pwn.pl/
|
2099 |
+
timeout: 5.0
|
2100 |
+
disabled: true
|
2101 |
+
|
2102 |
+
- name: stract
|
2103 |
+
engine: stract
|
2104 |
+
shortcut: str
|
2105 |
+
disabled: true
|
2106 |
+
|
2107 |
+
- name: svgrepo
|
2108 |
+
engine: svgrepo
|
2109 |
+
shortcut: svg
|
2110 |
+
timeout: 10.0
|
2111 |
+
disabled: true
|
2112 |
+
|
2113 |
+
- name: tootfinder
|
2114 |
+
engine: tootfinder
|
2115 |
+
shortcut: toot
|
2116 |
+
|
2117 |
+
- name: wallhaven
|
2118 |
+
engine: wallhaven
|
2119 |
+
# api_key: abcdefghijklmnopqrstuvwxyz
|
2120 |
+
shortcut: wh
|
2121 |
+
|
2122 |
+
# wikimini: online encyclopedia for children
|
2123 |
+
# The fulltext and title parameter is necessary for Wikimini because
|
2124 |
+
# sometimes it will not show the results and redirect instead
|
2125 |
+
- name: wikimini
|
2126 |
+
engine: xpath
|
2127 |
+
shortcut: wkmn
|
2128 |
+
search_url: https://fr.wikimini.org/w/index.php?search={query}&title=Sp%C3%A9cial%3ASearch&fulltext=Search
|
2129 |
+
url_xpath: //li/div[@class="mw-search-result-heading"]/a/@href
|
2130 |
+
title_xpath: //li//div[@class="mw-search-result-heading"]/a
|
2131 |
+
content_xpath: //li/div[@class="searchresult"]
|
2132 |
+
categories: general
|
2133 |
+
disabled: true
|
2134 |
+
about:
|
2135 |
+
website: https://wikimini.org/
|
2136 |
+
wikidata_id: Q3568032
|
2137 |
+
use_official_api: false
|
2138 |
+
require_api_key: false
|
2139 |
+
results: HTML
|
2140 |
+
language: fr
|
2141 |
+
|
2142 |
+
- name: wttr.in
|
2143 |
+
engine: wttr
|
2144 |
+
shortcut: wttr
|
2145 |
+
timeout: 9.0
|
2146 |
+
|
2147 |
+
- name: yummly
|
2148 |
+
engine: yummly
|
2149 |
+
shortcut: yum
|
2150 |
+
disabled: true
|
2151 |
+
|
2152 |
+
- name: brave
|
2153 |
+
engine: brave
|
2154 |
+
shortcut: br
|
2155 |
+
time_range_support: true
|
2156 |
+
paging: true
|
2157 |
+
categories: [general, web]
|
2158 |
+
brave_category: search
|
2159 |
+
# brave_spellcheck: true
|
2160 |
+
|
2161 |
+
- name: brave.images
|
2162 |
+
engine: brave
|
2163 |
+
network: brave
|
2164 |
+
shortcut: brimg
|
2165 |
+
categories: [images, web]
|
2166 |
+
brave_category: images
|
2167 |
+
|
2168 |
+
- name: brave.videos
|
2169 |
+
engine: brave
|
2170 |
+
network: brave
|
2171 |
+
shortcut: brvid
|
2172 |
+
categories: [videos, web]
|
2173 |
+
brave_category: videos
|
2174 |
+
|
2175 |
+
- name: brave.news
|
2176 |
+
engine: brave
|
2177 |
+
network: brave
|
2178 |
+
shortcut: brnews
|
2179 |
+
categories: news
|
2180 |
+
brave_category: news
|
2181 |
+
|
2182 |
+
# - name: brave.goggles
|
2183 |
+
# engine: brave
|
2184 |
+
# network: brave
|
2185 |
+
# shortcut: brgog
|
2186 |
+
# time_range_support: true
|
2187 |
+
# paging: true
|
2188 |
+
# categories: [general, web]
|
2189 |
+
# brave_category: goggles
|
2190 |
+
# Goggles: # required! This should be a URL ending in .goggle
|
2191 |
+
|
2192 |
+
- name: lib.rs
|
2193 |
+
shortcut: lrs
|
2194 |
+
engine: xpath
|
2195 |
+
search_url: https://lib.rs/search?q={query}
|
2196 |
+
results_xpath: /html/body/main/div/ol/li/a
|
2197 |
+
url_xpath: ./@href
|
2198 |
+
title_xpath: ./div[@class="h"]/h4
|
2199 |
+
content_xpath: ./div[@class="h"]/p
|
2200 |
+
categories: [it, packages]
|
2201 |
+
disabled: true
|
2202 |
+
about:
|
2203 |
+
website: https://lib.rs
|
2204 |
+
wikidata_id: Q113486010
|
2205 |
+
use_official_api: false
|
2206 |
+
require_api_key: false
|
2207 |
+
results: HTML
|
2208 |
+
|
2209 |
+
- name: sourcehut
|
2210 |
+
shortcut: srht
|
2211 |
+
engine: xpath
|
2212 |
+
paging: true
|
2213 |
+
search_url: https://sr.ht/projects?page={pageno}&search={query}
|
2214 |
+
results_xpath: (//div[@class="event-list"])[1]/div[@class="event"]
|
2215 |
+
url_xpath: ./h4/a[2]/@href
|
2216 |
+
title_xpath: ./h4/a[2]
|
2217 |
+
content_xpath: ./p
|
2218 |
+
first_page_num: 1
|
2219 |
+
categories: [it, repos]
|
2220 |
+
disabled: true
|
2221 |
+
about:
|
2222 |
+
website: https://sr.ht
|
2223 |
+
wikidata_id: Q78514485
|
2224 |
+
official_api_documentation: https://man.sr.ht/
|
2225 |
+
use_official_api: false
|
2226 |
+
require_api_key: false
|
2227 |
+
results: HTML
|
2228 |
+
|
2229 |
+
- name: goo
|
2230 |
+
shortcut: goo
|
2231 |
+
engine: xpath
|
2232 |
+
paging: true
|
2233 |
+
search_url: https://search.goo.ne.jp/web.jsp?MT={query}&FR={pageno}0
|
2234 |
+
url_xpath: //div[@class="result"]/p[@class='title fsL1']/a/@href
|
2235 |
+
title_xpath: //div[@class="result"]/p[@class='title fsL1']/a
|
2236 |
+
content_xpath: //p[contains(@class,'url fsM')]/following-sibling::p
|
2237 |
+
first_page_num: 0
|
2238 |
+
categories: [general, web]
|
2239 |
+
disabled: true
|
2240 |
+
timeout: 4.0
|
2241 |
+
about:
|
2242 |
+
website: https://search.goo.ne.jp
|
2243 |
+
wikidata_id: Q249044
|
2244 |
+
use_official_api: false
|
2245 |
+
require_api_key: false
|
2246 |
+
results: HTML
|
2247 |
+
language: ja
|
2248 |
+
|
2249 |
+
- name: bt4g
|
2250 |
+
engine: bt4g
|
2251 |
+
shortcut: bt4g
|
2252 |
+
|
2253 |
+
- name: pkg.go.dev
|
2254 |
+
engine: xpath
|
2255 |
+
shortcut: pgo
|
2256 |
+
search_url: https://pkg.go.dev/search?limit=100&m=package&q={query}
|
2257 |
+
results_xpath: /html/body/main/div[contains(@class,"SearchResults")]/div[not(@class)]/div[@class="SearchSnippet"]
|
2258 |
+
url_xpath: ./div[@class="SearchSnippet-headerContainer"]/h2/a/@href
|
2259 |
+
title_xpath: ./div[@class="SearchSnippet-headerContainer"]/h2/a
|
2260 |
+
content_xpath: ./p[@class="SearchSnippet-synopsis"]
|
2261 |
+
categories: [packages, it]
|
2262 |
+
timeout: 3.0
|
2263 |
+
disabled: true
|
2264 |
+
about:
|
2265 |
+
website: https://pkg.go.dev/
|
2266 |
+
use_official_api: false
|
2267 |
+
require_api_key: false
|
2268 |
+
results: HTML
|
2269 |
+
|
2270 |
+
# Doku engine lets you access to any Doku wiki instance:
|
2271 |
+
# A public one or a privete/corporate one.
|
2272 |
+
# - name: ubuntuwiki
|
2273 |
+
# engine: doku
|
2274 |
+
# shortcut: uw
|
2275 |
+
# base_url: 'https://doc.ubuntu-fr.org'
|
2276 |
+
|
2277 |
+
# Be careful when enabling this engine if you are
|
2278 |
+
# running a public instance. Do not expose any sensitive
|
2279 |
+
# information. You can restrict access by configuring a list
|
2280 |
+
# of access tokens under tokens.
|
2281 |
+
# - name: git grep
|
2282 |
+
# engine: command
|
2283 |
+
# command: ['git', 'grep', '{{QUERY}}']
|
2284 |
+
# shortcut: gg
|
2285 |
+
# tokens: []
|
2286 |
+
# disabled: true
|
2287 |
+
# delimiter:
|
2288 |
+
# chars: ':'
|
2289 |
+
# keys: ['filepath', 'code']
|
2290 |
+
|
2291 |
+
# Be careful when enabling this engine if you are
|
2292 |
+
# running a public instance. Do not expose any sensitive
|
2293 |
+
# information. You can restrict access by configuring a list
|
2294 |
+
# of access tokens under tokens.
|
2295 |
+
# - name: locate
|
2296 |
+
# engine: command
|
2297 |
+
# command: ['locate', '{{QUERY}}']
|
2298 |
+
# shortcut: loc
|
2299 |
+
# tokens: []
|
2300 |
+
# disabled: true
|
2301 |
+
# delimiter:
|
2302 |
+
# chars: ' '
|
2303 |
+
# keys: ['line']
|
2304 |
+
|
2305 |
+
# Be careful when enabling this engine if you are
|
2306 |
+
# running a public instance. Do not expose any sensitive
|
2307 |
+
# information. You can restrict access by configuring a list
|
2308 |
+
# of access tokens under tokens.
|
2309 |
+
# - name: find
|
2310 |
+
# engine: command
|
2311 |
+
# command: ['find', '.', '-name', '{{QUERY}}']
|
2312 |
+
# query_type: path
|
2313 |
+
# shortcut: fnd
|
2314 |
+
# tokens: []
|
2315 |
+
# disabled: true
|
2316 |
+
# delimiter:
|
2317 |
+
# chars: ' '
|
2318 |
+
# keys: ['line']
|
2319 |
+
|
2320 |
+
# Be careful when enabling this engine if you are
|
2321 |
+
# running a public instance. Do not expose any sensitive
|
2322 |
+
# information. You can restrict access by configuring a list
|
2323 |
+
# of access tokens under tokens.
|
2324 |
+
# - name: pattern search in files
|
2325 |
+
# engine: command
|
2326 |
+
# command: ['fgrep', '{{QUERY}}']
|
2327 |
+
# shortcut: fgr
|
2328 |
+
# tokens: []
|
2329 |
+
# disabled: true
|
2330 |
+
# delimiter:
|
2331 |
+
# chars: ' '
|
2332 |
+
# keys: ['line']
|
2333 |
+
|
2334 |
+
# Be careful when enabling this engine if you are
|
2335 |
+
# running a public instance. Do not expose any sensitive
|
2336 |
+
# information. You can restrict access by configuring a list
|
2337 |
+
# of access tokens under tokens.
|
2338 |
+
# - name: regex search in files
|
2339 |
+
# engine: command
|
2340 |
+
# command: ['grep', '{{QUERY}}']
|
2341 |
+
# shortcut: gr
|
2342 |
+
# tokens: []
|
2343 |
+
# disabled: true
|
2344 |
+
# delimiter:
|
2345 |
+
# chars: ' '
|
2346 |
+
# keys: ['line']
|
2347 |
+
|
2348 |
+
doi_resolvers:
|
2349 |
+
oadoi.org: 'https://oadoi.org/'
|
2350 |
+
doi.org: 'https://doi.org/'
|
2351 |
+
doai.io: 'https://dissem.in/'
|
2352 |
+
sci-hub.se: 'https://sci-hub.se/'
|
2353 |
+
sci-hub.st: 'https://sci-hub.st/'
|
2354 |
+
sci-hub.ru: 'https://sci-hub.ru/'
|
2355 |
+
|
2356 |
+
default_doi_resolver: 'oadoi.org'
|
searxng.dockerfile
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
FROM searxng/searxng
|
2 |
+
|
3 |
+
COPY searxng-settings.yml /etc/searxng/settings.yml
|
src/Perplexica - Shortcut.lnk
ADDED
Binary file (868 Bytes). View file
|
|
src/agents/academicSearchAgent.ts
ADDED
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { BaseMessage } from '@langchain/core/messages';
|
2 |
+
import {
|
3 |
+
PromptTemplate,
|
4 |
+
ChatPromptTemplate,
|
5 |
+
MessagesPlaceholder,
|
6 |
+
} from '@langchain/core/prompts';
|
7 |
+
import {
|
8 |
+
RunnableSequence,
|
9 |
+
RunnableMap,
|
10 |
+
RunnableLambda,
|
11 |
+
} from '@langchain/core/runnables';
|
12 |
+
import { StringOutputParser } from '@langchain/core/output_parsers';
|
13 |
+
import { Document } from '@langchain/core/documents';
|
14 |
+
import { searchSearxng } from '../lib/searxng';
|
15 |
+
import type { StreamEvent } from '@langchain/core/tracers/log_stream';
|
16 |
+
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
17 |
+
import type { Embeddings } from '@langchain/core/embeddings';
|
18 |
+
import formatChatHistoryAsString from '../utils/formatHistory';
|
19 |
+
import eventEmitter from 'events';
|
20 |
+
import computeSimilarity from '../utils/computeSimilarity';
|
21 |
+
import logger from '../utils/logger';
|
22 |
+
|
23 |
+
const basicAcademicSearchRetrieverPrompt = `
|
24 |
+
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.
|
25 |
+
If it is a writing task or a simple hi, hello rather than a question, you need to return \`not_needed\` as the response.
|
26 |
+
|
27 |
+
Example:
|
28 |
+
1. Follow up question: How does stable diffusion work?
|
29 |
+
Rephrased: Stable diffusion working
|
30 |
+
|
31 |
+
2. Follow up question: What is linear algebra?
|
32 |
+
Rephrased: Linear algebra
|
33 |
+
|
34 |
+
3. Follow up question: What is the third law of thermodynamics?
|
35 |
+
Rephrased: Third law of thermodynamics
|
36 |
+
|
37 |
+
Conversation:
|
38 |
+
{chat_history}
|
39 |
+
|
40 |
+
Follow up question: {query}
|
41 |
+
Rephrased question:
|
42 |
+
`;
|
43 |
+
|
44 |
+
const basicAcademicSearchResponsePrompt = `
|
45 |
+
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.
|
46 |
+
|
47 |
+
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).
|
48 |
+
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.
|
49 |
+
You must not tell the user to open any link or visit any website to get the answer. You must provide the answer in the response itself. If the user asks for links you can provide them.
|
50 |
+
Your responses should be medium to long in length be informative and relevant to the user's query. You can use markdowns to format your response. You should use bullet points to list the information. Make sure the answer is not short and is informative.
|
51 |
+
You have to cite the answer using [number] notation. You must cite the sentences with their relevent context number. You must cite each and every part of the answer so the user can know where the information is coming from.
|
52 |
+
Place these citations at the end of that particular sentence. You can cite the same sentence multiple times if it is relevant to the user's query like [number1][number2].
|
53 |
+
However you do not need to cite it using the same number. You can use different numbers to cite the same sentence multiple times. The number refers to the number of the search result (passed in the context) used to generate that part of the answer.
|
54 |
+
|
55 |
+
Aything inside the following \`context\` HTML block provided below is for your knowledge returned by the search engine and is not shared by the user. You have to answer question on the basis of it and cite the relevant information from it but you do not have to
|
56 |
+
talk about the context in your response.
|
57 |
+
|
58 |
+
<context>
|
59 |
+
{context}
|
60 |
+
</context>
|
61 |
+
|
62 |
+
If you think there's nothing relevant in the search results, you can say that 'Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?'.
|
63 |
+
Anything between the \`context\` is retrieved from a search engine and is not a part of the conversation with the user. Today's date is ${new Date().toISOString()}
|
64 |
+
`;
|
65 |
+
|
66 |
+
const strParser = new StringOutputParser();
|
67 |
+
|
68 |
+
const handleStream = async (
|
69 |
+
stream: AsyncGenerator<StreamEvent, any, unknown>,
|
70 |
+
emitter: eventEmitter,
|
71 |
+
) => {
|
72 |
+
for await (const event of stream) {
|
73 |
+
if (
|
74 |
+
event.event === 'on_chain_end' &&
|
75 |
+
event.name === 'FinalSourceRetriever'
|
76 |
+
) {
|
77 |
+
emitter.emit(
|
78 |
+
'data',
|
79 |
+
JSON.stringify({ type: 'sources', data: event.data.output }),
|
80 |
+
);
|
81 |
+
}
|
82 |
+
if (
|
83 |
+
event.event === 'on_chain_stream' &&
|
84 |
+
event.name === 'FinalResponseGenerator'
|
85 |
+
) {
|
86 |
+
emitter.emit(
|
87 |
+
'data',
|
88 |
+
JSON.stringify({ type: 'response', data: event.data.chunk }),
|
89 |
+
);
|
90 |
+
}
|
91 |
+
if (
|
92 |
+
event.event === 'on_chain_end' &&
|
93 |
+
event.name === 'FinalResponseGenerator'
|
94 |
+
) {
|
95 |
+
emitter.emit('end');
|
96 |
+
}
|
97 |
+
}
|
98 |
+
};
|
99 |
+
|
100 |
+
type BasicChainInput = {
|
101 |
+
chat_history: BaseMessage[];
|
102 |
+
query: string;
|
103 |
+
};
|
104 |
+
|
105 |
+
const createBasicAcademicSearchRetrieverChain = (llm: BaseChatModel) => {
|
106 |
+
return RunnableSequence.from([
|
107 |
+
PromptTemplate.fromTemplate(basicAcademicSearchRetrieverPrompt),
|
108 |
+
llm,
|
109 |
+
strParser,
|
110 |
+
RunnableLambda.from(async (input: string) => {
|
111 |
+
if (input === 'not_needed') {
|
112 |
+
return { query: '', docs: [] };
|
113 |
+
}
|
114 |
+
|
115 |
+
const res = await searchSearxng(input, {
|
116 |
+
language: 'en',
|
117 |
+
engines: [
|
118 |
+
'arxiv',
|
119 |
+
'google scholar',
|
120 |
+
'internetarchivescholar',
|
121 |
+
'pubmed',
|
122 |
+
],
|
123 |
+
});
|
124 |
+
|
125 |
+
const documents = res.results.map(
|
126 |
+
(result) =>
|
127 |
+
new Document({
|
128 |
+
pageContent: result.content,
|
129 |
+
metadata: {
|
130 |
+
title: result.title,
|
131 |
+
url: result.url,
|
132 |
+
...(result.img_src && { img_src: result.img_src }),
|
133 |
+
},
|
134 |
+
}),
|
135 |
+
);
|
136 |
+
|
137 |
+
return { query: input, docs: documents };
|
138 |
+
}),
|
139 |
+
]);
|
140 |
+
};
|
141 |
+
|
142 |
+
const createBasicAcademicSearchAnsweringChain = (
|
143 |
+
llm: BaseChatModel,
|
144 |
+
embeddings: Embeddings,
|
145 |
+
) => {
|
146 |
+
const basicAcademicSearchRetrieverChain =
|
147 |
+
createBasicAcademicSearchRetrieverChain(llm);
|
148 |
+
|
149 |
+
const processDocs = async (docs: Document[]) => {
|
150 |
+
return docs
|
151 |
+
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
|
152 |
+
.join('\n');
|
153 |
+
};
|
154 |
+
|
155 |
+
const rerankDocs = async ({
|
156 |
+
query,
|
157 |
+
docs,
|
158 |
+
}: {
|
159 |
+
query: string;
|
160 |
+
docs: Document[];
|
161 |
+
}) => {
|
162 |
+
if (docs.length === 0) {
|
163 |
+
return docs;
|
164 |
+
}
|
165 |
+
|
166 |
+
const docsWithContent = docs.filter(
|
167 |
+
(doc) => doc.pageContent && doc.pageContent.length > 0,
|
168 |
+
);
|
169 |
+
|
170 |
+
const [docEmbeddings, queryEmbedding] = await Promise.all([
|
171 |
+
embeddings.embedDocuments(docsWithContent.map((doc) => doc.pageContent)),
|
172 |
+
embeddings.embedQuery(query),
|
173 |
+
]);
|
174 |
+
|
175 |
+
const similarity = docEmbeddings.map((docEmbedding, i) => {
|
176 |
+
const sim = computeSimilarity(queryEmbedding, docEmbedding);
|
177 |
+
|
178 |
+
return {
|
179 |
+
index: i,
|
180 |
+
similarity: sim,
|
181 |
+
};
|
182 |
+
});
|
183 |
+
|
184 |
+
const sortedDocs = similarity
|
185 |
+
.sort((a, b) => b.similarity - a.similarity)
|
186 |
+
.slice(0, 15)
|
187 |
+
.map((sim) => docsWithContent[sim.index]);
|
188 |
+
|
189 |
+
return sortedDocs;
|
190 |
+
};
|
191 |
+
|
192 |
+
return RunnableSequence.from([
|
193 |
+
RunnableMap.from({
|
194 |
+
query: (input: BasicChainInput) => input.query,
|
195 |
+
chat_history: (input: BasicChainInput) => input.chat_history,
|
196 |
+
context: RunnableSequence.from([
|
197 |
+
(input) => ({
|
198 |
+
query: input.query,
|
199 |
+
chat_history: formatChatHistoryAsString(input.chat_history),
|
200 |
+
}),
|
201 |
+
basicAcademicSearchRetrieverChain
|
202 |
+
.pipe(rerankDocs)
|
203 |
+
.withConfig({
|
204 |
+
runName: 'FinalSourceRetriever',
|
205 |
+
})
|
206 |
+
.pipe(processDocs),
|
207 |
+
]),
|
208 |
+
}),
|
209 |
+
ChatPromptTemplate.fromMessages([
|
210 |
+
['system', basicAcademicSearchResponsePrompt],
|
211 |
+
new MessagesPlaceholder('chat_history'),
|
212 |
+
['user', '{query}'],
|
213 |
+
]),
|
214 |
+
llm,
|
215 |
+
strParser,
|
216 |
+
]).withConfig({
|
217 |
+
runName: 'FinalResponseGenerator',
|
218 |
+
});
|
219 |
+
};
|
220 |
+
|
221 |
+
const basicAcademicSearch = (
|
222 |
+
query: string,
|
223 |
+
history: BaseMessage[],
|
224 |
+
llm: BaseChatModel,
|
225 |
+
embeddings: Embeddings,
|
226 |
+
) => {
|
227 |
+
const emitter = new eventEmitter();
|
228 |
+
|
229 |
+
try {
|
230 |
+
const basicAcademicSearchAnsweringChain =
|
231 |
+
createBasicAcademicSearchAnsweringChain(llm, embeddings);
|
232 |
+
|
233 |
+
const stream = basicAcademicSearchAnsweringChain.streamEvents(
|
234 |
+
{
|
235 |
+
chat_history: history,
|
236 |
+
query: query,
|
237 |
+
},
|
238 |
+
{
|
239 |
+
version: 'v1',
|
240 |
+
},
|
241 |
+
);
|
242 |
+
|
243 |
+
handleStream(stream, emitter);
|
244 |
+
} catch (err) {
|
245 |
+
emitter.emit(
|
246 |
+
'error',
|
247 |
+
JSON.stringify({ data: 'An error has occurred please try again later' }),
|
248 |
+
);
|
249 |
+
logger.error(`Error in academic search: ${err}`);
|
250 |
+
}
|
251 |
+
|
252 |
+
return emitter;
|
253 |
+
};
|
254 |
+
|
255 |
+
const handleAcademicSearch = (
|
256 |
+
message: string,
|
257 |
+
history: BaseMessage[],
|
258 |
+
llm: BaseChatModel,
|
259 |
+
embeddings: Embeddings,
|
260 |
+
) => {
|
261 |
+
const emitter = basicAcademicSearch(message, history, llm, embeddings);
|
262 |
+
return emitter;
|
263 |
+
};
|
264 |
+
|
265 |
+
export default handleAcademicSearch;
|
src/agents/imageSearchAgent.ts
ADDED
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {
|
2 |
+
RunnableSequence,
|
3 |
+
RunnableMap,
|
4 |
+
RunnableLambda,
|
5 |
+
} from '@langchain/core/runnables';
|
6 |
+
import { PromptTemplate } from '@langchain/core/prompts';
|
7 |
+
import formatChatHistoryAsString from '../utils/formatHistory';
|
8 |
+
import { BaseMessage } from '@langchain/core/messages';
|
9 |
+
import { StringOutputParser } from '@langchain/core/output_parsers';
|
10 |
+
import { searchSearxng } from '../lib/searxng';
|
11 |
+
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
12 |
+
|
13 |
+
const imageSearchChainPrompt = `
|
14 |
+
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.
|
15 |
+
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
|
16 |
+
|
17 |
+
Example:
|
18 |
+
1. Follow up question: What is a cat?
|
19 |
+
Rephrased: A cat
|
20 |
+
|
21 |
+
2. Follow up question: What is a car? How does it works?
|
22 |
+
Rephrased: Car working
|
23 |
+
|
24 |
+
3. Follow up question: How does an AC work?
|
25 |
+
Rephrased: AC working
|
26 |
+
|
27 |
+
Conversation:
|
28 |
+
{chat_history}
|
29 |
+
|
30 |
+
Follow up question: {query}
|
31 |
+
Rephrased question:
|
32 |
+
`;
|
33 |
+
|
34 |
+
type ImageSearchChainInput = {
|
35 |
+
chat_history: BaseMessage[];
|
36 |
+
query: string;
|
37 |
+
};
|
38 |
+
|
39 |
+
const strParser = new StringOutputParser();
|
40 |
+
|
41 |
+
const createImageSearchChain = (llm: BaseChatModel) => {
|
42 |
+
return RunnableSequence.from([
|
43 |
+
RunnableMap.from({
|
44 |
+
chat_history: (input: ImageSearchChainInput) => {
|
45 |
+
return formatChatHistoryAsString(input.chat_history);
|
46 |
+
},
|
47 |
+
query: (input: ImageSearchChainInput) => {
|
48 |
+
return input.query;
|
49 |
+
},
|
50 |
+
}),
|
51 |
+
PromptTemplate.fromTemplate(imageSearchChainPrompt),
|
52 |
+
llm,
|
53 |
+
strParser,
|
54 |
+
RunnableLambda.from(async (input: string) => {
|
55 |
+
const res = await searchSearxng(input, {
|
56 |
+
engines: ['bing images', 'google images'],
|
57 |
+
});
|
58 |
+
|
59 |
+
const images = [];
|
60 |
+
|
61 |
+
res.results.forEach((result) => {
|
62 |
+
if (result.img_src && result.url && result.title) {
|
63 |
+
images.push({
|
64 |
+
img_src: result.img_src,
|
65 |
+
url: result.url,
|
66 |
+
title: result.title,
|
67 |
+
});
|
68 |
+
}
|
69 |
+
});
|
70 |
+
|
71 |
+
return images.slice(0, 10);
|
72 |
+
}),
|
73 |
+
]);
|
74 |
+
};
|
75 |
+
|
76 |
+
const handleImageSearch = (
|
77 |
+
input: ImageSearchChainInput,
|
78 |
+
llm: BaseChatModel,
|
79 |
+
) => {
|
80 |
+
const imageSearchChain = createImageSearchChain(llm);
|
81 |
+
return imageSearchChain.invoke(input);
|
82 |
+
};
|
83 |
+
|
84 |
+
export default handleImageSearch;
|
src/agents/redditSearchAgent.ts
ADDED
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { BaseMessage } from '@langchain/core/messages';
|
2 |
+
import {
|
3 |
+
PromptTemplate,
|
4 |
+
ChatPromptTemplate,
|
5 |
+
MessagesPlaceholder,
|
6 |
+
} from '@langchain/core/prompts';
|
7 |
+
import {
|
8 |
+
RunnableSequence,
|
9 |
+
RunnableMap,
|
10 |
+
RunnableLambda,
|
11 |
+
} from '@langchain/core/runnables';
|
12 |
+
import { StringOutputParser } from '@langchain/core/output_parsers';
|
13 |
+
import { Document } from '@langchain/core/documents';
|
14 |
+
import { searchSearxng } from '../lib/searxng';
|
15 |
+
import type { StreamEvent } from '@langchain/core/tracers/log_stream';
|
16 |
+
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
17 |
+
import type { Embeddings } from '@langchain/core/embeddings';
|
18 |
+
import formatChatHistoryAsString from '../utils/formatHistory';
|
19 |
+
import eventEmitter from 'events';
|
20 |
+
import computeSimilarity from '../utils/computeSimilarity';
|
21 |
+
import logger from '../utils/logger';
|
22 |
+
|
23 |
+
const basicRedditSearchRetrieverPrompt = `
|
24 |
+
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.
|
25 |
+
If it is a writing task or a simple hi, hello rather than a question, you need to return \`not_needed\` as the response.
|
26 |
+
|
27 |
+
Example:
|
28 |
+
1. Follow up question: Which company is most likely to create an AGI
|
29 |
+
Rephrased: Which company is most likely to create an AGI
|
30 |
+
|
31 |
+
2. Follow up question: Is Earth flat?
|
32 |
+
Rephrased: Is Earth flat?
|
33 |
+
|
34 |
+
3. Follow up question: Is there life on Mars?
|
35 |
+
Rephrased: Is there life on Mars?
|
36 |
+
|
37 |
+
Conversation:
|
38 |
+
{chat_history}
|
39 |
+
|
40 |
+
Follow up question: {query}
|
41 |
+
Rephrased question:
|
42 |
+
`;
|
43 |
+
|
44 |
+
const basicRedditSearchResponsePrompt = `
|
45 |
+
You are Perplexica, an AI model who is expert at searching the web and answering user's queries. You are set on focus mode 'Reddit', this means you will be searching for information, opinions and discussions on the web using Reddit.
|
46 |
+
|
47 |
+
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).
|
48 |
+
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.
|
49 |
+
You must not tell the user to open any link or visit any website to get the answer. You must provide the answer in the response itself. If the user asks for links you can provide them.
|
50 |
+
Your responses should be medium to long in length be informative and relevant to the user's query. You can use markdowns to format your response. You should use bullet points to list the information. Make sure the answer is not short and is informative.
|
51 |
+
You have to cite the answer using [number] notation. You must cite the sentences with their relevent context number. You must cite each and every part of the answer so the user can know where the information is coming from.
|
52 |
+
Place these citations at the end of that particular sentence. You can cite the same sentence multiple times if it is relevant to the user's query like [number1][number2].
|
53 |
+
However you do not need to cite it using the same number. You can use different numbers to cite the same sentence multiple times. The number refers to the number of the search result (passed in the context) used to generate that part of the answer.
|
54 |
+
|
55 |
+
Aything inside the following \`context\` HTML block provided below is for your knowledge returned by Reddit and is not shared by the user. You have to answer question on the basis of it and cite the relevant information from it but you do not have to
|
56 |
+
talk about the context in your response.
|
57 |
+
|
58 |
+
<context>
|
59 |
+
{context}
|
60 |
+
</context>
|
61 |
+
|
62 |
+
If you think there's nothing relevant in the search results, you can say that 'Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?'.
|
63 |
+
Anything between the \`context\` is retrieved from Reddit and is not a part of the conversation with the user. Today's date is ${new Date().toISOString()}
|
64 |
+
`;
|
65 |
+
|
66 |
+
const strParser = new StringOutputParser();
|
67 |
+
|
68 |
+
const handleStream = async (
|
69 |
+
stream: AsyncGenerator<StreamEvent, any, unknown>,
|
70 |
+
emitter: eventEmitter,
|
71 |
+
) => {
|
72 |
+
for await (const event of stream) {
|
73 |
+
if (
|
74 |
+
event.event === 'on_chain_end' &&
|
75 |
+
event.name === 'FinalSourceRetriever'
|
76 |
+
) {
|
77 |
+
emitter.emit(
|
78 |
+
'data',
|
79 |
+
JSON.stringify({ type: 'sources', data: event.data.output }),
|
80 |
+
);
|
81 |
+
}
|
82 |
+
if (
|
83 |
+
event.event === 'on_chain_stream' &&
|
84 |
+
event.name === 'FinalResponseGenerator'
|
85 |
+
) {
|
86 |
+
emitter.emit(
|
87 |
+
'data',
|
88 |
+
JSON.stringify({ type: 'response', data: event.data.chunk }),
|
89 |
+
);
|
90 |
+
}
|
91 |
+
if (
|
92 |
+
event.event === 'on_chain_end' &&
|
93 |
+
event.name === 'FinalResponseGenerator'
|
94 |
+
) {
|
95 |
+
emitter.emit('end');
|
96 |
+
}
|
97 |
+
}
|
98 |
+
};
|
99 |
+
|
100 |
+
type BasicChainInput = {
|
101 |
+
chat_history: BaseMessage[];
|
102 |
+
query: string;
|
103 |
+
};
|
104 |
+
|
105 |
+
const createBasicRedditSearchRetrieverChain = (llm: BaseChatModel) => {
|
106 |
+
return RunnableSequence.from([
|
107 |
+
PromptTemplate.fromTemplate(basicRedditSearchRetrieverPrompt),
|
108 |
+
llm,
|
109 |
+
strParser,
|
110 |
+
RunnableLambda.from(async (input: string) => {
|
111 |
+
if (input === 'not_needed') {
|
112 |
+
return { query: '', docs: [] };
|
113 |
+
}
|
114 |
+
|
115 |
+
const res = await searchSearxng(input, {
|
116 |
+
language: 'en',
|
117 |
+
engines: ['reddit'],
|
118 |
+
});
|
119 |
+
|
120 |
+
const documents = res.results.map(
|
121 |
+
(result) =>
|
122 |
+
new Document({
|
123 |
+
pageContent: result.content ? result.content : result.title,
|
124 |
+
metadata: {
|
125 |
+
title: result.title,
|
126 |
+
url: result.url,
|
127 |
+
...(result.img_src && { img_src: result.img_src }),
|
128 |
+
},
|
129 |
+
}),
|
130 |
+
);
|
131 |
+
|
132 |
+
return { query: input, docs: documents };
|
133 |
+
}),
|
134 |
+
]);
|
135 |
+
};
|
136 |
+
|
137 |
+
const createBasicRedditSearchAnsweringChain = (
|
138 |
+
llm: BaseChatModel,
|
139 |
+
embeddings: Embeddings,
|
140 |
+
) => {
|
141 |
+
const basicRedditSearchRetrieverChain =
|
142 |
+
createBasicRedditSearchRetrieverChain(llm);
|
143 |
+
|
144 |
+
const processDocs = async (docs: Document[]) => {
|
145 |
+
return docs
|
146 |
+
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
|
147 |
+
.join('\n');
|
148 |
+
};
|
149 |
+
|
150 |
+
const rerankDocs = async ({
|
151 |
+
query,
|
152 |
+
docs,
|
153 |
+
}: {
|
154 |
+
query: string;
|
155 |
+
docs: Document[];
|
156 |
+
}) => {
|
157 |
+
if (docs.length === 0) {
|
158 |
+
return docs;
|
159 |
+
}
|
160 |
+
|
161 |
+
const docsWithContent = docs.filter(
|
162 |
+
(doc) => doc.pageContent && doc.pageContent.length > 0,
|
163 |
+
);
|
164 |
+
|
165 |
+
const [docEmbeddings, queryEmbedding] = await Promise.all([
|
166 |
+
embeddings.embedDocuments(docsWithContent.map((doc) => doc.pageContent)),
|
167 |
+
embeddings.embedQuery(query),
|
168 |
+
]);
|
169 |
+
|
170 |
+
const similarity = docEmbeddings.map((docEmbedding, i) => {
|
171 |
+
const sim = computeSimilarity(queryEmbedding, docEmbedding);
|
172 |
+
|
173 |
+
return {
|
174 |
+
index: i,
|
175 |
+
similarity: sim,
|
176 |
+
};
|
177 |
+
});
|
178 |
+
|
179 |
+
const sortedDocs = similarity
|
180 |
+
.sort((a, b) => b.similarity - a.similarity)
|
181 |
+
.slice(0, 15)
|
182 |
+
.filter((sim) => sim.similarity > 0.3)
|
183 |
+
.map((sim) => docsWithContent[sim.index]);
|
184 |
+
|
185 |
+
return sortedDocs;
|
186 |
+
};
|
187 |
+
|
188 |
+
return RunnableSequence.from([
|
189 |
+
RunnableMap.from({
|
190 |
+
query: (input: BasicChainInput) => input.query,
|
191 |
+
chat_history: (input: BasicChainInput) => input.chat_history,
|
192 |
+
context: RunnableSequence.from([
|
193 |
+
(input) => ({
|
194 |
+
query: input.query,
|
195 |
+
chat_history: formatChatHistoryAsString(input.chat_history),
|
196 |
+
}),
|
197 |
+
basicRedditSearchRetrieverChain
|
198 |
+
.pipe(rerankDocs)
|
199 |
+
.withConfig({
|
200 |
+
runName: 'FinalSourceRetriever',
|
201 |
+
})
|
202 |
+
.pipe(processDocs),
|
203 |
+
]),
|
204 |
+
}),
|
205 |
+
ChatPromptTemplate.fromMessages([
|
206 |
+
['system', basicRedditSearchResponsePrompt],
|
207 |
+
new MessagesPlaceholder('chat_history'),
|
208 |
+
['user', '{query}'],
|
209 |
+
]),
|
210 |
+
llm,
|
211 |
+
strParser,
|
212 |
+
]).withConfig({
|
213 |
+
runName: 'FinalResponseGenerator',
|
214 |
+
});
|
215 |
+
};
|
216 |
+
|
217 |
+
const basicRedditSearch = (
|
218 |
+
query: string,
|
219 |
+
history: BaseMessage[],
|
220 |
+
llm: BaseChatModel,
|
221 |
+
embeddings: Embeddings,
|
222 |
+
) => {
|
223 |
+
const emitter = new eventEmitter();
|
224 |
+
|
225 |
+
try {
|
226 |
+
const basicRedditSearchAnsweringChain =
|
227 |
+
createBasicRedditSearchAnsweringChain(llm, embeddings);
|
228 |
+
const stream = basicRedditSearchAnsweringChain.streamEvents(
|
229 |
+
{
|
230 |
+
chat_history: history,
|
231 |
+
query: query,
|
232 |
+
},
|
233 |
+
{
|
234 |
+
version: 'v1',
|
235 |
+
},
|
236 |
+
);
|
237 |
+
|
238 |
+
handleStream(stream, emitter);
|
239 |
+
} catch (err) {
|
240 |
+
emitter.emit(
|
241 |
+
'error',
|
242 |
+
JSON.stringify({ data: 'An error has occurred please try again later' }),
|
243 |
+
);
|
244 |
+
logger.error(`Error in RedditSearch: ${err}`);
|
245 |
+
}
|
246 |
+
|
247 |
+
return emitter;
|
248 |
+
};
|
249 |
+
|
250 |
+
const handleRedditSearch = (
|
251 |
+
message: string,
|
252 |
+
history: BaseMessage[],
|
253 |
+
llm: BaseChatModel,
|
254 |
+
embeddings: Embeddings,
|
255 |
+
) => {
|
256 |
+
const emitter = basicRedditSearch(message, history, llm, embeddings);
|
257 |
+
return emitter;
|
258 |
+
};
|
259 |
+
|
260 |
+
export default handleRedditSearch;
|
src/agents/videoSearchAgent.ts
ADDED
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {
|
2 |
+
RunnableSequence,
|
3 |
+
RunnableMap,
|
4 |
+
RunnableLambda,
|
5 |
+
} from '@langchain/core/runnables';
|
6 |
+
import { PromptTemplate } from '@langchain/core/prompts';
|
7 |
+
import formatChatHistoryAsString from '../utils/formatHistory';
|
8 |
+
import { BaseMessage } from '@langchain/core/messages';
|
9 |
+
import { StringOutputParser } from '@langchain/core/output_parsers';
|
10 |
+
import { searchSearxng } from '../lib/searxng';
|
11 |
+
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
12 |
+
|
13 |
+
const VideoSearchChainPrompt = `
|
14 |
+
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.
|
15 |
+
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
|
16 |
+
|
17 |
+
Example:
|
18 |
+
1. Follow up question: How does a car work?
|
19 |
+
Rephrased: How does a car work?
|
20 |
+
|
21 |
+
2. Follow up question: What is the theory of relativity?
|
22 |
+
Rephrased: What is theory of relativity
|
23 |
+
|
24 |
+
3. Follow up question: How does an AC work?
|
25 |
+
Rephrased: How does an AC work
|
26 |
+
|
27 |
+
Conversation:
|
28 |
+
{chat_history}
|
29 |
+
|
30 |
+
Follow up question: {query}
|
31 |
+
Rephrased question:
|
32 |
+
`;
|
33 |
+
|
34 |
+
type VideoSearchChainInput = {
|
35 |
+
chat_history: BaseMessage[];
|
36 |
+
query: string;
|
37 |
+
};
|
38 |
+
|
39 |
+
const strParser = new StringOutputParser();
|
40 |
+
|
41 |
+
const createVideoSearchChain = (llm: BaseChatModel) => {
|
42 |
+
return RunnableSequence.from([
|
43 |
+
RunnableMap.from({
|
44 |
+
chat_history: (input: VideoSearchChainInput) => {
|
45 |
+
return formatChatHistoryAsString(input.chat_history);
|
46 |
+
},
|
47 |
+
query: (input: VideoSearchChainInput) => {
|
48 |
+
return input.query;
|
49 |
+
},
|
50 |
+
}),
|
51 |
+
PromptTemplate.fromTemplate(VideoSearchChainPrompt),
|
52 |
+
llm,
|
53 |
+
strParser,
|
54 |
+
RunnableLambda.from(async (input: string) => {
|
55 |
+
const res = await searchSearxng(input, {
|
56 |
+
engines: ['youtube'],
|
57 |
+
});
|
58 |
+
|
59 |
+
const videos = [];
|
60 |
+
|
61 |
+
res.results.forEach((result) => {
|
62 |
+
if (
|
63 |
+
result.thumbnail &&
|
64 |
+
result.url &&
|
65 |
+
result.title &&
|
66 |
+
result.iframe_src
|
67 |
+
) {
|
68 |
+
videos.push({
|
69 |
+
img_src: result.thumbnail,
|
70 |
+
url: result.url,
|
71 |
+
title: result.title,
|
72 |
+
iframe_src: result.iframe_src,
|
73 |
+
});
|
74 |
+
}
|
75 |
+
});
|
76 |
+
|
77 |
+
return videos.slice(0, 10);
|
78 |
+
}),
|
79 |
+
]);
|
80 |
+
};
|
81 |
+
|
82 |
+
const handleVideoSearch = (
|
83 |
+
input: VideoSearchChainInput,
|
84 |
+
llm: BaseChatModel,
|
85 |
+
) => {
|
86 |
+
const VideoSearchChain = createVideoSearchChain(llm);
|
87 |
+
return VideoSearchChain.invoke(input);
|
88 |
+
};
|
89 |
+
|
90 |
+
export default handleVideoSearch;
|
src/agents/webSearchAgent.ts
ADDED
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { BaseMessage } from '@langchain/core/messages';
|
2 |
+
import {
|
3 |
+
PromptTemplate,
|
4 |
+
ChatPromptTemplate,
|
5 |
+
MessagesPlaceholder,
|
6 |
+
} from '@langchain/core/prompts';
|
7 |
+
import {
|
8 |
+
RunnableSequence,
|
9 |
+
RunnableMap,
|
10 |
+
RunnableLambda,
|
11 |
+
} from '@langchain/core/runnables';
|
12 |
+
import { StringOutputParser } from '@langchain/core/output_parsers';
|
13 |
+
import { Document } from '@langchain/core/documents';
|
14 |
+
import { searchSearxng } from '../lib/searxng';
|
15 |
+
import type { StreamEvent } from '@langchain/core/tracers/log_stream';
|
16 |
+
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
17 |
+
import type { Embeddings } from '@langchain/core/embeddings';
|
18 |
+
import formatChatHistoryAsString from '../utils/formatHistory';
|
19 |
+
import eventEmitter from 'events';
|
20 |
+
import computeSimilarity from '../utils/computeSimilarity';
|
21 |
+
import logger from '../utils/logger';
|
22 |
+
|
23 |
+
const basicSearchRetrieverPrompt = `
|
24 |
+
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.
|
25 |
+
If it is a writing task or a simple hi, hello rather than a question, you need to return \`not_needed\` as the response.
|
26 |
+
|
27 |
+
Example:
|
28 |
+
1. Follow up question: What is the capital of France?
|
29 |
+
Rephrased: Capital of france
|
30 |
+
|
31 |
+
2. Follow up question: What is the population of New York City?
|
32 |
+
Rephrased: Population of New York City
|
33 |
+
|
34 |
+
3. Follow up question: What is Docker?
|
35 |
+
Rephrased: What is Docker
|
36 |
+
|
37 |
+
Conversation:
|
38 |
+
{chat_history}
|
39 |
+
|
40 |
+
Follow up question: {query}
|
41 |
+
Rephrased question:
|
42 |
+
`;
|
43 |
+
|
44 |
+
const basicWebSearchResponsePrompt = `
|
45 |
+
You are Perplexica, an AI model who is expert at searching the web and answering user's queries.
|
46 |
+
|
47 |
+
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).
|
48 |
+
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.
|
49 |
+
You must not tell the user to open any link or visit any website to get the answer. You must provide the answer in the response itself. If the user asks for links you can provide them.
|
50 |
+
Your responses should be medium to long in length be informative and relevant to the user's query. You can use markdowns to format your response. You should use bullet points to list the information. Make sure the answer is not short and is informative.
|
51 |
+
You have to cite the answer using [number] notation. You must cite the sentences with their relevent context number. You must cite each and every part of the answer so the user can know where the information is coming from.
|
52 |
+
Place these citations at the end of that particular sentence. You can cite the same sentence multiple times if it is relevant to the user's query like [number1][number2].
|
53 |
+
However you do not need to cite it using the same number. You can use different numbers to cite the same sentence multiple times. The number refers to the number of the search result (passed in the context) used to generate that part of the answer.
|
54 |
+
|
55 |
+
Aything inside the following \`context\` HTML block provided below is for your knowledge returned by the search engine and is not shared by the user. You have to answer question on the basis of it and cite the relevant information from it but you do not have to
|
56 |
+
talk about the context in your response.
|
57 |
+
|
58 |
+
<context>
|
59 |
+
{context}
|
60 |
+
</context>
|
61 |
+
|
62 |
+
If you think there's nothing relevant in the search results, you can say that 'Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?'.
|
63 |
+
Anything between the \`context\` is retrieved from a search engine and is not a part of the conversation with the user. Today's date is ${new Date().toISOString()}
|
64 |
+
`;
|
65 |
+
|
66 |
+
const strParser = new StringOutputParser();
|
67 |
+
|
68 |
+
const handleStream = async (
|
69 |
+
stream: AsyncGenerator<StreamEvent, any, unknown>,
|
70 |
+
emitter: eventEmitter,
|
71 |
+
) => {
|
72 |
+
for await (const event of stream) {
|
73 |
+
if (
|
74 |
+
event.event === 'on_chain_end' &&
|
75 |
+
event.name === 'FinalSourceRetriever'
|
76 |
+
) {
|
77 |
+
emitter.emit(
|
78 |
+
'data',
|
79 |
+
JSON.stringify({ type: 'sources', data: event.data.output }),
|
80 |
+
);
|
81 |
+
}
|
82 |
+
if (
|
83 |
+
event.event === 'on_chain_stream' &&
|
84 |
+
event.name === 'FinalResponseGenerator'
|
85 |
+
) {
|
86 |
+
emitter.emit(
|
87 |
+
'data',
|
88 |
+
JSON.stringify({ type: 'response', data: event.data.chunk }),
|
89 |
+
);
|
90 |
+
}
|
91 |
+
if (
|
92 |
+
event.event === 'on_chain_end' &&
|
93 |
+
event.name === 'FinalResponseGenerator'
|
94 |
+
) {
|
95 |
+
emitter.emit('end');
|
96 |
+
}
|
97 |
+
}
|
98 |
+
};
|
99 |
+
|
100 |
+
type BasicChainInput = {
|
101 |
+
chat_history: BaseMessage[];
|
102 |
+
query: string;
|
103 |
+
};
|
104 |
+
|
105 |
+
const createBasicWebSearchRetrieverChain = (llm: BaseChatModel) => {
|
106 |
+
return RunnableSequence.from([
|
107 |
+
PromptTemplate.fromTemplate(basicSearchRetrieverPrompt),
|
108 |
+
llm,
|
109 |
+
strParser,
|
110 |
+
RunnableLambda.from(async (input: string) => {
|
111 |
+
if (input === 'not_needed') {
|
112 |
+
return { query: '', docs: [] };
|
113 |
+
}
|
114 |
+
|
115 |
+
const res = await searchSearxng(input, {
|
116 |
+
language: 'en',
|
117 |
+
});
|
118 |
+
|
119 |
+
const documents = res.results.map(
|
120 |
+
(result) =>
|
121 |
+
new Document({
|
122 |
+
pageContent: result.content,
|
123 |
+
metadata: {
|
124 |
+
title: result.title,
|
125 |
+
url: result.url,
|
126 |
+
...(result.img_src && { img_src: result.img_src }),
|
127 |
+
},
|
128 |
+
}),
|
129 |
+
);
|
130 |
+
|
131 |
+
return { query: input, docs: documents };
|
132 |
+
}),
|
133 |
+
]);
|
134 |
+
};
|
135 |
+
|
136 |
+
const createBasicWebSearchAnsweringChain = (
|
137 |
+
llm: BaseChatModel,
|
138 |
+
embeddings: Embeddings,
|
139 |
+
) => {
|
140 |
+
const basicWebSearchRetrieverChain = createBasicWebSearchRetrieverChain(llm);
|
141 |
+
|
142 |
+
const processDocs = async (docs: Document[]) => {
|
143 |
+
return docs
|
144 |
+
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
|
145 |
+
.join('\n');
|
146 |
+
};
|
147 |
+
|
148 |
+
const rerankDocs = async ({
|
149 |
+
query,
|
150 |
+
docs,
|
151 |
+
}: {
|
152 |
+
query: string;
|
153 |
+
docs: Document[];
|
154 |
+
}) => {
|
155 |
+
if (docs.length === 0) {
|
156 |
+
return docs;
|
157 |
+
}
|
158 |
+
|
159 |
+
const docsWithContent = docs.filter(
|
160 |
+
(doc) => doc.pageContent && doc.pageContent.length > 0,
|
161 |
+
);
|
162 |
+
|
163 |
+
const [docEmbeddings, queryEmbedding] = await Promise.all([
|
164 |
+
embeddings.embedDocuments(docsWithContent.map((doc) => doc.pageContent)),
|
165 |
+
embeddings.embedQuery(query),
|
166 |
+
]);
|
167 |
+
|
168 |
+
const similarity = docEmbeddings.map((docEmbedding, i) => {
|
169 |
+
const sim = computeSimilarity(queryEmbedding, docEmbedding);
|
170 |
+
|
171 |
+
return {
|
172 |
+
index: i,
|
173 |
+
similarity: sim,
|
174 |
+
};
|
175 |
+
});
|
176 |
+
|
177 |
+
const sortedDocs = similarity
|
178 |
+
.sort((a, b) => b.similarity - a.similarity)
|
179 |
+
.filter((sim) => sim.similarity > 0.5)
|
180 |
+
.slice(0, 15)
|
181 |
+
.map((sim) => docsWithContent[sim.index]);
|
182 |
+
|
183 |
+
return sortedDocs;
|
184 |
+
};
|
185 |
+
|
186 |
+
return RunnableSequence.from([
|
187 |
+
RunnableMap.from({
|
188 |
+
query: (input: BasicChainInput) => input.query,
|
189 |
+
chat_history: (input: BasicChainInput) => input.chat_history,
|
190 |
+
context: RunnableSequence.from([
|
191 |
+
(input) => ({
|
192 |
+
query: input.query,
|
193 |
+
chat_history: formatChatHistoryAsString(input.chat_history),
|
194 |
+
}),
|
195 |
+
basicWebSearchRetrieverChain
|
196 |
+
.pipe(rerankDocs)
|
197 |
+
.withConfig({
|
198 |
+
runName: 'FinalSourceRetriever',
|
199 |
+
})
|
200 |
+
.pipe(processDocs),
|
201 |
+
]),
|
202 |
+
}),
|
203 |
+
ChatPromptTemplate.fromMessages([
|
204 |
+
['system', basicWebSearchResponsePrompt],
|
205 |
+
new MessagesPlaceholder('chat_history'),
|
206 |
+
['user', '{query}'],
|
207 |
+
]),
|
208 |
+
llm,
|
209 |
+
strParser,
|
210 |
+
]).withConfig({
|
211 |
+
runName: 'FinalResponseGenerator',
|
212 |
+
});
|
213 |
+
};
|
214 |
+
|
215 |
+
const basicWebSearch = (
|
216 |
+
query: string,
|
217 |
+
history: BaseMessage[],
|
218 |
+
llm: BaseChatModel,
|
219 |
+
embeddings: Embeddings,
|
220 |
+
) => {
|
221 |
+
const emitter = new eventEmitter();
|
222 |
+
|
223 |
+
try {
|
224 |
+
const basicWebSearchAnsweringChain = createBasicWebSearchAnsweringChain(
|
225 |
+
llm,
|
226 |
+
embeddings,
|
227 |
+
);
|
228 |
+
|
229 |
+
const stream = basicWebSearchAnsweringChain.streamEvents(
|
230 |
+
{
|
231 |
+
chat_history: history,
|
232 |
+
query: query,
|
233 |
+
},
|
234 |
+
{
|
235 |
+
version: 'v1',
|
236 |
+
},
|
237 |
+
);
|
238 |
+
|
239 |
+
handleStream(stream, emitter);
|
240 |
+
} catch (err) {
|
241 |
+
emitter.emit(
|
242 |
+
'error',
|
243 |
+
JSON.stringify({ data: 'An error has occurred please try again later' }),
|
244 |
+
);
|
245 |
+
logger.error(`Error in websearch: ${err}`);
|
246 |
+
}
|
247 |
+
|
248 |
+
return emitter;
|
249 |
+
};
|
250 |
+
|
251 |
+
const handleWebSearch = (
|
252 |
+
message: string,
|
253 |
+
history: BaseMessage[],
|
254 |
+
llm: BaseChatModel,
|
255 |
+
embeddings: Embeddings,
|
256 |
+
) => {
|
257 |
+
const emitter = basicWebSearch(message, history, llm, embeddings);
|
258 |
+
return emitter;
|
259 |
+
};
|
260 |
+
|
261 |
+
export default handleWebSearch;
|
src/agents/wolframAlphaSearchAgent.ts
ADDED
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { BaseMessage } from '@langchain/core/messages';
|
2 |
+
import {
|
3 |
+
PromptTemplate,
|
4 |
+
ChatPromptTemplate,
|
5 |
+
MessagesPlaceholder,
|
6 |
+
} from '@langchain/core/prompts';
|
7 |
+
import {
|
8 |
+
RunnableSequence,
|
9 |
+
RunnableMap,
|
10 |
+
RunnableLambda,
|
11 |
+
} from '@langchain/core/runnables';
|
12 |
+
import { StringOutputParser } from '@langchain/core/output_parsers';
|
13 |
+
import { Document } from '@langchain/core/documents';
|
14 |
+
import { searchSearxng } from '../lib/searxng';
|
15 |
+
import type { StreamEvent } from '@langchain/core/tracers/log_stream';
|
16 |
+
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
17 |
+
import type { Embeddings } from '@langchain/core/embeddings';
|
18 |
+
import formatChatHistoryAsString from '../utils/formatHistory';
|
19 |
+
import eventEmitter from 'events';
|
20 |
+
import logger from '../utils/logger';
|
21 |
+
|
22 |
+
const basicWolframAlphaSearchRetrieverPrompt = `
|
23 |
+
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.
|
24 |
+
If it is a writing task or a simple hi, hello rather than a question, you need to return \`not_needed\` as the response.
|
25 |
+
|
26 |
+
Example:
|
27 |
+
1. Follow up question: What is the atomic radius of S?
|
28 |
+
Rephrased: Atomic radius of S
|
29 |
+
|
30 |
+
2. Follow up question: What is linear algebra?
|
31 |
+
Rephrased: Linear algebra
|
32 |
+
|
33 |
+
3. Follow up question: What is the third law of thermodynamics?
|
34 |
+
Rephrased: Third law of thermodynamics
|
35 |
+
|
36 |
+
Conversation:
|
37 |
+
{chat_history}
|
38 |
+
|
39 |
+
Follow up question: {query}
|
40 |
+
Rephrased question:
|
41 |
+
`;
|
42 |
+
|
43 |
+
const basicWolframAlphaSearchResponsePrompt = `
|
44 |
+
You are Perplexica, an AI model who is expert at searching the web and answering user's queries. You are set on focus mode 'Wolfram Alpha', this means you will be searching for information on the web using Wolfram Alpha. It is a computational knowledge engine that can answer factual queries and perform computations.
|
45 |
+
|
46 |
+
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).
|
47 |
+
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.
|
48 |
+
You must not tell the user to open any link or visit any website to get the answer. You must provide the answer in the response itself. If the user asks for links you can provide them.
|
49 |
+
Your responses should be medium to long in length be informative and relevant to the user's query. You can use markdowns to format your response. You should use bullet points to list the information. Make sure the answer is not short and is informative.
|
50 |
+
You have to cite the answer using [number] notation. You must cite the sentences with their relevent context number. You must cite each and every part of the answer so the user can know where the information is coming from.
|
51 |
+
Place these citations at the end of that particular sentence. You can cite the same sentence multiple times if it is relevant to the user's query like [number1][number2].
|
52 |
+
However you do not need to cite it using the same number. You can use different numbers to cite the same sentence multiple times. The number refers to the number of the search result (passed in the context) used to generate that part of the answer.
|
53 |
+
|
54 |
+
Aything inside the following \`context\` HTML block provided below is for your knowledge returned by Wolfram Alpha and is not shared by the user. You have to answer question on the basis of it and cite the relevant information from it but you do not have to
|
55 |
+
talk about the context in your response.
|
56 |
+
|
57 |
+
<context>
|
58 |
+
{context}
|
59 |
+
</context>
|
60 |
+
|
61 |
+
If you think there's nothing relevant in the search results, you can say that 'Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?'.
|
62 |
+
Anything between the \`context\` is retrieved from Wolfram Alpha and is not a part of the conversation with the user. Today's date is ${new Date().toISOString()}
|
63 |
+
`;
|
64 |
+
|
65 |
+
const strParser = new StringOutputParser();
|
66 |
+
|
67 |
+
const handleStream = async (
|
68 |
+
stream: AsyncGenerator<StreamEvent, any, unknown>,
|
69 |
+
emitter: eventEmitter,
|
70 |
+
) => {
|
71 |
+
for await (const event of stream) {
|
72 |
+
if (
|
73 |
+
event.event === 'on_chain_end' &&
|
74 |
+
event.name === 'FinalSourceRetriever'
|
75 |
+
) {
|
76 |
+
emitter.emit(
|
77 |
+
'data',
|
78 |
+
JSON.stringify({ type: 'sources', data: event.data.output }),
|
79 |
+
);
|
80 |
+
}
|
81 |
+
if (
|
82 |
+
event.event === 'on_chain_stream' &&
|
83 |
+
event.name === 'FinalResponseGenerator'
|
84 |
+
) {
|
85 |
+
emitter.emit(
|
86 |
+
'data',
|
87 |
+
JSON.stringify({ type: 'response', data: event.data.chunk }),
|
88 |
+
);
|
89 |
+
}
|
90 |
+
if (
|
91 |
+
event.event === 'on_chain_end' &&
|
92 |
+
event.name === 'FinalResponseGenerator'
|
93 |
+
) {
|
94 |
+
emitter.emit('end');
|
95 |
+
}
|
96 |
+
}
|
97 |
+
};
|
98 |
+
|
99 |
+
type BasicChainInput = {
|
100 |
+
chat_history: BaseMessage[];
|
101 |
+
query: string;
|
102 |
+
};
|
103 |
+
|
104 |
+
const createBasicWolframAlphaSearchRetrieverChain = (llm: BaseChatModel) => {
|
105 |
+
return RunnableSequence.from([
|
106 |
+
PromptTemplate.fromTemplate(basicWolframAlphaSearchRetrieverPrompt),
|
107 |
+
llm,
|
108 |
+
strParser,
|
109 |
+
RunnableLambda.from(async (input: string) => {
|
110 |
+
if (input === 'not_needed') {
|
111 |
+
return { query: '', docs: [] };
|
112 |
+
}
|
113 |
+
|
114 |
+
const res = await searchSearxng(input, {
|
115 |
+
language: 'en',
|
116 |
+
engines: ['wolframalpha'],
|
117 |
+
});
|
118 |
+
|
119 |
+
const documents = res.results.map(
|
120 |
+
(result) =>
|
121 |
+
new Document({
|
122 |
+
pageContent: result.content,
|
123 |
+
metadata: {
|
124 |
+
title: result.title,
|
125 |
+
url: result.url,
|
126 |
+
...(result.img_src && { img_src: result.img_src }),
|
127 |
+
},
|
128 |
+
}),
|
129 |
+
);
|
130 |
+
|
131 |
+
return { query: input, docs: documents };
|
132 |
+
}),
|
133 |
+
]);
|
134 |
+
};
|
135 |
+
|
136 |
+
const createBasicWolframAlphaSearchAnsweringChain = (llm: BaseChatModel) => {
|
137 |
+
const basicWolframAlphaSearchRetrieverChain =
|
138 |
+
createBasicWolframAlphaSearchRetrieverChain(llm);
|
139 |
+
|
140 |
+
const processDocs = (docs: Document[]) => {
|
141 |
+
return docs
|
142 |
+
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
|
143 |
+
.join('\n');
|
144 |
+
};
|
145 |
+
|
146 |
+
return RunnableSequence.from([
|
147 |
+
RunnableMap.from({
|
148 |
+
query: (input: BasicChainInput) => input.query,
|
149 |
+
chat_history: (input: BasicChainInput) => input.chat_history,
|
150 |
+
context: RunnableSequence.from([
|
151 |
+
(input) => ({
|
152 |
+
query: input.query,
|
153 |
+
chat_history: formatChatHistoryAsString(input.chat_history),
|
154 |
+
}),
|
155 |
+
basicWolframAlphaSearchRetrieverChain
|
156 |
+
.pipe(({ query, docs }) => {
|
157 |
+
return docs;
|
158 |
+
})
|
159 |
+
.withConfig({
|
160 |
+
runName: 'FinalSourceRetriever',
|
161 |
+
})
|
162 |
+
.pipe(processDocs),
|
163 |
+
]),
|
164 |
+
}),
|
165 |
+
ChatPromptTemplate.fromMessages([
|
166 |
+
['system', basicWolframAlphaSearchResponsePrompt],
|
167 |
+
new MessagesPlaceholder('chat_history'),
|
168 |
+
['user', '{query}'],
|
169 |
+
]),
|
170 |
+
llm,
|
171 |
+
strParser,
|
172 |
+
]).withConfig({
|
173 |
+
runName: 'FinalResponseGenerator',
|
174 |
+
});
|
175 |
+
};
|
176 |
+
|
177 |
+
const basicWolframAlphaSearch = (
|
178 |
+
query: string,
|
179 |
+
history: BaseMessage[],
|
180 |
+
llm: BaseChatModel,
|
181 |
+
) => {
|
182 |
+
const emitter = new eventEmitter();
|
183 |
+
|
184 |
+
try {
|
185 |
+
const basicWolframAlphaSearchAnsweringChain =
|
186 |
+
createBasicWolframAlphaSearchAnsweringChain(llm);
|
187 |
+
const stream = basicWolframAlphaSearchAnsweringChain.streamEvents(
|
188 |
+
{
|
189 |
+
chat_history: history,
|
190 |
+
query: query,
|
191 |
+
},
|
192 |
+
{
|
193 |
+
version: 'v1',
|
194 |
+
},
|
195 |
+
);
|
196 |
+
|
197 |
+
handleStream(stream, emitter);
|
198 |
+
} catch (err) {
|
199 |
+
emitter.emit(
|
200 |
+
'error',
|
201 |
+
JSON.stringify({ data: 'An error has occurred please try again later' }),
|
202 |
+
);
|
203 |
+
logger.error(`Error in WolframAlphaSearch: ${err}`);
|
204 |
+
}
|
205 |
+
|
206 |
+
return emitter;
|
207 |
+
};
|
208 |
+
|
209 |
+
const handleWolframAlphaSearch = (
|
210 |
+
message: string,
|
211 |
+
history: BaseMessage[],
|
212 |
+
llm: BaseChatModel,
|
213 |
+
embeddings: Embeddings,
|
214 |
+
) => {
|
215 |
+
const emitter = basicWolframAlphaSearch(message, history, llm);
|
216 |
+
return emitter;
|
217 |
+
};
|
218 |
+
|
219 |
+
export default handleWolframAlphaSearch;
|
src/agents/writingAssistant.ts
ADDED
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { BaseMessage } from '@langchain/core/messages';
|
2 |
+
import {
|
3 |
+
ChatPromptTemplate,
|
4 |
+
MessagesPlaceholder,
|
5 |
+
} from '@langchain/core/prompts';
|
6 |
+
import { RunnableSequence } from '@langchain/core/runnables';
|
7 |
+
import { StringOutputParser } from '@langchain/core/output_parsers';
|
8 |
+
import type { StreamEvent } from '@langchain/core/tracers/log_stream';
|
9 |
+
import eventEmitter from 'events';
|
10 |
+
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
11 |
+
import type { Embeddings } from '@langchain/core/embeddings';
|
12 |
+
import logger from '../utils/logger';
|
13 |
+
|
14 |
+
const writingAssistantPrompt = `
|
15 |
+
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.
|
16 |
+
Since you are a writing assistant, you would not perform web searches. If you think you lack information to answer the query, you can ask the user for more information or suggest them to switch to a different focus mode.
|
17 |
+
`;
|
18 |
+
|
19 |
+
const strParser = new StringOutputParser();
|
20 |
+
|
21 |
+
const handleStream = async (
|
22 |
+
stream: AsyncGenerator<StreamEvent, any, unknown>,
|
23 |
+
emitter: eventEmitter,
|
24 |
+
) => {
|
25 |
+
for await (const event of stream) {
|
26 |
+
if (
|
27 |
+
event.event === 'on_chain_stream' &&
|
28 |
+
event.name === 'FinalResponseGenerator'
|
29 |
+
) {
|
30 |
+
emitter.emit(
|
31 |
+
'data',
|
32 |
+
JSON.stringify({ type: 'response', data: event.data.chunk }),
|
33 |
+
);
|
34 |
+
}
|
35 |
+
if (
|
36 |
+
event.event === 'on_chain_end' &&
|
37 |
+
event.name === 'FinalResponseGenerator'
|
38 |
+
) {
|
39 |
+
emitter.emit('end');
|
40 |
+
}
|
41 |
+
}
|
42 |
+
};
|
43 |
+
|
44 |
+
const createWritingAssistantChain = (llm: BaseChatModel) => {
|
45 |
+
return RunnableSequence.from([
|
46 |
+
ChatPromptTemplate.fromMessages([
|
47 |
+
['system', writingAssistantPrompt],
|
48 |
+
new MessagesPlaceholder('chat_history'),
|
49 |
+
['user', '{query}'],
|
50 |
+
]),
|
51 |
+
llm,
|
52 |
+
strParser,
|
53 |
+
]).withConfig({
|
54 |
+
runName: 'FinalResponseGenerator',
|
55 |
+
});
|
56 |
+
};
|
57 |
+
|
58 |
+
const handleWritingAssistant = (
|
59 |
+
query: string,
|
60 |
+
history: BaseMessage[],
|
61 |
+
llm: BaseChatModel,
|
62 |
+
embeddings: Embeddings,
|
63 |
+
) => {
|
64 |
+
const emitter = new eventEmitter();
|
65 |
+
|
66 |
+
try {
|
67 |
+
const writingAssistantChain = createWritingAssistantChain(llm);
|
68 |
+
const stream = writingAssistantChain.streamEvents(
|
69 |
+
{
|
70 |
+
chat_history: history,
|
71 |
+
query: query,
|
72 |
+
},
|
73 |
+
{
|
74 |
+
version: 'v1',
|
75 |
+
},
|
76 |
+
);
|
77 |
+
|
78 |
+
handleStream(stream, emitter);
|
79 |
+
} catch (err) {
|
80 |
+
emitter.emit(
|
81 |
+
'error',
|
82 |
+
JSON.stringify({ data: 'An error has occurred please try again later' }),
|
83 |
+
);
|
84 |
+
logger.error(`Error in writing assistant: ${err}`);
|
85 |
+
}
|
86 |
+
|
87 |
+
return emitter;
|
88 |
+
};
|
89 |
+
|
90 |
+
export default handleWritingAssistant;
|
src/agents/youtubeSearchAgent.ts
ADDED
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { BaseMessage } from '@langchain/core/messages';
|
2 |
+
import {
|
3 |
+
PromptTemplate,
|
4 |
+
ChatPromptTemplate,
|
5 |
+
MessagesPlaceholder,
|
6 |
+
} from '@langchain/core/prompts';
|
7 |
+
import {
|
8 |
+
RunnableSequence,
|
9 |
+
RunnableMap,
|
10 |
+
RunnableLambda,
|
11 |
+
} from '@langchain/core/runnables';
|
12 |
+
import { StringOutputParser } from '@langchain/core/output_parsers';
|
13 |
+
import { Document } from '@langchain/core/documents';
|
14 |
+
import { searchSearxng } from '../lib/searxng';
|
15 |
+
import type { StreamEvent } from '@langchain/core/tracers/log_stream';
|
16 |
+
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
17 |
+
import type { Embeddings } from '@langchain/core/embeddings';
|
18 |
+
import formatChatHistoryAsString from '../utils/formatHistory';
|
19 |
+
import eventEmitter from 'events';
|
20 |
+
import computeSimilarity from '../utils/computeSimilarity';
|
21 |
+
import logger from '../utils/logger';
|
22 |
+
|
23 |
+
const basicYoutubeSearchRetrieverPrompt = `
|
24 |
+
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.
|
25 |
+
If it is a writing task or a simple hi, hello rather than a question, you need to return \`not_needed\` as the response.
|
26 |
+
|
27 |
+
Example:
|
28 |
+
1. Follow up question: How does an A.C work?
|
29 |
+
Rephrased: A.C working
|
30 |
+
|
31 |
+
2. Follow up question: Linear algebra explanation video
|
32 |
+
Rephrased: What is linear algebra?
|
33 |
+
|
34 |
+
3. Follow up question: What is theory of relativity?
|
35 |
+
Rephrased: What is theory of relativity?
|
36 |
+
|
37 |
+
Conversation:
|
38 |
+
{chat_history}
|
39 |
+
|
40 |
+
Follow up question: {query}
|
41 |
+
Rephrased question:
|
42 |
+
`;
|
43 |
+
|
44 |
+
const basicYoutubeSearchResponsePrompt = `
|
45 |
+
You are Perplexica, an AI model who is expert at searching the web and answering user's queries. You are set on focus mode 'Youtube', this means you will be searching for videos on the web using Youtube and providing information based on the video's transcript.
|
46 |
+
|
47 |
+
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).
|
48 |
+
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.
|
49 |
+
You must not tell the user to open any link or visit any website to get the answer. You must provide the answer in the response itself. If the user asks for links you can provide them.
|
50 |
+
Your responses should be medium to long in length be informative and relevant to the user's query. You can use markdowns to format your response. You should use bullet points to list the information. Make sure the answer is not short and is informative.
|
51 |
+
You have to cite the answer using [number] notation. You must cite the sentences with their relevent context number. You must cite each and every part of the answer so the user can know where the information is coming from.
|
52 |
+
Place these citations at the end of that particular sentence. You can cite the same sentence multiple times if it is relevant to the user's query like [number1][number2].
|
53 |
+
However you do not need to cite it using the same number. You can use different numbers to cite the same sentence multiple times. The number refers to the number of the search result (passed in the context) used to generate that part of the answer.
|
54 |
+
|
55 |
+
Aything inside the following \`context\` HTML block provided below is for your knowledge returned by Youtube and is not shared by the user. You have to answer question on the basis of it and cite the relevant information from it but you do not have to
|
56 |
+
talk about the context in your response.
|
57 |
+
|
58 |
+
<context>
|
59 |
+
{context}
|
60 |
+
</context>
|
61 |
+
|
62 |
+
If you think there's nothing relevant in the search results, you can say that 'Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?'.
|
63 |
+
Anything between the \`context\` is retrieved from Youtube and is not a part of the conversation with the user. Today's date is ${new Date().toISOString()}
|
64 |
+
`;
|
65 |
+
|
66 |
+
const strParser = new StringOutputParser();
|
67 |
+
|
68 |
+
const handleStream = async (
|
69 |
+
stream: AsyncGenerator<StreamEvent, any, unknown>,
|
70 |
+
emitter: eventEmitter,
|
71 |
+
) => {
|
72 |
+
for await (const event of stream) {
|
73 |
+
if (
|
74 |
+
event.event === 'on_chain_end' &&
|
75 |
+
event.name === 'FinalSourceRetriever'
|
76 |
+
) {
|
77 |
+
emitter.emit(
|
78 |
+
'data',
|
79 |
+
JSON.stringify({ type: 'sources', data: event.data.output }),
|
80 |
+
);
|
81 |
+
}
|
82 |
+
if (
|
83 |
+
event.event === 'on_chain_stream' &&
|
84 |
+
event.name === 'FinalResponseGenerator'
|
85 |
+
) {
|
86 |
+
emitter.emit(
|
87 |
+
'data',
|
88 |
+
JSON.stringify({ type: 'response', data: event.data.chunk }),
|
89 |
+
);
|
90 |
+
}
|
91 |
+
if (
|
92 |
+
event.event === 'on_chain_end' &&
|
93 |
+
event.name === 'FinalResponseGenerator'
|
94 |
+
) {
|
95 |
+
emitter.emit('end');
|
96 |
+
}
|
97 |
+
}
|
98 |
+
};
|
99 |
+
|
100 |
+
type BasicChainInput = {
|
101 |
+
chat_history: BaseMessage[];
|
102 |
+
query: string;
|
103 |
+
};
|
104 |
+
|
105 |
+
const createBasicYoutubeSearchRetrieverChain = (llm: BaseChatModel) => {
|
106 |
+
return RunnableSequence.from([
|
107 |
+
PromptTemplate.fromTemplate(basicYoutubeSearchRetrieverPrompt),
|
108 |
+
llm,
|
109 |
+
strParser,
|
110 |
+
RunnableLambda.from(async (input: string) => {
|
111 |
+
if (input === 'not_needed') {
|
112 |
+
return { query: '', docs: [] };
|
113 |
+
}
|
114 |
+
|
115 |
+
const res = await searchSearxng(input, {
|
116 |
+
language: 'en',
|
117 |
+
engines: ['youtube'],
|
118 |
+
});
|
119 |
+
|
120 |
+
const documents = res.results.map(
|
121 |
+
(result) =>
|
122 |
+
new Document({
|
123 |
+
pageContent: result.content ? result.content : result.title,
|
124 |
+
metadata: {
|
125 |
+
title: result.title,
|
126 |
+
url: result.url,
|
127 |
+
...(result.img_src && { img_src: result.img_src }),
|
128 |
+
},
|
129 |
+
}),
|
130 |
+
);
|
131 |
+
|
132 |
+
return { query: input, docs: documents };
|
133 |
+
}),
|
134 |
+
]);
|
135 |
+
};
|
136 |
+
|
137 |
+
const createBasicYoutubeSearchAnsweringChain = (
|
138 |
+
llm: BaseChatModel,
|
139 |
+
embeddings: Embeddings,
|
140 |
+
) => {
|
141 |
+
const basicYoutubeSearchRetrieverChain =
|
142 |
+
createBasicYoutubeSearchRetrieverChain(llm);
|
143 |
+
|
144 |
+
const processDocs = async (docs: Document[]) => {
|
145 |
+
return docs
|
146 |
+
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
|
147 |
+
.join('\n');
|
148 |
+
};
|
149 |
+
|
150 |
+
const rerankDocs = async ({
|
151 |
+
query,
|
152 |
+
docs,
|
153 |
+
}: {
|
154 |
+
query: string;
|
155 |
+
docs: Document[];
|
156 |
+
}) => {
|
157 |
+
if (docs.length === 0) {
|
158 |
+
return docs;
|
159 |
+
}
|
160 |
+
|
161 |
+
const docsWithContent = docs.filter(
|
162 |
+
(doc) => doc.pageContent && doc.pageContent.length > 0,
|
163 |
+
);
|
164 |
+
|
165 |
+
const [docEmbeddings, queryEmbedding] = await Promise.all([
|
166 |
+
embeddings.embedDocuments(docsWithContent.map((doc) => doc.pageContent)),
|
167 |
+
embeddings.embedQuery(query),
|
168 |
+
]);
|
169 |
+
|
170 |
+
const similarity = docEmbeddings.map((docEmbedding, i) => {
|
171 |
+
const sim = computeSimilarity(queryEmbedding, docEmbedding);
|
172 |
+
|
173 |
+
return {
|
174 |
+
index: i,
|
175 |
+
similarity: sim,
|
176 |
+
};
|
177 |
+
});
|
178 |
+
|
179 |
+
const sortedDocs = similarity
|
180 |
+
.sort((a, b) => b.similarity - a.similarity)
|
181 |
+
.slice(0, 15)
|
182 |
+
.filter((sim) => sim.similarity > 0.3)
|
183 |
+
.map((sim) => docsWithContent[sim.index]);
|
184 |
+
|
185 |
+
return sortedDocs;
|
186 |
+
};
|
187 |
+
|
188 |
+
return RunnableSequence.from([
|
189 |
+
RunnableMap.from({
|
190 |
+
query: (input: BasicChainInput) => input.query,
|
191 |
+
chat_history: (input: BasicChainInput) => input.chat_history,
|
192 |
+
context: RunnableSequence.from([
|
193 |
+
(input) => ({
|
194 |
+
query: input.query,
|
195 |
+
chat_history: formatChatHistoryAsString(input.chat_history),
|
196 |
+
}),
|
197 |
+
basicYoutubeSearchRetrieverChain
|
198 |
+
.pipe(rerankDocs)
|
199 |
+
.withConfig({
|
200 |
+
runName: 'FinalSourceRetriever',
|
201 |
+
})
|
202 |
+
.pipe(processDocs),
|
203 |
+
]),
|
204 |
+
}),
|
205 |
+
ChatPromptTemplate.fromMessages([
|
206 |
+
['system', basicYoutubeSearchResponsePrompt],
|
207 |
+
new MessagesPlaceholder('chat_history'),
|
208 |
+
['user', '{query}'],
|
209 |
+
]),
|
210 |
+
llm,
|
211 |
+
strParser,
|
212 |
+
]).withConfig({
|
213 |
+
runName: 'FinalResponseGenerator',
|
214 |
+
});
|
215 |
+
};
|
216 |
+
|
217 |
+
const basicYoutubeSearch = (
|
218 |
+
query: string,
|
219 |
+
history: BaseMessage[],
|
220 |
+
llm: BaseChatModel,
|
221 |
+
embeddings: Embeddings,
|
222 |
+
) => {
|
223 |
+
const emitter = new eventEmitter();
|
224 |
+
|
225 |
+
try {
|
226 |
+
const basicYoutubeSearchAnsweringChain =
|
227 |
+
createBasicYoutubeSearchAnsweringChain(llm, embeddings);
|
228 |
+
|
229 |
+
const stream = basicYoutubeSearchAnsweringChain.streamEvents(
|
230 |
+
{
|
231 |
+
chat_history: history,
|
232 |
+
query: query,
|
233 |
+
},
|
234 |
+
{
|
235 |
+
version: 'v1',
|
236 |
+
},
|
237 |
+
);
|
238 |
+
|
239 |
+
handleStream(stream, emitter);
|
240 |
+
} catch (err) {
|
241 |
+
emitter.emit(
|
242 |
+
'error',
|
243 |
+
JSON.stringify({ data: 'An error has occurred please try again later' }),
|
244 |
+
);
|
245 |
+
logger.error(`Error in youtube search: ${err}`);
|
246 |
+
}
|
247 |
+
|
248 |
+
return emitter;
|
249 |
+
};
|
250 |
+
|
251 |
+
const handleYoutubeSearch = (
|
252 |
+
message: string,
|
253 |
+
history: BaseMessage[],
|
254 |
+
llm: BaseChatModel,
|
255 |
+
embeddings: Embeddings,
|
256 |
+
) => {
|
257 |
+
const emitter = basicYoutubeSearch(message, history, llm, embeddings);
|
258 |
+
return emitter;
|
259 |
+
};
|
260 |
+
|
261 |
+
export default handleYoutubeSearch;
|
src/app.ts
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { startWebSocketServer } from './websocket';
|
2 |
+
import express from 'express';
|
3 |
+
import cors from 'cors';
|
4 |
+
import http from 'http';
|
5 |
+
import routes from './routes';
|
6 |
+
import { getPort } from './config';
|
7 |
+
import logger from './utils/logger';
|
8 |
+
|
9 |
+
const port = getPort();
|
10 |
+
|
11 |
+
const app = express();
|
12 |
+
const server = http.createServer(app);
|
13 |
+
|
14 |
+
const corsOptions = {
|
15 |
+
origin: '*',
|
16 |
+
};
|
17 |
+
|
18 |
+
app.use(cors(corsOptions));
|
19 |
+
app.use(express.json());
|
20 |
+
|
21 |
+
app.use('/api', routes);
|
22 |
+
app.get('/api', (_, res) => {
|
23 |
+
res.status(200).json({ status: 'ok' });
|
24 |
+
});
|
25 |
+
|
26 |
+
server.listen(port, () => {
|
27 |
+
logger.info(`Server is running on port ${port}`);
|
28 |
+
});
|
29 |
+
|
30 |
+
startWebSocketServer(server);
|
src/config.ts
ADDED
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import fs from 'fs';
|
2 |
+
import path from 'path';
|
3 |
+
import toml from '@iarna/toml';
|
4 |
+
|
5 |
+
const configFileName = 'config.toml';
|
6 |
+
|
7 |
+
interface Config {
|
8 |
+
GENERAL: {
|
9 |
+
PORT: number;
|
10 |
+
SIMILARITY_MEASURE: string;
|
11 |
+
};
|
12 |
+
API_KEYS: {
|
13 |
+
OPENAI: string;
|
14 |
+
GROQ: string;
|
15 |
+
};
|
16 |
+
API_ENDPOINTS: {
|
17 |
+
SEARXNG: string;
|
18 |
+
OLLAMA: string;
|
19 |
+
};
|
20 |
+
}
|
21 |
+
|
22 |
+
type RecursivePartial<T> = {
|
23 |
+
[P in keyof T]?: RecursivePartial<T[P]>;
|
24 |
+
};
|
25 |
+
|
26 |
+
const loadConfig = () =>
|
27 |
+
toml.parse(
|
28 |
+
fs.readFileSync(path.join(__dirname, `../${configFileName}`), 'utf-8'),
|
29 |
+
) as any as Config;
|
30 |
+
|
31 |
+
export const getPort = () => loadConfig().GENERAL.PORT;
|
32 |
+
|
33 |
+
export const getSimilarityMeasure = () =>
|
34 |
+
loadConfig().GENERAL.SIMILARITY_MEASURE;
|
35 |
+
|
36 |
+
export const getOpenaiApiKey = () => loadConfig().API_KEYS.OPENAI;
|
37 |
+
|
38 |
+
export const getGroqApiKey = () => loadConfig().API_KEYS.GROQ;
|
39 |
+
|
40 |
+
export const getSearxngApiEndpoint = () => loadConfig().API_ENDPOINTS.SEARXNG;
|
41 |
+
|
42 |
+
export const getOllamaApiEndpoint = () => loadConfig().API_ENDPOINTS.OLLAMA;
|
43 |
+
|
44 |
+
export const updateConfig = (config: RecursivePartial<Config>) => {
|
45 |
+
const currentConfig = loadConfig();
|
46 |
+
|
47 |
+
for (const key in currentConfig) {
|
48 |
+
if (!config[key]) config[key] = {};
|
49 |
+
|
50 |
+
if (typeof currentConfig[key] === 'object' && currentConfig[key] !== null) {
|
51 |
+
for (const nestedKey in currentConfig[key]) {
|
52 |
+
if (
|
53 |
+
!config[key][nestedKey] &&
|
54 |
+
currentConfig[key][nestedKey] &&
|
55 |
+
config[key][nestedKey] !== ''
|
56 |
+
) {
|
57 |
+
config[key][nestedKey] = currentConfig[key][nestedKey];
|
58 |
+
}
|
59 |
+
}
|
60 |
+
} else if (currentConfig[key] && config[key] !== '') {
|
61 |
+
config[key] = currentConfig[key];
|
62 |
+
}
|
63 |
+
}
|
64 |
+
|
65 |
+
fs.writeFileSync(
|
66 |
+
path.join(__dirname, `../${configFileName}`),
|
67 |
+
toml.stringify(config),
|
68 |
+
);
|
69 |
+
};
|
src/lib/providers.ts
ADDED
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
|
2 |
+
import { ChatOllama } from '@langchain/community/chat_models/ollama';
|
3 |
+
import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama';
|
4 |
+
import {
|
5 |
+
getGroqApiKey,
|
6 |
+
getOllamaApiEndpoint,
|
7 |
+
getOpenaiApiKey,
|
8 |
+
} from '../config';
|
9 |
+
import logger from '../utils/logger';
|
10 |
+
|
11 |
+
export const getAvailableChatModelProviders = async () => {
|
12 |
+
const openAIApiKey = getOpenaiApiKey();
|
13 |
+
const groqApiKey = getGroqApiKey();
|
14 |
+
const ollamaEndpoint = getOllamaApiEndpoint();
|
15 |
+
|
16 |
+
const models = {};
|
17 |
+
|
18 |
+
if (openAIApiKey) {
|
19 |
+
try {
|
20 |
+
models['openai'] = {
|
21 |
+
'GPT-3.5 turbo': new ChatOpenAI({
|
22 |
+
openAIApiKey,
|
23 |
+
modelName: 'gpt-3.5-turbo',
|
24 |
+
temperature: 0.7,
|
25 |
+
}),
|
26 |
+
'GPT-4': new ChatOpenAI({
|
27 |
+
openAIApiKey,
|
28 |
+
modelName: 'gpt-4',
|
29 |
+
temperature: 0.7,
|
30 |
+
}),
|
31 |
+
'GPT-4 turbo': new ChatOpenAI({
|
32 |
+
openAIApiKey,
|
33 |
+
modelName: 'gpt-4-turbo',
|
34 |
+
temperature: 0.7,
|
35 |
+
}),
|
36 |
+
};
|
37 |
+
} catch (err) {
|
38 |
+
logger.error(`Error loading OpenAI models: ${err}`);
|
39 |
+
}
|
40 |
+
}
|
41 |
+
|
42 |
+
if (groqApiKey) {
|
43 |
+
try {
|
44 |
+
models['groq'] = {
|
45 |
+
'LLaMA3 8b': new ChatOpenAI(
|
46 |
+
{
|
47 |
+
openAIApiKey: groqApiKey,
|
48 |
+
modelName: 'llama3-8b-8192',
|
49 |
+
temperature: 0.7,
|
50 |
+
},
|
51 |
+
{
|
52 |
+
baseURL: 'https://api.groq.com/openai/v1',
|
53 |
+
},
|
54 |
+
),
|
55 |
+
'LLaMA3 70b': new ChatOpenAI(
|
56 |
+
{
|
57 |
+
openAIApiKey: groqApiKey,
|
58 |
+
modelName: 'llama3-70b-8192',
|
59 |
+
temperature: 0.7,
|
60 |
+
},
|
61 |
+
{
|
62 |
+
baseURL: 'https://api.groq.com/openai/v1',
|
63 |
+
},
|
64 |
+
),
|
65 |
+
'Mixtral 8x7b': new ChatOpenAI(
|
66 |
+
{
|
67 |
+
openAIApiKey: groqApiKey,
|
68 |
+
modelName: 'mixtral-8x7b-32768',
|
69 |
+
temperature: 0.7,
|
70 |
+
},
|
71 |
+
{
|
72 |
+
baseURL: 'https://api.groq.com/openai/v1',
|
73 |
+
},
|
74 |
+
),
|
75 |
+
'Gemma 7b': new ChatOpenAI(
|
76 |
+
{
|
77 |
+
openAIApiKey: groqApiKey,
|
78 |
+
modelName: 'gemma-7b-it',
|
79 |
+
temperature: 0.7,
|
80 |
+
},
|
81 |
+
{
|
82 |
+
baseURL: 'https://api.groq.com/openai/v1',
|
83 |
+
},
|
84 |
+
),
|
85 |
+
};
|
86 |
+
} catch (err) {
|
87 |
+
logger.error(`Error loading Groq models: ${err}`);
|
88 |
+
}
|
89 |
+
}
|
90 |
+
|
91 |
+
if (ollamaEndpoint) {
|
92 |
+
try {
|
93 |
+
const response = await fetch(`${ollamaEndpoint}/api/tags`);
|
94 |
+
|
95 |
+
const { models: ollamaModels } = (await response.json()) as any;
|
96 |
+
|
97 |
+
models['ollama'] = ollamaModels.reduce((acc, model) => {
|
98 |
+
acc[model.model] = new ChatOllama({
|
99 |
+
baseUrl: ollamaEndpoint,
|
100 |
+
model: model.model,
|
101 |
+
temperature: 0.7,
|
102 |
+
});
|
103 |
+
return acc;
|
104 |
+
}, {});
|
105 |
+
} catch (err) {
|
106 |
+
logger.error(`Error loading Ollama models: ${err}`);
|
107 |
+
}
|
108 |
+
}
|
109 |
+
|
110 |
+
models['custom_openai'] = {};
|
111 |
+
|
112 |
+
return models;
|
113 |
+
};
|
114 |
+
|
115 |
+
export const getAvailableEmbeddingModelProviders = async () => {
|
116 |
+
const openAIApiKey = getOpenaiApiKey();
|
117 |
+
const ollamaEndpoint = getOllamaApiEndpoint();
|
118 |
+
|
119 |
+
const models = {};
|
120 |
+
|
121 |
+
if (openAIApiKey) {
|
122 |
+
try {
|
123 |
+
models['openai'] = {
|
124 |
+
'Text embedding 3 small': new OpenAIEmbeddings({
|
125 |
+
openAIApiKey,
|
126 |
+
modelName: 'text-embedding-3-small',
|
127 |
+
}),
|
128 |
+
'Text embedding 3 large': new OpenAIEmbeddings({
|
129 |
+
openAIApiKey,
|
130 |
+
modelName: 'text-embedding-3-large',
|
131 |
+
}),
|
132 |
+
};
|
133 |
+
} catch (err) {
|
134 |
+
logger.error(`Error loading OpenAI embeddings: ${err}`);
|
135 |
+
}
|
136 |
+
}
|
137 |
+
|
138 |
+
if (ollamaEndpoint) {
|
139 |
+
try {
|
140 |
+
const response = await fetch(`${ollamaEndpoint}/api/tags`);
|
141 |
+
|
142 |
+
const { models: ollamaModels } = (await response.json()) as any;
|
143 |
+
|
144 |
+
models['ollama'] = ollamaModels.reduce((acc, model) => {
|
145 |
+
acc[model.model] = new OllamaEmbeddings({
|
146 |
+
baseUrl: ollamaEndpoint,
|
147 |
+
model: model.model,
|
148 |
+
});
|
149 |
+
return acc;
|
150 |
+
}, {});
|
151 |
+
} catch (err) {
|
152 |
+
logger.error(`Error loading Ollama embeddings: ${err}`);
|
153 |
+
}
|
154 |
+
}
|
155 |
+
|
156 |
+
return models;
|
157 |
+
};
|
src/lib/searxng.ts
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import axios from 'axios';
|
2 |
+
import { getSearxngApiEndpoint } from '../config';
|
3 |
+
|
4 |
+
interface SearxngSearchOptions {
|
5 |
+
categories?: string[];
|
6 |
+
engines?: string[];
|
7 |
+
language?: string;
|
8 |
+
pageno?: number;
|
9 |
+
}
|
10 |
+
|
11 |
+
interface SearxngSearchResult {
|
12 |
+
title: string;
|
13 |
+
url: string;
|
14 |
+
img_src?: string;
|
15 |
+
thumbnail_src?: string;
|
16 |
+
thumbnail?: string;
|
17 |
+
content?: string;
|
18 |
+
author?: string;
|
19 |
+
iframe_src?: string;
|
20 |
+
}
|
21 |
+
|
22 |
+
export const searchSearxng = async (
|
23 |
+
query: string,
|
24 |
+
opts?: SearxngSearchOptions,
|
25 |
+
) => {
|
26 |
+
const searxngURL = getSearxngApiEndpoint();
|
27 |
+
|
28 |
+
const url = new URL(`${searxngURL}/search?format=json`);
|
29 |
+
url.searchParams.append('q', query);
|
30 |
+
|
31 |
+
if (opts) {
|
32 |
+
Object.keys(opts).forEach((key) => {
|
33 |
+
if (Array.isArray(opts[key])) {
|
34 |
+
url.searchParams.append(key, opts[key].join(','));
|
35 |
+
return;
|
36 |
+
}
|
37 |
+
url.searchParams.append(key, opts[key]);
|
38 |
+
});
|
39 |
+
}
|
40 |
+
|
41 |
+
const res = await axios.get(url.toString());
|
42 |
+
|
43 |
+
const results: SearxngSearchResult[] = res.data.results;
|
44 |
+
const suggestions: string[] = res.data.suggestions;
|
45 |
+
|
46 |
+
return { results, suggestions };
|
47 |
+
};
|
src/routes/config.ts
ADDED
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import express from 'express';
|
2 |
+
import {
|
3 |
+
getAvailableChatModelProviders,
|
4 |
+
getAvailableEmbeddingModelProviders,
|
5 |
+
} from '../lib/providers';
|
6 |
+
import {
|
7 |
+
getGroqApiKey,
|
8 |
+
getOllamaApiEndpoint,
|
9 |
+
getOpenaiApiKey,
|
10 |
+
updateConfig,
|
11 |
+
} from '../config';
|
12 |
+
|
13 |
+
const router = express.Router();
|
14 |
+
|
15 |
+
router.get('/', async (_, res) => {
|
16 |
+
const config = {};
|
17 |
+
|
18 |
+
const [chatModelProviders, embeddingModelProviders] = await Promise.all([
|
19 |
+
getAvailableChatModelProviders(),
|
20 |
+
getAvailableEmbeddingModelProviders(),
|
21 |
+
]);
|
22 |
+
|
23 |
+
config['chatModelProviders'] = {};
|
24 |
+
config['embeddingModelProviders'] = {};
|
25 |
+
|
26 |
+
for (const provider in chatModelProviders) {
|
27 |
+
config['chatModelProviders'][provider] = Object.keys(
|
28 |
+
chatModelProviders[provider],
|
29 |
+
);
|
30 |
+
}
|
31 |
+
|
32 |
+
for (const provider in embeddingModelProviders) {
|
33 |
+
config['embeddingModelProviders'][provider] = Object.keys(
|
34 |
+
embeddingModelProviders[provider],
|
35 |
+
);
|
36 |
+
}
|
37 |
+
|
38 |
+
config['openaiApiKey'] = getOpenaiApiKey();
|
39 |
+
config['ollamaApiUrl'] = getOllamaApiEndpoint();
|
40 |
+
config['groqApiKey'] = getGroqApiKey();
|
41 |
+
|
42 |
+
res.status(200).json(config);
|
43 |
+
});
|
44 |
+
|
45 |
+
router.post('/', async (req, res) => {
|
46 |
+
const config = req.body;
|
47 |
+
|
48 |
+
const updatedConfig = {
|
49 |
+
API_KEYS: {
|
50 |
+
OPENAI: config.openaiApiKey,
|
51 |
+
GROQ: config.groqApiKey,
|
52 |
+
},
|
53 |
+
API_ENDPOINTS: {
|
54 |
+
OLLAMA: config.ollamaApiUrl,
|
55 |
+
},
|
56 |
+
};
|
57 |
+
|
58 |
+
updateConfig(updatedConfig);
|
59 |
+
|
60 |
+
res.status(200).json({ message: 'Config updated' });
|
61 |
+
});
|
62 |
+
|
63 |
+
export default router;
|
src/routes/images.ts
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import express from 'express';
|
2 |
+
import handleImageSearch from '../agents/imageSearchAgent';
|
3 |
+
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
4 |
+
import { getAvailableChatModelProviders } from '../lib/providers';
|
5 |
+
import { HumanMessage, AIMessage } from '@langchain/core/messages';
|
6 |
+
import logger from '../utils/logger';
|
7 |
+
|
8 |
+
const router = express.Router();
|
9 |
+
|
10 |
+
router.post('/', async (req, res) => {
|
11 |
+
try {
|
12 |
+
let { query, chat_history, chat_model_provider, chat_model } = req.body;
|
13 |
+
|
14 |
+
chat_history = chat_history.map((msg: any) => {
|
15 |
+
if (msg.role === 'user') {
|
16 |
+
return new HumanMessage(msg.content);
|
17 |
+
} else if (msg.role === 'assistant') {
|
18 |
+
return new AIMessage(msg.content);
|
19 |
+
}
|
20 |
+
});
|
21 |
+
|
22 |
+
const chatModels = await getAvailableChatModelProviders();
|
23 |
+
const provider = chat_model_provider || Object.keys(chatModels)[0];
|
24 |
+
const chatModel = chat_model || Object.keys(chatModels[provider])[0];
|
25 |
+
|
26 |
+
let llm: BaseChatModel | undefined;
|
27 |
+
|
28 |
+
if (chatModels[provider] && chatModels[provider][chatModel]) {
|
29 |
+
llm = chatModels[provider][chatModel] as BaseChatModel | undefined;
|
30 |
+
}
|
31 |
+
|
32 |
+
if (!llm) {
|
33 |
+
res.status(500).json({ message: 'Invalid LLM model selected' });
|
34 |
+
return;
|
35 |
+
}
|
36 |
+
|
37 |
+
const images = await handleImageSearch({ query, chat_history }, llm);
|
38 |
+
|
39 |
+
res.status(200).json({ images });
|
40 |
+
} catch (err) {
|
41 |
+
res.status(500).json({ message: 'An error has occurred.' });
|
42 |
+
logger.error(`Error in image search: ${err.message}`);
|
43 |
+
}
|
44 |
+
});
|
45 |
+
|
46 |
+
export default router;
|
src/routes/index.ts
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import express from 'express';
|
2 |
+
import imagesRouter from './images';
|
3 |
+
import videosRouter from './videos';
|
4 |
+
import configRouter from './config';
|
5 |
+
import modelsRouter from './models';
|
6 |
+
|
7 |
+
const router = express.Router();
|
8 |
+
|
9 |
+
router.use('/images', imagesRouter);
|
10 |
+
router.use('/videos', videosRouter);
|
11 |
+
router.use('/config', configRouter);
|
12 |
+
router.use('/models', modelsRouter);
|
13 |
+
|
14 |
+
export default router;
|
src/routes/models.ts
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import express from 'express';
|
2 |
+
import logger from '../utils/logger';
|
3 |
+
import {
|
4 |
+
getAvailableChatModelProviders,
|
5 |
+
getAvailableEmbeddingModelProviders,
|
6 |
+
} from '../lib/providers';
|
7 |
+
|
8 |
+
const router = express.Router();
|
9 |
+
|
10 |
+
router.get('/', async (req, res) => {
|
11 |
+
try {
|
12 |
+
const [chatModelProviders, embeddingModelProviders] = await Promise.all([
|
13 |
+
getAvailableChatModelProviders(),
|
14 |
+
getAvailableEmbeddingModelProviders(),
|
15 |
+
]);
|
16 |
+
|
17 |
+
res.status(200).json({ chatModelProviders, embeddingModelProviders });
|
18 |
+
} catch (err) {
|
19 |
+
res.status(500).json({ message: 'An error has occurred.' });
|
20 |
+
logger.error(err.message);
|
21 |
+
}
|
22 |
+
});
|
23 |
+
|
24 |
+
export default router;
|
src/routes/videos.ts
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import express from 'express';
|
2 |
+
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
3 |
+
import { getAvailableChatModelProviders } from '../lib/providers';
|
4 |
+
import { HumanMessage, AIMessage } from '@langchain/core/messages';
|
5 |
+
import logger from '../utils/logger';
|
6 |
+
import handleVideoSearch from '../agents/videoSearchAgent';
|
7 |
+
|
8 |
+
const router = express.Router();
|
9 |
+
|
10 |
+
router.post('/', async (req, res) => {
|
11 |
+
try {
|
12 |
+
let { query, chat_history, chat_model_provider, chat_model } = req.body;
|
13 |
+
|
14 |
+
chat_history = chat_history.map((msg: any) => {
|
15 |
+
if (msg.role === 'user') {
|
16 |
+
return new HumanMessage(msg.content);
|
17 |
+
} else if (msg.role === 'assistant') {
|
18 |
+
return new AIMessage(msg.content);
|
19 |
+
}
|
20 |
+
});
|
21 |
+
|
22 |
+
const chatModels = await getAvailableChatModelProviders();
|
23 |
+
const provider = chat_model_provider || Object.keys(chatModels)[0];
|
24 |
+
const chatModel = chat_model || Object.keys(chatModels[provider])[0];
|
25 |
+
|
26 |
+
let llm: BaseChatModel | undefined;
|
27 |
+
|
28 |
+
if (chatModels[provider] && chatModels[provider][chatModel]) {
|
29 |
+
llm = chatModels[provider][chatModel] as BaseChatModel | undefined;
|
30 |
+
}
|
31 |
+
|
32 |
+
if (!llm) {
|
33 |
+
res.status(500).json({ message: 'Invalid LLM model selected' });
|
34 |
+
return;
|
35 |
+
}
|
36 |
+
|
37 |
+
const videos = await handleVideoSearch({ chat_history, query }, llm);
|
38 |
+
|
39 |
+
res.status(200).json({ videos });
|
40 |
+
} catch (err) {
|
41 |
+
res.status(500).json({ message: 'An error has occurred.' });
|
42 |
+
logger.error(`Error in video search: ${err.message}`);
|
43 |
+
}
|
44 |
+
});
|
45 |
+
|
46 |
+
export default router;
|
src/utils/computeSimilarity.ts
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import dot from 'compute-dot';
|
2 |
+
import cosineSimilarity from 'compute-cosine-similarity';
|
3 |
+
import { getSimilarityMeasure } from '../config';
|
4 |
+
|
5 |
+
const computeSimilarity = (x: number[], y: number[]): number => {
|
6 |
+
const similarityMeasure = getSimilarityMeasure();
|
7 |
+
|
8 |
+
if (similarityMeasure === 'cosine') {
|
9 |
+
return cosineSimilarity(x, y);
|
10 |
+
} else if (similarityMeasure === 'dot') {
|
11 |
+
return dot(x, y);
|
12 |
+
}
|
13 |
+
|
14 |
+
throw new Error('Invalid similarity measure');
|
15 |
+
};
|
16 |
+
|
17 |
+
export default computeSimilarity;
|
src/utils/formatHistory.ts
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { BaseMessage } from '@langchain/core/messages';
|
2 |
+
|
3 |
+
const formatChatHistoryAsString = (history: BaseMessage[]) => {
|
4 |
+
return history
|
5 |
+
.map((message) => `${message._getType()}: ${message.content}`)
|
6 |
+
.join('\n');
|
7 |
+
};
|
8 |
+
|
9 |
+
export default formatChatHistoryAsString;
|
src/utils/logger.ts
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import winston from 'winston';
|
2 |
+
|
3 |
+
const logger = winston.createLogger({
|
4 |
+
level: 'info',
|
5 |
+
transports: [
|
6 |
+
new winston.transports.Console({
|
7 |
+
format: winston.format.combine(
|
8 |
+
winston.format.colorize(),
|
9 |
+
winston.format.simple(),
|
10 |
+
),
|
11 |
+
}),
|
12 |
+
new winston.transports.File({
|
13 |
+
filename: 'app.log',
|
14 |
+
format: winston.format.combine(
|
15 |
+
winston.format.timestamp(),
|
16 |
+
winston.format.json(),
|
17 |
+
),
|
18 |
+
}),
|
19 |
+
],
|
20 |
+
});
|
21 |
+
|
22 |
+
export default logger;
|
src/websocket/connectionManager.ts
ADDED
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { WebSocket } from 'ws';
|
2 |
+
import { handleMessage } from './messageHandler';
|
3 |
+
import {
|
4 |
+
getAvailableEmbeddingModelProviders,
|
5 |
+
getAvailableChatModelProviders,
|
6 |
+
} from '../lib/providers';
|
7 |
+
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
8 |
+
import type { Embeddings } from '@langchain/core/embeddings';
|
9 |
+
import type { IncomingMessage } from 'http';
|
10 |
+
import logger from '../utils/logger';
|
11 |
+
import { ChatOpenAI } from '@langchain/openai';
|
12 |
+
|
13 |
+
export const handleConnection = async (
|
14 |
+
ws: WebSocket,
|
15 |
+
request: IncomingMessage,
|
16 |
+
) => {
|
17 |
+
const searchParams = new URL(request.url, `http://${request.headers.host}`)
|
18 |
+
.searchParams;
|
19 |
+
|
20 |
+
const [chatModelProviders, embeddingModelProviders] = await Promise.all([
|
21 |
+
getAvailableChatModelProviders(),
|
22 |
+
getAvailableEmbeddingModelProviders(),
|
23 |
+
]);
|
24 |
+
|
25 |
+
const chatModelProvider =
|
26 |
+
searchParams.get('chatModelProvider') || Object.keys(chatModelProviders)[0];
|
27 |
+
const chatModel =
|
28 |
+
searchParams.get('chatModel') ||
|
29 |
+
Object.keys(chatModelProviders[chatModelProvider])[0];
|
30 |
+
|
31 |
+
const embeddingModelProvider =
|
32 |
+
searchParams.get('embeddingModelProvider') ||
|
33 |
+
Object.keys(embeddingModelProviders)[0];
|
34 |
+
const embeddingModel =
|
35 |
+
searchParams.get('embeddingModel') ||
|
36 |
+
Object.keys(embeddingModelProviders[embeddingModelProvider])[0];
|
37 |
+
|
38 |
+
let llm: BaseChatModel | undefined;
|
39 |
+
let embeddings: Embeddings | undefined;
|
40 |
+
|
41 |
+
if (
|
42 |
+
chatModelProviders[chatModelProvider] &&
|
43 |
+
chatModelProviders[chatModelProvider][chatModel] &&
|
44 |
+
chatModelProvider != 'custom_openai'
|
45 |
+
) {
|
46 |
+
llm = chatModelProviders[chatModelProvider][chatModel] as
|
47 |
+
| BaseChatModel
|
48 |
+
| undefined;
|
49 |
+
} else if (chatModelProvider == 'custom_openai') {
|
50 |
+
llm = new ChatOpenAI({
|
51 |
+
modelName: chatModel,
|
52 |
+
openAIApiKey: searchParams.get('openAIApiKey'),
|
53 |
+
temperature: 0.7,
|
54 |
+
configuration: {
|
55 |
+
baseURL: searchParams.get('openAIBaseURL'),
|
56 |
+
},
|
57 |
+
});
|
58 |
+
}
|
59 |
+
|
60 |
+
if (
|
61 |
+
embeddingModelProviders[embeddingModelProvider] &&
|
62 |
+
embeddingModelProviders[embeddingModelProvider][embeddingModel]
|
63 |
+
) {
|
64 |
+
embeddings = embeddingModelProviders[embeddingModelProvider][
|
65 |
+
embeddingModel
|
66 |
+
] as Embeddings | undefined;
|
67 |
+
}
|
68 |
+
|
69 |
+
if (!llm || !embeddings) {
|
70 |
+
ws.send(
|
71 |
+
JSON.stringify({
|
72 |
+
type: 'error',
|
73 |
+
data: 'Invalid LLM or embeddings model selected',
|
74 |
+
}),
|
75 |
+
);
|
76 |
+
ws.close();
|
77 |
+
}
|
78 |
+
|
79 |
+
ws.on(
|
80 |
+
'message',
|
81 |
+
async (message) =>
|
82 |
+
await handleMessage(message.toString(), ws, llm, embeddings),
|
83 |
+
);
|
84 |
+
|
85 |
+
ws.on('close', () => logger.debug('Connection closed'));
|
86 |
+
};
|
src/websocket/index.ts
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { initServer } from './websocketServer';
|
2 |
+
import http from 'http';
|
3 |
+
|
4 |
+
export const startWebSocketServer = (
|
5 |
+
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
6 |
+
) => {
|
7 |
+
initServer(server);
|
8 |
+
};
|
src/websocket/messageHandler.ts
ADDED
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { EventEmitter, WebSocket } from 'ws';
|
2 |
+
import { BaseMessage, AIMessage, HumanMessage } from '@langchain/core/messages';
|
3 |
+
import handleWebSearch from '../agents/webSearchAgent';
|
4 |
+
import handleAcademicSearch from '../agents/academicSearchAgent';
|
5 |
+
import handleWritingAssistant from '../agents/writingAssistant';
|
6 |
+
import handleWolframAlphaSearch from '../agents/wolframAlphaSearchAgent';
|
7 |
+
import handleYoutubeSearch from '../agents/youtubeSearchAgent';
|
8 |
+
import handleRedditSearch from '../agents/redditSearchAgent';
|
9 |
+
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
10 |
+
import type { Embeddings } from '@langchain/core/embeddings';
|
11 |
+
import logger from '../utils/logger';
|
12 |
+
|
13 |
+
type Message = {
|
14 |
+
type: string;
|
15 |
+
content: string;
|
16 |
+
copilot: boolean;
|
17 |
+
focusMode: string;
|
18 |
+
history: Array<[string, string]>;
|
19 |
+
};
|
20 |
+
|
21 |
+
const searchHandlers = {
|
22 |
+
webSearch: handleWebSearch,
|
23 |
+
academicSearch: handleAcademicSearch,
|
24 |
+
writingAssistant: handleWritingAssistant,
|
25 |
+
wolframAlphaSearch: handleWolframAlphaSearch,
|
26 |
+
youtubeSearch: handleYoutubeSearch,
|
27 |
+
redditSearch: handleRedditSearch,
|
28 |
+
};
|
29 |
+
|
30 |
+
const handleEmitterEvents = (
|
31 |
+
emitter: EventEmitter,
|
32 |
+
ws: WebSocket,
|
33 |
+
id: string,
|
34 |
+
) => {
|
35 |
+
emitter.on('data', (data) => {
|
36 |
+
const parsedData = JSON.parse(data);
|
37 |
+
if (parsedData.type === 'response') {
|
38 |
+
ws.send(
|
39 |
+
JSON.stringify({
|
40 |
+
type: 'message',
|
41 |
+
data: parsedData.data,
|
42 |
+
messageId: id,
|
43 |
+
}),
|
44 |
+
);
|
45 |
+
} else if (parsedData.type === 'sources') {
|
46 |
+
ws.send(
|
47 |
+
JSON.stringify({
|
48 |
+
type: 'sources',
|
49 |
+
data: parsedData.data,
|
50 |
+
messageId: id,
|
51 |
+
}),
|
52 |
+
);
|
53 |
+
}
|
54 |
+
});
|
55 |
+
emitter.on('end', () => {
|
56 |
+
ws.send(JSON.stringify({ type: 'messageEnd', messageId: id }));
|
57 |
+
});
|
58 |
+
emitter.on('error', (data) => {
|
59 |
+
const parsedData = JSON.parse(data);
|
60 |
+
ws.send(JSON.stringify({ type: 'error', data: parsedData.data }));
|
61 |
+
});
|
62 |
+
};
|
63 |
+
|
64 |
+
export const handleMessage = async (
|
65 |
+
message: string,
|
66 |
+
ws: WebSocket,
|
67 |
+
llm: BaseChatModel,
|
68 |
+
embeddings: Embeddings,
|
69 |
+
) => {
|
70 |
+
try {
|
71 |
+
const parsedMessage = JSON.parse(message) as Message;
|
72 |
+
const id = Math.random().toString(36).substring(7);
|
73 |
+
|
74 |
+
if (!parsedMessage.content)
|
75 |
+
return ws.send(
|
76 |
+
JSON.stringify({ type: 'error', data: 'Invalid message format' }),
|
77 |
+
);
|
78 |
+
|
79 |
+
const history: BaseMessage[] = parsedMessage.history.map((msg) => {
|
80 |
+
if (msg[0] === 'human') {
|
81 |
+
return new HumanMessage({
|
82 |
+
content: msg[1],
|
83 |
+
});
|
84 |
+
} else {
|
85 |
+
return new AIMessage({
|
86 |
+
content: msg[1],
|
87 |
+
});
|
88 |
+
}
|
89 |
+
});
|
90 |
+
|
91 |
+
if (parsedMessage.type === 'message') {
|
92 |
+
const handler = searchHandlers[parsedMessage.focusMode];
|
93 |
+
if (handler) {
|
94 |
+
const emitter = handler(
|
95 |
+
parsedMessage.content,
|
96 |
+
history,
|
97 |
+
llm,
|
98 |
+
embeddings,
|
99 |
+
);
|
100 |
+
handleEmitterEvents(emitter, ws, id);
|
101 |
+
} else {
|
102 |
+
ws.send(JSON.stringify({ type: 'error', data: 'Invalid focus mode' }));
|
103 |
+
}
|
104 |
+
}
|
105 |
+
} catch (err) {
|
106 |
+
ws.send(JSON.stringify({ type: 'error', data: 'Invalid message format' }));
|
107 |
+
logger.error(`Failed to handle message: ${err}`);
|
108 |
+
}
|
109 |
+
};
|
src/websocket/websocketServer.ts
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { WebSocketServer } from 'ws';
|
2 |
+
import { handleConnection } from './connectionManager';
|
3 |
+
import http from 'http';
|
4 |
+
import { getPort } from '../config';
|
5 |
+
import logger from '../utils/logger';
|
6 |
+
|
7 |
+
export const initServer = (
|
8 |
+
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
9 |
+
) => {
|
10 |
+
const port = getPort();
|
11 |
+
const wss = new WebSocketServer({ server });
|
12 |
+
|
13 |
+
wss.on('connection', handleConnection);
|
14 |
+
|
15 |
+
logger.info(`WebSocket server started on port ${port}`);
|
16 |
+
};
|
tsconfig.json
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"compilerOptions": {
|
3 |
+
"lib": ["ESNext"],
|
4 |
+
"module": "commonjs",
|
5 |
+
"target": "ESNext",
|
6 |
+
"outDir": "dist",
|
7 |
+
"sourceMap": false,
|
8 |
+
"esModuleInterop": true,
|
9 |
+
"experimentalDecorators": true,
|
10 |
+
"emitDecoratorMetadata": true,
|
11 |
+
"allowSyntheticDefaultImports": true,
|
12 |
+
"skipLibCheck": true,
|
13 |
+
"skipDefaultLibCheck": true
|
14 |
+
},
|
15 |
+
"include": ["src"],
|
16 |
+
"exclude": ["node_modules", "**/*.spec.ts"]
|
17 |
+
}
|
ui/.env.example
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
NEXT_PUBLIC_WS_URL=ws://localhost:3001
|
2 |
+
NEXT_PUBLIC_API_URL=http://localhost:3001/api
|
ui/.eslintrc.json
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"extends": "next/core-web-vitals"
|
3 |
+
}
|
ui/.gitignore
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# dependencies
|
2 |
+
/node_modules
|
3 |
+
/.pnp
|
4 |
+
.pnp.js
|
5 |
+
.yarn/install-state.gz
|
6 |
+
|
7 |
+
# testing
|
8 |
+
/coverage
|
9 |
+
|
10 |
+
# next.js
|
11 |
+
/.next/
|
12 |
+
/out/
|
13 |
+
|
14 |
+
# production
|
15 |
+
/build
|
16 |
+
|
17 |
+
# misc
|
18 |
+
.DS_Store
|
19 |
+
*.pem
|
20 |
+
|
21 |
+
# debug
|
22 |
+
npm-debug.log*
|
23 |
+
yarn-debug.log*
|
24 |
+
yarn-error.log*
|
25 |
+
|
26 |
+
# local env files
|
27 |
+
.env*.local
|
28 |
+
|
29 |
+
# vercel
|
30 |
+
.vercel
|
31 |
+
|
32 |
+
# typescript
|
33 |
+
*.tsbuildinfo
|
34 |
+
next-env.d.ts
|
ui/.prettierrc.js
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/** @type {import("prettier").Config} */
|
2 |
+
|
3 |
+
const config = {
|
4 |
+
printWidth: 80,
|
5 |
+
trailingComma: 'all',
|
6 |
+
endOfLine: 'auto',
|
7 |
+
singleQuote: true,
|
8 |
+
tabWidth: 2,
|
9 |
+
};
|
10 |
+
|
11 |
+
module.exports = config;
|
ui/app/discover/page.tsx
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const Page = () => {
|
2 |
+
return <div>page</div>;
|
3 |
+
};
|
4 |
+
|
5 |
+
export default Page;
|