mirror of
				https://github.com/ItzCrazyKns/Perplexica.git
				synced 2025-10-30 19:08:15 +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