mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-06-20 16:58:30 +00:00
Compare commits
31 Commits
a6c56338d0
...
develop/v1
Author | SHA1 | Date | |
---|---|---|---|
59ab10110a | |||
10f9cd2f79 | |||
82e1dd73b0 | |||
728b499281 | |||
5a4dafc753 | |||
4ac99786f0 | |||
1224281278 | |||
3daae29a5d | |||
50bcaa13f2 | |||
31e4abf068 | |||
fd6e701cf0 | |||
89880a2555 | |||
07776d8699 | |||
32fb6ac131 | |||
99137d95e7 | |||
490a8db538 | |||
aba702c51b | |||
89a6e7fbb1 | |||
f19d2e3a97 | |||
4a7ca8fc68 | |||
3d642f2539 | |||
aa91d3bc60 | |||
93c5ed46f6 | |||
af4b97b766 | |||
ca86a7e358 | |||
99351fc2a6 | |||
7a816efc04 | |||
4d41243108 | |||
6c218b5fee | |||
1c1f31e23a | |||
5b15bcfe17 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -2,6 +2,7 @@
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
package-lock.json
|
||||
|
||||
# Build output
|
||||
/.next/
|
||||
@ -37,3 +38,6 @@ Thumbs.db
|
||||
# Db
|
||||
db.sqlite
|
||||
/searxng
|
||||
|
||||
# Dev
|
||||
docker-compose-dev.yaml
|
||||
|
27
README.md
27
README.md
@ -2,7 +2,6 @@
|
||||
|
||||
[](https://discord.gg/26aArMy8tT)
|
||||
|
||||
|
||||

|
||||
|
||||
## Table of Contents <!-- omit in toc -->
|
||||
@ -13,11 +12,10 @@
|
||||
- [Installation](#installation)
|
||||
- [Getting Started with Docker (Recommended)](#getting-started-with-docker-recommended)
|
||||
- [Non-Docker Installation](#non-docker-installation)
|
||||
- [Nginx Reverse Proxy](#nginx-reverse-proxy)
|
||||
- [Ollama Connection Errors](#ollama-connection-errors)
|
||||
- [Using as a Search Engine](#using-as-a-search-engine)
|
||||
- [Using Perplexica's API](#using-perplexicas-api)
|
||||
- [Expose Perplexica to a Network](#expose-perplexica-to-a-network)
|
||||
- [Expose Perplexica to a network](#expose-perplexica-to-network)
|
||||
- [One-Click Deployment](#one-click-deployment)
|
||||
- [Upcoming Features](#upcoming-features)
|
||||
- [Support Us](#support-us)
|
||||
@ -104,17 +102,6 @@ There are mainly 2 ways of installing Perplexica - With Docker, Without Docker.
|
||||
|
||||
See the [installation documentation](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/installation) for more information like exposing it your network, etc.
|
||||
|
||||
### Nginx Reverse Proxy
|
||||
|
||||
Perplexica includes an Nginx reverse proxy that provides several key benefits:
|
||||
|
||||
- **Single Port Access**: Access both frontend and backend through a single port (8080)
|
||||
- **Dynamic Configuration**: Works with any domain or IP without rebuilding
|
||||
- **WebSocket Support**: Automatic WebSocket URL configuration based on the current domain
|
||||
- **Security Headers**: Enhanced security with proper HTTP headers
|
||||
|
||||
When using Docker, the reverse proxy is automatically configured. Access Perplexica at `http://localhost:8080` or `http://your-ip:8080` after starting the containers.
|
||||
|
||||
### Ollama Connection Errors
|
||||
|
||||
If you're encountering an Ollama connection error, it is likely due to the backend being unable to connect to Ollama's API. To fix this issue you can:
|
||||
@ -140,7 +127,7 @@ If you wish to use Perplexica as an alternative to traditional search engines li
|
||||
|
||||
1. Open your browser's settings.
|
||||
2. Navigate to the 'Search Engines' section.
|
||||
3. Add a new site search with the following URL: `http://localhost:8080/?q=%s`. Replace `localhost` with your IP address or domain name if needed.
|
||||
3. Add a new site search with the following URL: `http://localhost:3000/?q=%s`. Replace `localhost` with your IP address or domain name, and `3000` with the port number if Perplexica is not hosted locally.
|
||||
4. Click the add button. Now, you can use Perplexica directly from your browser's search bar.
|
||||
|
||||
## Using Perplexica's API
|
||||
@ -149,15 +136,9 @@ Perplexica also provides an API for developers looking to integrate its powerful
|
||||
|
||||
For more details, check out the full documentation [here](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/API/SEARCH.md).
|
||||
|
||||
## Expose Perplexica to a Network
|
||||
## Expose Perplexica to network
|
||||
|
||||
Perplexica can be easily accessed over your home network or exposed to the internet through the Nginx reverse proxy. With this setup:
|
||||
|
||||
1. **Local Network Access**: Access Perplexica from any device on your network using `http://server-ip:8080`
|
||||
2. **Domain Configuration**: If you have a domain name, point it to your server and access Perplexica with `http://your-domain.com:8080`
|
||||
3. **SSL Support**: Configure SSL certificates in Nginx for secure `https://` access
|
||||
|
||||
For more network configuration details, see our [networking guide](https://github.com/ItzCrazyKns/Perplexica/blob/master/docs/installation/NETWORKING.md).
|
||||
You can access Perplexica over your home network by following our networking guide [here](https://github.com/ItzCrazyKns/Perplexica/blob/master/docs/installation/NETWORKING.md).
|
||||
|
||||
## One-Click Deployment
|
||||
|
||||
|
@ -4,7 +4,7 @@ services:
|
||||
volumes:
|
||||
- ./searxng:/etc/searxng:rw
|
||||
ports:
|
||||
- 4000:8080
|
||||
- '4000:8080'
|
||||
networks:
|
||||
- perplexica-network
|
||||
restart: unless-stopped
|
||||
@ -19,7 +19,7 @@ services:
|
||||
depends_on:
|
||||
- searxng
|
||||
ports:
|
||||
- 3001:3001
|
||||
- '3001:3001'
|
||||
volumes:
|
||||
- backend-dbstore:/home/perplexica/data
|
||||
- uploads:/home/perplexica/uploads
|
||||
@ -35,26 +35,13 @@ services:
|
||||
context: .
|
||||
dockerfile: app.dockerfile
|
||||
args:
|
||||
- NEXT_PUBLIC_API_URL=/api
|
||||
- NEXT_PUBLIC_WS_URL=auto
|
||||
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api
|
||||
- NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001
|
||||
image: itzcrazykns1337/perplexica-frontend:main
|
||||
depends_on:
|
||||
- perplexica-backend
|
||||
ports:
|
||||
- 3000:3000
|
||||
networks:
|
||||
- perplexica-network
|
||||
restart: unless-stopped
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
depends_on:
|
||||
- perplexica-frontend
|
||||
- perplexica-backend
|
||||
- '3000:3000'
|
||||
networks:
|
||||
- perplexica-network
|
||||
restart: unless-stopped
|
||||
|
@ -1,106 +1,109 @@
|
||||
# Exposing Perplexica to a Network
|
||||
# Expose Perplexica to a network
|
||||
|
||||
This guide explains how to make Perplexica available over a network using the built-in Nginx reverse proxy.
|
||||
This guide will show you how to make Perplexica available over a network. Follow these steps to allow computers on the same network to interact with Perplexica. Choose the instructions that match the operating system you are using.
|
||||
|
||||
## Accessing Perplexica Over a Network
|
||||
## Windows
|
||||
|
||||
### Basic Access
|
||||
1. Open PowerShell as Administrator
|
||||
|
||||
With the Nginx reverse proxy, Perplexica is automatically accessible from any device on your network:
|
||||
2. Navigate to the directory containing the `docker-compose.yaml` file
|
||||
|
||||
3. Stop and remove the existing Perplexica containers and images:
|
||||
|
||||
1. Start Perplexica using Docker Compose:
|
||||
```bash
|
||||
docker compose up -d
|
||||
docker compose down --rmi all
|
||||
```
|
||||
|
||||
2. Find your server's IP address:
|
||||
- **Windows**: `ipconfig` in Command Prompt
|
||||
- **macOS**: `ifconfig | grep "inet "` in Terminal
|
||||
- **Linux**: `ip addr show | grep "inet "` in Terminal
|
||||
4. Open the `docker-compose.yaml` file in a text editor like Notepad++
|
||||
|
||||
3. Access Perplexica from any device on your network:
|
||||
```
|
||||
http://YOUR_SERVER_IP:8080
|
||||
```
|
||||
5. Replace `127.0.0.1` with the IP address of the server Perplexica is running on in these two lines:
|
||||
|
||||
### Domain Configuration
|
||||
|
||||
If you have a domain name, you can point it to your server:
|
||||
|
||||
1. Configure your domain's DNS settings to point to your server IP
|
||||
|
||||
2. Access Perplexica via:
|
||||
```
|
||||
http://your-domain.com:8080
|
||||
```
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom Port
|
||||
|
||||
If you need to use a different port instead of the default 8080:
|
||||
|
||||
1. Modify the `docker-compose.yaml` file:
|
||||
```yaml
|
||||
nginx:
|
||||
ports:
|
||||
- "YOUR_CUSTOM_PORT:80"
|
||||
```
|
||||
|
||||
2. Restart the containers:
|
||||
```bash
|
||||
docker compose down && docker compose up -d
|
||||
args:
|
||||
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api
|
||||
- NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001
|
||||
```
|
||||
|
||||
### SSL/HTTPS Configuration
|
||||
6. Save and close the `docker-compose.yaml` file
|
||||
|
||||
For secure HTTPS access:
|
||||
7. Rebuild and restart the Perplexica container:
|
||||
|
||||
1. Modify the Nginx configuration to include SSL:
|
||||
```nginx
|
||||
# In nginx.conf
|
||||
server {
|
||||
listen 80;
|
||||
listen 443 ssl;
|
||||
|
||||
ssl_certificate /path/to/certificate.crt;
|
||||
ssl_certificate_key /path/to/private.key;
|
||||
|
||||
# Rest of configuration...
|
||||
}
|
||||
```
|
||||
|
||||
2. Update the Docker volume to include your certificates:
|
||||
```yaml
|
||||
nginx:
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./ssl:/path/to/ssl:ro
|
||||
```
|
||||
|
||||
3. Restart the containers:
|
||||
```bash
|
||||
docker compose down && docker compose up -d
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
4. Or just use another reverse proxy on top of this one...
|
||||
## macOS
|
||||
|
||||
## Troubleshooting
|
||||
1. Open the Terminal application
|
||||
|
||||
If you encounter issues accessing Perplexica over your network:
|
||||
2. Navigate to the directory with the `docker-compose.yaml` file:
|
||||
|
||||
1. **Firewall Settings**: Ensure port 8080 (or your custom port) is allowed in your firewall
|
||||
|
||||
2. **Docker Network**: Check if Docker's network settings allow external connections:
|
||||
```bash
|
||||
docker network inspect perplexica_perplexica-network
|
||||
cd /path/to/docker-compose.yaml
|
||||
```
|
||||
|
||||
3. **Nginx Logs**: Check for any connection issues:
|
||||
3. Stop and remove existing containers and images:
|
||||
|
||||
```bash
|
||||
docker logs perplexica-nginx-1
|
||||
docker compose down --rmi all
|
||||
```
|
||||
|
||||
4. **Direct Access**: Verify if you can access the services directly:
|
||||
- Frontend: http://YOUR_SERVER_IP:3000
|
||||
- Backend: http://YOUR_SERVER_IP:3001
|
||||
4. Open `docker-compose.yaml` in a text editor like Sublime Text:
|
||||
|
||||
```bash
|
||||
nano docker-compose.yaml
|
||||
```
|
||||
|
||||
5. Replace `127.0.0.1` with the server IP in these lines:
|
||||
|
||||
```bash
|
||||
args:
|
||||
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api
|
||||
- NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001
|
||||
```
|
||||
|
||||
6. Save and exit the editor
|
||||
|
||||
7. Rebuild and restart Perplexica:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
## Linux
|
||||
|
||||
1. Open the terminal
|
||||
|
||||
2. Navigate to the `docker-compose.yaml` directory:
|
||||
|
||||
```bash
|
||||
cd /path/to/docker-compose.yaml
|
||||
```
|
||||
|
||||
3. Stop and remove containers and images:
|
||||
|
||||
```bash
|
||||
docker compose down --rmi all
|
||||
```
|
||||
|
||||
4. Edit `docker-compose.yaml`:
|
||||
|
||||
```bash
|
||||
nano docker-compose.yaml
|
||||
```
|
||||
|
||||
5. Replace `127.0.0.1` with the server IP:
|
||||
|
||||
```bash
|
||||
args:
|
||||
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api
|
||||
- NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001
|
||||
```
|
||||
|
||||
6. Save and exit the editor
|
||||
|
||||
7. Rebuild and restart Perplexica:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
50
nginx.conf
50
nginx.conf
@ -1,50 +0,0 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
port_in_redirect on;
|
||||
absolute_redirect off;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost:8080;
|
||||
|
||||
# API requests
|
||||
location /api {
|
||||
proxy_pass http://perplexica-backend:3001;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# WebSocket requests
|
||||
location /ws {
|
||||
proxy_pass http://perplexica-backend:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Frontend requests
|
||||
location / {
|
||||
proxy_pass http://perplexica-frontend:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Security headers
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
server_tokens off;
|
||||
}
|
||||
}
|
@ -30,8 +30,8 @@
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@langchain/anthropic": "^0.2.3",
|
||||
"@langchain/community": "^0.2.16",
|
||||
"@langchain/openai": "^0.0.25",
|
||||
"@langchain/google-genai": "^0.0.23",
|
||||
"@langchain/openai": "^0.0.25",
|
||||
"@xenova/transformers": "^2.17.1",
|
||||
"axios": "^1.6.8",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
|
@ -3,6 +3,12 @@ PORT = 3001 # Port to run the server on
|
||||
SIMILARITY_MEASURE = "cosine" # "cosine" or "dot"
|
||||
KEEP_ALIVE = "5m" # How long to keep Ollama models loaded into memory. (Instead of using -1 use "-1m")
|
||||
|
||||
[SEARCH_ENGINE_BACKENDS] # "google" | "searxng" | "bing" | "brave" | "yacy"
|
||||
SEARCH = "searxng"
|
||||
IMAGE = "searxng"
|
||||
VIDEO = "searxng"
|
||||
NEWS = "searxng"
|
||||
|
||||
[MODELS.OPENAI]
|
||||
API_KEY = ""
|
||||
|
||||
@ -22,5 +28,18 @@ API_URL = ""
|
||||
[MODELS.OLLAMA]
|
||||
API_URL = "" # Ollama API URL - http://host.docker.internal:11434
|
||||
|
||||
[API_ENDPOINTS]
|
||||
SEARXNG = "http://localhost:32768" # SearxNG API URL
|
||||
[SEARCH_ENGINES.GOOGLE]
|
||||
API_KEY = ""
|
||||
CSE_ID = ""
|
||||
|
||||
[SEARCH_ENGINES.SEARXNG]
|
||||
ENDPOINT = ""
|
||||
|
||||
[SEARCH_ENGINES.BING]
|
||||
SUBSCRIPTION_KEY = ""
|
||||
|
||||
[SEARCH_ENGINES.BRAVE]
|
||||
API_KEY = ""
|
||||
|
||||
[SEARCH_ENGINES.YACY]
|
||||
ENDPOINT = ""
|
||||
|
@ -15,3 +15,5 @@ server:
|
||||
engines:
|
||||
- name: wolframalpha
|
||||
disabled: false
|
||||
- name: qwant
|
||||
disabled: true
|
||||
|
@ -7,7 +7,12 @@ import { PromptTemplate } from '@langchain/core/prompts';
|
||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
||||
import { BaseMessage } from '@langchain/core/messages';
|
||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
||||
import { searchSearxng } from '../lib/searxng';
|
||||
import { searchSearxng } from '../lib/searchEngines/searxng';
|
||||
import { searchGooglePSE } from '../lib/searchEngines/google_pse';
|
||||
import { searchBraveAPI } from '../lib/searchEngines/brave';
|
||||
import { searchYaCy } from '../lib/searchEngines/yacy';
|
||||
import { searchBingAPI } from '../lib/searchEngines/bing';
|
||||
import { getImageSearchEngineBackend } from '../config';
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
|
||||
const imageSearchChainPrompt = `
|
||||
@ -36,6 +41,103 @@ type ImageSearchChainInput = {
|
||||
query: string;
|
||||
};
|
||||
|
||||
async function performImageSearch(query: string) {
|
||||
const searchEngine = getImageSearchEngineBackend();
|
||||
let images = [];
|
||||
|
||||
switch (searchEngine) {
|
||||
case 'google': {
|
||||
const googleResult = await searchGooglePSE(query);
|
||||
images = googleResult.results
|
||||
.map((result) => {
|
||||
if (result.img_src && result.url && result.title) {
|
||||
return {
|
||||
img_src: result.img_src,
|
||||
url: result.url,
|
||||
title: result.title,
|
||||
source: result.displayLink,
|
||||
};
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'searxng': {
|
||||
const searxResult = await searchSearxng(query, {
|
||||
engines: ['google images', 'bing images'],
|
||||
pageno: 1,
|
||||
});
|
||||
searxResult.results.forEach((result) => {
|
||||
if (result.img_src && result.url && result.title) {
|
||||
images.push({
|
||||
img_src: result.img_src,
|
||||
url: result.url,
|
||||
title: result.title,
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'brave': {
|
||||
const braveResult = await searchBraveAPI(query);
|
||||
images = braveResult.results
|
||||
.map((result) => {
|
||||
if (result.img_src && result.url && result.title) {
|
||||
return {
|
||||
img_src: result.img_src,
|
||||
url: result.url,
|
||||
title: result.title,
|
||||
source: result.url,
|
||||
};
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'yacy': {
|
||||
const yacyResult = await searchYaCy(query);
|
||||
images = yacyResult.results
|
||||
.map((result) => {
|
||||
if (result.img_src && result.url && result.title) {
|
||||
return {
|
||||
img_src: result.img_src,
|
||||
url: result.url,
|
||||
title: result.title,
|
||||
source: result.url,
|
||||
};
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'bing': {
|
||||
const bingResult = await searchBingAPI(query);
|
||||
images = bingResult.results
|
||||
.map((result) => {
|
||||
if (result.img_src && result.url && result.title) {
|
||||
return {
|
||||
img_src: result.img_src,
|
||||
url: result.url,
|
||||
title: result.title,
|
||||
source: result.url,
|
||||
};
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown search engine ${searchEngine}`);
|
||||
}
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
const strParser = new StringOutputParser();
|
||||
|
||||
const createImageSearchChain = (llm: BaseChatModel) => {
|
||||
@ -52,22 +154,7 @@ const createImageSearchChain = (llm: BaseChatModel) => {
|
||||
llm,
|
||||
strParser,
|
||||
RunnableLambda.from(async (input: string) => {
|
||||
const res = await searchSearxng(input, {
|
||||
engines: ['bing images', 'google images'],
|
||||
});
|
||||
|
||||
const images = [];
|
||||
|
||||
res.results.forEach((result) => {
|
||||
if (result.img_src && result.url && result.title) {
|
||||
images.push({
|
||||
img_src: result.img_src,
|
||||
url: result.url,
|
||||
title: result.title,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const images = await performImageSearch(input);
|
||||
return images.slice(0, 10);
|
||||
}),
|
||||
]);
|
||||
|
@ -7,7 +7,11 @@ import { PromptTemplate } from '@langchain/core/prompts';
|
||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
||||
import { BaseMessage } from '@langchain/core/messages';
|
||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
||||
import { searchSearxng } from '../lib/searxng';
|
||||
import { searchSearxng } from '../lib/searchEngines/searxng';
|
||||
import { searchGooglePSE } from '../lib/searchEngines/google_pse';
|
||||
import { searchBraveAPI } from '../lib/searchEngines/brave';
|
||||
import { searchBingAPI } from '../lib/searchEngines/bing';
|
||||
import { getVideoSearchEngineBackend } from '../config';
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
|
||||
const VideoSearchChainPrompt = `
|
||||
@ -38,27 +42,36 @@ type VideoSearchChainInput = {
|
||||
|
||||
const strParser = new StringOutputParser();
|
||||
|
||||
const createVideoSearchChain = (llm: BaseChatModel) => {
|
||||
return RunnableSequence.from([
|
||||
RunnableMap.from({
|
||||
chat_history: (input: VideoSearchChainInput) => {
|
||||
return formatChatHistoryAsString(input.chat_history);
|
||||
},
|
||||
query: (input: VideoSearchChainInput) => {
|
||||
return input.query;
|
||||
},
|
||||
}),
|
||||
PromptTemplate.fromTemplate(VideoSearchChainPrompt),
|
||||
llm,
|
||||
strParser,
|
||||
RunnableLambda.from(async (input: string) => {
|
||||
const res = await searchSearxng(input, {
|
||||
async function performVideoSearch(query: string) {
|
||||
const searchEngine = getVideoSearchEngineBackend();
|
||||
const youtubeQuery = `${query} site:youtube.com`;
|
||||
let videos = [];
|
||||
|
||||
switch (searchEngine) {
|
||||
case 'google': {
|
||||
const googleResult = await searchGooglePSE(youtubeQuery);
|
||||
googleResult.results.forEach((result) => {
|
||||
// Use .results instead of .originalres
|
||||
if (result.img_src && result.url && result.title) {
|
||||
const videoId = new URL(result.url).searchParams.get('v');
|
||||
videos.push({
|
||||
img_src: result.img_src,
|
||||
url: result.url,
|
||||
title: result.title,
|
||||
iframe_src: videoId
|
||||
? `https://www.youtube.com/embed/${videoId}`
|
||||
: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'searxng': {
|
||||
const searxResult = await searchSearxng(query, {
|
||||
engines: ['youtube'],
|
||||
});
|
||||
|
||||
const videos = [];
|
||||
|
||||
res.results.forEach((result) => {
|
||||
searxResult.results.forEach((result) => {
|
||||
if (
|
||||
result.thumbnail &&
|
||||
result.url &&
|
||||
@ -73,7 +86,73 @@ const createVideoSearchChain = (llm: BaseChatModel) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'brave': {
|
||||
const braveResult = await searchBraveAPI(youtubeQuery);
|
||||
braveResult.results.forEach((result) => {
|
||||
if (result.img_src && result.url && result.title) {
|
||||
const videoId = new URL(result.url).searchParams.get('v');
|
||||
videos.push({
|
||||
img_src: result.img_src,
|
||||
url: result.url,
|
||||
title: result.title,
|
||||
iframe_src: videoId
|
||||
? `https://www.youtube.com/embed/${videoId}`
|
||||
: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'yacy': {
|
||||
console.log('Not available for yacy');
|
||||
videos = [];
|
||||
break;
|
||||
}
|
||||
|
||||
case 'bing': {
|
||||
const bingResult = await searchBingAPI(youtubeQuery);
|
||||
bingResult.results.forEach((result) => {
|
||||
if (result.img_src && result.url && result.title) {
|
||||
const videoId = new URL(result.url).searchParams.get('v');
|
||||
videos.push({
|
||||
img_src: result.img_src,
|
||||
url: result.url,
|
||||
title: result.title,
|
||||
iframe_src: videoId
|
||||
? `https://www.youtube.com/embed/${videoId}`
|
||||
: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown search engine ${searchEngine}`);
|
||||
}
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
const createVideoSearchChain = (llm: BaseChatModel) => {
|
||||
return RunnableSequence.from([
|
||||
RunnableMap.from({
|
||||
chat_history: (input: VideoSearchChainInput) => {
|
||||
return formatChatHistoryAsString(input.chat_history);
|
||||
},
|
||||
query: (input: VideoSearchChainInput) => {
|
||||
return input.query;
|
||||
},
|
||||
}),
|
||||
PromptTemplate.fromTemplate(VideoSearchChainPrompt),
|
||||
llm,
|
||||
strParser,
|
||||
RunnableLambda.from(async (input: string) => {
|
||||
const videos = await performVideoSearch(input);
|
||||
return videos.slice(0, 10);
|
||||
}),
|
||||
]);
|
||||
|
@ -10,6 +10,12 @@ interface Config {
|
||||
SIMILARITY_MEASURE: string;
|
||||
KEEP_ALIVE: string;
|
||||
};
|
||||
SEARCH_ENGINE_BACKENDS: {
|
||||
SEARCH: string;
|
||||
IMAGE: string;
|
||||
VIDEO: string;
|
||||
NEWS: string;
|
||||
};
|
||||
MODELS: {
|
||||
OPENAI: {
|
||||
API_KEY: string;
|
||||
@ -32,8 +38,23 @@ interface Config {
|
||||
MODEL_NAME: string;
|
||||
};
|
||||
};
|
||||
API_ENDPOINTS: {
|
||||
SEARXNG: string;
|
||||
SEARCH_ENGINES: {
|
||||
GOOGLE: {
|
||||
API_KEY: string;
|
||||
CSE_ID: string;
|
||||
};
|
||||
SEARXNG: {
|
||||
ENDPOINT: string;
|
||||
};
|
||||
BING: {
|
||||
SUBSCRIPTION_KEY: string;
|
||||
};
|
||||
BRAVE: {
|
||||
API_KEY: string;
|
||||
};
|
||||
YACY: {
|
||||
ENDPOINT: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@ -61,8 +82,32 @@ export const getAnthropicApiKey = () => loadConfig().MODELS.ANTHROPIC.API_KEY;
|
||||
|
||||
export const getGeminiApiKey = () => loadConfig().MODELS.GEMINI.API_KEY;
|
||||
|
||||
export const getSearchEngineBackend = () =>
|
||||
loadConfig().SEARCH_ENGINE_BACKENDS.SEARCH;
|
||||
|
||||
export const getImageSearchEngineBackend = () =>
|
||||
loadConfig().SEARCH_ENGINE_BACKENDS.IMAGE || getSearchEngineBackend();
|
||||
|
||||
export const getVideoSearchEngineBackend = () =>
|
||||
loadConfig().SEARCH_ENGINE_BACKENDS.VIDEO || getSearchEngineBackend();
|
||||
|
||||
export const getNewsSearchEngineBackend = () =>
|
||||
loadConfig().SEARCH_ENGINE_BACKENDS.NEWS || getSearchEngineBackend();
|
||||
|
||||
export const getGoogleApiKey = () => loadConfig().SEARCH_ENGINES.GOOGLE.API_KEY;
|
||||
|
||||
export const getGoogleCseId = () => loadConfig().SEARCH_ENGINES.GOOGLE.CSE_ID;
|
||||
|
||||
export const getBraveApiKey = () => loadConfig().SEARCH_ENGINES.BRAVE.API_KEY;
|
||||
|
||||
export const getBingSubscriptionKey = () =>
|
||||
loadConfig().SEARCH_ENGINES.BING.SUBSCRIPTION_KEY;
|
||||
|
||||
export const getYacyJsonEndpoint = () =>
|
||||
loadConfig().SEARCH_ENGINES.YACY.ENDPOINT;
|
||||
|
||||
export const getSearxngApiEndpoint = () =>
|
||||
process.env.SEARXNG_API_URL || loadConfig().API_ENDPOINTS.SEARXNG;
|
||||
process.env.SEARXNG_API_URL || loadConfig().SEARCH_ENGINES.SEARXNG.ENDPOINT;
|
||||
|
||||
export const getOllamaApiEndpoint = () => loadConfig().MODELS.OLLAMA.API_URL;
|
||||
|
||||
|
105
src/lib/searchEngines/bing.ts
Normal file
105
src/lib/searchEngines/bing.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import axios from 'axios';
|
||||
import { getBingSubscriptionKey } from '../../config';
|
||||
|
||||
interface BingAPISearchResult {
|
||||
_type: string;
|
||||
name: string;
|
||||
url: string;
|
||||
displayUrl: string;
|
||||
snippet?: string;
|
||||
dateLastCrawled?: string;
|
||||
thumbnailUrl?: string;
|
||||
contentUrl?: string;
|
||||
hostPageUrl?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
accentColor?: string;
|
||||
contentSize?: string;
|
||||
datePublished?: string;
|
||||
encodingFormat?: string;
|
||||
hostPageDisplayUrl?: string;
|
||||
id?: string;
|
||||
isLicensed?: boolean;
|
||||
isFamilyFriendly?: boolean;
|
||||
language?: string;
|
||||
mediaUrl?: string;
|
||||
motionThumbnailUrl?: string;
|
||||
publisher?: string;
|
||||
viewCount?: number;
|
||||
webSearchUrl?: string;
|
||||
primaryImageOfPage?: {
|
||||
thumbnailUrl?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
video?: {
|
||||
allowHttpsEmbed?: boolean;
|
||||
embedHtml?: string;
|
||||
allowMobileEmbed?: boolean;
|
||||
viewCount?: number;
|
||||
duration?: string;
|
||||
};
|
||||
image?: {
|
||||
thumbnail?: {
|
||||
contentUrl?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
imageInsightsToken?: string;
|
||||
imageId?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const searchBingAPI = async (query: string) => {
|
||||
try {
|
||||
const bingApiKey = await getBingSubscriptionKey();
|
||||
const url = new URL(`https://api.cognitive.microsoft.com/bing/v7.0/search`);
|
||||
url.searchParams.append('q', query);
|
||||
url.searchParams.append('responseFilter', 'Webpages,Images,Videos');
|
||||
|
||||
const res = await axios.get(url.toString(), {
|
||||
headers: {
|
||||
'Ocp-Apim-Subscription-Key': bingApiKey,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (res.data.error) {
|
||||
throw new Error(`Bing API Error: ${res.data.error.message}`);
|
||||
}
|
||||
|
||||
const originalres = res.data;
|
||||
|
||||
// Extract web, image, and video results
|
||||
const webResults = originalres.webPages?.value || [];
|
||||
const imageResults = originalres.images?.value || [];
|
||||
const videoResults = originalres.videos?.value || [];
|
||||
|
||||
const results = webResults.map((item: BingAPISearchResult) => ({
|
||||
title: item.name,
|
||||
url: item.url,
|
||||
content: item.snippet,
|
||||
img_src:
|
||||
item.primaryImageOfPage?.thumbnailUrl ||
|
||||
imageResults.find((img: any) => img.hostPageUrl === item.url)
|
||||
?.thumbnailUrl ||
|
||||
videoResults.find((vid: any) => vid.hostPageUrl === item.url)
|
||||
?.thumbnailUrl,
|
||||
...(item.video && {
|
||||
videoData: {
|
||||
duration: item.video.duration,
|
||||
embedUrl: item.video.embedHtml?.match(/src="(.*?)"/)?.[1],
|
||||
},
|
||||
publisher: item.publisher,
|
||||
datePublished: item.datePublished,
|
||||
}),
|
||||
}));
|
||||
|
||||
return { results, originalres };
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data
|
||||
? JSON.stringify(error.response.data, null, 2)
|
||||
: error.message || 'Unknown error';
|
||||
throw new Error(`Bing API Error: ${errorMessage}`);
|
||||
}
|
||||
};
|
102
src/lib/searchEngines/brave.ts
Normal file
102
src/lib/searchEngines/brave.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import axios from 'axios';
|
||||
import { getBraveApiKey } from '../../config';
|
||||
|
||||
interface BraveSearchResult {
|
||||
title: string;
|
||||
url: string;
|
||||
content?: string;
|
||||
img_src?: string;
|
||||
age?: string;
|
||||
family_friendly?: boolean;
|
||||
language?: string;
|
||||
video?: {
|
||||
embedUrl?: string;
|
||||
duration?: string;
|
||||
};
|
||||
rating?: {
|
||||
value: number;
|
||||
scale: number;
|
||||
};
|
||||
products?: Array<{
|
||||
name: string;
|
||||
price?: string;
|
||||
}>;
|
||||
recipe?: {
|
||||
ingredients?: string[];
|
||||
cookTime?: string;
|
||||
};
|
||||
meta?: {
|
||||
fetched?: string;
|
||||
lastCrawled?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const searchBraveAPI = async (
|
||||
query: string,
|
||||
numResults: number = 20,
|
||||
): Promise<{ results: BraveSearchResult[]; originalres: any }> => {
|
||||
try {
|
||||
const braveApiKey = await getBraveApiKey();
|
||||
const url = new URL(`https://api.search.brave.com/res/v1/web/search`);
|
||||
|
||||
url.searchParams.append('q', query);
|
||||
url.searchParams.append('count', numResults.toString());
|
||||
|
||||
const res = await axios.get(url.toString(), {
|
||||
headers: {
|
||||
'X-Subscription-Token': braveApiKey,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (res.data.error) {
|
||||
throw new Error(`Brave API Error: ${res.data.error.message}`);
|
||||
}
|
||||
|
||||
const originalres = res.data;
|
||||
const webResults = originalres.web?.results || [];
|
||||
|
||||
const results: BraveSearchResult[] = webResults.map((item: any) => ({
|
||||
title: item.title,
|
||||
url: item.url,
|
||||
content: item.description,
|
||||
img_src: item.thumbnail?.src || item.deep_results?.images?.[0]?.src,
|
||||
age: item.age,
|
||||
family_friendly: item.family_friendly,
|
||||
language: item.language,
|
||||
video: item.video
|
||||
? {
|
||||
embedUrl: item.video.embed_url,
|
||||
duration: item.video.duration,
|
||||
}
|
||||
: undefined,
|
||||
rating: item.rating
|
||||
? {
|
||||
value: item.rating.value,
|
||||
scale: item.rating.scale_max,
|
||||
}
|
||||
: undefined,
|
||||
products: item.deep_results?.product_cluster?.map((p: any) => ({
|
||||
name: p.name,
|
||||
price: p.price,
|
||||
})),
|
||||
recipe: item.recipe
|
||||
? {
|
||||
ingredients: item.recipe.ingredients,
|
||||
cookTime: item.recipe.cook_time,
|
||||
}
|
||||
: undefined,
|
||||
meta: {
|
||||
fetched: item.meta?.fetched,
|
||||
lastCrawled: item.meta?.last_crawled,
|
||||
},
|
||||
}));
|
||||
|
||||
return { results, originalres };
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data
|
||||
? JSON.stringify(error.response.data, null, 2)
|
||||
: error.message || 'Unknown error';
|
||||
throw new Error(`Brave API Error: ${errorMessage}`);
|
||||
}
|
||||
};
|
85
src/lib/searchEngines/google_pse.ts
Normal file
85
src/lib/searchEngines/google_pse.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import axios from 'axios';
|
||||
import { getGoogleApiKey, getGoogleCseId } from '../../config';
|
||||
|
||||
interface GooglePSESearchResult {
|
||||
kind: string;
|
||||
title: string;
|
||||
htmlTitle: string;
|
||||
link: string;
|
||||
displayLink: string;
|
||||
snippet?: string;
|
||||
htmlSnippet?: string;
|
||||
cacheId?: string;
|
||||
formattedUrl: string;
|
||||
htmlFormattedUrl: string;
|
||||
pagemap?: {
|
||||
videoobject: any;
|
||||
cse_thumbnail?: Array<{
|
||||
src: string;
|
||||
width: string;
|
||||
height: string;
|
||||
}>;
|
||||
metatags?: Array<{
|
||||
[key: string]: string;
|
||||
author?: string;
|
||||
}>;
|
||||
cse_image?: Array<{
|
||||
src: string;
|
||||
}>;
|
||||
};
|
||||
fileFormat?: string;
|
||||
image?: {
|
||||
contextLink: string;
|
||||
thumbnailLink: string;
|
||||
};
|
||||
mime?: string;
|
||||
labels?: Array<{
|
||||
name: string;
|
||||
displayName: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const searchGooglePSE = async (query: string) => {
|
||||
try {
|
||||
const [googleApiKey, googleCseID] = await Promise.all([
|
||||
getGoogleApiKey(),
|
||||
getGoogleCseId(),
|
||||
]);
|
||||
|
||||
const url = new URL(`https://www.googleapis.com/customsearch/v1`);
|
||||
url.searchParams.append('q', query);
|
||||
url.searchParams.append('cx', googleCseID);
|
||||
url.searchParams.append('key', googleApiKey);
|
||||
|
||||
const res = await axios.get(url.toString());
|
||||
|
||||
if (res.data.error) {
|
||||
throw new Error(`Google PSE Error: ${res.data.error.message}`);
|
||||
}
|
||||
|
||||
const originalres = res.data.items;
|
||||
|
||||
const results = originalres.map((item: GooglePSESearchResult) => ({
|
||||
title: item.title,
|
||||
url: item.link,
|
||||
content: item.snippet,
|
||||
img_src:
|
||||
item.pagemap?.cse_image?.[0]?.src ||
|
||||
item.pagemap?.cse_thumbnail?.[0]?.src ||
|
||||
item.image?.thumbnailLink,
|
||||
...(item.pagemap?.videoobject?.[0] && {
|
||||
videoData: {
|
||||
duration: item.pagemap.videoobject[0].duration,
|
||||
embedUrl: item.pagemap.videoobject[0].embedurl,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
return { results, originalres };
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data
|
||||
? JSON.stringify(error.response.data, null, 2)
|
||||
: error.message || 'Unknown error';
|
||||
throw new Error(`Google PSE Error: ${errorMessage}`);
|
||||
}
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { getSearxngApiEndpoint } from '../config';
|
||||
import { getSearxngApiEndpoint } from '../../config';
|
||||
|
||||
interface SearxngSearchOptions {
|
||||
categories?: string[];
|
79
src/lib/searchEngines/yacy.ts
Normal file
79
src/lib/searchEngines/yacy.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import axios from 'axios';
|
||||
import { getYacyJsonEndpoint } from '../../config';
|
||||
|
||||
interface YaCySearchResult {
|
||||
channels: {
|
||||
title: string;
|
||||
description: string;
|
||||
link: string;
|
||||
image: {
|
||||
url: string;
|
||||
title: string;
|
||||
link: string;
|
||||
};
|
||||
startIndex: string;
|
||||
itemsPerPage: string;
|
||||
searchTerms: string;
|
||||
items: {
|
||||
title: string;
|
||||
link: string;
|
||||
code: string;
|
||||
description: string;
|
||||
pubDate: string;
|
||||
image?: string;
|
||||
size: string;
|
||||
sizename: string;
|
||||
guid: string;
|
||||
faviconUrl: string;
|
||||
host: string;
|
||||
path: string;
|
||||
file: string;
|
||||
urlhash: string;
|
||||
ranking: string;
|
||||
}[];
|
||||
navigation: {
|
||||
facetname: string;
|
||||
displayname: string;
|
||||
type: string;
|
||||
min: string;
|
||||
max: string;
|
||||
mean: string;
|
||||
elements: {
|
||||
name: string;
|
||||
count: string;
|
||||
modifier: string;
|
||||
url: string;
|
||||
}[];
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export const searchYaCy = async (query: string, numResults: number = 20) => {
|
||||
try {
|
||||
const yacyBaseUrl = getYacyJsonEndpoint();
|
||||
|
||||
const url = new URL(`${yacyBaseUrl}/yacysearch.json`);
|
||||
url.searchParams.append('query', query);
|
||||
url.searchParams.append('count', numResults.toString());
|
||||
|
||||
const res = await axios.get(url.toString());
|
||||
|
||||
const originalres = res.data as YaCySearchResult;
|
||||
|
||||
const results = originalres.channels[0].items.map((item) => ({
|
||||
title: item.title,
|
||||
url: item.link,
|
||||
content: item.description,
|
||||
img_src: item.image || null,
|
||||
pubDate: item.pubDate,
|
||||
host: item.host,
|
||||
}));
|
||||
|
||||
return { results, originalres };
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data
|
||||
? JSON.stringify(error.response.data, null, 2)
|
||||
: error.message || 'Unknown error';
|
||||
throw new Error(`YaCy Error: ${errorMessage}`);
|
||||
}
|
||||
};
|
@ -50,7 +50,7 @@ export const academicSearchResponsePrompt = `
|
||||
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
|
||||
- If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query.
|
||||
- You are set on focus mode 'Academic', this means you will be searching for academic papers and articles on the web.
|
||||
|
||||
|
||||
### Example Output
|
||||
- Begin with a brief introduction summarizing the event or query topic.
|
||||
- Follow with detailed sections under clear headings, covering all aspects of the query if possible.
|
||||
|
@ -50,7 +50,7 @@ export const redditSearchResponsePrompt = `
|
||||
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
|
||||
- If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query.
|
||||
- You are set on focus mode 'Reddit', this means you will be searching for information, opinions and discussions on the web using Reddit.
|
||||
|
||||
|
||||
### Example Output
|
||||
- Begin with a brief introduction summarizing the event or query topic.
|
||||
- Follow with detailed sections under clear headings, covering all aspects of the query if possible.
|
||||
|
@ -50,7 +50,7 @@ export const wolframAlphaSearchResponsePrompt = `
|
||||
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
|
||||
- If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query.
|
||||
- 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.
|
||||
|
||||
|
||||
### Example Output
|
||||
- Begin with a brief introduction summarizing the event or query topic.
|
||||
- Follow with detailed sections under clear headings, covering all aspects of the query if possible.
|
||||
|
@ -50,7 +50,7 @@ export const youtubeSearchResponsePrompt = `
|
||||
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
|
||||
- If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query.
|
||||
- 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 transcrip
|
||||
|
||||
|
||||
### Example Output
|
||||
- Begin with a brief introduction summarizing the event or query topic.
|
||||
- Follow with detailed sections under clear headings, covering all aspects of the query if possible.
|
||||
|
@ -13,6 +13,16 @@ import {
|
||||
getCustomOpenaiApiUrl,
|
||||
getCustomOpenaiApiKey,
|
||||
getCustomOpenaiModelName,
|
||||
getSearchEngineBackend,
|
||||
getImageSearchEngineBackend,
|
||||
getVideoSearchEngineBackend,
|
||||
getNewsSearchEngineBackend,
|
||||
getSearxngApiEndpoint,
|
||||
getGoogleApiKey,
|
||||
getGoogleCseId,
|
||||
getBingSubscriptionKey,
|
||||
getBraveApiKey,
|
||||
getYacyJsonEndpoint,
|
||||
} from '../config';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
@ -60,6 +70,21 @@ router.get('/', async (_, res) => {
|
||||
config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl();
|
||||
config['customOpenaiApiKey'] = getCustomOpenaiApiKey();
|
||||
config['customOpenaiModelName'] = getCustomOpenaiModelName();
|
||||
|
||||
// Add search engine configuration
|
||||
config['searchEngineBackends'] = {
|
||||
search: getSearchEngineBackend(),
|
||||
image: getImageSearchEngineBackend(),
|
||||
video: getVideoSearchEngineBackend(),
|
||||
news: getNewsSearchEngineBackend(),
|
||||
};
|
||||
|
||||
config['searxngEndpoint'] = getSearxngApiEndpoint();
|
||||
config['googleApiKey'] = getGoogleApiKey();
|
||||
config['googleCseId'] = getGoogleCseId();
|
||||
config['bingSubscriptionKey'] = getBingSubscriptionKey();
|
||||
config['braveApiKey'] = getBraveApiKey();
|
||||
config['yacyEndpoint'] = getYacyJsonEndpoint();
|
||||
|
||||
res.status(200).json(config);
|
||||
} catch (err: any) {
|
||||
@ -94,6 +119,30 @@ router.post('/', async (req, res) => {
|
||||
MODEL_NAME: config.customOpenaiModelName,
|
||||
},
|
||||
},
|
||||
SEARCH_ENGINE_BACKENDS: config.searchEngineBackends ? {
|
||||
SEARCH: config.searchEngineBackends.search,
|
||||
IMAGE: config.searchEngineBackends.image,
|
||||
VIDEO: config.searchEngineBackends.video,
|
||||
NEWS: config.searchEngineBackends.news,
|
||||
} : undefined,
|
||||
SEARCH_ENGINES: {
|
||||
GOOGLE: {
|
||||
API_KEY: config.googleApiKey,
|
||||
CSE_ID: config.googleCseId,
|
||||
},
|
||||
SEARXNG: {
|
||||
ENDPOINT: config.searxngEndpoint,
|
||||
},
|
||||
BING: {
|
||||
SUBSCRIPTION_KEY: config.bingSubscriptionKey,
|
||||
},
|
||||
BRAVE: {
|
||||
API_KEY: config.braveApiKey,
|
||||
},
|
||||
YACY: {
|
||||
ENDPOINT: config.yacyEndpoint,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
updateConfig(updatedConfig);
|
||||
|
@ -1,42 +1,125 @@
|
||||
import express from 'express';
|
||||
import { searchSearxng } from '../lib/searxng';
|
||||
import { searchSearxng } from '../lib/searchEngines/searxng';
|
||||
import { searchGooglePSE } from '../lib/searchEngines/google_pse';
|
||||
import { searchBraveAPI } from '../lib/searchEngines/brave';
|
||||
import { searchYaCy } from '../lib/searchEngines/yacy';
|
||||
import { searchBingAPI } from '../lib/searchEngines/bing';
|
||||
import { getNewsSearchEngineBackend } from '../config';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
async function performSearch(query: string, site: string) {
|
||||
const searchEngine = getNewsSearchEngineBackend();
|
||||
switch (searchEngine) {
|
||||
case 'google': {
|
||||
const googleResult = await searchGooglePSE(query);
|
||||
|
||||
return googleResult.originalres.map((item) => {
|
||||
const imageSources = [
|
||||
item.pagemap?.cse_image?.[0]?.src,
|
||||
item.pagemap?.cse_thumbnail?.[0]?.src,
|
||||
item.pagemap?.metatags?.[0]?.['og:image'],
|
||||
item.pagemap?.metatags?.[0]?.['twitter:image'],
|
||||
item.pagemap?.metatags?.[0]?.['image'],
|
||||
].filter(Boolean); // Remove undefined values
|
||||
|
||||
return {
|
||||
title: item.title,
|
||||
url: item.link,
|
||||
content: item.snippet,
|
||||
thumbnail: imageSources[0], // First available image
|
||||
img_src: imageSources[0], // Same as thumbnail for consistency
|
||||
iframe_src: null,
|
||||
author: item.pagemap?.metatags?.[0]?.['og:site_name'] || site,
|
||||
publishedDate:
|
||||
item.pagemap?.metatags?.[0]?.['article:published_time'],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
case 'searxng': {
|
||||
const searxResult = await searchSearxng(query, {
|
||||
engines: ['bing news'],
|
||||
pageno: 1,
|
||||
});
|
||||
return searxResult.results;
|
||||
}
|
||||
|
||||
case 'brave': {
|
||||
const braveResult = await searchBraveAPI(query);
|
||||
return braveResult.results.map((item) => ({
|
||||
title: item.title,
|
||||
url: item.url,
|
||||
content: item.content,
|
||||
thumbnail: item.img_src,
|
||||
img_src: item.img_src,
|
||||
iframe_src: null,
|
||||
author: item.meta?.fetched || site,
|
||||
publishedDate: item.meta?.lastCrawled,
|
||||
}));
|
||||
}
|
||||
|
||||
case 'yacy': {
|
||||
const yacyResult = await searchYaCy(query);
|
||||
return yacyResult.results.map((item) => ({
|
||||
title: item.title,
|
||||
url: item.url,
|
||||
content: item.content,
|
||||
thumbnail: item.img_src,
|
||||
img_src: item.img_src,
|
||||
iframe_src: null,
|
||||
author: item?.host || site,
|
||||
publishedDate: item?.pubDate,
|
||||
}));
|
||||
}
|
||||
|
||||
case 'bing': {
|
||||
const bingResult = await searchBingAPI(query);
|
||||
return bingResult.results.map((item) => ({
|
||||
title: item.title,
|
||||
url: item.url,
|
||||
content: item.content,
|
||||
thumbnail: item.img_src,
|
||||
img_src: item.img_src,
|
||||
iframe_src: null,
|
||||
author: item?.publisher || site,
|
||||
publishedDate: item?.datePublished,
|
||||
}));
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown search engine ${searchEngine}`);
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const queries = [
|
||||
{ site: 'businessinsider.com', topic: 'AI' },
|
||||
{ site: 'www.exchangewire.com', topic: 'AI' },
|
||||
{ site: 'yahoo.com', topic: 'AI' },
|
||||
{ site: 'businessinsider.com', topic: 'tech' },
|
||||
{ site: 'www.exchangewire.com', topic: 'tech' },
|
||||
{ site: 'yahoo.com', topic: 'tech' },
|
||||
];
|
||||
|
||||
const data = (
|
||||
await Promise.all([
|
||||
searchSearxng('site:businessinsider.com AI', {
|
||||
engines: ['bing news'],
|
||||
pageno: 1,
|
||||
await Promise.all(
|
||||
queries.map(async ({ site, topic }) => {
|
||||
try {
|
||||
const query = `site:${site} ${topic}`;
|
||||
return await performSearch(query, site);
|
||||
} catch (error) {
|
||||
logger.error(`Error searching ${site}: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
searchSearxng('site:www.exchangewire.com AI', {
|
||||
engines: ['bing news'],
|
||||
pageno: 1,
|
||||
}),
|
||||
searchSearxng('site:yahoo.com AI', {
|
||||
engines: ['bing news'],
|
||||
pageno: 1,
|
||||
}),
|
||||
searchSearxng('site:businessinsider.com tech', {
|
||||
engines: ['bing news'],
|
||||
pageno: 1,
|
||||
}),
|
||||
searchSearxng('site:www.exchangewire.com tech', {
|
||||
engines: ['bing news'],
|
||||
pageno: 1,
|
||||
}),
|
||||
searchSearxng('site:yahoo.com tech', {
|
||||
engines: ['bing news'],
|
||||
pageno: 1,
|
||||
}),
|
||||
])
|
||||
)
|
||||
)
|
||||
.map((result) => result.results)
|
||||
.flat()
|
||||
.sort(() => Math.random() - 0.5);
|
||||
.sort(() => Math.random() - 0.5)
|
||||
.filter((item) => item.title && item.url && item.content);
|
||||
|
||||
return res.json({ blogs: data });
|
||||
} catch (err: any) {
|
||||
|
@ -17,7 +17,12 @@ import LineListOutputParser from '../lib/outputParsers/listLineOutputParser';
|
||||
import LineOutputParser from '../lib/outputParsers/lineOutputParser';
|
||||
import { getDocumentsFromLinks } from '../utils/documents';
|
||||
import { Document } from 'langchain/document';
|
||||
import { searchSearxng } from '../lib/searxng';
|
||||
import { searchSearxng } from '../lib/searchEngines/searxng';
|
||||
import { searchGooglePSE } from '../lib/searchEngines/google_pse';
|
||||
import { searchBingAPI } from '../lib/searchEngines/bing';
|
||||
import { searchBraveAPI } from '../lib/searchEngines/brave';
|
||||
import { searchYaCy } from '../lib/searchEngines/yacy';
|
||||
import { getSearchEngineBackend } from '../config';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import computeSimilarity from '../utils/computeSimilarity';
|
||||
@ -203,10 +208,37 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
||||
|
||||
return { query: question, docs: docs };
|
||||
} else {
|
||||
const res = await searchSearxng(question, {
|
||||
language: 'en',
|
||||
engines: this.config.activeEngines,
|
||||
});
|
||||
const searchEngine = getSearchEngineBackend();
|
||||
|
||||
let res;
|
||||
switch (searchEngine) {
|
||||
case 'searxng':
|
||||
res = await searchSearxng(question, {
|
||||
language: 'en',
|
||||
engines: this.config.activeEngines,
|
||||
});
|
||||
break;
|
||||
case 'google':
|
||||
res = await searchGooglePSE(question);
|
||||
break;
|
||||
case 'bing':
|
||||
res = await searchBingAPI(question);
|
||||
break;
|
||||
case 'brave':
|
||||
res = await searchBraveAPI(question);
|
||||
break;
|
||||
case 'yacy':
|
||||
res = await searchYaCy(question);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown search engine ${searchEngine}`);
|
||||
}
|
||||
|
||||
if (!res?.results) {
|
||||
throw new Error(
|
||||
`No results found for search engine: ${searchEngine}`,
|
||||
);
|
||||
}
|
||||
|
||||
const documents = res.results.map(
|
||||
(result) =>
|
||||
|
@ -23,6 +23,18 @@ interface SettingsType {
|
||||
customOpenaiApiKey: string;
|
||||
customOpenaiApiUrl: string;
|
||||
customOpenaiModelName: string;
|
||||
searchEngineBackends: {
|
||||
search: string;
|
||||
image: string;
|
||||
video: string;
|
||||
news: string;
|
||||
};
|
||||
searxngEndpoint: string;
|
||||
googleApiKey: string;
|
||||
googleCseId: string;
|
||||
bingSubscriptionKey: string;
|
||||
braveApiKey: string;
|
||||
yacyEndpoint: string;
|
||||
}
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
@ -112,6 +124,12 @@ const Page = () => {
|
||||
const [automaticImageSearch, setAutomaticImageSearch] = useState(false);
|
||||
const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false);
|
||||
const [savingStates, setSavingStates] = useState<Record<string, boolean>>({});
|
||||
const [searchEngineBackends, setSearchEngineBackends] = useState({
|
||||
search: '',
|
||||
image: '',
|
||||
video: '',
|
||||
news: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
@ -125,6 +143,16 @@ const Page = () => {
|
||||
const data = (await res.json()) as SettingsType;
|
||||
setConfig(data);
|
||||
|
||||
// Set search engine backends if they exist in the response
|
||||
if (data.searchEngineBackends) {
|
||||
setSearchEngineBackends({
|
||||
search: data.searchEngineBackends.search || '',
|
||||
image: data.searchEngineBackends.image || '',
|
||||
video: data.searchEngineBackends.video || '',
|
||||
news: data.searchEngineBackends.news || '',
|
||||
});
|
||||
}
|
||||
|
||||
const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {});
|
||||
const embeddingModelProvidersKeys = Object.keys(
|
||||
data.embeddingModelProviders || {},
|
||||
@ -331,6 +359,8 @@ const Page = () => {
|
||||
localStorage.setItem('embeddingModelProvider', value);
|
||||
} else if (key === 'embeddingModel') {
|
||||
localStorage.setItem('embeddingModel', value);
|
||||
} else if (key === 'searchEngineBackends') {
|
||||
localStorage.setItem('searchEngineBackends', value);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to save:', err);
|
||||
@ -793,6 +823,234 @@ const Page = () => {
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title="Search Engine Settings">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Default Search Engine
|
||||
</p>
|
||||
<Select
|
||||
value={searchEngineBackends.search}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSearchEngineBackends((prev) => ({
|
||||
...prev,
|
||||
search: value,
|
||||
}));
|
||||
saveConfig('searchEngineBackends', {
|
||||
...searchEngineBackends,
|
||||
search: value,
|
||||
});
|
||||
}}
|
||||
options={[
|
||||
{ value: 'searxng', label: 'SearXNG' },
|
||||
{ value: 'google', label: 'Google' },
|
||||
{ value: 'bing', label: 'Bing' },
|
||||
{ value: 'brave', label: 'Brave' },
|
||||
{ value: 'yacy', label: 'YaCy' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Image Search Engine
|
||||
</p>
|
||||
<Select
|
||||
value={searchEngineBackends.image}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSearchEngineBackends((prev) => ({
|
||||
...prev,
|
||||
image: value,
|
||||
}));
|
||||
saveConfig('searchEngineBackends', {
|
||||
...searchEngineBackends,
|
||||
image: value,
|
||||
});
|
||||
}}
|
||||
options={[
|
||||
{ value: '', label: 'Use Default Search Engine' },
|
||||
{ value: 'searxng', label: 'SearXNG' },
|
||||
{ value: 'google', label: 'Google' },
|
||||
{ value: 'bing', label: 'Bing' },
|
||||
{ value: 'brave', label: 'Brave' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Video Search Engine
|
||||
</p>
|
||||
<Select
|
||||
value={searchEngineBackends.video}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSearchEngineBackends((prev) => ({
|
||||
...prev,
|
||||
video: value,
|
||||
}));
|
||||
saveConfig('searchEngineBackends', {
|
||||
...searchEngineBackends,
|
||||
video: value,
|
||||
});
|
||||
}}
|
||||
options={[
|
||||
{ value: '', label: 'Use Default Search Engine' },
|
||||
{ value: 'searxng', label: 'SearXNG' },
|
||||
{ value: 'google', label: 'Google' },
|
||||
{ value: 'bing', label: 'Bing' },
|
||||
{ value: 'brave', label: 'Brave' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
News Search Engine
|
||||
</p>
|
||||
<Select
|
||||
value={searchEngineBackends.news}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSearchEngineBackends((prev) => ({
|
||||
...prev,
|
||||
news: value,
|
||||
}));
|
||||
saveConfig('searchEngineBackends', {
|
||||
...searchEngineBackends,
|
||||
news: value,
|
||||
});
|
||||
}}
|
||||
options={[
|
||||
{ value: '', label: 'Use Default Search Engine' },
|
||||
{ value: 'searxng', label: 'SearXNG' },
|
||||
{ value: 'google', label: 'Google' },
|
||||
{ value: 'bing', label: 'Bing' },
|
||||
{ value: 'brave', label: 'Brave' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-light-200 dark:border-dark-200">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
SearXNG Endpoint
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="SearXNG API Endpoint"
|
||||
value={config.searxngEndpoint || ''}
|
||||
isSaving={savingStates['searxngEndpoint']}
|
||||
onChange={(e) => {
|
||||
setConfig((prev) => ({
|
||||
...prev!,
|
||||
searxngEndpoint: e.target.value,
|
||||
}));
|
||||
}}
|
||||
onSave={(value) => saveConfig('searxngEndpoint', value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Google API Key
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Google API Key"
|
||||
value={config.googleApiKey || ''}
|
||||
isSaving={savingStates['googleApiKey']}
|
||||
onChange={(e) => {
|
||||
setConfig((prev) => ({
|
||||
...prev!,
|
||||
googleApiKey: e.target.value,
|
||||
}));
|
||||
}}
|
||||
onSave={(value) => saveConfig('googleApiKey', value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Google CSE ID
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Google Custom Search Engine ID"
|
||||
value={config.googleCseId || ''}
|
||||
isSaving={savingStates['googleCseId']}
|
||||
onChange={(e) => {
|
||||
setConfig((prev) => ({
|
||||
...prev!,
|
||||
googleCseId: e.target.value,
|
||||
}));
|
||||
}}
|
||||
onSave={(value) => saveConfig('googleCseId', value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Bing Subscription Key
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Bing Subscription Key"
|
||||
value={config.bingSubscriptionKey || ''}
|
||||
isSaving={savingStates['bingSubscriptionKey']}
|
||||
onChange={(e) => {
|
||||
setConfig((prev) => ({
|
||||
...prev!,
|
||||
bingSubscriptionKey: e.target.value,
|
||||
}));
|
||||
}}
|
||||
onSave={(value) => saveConfig('bingSubscriptionKey', value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Brave API Key
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Brave API Key"
|
||||
value={config.braveApiKey || ''}
|
||||
isSaving={savingStates['braveApiKey']}
|
||||
onChange={(e) => {
|
||||
setConfig((prev) => ({
|
||||
...prev!,
|
||||
braveApiKey: e.target.value,
|
||||
}));
|
||||
}}
|
||||
onSave={(value) => saveConfig('braveApiKey', value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
YaCy Endpoint
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="YaCy API Endpoint"
|
||||
value={config.yacyEndpoint || ''}
|
||||
isSaving={savingStates['yacyEndpoint']}
|
||||
onChange={(e) => {
|
||||
setConfig((prev) => ({
|
||||
...prev!,
|
||||
yacyEndpoint: e.target.value,
|
||||
}));
|
||||
}}
|
||||
onSave={(value) => saveConfig('yacyEndpoint', value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
@ -368,7 +368,7 @@ const loadMessages = async (
|
||||
|
||||
const ChatWindow = ({ id }: { id?: string }) => {
|
||||
const searchParams = useSearchParams();
|
||||
const initialMessage = searchParams?.get('q');
|
||||
const initialMessage = searchParams.get('q');
|
||||
|
||||
const [chatId, setChatId] = useState<string | undefined>(id);
|
||||
const [newChatCreated, setNewChatCreated] = useState(false);
|
||||
@ -378,9 +378,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
||||
|
||||
const [isWSReady, setIsWSReady] = useState(false);
|
||||
const ws = useSocket(
|
||||
process.env.NEXT_PUBLIC_WS_URL === 'auto'
|
||||
? `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`
|
||||
: process.env.NEXT_PUBLIC_WS_URL!,
|
||||
process.env.NEXT_PUBLIC_WS_URL!,
|
||||
setIsWSReady,
|
||||
setHasError,
|
||||
);
|
||||
|
@ -68,7 +68,13 @@ const MessageBox = ({
|
||||
return (
|
||||
<div>
|
||||
{message.role === 'user' && (
|
||||
<div className={cn('w-full', messageIndex === 0 ? 'pt-16' : 'pt-8', 'break-words')}>
|
||||
<div
|
||||
className={cn(
|
||||
'w-full',
|
||||
messageIndex === 0 ? 'pt-16' : 'pt-8',
|
||||
'break-words',
|
||||
)}
|
||||
>
|
||||
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
|
||||
{message.content}
|
||||
</h2>
|
||||
|
Reference in New Issue
Block a user