diff --git a/public/opensearch.xml b/public/opensearch.xml new file mode 100644 index 0000000..7a9bc99 --- /dev/null +++ b/public/opensearch.xml @@ -0,0 +1,9 @@ + + + Perplexica + Search with Perplexica AI + UTF-8 + /favicon.ico + + + diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 42582ed..d71856d 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -7,7 +7,7 @@ import db from '@/lib/db'; import { chats, messages as messagesSchema } from '@/lib/db/schema'; import { getAvailableChatModelProviders, - getAvailableEmbeddingModelProviders + getAvailableEmbeddingModelProviders, } from '@/lib/providers'; import { searchHandlers } from '@/lib/search'; import { getFileDetails } from '@/lib/utils/files'; @@ -66,6 +66,7 @@ const handleEmitterEvents = async ( let recievedMessage = ''; let sources: any[] = []; let searchQuery: string | undefined; + let searchUrl: string | undefined; stream.on('data', (data) => { const parsedData = JSON.parse(data); @@ -86,6 +87,9 @@ const handleEmitterEvents = async ( if (parsedData.searchQuery) { searchQuery = parsedData.searchQuery; } + if (parsedData.searchUrl) { + searchUrl = parsedData.searchUrl; + } writer.write( encoder.encode( @@ -94,6 +98,7 @@ const handleEmitterEvents = async ( data: parsedData.data, searchQuery: parsedData.searchQuery, messageId: aiMessageId, + searchUrl: searchUrl, }) + '\n', ), ); @@ -128,6 +133,7 @@ const handleEmitterEvents = async ( messageId: aiMessageId, modelStats: modelStats, searchQuery: searchQuery, + searchUrl: searchUrl, }) + '\n', ), ); @@ -144,6 +150,7 @@ const handleEmitterEvents = async ( ...(sources && sources.length > 0 && { sources }), ...(searchQuery && { searchQuery }), modelStats: modelStats, + ...(searchUrl && { searchUrl }), }), }) .execute(); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 684a99c..f90be8a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -26,6 +26,14 @@ export default function RootLayout({ }>) { return ( + + + {children} diff --git a/src/components/ChatWindow.tsx b/src/components/ChatWindow.tsx index 803d720..fa76796 100644 --- a/src/components/ChatWindow.tsx +++ b/src/components/ChatWindow.tsx @@ -28,6 +28,7 @@ export type Message = { sources?: Document[]; modelStats?: ModelStats; searchQuery?: string; + searchUrl?: string; }; export interface File { @@ -417,7 +418,6 @@ const ChatWindow = ({ id }: { id?: string }) => { if (data.type === 'sources') { sources = data.data; - const searchQuery = data.searchQuery; if (!added) { setMessages((prevMessages) => [ ...prevMessages, @@ -427,7 +427,8 @@ const ChatWindow = ({ id }: { id?: string }) => { chatId: chatId!, role: 'assistant', sources: sources, - searchQuery: searchQuery, + searchQuery: data.searchQuery, + searchUrl: data.searchUrl, createdAt: new Date(), }, ]); @@ -486,6 +487,7 @@ const ChatWindow = ({ id }: { id?: string }) => { modelStats: data.modelStats || null, // Make sure the searchQuery is preserved (if available in the message data) searchQuery: message.searchQuery || data.searchQuery, + searchUrl: message.searchUrl || data.searchUrl, }; } return message; diff --git a/src/components/MessageBox.tsx b/src/components/MessageBox.tsx index 0b08650..aff79c7 100644 --- a/src/components/MessageBox.tsx +++ b/src/components/MessageBox.tsx @@ -280,8 +280,23 @@ const MessageBox = ({ {message.searchQuery && (
- Search query:{' '} - {message.searchQuery} + + Search query: + {' '} + {message.searchUrl ? ( + + {message.searchQuery} + + ) : ( + + {message.searchQuery} + + )}
)} diff --git a/src/components/MessageInput.tsx b/src/components/MessageInput.tsx index 3303dea..2bd0dec 100644 --- a/src/components/MessageInput.tsx +++ b/src/components/MessageInput.tsx @@ -86,7 +86,7 @@ const MessageInput = ({ setMessage(''); }; - return ( + return (
{ e.preventDefault(); @@ -107,7 +107,7 @@ const MessageInput = ({ onChange={(e) => setMessage(e.target.value)} minRows={2} className="bg-transparent placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48" - placeholder={firstMessage ? "Ask anything..." :"Ask a follow-up"} + placeholder={firstMessage ? 'Ask anything...' : 'Ask a follow-up'} />
@@ -134,7 +134,11 @@ const MessageInput = ({ className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 disabled:bg-[#e0e0dc] dark:disabled:bg-[#ececec21] hover:bg-opacity-85 transition duration-100 rounded-full p-2" type="submit" > - {firstMessage ? : } + {firstMessage ? ( + + ) : ( + + )}
diff --git a/src/lib/chains/suggestionGeneratorAgent.ts b/src/lib/chains/suggestionGeneratorAgent.ts index 9129059..aa3464b 100644 --- a/src/lib/chains/suggestionGeneratorAgent.ts +++ b/src/lib/chains/suggestionGeneratorAgent.ts @@ -10,6 +10,7 @@ const suggestionGeneratorPrompt = ` You are an AI suggestion generator for an AI powered search engine. You will be given a conversation below. You need to generate 4-5 suggestions based on the conversation. The suggestion should be relevant to the conversation that can be used by the user to ask the chat model for more information. You need to make sure the suggestions are relevant to the conversation and are helpful to the user. Keep a note that the user might use these suggestions to ask a chat model for more information. Make sure the suggestions are medium in length and are informative and relevant to the conversation. +If you are a thinking or reasoning AI, you should avoid using \`\` and \`\` tags in your thinking. Those tags should only be used in the final output. Provide these suggestions separated by newlines between the XML tags and . For example: diff --git a/src/lib/outputParsers/lineOutputParser.ts b/src/lib/outputParsers/lineOutputParser.ts index fcd69ef..a8a21af 100644 --- a/src/lib/outputParsers/lineOutputParser.ts +++ b/src/lib/outputParsers/lineOutputParser.ts @@ -21,6 +21,10 @@ class LineOutputParser extends BaseOutputParser { async parse(text: string): Promise { text = text.trim() || ''; + // First, remove all ... blocks to avoid parsing tags inside thinking content + // This might be a little aggressive. Prompt massaging might be all we need, but this is a guarantee and should rarely mess anything up. + text = this.removeThinkingBlocks(text); + const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/; const startKeyIndex = text.indexOf(`<${this.key}>`); const endKeyIndex = text.indexOf(``); @@ -40,6 +44,17 @@ class LineOutputParser extends BaseOutputParser { return line; } + /** + * Removes all content within ... blocks + * @param text The input text containing thinking blocks + * @returns The text with all thinking blocks removed + */ + private removeThinkingBlocks(text: string): string { + // Use regex to identify and remove all ... blocks + // Using the 's' flag to make dot match newlines + return text.replace(/[\s\S]*?<\/think>/g, '').trim(); + } + getFormatInstructions(): string { throw new Error('Not implemented.'); } diff --git a/src/lib/outputParsers/listLineOutputParser.ts b/src/lib/outputParsers/listLineOutputParser.ts index 6409db9..eb41b95 100644 --- a/src/lib/outputParsers/listLineOutputParser.ts +++ b/src/lib/outputParsers/listLineOutputParser.ts @@ -21,6 +21,10 @@ class LineListOutputParser extends BaseOutputParser { async parse(text: string): Promise { text = text.trim() || ''; + // First, remove all ... blocks to avoid parsing tags inside thinking content + // This might be a little aggressive. Prompt massaging might be all we need, but this is a guarantee and should rarely mess anything up. + text = this.removeThinkingBlocks(text); + const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/; const startKeyIndex = text.indexOf(`<${this.key}>`); const endKeyIndex = text.indexOf(``); @@ -42,6 +46,17 @@ class LineListOutputParser extends BaseOutputParser { return lines; } + /** + * Removes all content within ... blocks + * @param text The input text containing thinking blocks + * @returns The text with all thinking blocks removed + */ + private removeThinkingBlocks(text: string): string { + // Use regex to identify and remove all ... blocks + // Using [\s\S] pattern to match all characters including newlines + return text.replace(/[\s\S]*?<\/think>/g, '').trim(); + } + getFormatInstructions(): string { throw new Error('Not implemented.'); } diff --git a/src/lib/prompts/webSearch.ts b/src/lib/prompts/webSearch.ts index f6cc283..dcc29b6 100644 --- a/src/lib/prompts/webSearch.ts +++ b/src/lib/prompts/webSearch.ts @@ -3,6 +3,7 @@ You are an AI question rephraser. You will be given a conversation and a follow- If it is a simple writing task or a greeting (unless the greeting contains a question after it) like Hi, Hello, How are you, etc. than a question then you need to return \`not_needed\` as the response (This is because the LLM won't need to search the web for finding information on this topic). If the user asks some question from some URL or wants you to summarize a PDF or a webpage (via URL) you need to return the links inside the \`links\` XML block and the question inside the \`question\` XML block. If the user wants to you to summarize the webpage or the PDF you need to return \`summarize\` inside the \`question\` XML block in place of a question and the link to summarize in the \`links\` XML block. You must always return the rephrased question inside the \`question\` XML block, if there are no links in the follow-up question then don't insert a \`links\` XML block in your response. +If you are a thinking or reasoning AI, you should avoid using \`\` and \`\` tags in your thinking. Those tags should only be used in the final output. You should also avoid using \`\` and \`\` tags in your thinking. Those tags should only be used in the final output. There are several examples attached for your reference inside the below \`examples\` XML block diff --git a/src/lib/search/metaSearchAgent.ts b/src/lib/search/metaSearchAgent.ts index a7cbdf1..bf1c565 100644 --- a/src/lib/search/metaSearchAgent.ts +++ b/src/lib/search/metaSearchAgent.ts @@ -56,6 +56,7 @@ class MetaSearchAgent implements MetaSearchAgentType { private config: Config; private strParser = new StringOutputParser(); private searchQuery?: string; + private searxngUrl?: string; constructor(config: Config) { this.config = config; @@ -81,6 +82,7 @@ class MetaSearchAgent implements MetaSearchAgentType { let question = this.config.summarizer ? await questionOutputParser.parse(input) : input; + console.log('question', question); if (question === 'not_needed') { return { query: '', docs: [] }; @@ -206,12 +208,15 @@ class MetaSearchAgent implements MetaSearchAgentType { } else { question = question.replace(/.*?<\/think>/g, ''); - const res = await searchSearxng(question, { + const searxngResult = await searchSearxng(question, { language: 'en', engines: this.config.activeEngines, }); - const documents = res.results.map( + // Store the SearXNG URL for later use in emitting to the client + this.searxngUrl = searxngResult.searchUrl; + + const documents = searxngResult.results.map( (result) => new Document({ pageContent: @@ -265,7 +270,7 @@ class MetaSearchAgent implements MetaSearchAgentType { query = searchRetrieverResult.query; docs = searchRetrieverResult.docs; - + // Store the search query in the context for emitting to the client if (searchRetrieverResult.searchQuery) { this.searchQuery = searchRetrieverResult.searchQuery; @@ -447,16 +452,15 @@ class MetaSearchAgent implements MetaSearchAgentType { event.event === 'on_chain_end' && event.name === 'FinalSourceRetriever' ) { - // Add searchQuery to the sources data if it exists const sourcesData = event.data.output; - // @ts-ignore - we added searchQuery property if (this.searchQuery) { emitter.emit( 'data', - JSON.stringify({ - type: 'sources', + JSON.stringify({ + type: 'sources', data: sourcesData, - searchQuery: this.searchQuery + searchQuery: this.searchQuery, + searchUrl: this.searxngUrl, }), ); } else { diff --git a/src/lib/searxng.ts b/src/lib/searxng.ts index ae19db2..92dc2fc 100644 --- a/src/lib/searxng.ts +++ b/src/lib/searxng.ts @@ -19,6 +19,12 @@ interface SearxngSearchResult { iframe_src?: string; } +interface SearxngResponse { + results: SearxngSearchResult[]; + suggestions: string[]; + searchUrl: string; +} + export const searchSearxng = async ( query: string, opts?: SearxngSearchOptions, @@ -44,5 +50,16 @@ export const searchSearxng = async ( const results: SearxngSearchResult[] = res.data.results; const suggestions: string[] = res.data.suggestions; - return { results, suggestions }; + // Create a URL for viewing the search results in the SearXNG web interface + const searchUrl = new URL(searxngURL); + searchUrl.pathname = '/search'; + searchUrl.searchParams.append('q', query); + if (opts?.engines?.length) { + searchUrl.searchParams.append('engines', opts.engines.join(',')); + } + if (opts?.language) { + searchUrl.searchParams.append('language', opts.language); + } + + return { results, suggestions, searchUrl: searchUrl.toString() }; };