mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-04-30 08:12:26 +00:00
Compare commits
15 Commits
feat/deeps
...
e20d5ecc01
Author | SHA1 | Date | |
---|---|---|---|
|
e20d5ecc01 | ||
|
41b258e4d8 | ||
|
18533d58c2 | ||
|
54c71e33e0 | ||
|
da1123d84b | ||
|
627775c430 | ||
|
245573efca | ||
|
2c56aa3cb3 | ||
|
a85f762c58 | ||
|
3ddcceda0a | ||
|
e226645bc7 | ||
|
5447530ece | ||
|
ed6d46a440 | ||
|
bf705afc21 | ||
|
2e4433a6b3 |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "perplexica-frontend",
|
||||
"version": "1.10.1",
|
||||
"version": "1.10.2",
|
||||
"license": "MIT",
|
||||
"author": "ItzCrazyKns",
|
||||
"scripts": {
|
||||
|
@ -26,4 +26,8 @@ API_URL = "" # Ollama API URL - http://host.docker.internal:11434
|
||||
API_KEY = ""
|
||||
|
||||
[API_ENDPOINTS]
|
||||
SEARXNG = "" # SearxNG API URL - http://localhost:32768
|
||||
SEARXNG = "" # SearxNG API URL - http://localhost:32768
|
||||
TAVILY = "" # Tavily API key
|
||||
|
||||
[SEARCH]
|
||||
ENGINE = "searxng" # "searxng" or "tavily"
|
@ -8,6 +8,8 @@ import {
|
||||
getOllamaApiEndpoint,
|
||||
getOpenaiApiKey,
|
||||
getDeepseekApiKey,
|
||||
getSearchEngine,
|
||||
getTavilyApiKey,
|
||||
updateConfig,
|
||||
} from '@/lib/config';
|
||||
import {
|
||||
@ -58,6 +60,8 @@ export const GET = async (req: Request) => {
|
||||
config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl();
|
||||
config['customOpenaiApiKey'] = getCustomOpenaiApiKey();
|
||||
config['customOpenaiModelName'] = getCustomOpenaiModelName();
|
||||
config['searchEngine'] = getSearchEngine();
|
||||
config['tavilyApiKey'] = getTavilyApiKey();
|
||||
|
||||
return Response.json({ ...config }, { status: 200 });
|
||||
} catch (err) {
|
||||
@ -99,6 +103,12 @@ export const POST = async (req: Request) => {
|
||||
MODEL_NAME: config.customOpenaiModelName,
|
||||
},
|
||||
},
|
||||
SEARCH: {
|
||||
ENGINE: config.searchEngine,
|
||||
},
|
||||
API_ENDPOINTS: {
|
||||
TAVILY: config.tavilyApiKey || '',
|
||||
},
|
||||
};
|
||||
|
||||
updateConfig(updatedConfig);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { searchSearxng } from '@/lib/searxng';
|
||||
import { searchSearxng } from '../../../lib/searchEngines/searxng';
|
||||
|
||||
const articleWebsites = [
|
||||
'yahoo.com',
|
||||
|
@ -24,6 +24,8 @@ interface SettingsType {
|
||||
customOpenaiApiKey: string;
|
||||
customOpenaiApiUrl: string;
|
||||
customOpenaiModelName: string;
|
||||
searchEngine: string;
|
||||
tavilyApiKey?: string;
|
||||
}
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
@ -145,6 +147,7 @@ const Page = () => {
|
||||
const [automaticImageSearch, setAutomaticImageSearch] = useState(false);
|
||||
const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false);
|
||||
const [systemInstructions, setSystemInstructions] = useState<string>('');
|
||||
const [searchEngine, setSearchEngine] = useState<string>('searxng');
|
||||
const [savingStates, setSavingStates] = useState<Record<string, boolean>>({});
|
||||
|
||||
useEffect(() => {
|
||||
@ -207,6 +210,7 @@ const Page = () => {
|
||||
);
|
||||
|
||||
setSystemInstructions(localStorage.getItem('systemInstructions')!);
|
||||
setSearchEngine(localStorage.getItem('searchEngine') || 'searxng');
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
@ -366,6 +370,10 @@ const Page = () => {
|
||||
localStorage.setItem('embeddingModel', value);
|
||||
} else if (key === 'systemInstructions') {
|
||||
localStorage.setItem('systemInstructions', value);
|
||||
} else if (key === 'searchEngine') {
|
||||
localStorage.setItem('searchEngine', value);
|
||||
} else if (key === 'tavilyApiKey') {
|
||||
localStorage.setItem('tavilyApiKey', value);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to save:', err);
|
||||
@ -508,6 +516,32 @@ const Page = () => {
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1 mt-2">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Search Engine
|
||||
</p>
|
||||
<Select
|
||||
value={searchEngine}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSearchEngine(value);
|
||||
saveConfig('searchEngine', value);
|
||||
}}
|
||||
options={[
|
||||
{ value: 'searxng', label: 'SearxNG' },
|
||||
...(config.tavilyApiKey ? [{ value: 'tavily', label: 'Tavily' }] : []),
|
||||
]}
|
||||
/>
|
||||
<p className="text-xs text-black/60 dark:text-white/60 mt-1">
|
||||
Select which search engine to use for web searches
|
||||
</p>
|
||||
{searchEngine === 'tavily' && !config.tavilyApiKey && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
Tavily API key is required to use this search engine
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
@ -858,6 +892,32 @@ const Page = () => {
|
||||
onSave={(value) => saveConfig('deepseekApiKey', value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1 mt-4 pt-4 border-t border-light-200 dark:border-dark-200">
|
||||
<p className="text-black/90 dark:text-white/90 font-medium">Search Engine API Keys</p>
|
||||
<p className="text-sm text-black/60 dark:text-white/60 mt-0.5">
|
||||
API keys for search engines used in the application
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Tavily API Key
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Tavily API key"
|
||||
value={config.tavilyApiKey || ''}
|
||||
isSaving={savingStates['tavilyApiKey']}
|
||||
onChange={(e) => {
|
||||
setConfig((prev) => ({
|
||||
...prev!,
|
||||
tavilyApiKey: e.target.value,
|
||||
}));
|
||||
}}
|
||||
onSave={(value) => saveConfig('tavilyApiKey', value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
|
@ -48,6 +48,7 @@ const MessageBox = ({
|
||||
const [speechMessage, setSpeechMessage] = useState(message.content);
|
||||
|
||||
useEffect(() => {
|
||||
const citationRegex = /\[([^\]]+)\]/g;
|
||||
const regex = /\[(\d+)\]/g;
|
||||
let processedMessage = message.content;
|
||||
|
||||
@ -67,13 +68,36 @@ const MessageBox = ({
|
||||
) {
|
||||
setParsedMessage(
|
||||
processedMessage.replace(
|
||||
regex,
|
||||
(_, number) =>
|
||||
`<a href="${
|
||||
message.sources?.[number - 1]?.metadata?.url
|
||||
}" target="_blank" className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative">${number}</a>`,
|
||||
citationRegex,
|
||||
(_, capturedContent: string) => {
|
||||
const numbers = capturedContent
|
||||
.split(',')
|
||||
.map((numStr) => numStr.trim());
|
||||
|
||||
const linksHtml = numbers
|
||||
.map((numStr) => {
|
||||
const number = parseInt(numStr);
|
||||
|
||||
if (isNaN(number) || number <= 0) {
|
||||
return `[${numStr}]`;
|
||||
}
|
||||
|
||||
const source = message.sources?.[number - 1];
|
||||
const url = source?.metadata?.url;
|
||||
|
||||
if (url) {
|
||||
return `<a href="${url}" target="_blank" className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative">${numStr}</a>`;
|
||||
} else {
|
||||
return `[${numStr}]`;
|
||||
}
|
||||
})
|
||||
.join('');
|
||||
|
||||
return linksHtml;
|
||||
},
|
||||
),
|
||||
);
|
||||
setSpeechMessage(message.content.replace(regex, ''));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ 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 '../searxng';
|
||||
import { searchSearxng } from '../searchEngines/searxng';
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
|
||||
const imageSearchChainPrompt = `
|
||||
|
@ -7,7 +7,7 @@ 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 '../searxng';
|
||||
import { searchSearxng } from '../searchEngines/searxng';
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
|
||||
const VideoSearchChainPrompt = `
|
||||
|
@ -36,6 +36,10 @@ interface Config {
|
||||
};
|
||||
API_ENDPOINTS: {
|
||||
SEARXNG: string;
|
||||
TAVILY: string;
|
||||
};
|
||||
SEARCH: {
|
||||
ENGINE: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -64,6 +68,12 @@ export const getGeminiApiKey = () => loadConfig().MODELS.GEMINI.API_KEY;
|
||||
export const getSearxngApiEndpoint = () =>
|
||||
process.env.SEARXNG_API_URL || loadConfig().API_ENDPOINTS.SEARXNG;
|
||||
|
||||
export const getTavilyApiKey = () =>
|
||||
process.env.TAVILY_API_KEY || loadConfig().API_ENDPOINTS.TAVILY;
|
||||
|
||||
export const getSearchEngine = () =>
|
||||
process.env.SEARCH_ENGINE || loadConfig().SEARCH?.ENGINE || 'searxng';
|
||||
|
||||
export const getOllamaApiEndpoint = () => loadConfig().MODELS.OLLAMA.API_URL;
|
||||
|
||||
export const getDeepseekApiKey = () => loadConfig().MODELS.DEEPSEEK.API_KEY;
|
||||
|
@ -40,8 +40,12 @@ const geminiChatModels: Record<string, string>[] = [
|
||||
|
||||
const geminiEmbeddingModels: Record<string, string>[] = [
|
||||
{
|
||||
displayName: 'Gemini Embedding',
|
||||
key: 'gemini-embedding-exp',
|
||||
displayName: 'Text Embedding 004',
|
||||
key: 'models/text-embedding-004',
|
||||
},
|
||||
{
|
||||
displayName: 'Embedding 001',
|
||||
key: 'models/embedding-001',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -72,6 +72,14 @@ const groqChatModels: Record<string, string>[] = [
|
||||
displayName: 'Llama 3.2 90B Vision Preview (Preview)',
|
||||
key: 'llama-3.2-90b-vision-preview',
|
||||
},
|
||||
/* {
|
||||
displayName: 'Llama 4 Maverick 17B 128E Instruct (Preview)',
|
||||
key: 'meta-llama/llama-4-maverick-17b-128e-instruct',
|
||||
}, */
|
||||
{
|
||||
displayName: 'Llama 4 Scout 17B 16E Instruct (Preview)',
|
||||
key: 'meta-llama/llama-4-scout-17b-16e-instruct',
|
||||
},
|
||||
];
|
||||
|
||||
export const loadGroqChatModels = async () => {
|
||||
|
@ -17,7 +17,9 @@ import LineListOutputParser from '../outputParsers/listLineOutputParser';
|
||||
import LineOutputParser from '../outputParsers/lineOutputParser';
|
||||
import { getDocumentsFromLinks } from '../utils/documents';
|
||||
import { Document } from 'langchain/document';
|
||||
import { searchSearxng } from '../searxng';
|
||||
import { searchTavily } from '../searchEngines/tavily';
|
||||
import { searchSearxng } from '../searchEngines/searxng';
|
||||
import { getSearchEngine } from '../config';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import computeSimilarity from '../utils/computeSimilarity';
|
||||
@ -205,25 +207,42 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
||||
} else {
|
||||
question = question.replace(/<think>.*?<\/think>/g, '');
|
||||
|
||||
const res = await searchSearxng(question, {
|
||||
language: 'en',
|
||||
engines: this.config.activeEngines,
|
||||
});
|
||||
const searchEngine = getSearchEngine();
|
||||
|
||||
const documents = res.results.map(
|
||||
(result) =>
|
||||
new Document({
|
||||
pageContent:
|
||||
result.content ||
|
||||
(this.config.activeEngines.includes('youtube')
|
||||
? result.title
|
||||
: '') /* Todo: Implement transcript grabbing using Youtubei (source: https://www.npmjs.com/package/youtubei) */,
|
||||
metadata: {
|
||||
title: result.title,
|
||||
url: result.url,
|
||||
...(result.img_src && { img_src: result.img_src }),
|
||||
},
|
||||
}),
|
||||
let res;
|
||||
|
||||
if (searchEngine === 'tavily') {
|
||||
res = await searchTavily(question, {
|
||||
search_depth: 'basic',
|
||||
max_results: 15,
|
||||
include_images: true,
|
||||
});
|
||||
} else {
|
||||
// Default to SearxNG
|
||||
res = await searchSearxng(question, {
|
||||
language: 'en',
|
||||
engines: this.config.activeEngines,
|
||||
});
|
||||
}
|
||||
|
||||
let documents: Document[] = [];
|
||||
|
||||
documents = documents.concat(
|
||||
res.results.map(
|
||||
(result) =>
|
||||
new Document({
|
||||
pageContent:
|
||||
result.content ||
|
||||
(this.config.activeEngines.includes('youtube')
|
||||
? result.title
|
||||
: ''),
|
||||
metadata: {
|
||||
title: result.title,
|
||||
url: result.url,
|
||||
...(result.img_src ? { img_src: result.img_src } : {}),
|
||||
},
|
||||
}),
|
||||
)
|
||||
);
|
||||
|
||||
return { query: question, docs: documents };
|
||||
|
@ -1,5 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { getSearxngApiEndpoint } from './config';
|
||||
import { getSearxngApiEndpoint } from '../config';
|
||||
|
||||
interface SearxngSearchOptions {
|
||||
categories?: string[];
|
79
src/lib/searchEngines/tavily.ts
Normal file
79
src/lib/searchEngines/tavily.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import axios from 'axios';
|
||||
import { getTavilyApiKey } from '../config';
|
||||
|
||||
interface TavilySearchOptions {
|
||||
topic?: 'general' | 'news';
|
||||
search_depth?: 'basic' | 'advanced';
|
||||
chunks_per_source?: number;
|
||||
max_results?: number;
|
||||
time_range?: 'day' | 'week' | 'month' | 'year' | 'd' | 'w' | 'm' | 'y';
|
||||
days?: number;
|
||||
include_answer?: boolean | 'basic' | 'advanced';
|
||||
include_raw_content?: boolean;
|
||||
include_images?: boolean;
|
||||
include_image_descriptions?: boolean;
|
||||
include_domains?: string[];
|
||||
exclude_domains?: string[];
|
||||
}
|
||||
|
||||
interface TavilySearchResult {
|
||||
title: string;
|
||||
url: string;
|
||||
content: string;
|
||||
score: number;
|
||||
raw_content?: string;
|
||||
}
|
||||
|
||||
interface TavilySearchResponse {
|
||||
query: string;
|
||||
answer?: string;
|
||||
images?: Array<{
|
||||
url: string;
|
||||
description?: string;
|
||||
}>;
|
||||
results: TavilySearchResult[];
|
||||
response_time: string;
|
||||
}
|
||||
|
||||
export const searchTavily = async (
|
||||
query: string,
|
||||
opts?: TavilySearchOptions,
|
||||
) => {
|
||||
const tavilyApiKey = getTavilyApiKey();
|
||||
|
||||
if (!tavilyApiKey) {
|
||||
throw new Error('Tavily API key is not configured');
|
||||
}
|
||||
|
||||
const url = 'https://api.tavily.com/search';
|
||||
|
||||
const response = await axios.post<TavilySearchResponse>(
|
||||
url,
|
||||
{
|
||||
query,
|
||||
...opts,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${tavilyApiKey}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const results = response.data.results;
|
||||
|
||||
// Convert Tavily results to match the format expected by the rest of the application
|
||||
const formattedResults = results.map(result => ({
|
||||
title: result.title,
|
||||
url: result.url,
|
||||
content: result.content,
|
||||
img_src: undefined, // Tavily doesn't provide image URLs in the standard response
|
||||
}));
|
||||
|
||||
return {
|
||||
results: formattedResults,
|
||||
suggestions: [], // Tavily doesn't provide suggestions, so return empty array
|
||||
answer: response.data.answer, // Include the AI-generated answer if available
|
||||
};
|
||||
};
|
Reference in New Issue
Block a user