From 1a8889c71c122c49316b8f3625899827b31bb2b1 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:45:48 +0530 Subject: [PATCH 001/196] feat(app): add new agents directory --- src/app/api/images/route.ts | 2 +- src/app/api/suggestions/route.ts | 2 +- src/app/api/videos/route.ts | 2 +- .../imageSearchAgent.ts => agents/media/image.ts} | 10 ++++++---- .../videoSearchAgent.ts => agents/media/video.ts} | 6 +++--- .../suggestions/index.ts} | 4 ++-- 6 files changed, 14 insertions(+), 12 deletions(-) rename src/lib/{chains/imageSearchAgent.ts => agents/media/image.ts} (90%) rename src/lib/{chains/videoSearchAgent.ts => agents/media/video.ts} (94%) rename src/lib/{chains/suggestionGeneratorAgent.ts => agents/suggestions/index.ts} (93%) diff --git a/src/app/api/images/route.ts b/src/app/api/images/route.ts index d3416ca..71d679e 100644 --- a/src/app/api/images/route.ts +++ b/src/app/api/images/route.ts @@ -1,4 +1,4 @@ -import handleImageSearch from '@/lib/chains/imageSearchAgent'; +import handleImageSearch from '@/lib/agents/media/image'; import ModelRegistry from '@/lib/models/registry'; import { ModelWithProvider } from '@/lib/models/types'; import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; diff --git a/src/app/api/suggestions/route.ts b/src/app/api/suggestions/route.ts index d8312cf..f942758 100644 --- a/src/app/api/suggestions/route.ts +++ b/src/app/api/suggestions/route.ts @@ -1,4 +1,4 @@ -import generateSuggestions from '@/lib/chains/suggestionGeneratorAgent'; +import generateSuggestions from '@/lib/agents/suggestions'; import ModelRegistry from '@/lib/models/registry'; import { ModelWithProvider } from '@/lib/models/types'; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; diff --git a/src/app/api/videos/route.ts b/src/app/api/videos/route.ts index 02e5909..0ace57f 100644 --- a/src/app/api/videos/route.ts +++ b/src/app/api/videos/route.ts @@ -1,4 +1,4 @@ -import handleVideoSearch from '@/lib/chains/videoSearchAgent'; +import handleVideoSearch from '@/lib/agents/media/video'; import ModelRegistry from '@/lib/models/registry'; import { ModelWithProvider } from '@/lib/models/types'; import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; diff --git a/src/lib/chains/imageSearchAgent.ts b/src/lib/agents/media/image.ts similarity index 90% rename from src/lib/chains/imageSearchAgent.ts rename to src/lib/agents/media/image.ts index a91b7bb..2dd719b 100644 --- a/src/lib/chains/imageSearchAgent.ts +++ b/src/lib/agents/media/image.ts @@ -1,15 +1,17 @@ +/* I don't think can be classified as agents but to keep the structure consistent i guess ill keep it here */ + import { RunnableSequence, RunnableMap, RunnableLambda, } from '@langchain/core/runnables'; import { ChatPromptTemplate } from '@langchain/core/prompts'; -import formatChatHistoryAsString from '../utils/formatHistory'; +import formatChatHistoryAsString from '@/lib/utils/formatHistory'; import { BaseMessage } from '@langchain/core/messages'; import { StringOutputParser } from '@langchain/core/output_parsers'; -import { searchSearxng } from '../searxng'; +import { searchSearxng } from '@/lib/searxng'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import LineOutputParser from '../outputParsers/lineOutputParser'; +import LineOutputParser from '@/lib/outputParsers/lineOutputParser'; const imageSearchChainPrompt = ` You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search the web for images. @@ -102,4 +104,4 @@ const handleImageSearch = ( return imageSearchChain.invoke(input); }; -export default handleImageSearch; +export default handleImageSearch; \ No newline at end of file diff --git a/src/lib/chains/videoSearchAgent.ts b/src/lib/agents/media/video.ts similarity index 94% rename from src/lib/chains/videoSearchAgent.ts rename to src/lib/agents/media/video.ts index 3f878a8..1bf7f75 100644 --- a/src/lib/chains/videoSearchAgent.ts +++ b/src/lib/agents/media/video.ts @@ -4,12 +4,12 @@ import { RunnableLambda, } from '@langchain/core/runnables'; import { ChatPromptTemplate } from '@langchain/core/prompts'; -import formatChatHistoryAsString from '../utils/formatHistory'; +import formatChatHistoryAsString from '@/lib/utils/formatHistory'; import { BaseMessage } from '@langchain/core/messages'; import { StringOutputParser } from '@langchain/core/output_parsers'; -import { searchSearxng } from '../searxng'; +import { searchSearxng } from '@/lib/searxng'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import LineOutputParser from '../outputParsers/lineOutputParser'; +import LineOutputParser from '@/lib/outputParsers/lineOutputParser'; const videoSearchChainPrompt = ` You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search Youtube for videos. diff --git a/src/lib/chains/suggestionGeneratorAgent.ts b/src/lib/agents/suggestions/index.ts similarity index 93% rename from src/lib/chains/suggestionGeneratorAgent.ts rename to src/lib/agents/suggestions/index.ts index 9129059..3beb1bd 100644 --- a/src/lib/chains/suggestionGeneratorAgent.ts +++ b/src/lib/agents/suggestions/index.ts @@ -1,7 +1,7 @@ import { RunnableSequence, RunnableMap } from '@langchain/core/runnables'; -import ListLineOutputParser from '../outputParsers/listLineOutputParser'; +import ListLineOutputParser from '@/lib/outputParsers/listLineOutputParser'; import { PromptTemplate } from '@langchain/core/prompts'; -import formatChatHistoryAsString from '../utils/formatHistory'; +import formatChatHistoryAsString from '@/lib/utils/formatHistory'; import { BaseMessage } from '@langchain/core/messages'; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { ChatOpenAI } from '@langchain/openai'; From 41fe0098471f52cb75f2bebc13980de984839d2a Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:47:28 +0530 Subject: [PATCH 002/196] feat(app): migrate suggestion chain --- src/app/api/suggestions/route.ts | 17 +++++---- src/lib/agents/suggestions/index.ts | 53 ++++++++-------------------- src/lib/prompts/suggestions/index.ts | 15 ++++++++ 3 files changed, 38 insertions(+), 47 deletions(-) create mode 100644 src/lib/prompts/suggestions/index.ts diff --git a/src/app/api/suggestions/route.ts b/src/app/api/suggestions/route.ts index f942758..2dc7248 100644 --- a/src/app/api/suggestions/route.ts +++ b/src/app/api/suggestions/route.ts @@ -1,7 +1,6 @@ import generateSuggestions from '@/lib/agents/suggestions'; import ModelRegistry from '@/lib/models/registry'; import { ModelWithProvider } from '@/lib/models/types'; -import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; interface SuggestionsGenerationBody { @@ -13,6 +12,13 @@ export const POST = async (req: Request) => { try { const body: SuggestionsGenerationBody = await req.json(); + const registry = new ModelRegistry(); + + const llm = await registry.loadChatModel( + body.chatModel.providerId, + body.chatModel.key, + ); + const chatHistory = body.chatHistory .map((msg: any) => { if (msg.role === 'user') { @@ -23,16 +29,9 @@ export const POST = async (req: Request) => { }) .filter((msg) => msg !== undefined) as BaseMessage[]; - const registry = new ModelRegistry(); - - const llm = await registry.loadChatModel( - body.chatModel.providerId, - body.chatModel.key, - ); - const suggestions = await generateSuggestions( { - chat_history: chatHistory, + chatHistory, }, llm, ); diff --git a/src/lib/agents/suggestions/index.ts b/src/lib/agents/suggestions/index.ts index 3beb1bd..03302ac 100644 --- a/src/lib/agents/suggestions/index.ts +++ b/src/lib/agents/suggestions/index.ts @@ -1,55 +1,32 @@ -import { RunnableSequence, RunnableMap } from '@langchain/core/runnables'; import ListLineOutputParser from '@/lib/outputParsers/listLineOutputParser'; -import { PromptTemplate } from '@langchain/core/prompts'; +import { ChatPromptTemplate, PromptTemplate } from '@langchain/core/prompts'; import formatChatHistoryAsString from '@/lib/utils/formatHistory'; -import { BaseMessage } from '@langchain/core/messages'; +import { BaseMessage, HumanMessage, SystemMessage } from '@langchain/core/messages'; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import { ChatOpenAI } from '@langchain/openai'; - -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. - -Provide these suggestions separated by newlines between the XML tags and . For example: - - -Tell me more about SpaceX and their recent projects -What is the latest news on SpaceX? -Who is the CEO of SpaceX? - - -Conversation: -{chat_history} -`; +import { suggestionGeneratorPrompt } from '@/lib/prompts/suggestions'; type SuggestionGeneratorInput = { - chat_history: BaseMessage[]; + chatHistory: BaseMessage[]; }; const outputParser = new ListLineOutputParser({ key: 'suggestions', }); -const createSuggestionGeneratorChain = (llm: BaseChatModel) => { - return RunnableSequence.from([ - RunnableMap.from({ - chat_history: (input: SuggestionGeneratorInput) => - formatChatHistoryAsString(input.chat_history), - }), - PromptTemplate.fromTemplate(suggestionGeneratorPrompt), - llm, - outputParser, - ]); -}; - -const generateSuggestions = ( +const generateSuggestions = async ( input: SuggestionGeneratorInput, llm: BaseChatModel, ) => { - (llm as unknown as ChatOpenAI).temperature = 0; - const suggestionGeneratorChain = createSuggestionGeneratorChain(llm); - return suggestionGeneratorChain.invoke(input); + const chatPrompt = await ChatPromptTemplate.fromMessages([ + new SystemMessage(suggestionGeneratorPrompt), + new HumanMessage(`${formatChatHistoryAsString(input.chatHistory)}`) + ]).formatMessages({}) + + const res = await llm.invoke(chatPrompt) + + const suggestions = await outputParser.invoke(res) + + return suggestions }; export default generateSuggestions; diff --git a/src/lib/prompts/suggestions/index.ts b/src/lib/prompts/suggestions/index.ts new file mode 100644 index 0000000..daa99d4 --- /dev/null +++ b/src/lib/prompts/suggestions/index.ts @@ -0,0 +1,15 @@ +export 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. + +Provide these suggestions separated by newlines between the XML tags and . For example: + + +Tell me more about SpaceX and their recent projects +What is the latest news on SpaceX? +Who is the CEO of SpaceX? + + +Today's date is ${new Date().toISOString()} +`; \ No newline at end of file From 33b736e1e830bead1e803e2829f290d344d677e5 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:51:13 +0530 Subject: [PATCH 003/196] feat(app): migrate image search chain --- src/app/api/images/route.ts | 20 +++--- src/lib/agents/media/image.ts | 114 +++++++++++---------------------- src/lib/prompts/media/image.ts | 26 ++++++++ 3 files changed, 72 insertions(+), 88 deletions(-) create mode 100644 src/lib/prompts/media/image.ts diff --git a/src/app/api/images/route.ts b/src/app/api/images/route.ts index 71d679e..bc62a1d 100644 --- a/src/app/api/images/route.ts +++ b/src/app/api/images/route.ts @@ -1,4 +1,4 @@ -import handleImageSearch from '@/lib/agents/media/image'; +import searchImages from '@/lib/agents/media/image'; import ModelRegistry from '@/lib/models/registry'; import { ModelWithProvider } from '@/lib/models/types'; import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; @@ -13,6 +13,13 @@ export const POST = async (req: Request) => { try { const body: ImageSearchBody = await req.json(); + const registry = new ModelRegistry(); + + const llm = await registry.loadChatModel( + body.chatModel.providerId, + body.chatModel.key, + ); + const chatHistory = body.chatHistory .map((msg: any) => { if (msg.role === 'user') { @@ -23,16 +30,9 @@ export const POST = async (req: Request) => { }) .filter((msg) => msg !== undefined) as BaseMessage[]; - const registry = new ModelRegistry(); - - const llm = await registry.loadChatModel( - body.chatModel.providerId, - body.chatModel.key, - ); - - const images = await handleImageSearch( + const images = await searchImages( { - chat_history: chatHistory, + chatHistory: chatHistory, query: body.query, }, llm, diff --git a/src/lib/agents/media/image.ts b/src/lib/agents/media/image.ts index 2dd719b..648b5ce 100644 --- a/src/lib/agents/media/image.ts +++ b/src/lib/agents/media/image.ts @@ -7,101 +7,59 @@ import { } from '@langchain/core/runnables'; import { ChatPromptTemplate } from '@langchain/core/prompts'; import formatChatHistoryAsString from '@/lib/utils/formatHistory'; -import { BaseMessage } from '@langchain/core/messages'; +import { BaseMessage, HumanMessage, SystemMessage } from '@langchain/core/messages'; import { StringOutputParser } from '@langchain/core/output_parsers'; import { searchSearxng } from '@/lib/searxng'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; import LineOutputParser from '@/lib/outputParsers/lineOutputParser'; - -const imageSearchChainPrompt = ` -You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search the web for images. -You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation. -Output only the rephrased query wrapped in an XML element. Do not include any explanation or additional text. -`; +import { imageSearchFewShots, imageSearchPrompt } from '@/lib/prompts/media/image'; type ImageSearchChainInput = { - chat_history: BaseMessage[]; + chatHistory: BaseMessage[]; query: string; }; -interface ImageSearchResult { +type ImageSearchResult = { img_src: string; url: string; title: string; } -const strParser = new StringOutputParser(); +const outputParser = new LineOutputParser({ + key: 'query', +}) -const createImageSearchChain = (llm: BaseChatModel) => { - return RunnableSequence.from([ - RunnableMap.from({ - chat_history: (input: ImageSearchChainInput) => { - return formatChatHistoryAsString(input.chat_history); - }, - query: (input: ImageSearchChainInput) => { - return input.query; - }, - }), - ChatPromptTemplate.fromMessages([ - ['system', imageSearchChainPrompt], - [ - 'user', - '\n\n\nWhat is a cat?\n', - ], - ['assistant', 'A cat'], - - [ - 'user', - '\n\n\nWhat is a car? How does it work?\n', - ], - ['assistant', 'Car working'], - [ - 'user', - '\n\n\nHow does an AC work?\n', - ], - ['assistant', 'AC working'], - [ - 'user', - '{chat_history}\n\n{query}\n', - ], - ]), - llm, - strParser, - RunnableLambda.from(async (input: string) => { - const queryParser = new LineOutputParser({ - key: 'query', - }); - - return await queryParser.parse(input); - }), - RunnableLambda.from(async (input: string) => { - const res = await searchSearxng(input, { - engines: ['bing images', 'google images'], - }); - - const images: ImageSearchResult[] = []; - - 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, - }); - } - }); - - return images.slice(0, 10); - }), - ]); -}; - -const handleImageSearch = ( +const searchImages = async ( input: ImageSearchChainInput, llm: BaseChatModel, ) => { - const imageSearchChain = createImageSearchChain(llm); - return imageSearchChain.invoke(input); + const chatPrompt = await ChatPromptTemplate.fromMessages([ + new SystemMessage(imageSearchPrompt), + ...imageSearchFewShots, + new HumanMessage(`\n${formatChatHistoryAsString(input.chatHistory)}\n\n\n${input.query}\n`) + ]).formatMessages({}) + + const res = await llm.invoke(chatPrompt) + + const query = await outputParser.invoke(res) + + const searchRes = await searchSearxng(query!, { + engines: ['bing images', 'google images'], + }); + + const images: ImageSearchResult[] = []; + + searchRes.results.forEach((result) => { + if (result.img_src && result.url && result.title) { + images.push({ + img_src: result.img_src, + url: result.url, + title: result.title, + }); + } + }); + + return images.slice(0, 10); }; -export default handleImageSearch; \ No newline at end of file +export default searchImages; \ No newline at end of file diff --git a/src/lib/prompts/media/image.ts b/src/lib/prompts/media/image.ts new file mode 100644 index 0000000..5f707c1 --- /dev/null +++ b/src/lib/prompts/media/image.ts @@ -0,0 +1,26 @@ +import { BaseMessageLike } from "@langchain/core/messages"; + +export const imageSearchPrompt = ` +You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search the web for images. +You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation. +Output only the rephrased query wrapped in an XML element. Do not include any explanation or additional text. +`; + +export const imageSearchFewShots: BaseMessageLike[] = [ + [ + 'user', + '\n\n\nWhat is a cat?\n', + ], + ['assistant', 'A cat'], + + [ + 'user', + '\n\n\nWhat is a car? How does it work?\n', + ], + ['assistant', 'Car working'], + [ + 'user', + '\n\n\nHow does an AC work?\n', + ], + ['assistant', 'AC working'] +] \ No newline at end of file From e499c0b96e8bfe62b9020ef844e7c71fd8f9a18c Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:51:25 +0530 Subject: [PATCH 004/196] feat(app): migrate video search chain --- src/app/api/videos/route.ts | 16 ++-- src/lib/agents/media/video.ts | 131 +++++++++++--------------------- src/lib/prompts/media/videos.ts | 25 ++++++ 3 files changed, 76 insertions(+), 96 deletions(-) create mode 100644 src/lib/prompts/media/videos.ts diff --git a/src/app/api/videos/route.ts b/src/app/api/videos/route.ts index 0ace57f..1417226 100644 --- a/src/app/api/videos/route.ts +++ b/src/app/api/videos/route.ts @@ -13,6 +13,13 @@ export const POST = async (req: Request) => { try { const body: VideoSearchBody = await req.json(); + const registry = new ModelRegistry(); + + const llm = await registry.loadChatModel( + body.chatModel.providerId, + body.chatModel.key, + ); + const chatHistory = body.chatHistory .map((msg: any) => { if (msg.role === 'user') { @@ -23,16 +30,9 @@ export const POST = async (req: Request) => { }) .filter((msg) => msg !== undefined) as BaseMessage[]; - const registry = new ModelRegistry(); - - const llm = await registry.loadChatModel( - body.chatModel.providerId, - body.chatModel.key, - ); - const videos = await handleVideoSearch( { - chat_history: chatHistory, + chatHistory: chatHistory, query: body.query, }, llm, diff --git a/src/lib/agents/media/video.ts b/src/lib/agents/media/video.ts index 1bf7f75..60fc04f 100644 --- a/src/lib/agents/media/video.ts +++ b/src/lib/agents/media/video.ts @@ -1,110 +1,65 @@ -import { - RunnableSequence, - RunnableMap, - RunnableLambda, -} from '@langchain/core/runnables'; import { ChatPromptTemplate } from '@langchain/core/prompts'; import formatChatHistoryAsString from '@/lib/utils/formatHistory'; -import { BaseMessage } from '@langchain/core/messages'; -import { StringOutputParser } from '@langchain/core/output_parsers'; +import { BaseMessage, HumanMessage, SystemMessage } from '@langchain/core/messages'; import { searchSearxng } from '@/lib/searxng'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; import LineOutputParser from '@/lib/outputParsers/lineOutputParser'; - -const videoSearchChainPrompt = ` -You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search Youtube for videos. -You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation. -Output only the rephrased query wrapped in an XML element. Do not include any explanation or additional text. -`; +import { videoSearchFewShots, videoSearchPrompt } from '@/lib/prompts/media/videos'; type VideoSearchChainInput = { - chat_history: BaseMessage[]; + chatHistory: BaseMessage[]; query: string; }; -interface VideoSearchResult { +type VideoSearchResult = { img_src: string; url: string; title: string; iframe_src: string; } -const strParser = new StringOutputParser(); +const outputParser = new LineOutputParser({ + key: 'query', +}); -const createVideoSearchChain = (llm: BaseChatModel) => { - return RunnableSequence.from([ - RunnableMap.from({ - chat_history: (input: VideoSearchChainInput) => { - return formatChatHistoryAsString(input.chat_history); - }, - query: (input: VideoSearchChainInput) => { - return input.query; - }, - }), - ChatPromptTemplate.fromMessages([ - ['system', videoSearchChainPrompt], - [ - 'user', - '\n\n\nHow does a car work?\n', - ], - ['assistant', 'How does a car work?'], - [ - 'user', - '\n\n\nWhat is the theory of relativity?\n', - ], - ['assistant', 'Theory of relativity'], - [ - 'user', - '\n\n\nHow does an AC work?\n', - ], - ['assistant', 'AC working'], - [ - 'user', - '{chat_history}\n\n{query}\n', - ], - ]), - llm, - strParser, - RunnableLambda.from(async (input: string) => { - const queryParser = new LineOutputParser({ - key: 'query', - }); - return await queryParser.parse(input); - }), - RunnableLambda.from(async (input: string) => { - const res = await searchSearxng(input, { - engines: ['youtube'], - }); - - const videos: VideoSearchResult[] = []; - - res.results.forEach((result) => { - if ( - result.thumbnail && - result.url && - result.title && - result.iframe_src - ) { - videos.push({ - img_src: result.thumbnail, - url: result.url, - title: result.title, - iframe_src: result.iframe_src, - }); - } - }); - - return videos.slice(0, 10); - }), - ]); -}; - -const handleVideoSearch = ( +const searchVideos = async ( input: VideoSearchChainInput, llm: BaseChatModel, ) => { - const videoSearchChain = createVideoSearchChain(llm); - return videoSearchChain.invoke(input); + const chatPrompt = await ChatPromptTemplate.fromMessages([ + new SystemMessage(videoSearchPrompt), + ...videoSearchFewShots, + new HumanMessage(`${formatChatHistoryAsString(input.chatHistory)}\n\n\n${input.query}\n`) + ]).formatMessages({}) + + const res = await llm.invoke(chatPrompt) + + const query = await outputParser.invoke(res) + + const searchRes = await searchSearxng(query!, { + engines: ['youtube'], + }); + + const videos: VideoSearchResult[] = []; + + searchRes.results.forEach((result) => { + if ( + result.thumbnail && + result.url && + result.title && + result.iframe_src + ) { + videos.push({ + img_src: result.thumbnail, + url: result.url, + title: result.title, + iframe_src: result.iframe_src, + }); + } + }); + + return videos.slice(0, 10); + }; -export default handleVideoSearch; +export default searchVideos; diff --git a/src/lib/prompts/media/videos.ts b/src/lib/prompts/media/videos.ts new file mode 100644 index 0000000..b4a0d55 --- /dev/null +++ b/src/lib/prompts/media/videos.ts @@ -0,0 +1,25 @@ +import { BaseMessageLike } from "@langchain/core/messages"; + +export const videoSearchPrompt = ` +You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search Youtube for videos. +You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation. +Output only the rephrased query wrapped in an XML element. Do not include any explanation or additional text. +`; + +export const videoSearchFewShots: BaseMessageLike[] = [ + [ + 'user', + '\n\n\nHow does a car work?\n', + ], + ['assistant', 'How does a car work?'], + [ + 'user', + '\n\n\nWhat is the theory of relativity?\n', + ], + ['assistant', 'Theory of relativity'], + [ + 'user', + '\n\n\nHow does an AC work?\n', + ], + ['assistant', 'AC working'], +] \ No newline at end of file From 3bcf646af15d0d1053cc8128529e1e97b8bb0eda Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:52:12 +0530 Subject: [PATCH 005/196] feat(search-route): handle history processing after llm validation --- src/app/api/search/route.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts index bc7255f..f737a55 100644 --- a/src/app/api/search/route.ts +++ b/src/app/api/search/route.ts @@ -30,12 +30,6 @@ export const POST = async (req: Request) => { body.optimizationMode = body.optimizationMode || 'balanced'; body.stream = body.stream || false; - const history: BaseMessage[] = body.history.map((msg) => { - return msg[0] === 'human' - ? new HumanMessage({ content: msg[1] }) - : new AIMessage({ content: msg[1] }); - }); - const registry = new ModelRegistry(); const [llm, embeddings] = await Promise.all([ @@ -46,6 +40,12 @@ export const POST = async (req: Request) => { ), ]); + const history: BaseMessage[] = body.history.map((msg) => { + return msg[0] === 'human' + ? new HumanMessage({ content: msg[1] }) + : new AIMessage({ content: msg[1] }); + }); + const searchHandler: MetaSearchAgentType = searchHandlers[body.focusMode]; if (!searchHandler) { @@ -128,7 +128,7 @@ export const POST = async (req: Request) => { try { controller.close(); - } catch (error) {} + } catch (error) { } }); emitter.on('data', (data: string) => { From 07a17925b195df27d30a9b1b5028f842cec0aa51 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:53:53 +0530 Subject: [PATCH 006/196] feat(media-search): supply full history --- src/components/MessageBox.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MessageBox.tsx b/src/components/MessageBox.tsx index 062bb90..b716202 100644 --- a/src/components/MessageBox.tsx +++ b/src/components/MessageBox.tsx @@ -205,11 +205,11 @@ const MessageBox = ({
From bd5628b390d0f6b6c0686007068f0aa8241d6d23 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:45:48 +0530 Subject: [PATCH 007/196] feat(package): bump langchain package --- package.json | 19 +++---- yarn.lock | 141 +++++++++++++++++++++++++++------------------------ 2 files changed, 84 insertions(+), 76 deletions(-) diff --git a/package.json b/package.json index 7083b66..b51dc6f 100644 --- a/package.json +++ b/package.json @@ -16,13 +16,14 @@ "@huggingface/transformers": "^3.7.5", "@iarna/toml": "^2.2.5", "@icons-pack/react-simple-icons": "^12.3.0", - "@langchain/anthropic": "^1.0.0", - "@langchain/community": "^1.0.0", - "@langchain/core": "^1.0.1", - "@langchain/google-genai": "^1.0.0", - "@langchain/groq": "^1.0.0", - "@langchain/ollama": "^1.0.0", - "@langchain/openai": "^1.0.0", + "@langchain/anthropic": "^1.0.1", + "@langchain/community": "^1.0.3", + "@langchain/core": "^1.0.5", + "@langchain/google-genai": "^1.0.1", + "@langchain/groq": "^1.0.1", + "@langchain/langgraph": "^1.0.1", + "@langchain/ollama": "^1.0.1", + "@langchain/openai": "^1.1.1", "@langchain/textsplitters": "^1.0.0", "@tailwindcss/typography": "^0.5.12", "axios": "^1.8.3", @@ -33,7 +34,7 @@ "framer-motion": "^12.23.24", "html-to-text": "^9.0.5", "jspdf": "^3.0.1", - "langchain": "^1.0.1", + "langchain": "^1.0.4", "lucide-react": "^0.363.0", "mammoth": "^1.9.1", "markdown-to-jsx": "^7.7.2", @@ -48,7 +49,7 @@ "tailwind-merge": "^2.2.2", "winston": "^3.17.0", "yet-another-react-lightbox": "^3.17.2", - "zod": "^3.22.4" + "zod": "^4.1.12" }, "devDependencies": { "@types/better-sqlite3": "^7.6.12", diff --git a/yarn.lock b/yarn.lock index 5e29348..8844d50 100644 --- a/yarn.lock +++ b/yarn.lock @@ -746,19 +746,19 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@langchain/anthropic@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@langchain/anthropic/-/anthropic-1.0.0.tgz#48535c5682851bf8fddcf37aa7ca78d4d93da932" - integrity sha512-Lud/FrkFmXMYW5R9y0FC+RGdgjBBVQ2JAnG3A8E1I4+sqv5JgJttw3HKRpFkyBUSyacs6LMfSn5dbJ6TT9nMiQ== +"@langchain/anthropic@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@langchain/anthropic/-/anthropic-1.0.1.tgz#a9f836b11ecbce282fc2afb8d707c52fd37711c4" + integrity sha512-yVKePAT+nNHtybyyPlWqiq6lqcoDlIuMgL9B4WMEU5gbmzL170iodiqcgcZNFQLOC1V2wCOzywq6Zr0kB24AFg== dependencies: "@anthropic-ai/sdk" "^0.65.0" -"@langchain/classic@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@langchain/classic/-/classic-1.0.0.tgz#acbc15eebba03499cf93e73d2c93703a3da0a46e" - integrity sha512-darZFvO5g5e3TqZ4rvZ938F94D4a34v2ZdWfyipmyu7WB4RXMshmYtWCp98o4ec3bfRD9S4+oHMmaPcnk5cs5A== +"@langchain/classic@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@langchain/classic/-/classic-1.0.3.tgz#92482cb4cb8692407b4ecde0df312f035934472f" + integrity sha512-XyoaiJSi4y7SzrZMCb3DdDfC+M3gqIQpVH2cOCh9xQf4244jNrncpLXF/MwOJYWxzTsjfcCAHIbFJ0kSH5nqmg== dependencies: - "@langchain/openai" "1.0.0-alpha.3" + "@langchain/openai" "1.1.1" "@langchain/textsplitters" "1.0.0" handlebars "^4.7.8" js-yaml "^4.1.0" @@ -771,24 +771,24 @@ optionalDependencies: langsmith "^0.3.64" -"@langchain/community@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@langchain/community/-/community-1.0.0.tgz#8e587605b7c981882e20281aa9e644a166620145" - integrity sha512-CM4vUZHaFHq8HpWBMIWPO5bo/rmRPJ1/iaJk7s8CghkkQ0WLaZzDtoG/wJKJZMDJOUVCtZKTw+TytlGu00/9dg== +"@langchain/community@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@langchain/community/-/community-1.0.3.tgz#278c82eee22ff37b120e182b07b7c23ffc6786ab" + integrity sha512-86L7qooSY8Fh5Sf2Tu/X8PvDJqvEXohyZUGusuv0XtnWGivwtecBm0vEbVPkLF07I2ZMtyAGzHJOblbveq6Nmg== dependencies: - "@langchain/classic" "1.0.0" - "@langchain/openai" "1.0.0" + "@langchain/classic" "1.0.3" + "@langchain/openai" "1.1.1" binary-extensions "^2.2.0" - expr-eval "^2.0.2" flat "^5.0.2" js-yaml "^4.1.0" + math-expression-evaluator "^2.0.0" uuid "^10.0.0" zod "^3.25.76 || ^4" -"@langchain/core@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@langchain/core/-/core-1.0.1.tgz#c2bdbdff87649fe17b2c86bf535d749ac73a586c" - integrity sha512-hVM3EkojYOk4ISJQKjLuWYSH6kyyOFlZIrLFETDA1L0Z2/Iu0q32aJawZ0FDn6rlXE8QZjBt/9OaOL36rXc05w== +"@langchain/core@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@langchain/core/-/core-1.0.5.tgz#1e20ecce80fa4d0b979ea05b24b879b8357d8092" + integrity sha512-9Hy/b9+j+mm0Bhnm8xD9B0KpBYTidroLrDHdbrHoMC2DqXoY2umvi1M3M/9D744qsMSaIMP0ZwFcy5YbqI/dGw== dependencies: "@cfworker/json-schema" "^4.0.2" ansi-styles "^5.0.0" @@ -802,18 +802,18 @@ uuid "^10.0.0" zod "^3.25.76 || ^4" -"@langchain/google-genai@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@langchain/google-genai/-/google-genai-1.0.0.tgz#2785fa163788cb6214dffc1dc29fcd5bbb751493" - integrity sha512-ICUBZl/46nG6+Yhe5v7kp/2TQBGOzqEkpfKPLDeNyJ4x9OOL46xsW3ZZrHJjhGMQuR6/JMmQMTU9kLoYgsd1Tg== +"@langchain/google-genai@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@langchain/google-genai/-/google-genai-1.0.1.tgz#3601645f652f24e3beb55acc61878070b49c24ed" + integrity sha512-a9Bzaswp1P+eA2V8hAWSBypqjxmH+/zhOY1TBdalQuPQBTRH35jBMVgX3CTTAheAzBUGQtlDD4/dR9tyemDbhw== dependencies: "@google/generative-ai" "^0.24.0" uuid "^11.1.0" -"@langchain/groq@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@langchain/groq/-/groq-1.0.0.tgz#413b02158761ff406238467325cd4f9fe0990f3a" - integrity sha512-6fG9MEQHNXnxgObFHSPh+BPYyTGcoDnKd+GhI9l96cpHh+QNI+IvypicRCZVSsLdqzRCFHISvBQaH+SP5vgjIw== +"@langchain/groq@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@langchain/groq/-/groq-1.0.1.tgz#7ec8822cd2f29eef4ae0f9c20f67268d1924ab96" + integrity sha512-vDQzv6A3mjG0/W/7vL4Iq+dnmhSbMHln+b7Rna810trjZzfNPZhAP6omqZyzCKIqjsQYUH4ODLnSUCNiarfYsQ== dependencies: groq-sdk "^0.19.0" @@ -842,30 +842,30 @@ "@langchain/langgraph-sdk" "~1.0.0" uuid "^10.0.0" -"@langchain/ollama@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@langchain/ollama/-/ollama-1.0.0.tgz#803c353e9dfb1a9e7b20f1460a6a201fec29bb77" - integrity sha512-zqn6i7haMjvZW4FQWo0GrF4wYL5mLurdL0qoe+moYWYSCGaay4K7e/4dqM5C/MR16/HPFDzFbBRMkni2PDRBgA== +"@langchain/langgraph@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-1.0.1.tgz#d0be714653e8a27665f86ea795c5c34189455406" + integrity sha512-7y8OTDLrHrpJ55Y5x7c7zU2BbqNllXwxM106Xrd+NaQB5CpEb4hbUfIwe4XmhhscKPwvhXAq3tjeUxw9MCiurQ== + dependencies: + "@langchain/langgraph-checkpoint" "^1.0.0" + "@langchain/langgraph-sdk" "~1.0.0" + uuid "^10.0.0" + +"@langchain/ollama@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@langchain/ollama/-/ollama-1.0.1.tgz#c63ac6db65110beef4020a5e2b167ad0bc678d33" + integrity sha512-Pe32hhTpMvnRlNFJxkdu6r1QzsONGz5uvoLiMU1TpgAUu7EyKr2osymlgjBLqDe2vMKUmqHb+yWRH0IppDBUOg== dependencies: ollama "^0.5.12" uuid "^10.0.0" -"@langchain/openai@1.0.0", "@langchain/openai@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@langchain/openai/-/openai-1.0.0.tgz#03b15312286b30ce0149f6052620c6c95b4387bc" - integrity sha512-olKEUIjb3HBOiD/NR056iGJz4wiN6HhQ/u65YmGWYadWWoKOcGwheBw/FE0x6SH4zDlI3QmP+vMhuQoaww19BQ== +"@langchain/openai@1.1.1", "@langchain/openai@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@langchain/openai/-/openai-1.1.1.tgz#67ddcf54ee7ac402f6b75b4b9e25447e78c56a93" + integrity sha512-0kUaXejo/sn6QAohWHDaAUapC4CJRkJIajGaWfJC+llSqpDBnmBE1oHg1M2fi1OCeP+ns9SxB6BTsq4Qbiqmig== dependencies: js-tiktoken "^1.0.12" - openai "^6.3.0" - zod "^3.25.76 || ^4" - -"@langchain/openai@1.0.0-alpha.3": - version "1.0.0-alpha.3" - resolved "https://registry.yarnpkg.com/@langchain/openai/-/openai-1.0.0-alpha.3.tgz#35c4e770e3421b75a226087af54fbeff147e201a" - integrity sha512-re2NXLYeLatPzoB6YRoFgB1fW6i5ygcLGa7PlNOhi3f93uU1vSlWMgjkO9dcN9ALmr/bhoruqJEn7U0Eva+6/w== - dependencies: - js-tiktoken "^1.0.12" - openai "^6.3.0" + openai "^6.9.0" zod "^3.25.76 || ^4" "@langchain/textsplitters@1.0.0", "@langchain/textsplitters@^1.0.0": @@ -2607,11 +2607,6 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== -expr-eval@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/expr-eval/-/expr-eval-2.0.2.tgz#fa6f044a7b0c93fde830954eb9c5b0f7fbc7e201" - integrity sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg== - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3514,17 +3509,16 @@ kuler@^2.0.0: resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== -langchain@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/langchain/-/langchain-1.0.1.tgz#fb181176f4aa443ef02e9e5b563bcb4e170dfeb6" - integrity sha512-IT4JBVbKBh2AjaUFT9OsmOfeK3UbKy3SgdzZOuvet25sAaMpAR8IaM9XVddRs+OXQqVg6sOS01KUUVCJksVhHg== +langchain@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/langchain/-/langchain-1.0.4.tgz#c4fa22d927f41d56c356ecfccea5c08ae7b682ef" + integrity sha512-g7z2kKvnXOecybbVGHfI2ZmdmP309mxC1FYlq6WC/7RsKgX5MwY9gBjwK16mpKOaozOD9QCo1Ia7o2UcUBRb9Q== dependencies: "@langchain/langgraph" "^1.0.0" "@langchain/langgraph-checkpoint" "^1.0.0" + langsmith "~0.3.74" uuid "^10.0.0" zod "^3.25.76 || ^4" - optionalDependencies: - langsmith "^0.3.64" langsmith@^0.3.64: version "0.3.74" @@ -3539,6 +3533,19 @@ langsmith@^0.3.64: semver "^7.6.3" uuid "^10.0.0" +langsmith@~0.3.74: + version "0.3.79" + resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.3.79.tgz#6c845644da26e7fdd8e9b80706091669fc43bda4" + integrity sha512-j5uiAsyy90zxlxaMuGjb7EdcL51Yx61SpKfDOI1nMPBbemGju+lf47he4e59Hp5K63CY8XWgFP42WeZ+zuIU4Q== + dependencies: + "@types/uuid" "^10.0.0" + chalk "^4.1.2" + console-table-printer "^2.12.1" + p-queue "^6.6.2" + p-retry "4" + semver "^7.6.3" + uuid "^10.0.0" + language-subtag-registry@^0.3.20: version "0.3.22" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" @@ -3686,6 +3693,11 @@ matcher@^3.0.0: dependencies: escape-string-regexp "^4.0.0" +math-expression-evaluator@^2.0.0: + version "2.0.7" + resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-2.0.7.tgz#dc99a80ce2bf7f9b7df878126feb5c506c1fdf5f" + integrity sha512-uwliJZ6BPHRq4eiqNWxZBDzKUiS5RIynFFcgchqhBOloVLVBpZpNG8jRYkedLcBvhph8TnRyWEuxPqiQcwIdog== + math-intrinsics@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" @@ -4025,10 +4037,10 @@ onnxruntime-web@1.22.0-dev.20250409-89f8206ba4: platform "^1.3.6" protobufjs "^7.2.4" -openai@^6.3.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/openai/-/openai-6.5.0.tgz#7dd9c4c0ca6e394c1d1e738b2000e084024685b2" - integrity sha512-bNqJ15Ijbs41KuJ2iYz/mGAruFHzQQt7zXo4EvjNLoB64aJdgn1jlMeDTsXjEg+idVYafg57QB/5Rd16oqvZ6A== +openai@^6.9.0: + version "6.9.0" + resolved "https://registry.yarnpkg.com/openai/-/openai-6.9.0.tgz#acd15b2233c42b165981f3de8f4cfce27f844fce" + integrity sha512-n2sJRYmM+xfJ0l3OfH8eNnIyv3nQY7L08gZQu3dw6wSdfPtKAk92L83M2NIP5SS8Cl/bsBBG3yKzEOjkx0O+7A== openapi-types@^12.1.3: version "12.1.3" @@ -5491,12 +5503,7 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -zod@^3.22.4: - version "3.22.4" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" - integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg== - -"zod@^3.25.76 || ^4": +"zod@^3.25.76 || ^4", zod@^4.1.12: version "4.1.12" resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.12.tgz#64f1ea53d00eab91853195653b5af9eee68970f0" integrity sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ== From e88e1c627c1924fdc790e28e1b26c32f34be233d Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:12:43 +0530 Subject: [PATCH 008/196] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ef1f1d..825e98d 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ We'd also like to thank the following partners for their generous support: - Exa + Exa From f767717d7f421ce95122bdaca7903fb5063053f8 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:13:40 +0530 Subject: [PATCH 009/196] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 825e98d..9a00935 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ We'd also like to thank the following partners for their generous support: - Exa + Exa From 9934c1dbe05256ace290542cf114ab4094b39db6 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:15:06 +0530 Subject: [PATCH 010/196] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9a00935..dc654df 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,9 @@ We'd also like to thank the following partners for their generous support: -
+ - Exa + Exa From a00f2231d4b61e453523b8028aad6aa436a61908 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 14 Nov 2025 23:17:41 +0530 Subject: [PATCH 011/196] feat(chat-window): remove loading state --- src/components/ChatWindow.tsx | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/components/ChatWindow.tsx b/src/components/ChatWindow.tsx index c04b4ea..dc6ab01 100644 --- a/src/components/ChatWindow.tsx +++ b/src/components/ChatWindow.tsx @@ -8,7 +8,6 @@ import { Settings } from 'lucide-react'; import Link from 'next/link'; import NextError from 'next/error'; import { useChat } from '@/lib/hooks/useChat'; -import Loader from './ui/Loader'; import SettingsButtonMobile from './Settings/SettingsButtonMobile'; export interface BaseMessage { @@ -52,7 +51,7 @@ export interface File { } const ChatWindow = () => { - const { hasError, isReady, notFound, messages } = useChat(); + const { hasError, notFound, messages } = useChat(); if (hasError) { return (
@@ -68,24 +67,18 @@ const ChatWindow = () => { ); } - return isReady ? ( - notFound ? ( - - ) : ( -
- {messages.length > 0 ? ( - <> - - - - ) : ( - - )} -
- ) + return notFound ? ( + ) : ( -
- +
+ {messages.length > 0 ? ( + <> + + + + ) : ( + + )}
); }; From f6dac43d7acdca967716f3e7b89ca5d6e851451b Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 18 Nov 2025 01:17:19 +0530 Subject: [PATCH 012/196] feat(types): add message & chunk type --- src/lib/types.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/lib/types.ts diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..f7a9ac9 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,9 @@ +type Message = { + role: 'user' | 'assistant' | 'system'; + content: string; +} + +type Chunk = { + content: string; + metadata: Record; +} \ No newline at end of file From 657a577ec8dd3fa302547ea9337e3a5413c18696 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:37:41 +0530 Subject: [PATCH 013/196] feat(app): enhance UI --- src/components/MessageActions/Copy.tsx | 4 +-- src/components/MessageActions/Rewrite.tsx | 9 +++--- src/components/MessageBox.tsx | 35 +++++++++++++---------- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/components/MessageActions/Copy.tsx b/src/components/MessageActions/Copy.tsx index eb48e30..f74b9f3 100644 --- a/src/components/MessageActions/Copy.tsx +++ b/src/components/MessageActions/Copy.tsx @@ -20,9 +20,9 @@ const Copy = ({ setCopied(true); setTimeout(() => setCopied(false), 1000); }} - className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white" + className="p-2 text-black/70 dark:text-white/70 rounded-full hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white" > - {copied ? : } + {copied ? : } ); }; diff --git a/src/components/MessageActions/Rewrite.tsx b/src/components/MessageActions/Rewrite.tsx index 80fadb3..3902e1e 100644 --- a/src/components/MessageActions/Rewrite.tsx +++ b/src/components/MessageActions/Rewrite.tsx @@ -1,4 +1,4 @@ -import { ArrowLeftRight } from 'lucide-react'; +import { ArrowLeftRight, Repeat } from 'lucide-react'; const Rewrite = ({ rewrite, @@ -10,12 +10,11 @@ const Rewrite = ({ return ( ); }; - +1; export default Rewrite; diff --git a/src/components/MessageBox.tsx b/src/components/MessageBox.tsx index b716202..30e181e 100644 --- a/src/components/MessageBox.tsx +++ b/src/components/MessageBox.tsx @@ -10,6 +10,7 @@ import { StopCircle, Layers3, Plus, + CornerDownRight, } from 'lucide-react'; import Markdown, { MarkdownToJSX } from 'markdown-to-jsx'; import Copy from './MessageActions/Copy'; @@ -122,14 +123,14 @@ const MessageBox = ({ {loading && isLast ? null : ( -
-
+
+
-
+
{speechStatus === 'started' ? ( - + ) : ( - + )}
@@ -159,7 +160,7 @@ const MessageBox = ({ section.suggestions.length > 0 && section.assistantMessage && !loading && ( -
+
(
- {i > 0 && ( -
- )} +
From 5272c7fd3ebbc272ea510b786e4a03219c13abf1 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:38:12 +0530 Subject: [PATCH 014/196] feat(models): add new base classes --- src/lib/models/base/embedding.ts | 7 +++++ src/lib/models/base/llm.ts | 26 ++++++++++++++++++ src/lib/models/base/provider.ts | 47 ++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 src/lib/models/base/embedding.ts create mode 100644 src/lib/models/base/llm.ts create mode 100644 src/lib/models/base/provider.ts diff --git a/src/lib/models/base/embedding.ts b/src/lib/models/base/embedding.ts new file mode 100644 index 0000000..35413ed --- /dev/null +++ b/src/lib/models/base/embedding.ts @@ -0,0 +1,7 @@ +abstract class BaseEmbedding { + constructor(protected config: CONFIG) {} + abstract embedText(texts: string[]): Promise; + abstract embedChunks(chunks: Chunk[]): Promise; +} + +export default BaseEmbedding; diff --git a/src/lib/models/base/llm.ts b/src/lib/models/base/llm.ts new file mode 100644 index 0000000..5d6f52d --- /dev/null +++ b/src/lib/models/base/llm.ts @@ -0,0 +1,26 @@ +import { + GenerateObjectInput, + GenerateObjectOutput, + GenerateOptions, + GenerateTextInput, + GenerateTextOutput, + StreamObjectOutput, + StreamTextOutput, +} from '../types'; + +abstract class BaseLLM { + constructor(protected config: CONFIG) {} + abstract withOptions(options: GenerateOptions): this; + abstract generateText(input: GenerateTextInput): Promise; + abstract streamText( + input: GenerateTextInput, + ): AsyncGenerator; + abstract generateObject( + input: GenerateObjectInput, + ): Promise>; + abstract streamObject( + input: GenerateObjectInput, + ): AsyncGenerator>; +} + +export default BaseLLM; diff --git a/src/lib/models/base/provider.ts b/src/lib/models/base/provider.ts new file mode 100644 index 0000000..950525e --- /dev/null +++ b/src/lib/models/base/provider.ts @@ -0,0 +1,47 @@ +import { Embeddings } from '@langchain/core/embeddings'; +import { BaseChatModel } from '@langchain/core/language_models/chat_models'; +import { ModelList, ProviderMetadata } from '../types'; +import { UIConfigField } from '@/lib/config/types'; +import BaseLLM from './llm'; +import BaseEmbedding from './embedding'; + +abstract class BaseModelProvider { + constructor( + protected id: string, + protected name: string, + protected config: CONFIG, + ) {} + abstract getDefaultModels(): Promise; + abstract getModelList(): Promise; + abstract loadChatModel(modelName: string): Promise>; + abstract loadEmbeddingModel(modelName: string): Promise>; + static getProviderConfigFields(): UIConfigField[] { + throw new Error('Method not implemented.'); + } + static getProviderMetadata(): ProviderMetadata { + throw new Error('Method not Implemented.'); + } + static parseAndValidate(raw: any): any { + /* Static methods can't access class type parameters */ + throw new Error('Method not Implemented.'); + } +} + +export type ProviderConstructor = { + new (id: string, name: string, config: CONFIG): BaseModelProvider; + parseAndValidate(raw: any): CONFIG; + getProviderConfigFields: () => UIConfigField[]; + getProviderMetadata: () => ProviderMetadata; +}; + +export const createProviderInstance =

>( + Provider: P, + id: string, + name: string, + rawConfig: unknown, +): InstanceType

=> { + const cfg = Provider.parseAndValidate(rawConfig); + return new Provider(id, name, cfg) as InstanceType

; +}; + +export default BaseModelProvider; From 4bcbdad6cb3346faabc1ac3b1e0439ac0f00d10d Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:39:04 +0530 Subject: [PATCH 015/196] feat(providers): implement custom classes --- src/lib/models/providers/baseProvider.ts | 45 ----- src/lib/models/providers/index.ts | 18 +- .../providers/{ollama.ts => ollama/index.ts} | 24 +-- .../providers/ollama/ollamaEmbedding.ts | 39 +++++ src/lib/models/providers/ollama/ollamaLLM.ts | 149 ++++++++++++++++ .../providers/{openai.ts => openai/index.ts} | 26 ++- .../providers/openai/openaiEmbedding.ts | 41 +++++ src/lib/models/providers/openai/openaiLLM.ts | 163 ++++++++++++++++++ 8 files changed, 417 insertions(+), 88 deletions(-) delete mode 100644 src/lib/models/providers/baseProvider.ts rename src/lib/models/providers/{ollama.ts => ollama/index.ts} (83%) create mode 100644 src/lib/models/providers/ollama/ollamaEmbedding.ts create mode 100644 src/lib/models/providers/ollama/ollamaLLM.ts rename src/lib/models/providers/{openai.ts => openai/index.ts} (87%) create mode 100644 src/lib/models/providers/openai/openaiEmbedding.ts create mode 100644 src/lib/models/providers/openai/openaiLLM.ts diff --git a/src/lib/models/providers/baseProvider.ts b/src/lib/models/providers/baseProvider.ts deleted file mode 100644 index 980a2b2..0000000 --- a/src/lib/models/providers/baseProvider.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Embeddings } from '@langchain/core/embeddings'; -import { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import { Model, ModelList, ProviderMetadata } from '../types'; -import { UIConfigField } from '@/lib/config/types'; - -abstract class BaseModelProvider { - constructor( - protected id: string, - protected name: string, - protected config: CONFIG, - ) {} - abstract getDefaultModels(): Promise; - abstract getModelList(): Promise; - abstract loadChatModel(modelName: string): Promise; - abstract loadEmbeddingModel(modelName: string): Promise; - static getProviderConfigFields(): UIConfigField[] { - throw new Error('Method not implemented.'); - } - static getProviderMetadata(): ProviderMetadata { - throw new Error('Method not Implemented.'); - } - static parseAndValidate(raw: any): any { - /* Static methods can't access class type parameters */ - throw new Error('Method not Implemented.'); - } -} - -export type ProviderConstructor = { - new (id: string, name: string, config: CONFIG): BaseModelProvider; - parseAndValidate(raw: any): CONFIG; - getProviderConfigFields: () => UIConfigField[]; - getProviderMetadata: () => ProviderMetadata; -}; - -export const createProviderInstance =

>( - Provider: P, - id: string, - name: string, - rawConfig: unknown, -): InstanceType

=> { - const cfg = Provider.parseAndValidate(rawConfig); - return new Provider(id, name, cfg) as InstanceType

; -}; - -export default BaseModelProvider; diff --git a/src/lib/models/providers/index.ts b/src/lib/models/providers/index.ts index addca61..6e508e1 100644 --- a/src/lib/models/providers/index.ts +++ b/src/lib/models/providers/index.ts @@ -1,27 +1,11 @@ import { ModelProviderUISection } from '@/lib/config/types'; -import { ProviderConstructor } from './baseProvider'; +import { ProviderConstructor } from '../base/provider'; import OpenAIProvider from './openai'; import OllamaProvider from './ollama'; -import TransformersProvider from './transformers'; -import AnthropicProvider from './anthropic'; -import GeminiProvider from './gemini'; -import GroqProvider from './groq'; -import DeepSeekProvider from './deepseek'; -import LMStudioProvider from './lmstudio'; -import LemonadeProvider from './lemonade'; -import AimlProvider from '@/lib/models/providers/aiml'; export const providers: Record> = { openai: OpenAIProvider, ollama: OllamaProvider, - transformers: TransformersProvider, - anthropic: AnthropicProvider, - gemini: GeminiProvider, - groq: GroqProvider, - deepseek: DeepSeekProvider, - aiml: AimlProvider, - lmstudio: LMStudioProvider, - lemonade: LemonadeProvider, }; export const getModelProvidersUIConfigSection = diff --git a/src/lib/models/providers/ollama.ts b/src/lib/models/providers/ollama/index.ts similarity index 83% rename from src/lib/models/providers/ollama.ts rename to src/lib/models/providers/ollama/index.ts index 9ae5899..762c2bf 100644 --- a/src/lib/models/providers/ollama.ts +++ b/src/lib/models/providers/ollama/index.ts @@ -1,10 +1,11 @@ -import { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import { Model, ModelList, ProviderMetadata } from '../types'; -import BaseModelProvider from './baseProvider'; -import { ChatOllama, OllamaEmbeddings } from '@langchain/ollama'; -import { Embeddings } from '@langchain/core/embeddings'; import { UIConfigField } from '@/lib/config/types'; import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry'; +import BaseModelProvider from '../../base/provider'; +import { Model, ModelList, ProviderMetadata } from '../../types'; +import BaseLLM from '../../base/llm'; +import BaseEmbedding from '../../base/embedding'; +import OllamaLLM from './ollamaLLM'; +import OllamaEmbedding from './ollamaEmbedding'; interface OllamaConfig { baseURL: string; @@ -76,7 +77,7 @@ class OllamaProvider extends BaseModelProvider { }; } - async loadChatModel(key: string): Promise { + async loadChatModel(key: string): Promise> { const modelList = await this.getModelList(); const exists = modelList.chat.find((m) => m.key === key); @@ -87,14 +88,13 @@ class OllamaProvider extends BaseModelProvider { ); } - return new ChatOllama({ - temperature: 0.7, + return new OllamaLLM({ + baseURL: this.config.baseURL, model: key, - baseUrl: this.config.baseURL, }); } - async loadEmbeddingModel(key: string): Promise { + async loadEmbeddingModel(key: string): Promise> { const modelList = await this.getModelList(); const exists = modelList.embedding.find((m) => m.key === key); @@ -104,9 +104,9 @@ class OllamaProvider extends BaseModelProvider { ); } - return new OllamaEmbeddings({ + return new OllamaEmbedding({ model: key, - baseUrl: this.config.baseURL, + baseURL: this.config.baseURL, }); } diff --git a/src/lib/models/providers/ollama/ollamaEmbedding.ts b/src/lib/models/providers/ollama/ollamaEmbedding.ts new file mode 100644 index 0000000..0fd306a --- /dev/null +++ b/src/lib/models/providers/ollama/ollamaEmbedding.ts @@ -0,0 +1,39 @@ +import { Ollama } from 'ollama'; +import BaseEmbedding from '../../base/embedding'; + +type OllamaConfig = { + model: string; + baseURL?: string; +}; + +class OllamaEmbedding extends BaseEmbedding { + ollamaClient: Ollama; + + constructor(protected config: OllamaConfig) { + super(config); + + this.ollamaClient = new Ollama({ + host: this.config.baseURL || 'http://localhost:11434', + }); + } + + async embedText(texts: string[]): Promise { + const response = await this.ollamaClient.embed({ + input: texts, + model: this.config.model, + }); + + return response.embeddings; + } + + async embedChunks(chunks: Chunk[]): Promise { + const response = await this.ollamaClient.embed({ + input: chunks.map((c) => c.content), + model: this.config.model, + }); + + return response.embeddings; + } +} + +export default OllamaEmbedding; diff --git a/src/lib/models/providers/ollama/ollamaLLM.ts b/src/lib/models/providers/ollama/ollamaLLM.ts new file mode 100644 index 0000000..fd12b77 --- /dev/null +++ b/src/lib/models/providers/ollama/ollamaLLM.ts @@ -0,0 +1,149 @@ +import z from 'zod'; +import BaseLLM from '../../base/llm'; +import { + GenerateObjectInput, + GenerateOptions, + GenerateTextInput, + GenerateTextOutput, + StreamTextOutput, +} from '../../types'; +import { Ollama } from 'ollama'; +import { parse } from 'partial-json'; + +type OllamaConfig = { + baseURL: string; + model: string; + options?: GenerateOptions; +}; + +class OllamaLLM extends BaseLLM { + ollamaClient: Ollama; + + constructor(protected config: OllamaConfig) { + super(config); + + this.ollamaClient = new Ollama({ + host: this.config.baseURL || 'http://localhost:11434', + }); + } + + withOptions(options: GenerateOptions) { + this.config.options = { + ...this.config.options, + ...options, + }; + return this; + } + + async generateText(input: GenerateTextInput): Promise { + this.withOptions(input.options || {}); + + const res = await this.ollamaClient.chat({ + model: this.config.model, + messages: input.messages, + options: { + top_p: this.config.options?.topP, + temperature: this.config.options?.temperature, + num_predict: this.config.options?.maxTokens, + frequency_penalty: this.config.options?.frequencyPenalty, + presence_penalty: this.config.options?.presencePenalty, + stop: this.config.options?.stopSequences, + }, + }); + + return { + content: res.message.content, + additionalInfo: { + reasoning: res.message.thinking, + }, + }; + } + + async *streamText( + input: GenerateTextInput, + ): AsyncGenerator { + this.withOptions(input.options || {}); + + const stream = await this.ollamaClient.chat({ + model: this.config.model, + messages: input.messages, + stream: true, + options: { + top_p: this.config.options?.topP, + temperature: this.config.options?.temperature, + num_predict: this.config.options?.maxTokens, + frequency_penalty: this.config.options?.frequencyPenalty, + presence_penalty: this.config.options?.presencePenalty, + stop: this.config.options?.stopSequences, + }, + }); + + for await (const chunk of stream) { + yield { + contentChunk: chunk.message.content, + done: chunk.done, + additionalInfo: { + reasoning: chunk.message.thinking, + }, + }; + } + } + + async generateObject(input: GenerateObjectInput): Promise { + this.withOptions(input.options || {}); + + const response = await this.ollamaClient.chat({ + model: this.config.model, + messages: input.messages, + format: z.toJSONSchema(input.schema), + options: { + top_p: this.config.options?.topP, + temperature: this.config.options?.temperature, + num_predict: this.config.options?.maxTokens, + frequency_penalty: this.config.options?.frequencyPenalty, + presence_penalty: this.config.options?.presencePenalty, + stop: this.config.options?.stopSequences, + }, + }); + + try { + return input.schema.parse(JSON.parse(response.message.content)) as T; + } catch (err) { + throw new Error(`Error parsing response from Ollama: ${err}`); + } + } + + async *streamObject(input: GenerateObjectInput): AsyncGenerator { + let recievedObj: string = ''; + + this.withOptions(input.options || {}); + + const stream = await this.ollamaClient.chat({ + model: this.config.model, + messages: input.messages, + format: z.toJSONSchema(input.schema), + stream: true, + options: { + top_p: this.config.options?.topP, + temperature: this.config.options?.temperature, + num_predict: this.config.options?.maxTokens, + frequency_penalty: this.config.options?.frequencyPenalty, + presence_penalty: this.config.options?.presencePenalty, + stop: this.config.options?.stopSequences, + }, + }); + + for await (const chunk of stream) { + recievedObj += chunk.message.content; + + try { + yield parse(recievedObj) as T; + } catch (err) { + console.log('Error parsing partial object from Ollama:', err); + yield {} as T; + } + } + } +} + +export default OllamaLLM; diff --git a/src/lib/models/providers/openai.ts b/src/lib/models/providers/openai/index.ts similarity index 87% rename from src/lib/models/providers/openai.ts rename to src/lib/models/providers/openai/index.ts index 6055b34..8b5eacb 100644 --- a/src/lib/models/providers/openai.ts +++ b/src/lib/models/providers/openai/index.ts @@ -1,10 +1,13 @@ import { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import { Model, ModelList, ProviderMetadata } from '../types'; -import BaseModelProvider from './baseProvider'; -import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai'; import { Embeddings } from '@langchain/core/embeddings'; import { UIConfigField } from '@/lib/config/types'; import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry'; +import { Model, ModelList, ProviderMetadata } from '../../types'; +import OpenAIEmbedding from './openaiEmbedding'; +import BaseEmbedding from '../../base/embedding'; +import BaseModelProvider from '../../base/provider'; +import BaseLLM from '../../base/llm'; +import OpenAILLM from './openaiLLM'; interface OpenAIConfig { apiKey: string; @@ -145,7 +148,7 @@ class OpenAIProvider extends BaseModelProvider { }; } - async loadChatModel(key: string): Promise { + async loadChatModel(key: string): Promise> { const modelList = await this.getModelList(); const exists = modelList.chat.find((m) => m.key === key); @@ -156,17 +159,14 @@ class OpenAIProvider extends BaseModelProvider { ); } - return new ChatOpenAI({ + return new OpenAILLM({ apiKey: this.config.apiKey, - temperature: 0.7, model: key, - configuration: { - baseURL: this.config.baseURL, - }, + baseURL: this.config.baseURL, }); } - async loadEmbeddingModel(key: string): Promise { + async loadEmbeddingModel(key: string): Promise> { const modelList = await this.getModelList(); const exists = modelList.embedding.find((m) => m.key === key); @@ -176,12 +176,10 @@ class OpenAIProvider extends BaseModelProvider { ); } - return new OpenAIEmbeddings({ + return new OpenAIEmbedding({ apiKey: this.config.apiKey, model: key, - configuration: { - baseURL: this.config.baseURL, - }, + baseURL: this.config.baseURL, }); } diff --git a/src/lib/models/providers/openai/openaiEmbedding.ts b/src/lib/models/providers/openai/openaiEmbedding.ts new file mode 100644 index 0000000..ea15680 --- /dev/null +++ b/src/lib/models/providers/openai/openaiEmbedding.ts @@ -0,0 +1,41 @@ +import OpenAI from 'openai'; +import BaseEmbedding from '../../base/embedding'; + +type OpenAIConfig = { + apiKey: string; + model: string; + baseURL?: string; +}; + +class OpenAIEmbedding extends BaseEmbedding { + openAIClient: OpenAI; + + constructor(protected config: OpenAIConfig) { + super(config); + + this.openAIClient = new OpenAI({ + apiKey: config.apiKey, + baseURL: config.baseURL, + }); + } + + async embedText(texts: string[]): Promise { + const response = await this.openAIClient.embeddings.create({ + model: this.config.model, + input: texts, + }); + + return response.data.map((embedding) => embedding.embedding); + } + + async embedChunks(chunks: Chunk[]): Promise { + const response = await this.openAIClient.embeddings.create({ + model: this.config.model, + input: chunks.map((c) => c.content), + }); + + return response.data.map((embedding) => embedding.embedding); + } +} + +export default OpenAIEmbedding; diff --git a/src/lib/models/providers/openai/openaiLLM.ts b/src/lib/models/providers/openai/openaiLLM.ts new file mode 100644 index 0000000..95594e6 --- /dev/null +++ b/src/lib/models/providers/openai/openaiLLM.ts @@ -0,0 +1,163 @@ +import OpenAI from 'openai'; +import BaseLLM from '../../base/llm'; +import { zodTextFormat, zodResponseFormat } from 'openai/helpers/zod'; +import { + GenerateObjectInput, + GenerateOptions, + GenerateTextInput, + GenerateTextOutput, + StreamTextOutput, +} from '../../types'; +import { parse } from 'partial-json'; + +type OpenAIConfig = { + apiKey: string; + model: string; + baseURL?: string; + options?: GenerateOptions; +}; + +class OpenAILLM extends BaseLLM { + openAIClient: OpenAI; + + constructor(protected config: OpenAIConfig) { + super(config); + + this.openAIClient = new OpenAI({ + apiKey: this.config.apiKey, + baseURL: this.config.baseURL || 'https://api.openai.com/v1', + }); + } + + withOptions(options: GenerateOptions) { + this.config.options = { + ...this.config.options, + ...options, + }; + + return this; + } + + async generateText(input: GenerateTextInput): Promise { + this.withOptions(input.options || {}); + + const response = await this.openAIClient.chat.completions.create({ + model: this.config.model, + messages: input.messages, + temperature: this.config.options?.temperature || 1.0, + top_p: this.config.options?.topP, + max_completion_tokens: this.config.options?.maxTokens, + stop: this.config.options?.stopSequences, + frequency_penalty: this.config.options?.frequencyPenalty, + presence_penalty: this.config.options?.presencePenalty, + }); + + if (response.choices && response.choices.length > 0) { + return { + content: response.choices[0].message.content!, + additionalInfo: { + finishReason: response.choices[0].finish_reason, + }, + }; + } + + throw new Error('No response from OpenAI'); + } + + async *streamText( + input: GenerateTextInput, + ): AsyncGenerator { + this.withOptions(input.options || {}); + + const stream = await this.openAIClient.chat.completions.create({ + model: this.config.model, + messages: input.messages, + temperature: this.config.options?.temperature || 1.0, + top_p: this.config.options?.topP, + max_completion_tokens: this.config.options?.maxTokens, + stop: this.config.options?.stopSequences, + frequency_penalty: this.config.options?.frequencyPenalty, + presence_penalty: this.config.options?.presencePenalty, + stream: true, + }); + + for await (const chunk of stream) { + if (chunk.choices && chunk.choices.length > 0) { + yield { + contentChunk: chunk.choices[0].delta.content || '', + done: chunk.choices[0].finish_reason !== null, + additionalInfo: { + finishReason: chunk.choices[0].finish_reason, + }, + }; + } + } + } + + async generateObject(input: GenerateObjectInput): Promise { + this.withOptions(input.options || {}); + + const response = await this.openAIClient.chat.completions.parse({ + messages: input.messages, + model: this.config.model, + temperature: this.config.options?.temperature || 1.0, + top_p: this.config.options?.topP, + max_completion_tokens: this.config.options?.maxTokens, + stop: this.config.options?.stopSequences, + frequency_penalty: this.config.options?.frequencyPenalty, + presence_penalty: this.config.options?.presencePenalty, + response_format: zodResponseFormat(input.schema, 'object'), + }); + + if (response.choices && response.choices.length > 0) { + try { + return input.schema.parse(response.choices[0].message.parsed) as T; + } catch (err) { + throw new Error(`Error parsing response from OpenAI: ${err}`); + } + } + + throw new Error('No response from OpenAI'); + } + + async *streamObject(input: GenerateObjectInput): AsyncGenerator { + let recievedObj: string = ''; + + this.withOptions(input.options || {}); + + const stream = this.openAIClient.responses.stream({ + model: this.config.model, + input: input.messages, + temperature: this.config.options?.temperature || 1.0, + top_p: this.config.options?.topP, + max_completion_tokens: this.config.options?.maxTokens, + stop: this.config.options?.stopSequences, + frequency_penalty: this.config.options?.frequencyPenalty, + presence_penalty: this.config.options?.presencePenalty, + text: { + format: zodTextFormat(input.schema, 'object'), + }, + }); + + for await (const chunk of stream) { + if (chunk.type === 'response.output_text.delta' && chunk.delta) { + recievedObj += chunk.delta; + + try { + yield parse(recievedObj) as T; + } catch (err) { + console.log('Error parsing partial object from OpenAI:', err); + yield {} as T; + } + } else if (chunk.type === 'response.output_text.done' && chunk.text) { + try { + yield parse(chunk.text) as T; + } catch (err) { + throw new Error(`Error parsing response from OpenAI: ${err}`); + } + } + } + } +} + +export default OpenAILLM; From f44ad973aa563d8ba13956cff16ae239129f4480 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:39:43 +0530 Subject: [PATCH 016/196] feat(types): add llm types --- src/lib/models/types.ts | 51 +++++++++++++++++++++++++++++++++++++++++ src/lib/types.ts | 12 +++++----- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/lib/models/types.ts b/src/lib/models/types.ts index fdd5df2..ce77d3b 100644 --- a/src/lib/models/types.ts +++ b/src/lib/models/types.ts @@ -1,3 +1,5 @@ +import z from 'zod'; + type Model = { name: string; key: string; @@ -25,10 +27,59 @@ type ModelWithProvider = { providerId: string; }; +type GenerateOptions = { + temperature?: number; + maxTokens?: number; + topP?: number; + stopSequences?: string[]; + frequencyPenalty?: number; + presencePenalty?: number; +}; + +type GenerateTextInput = { + messages: Message[]; + options?: GenerateOptions; +}; + +type GenerateTextOutput = { + content: string; + additionalInfo?: Record; +}; + +type StreamTextOutput = { + contentChunk: string; + additionalInfo?: Record; + done?: boolean; +}; + +type GenerateObjectInput = { + schema: z.ZodTypeAny; + messages: Message[]; + options?: GenerateOptions; +}; + +type GenerateObjectOutput = { + object: T; + additionalInfo?: Record; +}; + +type StreamObjectOutput = { + objectChunk: Partial; + additionalInfo?: Record; + done?: boolean; +}; + export type { Model, ModelList, ProviderMetadata, MinimalProvider, ModelWithProvider, + GenerateOptions, + GenerateTextInput, + GenerateTextOutput, + StreamTextOutput, + GenerateObjectInput, + GenerateObjectOutput, + StreamObjectOutput, }; diff --git a/src/lib/types.ts b/src/lib/types.ts index f7a9ac9..3dcb0d6 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,9 +1,9 @@ type Message = { - role: 'user' | 'assistant' | 'system'; - content: string; -} + role: 'user' | 'assistant' | 'system'; + content: string; +}; type Chunk = { - content: string; - metadata: Record; -} \ No newline at end of file + content: string; + metadata: Record; +}; From c3830795cb800329a898f201e67cdc5e2c9b9487 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:51:17 +0530 Subject: [PATCH 017/196] feat(app): add new session manager --- src/lib/session.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/lib/types.ts | 6 ++++++ 2 files changed, 51 insertions(+) create mode 100644 src/lib/session.ts diff --git a/src/lib/session.ts b/src/lib/session.ts new file mode 100644 index 0000000..6c5e18e --- /dev/null +++ b/src/lib/session.ts @@ -0,0 +1,45 @@ +import { EventEmitter } from 'stream'; +/* todo implement history saving and better artifact typing and handling */ +class SessionManager { + private static sessions = new Map(); + readonly id: string; + private artifacts = new Map(); + private emitter = new EventEmitter(); + + constructor() { + this.id = crypto.randomUUID(); + } + + static getSession(id: string): SessionManager | undefined { + return this.sessions.get(id); + } + + static getAllSessions(): SessionManager[] { + return Array.from(this.sessions.values()); + } + + emit(event: string, data: any) { + this.emitter.emit(event, data); + } + + emitArtifact(artifact: Artifact) { + this.artifacts.set(artifact.id, artifact); + this.emitter.emit('addArtifact', artifact); + } + + appendToArtifact(artifactId: string, data: any) { + const artifact = this.artifacts.get(artifactId); + if (artifact) { + if (typeof artifact.data === 'string') { + artifact.data += data; + } else if (Array.isArray(artifact.data)) { + artifact.data.push(data); + } else if (typeof artifact.data === 'object') { + Object.assign(artifact.data, data); + } + this.emitter.emit('updateArtifact', artifact); + } + } +} + +export default SessionManager; diff --git a/src/lib/types.ts b/src/lib/types.ts index 3dcb0d6..a96c7ca 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -7,3 +7,9 @@ type Chunk = { content: string; metadata: Record; }; + +type Artifact = { + id: string; + type: string; + data: any; +}; From 3cc8882b28ef1430b59b7383e4cc7ffe862d39a7 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:51:49 +0530 Subject: [PATCH 018/196] feat(prompts): add classifier prompt --- src/lib/prompts/search/classifier.ts | 176 +++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 src/lib/prompts/search/classifier.ts diff --git a/src/lib/prompts/search/classifier.ts b/src/lib/prompts/search/classifier.ts new file mode 100644 index 0000000..40e3203 --- /dev/null +++ b/src/lib/prompts/search/classifier.ts @@ -0,0 +1,176 @@ +export const getClassifierPrompt = (input: { + intentDesc: string; + widgetDesc: string; +}) => { + return ` + +You are an expert query classifier for an intelligent search agent. Your task is to analyze user queries and determine the optimal way to answer them—selecting the right intent(s) and widgets. + + + +Given a conversation history and follow-up question, you must: +1. Determine if search should be skipped (skipSearch: boolean) +2. Generate a standalone, self-contained version of the question (standaloneFollowUp: string) +3. Identify the intent(s) that describe how to fulfill the query (intent: array) +4. Select appropriate widgets (widgets: array) + + + +**THE MOST IMPORTANT RULE**: skipSearch should be TRUE only in TWO cases: +1. Widget-only queries (weather, stocks, calculator) +2. Greetings or simple writing tasks (NOT questions) + +**DEFAULT TO skipSearch: false** for everything else, including: +- Any question ("what is", "how does", "explain", "tell me about") +- Any request for information or facts +- Anything you're unsure about + +Ask yourself: "Is the user ASKING about something or requesting INFORMATION?" +- YES → skipSearch: false (use web_search) +- NO (just greeting or simple writing) → skipSearch: true + + + +Follow this decision tree IN ORDER: + +1. **Widget-Only Queries** → skipSearch: TRUE, intent: ['widget_response'] + - Weather queries: "weather in NYC", "temperature in Paris", "is it raining in Seattle" + - Stock queries: "AAPL stock price", "how is Tesla doing", "MSFT stock" + - Calculator queries: "what is 25% of 80", "calculate 15*23", "sqrt(144)" + - These are COMPLETE answers—no search needed + +2. **Writing/Greeting Tasks** → skipSearch: TRUE, intent: ['writing_task'] + - ONLY for greetings and simple writing: + - Greetings: "hello", "hi", "how are you", "thanks", "goodbye" + - Simple writing needing NO facts: "write a thank you email", "draft a birthday message", "compose a poem" + - NEVER for: questions, "what is X", "how does X work", explanations, definitions, facts, code help + - If user is ASKING about something (not requesting writing), use web_search + +3. **Image Display Queries** → skipSearch: FALSE, intent: ['image_preview'] + - "Show me images of cats" + - "Pictures of the Eiffel Tower" + - "Visual examples of modern architecture" + - Requests for images to visualize something + +4. **Widget + Additional Info** → skipSearch: FALSE, intent: ['web_search', 'widget_response'] + - "weather in NYC and best things to do there" + - "AAPL stock and recent Apple news" + - "calculate my mortgage and explain how interest works" + +5. **Pure Search Queries** → skipSearch: FALSE + - Default to web_search for general questions + - Use discussions_search when user explicitly mentions Reddit, forums, opinions, experiences + - Use academic_search when user explicitly mentions research, papers, studies, scientific + - Can combine multiple search intents when appropriate + +6. **Fallback when web_search unavailable** → skipSearch: TRUE, intent: ['writing_task'] or [] + - If no search intents are available and no widgets apply + - Set skipSearch to true and use writing_task or empty intent + + + +Example 1: Widget-only query +Query: "What is the weather in New York?" +Reasoning: User wants current weather → weather widget provides this completely +Output: skipSearch: true, intent: ['widget_response'], widgets: [weather widget for New York] + +Example 2: Widget-only query +Query: "AAPL stock price" +Reasoning: User wants stock price → stock_ticker widget provides this completely +Output: skipSearch: true, intent: ['widget_response'], widgets: [stock_ticker for AAPL] + +Example 3: Widget + search query +Query: "What's the weather in NYC and what are some good outdoor activities?" +Reasoning: Weather widget handles weather, but outdoor activities need web search +Output: skipSearch: false, intent: ['web_search', 'widget_response'], widgets: [weather widget for NYC] + +Example 4: Pure search query +Query: "What are the latest developments in AI?" +Reasoning: No widget applies, needs current web information +Output: skipSearch: false, intent: ['web_search'], widgets: [] + +Example 5: Writing task (greeting/simple writing only) +Query: "Write me a thank you email for a job interview" +Reasoning: Simple writing task needing no external facts → writing_task +Output: skipSearch: true, intent: ['writing_task'], widgets: [] + +Example 5b: Question about something - ALWAYS needs search +Query: "What is Kimi K2?" +Reasoning: User is ASKING about something → needs web search for accurate info +Output: skipSearch: false, intent: ['web_search'], widgets: [] + +Example 5c: Another question - needs search +Query: "Explain how photosynthesis works" +Reasoning: User is ASKING for explanation → needs web search +Output: skipSearch: false, intent: ['web_search'], widgets: [] + +Example 6: Image display +Query: "Show me images of cats" +Reasoning: User wants to see images → requires image search +Output: skipSearch: false, intent: ['image_preview'], widgets: [] + +Example 7: Multiple search sources +Query: "What does the research say about meditation benefits?" +Reasoning: Benefits from both academic papers and web articles +Output: skipSearch: false, intent: ['academic_search', 'web_search'], widgets: [] + +Example 8: Discussions search +Query: "What do people on Reddit think about the new iPhone?" +Reasoning: User explicitly wants forum/community opinions → discussions_search +Output: skipSearch: false, intent: ['discussions_search'], widgets: [] + +Example 9: Academic search only +Query: "Find scientific papers on climate change effects" +Reasoning: User explicitly wants academic/research papers +Output: skipSearch: false, intent: ['academic_search'], widgets: [] + + + +Transform the follow-up into a self-contained question: +- Include ALL necessary context from chat history +- Replace pronouns (it, they, this, that) with specific nouns +- Replace references ("the previous one", "what you mentioned") with actual content +- Preserve the original complexity—don't over-elaborate simple questions +- The question should be answerable without seeing the conversation + + + +Available intents: +${input.intentDesc} + +Rules: +- Include at least one intent when applicable +- For questions/information requests: + - Default to web_search unless user explicitly requests another source + - Use discussions_search when user mentions: Reddit, forums, opinions, experiences, "what do people think" + - Use academic_search when user mentions: research, papers, studies, scientific, scholarly + - Can combine intents (e.g., ['academic_search', 'web_search']) +- If web_search is NOT in available intents and query needs search: + - Check if discussions_search or academic_search applies + - If no search intent available and no widgets: use writing_task or empty array [] +- private_search: ONLY when user provides specific URLs/documents +- widget_response: when widgets fully answer the query +- writing_task: ONLY for greetings and simple writing (never for questions) + + + +Available widgets: +${input.widgetDesc} + +Rules: +- Include ALL applicable widgets regardless of skipSearch value +- Each widget type can only be included once +- Widgets provide structured, real-time data that enhances any response + + + +Your classification must be precise and consistent: +{ + "skipSearch": , + "standaloneFollowUp": "", + "intent": [], + "widgets": [] +} + + `; +}; From afc68ca91f619d3e445d86cd58f55bf54efce171 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:52:24 +0530 Subject: [PATCH 019/196] feat(ollamaLLM): disable thinking in obj mode --- src/lib/models/providers/ollama/ollamaLLM.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/models/providers/ollama/ollamaLLM.ts b/src/lib/models/providers/ollama/ollamaLLM.ts index fd12b77..0e64d7b 100644 --- a/src/lib/models/providers/ollama/ollamaLLM.ts +++ b/src/lib/models/providers/ollama/ollamaLLM.ts @@ -96,9 +96,10 @@ class OllamaLLM extends BaseLLM { model: this.config.model, messages: input.messages, format: z.toJSONSchema(input.schema), + think: false, options: { top_p: this.config.options?.topP, - temperature: this.config.options?.temperature, + temperature: 0, num_predict: this.config.options?.maxTokens, frequency_penalty: this.config.options?.frequencyPenalty, presence_penalty: this.config.options?.presencePenalty, @@ -123,6 +124,7 @@ class OllamaLLM extends BaseLLM { messages: input.messages, format: z.toJSONSchema(input.schema), stream: true, + think: false, options: { top_p: this.config.options?.topP, temperature: this.config.options?.temperature, From cbcb03c7acafd2d05cd2f1873396d03dc6db1915 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:52:41 +0530 Subject: [PATCH 020/196] feat(llm): update return type to partial --- src/lib/models/base/llm.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/lib/models/base/llm.ts b/src/lib/models/base/llm.ts index 5d6f52d..e701fa9 100644 --- a/src/lib/models/base/llm.ts +++ b/src/lib/models/base/llm.ts @@ -1,10 +1,8 @@ import { GenerateObjectInput, - GenerateObjectOutput, GenerateOptions, GenerateTextInput, GenerateTextOutput, - StreamObjectOutput, StreamTextOutput, } from '../types'; @@ -15,12 +13,10 @@ abstract class BaseLLM { abstract streamText( input: GenerateTextInput, ): AsyncGenerator; - abstract generateObject( - input: GenerateObjectInput, - ): Promise>; + abstract generateObject(input: GenerateObjectInput): Promise; abstract streamObject( input: GenerateObjectInput, - ): AsyncGenerator>; + ): AsyncGenerator>; } export default BaseLLM; From 8b515201f341a3aba53f2f3bcbc7bc3a58b14303 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:53:03 +0530 Subject: [PATCH 021/196] feat(app): add search types --- src/lib/agents/search/types.ts | 64 ++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/lib/agents/search/types.ts diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts new file mode 100644 index 0000000..c65d940 --- /dev/null +++ b/src/lib/agents/search/types.ts @@ -0,0 +1,64 @@ +import { EventEmitter } from 'stream'; +import z from 'zod'; +import BaseLLM from '../../models/base/llm'; +import BaseEmbedding from '@/lib/models/base/embedding'; + +export type SearchSources = 'web' | 'discussions' | 'academic'; + +export type SearchAgentConfig = { + sources: SearchSources[]; + llm: BaseLLM; + embedding: BaseEmbedding; +}; + +export type SearchAgentInput = { + chatHistory: Message[]; + followUp: string; + config: SearchAgentConfig; +}; + +export interface Intent { + name: string; + description: string; + requiresSearch: boolean; + enabled: (config: { sources: SearchSources[] }) => boolean; +} + +export type Widget = z.ZodObject> = { + name: string; + description: string; + schema: TSchema; + execute: ( + params: z.infer, + additionalConfig: AdditionalConfig, + ) => Promise; +}; + +export type WidgetConfig = { + type: string; + params: Record; +}; + +export type WidgetOutput = { + type: string; + data: any; +}; + +export type ClassifierInput = { + llm: BaseLLM; + enabledSources: SearchSources[]; + query: string; + chatHistory: Message[]; +}; + +export type ClassifierOutput = { + skipSearch: boolean; + intents: string[]; + widgets: WidgetConfig[]; +}; + +export type AdditionalConfig = { + llm: BaseLLM; + embedding: BaseLLM; + emitter: EventEmitter; +}; From 036b44611f054d20240a3ca5198b2afc12f710fa Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:55:24 +0530 Subject: [PATCH 022/196] feat(search): add classifier --- src/lib/agents/search/classifier/index.ts | 72 +++++++++++++++++++ .../classifier/intents/academicSearch.ts | 11 +++ .../classifier/intents/discussionSearch.ts | 11 +++ .../agents/search/classifier/intents/index.ts | 14 ++++ .../search/classifier/intents/registry.ts | 29 ++++++++ .../search/classifier/intents/webSearch.ts | 11 +++ .../classifier/intents/widgetResponse.ts | 11 +++ .../search/classifier/intents/writingTask.ts | 11 +++ src/lib/agents/search/types.ts | 1 + 9 files changed, 171 insertions(+) create mode 100644 src/lib/agents/search/classifier/index.ts create mode 100644 src/lib/agents/search/classifier/intents/academicSearch.ts create mode 100644 src/lib/agents/search/classifier/intents/discussionSearch.ts create mode 100644 src/lib/agents/search/classifier/intents/index.ts create mode 100644 src/lib/agents/search/classifier/intents/registry.ts create mode 100644 src/lib/agents/search/classifier/intents/webSearch.ts create mode 100644 src/lib/agents/search/classifier/intents/widgetResponse.ts create mode 100644 src/lib/agents/search/classifier/intents/writingTask.ts diff --git a/src/lib/agents/search/classifier/index.ts b/src/lib/agents/search/classifier/index.ts new file mode 100644 index 0000000..9b2bf6b --- /dev/null +++ b/src/lib/agents/search/classifier/index.ts @@ -0,0 +1,72 @@ +import z from 'zod'; +import { ClassifierInput, ClassifierOutput } from '../types'; +import { WidgetRegistry } from '../widgets'; +import { IntentRegistry } from './intents'; +import { getClassifierPrompt } from '@/lib/prompts/search/classifier'; +import formatChatHistoryAsString from '@/lib/utils/formatHistory'; + +class Classifier { + async classify(input: ClassifierInput): Promise { + const availableIntents = IntentRegistry.getAvailableIntents({ + sources: input.enabledSources, + }); + const availableWidgets = WidgetRegistry.getAll(); + + const classificationSchema = z.object({ + skipSearch: z + .boolean() + .describe( + 'Set to true to SKIP search. Skip ONLY when: (1) widgets alone fully answer the query (e.g., weather, stocks, calculator), (2) simple greetings or writing tasks (NOT questions). Set to false for ANY question or information request.', + ), + standaloneFollowUp: z + .string() + .describe( + 'A self-contained, context-independent reformulation of the user\'s question. Must include all necessary context from chat history, replace pronouns with specific nouns, and be clear enough to answer without seeing the conversation. Keep the same complexity as the original question.', + ), + intents: z + .array(z.enum(availableIntents.map((i) => i.name))) + .describe( + 'The intent(s) that best describe how to fulfill the user\'s query. Can include multiple intents (e.g., [\'web_search\', \'widget_response\'] for \'weather in NYC and recent news\'). Always include at least one intent when applicable.', + ), + widgets: z + .array(z.union(availableWidgets.map((w) => w.schema))) + .describe( + 'Widgets that can display structured data to answer (fully or partially) the query. Include all applicable widgets regardless of skipSearch value.', + ), + }); + + const classifierPrompt = getClassifierPrompt({ + intentDesc: IntentRegistry.getDescriptions({ + sources: input.enabledSources, + }), + widgetDesc: WidgetRegistry.getDescriptions(), + }); + + const res = await input.llm.generateObject< + z.infer + >({ + messages: [ + { + role: 'system', + content: classifierPrompt, + }, + { + role: 'user', + content: `${formatChatHistoryAsString(input.chatHistory)}\n\n${input.query}`, + }, + ], + schema: classificationSchema, + }); + + res.widgets = res.widgets.map((widgetConfig) => { + return { + type: widgetConfig.type, + params: widgetConfig, + }; + }); + + return res as ClassifierOutput; + } +} + +export default Classifier; diff --git a/src/lib/agents/search/classifier/intents/academicSearch.ts b/src/lib/agents/search/classifier/intents/academicSearch.ts new file mode 100644 index 0000000..b6da377 --- /dev/null +++ b/src/lib/agents/search/classifier/intents/academicSearch.ts @@ -0,0 +1,11 @@ +import { Intent } from '../../types'; + +const academicSearchIntent: Intent = { + name: 'academic_search', + description: + 'Use this intent to find scholarly articles, research papers, and academic resources when the user is seeking credible and authoritative information on a specific topic.', + requiresSearch: true, + enabled: (config) => config.sources.includes('academic'), +}; + +export default academicSearchIntent; diff --git a/src/lib/agents/search/classifier/intents/discussionSearch.ts b/src/lib/agents/search/classifier/intents/discussionSearch.ts new file mode 100644 index 0000000..76b3b01 --- /dev/null +++ b/src/lib/agents/search/classifier/intents/discussionSearch.ts @@ -0,0 +1,11 @@ +import { Intent } from '../../types'; + +const discussionSearchIntent: Intent = { + name: 'discussion_search', + description: + 'Use this intent to search through discussion forums, community boards, or social media platforms when the user is looking for opinions, experiences, or community-driven information on a specific topic.', + requiresSearch: true, + enabled: (config) => config.sources.includes('discussions'), +}; + +export default discussionSearchIntent; diff --git a/src/lib/agents/search/classifier/intents/index.ts b/src/lib/agents/search/classifier/intents/index.ts new file mode 100644 index 0000000..feefd2d --- /dev/null +++ b/src/lib/agents/search/classifier/intents/index.ts @@ -0,0 +1,14 @@ +import academicSearchIntent from './academicSearch'; +import discussionSearchIntent from './discussionSearch'; +import IntentRegistry from './registry'; +import webSearchIntent from './webSearch'; +import widgetResponseIntent from './widgetResponse'; +import writingTaskIntent from './writingTask'; + +IntentRegistry.register(webSearchIntent); +IntentRegistry.register(academicSearchIntent); +IntentRegistry.register(discussionSearchIntent); +IntentRegistry.register(widgetResponseIntent); +IntentRegistry.register(writingTaskIntent); + +export { IntentRegistry }; diff --git a/src/lib/agents/search/classifier/intents/registry.ts b/src/lib/agents/search/classifier/intents/registry.ts new file mode 100644 index 0000000..bc3464b --- /dev/null +++ b/src/lib/agents/search/classifier/intents/registry.ts @@ -0,0 +1,29 @@ +import { Intent, SearchAgentConfig, SearchSources } from '../../types'; + +class IntentRegistry { + private static intents = new Map(); + + static register(intent: Intent) { + this.intents.set(intent.name, intent); + } + + static get(name: string): Intent | undefined { + return this.intents.get(name); + } + + static getAvailableIntents(config: { sources: SearchSources[] }): Intent[] { + return Array.from( + this.intents.values().filter((intent) => intent.enabled(config)), + ); + } + + static getDescriptions(config: { sources: SearchSources[] }): string { + const availableintnets = this.getAvailableIntents(config); + + return availableintnets + .map((intent) => `${intent.name}: ${intent.description}`) + .join('\n\n'); + } +} + +export default IntentRegistry; diff --git a/src/lib/agents/search/classifier/intents/webSearch.ts b/src/lib/agents/search/classifier/intents/webSearch.ts new file mode 100644 index 0000000..9fccd2f --- /dev/null +++ b/src/lib/agents/search/classifier/intents/webSearch.ts @@ -0,0 +1,11 @@ +import { Intent } from '../../types'; + +const webSearchIntent: Intent = { + name: 'web_search', + description: + 'Use this intent to find current information from the web when the user is asking a question or needs up-to-date information that cannot be provided by widgets or other intents.', + requiresSearch: true, + enabled: (config) => config.sources.includes('web'), +}; + +export default webSearchIntent; diff --git a/src/lib/agents/search/classifier/intents/widgetResponse.ts b/src/lib/agents/search/classifier/intents/widgetResponse.ts new file mode 100644 index 0000000..0cfd58d --- /dev/null +++ b/src/lib/agents/search/classifier/intents/widgetResponse.ts @@ -0,0 +1,11 @@ +import { Intent } from '../../types'; + +const widgetResponseIntent: Intent = { + name: 'widget_response', + description: + 'Use this intent to respond to user queries using available widgets when the required information can be obtained from them.', + requiresSearch: false, + enabled: (config) => true, +}; + +export default widgetResponseIntent; diff --git a/src/lib/agents/search/classifier/intents/writingTask.ts b/src/lib/agents/search/classifier/intents/writingTask.ts new file mode 100644 index 0000000..95b5af6 --- /dev/null +++ b/src/lib/agents/search/classifier/intents/writingTask.ts @@ -0,0 +1,11 @@ +import { Intent } from '../../types'; + +const writingTaskIntent: Intent = { + name: 'writing_task', + description: + 'Use this intent to assist users with writing tasks such as drafting emails, creating documents, or generating content based on their instructions or greetings.', + requiresSearch: false, + enabled: (config) => true, +}; + +export default writingTaskIntent; diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts index c65d940..72d7ecf 100644 --- a/src/lib/agents/search/types.ts +++ b/src/lib/agents/search/types.ts @@ -53,6 +53,7 @@ export type ClassifierInput = { export type ClassifierOutput = { skipSearch: boolean; + standaloneFollowUp: string; intents: string[]; widgets: WidgetConfig[]; }; From 1614cfa5e54395610831c6e5f5df9ed9d3489838 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:55:50 +0530 Subject: [PATCH 023/196] feat(app): add widgets --- src/lib/agents/search/widgets/index.ts | 6 + src/lib/agents/search/widgets/registry.ts | 65 +++++++++ .../agents/search/widgets/weatherWidget.ts | 123 ++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 src/lib/agents/search/widgets/index.ts create mode 100644 src/lib/agents/search/widgets/registry.ts create mode 100644 src/lib/agents/search/widgets/weatherWidget.ts diff --git a/src/lib/agents/search/widgets/index.ts b/src/lib/agents/search/widgets/index.ts new file mode 100644 index 0000000..7ddc597 --- /dev/null +++ b/src/lib/agents/search/widgets/index.ts @@ -0,0 +1,6 @@ +import WidgetRegistry from './registry'; +import weatherWidget from './weatherWidget'; + +WidgetRegistry.register(weatherWidget); + +export { WidgetRegistry }; diff --git a/src/lib/agents/search/widgets/registry.ts b/src/lib/agents/search/widgets/registry.ts new file mode 100644 index 0000000..d8ceaba --- /dev/null +++ b/src/lib/agents/search/widgets/registry.ts @@ -0,0 +1,65 @@ +import { + AdditionalConfig, + SearchAgentConfig, + Widget, + WidgetConfig, + WidgetOutput, +} from '../types'; + +class WidgetRegistry { + private static widgets = new Map(); + + static register(widget: Widget) { + this.widgets.set(widget.name, widget); + } + + static get(name: string): Widget | undefined { + return this.widgets.get(name); + } + + static getAll(): Widget[] { + return Array.from(this.widgets.values()); + } + + static getDescriptions(): string { + return Array.from(this.widgets.values()) + .map((widget) => `${widget.name}: ${widget.description}`) + .join('\n\n'); + } + + static async execute( + name: string, + params: any, + config: AdditionalConfig, + ): Promise { + const widget = this.get(name); + + if (!widget) { + throw new Error(`Widget with name ${name} not found`); + } + + return widget.execute(params, config); + } + + static async executeAll( + widgets: WidgetConfig[], + additionalConfig: AdditionalConfig, + ): Promise { + const results: WidgetOutput[] = []; + + await Promise.all( + widgets.map(async (widgetConfig) => { + const output = await this.execute( + widgetConfig.type, + widgetConfig.params, + additionalConfig, + ); + results.push(output); + }), + ); + + return results; + } +} + +export default WidgetRegistry; diff --git a/src/lib/agents/search/widgets/weatherWidget.ts b/src/lib/agents/search/widgets/weatherWidget.ts new file mode 100644 index 0000000..b9d048c --- /dev/null +++ b/src/lib/agents/search/widgets/weatherWidget.ts @@ -0,0 +1,123 @@ +import z from 'zod'; +import { Widget } from '../types'; + +const WeatherWidgetSchema = z.object({ + type: z.literal('weather'), + location: z + .string() + .describe( + 'Human-readable location name (e.g., "New York, NY, USA", "London, UK"). Use this OR lat/lon coordinates, never both. Leave empty string if providing coordinates.', + ), + lat: z + .number() + .describe( + 'Latitude coordinate in decimal degrees (e.g., 40.7128). Only use when location name is empty.', + ), + lon: z + .number() + .describe( + 'Longitude coordinate in decimal degrees (e.g., -74.0060). Only use when location name is empty.', + ), +}); + +const weatherWidget = { + name: 'weather', + description: + 'Provides current weather information for a specified location. It can return details such as temperature, humidity, wind speed, and weather conditions. It needs either a location name or latitude/longitude coordinates to function.', + schema: WeatherWidgetSchema, + execute: async (params, _) => { + if ( + params.location === '' && + (params.lat === undefined || params.lon === undefined) + ) { + throw new Error( + 'Either location name or both latitude and longitude must be provided.', + ); + } + + if (params.location !== '') { + const openStreetMapUrl = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(params.location)}&format=json&limit=1`; + + const locationRes = await fetch(openStreetMapUrl, { + headers: { + 'User-Agent': 'Perplexica', + 'Content-Type': 'application/json', + }, + }); + + const data = await locationRes.json(); + + const location = data[0]; + + if (!location) { + throw new Error( + `Could not find coordinates for location: ${params.location}`, + ); + } + + const weatherRes = await fetch( + `https://api.open-meteo.com/v1/forecast?latitude=${location.lat}&longitude=${location.lon}¤t_weather=true`, + { + headers: { + 'User-Agent': 'Perplexica', + 'Content-Type': 'application/json', + }, + }, + ); + + const weatherData = await weatherRes.json(); + + /* this is like a very simple implementation just to see the bacckend works, when we're working on the frontend, we'll return more data i guess? */ + return { + type: 'weather', + data: { + location: params.location, + latitude: location.lat, + longitude: location.lon, + weather: weatherData.current_weather, + }, + }; + } else if (params.lat !== undefined && params.lon !== undefined) { + const [weatherRes, locationRes] = await Promise.all([ + fetch( + `https://api.open-meteo.com/v1/forecast?latitude=${params.lat}&longitude=${params.lon}¤t_weather=true`, + { + headers: { + 'User-Agent': 'Perplexica', + 'Content-Type': 'application/json', + }, + }, + ), + fetch( + `https://nominatim.openstreetmap.org/reverse?lat=${params.lat}&lon=${params.lon}&format=json`, + { + headers: { + 'User-Agent': 'Perplexica', + 'Content-Type': 'application/json', + }, + }, + ), + ]); + + const weatherData = await weatherRes.json(); + const locationData = await locationRes.json(); + + return { + type: 'weather', + data: { + location: locationData.display_name, + latitude: params.lat, + longitude: params.lon, + weather: weatherData.current_weather, + }, + }; + } + + return { + type: 'weather', + data: null, + }; + }, +} satisfies Widget; + +export default weatherWidget; From 9b85c63a80e5ee3e5673c0776e44de6c5fb95da8 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 21 Nov 2025 23:49:14 +0530 Subject: [PATCH 024/196] feat(db): migrate schema --- src/lib/db/schema.ts | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index b6924ac..71d441f 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -1,26 +1,22 @@ import { sql } from 'drizzle-orm'; import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core'; -import { Document } from '@langchain/core/documents'; export const messages = sqliteTable('messages', { id: integer('id').primaryKey(), - role: text('type', { enum: ['assistant', 'user', 'source'] }).notNull(), - chatId: text('chatId').notNull(), - createdAt: text('createdAt') - .notNull() - .default(sql`CURRENT_TIMESTAMP`), messageId: text('messageId').notNull(), - - content: text('content'), - - sources: text('sources', { - mode: 'json', - }) - .$type() + chatId: text('chatId').notNull(), + backendId: text('backendId').notNull(), + query: text('query').notNull(), + createdAt: text('createdAt').notNull(), + responseBlocks: text('responseBlocks', { mode: 'json' }) + .$type() .default(sql`'[]'`), + status: text({ enum: ['answering', 'completed', 'error'] }).default( + 'answering', + ), }); -interface File { +interface DBFile { name: string; fileId: string; } @@ -31,6 +27,6 @@ export const chats = sqliteTable('chats', { createdAt: text('createdAt').notNull(), focusMode: text('focusMode').notNull(), files: text('files', { mode: 'json' }) - .$type() + .$type() .default(sql`'[]'`), }); From a494d4c3296958c7c2b01aa54da4053e8ac42838 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 21 Nov 2025 23:49:27 +0530 Subject: [PATCH 025/196] feat(app): fix migration errors --- src/lib/db/migrate.ts | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/lib/db/migrate.ts b/src/lib/db/migrate.ts index e4c6987..5e2c374 100644 --- a/src/lib/db/migrate.ts +++ b/src/lib/db/migrate.ts @@ -18,12 +18,18 @@ db.exec(` `); function sanitizeSql(content: string) { - return content - .split(/\r?\n/) - .filter( - (l) => !l.trim().startsWith('-->') && !l.includes('statement-breakpoint'), + const statements = content + .split(/--> statement-breakpoint/g) + .map((stmt) => + stmt + .split(/\r?\n/) + .filter((l) => !l.trim().startsWith('-->')) + .join('\n') + .trim(), ) - .join('\n'); + .filter((stmt) => stmt.length > 0); + + return statements; } fs.readdirSync(migrationsFolder) @@ -32,7 +38,7 @@ fs.readdirSync(migrationsFolder) .forEach((file) => { const filePath = path.join(migrationsFolder, file); let content = fs.readFileSync(filePath, 'utf-8'); - content = sanitizeSql(content); + const statements = sanitizeSql(content); const migrationName = file.split('_')[0] || file; @@ -108,7 +114,12 @@ fs.readdirSync(migrationsFolder) db.exec('DROP TABLE messages;'); db.exec('ALTER TABLE messages_with_sources RENAME TO messages;'); } else { - db.exec(content); + // Execute each statement separately + statements.forEach((stmt) => { + if (stmt.trim()) { + db.exec(stmt); + } + }); } db.prepare('INSERT OR IGNORE INTO ran_migrations (name) VALUES (?)').run( From 2568088341e168fe30cf675e0d8f7f157880456b Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 21 Nov 2025 23:49:52 +0530 Subject: [PATCH 026/196] feat(db): add new migration files --- drizzle/0002_daffy_wrecker.sql | 15 ++++ drizzle/meta/0002_snapshot.json | 132 ++++++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 ++ 3 files changed, 154 insertions(+) create mode 100644 drizzle/0002_daffy_wrecker.sql create mode 100644 drizzle/meta/0002_snapshot.json diff --git a/drizzle/0002_daffy_wrecker.sql b/drizzle/0002_daffy_wrecker.sql new file mode 100644 index 0000000..78b6685 --- /dev/null +++ b/drizzle/0002_daffy_wrecker.sql @@ -0,0 +1,15 @@ +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_messages` ( + `id` integer PRIMARY KEY NOT NULL, + `messageId` text NOT NULL, + `chatId` text NOT NULL, + `backendId` text NOT NULL, + `query` text NOT NULL, + `createdAt` text NOT NULL, + `responseBlocks` text DEFAULT '[]', + `status` text DEFAULT 'answering' +); +--> statement-breakpoint +DROP TABLE `messages`;--> statement-breakpoint +ALTER TABLE `__new_messages` RENAME TO `messages`;--> statement-breakpoint +PRAGMA foreign_keys=ON; \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..2890f66 --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,132 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "1c5eb804-d6b4-48ec-9a8f-75fb729c8e52", + "prevId": "6dedf55f-0e44-478f-82cf-14a21ac686f8", + "tables": { + "chats": { + "name": "chats", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "focusMode": { + "name": "focusMode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "files": { + "name": "files", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "messageId": { + "name": "messageId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "chatId": { + "name": "chatId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "backendId": { + "name": "backendId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "query": { + "name": "query", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "responseBlocks": { + "name": "responseBlocks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'answering'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index cf1610b..c271ddc 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1758863991284, "tag": "0001_wise_rockslide", "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1763732708332, + "tag": "0002_daffy_wrecker", + "breakpoints": true } ] } From 70bcd8c6f1535a2b43ca4351f228be0815800ce8 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 21 Nov 2025 23:51:09 +0530 Subject: [PATCH 027/196] feat(types): add artifact to block, add more blocks --- src/lib/types.ts | 64 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/src/lib/types.ts b/src/lib/types.ts index a96c7ca..824aea0 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,4 +1,4 @@ -type Message = { +type ChatTurnMessage = { role: 'user' | 'assistant' | 'system'; content: string; }; @@ -8,8 +8,64 @@ type Chunk = { metadata: Record; }; -type Artifact = { +type TextBlock = { id: string; - type: string; - data: any; + type: 'text'; + data: string; }; + +type SourceBlock = { + id: string; + type: 'source'; + data: Chunk[]; +}; + +type SuggestionBlock = { + id: string; + type: 'suggestion'; + data: string[]; +}; + +type WidgetBlock = { + id: string; + type: 'widget'; + data: { + widgetType: string; + params: Record; + }; +}; + +type ReasoningResearchBlock = { + id: string; + reasoning: string; +}; + +type SearchingResearchBlock = { + id: string; + searching: string[]; +}; + +type ReadingResearchBlock = { + id: string; + reading: Chunk[]; +}; + +type ResearchBlockSubStep = + | ReasoningResearchBlock + | SearchingResearchBlock + | ReadingResearchBlock; + +type ResearchBlock = { + id: string; + type: 'research'; + data: { + subSteps: ResearchBlockSubStep[]; + }; +}; + +type Block = + | TextBlock + | SourceBlock + | SuggestionBlock + | WidgetBlock + | ResearchBlock; From f7a43b3cb95dfb5ad49ec6c1d34f25638438037e Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 21 Nov 2025 23:52:55 +0530 Subject: [PATCH 028/196] feat(session): use blocks, use rfc6902 for stream with patching --- src/lib/session.ts | 72 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/src/lib/session.ts b/src/lib/session.ts index 6c5e18e..834cbfd 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -1,13 +1,20 @@ import { EventEmitter } from 'stream'; -/* todo implement history saving and better artifact typing and handling */ +import { applyPatch } from 'rfc6902'; + class SessionManager { private static sessions = new Map(); readonly id: string; - private artifacts = new Map(); + private blocks = new Map(); + private events: { event: string; data: any }[] = []; private emitter = new EventEmitter(); + private TTL_MS = 30 * 60 * 1000; - constructor() { - this.id = crypto.randomUUID(); + constructor(id?: string) { + this.id = id ?? crypto.randomUUID(); + + setTimeout(() => { + SessionManager.sessions.delete(this.id); + }, this.TTL_MS); } static getSession(id: string): SessionManager | undefined { @@ -18,26 +25,55 @@ class SessionManager { return Array.from(this.sessions.values()); } + static createSession(): SessionManager { + const session = new SessionManager(); + this.sessions.set(session.id, session); + return session; + } + + removeAllListeners() { + this.emitter.removeAllListeners(); + } + emit(event: string, data: any) { this.emitter.emit(event, data); + this.events.push({ event, data }); } - emitArtifact(artifact: Artifact) { - this.artifacts.set(artifact.id, artifact); - this.emitter.emit('addArtifact', artifact); + emitBlock(block: Block) { + this.blocks.set(block.id, block); + this.emit('data', { + type: 'block', + block: block, + }); } - appendToArtifact(artifactId: string, data: any) { - const artifact = this.artifacts.get(artifactId); - if (artifact) { - if (typeof artifact.data === 'string') { - artifact.data += data; - } else if (Array.isArray(artifact.data)) { - artifact.data.push(data); - } else if (typeof artifact.data === 'object') { - Object.assign(artifact.data, data); - } - this.emitter.emit('updateArtifact', artifact); + getBlock(blockId: string): Block | undefined { + return this.blocks.get(blockId); + } + + updateBlock(blockId: string, patch: any[]) { + const block = this.blocks.get(blockId); + + if (block) { + applyPatch(block, patch); + this.blocks.set(blockId, block); + this.emit('data', { + type: 'updateBlock', + blockId: blockId, + patch: patch, + }); + } + } + + addListener(event: string, listener: (data: any) => void) { + this.emitter.addListener(event, listener); + } + + replay() { + for (const { event, data } of this.events) { + /* Using emitter directly to avoid infinite loop */ + this.emitter.emit(event, data); } } } From 4016b21bdfaa37165609098ad133617178c427c6 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 21 Nov 2025 23:54:16 +0530 Subject: [PATCH 029/196] Update formatHistory.ts --- src/lib/utils/formatHistory.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib/utils/formatHistory.ts b/src/lib/utils/formatHistory.ts index 733ffc0..8256ffa 100644 --- a/src/lib/utils/formatHistory.ts +++ b/src/lib/utils/formatHistory.ts @@ -1,10 +1,8 @@ -import { BaseMessage, isAIMessage } from '@langchain/core/messages'; - -const formatChatHistoryAsString = (history: BaseMessage[]) => { +const formatChatHistoryAsString = (history: Message[]) => { return history .map( (message) => - `${isAIMessage(message) ? 'AI' : 'User'}: ${message.content}`, + `${message.role === 'assistant' ? 'AI' : 'User'}: ${message.content}`, ) .join('\n'); }; From 0df0114e76ab0b637026c8bdf6e0f0a9eb2853f1 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 21 Nov 2025 23:54:30 +0530 Subject: [PATCH 030/196] feat(prompts): add researcher prompt --- src/lib/prompts/search/researcher.ts | 241 +++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 src/lib/prompts/search/researcher.ts diff --git a/src/lib/prompts/search/researcher.ts b/src/lib/prompts/search/researcher.ts new file mode 100644 index 0000000..2c58b9b --- /dev/null +++ b/src/lib/prompts/search/researcher.ts @@ -0,0 +1,241 @@ +export const getResearcherPrompt = ( + actionDesc: string, + mode: 'fast' | 'balanced' | 'deep_research', +) => { + const today = new Date().toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }); + + return ` +You are an action orchestrator. Your job is to fulfill user requests by selecting and executing appropriate actions - whether that's searching for information, creating calendar events, sending emails, or any other available action. + +Today's date: ${today} + +You are operating in "${mode}" mode. ${ + mode === 'fast' + ? 'Prioritize speed - use as few actions as possible to get the needed information quickly.' + : mode === 'balanced' + ? 'Balance speed and depth - use a moderate number of actions to get good information efficiently. Never stop at the first action unless there is no action available or the query is simple.' + : 'Conduct deep research - use multiple actions to gather comprehensive information, even if it takes longer.' + } + + +${actionDesc} + + + + +NEVER ASSUME - your knowledge may be outdated. When a user asks about something you're not certain about, go find out. Don't assume it exists or doesn't exist - just look it up directly. + + + + + +Think like a human would. Your reasoning should be natural and show: +- What the user is asking for +- What you need to find out or do +- Your plan to accomplish it + +Keep it to 2-3 natural sentences. + + + + + +## Example 1: Unknown Subject + +User: "What is Kimi K2?" + +Good reasoning: +"I'm not sure what Kimi K2 is - could be an AI model, a product, or something else. Let me look it up to find out what it actually is and get the relevant details." + +Actions: web_search ["Kimi K2", "Kimi K2 AI"] + +## Example 2: Subject You're Uncertain About + +User: "What are the features of GPT-5.1?" + +Good reasoning: +"I don't have current information on GPT-5.1 - my knowledge might be outdated. Let me look up GPT-5.1 to see what's available and what features it has." + +Actions: web_search ["GPT-5.1", "GPT-5.1 features", "GPT-5.1 release"] + +Bad reasoning (wastes time on verification): +"GPT-5.1 might not exist based on my knowledge. I need to verify if it exists first before looking for features." + +## Example 3: After Actions Return Results + +User: "What are the features of GPT-5.1?" +[Previous actions returned information about GPT-5.1] + +Good reasoning: +"Got the information I needed about GPT-5.1. The results cover its features and capabilities - I can now provide a complete answer." + +Action: done + +## Example 4: Ambiguous Query + +User: "Tell me about Mercury" + +Good reasoning: +"Mercury could refer to several things - the planet, the element, or something else. I'll look up both main interpretations to give a useful answer." + +Actions: web_search ["Mercury planet facts", "Mercury element"] + +## Example 5: Current Events + +User: "What's happening with AI regulation?" + +Good reasoning: +"I need current news on AI regulation developments. Let me find the latest updates on this topic." + +Actions: web_search ["AI regulation news 2024", "AI regulation bill latest"] + +## Example 6: Technical Query + +User: "How do I set up authentication in Next.js 14?" + +Good reasoning: +"This is a technical implementation question. I'll find the current best practices and documentation for Next.js 14 authentication." + +Actions: web_search ["Next.js 14 authentication guide", "NextAuth.js App Router"] + +## Example 7: Comparison Query + +User: "Prisma vs Drizzle - which should I use?" + +Good reasoning: +"Need to find factual comparisons between these ORMs - performance, features, trade-offs. Let me gather objective information." + +Actions: web_search ["Prisma vs Drizzle comparison 2024", "Drizzle ORM performance"] + +## Example 8: Fact-Check + +User: "Is it true you only use 10% of your brain?" + +Good reasoning: +"This is a common claim that needs scientific verification. Let me find what the actual research says about this." + +Actions: web_search ["10 percent brain myth science", "brain usage neuroscience"] + +## Example 9: Recent Product + +User: "What are the specs of MacBook Pro M4?" + +Good reasoning: +"I need current information on the MacBook Pro M4. Let me look up the latest specs and details." + +Actions: web_search ["MacBook Pro M4 specs", "MacBook Pro M4 specifications Apple"] + +## Example 10: Multi-Part Query + +User: "Population of Tokyo vs New York?" + +Good reasoning: +"Need current population stats for both cities. I'll look up the comparison data." + +Actions: web_search ["Tokyo population 2024", "Tokyo vs New York population"] + +## Example 11: Calendar Task + +User: "Add a meeting with John tomorrow at 3pm" + +Good reasoning: +"This is a calendar task. I have all the details - meeting with John, tomorrow, 3pm. I'll create the event." + +Action: create_calendar_event with the provided details + +## Example 12: Email Task + +User: "Send an email to sarah@company.com about the project update" + +Good reasoning: +"Need to send an email. I have the recipient but need to compose appropriate content about the project update." + +Action: send_email to sarah@company.com with project update content + +## Example 13: Multi-Step Task + +User: "What's the weather in Tokyo and add a reminder to pack an umbrella if it's rainy" + +Good reasoning: +"Two things here - first I need to check Tokyo's weather, then based on that I might need to create a reminder. Let me start with the weather lookup." + +Actions: web_search ["Tokyo weather today forecast"] + +## Example 14: Research Then Act + +User: "Find the best Italian restaurant near me and make a reservation for 7pm" + +Good reasoning: +"I need to first find top Italian restaurants in the area, then make a reservation. Let me start by finding the options." + +Actions: web_search ["best Italian restaurant near me", "top rated Italian restaurants"] + + + + + +## For Information Queries: +- Just look it up - don't overthink whether something exists +- Use 1-3 targeted queries +- Done when you have useful information to answer with + +## For Task Execution: +- Calendar, email, reminders: execute directly with the provided details +- If details are missing, note what you need + +## For Multi-Step Requests: +- Break it down logically +- Complete one part before moving to the next +- Some tasks require information before you can act + +## When to Select "done": +- You have the information needed to answer +- You've completed the requested task +- Further actions would be redundant + + + + + +**General subjects:** +- ["subject name", "subject name + context"] + +**Current events:** +- Include year: "topic 2024", "topic latest news" + +**Technical topics:** +- Include versions: "framework v14 guide" +- Add context: "documentation", "tutorial", "how to" + +**Comparisons:** +- "X vs Y comparison", "X vs Y benchmarks" + +**Keep it simple:** +- 1-3 actions per iteration +- Don't over-complicate queries + + + + + +1. **Over-assuming**: Don't assume things exist or don't exist - just look them up + +2. **Verification obsession**: Don't waste actions "verifying existence" - just search for the thing directly + +3. **Endless loops**: If 2-3 actions don't find something, it probably doesn't exist - report that and move on + +4. **Ignoring task context**: If user wants a calendar event, don't just search - create the event + +5. **Overthinking**: Keep reasoning simple and action-focused + + + + +Reasoning should be 2-3 natural sentences showing your thought process and plan. Then select and configure the appropriate action(s). + +`; +}; From 08feb18197f68c87fba1d8c952676d7eb90d2f73 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 21 Nov 2025 23:57:29 +0530 Subject: [PATCH 031/196] feat(search-agent): add researcher, research actions --- .../agents/search/researcher/actions/done.ts | 19 +++ .../agents/search/researcher/actions/index.ts | 8 ++ .../search/researcher/actions/registry.ts | 73 +++++++++++ .../search/researcher/actions/webSearch.ts | 54 +++++++++ src/lib/agents/search/researcher/index.ts | 113 ++++++++++++++++++ src/lib/agents/search/types.ts | 51 +++++++- 6 files changed, 313 insertions(+), 5 deletions(-) create mode 100644 src/lib/agents/search/researcher/actions/done.ts create mode 100644 src/lib/agents/search/researcher/actions/index.ts create mode 100644 src/lib/agents/search/researcher/actions/registry.ts create mode 100644 src/lib/agents/search/researcher/actions/webSearch.ts create mode 100644 src/lib/agents/search/researcher/index.ts diff --git a/src/lib/agents/search/researcher/actions/done.ts b/src/lib/agents/search/researcher/actions/done.ts new file mode 100644 index 0000000..9ce93ae --- /dev/null +++ b/src/lib/agents/search/researcher/actions/done.ts @@ -0,0 +1,19 @@ +import z from 'zod'; +import { ResearchAction } from '../../types'; + +const doneAction: ResearchAction = { + name: 'done', + description: + "Indicates that the research process is complete and no further actions are needed. Use this action when you have gathered sufficient information to answer the user's query.", + enabled: (_) => true, + schema: z.object({ + type: z.literal('done'), + }), + execute: async (params, additionalConfig) => { + return { + type: 'done', + }; + }, +}; + +export default doneAction; diff --git a/src/lib/agents/search/researcher/actions/index.ts b/src/lib/agents/search/researcher/actions/index.ts new file mode 100644 index 0000000..4814b1e --- /dev/null +++ b/src/lib/agents/search/researcher/actions/index.ts @@ -0,0 +1,8 @@ +import doneAction from './done'; +import ActionRegistry from './registry'; +import webSearchAction from './webSearch'; + +ActionRegistry.register(webSearchAction); +ActionRegistry.register(doneAction); + +export { ActionRegistry }; diff --git a/src/lib/agents/search/researcher/actions/registry.ts b/src/lib/agents/search/researcher/actions/registry.ts new file mode 100644 index 0000000..4172e8b --- /dev/null +++ b/src/lib/agents/search/researcher/actions/registry.ts @@ -0,0 +1,73 @@ +import { + ActionConfig, + ActionOutput, + AdditionalConfig, + ClassifierOutput, + ResearchAction, +} from '../../types'; + +class ActionRegistry { + private static actions: Map = new Map(); + + static register(action: ResearchAction) { + this.actions.set(action.name, action); + } + + static get(name: string): ResearchAction | undefined { + return this.actions.get(name); + } + + static getAvailableActions(config: { + classification: ClassifierOutput; + }): ResearchAction[] { + return Array.from( + this.actions.values().filter((action) => action.enabled(config)), + ); + } + + static getAvailableActionsDescriptions(config: { + classification: ClassifierOutput; + }): string { + const availableActions = this.getAvailableActions(config); + + return availableActions + .map((action) => `------------\n##${action.name}\n${action.description}`) + .join('\n\n'); + } + + static async execute( + name: string, + params: any, + additionalConfig: AdditionalConfig, + ) { + const action = this.actions.get(name); + + if (!action) { + throw new Error(`Action with name ${name} not found`); + } + + return action.execute(params, additionalConfig); + } + + static async executeAll( + actions: ActionConfig[], + additionalConfig: AdditionalConfig, + ): Promise { + const results: ActionOutput[] = []; + + await Promise.all( + actions.map(async (actionConfig) => { + const output = await this.execute( + actionConfig.type, + actionConfig.params, + additionalConfig, + ); + results.push(output); + }), + ); + + return results; + } +} + +export default ActionRegistry; diff --git a/src/lib/agents/search/researcher/actions/webSearch.ts b/src/lib/agents/search/researcher/actions/webSearch.ts new file mode 100644 index 0000000..943afff --- /dev/null +++ b/src/lib/agents/search/researcher/actions/webSearch.ts @@ -0,0 +1,54 @@ +import z from 'zod'; +import { ResearchAction } from '../../types'; +import { searchSearxng } from '@/lib/searxng'; + +const actionSchema = z.object({ + type: z.literal('web_search'), + queries: z + .array(z.string()) + .describe('An array of search queries to perform web searches for.'), +}); + +const actionDescription = ` +You have to use this action aggressively to find relevant information from the web to answer user queries. You can combine this action with other actions to gather comprehensive data. Always ensure that you provide accurate and up-to-date information by leveraging web search results. +When this action is present, you must use it to obtain current information from the web. + +### How to use: +1. For fast search mode, you can use this action once. Make sure to cover all aspects of the user's query in that single search. +2. If you're on quality mode, you'll get to use this action up to two times. Use the first search to gather general information, and the second search to fill in any gaps or get more specific details based on the initial findings. +3. If you're set on Deep research mode, then you will get to use this action multiple times to gather more information. Use your judgment to decide when additional searches are necessary to provide a thorough and accurate response. + +Input: An array of search queries. Make sure the queries are relevant to the user's request and cover different aspects if necessary. You can include a maximum of 3 queries. Make sure the queries are SEO friendly and not sentences rather keywords which can be used to search a search engine like Google, Bing, etc. +`; + +const webSearchAction: ResearchAction = { + name: 'web_search', + description: actionDescription, + schema: actionSchema, + enabled: (config) => config.classification.intents.includes('web_search'), + execute: async (input, _) => { + let results: Chunk[] = []; + + const search = async (q: string) => { + const res = await searchSearxng(q); + res.results.forEach((r) => { + results.push({ + content: r.content || r.title, + metadata: { + title: r.title, + url: r.url, + }, + }); + }); + }; + + await Promise.all(input.queries.map(search)); + + return { + type: 'search_results', + results, + }; + }, +}; + +export default webSearchAction; diff --git a/src/lib/agents/search/researcher/index.ts b/src/lib/agents/search/researcher/index.ts new file mode 100644 index 0000000..300de72 --- /dev/null +++ b/src/lib/agents/search/researcher/index.ts @@ -0,0 +1,113 @@ +import z from 'zod'; +import { + ActionConfig, + ActionOutput, + ResearcherInput, + ResearcherOutput, +} from '../types'; +import { ActionRegistry } from './actions'; +import { getResearcherPrompt } from '@/lib/prompts/search/researcher'; +import SessionManager from '@/lib/session'; + +class Researcher { + async research( + session: SessionManager, + input: ResearcherInput, + ): Promise { + let findings: string = ''; + let actionOutput: ActionOutput[] = []; + let maxIteration = + input.config.mode === 'fast' + ? 1 + : input.config.mode === 'balanced' + ? 3 + : 25; + + const availableActions = ActionRegistry.getAvailableActions({ + classification: input.classification, + }); + + const schema = z.object({ + reasoning: z + .string() + .describe('The reasoning behind choosing the next action.'), + action: z + .union(availableActions.map((a) => a.schema)) + .describe('The action to be performed next.'), + }); + + const availableActionsDescription = + ActionRegistry.getAvailableActionsDescriptions({ + classification: input.classification, + }); + + for (let i = 0; i < maxIteration; i++) { + const researcherPrompt = getResearcherPrompt(availableActionsDescription); + + const res = await input.config.llm.generateObject>( + { + messages: [ + { + role: 'system', + content: researcherPrompt, + }, + { + role: 'user', + content: ` + + ${input.classification.standaloneFollowUp} + + + + ${findings} + + `, + }, + ], + schema, + }, + ); + + + if (res.action.type === 'done') { + console.log('Research complete - "done" action selected'); + break; + } + + const actionConfig: ActionConfig = { + type: res.action.type as string, + params: res.action, + }; + + findings += 'Reasoning: ' + res.reasoning + '\n'; + findings += `Executing Action: ${actionConfig.type} with params ${JSON.stringify(actionConfig.params)}\n`; + + const actionResult = await ActionRegistry.execute( + actionConfig.type, + actionConfig.params, + { + llm: input.config.llm, + embedding: input.config.embedding, + session: session, + }, + ); + + actionOutput.push(actionResult); + + if (actionResult.type === 'search_results') { + findings += actionResult.results + .map( + (r) => + `Title: ${r.metadata.title}\nURL: ${r.metadata.url}\nContent: ${r.content}\n`, + ) + .join('\n'); + } + } + + return { + findings: actionOutput, + }; + } +} + +export default Researcher; diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts index 72d7ecf..fc0735d 100644 --- a/src/lib/agents/search/types.ts +++ b/src/lib/agents/search/types.ts @@ -1,7 +1,7 @@ -import { EventEmitter } from 'stream'; import z from 'zod'; import BaseLLM from '../../models/base/llm'; import BaseEmbedding from '@/lib/models/base/embedding'; +import SessionManager from '@/lib/session'; export type SearchSources = 'web' | 'discussions' | 'academic'; @@ -9,10 +9,11 @@ export type SearchAgentConfig = { sources: SearchSources[]; llm: BaseLLM; embedding: BaseEmbedding; + mode: 'fast' | 'balanced' | 'deep_research'; }; export type SearchAgentInput = { - chatHistory: Message[]; + chatHistory: ChatTurnMessage[]; followUp: string; config: SearchAgentConfig; }; @@ -48,7 +49,7 @@ export type ClassifierInput = { llm: BaseLLM; enabledSources: SearchSources[]; query: string; - chatHistory: Message[]; + chatHistory: ChatTurnMessage[]; }; export type ClassifierOutput = { @@ -60,6 +61,46 @@ export type ClassifierOutput = { export type AdditionalConfig = { llm: BaseLLM; - embedding: BaseLLM; - emitter: EventEmitter; + embedding: BaseEmbedding; + session: SessionManager; +}; + +export type ResearcherInput = { + chatHistory: ChatTurnMessage[]; + followUp: string; + classification: ClassifierOutput; + config: SearchAgentConfig; +}; + +export type ResearcherOutput = { + findings: ActionOutput[]; +}; + +export type SearchActionOutput = { + type: 'search_results'; + results: Chunk[]; +}; + +export type DoneActionOutput = { + type: 'done'; +}; + +export type ActionOutput = SearchActionOutput | DoneActionOutput; + +export interface ResearchAction< + TSchema extends z.ZodObject = z.ZodObject, +> { + name: string; + description: string; + schema: z.ZodObject; + enabled: (config: { classification: ClassifierOutput }) => boolean; + execute: ( + params: z.infer, + additionalConfig: AdditionalConfig, + ) => Promise; +} + +export type ActionConfig = { + type: string; + params: Record; }; From c4acc83fd596caf04f11b3d3b20f03bfc444de18 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 21 Nov 2025 23:57:50 +0530 Subject: [PATCH 032/196] feat(agents): add search agent --- src/lib/agents/search/index.ts | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/lib/agents/search/index.ts diff --git a/src/lib/agents/search/index.ts b/src/lib/agents/search/index.ts new file mode 100644 index 0000000..141ea7f --- /dev/null +++ b/src/lib/agents/search/index.ts @@ -0,0 +1,48 @@ +import { ResearcherOutput, SearchAgentInput } from './types'; +import SessionManager from '@/lib/session'; +import Classifier from './classifier'; +import { WidgetRegistry } from './widgets'; +import Researcher from './researcher'; + +class SearchAgent { + async searchAsync(session: SessionManager, input: SearchAgentInput) { + const classifier = new Classifier(); + + const classification = await classifier.classify({ + chatHistory: input.chatHistory, + enabledSources: input.config.sources, + query: input.followUp, + llm: input.config.llm, + }); + + session.emit('data', { + type: 'classification', + classification: classification, + }); + + const widgetPromise = WidgetRegistry.executeAll(classification.widgets, { + llm: input.config.llm, + embedding: input.config.embedding, + session: session, + }); + + let searchPromise: Promise | null = null; + + if (!classification.skipSearch) { + const researcher = new Researcher(); + searchPromise = researcher.research(session, { + chatHistory: input.chatHistory, + followUp: input.followUp, + classification: classification, + config: input.config, + }); + } + + const [widgetOutputs, searchResults] = await Promise.all([ + widgetPromise, + searchPromise, + ]); + } +} + +export default SearchAgent; From 55cf88822d0c0184a46ce587d9a2f1c7659ba43f Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 21 Nov 2025 23:58:04 +0530 Subject: [PATCH 033/196] feat(package): add modules --- package.json | 4 ++++ yarn.lock | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/package.json b/package.json index b51dc6f..5752b9f 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,15 @@ "markdown-to-jsx": "^7.7.2", "next": "^15.2.2", "next-themes": "^0.3.0", + "ollama": "^0.6.3", + "openai": "^6.9.0", + "partial-json": "^0.1.7", "pdf-parse": "^1.1.1", "react": "^18", "react-dom": "^18", "react-text-to-speech": "^0.14.5", "react-textarea-autosize": "^8.5.3", + "rfc6902": "^5.1.2", "sonner": "^1.4.41", "tailwind-merge": "^2.2.2", "winston": "^3.17.0", diff --git a/yarn.lock b/yarn.lock index 8844d50..d202f75 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3992,6 +3992,13 @@ ollama@^0.5.12: dependencies: whatwg-fetch "^3.6.20" +ollama@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/ollama/-/ollama-0.6.3.tgz#b188573dd0ccb3b4759c1f8fa85067cb17f6673c" + integrity sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg== + dependencies: + whatwg-fetch "^3.6.20" + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -4126,6 +4133,11 @@ parseley@^0.12.0: leac "^0.6.0" peberminta "^0.9.0" +partial-json@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/partial-json/-/partial-json-0.1.7.tgz#b735a89edb3e25f231a3c4caeaae71dc9f578605" + integrity sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA== + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -4518,6 +4530,11 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfc6902@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/rfc6902/-/rfc6902-5.1.2.tgz#774262ba7b032ab9abf9eb8e0312927e8f425062" + integrity sha512-zxcb+PWlE8PwX0tiKE6zP97THQ8/lHmeiwucRrJ3YFupWEmp25RmFSlB1dNTqjkovwqG4iq+u1gzJMBS3um8mA== + rgbcolor@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d" From 9ac2da36079fe9147658ee8252a165a0149e35d0 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 22 Nov 2025 22:22:34 +0530 Subject: [PATCH 034/196] feat(app): remove old search agent --- src/lib/prompts/webSearch.ts | 137 -------- src/lib/prompts/writingAssistant.ts | 17 - src/lib/search/index.ts | 59 ---- src/lib/search/metaSearchAgent.ts | 514 ---------------------------- 4 files changed, 727 deletions(-) delete mode 100644 src/lib/prompts/webSearch.ts delete mode 100644 src/lib/prompts/writingAssistant.ts delete mode 100644 src/lib/search/index.ts delete mode 100644 src/lib/search/metaSearchAgent.ts diff --git a/src/lib/prompts/webSearch.ts b/src/lib/prompts/webSearch.ts deleted file mode 100644 index b99b542..0000000 --- a/src/lib/prompts/webSearch.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { BaseMessageLike } from '@langchain/core/messages'; - -export const webSearchRetrieverPrompt = ` -You are an AI question rephraser. You will be given a conversation and a follow-up question, you will have to rephrase the follow up question so it is a standalone question and can be used by another LLM to search the web for information to answer it. -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. - -**Note**: All user messages are individual entities and should be treated as such do not mix conversations. -`; - -export const webSearchRetrieverFewShots: BaseMessageLike[] = [ - [ - 'user', - ` - - -What is the capital of France -`, - ], - [ - 'assistant', - ` -Capital of france -`, - ], - [ - 'user', - ` - - -Hi, how are you? -`, - ], - [ - 'assistant', - ` -not_needed -`, - ], - [ - 'user', - ` - - -What is Docker? -`, - ], - [ - 'assistant', - ` -What is Docker -`, - ], - [ - 'user', - ` - - -Can you tell me what is X from https://example.com -`, - ], - [ - 'assistant', - ` -What is X? - - -https://example.com -`, - ], - [ - 'user', - ` - - -Summarize the content from https://example.com -`, - ], - [ - 'assistant', - ` -summarize - - -https://example.com -`, - ], -]; - -export const webSearchResponsePrompt = ` - You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses. - - Your task is to provide answers that are: - - **Informative and relevant**: Thoroughly address the user's query using the given context. - - **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically. - - **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights. - - **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included. - - **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable. - - ### Formatting Instructions - - **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate. - - **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience. - - **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability. - - **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience. - - **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title. - - **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate. - - ### Citation Requirements - - Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`. - - Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]." - - Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context. - - Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]." - - Always prioritize credibility and accuracy by linking all statements back to their respective context sources. - - Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation. - - ### Special Instructions - - If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity. - - 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. - - ### User instructions - These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines. - {systemInstructions} - - ### 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. - - Provide explanations or historical context as needed to enhance understanding. - - End with a conclusion or overall perspective if relevant. - - - {context} - - - Current date & time in ISO format (UTC timezone) is: {date}. -`; diff --git a/src/lib/prompts/writingAssistant.ts b/src/lib/prompts/writingAssistant.ts deleted file mode 100644 index 565827a..0000000 --- a/src/lib/prompts/writingAssistant.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const writingAssistantPrompt = ` -You are Perplexica, an AI model who is expert at searching the web and answering user's queries. You are currently set on focus mode 'Writing Assistant', this means you will be helping the user write a response to a given query. -Since you are a writing assistant, you would not perform web searches. If you think you lack information to answer the query, you can ask the user for more information or suggest them to switch to a different focus mode. -You will be shared a context that can contain information from files user has uploaded to get answers from. You will have to generate answers upon that. - -You have to cite the answer using [number] notation. You must cite the sentences with their relevent context number. You must cite each and every part of the answer so the user can know where the information is coming from. -Place these citations at the end of that particular sentence. You can cite the same sentence multiple times if it is relevant to the user's query like [number1][number2]. -However you do not need to cite it using the same number. You can use different numbers to cite the same sentence multiple times. The number refers to the number of the search result (passed in the context) used to generate that part of the answer. - -### User instructions -These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines. -{systemInstructions} - - -{context} - -`; diff --git a/src/lib/search/index.ts b/src/lib/search/index.ts deleted file mode 100644 index 8eb8ab0..0000000 --- a/src/lib/search/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import MetaSearchAgent from '@/lib/search/metaSearchAgent'; -import prompts from '../prompts'; - -export const searchHandlers: Record = { - webSearch: new MetaSearchAgent({ - activeEngines: [], - queryGeneratorPrompt: prompts.webSearchRetrieverPrompt, - responsePrompt: prompts.webSearchResponsePrompt, - queryGeneratorFewShots: prompts.webSearchRetrieverFewShots, - rerank: true, - rerankThreshold: 0.3, - searchWeb: true, - }), - academicSearch: new MetaSearchAgent({ - activeEngines: ['arxiv', 'google scholar', 'pubmed'], - queryGeneratorPrompt: prompts.webSearchRetrieverPrompt, - responsePrompt: prompts.webSearchResponsePrompt, - queryGeneratorFewShots: prompts.webSearchRetrieverFewShots, - rerank: true, - rerankThreshold: 0, - searchWeb: true, - }), - writingAssistant: new MetaSearchAgent({ - activeEngines: [], - queryGeneratorPrompt: '', - queryGeneratorFewShots: [], - responsePrompt: prompts.writingAssistantPrompt, - rerank: true, - rerankThreshold: 0, - searchWeb: false, - }), - wolframAlphaSearch: new MetaSearchAgent({ - activeEngines: ['wolframalpha'], - queryGeneratorPrompt: prompts.webSearchRetrieverPrompt, - responsePrompt: prompts.webSearchResponsePrompt, - queryGeneratorFewShots: prompts.webSearchRetrieverFewShots, - rerank: false, - rerankThreshold: 0, - searchWeb: true, - }), - youtubeSearch: new MetaSearchAgent({ - activeEngines: ['youtube'], - queryGeneratorPrompt: prompts.webSearchRetrieverPrompt, - responsePrompt: prompts.webSearchResponsePrompt, - queryGeneratorFewShots: prompts.webSearchRetrieverFewShots, - rerank: true, - rerankThreshold: 0.3, - searchWeb: true, - }), - redditSearch: new MetaSearchAgent({ - activeEngines: ['reddit'], - queryGeneratorPrompt: prompts.webSearchRetrieverPrompt, - responsePrompt: prompts.webSearchResponsePrompt, - queryGeneratorFewShots: prompts.webSearchRetrieverFewShots, - rerank: true, - rerankThreshold: 0.3, - searchWeb: true, - }), -}; diff --git a/src/lib/search/metaSearchAgent.ts b/src/lib/search/metaSearchAgent.ts deleted file mode 100644 index 1f72f79..0000000 --- a/src/lib/search/metaSearchAgent.ts +++ /dev/null @@ -1,514 +0,0 @@ -import { ChatOpenAI } from '@langchain/openai'; -import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import type { Embeddings } from '@langchain/core/embeddings'; -import { - ChatPromptTemplate, - MessagesPlaceholder, - PromptTemplate, -} from '@langchain/core/prompts'; -import { - RunnableLambda, - RunnableMap, - RunnableSequence, -} from '@langchain/core/runnables'; -import { BaseMessage, BaseMessageLike } from '@langchain/core/messages'; -import { StringOutputParser } from '@langchain/core/output_parsers'; -import LineListOutputParser from '../outputParsers/listLineOutputParser'; -import LineOutputParser from '../outputParsers/lineOutputParser'; -import { getDocumentsFromLinks } from '../utils/documents'; -import { Document } from '@langchain/core/documents'; -import { searchSearxng } from '../searxng'; -import path from 'node:path'; -import fs from 'node:fs'; -import computeSimilarity from '../utils/computeSimilarity'; -import formatChatHistoryAsString from '../utils/formatHistory'; -import eventEmitter from 'events'; -import { StreamEvent } from '@langchain/core/tracers/log_stream'; - -export interface MetaSearchAgentType { - searchAndAnswer: ( - message: string, - history: BaseMessage[], - llm: BaseChatModel, - embeddings: Embeddings, - optimizationMode: 'speed' | 'balanced' | 'quality', - fileIds: string[], - systemInstructions: string, - ) => Promise; -} - -interface Config { - searchWeb: boolean; - rerank: boolean; - rerankThreshold: number; - queryGeneratorPrompt: string; - queryGeneratorFewShots: BaseMessageLike[]; - responsePrompt: string; - activeEngines: string[]; -} - -type BasicChainInput = { - chat_history: BaseMessage[]; - query: string; -}; - -class MetaSearchAgent implements MetaSearchAgentType { - private config: Config; - private strParser = new StringOutputParser(); - - constructor(config: Config) { - this.config = config; - } - - private async createSearchRetrieverChain(llm: BaseChatModel) { - (llm as unknown as ChatOpenAI).temperature = 0; - - return RunnableSequence.from([ - ChatPromptTemplate.fromMessages([ - ['system', this.config.queryGeneratorPrompt], - ...this.config.queryGeneratorFewShots, - [ - 'user', - ` - - {chat_history} - - - - {query} - - `, - ], - ]), - llm, - this.strParser, - RunnableLambda.from(async (input: string) => { - const linksOutputParser = new LineListOutputParser({ - key: 'links', - }); - - const questionOutputParser = new LineOutputParser({ - key: 'question', - }); - - const links = await linksOutputParser.parse(input); - let question = (await questionOutputParser.parse(input)) ?? input; - - if (question === 'not_needed') { - return { query: '', docs: [] }; - } - - if (links.length > 0) { - if (question.length === 0) { - question = 'summarize'; - } - - let docs: Document[] = []; - - const linkDocs = await getDocumentsFromLinks({ links }); - - const docGroups: Document[] = []; - - linkDocs.map((doc) => { - const URLDocExists = docGroups.find( - (d) => - d.metadata.url === doc.metadata.url && - d.metadata.totalDocs < 10, - ); - - if (!URLDocExists) { - docGroups.push({ - ...doc, - metadata: { - ...doc.metadata, - totalDocs: 1, - }, - }); - } - - const docIndex = docGroups.findIndex( - (d) => - d.metadata.url === doc.metadata.url && - d.metadata.totalDocs < 10, - ); - - if (docIndex !== -1) { - docGroups[docIndex].pageContent = - docGroups[docIndex].pageContent + `\n\n` + doc.pageContent; - docGroups[docIndex].metadata.totalDocs += 1; - } - }); - - await Promise.all( - docGroups.map(async (doc) => { - const res = await llm.invoke(` - You are a web search summarizer, tasked with summarizing a piece of text retrieved from a web search. Your job is to summarize the - text into a detailed, 2-4 paragraph explanation that captures the main ideas and provides a comprehensive answer to the query. - If the query is \"summarize\", you should provide a detailed summary of the text. If the query is a specific question, you should answer it in the summary. - - - **Journalistic tone**: The summary should sound professional and journalistic, not too casual or vague. - - **Thorough and detailed**: Ensure that every key point from the text is captured and that the summary directly answers the query. - - **Not too lengthy, but detailed**: The summary should be informative but not excessively long. Focus on providing detailed information in a concise format. - - The text will be shared inside the \`text\` XML tag, and the query inside the \`query\` XML tag. - - - 1. \` - Docker is a set of platform-as-a-service products that use OS-level virtualization to deliver software in packages called containers. - It was first released in 2013 and is developed by Docker, Inc. Docker is designed to make it easier to create, deploy, and run applications - by using containers. - - - - What is Docker and how does it work? - - - Response: - Docker is a revolutionary platform-as-a-service product developed by Docker, Inc., that uses container technology to make application - deployment more efficient. It allows developers to package their software with all necessary dependencies, making it easier to run in - any environment. Released in 2013, Docker has transformed the way applications are built, deployed, and managed. - \` - 2. \` - The theory of relativity, or simply relativity, encompasses two interrelated theories of Albert Einstein: special relativity and general - relativity. However, the word "relativity" is sometimes used in reference to Galilean invariance. The term "theory of relativity" was based - on the expression "relative theory" used by Max Planck in 1906. The theory of relativity usually encompasses two interrelated theories by - Albert Einstein: special relativity and general relativity. Special relativity applies to all physical phenomena in the absence of gravity. - General relativity explains the law of gravitation and its relation to other forces of nature. It applies to the cosmological and astrophysical - realm, including astronomy. - - - - summarize - - - Response: - The theory of relativity, developed by Albert Einstein, encompasses two main theories: special relativity and general relativity. Special - relativity applies to all physical phenomena in the absence of gravity, while general relativity explains the law of gravitation and its - relation to other forces of nature. The theory of relativity is based on the concept of "relative theory," as introduced by Max Planck in - 1906. It is a fundamental theory in physics that has revolutionized our understanding of the universe. - \` - - - Everything below is the actual data you will be working with. Good luck! - - - ${question} - - - - ${doc.pageContent} - - - Make sure to answer the query in the summary. - `); - - const document = new Document({ - pageContent: res.content as string, - metadata: { - title: doc.metadata.title, - url: doc.metadata.url, - }, - }); - - docs.push(document); - }), - ); - - return { query: question, docs: docs }; - } else { - question = question.replace(/.*?<\/think>/g, ''); - - const res = await searchSearxng(question, { - language: 'en', - engines: this.config.activeEngines, - }); - - 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 }), - }, - }), - ); - - return { query: question, docs: documents }; - } - }), - ]); - } - - private async createAnsweringChain( - llm: BaseChatModel, - fileIds: string[], - embeddings: Embeddings, - optimizationMode: 'speed' | 'balanced' | 'quality', - systemInstructions: string, - ) { - return RunnableSequence.from([ - RunnableMap.from({ - systemInstructions: () => systemInstructions, - query: (input: BasicChainInput) => input.query, - chat_history: (input: BasicChainInput) => input.chat_history, - date: () => new Date().toISOString(), - context: RunnableLambda.from(async (input: BasicChainInput) => { - const processedHistory = formatChatHistoryAsString( - input.chat_history, - ); - - let docs: Document[] | null = null; - let query = input.query; - - if (this.config.searchWeb) { - const searchRetrieverChain = - await this.createSearchRetrieverChain(llm); - - const searchRetrieverResult = await searchRetrieverChain.invoke({ - chat_history: processedHistory, - query, - }); - - query = searchRetrieverResult.query; - docs = searchRetrieverResult.docs; - } - - const sortedDocs = await this.rerankDocs( - query, - docs ?? [], - fileIds, - embeddings, - optimizationMode, - ); - - return sortedDocs; - }) - .withConfig({ - runName: 'FinalSourceRetriever', - }) - .pipe(this.processDocs), - }), - ChatPromptTemplate.fromMessages([ - ['system', this.config.responsePrompt], - new MessagesPlaceholder('chat_history'), - ['user', '{query}'], - ]), - llm, - this.strParser, - ]).withConfig({ - runName: 'FinalResponseGenerator', - }); - } - - private async rerankDocs( - query: string, - docs: Document[], - fileIds: string[], - embeddings: Embeddings, - optimizationMode: 'speed' | 'balanced' | 'quality', - ) { - if (docs.length === 0 && fileIds.length === 0) { - return docs; - } - - const filesData = fileIds - .map((file) => { - const filePath = path.join(process.cwd(), 'uploads', file); - - const contentPath = filePath + '-extracted.json'; - const embeddingsPath = filePath + '-embeddings.json'; - - const content = JSON.parse(fs.readFileSync(contentPath, 'utf8')); - const embeddings = JSON.parse(fs.readFileSync(embeddingsPath, 'utf8')); - - const fileSimilaritySearchObject = content.contents.map( - (c: string, i: number) => { - return { - fileName: content.title, - content: c, - embeddings: embeddings.embeddings[i], - }; - }, - ); - - return fileSimilaritySearchObject; - }) - .flat(); - - if (query.toLocaleLowerCase() === 'summarize') { - return docs.slice(0, 15); - } - - const docsWithContent = docs.filter( - (doc) => doc.pageContent && doc.pageContent.length > 0, - ); - - if (optimizationMode === 'speed' || this.config.rerank === false) { - if (filesData.length > 0) { - const [queryEmbedding] = await Promise.all([ - embeddings.embedQuery(query), - ]); - - const fileDocs = filesData.map((fileData) => { - return new Document({ - pageContent: fileData.content, - metadata: { - title: fileData.fileName, - url: `File`, - }, - }); - }); - - const similarity = filesData.map((fileData, i) => { - const sim = computeSimilarity(queryEmbedding, fileData.embeddings); - - return { - index: i, - similarity: sim, - }; - }); - - let sortedDocs = similarity - .filter( - (sim) => sim.similarity > (this.config.rerankThreshold ?? 0.3), - ) - .sort((a, b) => b.similarity - a.similarity) - .slice(0, 15) - .map((sim) => fileDocs[sim.index]); - - sortedDocs = - docsWithContent.length > 0 ? sortedDocs.slice(0, 8) : sortedDocs; - - return [ - ...sortedDocs, - ...docsWithContent.slice(0, 15 - sortedDocs.length), - ]; - } else { - return docsWithContent.slice(0, 15); - } - } else if (optimizationMode === 'balanced') { - const [docEmbeddings, queryEmbedding] = await Promise.all([ - embeddings.embedDocuments( - docsWithContent.map((doc) => doc.pageContent), - ), - embeddings.embedQuery(query), - ]); - - docsWithContent.push( - ...filesData.map((fileData) => { - return new Document({ - pageContent: fileData.content, - metadata: { - title: fileData.fileName, - url: `File`, - }, - }); - }), - ); - - docEmbeddings.push(...filesData.map((fileData) => fileData.embeddings)); - - const similarity = docEmbeddings.map((docEmbedding, i) => { - const sim = computeSimilarity(queryEmbedding, docEmbedding); - - return { - index: i, - similarity: sim, - }; - }); - - const sortedDocs = similarity - .filter((sim) => sim.similarity > (this.config.rerankThreshold ?? 0.3)) - .sort((a, b) => b.similarity - a.similarity) - .slice(0, 15) - .map((sim) => docsWithContent[sim.index]); - - return sortedDocs; - } - - return []; - } - - private processDocs(docs: Document[]) { - return docs - .map( - (_, index) => - `${index + 1}. ${docs[index].metadata.title} ${docs[index].pageContent}`, - ) - .join('\n'); - } - - private async handleStream( - stream: AsyncGenerator, - emitter: eventEmitter, - ) { - for await (const event of stream) { - if ( - event.event === 'on_chain_end' && - event.name === 'FinalSourceRetriever' - ) { - emitter.emit( - 'data', - JSON.stringify({ type: 'sources', data: event.data.output }), - ); - } - if ( - event.event === 'on_chain_stream' && - event.name === 'FinalResponseGenerator' - ) { - emitter.emit( - 'data', - JSON.stringify({ type: 'response', data: event.data.chunk }), - ); - } - if ( - event.event === 'on_chain_end' && - event.name === 'FinalResponseGenerator' - ) { - emitter.emit('end'); - } - } - } - - async searchAndAnswer( - message: string, - history: BaseMessage[], - llm: BaseChatModel, - embeddings: Embeddings, - optimizationMode: 'speed' | 'balanced' | 'quality', - fileIds: string[], - systemInstructions: string, - ) { - const emitter = new eventEmitter(); - - const answeringChain = await this.createAnsweringChain( - llm, - fileIds, - embeddings, - optimizationMode, - systemInstructions, - ); - - const stream = answeringChain.streamEvents( - { - chat_history: history, - query: message, - }, - { - version: 'v1', - }, - ); - - this.handleStream(stream, emitter); - - return emitter; - } -} - -export default MetaSearchAgent; From 8d04f636d05d014c86fe40cd13881476d9e08f5b Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 22 Nov 2025 22:22:43 +0530 Subject: [PATCH 035/196] Delete index.ts --- src/lib/prompts/index.ts | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 src/lib/prompts/index.ts diff --git a/src/lib/prompts/index.ts b/src/lib/prompts/index.ts deleted file mode 100644 index fd1a85a..0000000 --- a/src/lib/prompts/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - webSearchResponsePrompt, - webSearchRetrieverFewShots, - webSearchRetrieverPrompt, -} from './webSearch'; -import { writingAssistantPrompt } from './writingAssistant'; - -export default { - webSearchResponsePrompt, - webSearchRetrieverPrompt, - webSearchRetrieverFewShots, - writingAssistantPrompt, -}; From d6c364fdcbd5e10cdef2da961976e7952ee83990 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 22 Nov 2025 22:23:10 +0530 Subject: [PATCH 036/196] feat(models): remove old providers --- src/lib/models/providers/aiml.ts | 152 ---------------------- src/lib/models/providers/anthropic.ts | 115 ----------------- src/lib/models/providers/deepseek.ts | 107 --------------- src/lib/models/providers/gemini.ts | 145 --------------------- src/lib/models/providers/groq.ts | 118 ----------------- src/lib/models/providers/lemonade.ts | 158 ----------------------- src/lib/models/providers/lmstudio.ts | 148 --------------------- src/lib/models/providers/transformers.ts | 87 ------------- 8 files changed, 1030 deletions(-) delete mode 100644 src/lib/models/providers/aiml.ts delete mode 100644 src/lib/models/providers/anthropic.ts delete mode 100644 src/lib/models/providers/deepseek.ts delete mode 100644 src/lib/models/providers/gemini.ts delete mode 100644 src/lib/models/providers/groq.ts delete mode 100644 src/lib/models/providers/lemonade.ts delete mode 100644 src/lib/models/providers/lmstudio.ts delete mode 100644 src/lib/models/providers/transformers.ts diff --git a/src/lib/models/providers/aiml.ts b/src/lib/models/providers/aiml.ts deleted file mode 100644 index 35ccf79..0000000 --- a/src/lib/models/providers/aiml.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import { Model, ModelList, ProviderMetadata } from '../types'; -import BaseModelProvider from './baseProvider'; -import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai'; -import { Embeddings } from '@langchain/core/embeddings'; -import { UIConfigField } from '@/lib/config/types'; -import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry'; - -interface AimlConfig { - apiKey: string; -} - -const providerConfigFields: UIConfigField[] = [ - { - type: 'password', - name: 'API Key', - key: 'apiKey', - description: 'Your AI/ML API key', - required: true, - placeholder: 'AI/ML API Key', - env: 'AIML_API_KEY', - scope: 'server', - }, -]; - -class AimlProvider extends BaseModelProvider { - constructor(id: string, name: string, config: AimlConfig) { - super(id, name, config); - } - - async getDefaultModels(): Promise { - try { - const res = await fetch('https://api.aimlapi.com/models', { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.config.apiKey}`, - }, - }); - - const data = await res.json(); - - const chatModels: Model[] = data.data - .filter((m: any) => m.type === 'chat-completion') - .map((m: any) => { - return { - name: m.id, - key: m.id, - }; - }); - - const embeddingModels: Model[] = data.data - .filter((m: any) => m.type === 'embedding') - .map((m: any) => { - return { - name: m.id, - key: m.id, - }; - }); - - return { - embedding: embeddingModels, - chat: chatModels, - }; - } catch (err) { - if (err instanceof TypeError) { - throw new Error( - 'Error connecting to AI/ML API. Please ensure your API key is correct and the service is available.', - ); - } - - throw err; - } - } - - async getModelList(): Promise { - const defaultModels = await this.getDefaultModels(); - const configProvider = getConfiguredModelProviderById(this.id)!; - - return { - embedding: [ - ...defaultModels.embedding, - ...configProvider.embeddingModels, - ], - chat: [...defaultModels.chat, ...configProvider.chatModels], - }; - } - - async loadChatModel(key: string): Promise { - const modelList = await this.getModelList(); - - const exists = modelList.chat.find((m) => m.key === key); - - if (!exists) { - throw new Error( - 'Error Loading AI/ML API Chat Model. Invalid Model Selected', - ); - } - - return new ChatOpenAI({ - apiKey: this.config.apiKey, - temperature: 0.7, - model: key, - configuration: { - baseURL: 'https://api.aimlapi.com', - }, - }); - } - - async loadEmbeddingModel(key: string): Promise { - const modelList = await this.getModelList(); - const exists = modelList.embedding.find((m) => m.key === key); - - if (!exists) { - throw new Error( - 'Error Loading AI/ML API Embedding Model. Invalid Model Selected.', - ); - } - - return new OpenAIEmbeddings({ - apiKey: this.config.apiKey, - model: key, - configuration: { - baseURL: 'https://api.aimlapi.com', - }, - }); - } - - static parseAndValidate(raw: any): AimlConfig { - if (!raw || typeof raw !== 'object') - throw new Error('Invalid config provided. Expected object'); - if (!raw.apiKey) - throw new Error('Invalid config provided. API key must be provided'); - - return { - apiKey: String(raw.apiKey), - }; - } - - static getProviderConfigFields(): UIConfigField[] { - return providerConfigFields; - } - - static getProviderMetadata(): ProviderMetadata { - return { - key: 'aiml', - name: 'AI/ML API', - }; - } -} - -export default AimlProvider; diff --git a/src/lib/models/providers/anthropic.ts b/src/lib/models/providers/anthropic.ts deleted file mode 100644 index e071159..0000000 --- a/src/lib/models/providers/anthropic.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import { Model, ModelList, ProviderMetadata } from '../types'; -import BaseModelProvider from './baseProvider'; -import { ChatAnthropic } from '@langchain/anthropic'; -import { Embeddings } from '@langchain/core/embeddings'; -import { UIConfigField } from '@/lib/config/types'; -import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry'; - -interface AnthropicConfig { - apiKey: string; -} - -const providerConfigFields: UIConfigField[] = [ - { - type: 'password', - name: 'API Key', - key: 'apiKey', - description: 'Your Anthropic API key', - required: true, - placeholder: 'Anthropic API Key', - env: 'ANTHROPIC_API_KEY', - scope: 'server', - }, -]; - -class AnthropicProvider extends BaseModelProvider { - constructor(id: string, name: string, config: AnthropicConfig) { - super(id, name, config); - } - - async getDefaultModels(): Promise { - const res = await fetch('https://api.anthropic.com/v1/models?limit=999', { - method: 'GET', - headers: { - 'x-api-key': this.config.apiKey, - 'anthropic-version': '2023-06-01', - 'Content-type': 'application/json', - }, - }); - - if (!res.ok) { - throw new Error(`Failed to fetch Anthropic models: ${res.statusText}`); - } - - const data = (await res.json()).data; - - const models: Model[] = data.map((m: any) => { - return { - key: m.id, - name: m.display_name, - }; - }); - - return { - embedding: [], - chat: models, - }; - } - - async getModelList(): Promise { - const defaultModels = await this.getDefaultModels(); - const configProvider = getConfiguredModelProviderById(this.id)!; - - return { - embedding: [], - chat: [...defaultModels.chat, ...configProvider.chatModels], - }; - } - - async loadChatModel(key: string): Promise { - const modelList = await this.getModelList(); - - const exists = modelList.chat.find((m) => m.key === key); - - if (!exists) { - throw new Error( - 'Error Loading Anthropic Chat Model. Invalid Model Selected', - ); - } - - return new ChatAnthropic({ - apiKey: this.config.apiKey, - temperature: 0.7, - model: key, - }); - } - - async loadEmbeddingModel(key: string): Promise { - throw new Error('Anthropic provider does not support embedding models.'); - } - - static parseAndValidate(raw: any): AnthropicConfig { - if (!raw || typeof raw !== 'object') - throw new Error('Invalid config provided. Expected object'); - if (!raw.apiKey) - throw new Error('Invalid config provided. API key must be provided'); - - return { - apiKey: String(raw.apiKey), - }; - } - - static getProviderConfigFields(): UIConfigField[] { - return providerConfigFields; - } - - static getProviderMetadata(): ProviderMetadata { - return { - key: 'anthropic', - name: 'Anthropic', - }; - } -} - -export default AnthropicProvider; diff --git a/src/lib/models/providers/deepseek.ts b/src/lib/models/providers/deepseek.ts deleted file mode 100644 index 9b29d83..0000000 --- a/src/lib/models/providers/deepseek.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import { Model, ModelList, ProviderMetadata } from '../types'; -import BaseModelProvider from './baseProvider'; -import { ChatOpenAI } from '@langchain/openai'; -import { Embeddings } from '@langchain/core/embeddings'; -import { UIConfigField } from '@/lib/config/types'; -import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry'; - -interface DeepSeekConfig { - apiKey: string; -} - -const defaultChatModels: Model[] = [ - { - name: 'Deepseek Chat / DeepSeek V3.2 Exp', - key: 'deepseek-chat', - }, - { - name: 'Deepseek Reasoner / DeepSeek V3.2 Exp', - key: 'deepseek-reasoner', - }, -]; - -const providerConfigFields: UIConfigField[] = [ - { - type: 'password', - name: 'API Key', - key: 'apiKey', - description: 'Your DeepSeek API key', - required: true, - placeholder: 'DeepSeek API Key', - env: 'DEEPSEEK_API_KEY', - scope: 'server', - }, -]; - -class DeepSeekProvider extends BaseModelProvider { - constructor(id: string, name: string, config: DeepSeekConfig) { - super(id, name, config); - } - - async getDefaultModels(): Promise { - return { - embedding: [], - chat: defaultChatModels, - }; - } - - async getModelList(): Promise { - const defaultModels = await this.getDefaultModels(); - const configProvider = getConfiguredModelProviderById(this.id)!; - - return { - embedding: [], - chat: [...defaultModels.chat, ...configProvider.chatModels], - }; - } - - async loadChatModel(key: string): Promise { - const modelList = await this.getModelList(); - - const exists = modelList.chat.find((m) => m.key === key); - - if (!exists) { - throw new Error( - 'Error Loading DeepSeek Chat Model. Invalid Model Selected', - ); - } - - return new ChatOpenAI({ - apiKey: this.config.apiKey, - temperature: 0.7, - model: key, - configuration: { - baseURL: 'https://api.deepseek.com', - }, - }); - } - - async loadEmbeddingModel(key: string): Promise { - throw new Error('DeepSeek provider does not support embedding models.'); - } - - static parseAndValidate(raw: any): DeepSeekConfig { - if (!raw || typeof raw !== 'object') - throw new Error('Invalid config provided. Expected object'); - if (!raw.apiKey) - throw new Error('Invalid config provided. API key must be provided'); - - return { - apiKey: String(raw.apiKey), - }; - } - - static getProviderConfigFields(): UIConfigField[] { - return providerConfigFields; - } - - static getProviderMetadata(): ProviderMetadata { - return { - key: 'deepseek', - name: 'Deepseek AI', - }; - } -} - -export default DeepSeekProvider; diff --git a/src/lib/models/providers/gemini.ts b/src/lib/models/providers/gemini.ts deleted file mode 100644 index 6cfd913..0000000 --- a/src/lib/models/providers/gemini.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import { Model, ModelList, ProviderMetadata } from '../types'; -import BaseModelProvider from './baseProvider'; -import { - ChatGoogleGenerativeAI, - GoogleGenerativeAIEmbeddings, -} from '@langchain/google-genai'; -import { Embeddings } from '@langchain/core/embeddings'; -import { UIConfigField } from '@/lib/config/types'; -import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry'; - -interface GeminiConfig { - apiKey: string; -} - -const providerConfigFields: UIConfigField[] = [ - { - type: 'password', - name: 'API Key', - key: 'apiKey', - description: 'Your Google Gemini API key', - required: true, - placeholder: 'Google Gemini API Key', - env: 'GEMINI_API_KEY', - scope: 'server', - }, -]; - -class GeminiProvider extends BaseModelProvider { - constructor(id: string, name: string, config: GeminiConfig) { - super(id, name, config); - } - - async getDefaultModels(): Promise { - const res = await fetch( - `https://generativelanguage.googleapis.com/v1beta/models?key=${this.config.apiKey}`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - - const data = await res.json(); - - let defaultEmbeddingModels: Model[] = []; - let defaultChatModels: Model[] = []; - - data.models.forEach((m: any) => { - if ( - m.supportedGenerationMethods.some( - (genMethod: string) => - genMethod === 'embedText' || genMethod === 'embedContent', - ) - ) { - defaultEmbeddingModels.push({ - key: m.name, - name: m.displayName, - }); - } else if (m.supportedGenerationMethods.includes('generateContent')) { - defaultChatModels.push({ - key: m.name, - name: m.displayName, - }); - } - }); - - return { - embedding: defaultEmbeddingModels, - chat: defaultChatModels, - }; - } - - async getModelList(): Promise { - const defaultModels = await this.getDefaultModels(); - const configProvider = getConfiguredModelProviderById(this.id)!; - - return { - embedding: [ - ...defaultModels.embedding, - ...configProvider.embeddingModels, - ], - chat: [...defaultModels.chat, ...configProvider.chatModels], - }; - } - - async loadChatModel(key: string): Promise { - const modelList = await this.getModelList(); - - const exists = modelList.chat.find((m) => m.key === key); - - if (!exists) { - throw new Error( - 'Error Loading Gemini Chat Model. Invalid Model Selected', - ); - } - - return new ChatGoogleGenerativeAI({ - apiKey: this.config.apiKey, - temperature: 0.7, - model: key, - }); - } - - async loadEmbeddingModel(key: string): Promise { - const modelList = await this.getModelList(); - const exists = modelList.embedding.find((m) => m.key === key); - - if (!exists) { - throw new Error( - 'Error Loading Gemini Embedding Model. Invalid Model Selected.', - ); - } - - return new GoogleGenerativeAIEmbeddings({ - apiKey: this.config.apiKey, - model: key, - }); - } - - static parseAndValidate(raw: any): GeminiConfig { - if (!raw || typeof raw !== 'object') - throw new Error('Invalid config provided. Expected object'); - if (!raw.apiKey) - throw new Error('Invalid config provided. API key must be provided'); - - return { - apiKey: String(raw.apiKey), - }; - } - - static getProviderConfigFields(): UIConfigField[] { - return providerConfigFields; - } - - static getProviderMetadata(): ProviderMetadata { - return { - key: 'gemini', - name: 'Google Gemini', - }; - } -} - -export default GeminiProvider; diff --git a/src/lib/models/providers/groq.ts b/src/lib/models/providers/groq.ts deleted file mode 100644 index a87ea88..0000000 --- a/src/lib/models/providers/groq.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import { Model, ModelList, ProviderMetadata } from '../types'; -import BaseModelProvider from './baseProvider'; -import { ChatGroq } from '@langchain/groq'; -import { Embeddings } from '@langchain/core/embeddings'; -import { UIConfigField } from '@/lib/config/types'; -import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry'; - -interface GroqConfig { - apiKey: string; -} - -const providerConfigFields: UIConfigField[] = [ - { - type: 'password', - name: 'API Key', - key: 'apiKey', - description: 'Your Groq API key', - required: true, - placeholder: 'Groq API Key', - env: 'GROQ_API_KEY', - scope: 'server', - }, -]; - -class GroqProvider extends BaseModelProvider { - constructor(id: string, name: string, config: GroqConfig) { - super(id, name, config); - } - - async getDefaultModels(): Promise { - try { - const res = await fetch('https://api.groq.com/openai/v1/models', { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.config.apiKey}`, - }, - }); - - const data = await res.json(); - - const models: Model[] = data.data.map((m: any) => { - return { - name: m.id, - key: m.id, - }; - }); - - return { - embedding: [], - chat: models, - }; - } catch (err) { - if (err instanceof TypeError) { - throw new Error( - 'Error connecting to Groq API. Please ensure your API key is correct and the Groq service is available.', - ); - } - - throw err; - } - } - - async getModelList(): Promise { - const defaultModels = await this.getDefaultModels(); - const configProvider = getConfiguredModelProviderById(this.id)!; - - return { - embedding: [], - chat: [...defaultModels.chat, ...configProvider.chatModels], - }; - } - - async loadChatModel(key: string): Promise { - const modelList = await this.getModelList(); - - const exists = modelList.chat.find((m) => m.key === key); - - if (!exists) { - throw new Error('Error Loading Groq Chat Model. Invalid Model Selected'); - } - - return new ChatGroq({ - apiKey: this.config.apiKey, - temperature: 0.7, - model: key, - }); - } - - async loadEmbeddingModel(key: string): Promise { - throw new Error('Groq provider does not support embedding models.'); - } - - static parseAndValidate(raw: any): GroqConfig { - if (!raw || typeof raw !== 'object') - throw new Error('Invalid config provided. Expected object'); - if (!raw.apiKey) - throw new Error('Invalid config provided. API key must be provided'); - - return { - apiKey: String(raw.apiKey), - }; - } - - static getProviderConfigFields(): UIConfigField[] { - return providerConfigFields; - } - - static getProviderMetadata(): ProviderMetadata { - return { - key: 'groq', - name: 'Groq', - }; - } -} - -export default GroqProvider; diff --git a/src/lib/models/providers/lemonade.ts b/src/lib/models/providers/lemonade.ts deleted file mode 100644 index 20680a8..0000000 --- a/src/lib/models/providers/lemonade.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import { Model, ModelList, ProviderMetadata } from '../types'; -import BaseModelProvider from './baseProvider'; -import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai'; -import { Embeddings } from '@langchain/core/embeddings'; -import { UIConfigField } from '@/lib/config/types'; -import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry'; - -interface LemonadeConfig { - baseURL: string; - apiKey?: string; -} - -const providerConfigFields: UIConfigField[] = [ - { - type: 'string', - name: 'Base URL', - key: 'baseURL', - description: 'The base URL for Lemonade API', - required: true, - placeholder: 'https://api.lemonade.ai/v1', - env: 'LEMONADE_BASE_URL', - scope: 'server', - }, - { - type: 'password', - name: 'API Key', - key: 'apiKey', - description: 'Your Lemonade API key (optional)', - required: false, - placeholder: 'Lemonade API Key', - env: 'LEMONADE_API_KEY', - scope: 'server', - }, -]; - -class LemonadeProvider extends BaseModelProvider { - constructor(id: string, name: string, config: LemonadeConfig) { - super(id, name, config); - } - - async getDefaultModels(): Promise { - try { - const headers: Record = { - 'Content-Type': 'application/json', - }; - - if (this.config.apiKey) { - headers['Authorization'] = `Bearer ${this.config.apiKey}`; - } - - const res = await fetch(`${this.config.baseURL}/models`, { - method: 'GET', - headers, - }); - - const data = await res.json(); - - const models: Model[] = data.data.map((m: any) => { - return { - name: m.id, - key: m.id, - }; - }); - - return { - embedding: models, - chat: models, - }; - } catch (err) { - if (err instanceof TypeError) { - throw new Error( - 'Error connecting to Lemonade API. Please ensure the base URL is correct and the service is available.', - ); - } - - throw err; - } - } - - async getModelList(): Promise { - const defaultModels = await this.getDefaultModels(); - const configProvider = getConfiguredModelProviderById(this.id)!; - - return { - embedding: [ - ...defaultModels.embedding, - ...configProvider.embeddingModels, - ], - chat: [...defaultModels.chat, ...configProvider.chatModels], - }; - } - - async loadChatModel(key: string): Promise { - const modelList = await this.getModelList(); - - const exists = modelList.chat.find((m) => m.key === key); - - if (!exists) { - throw new Error( - 'Error Loading Lemonade Chat Model. Invalid Model Selected', - ); - } - - return new ChatOpenAI({ - apiKey: this.config.apiKey || 'not-needed', - temperature: 0.7, - model: key, - configuration: { - baseURL: this.config.baseURL, - }, - }); - } - - async loadEmbeddingModel(key: string): Promise { - const modelList = await this.getModelList(); - const exists = modelList.embedding.find((m) => m.key === key); - - if (!exists) { - throw new Error( - 'Error Loading Lemonade Embedding Model. Invalid Model Selected.', - ); - } - - return new OpenAIEmbeddings({ - apiKey: this.config.apiKey || 'not-needed', - model: key, - configuration: { - baseURL: this.config.baseURL, - }, - }); - } - - static parseAndValidate(raw: any): LemonadeConfig { - if (!raw || typeof raw !== 'object') - throw new Error('Invalid config provided. Expected object'); - if (!raw.baseURL) - throw new Error('Invalid config provided. Base URL must be provided'); - - return { - baseURL: String(raw.baseURL), - apiKey: raw.apiKey ? String(raw.apiKey) : undefined, - }; - } - - static getProviderConfigFields(): UIConfigField[] { - return providerConfigFields; - } - - static getProviderMetadata(): ProviderMetadata { - return { - key: 'lemonade', - name: 'Lemonade', - }; - } -} - -export default LemonadeProvider; diff --git a/src/lib/models/providers/lmstudio.ts b/src/lib/models/providers/lmstudio.ts deleted file mode 100644 index 3a73a34..0000000 --- a/src/lib/models/providers/lmstudio.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import { Model, ModelList, ProviderMetadata } from '../types'; -import BaseModelProvider from './baseProvider'; -import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai'; -import { Embeddings } from '@langchain/core/embeddings'; -import { UIConfigField } from '@/lib/config/types'; -import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry'; - -interface LMStudioConfig { - baseURL: string; -} - -const providerConfigFields: UIConfigField[] = [ - { - type: 'string', - name: 'Base URL', - key: 'baseURL', - description: 'The base URL for LM Studio server', - required: true, - placeholder: 'http://localhost:1234', - env: 'LM_STUDIO_BASE_URL', - scope: 'server', - }, -]; - -class LMStudioProvider extends BaseModelProvider { - constructor(id: string, name: string, config: LMStudioConfig) { - super(id, name, config); - } - - private normalizeBaseURL(url: string): string { - const trimmed = url.trim().replace(/\/+$/, ''); - return trimmed.endsWith('/v1') ? trimmed : `${trimmed}/v1`; - } - - async getDefaultModels(): Promise { - try { - const baseURL = this.normalizeBaseURL(this.config.baseURL); - - const res = await fetch(`${baseURL}/models`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - const data = await res.json(); - - const models: Model[] = data.data.map((m: any) => { - return { - name: m.id, - key: m.id, - }; - }); - - return { - embedding: models, - chat: models, - }; - } catch (err) { - if (err instanceof TypeError) { - throw new Error( - 'Error connecting to LM Studio. Please ensure the base URL is correct and the LM Studio server is running.', - ); - } - - throw err; - } - } - - async getModelList(): Promise { - const defaultModels = await this.getDefaultModels(); - const configProvider = getConfiguredModelProviderById(this.id)!; - - return { - embedding: [ - ...defaultModels.embedding, - ...configProvider.embeddingModels, - ], - chat: [...defaultModels.chat, ...configProvider.chatModels], - }; - } - - async loadChatModel(key: string): Promise { - const modelList = await this.getModelList(); - - const exists = modelList.chat.find((m) => m.key === key); - - if (!exists) { - throw new Error( - 'Error Loading LM Studio Chat Model. Invalid Model Selected', - ); - } - - return new ChatOpenAI({ - apiKey: 'lm-studio', - temperature: 0.7, - model: key, - streaming: true, - configuration: { - baseURL: this.normalizeBaseURL(this.config.baseURL), - }, - }); - } - - async loadEmbeddingModel(key: string): Promise { - const modelList = await this.getModelList(); - const exists = modelList.embedding.find((m) => m.key === key); - - if (!exists) { - throw new Error( - 'Error Loading LM Studio Embedding Model. Invalid Model Selected.', - ); - } - - return new OpenAIEmbeddings({ - apiKey: 'lm-studio', - model: key, - configuration: { - baseURL: this.normalizeBaseURL(this.config.baseURL), - }, - }); - } - - static parseAndValidate(raw: any): LMStudioConfig { - if (!raw || typeof raw !== 'object') - throw new Error('Invalid config provided. Expected object'); - if (!raw.baseURL) - throw new Error('Invalid config provided. Base URL must be provided'); - - return { - baseURL: String(raw.baseURL), - }; - } - - static getProviderConfigFields(): UIConfigField[] { - return providerConfigFields; - } - - static getProviderMetadata(): ProviderMetadata { - return { - key: 'lmstudio', - name: 'LM Studio', - }; - } -} - -export default LMStudioProvider; diff --git a/src/lib/models/providers/transformers.ts b/src/lib/models/providers/transformers.ts deleted file mode 100644 index afd6b9e..0000000 --- a/src/lib/models/providers/transformers.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import { Model, ModelList, ProviderMetadata } from '../types'; -import BaseModelProvider from './baseProvider'; -import { Embeddings } from '@langchain/core/embeddings'; -import { UIConfigField } from '@/lib/config/types'; -import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry'; -import { HuggingFaceTransformersEmbeddings } from '@langchain/community/embeddings/huggingface_transformers'; -interface TransformersConfig {} - -const defaultEmbeddingModels: Model[] = [ - { - name: 'all-MiniLM-L6-v2', - key: 'Xenova/all-MiniLM-L6-v2', - }, - { - name: 'mxbai-embed-large-v1', - key: 'mixedbread-ai/mxbai-embed-large-v1', - }, - { - name: 'nomic-embed-text-v1', - key: 'Xenova/nomic-embed-text-v1', - }, -]; - -const providerConfigFields: UIConfigField[] = []; - -class TransformersProvider extends BaseModelProvider { - constructor(id: string, name: string, config: TransformersConfig) { - super(id, name, config); - } - - async getDefaultModels(): Promise { - return { - embedding: [...defaultEmbeddingModels], - chat: [], - }; - } - - async getModelList(): Promise { - const defaultModels = await this.getDefaultModels(); - const configProvider = getConfiguredModelProviderById(this.id)!; - - return { - embedding: [ - ...defaultModels.embedding, - ...configProvider.embeddingModels, - ], - chat: [], - }; - } - - async loadChatModel(key: string): Promise { - throw new Error('Transformers Provider does not support chat models.'); - } - - async loadEmbeddingModel(key: string): Promise { - const modelList = await this.getModelList(); - const exists = modelList.embedding.find((m) => m.key === key); - - if (!exists) { - throw new Error( - 'Error Loading OpenAI Embedding Model. Invalid Model Selected.', - ); - } - - return new HuggingFaceTransformersEmbeddings({ - model: key, - }); - } - - static parseAndValidate(raw: any): TransformersConfig { - return {}; - } - - static getProviderConfigFields(): UIConfigField[] { - return providerConfigFields; - } - - static getProviderMetadata(): ProviderMetadata { - return { - key: 'transformers', - name: 'Transformers', - }; - } -} - -export default TransformersProvider; From 6d35d60b49c3921c1c8155d71c045997ae6e617d Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:21:16 +0530 Subject: [PATCH 037/196] Remove unused output parsers and document utility --- src/lib/outputParsers/lineOutputParser.ts | 48 --------- src/lib/outputParsers/listLineOutputParser.ts | 50 ---------- src/lib/utils/documents.ts | 99 ------------------- 3 files changed, 197 deletions(-) delete mode 100644 src/lib/outputParsers/lineOutputParser.ts delete mode 100644 src/lib/outputParsers/listLineOutputParser.ts delete mode 100644 src/lib/utils/documents.ts diff --git a/src/lib/outputParsers/lineOutputParser.ts b/src/lib/outputParsers/lineOutputParser.ts deleted file mode 100644 index 5c795f2..0000000 --- a/src/lib/outputParsers/lineOutputParser.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { BaseOutputParser } from '@langchain/core/output_parsers'; - -interface LineOutputParserArgs { - key?: string; -} - -class LineOutputParser extends BaseOutputParser { - private key = 'questions'; - - constructor(args?: LineOutputParserArgs) { - super(); - this.key = args?.key ?? this.key; - } - - static lc_name() { - return 'LineOutputParser'; - } - - lc_namespace = ['langchain', 'output_parsers', 'line_output_parser']; - - async parse(text: string): Promise { - text = text.trim() || ''; - - const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/; - const startKeyIndex = text.indexOf(`<${this.key}>`); - const endKeyIndex = text.indexOf(``); - - if (startKeyIndex === -1 || endKeyIndex === -1) { - return undefined; - } - - const questionsStartIndex = - startKeyIndex === -1 ? 0 : startKeyIndex + `<${this.key}>`.length; - const questionsEndIndex = endKeyIndex === -1 ? text.length : endKeyIndex; - const line = text - .slice(questionsStartIndex, questionsEndIndex) - .trim() - .replace(regex, ''); - - return line; - } - - getFormatInstructions(): string { - throw new Error('Not implemented.'); - } -} - -export default LineOutputParser; diff --git a/src/lib/outputParsers/listLineOutputParser.ts b/src/lib/outputParsers/listLineOutputParser.ts deleted file mode 100644 index 6409db9..0000000 --- a/src/lib/outputParsers/listLineOutputParser.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { BaseOutputParser } from '@langchain/core/output_parsers'; - -interface LineListOutputParserArgs { - key?: string; -} - -class LineListOutputParser extends BaseOutputParser { - private key = 'questions'; - - constructor(args?: LineListOutputParserArgs) { - super(); - this.key = args?.key ?? this.key; - } - - static lc_name() { - return 'LineListOutputParser'; - } - - lc_namespace = ['langchain', 'output_parsers', 'line_list_output_parser']; - - async parse(text: string): Promise { - text = text.trim() || ''; - - const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/; - const startKeyIndex = text.indexOf(`<${this.key}>`); - const endKeyIndex = text.indexOf(``); - - if (startKeyIndex === -1 || endKeyIndex === -1) { - return []; - } - - const questionsStartIndex = - startKeyIndex === -1 ? 0 : startKeyIndex + `<${this.key}>`.length; - const questionsEndIndex = endKeyIndex === -1 ? text.length : endKeyIndex; - const lines = text - .slice(questionsStartIndex, questionsEndIndex) - .trim() - .split('\n') - .filter((line) => line.trim() !== '') - .map((line) => line.replace(regex, '')); - - return lines; - } - - getFormatInstructions(): string { - throw new Error('Not implemented.'); - } -} - -export default LineListOutputParser; diff --git a/src/lib/utils/documents.ts b/src/lib/utils/documents.ts deleted file mode 100644 index 51509ff..0000000 --- a/src/lib/utils/documents.ts +++ /dev/null @@ -1,99 +0,0 @@ -import axios from 'axios'; -import { htmlToText } from 'html-to-text'; -import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'; -import { Document } from '@langchain/core/documents'; -import pdfParse from 'pdf-parse'; - -export const getDocumentsFromLinks = async ({ links }: { links: string[] }) => { - const splitter = new RecursiveCharacterTextSplitter(); - - let docs: Document[] = []; - - await Promise.all( - links.map(async (link) => { - link = - link.startsWith('http://') || link.startsWith('https://') - ? link - : `https://${link}`; - - try { - const res = await axios.get(link, { - responseType: 'arraybuffer', - }); - - const isPdf = res.headers['content-type'] === 'application/pdf'; - - if (isPdf) { - const pdfText = await pdfParse(res.data); - const parsedText = pdfText.text - .replace(/(\r\n|\n|\r)/gm, ' ') - .replace(/\s+/g, ' ') - .trim(); - - const splittedText = await splitter.splitText(parsedText); - const title = 'PDF Document'; - - const linkDocs = splittedText.map((text) => { - return new Document({ - pageContent: text, - metadata: { - title: title, - url: link, - }, - }); - }); - - docs.push(...linkDocs); - return; - } - - const parsedText = htmlToText(res.data.toString('utf8'), { - selectors: [ - { - selector: 'a', - options: { - ignoreHref: true, - }, - }, - ], - }) - .replace(/(\r\n|\n|\r)/gm, ' ') - .replace(/\s+/g, ' ') - .trim(); - - const splittedText = await splitter.splitText(parsedText); - const title = res.data - .toString('utf8') - .match(/(.*?)<\/title>/)?.[1]; - - const linkDocs = splittedText.map((text) => { - return new Document({ - pageContent: text, - metadata: { - title: title || link, - url: link, - }, - }); - }); - - docs.push(...linkDocs); - } catch (err) { - console.error( - 'An error occurred while getting documents from links: ', - err, - ); - docs.push( - new Document({ - pageContent: `Failed to retrieve content from the link: ${err}`, - metadata: { - title: 'Failed to retrieve content', - url: link, - }, - }), - ); - } - }), - ); - - return docs; -}; From d7dd17c0695ab182fbf4117162e2c650fe40d49a Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:22:11 +0530 Subject: [PATCH 038/196] feat(app): fix type resolving issues --- src/lib/agents/search/researcher/actions/webSearch.ts | 1 + src/lib/agents/search/types.ts | 1 + src/lib/db/schema.ts | 1 + src/lib/models/base/embedding.ts | 2 ++ src/lib/models/providers/ollama/ollamaEmbedding.ts | 1 + src/lib/models/providers/openai/openaiEmbedding.ts | 1 + src/lib/models/registry.ts | 4 +--- src/lib/models/types.ts | 5 +++-- 8 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/lib/agents/search/researcher/actions/webSearch.ts b/src/lib/agents/search/researcher/actions/webSearch.ts index 943afff..5ceb2ed 100644 --- a/src/lib/agents/search/researcher/actions/webSearch.ts +++ b/src/lib/agents/search/researcher/actions/webSearch.ts @@ -1,6 +1,7 @@ import z from 'zod'; import { ResearchAction } from '../../types'; import { searchSearxng } from '@/lib/searxng'; +import { Chunk } from '@/lib/types'; const actionSchema = z.object({ type: z.literal('web_search'), diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts index fc0735d..0914503 100644 --- a/src/lib/agents/search/types.ts +++ b/src/lib/agents/search/types.ts @@ -2,6 +2,7 @@ import z from 'zod'; import BaseLLM from '../../models/base/llm'; import BaseEmbedding from '@/lib/models/base/embedding'; import SessionManager from '@/lib/session'; +import { ChatTurnMessage, Chunk } from '@/lib/types'; export type SearchSources = 'web' | 'discussions' | 'academic'; diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 71d441f..50dd14c 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -1,5 +1,6 @@ import { sql } from 'drizzle-orm'; import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core'; +import { Block } from '../types'; export const messages = sqliteTable('messages', { id: integer('id').primaryKey(), diff --git a/src/lib/models/base/embedding.ts b/src/lib/models/base/embedding.ts index 35413ed..a817605 100644 --- a/src/lib/models/base/embedding.ts +++ b/src/lib/models/base/embedding.ts @@ -1,3 +1,5 @@ +import { Chunk } from '@/lib/types'; + abstract class BaseEmbedding { constructor(protected config: CONFIG) {} abstract embedText(texts: string[]): Promise; diff --git a/src/lib/models/providers/ollama/ollamaEmbedding.ts b/src/lib/models/providers/ollama/ollamaEmbedding.ts index 0fd306a..7bb00b8 100644 --- a/src/lib/models/providers/ollama/ollamaEmbedding.ts +++ b/src/lib/models/providers/ollama/ollamaEmbedding.ts @@ -1,5 +1,6 @@ import { Ollama } from 'ollama'; import BaseEmbedding from '../../base/embedding'; +import { Chunk } from '@/lib/types'; type OllamaConfig = { model: string; diff --git a/src/lib/models/providers/openai/openaiEmbedding.ts b/src/lib/models/providers/openai/openaiEmbedding.ts index ea15680..4e137ad 100644 --- a/src/lib/models/providers/openai/openaiEmbedding.ts +++ b/src/lib/models/providers/openai/openaiEmbedding.ts @@ -1,5 +1,6 @@ import OpenAI from 'openai'; import BaseEmbedding from '../../base/embedding'; +import { Chunk } from '@/lib/types'; type OpenAIConfig = { apiKey: string; diff --git a/src/lib/models/registry.ts b/src/lib/models/registry.ts index 5067b6d..687c84c 100644 --- a/src/lib/models/registry.ts +++ b/src/lib/models/registry.ts @@ -1,7 +1,5 @@ import { ConfigModelProvider } from '../config/types'; -import BaseModelProvider, { - createProviderInstance, -} from './providers/baseProvider'; +import BaseModelProvider, { createProviderInstance } from './base/provider'; import { getConfiguredModelProviders } from '../config/serverRegistry'; import { providers } from './providers'; import { MinimalProvider, ModelList } from './types'; diff --git a/src/lib/models/types.ts b/src/lib/models/types.ts index ce77d3b..45560d1 100644 --- a/src/lib/models/types.ts +++ b/src/lib/models/types.ts @@ -1,4 +1,5 @@ import z from 'zod'; +import { ChatTurnMessage } from '../types'; type Model = { name: string; @@ -37,7 +38,7 @@ type GenerateOptions = { }; type GenerateTextInput = { - messages: Message[]; + messages: ChatTurnMessage[]; options?: GenerateOptions; }; @@ -54,7 +55,7 @@ type StreamTextOutput = { type GenerateObjectInput = { schema: z.ZodTypeAny; - messages: Message[]; + messages: ChatTurnMessage[]; options?: GenerateOptions; }; From 74bc08d18960842d123db19189d32b738e8dae7a Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:22:27 +0530 Subject: [PATCH 039/196] Refactor types and imports for consistency --- src/lib/session.ts | 1 + src/lib/types.ts | 27 +++++++++++++++------------ src/lib/utils/formatHistory.ts | 4 +++- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/lib/session.ts b/src/lib/session.ts index 834cbfd..e5a9bc1 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -1,5 +1,6 @@ import { EventEmitter } from 'stream'; import { applyPatch } from 'rfc6902'; +import { Block } from './types'; class SessionManager { private static sessions = new Map(); diff --git a/src/lib/types.ts b/src/lib/types.ts index 824aea0..9793077 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,32 +1,32 @@ -type ChatTurnMessage = { +export type ChatTurnMessage = { role: 'user' | 'assistant' | 'system'; content: string; }; -type Chunk = { +export type Chunk = { content: string; metadata: Record; }; -type TextBlock = { +export type TextBlock = { id: string; type: 'text'; data: string; }; -type SourceBlock = { +export type SourceBlock = { id: string; type: 'source'; data: Chunk[]; }; -type SuggestionBlock = { +export type SuggestionBlock = { id: string; type: 'suggestion'; data: string[]; }; -type WidgetBlock = { +export type WidgetBlock = { id: string; type: 'widget'; data: { @@ -35,27 +35,30 @@ type WidgetBlock = { }; }; -type ReasoningResearchBlock = { +export type ReasoningResearchBlock = { id: string; + type: 'reasoning'; reasoning: string; }; -type SearchingResearchBlock = { +export type SearchingResearchBlock = { id: string; + type: 'searching'; searching: string[]; }; -type ReadingResearchBlock = { +export type ReadingResearchBlock = { id: string; + type: 'reading'; reading: Chunk[]; }; -type ResearchBlockSubStep = +export type ResearchBlockSubStep = | ReasoningResearchBlock | SearchingResearchBlock | ReadingResearchBlock; -type ResearchBlock = { +export type ResearchBlock = { id: string; type: 'research'; data: { @@ -63,7 +66,7 @@ type ResearchBlock = { }; }; -type Block = +export type Block = | TextBlock | SourceBlock | SuggestionBlock diff --git a/src/lib/utils/formatHistory.ts b/src/lib/utils/formatHistory.ts index 8256ffa..7c5e4a0 100644 --- a/src/lib/utils/formatHistory.ts +++ b/src/lib/utils/formatHistory.ts @@ -1,4 +1,6 @@ -const formatChatHistoryAsString = (history: Message[]) => { +import { ChatTurnMessage } from '../types'; + +const formatChatHistoryAsString = (history: ChatTurnMessage[]) => { return history .map( (message) => From 0ac8569a9ef763e4db07ec350fe000a4dd220c2e Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:23:18 +0530 Subject: [PATCH 040/196] feat(agents): update suggestion generator --- src/app/api/suggestions/route.ts | 12 +------- src/lib/agents/suggestions/index.ts | 41 ++++++++++++++++------------ src/lib/prompts/suggestions/index.ts | 18 ++++++------ 3 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/app/api/suggestions/route.ts b/src/app/api/suggestions/route.ts index 2dc7248..0c70cba 100644 --- a/src/app/api/suggestions/route.ts +++ b/src/app/api/suggestions/route.ts @@ -19,19 +19,9 @@ export const POST = async (req: Request) => { body.chatModel.key, ); - const chatHistory = body.chatHistory - .map((msg: any) => { - if (msg.role === 'user') { - return new HumanMessage(msg.content); - } else if (msg.role === 'assistant') { - return new AIMessage(msg.content); - } - }) - .filter((msg) => msg !== undefined) as BaseMessage[]; - const suggestions = await generateSuggestions( { - chatHistory, + chatHistory: body.chatHistory, }, llm, ); diff --git a/src/lib/agents/suggestions/index.ts b/src/lib/agents/suggestions/index.ts index 03302ac..050eac7 100644 --- a/src/lib/agents/suggestions/index.ts +++ b/src/lib/agents/suggestions/index.ts @@ -1,32 +1,39 @@ -import ListLineOutputParser from '@/lib/outputParsers/listLineOutputParser'; -import { ChatPromptTemplate, PromptTemplate } from '@langchain/core/prompts'; import formatChatHistoryAsString from '@/lib/utils/formatHistory'; -import { BaseMessage, HumanMessage, SystemMessage } from '@langchain/core/messages'; -import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { suggestionGeneratorPrompt } from '@/lib/prompts/suggestions'; +import { ChatTurnMessage } from '@/lib/types'; +import z from 'zod'; +import BaseLLM from '@/lib/models/base/llm'; +import { i } from 'mathjs'; type SuggestionGeneratorInput = { - chatHistory: BaseMessage[]; + chatHistory: ChatTurnMessage[]; }; -const outputParser = new ListLineOutputParser({ - key: 'suggestions', +const schema = z.object({ + suggestions: z + .array(z.string()) + .describe('List of suggested questions or prompts'), }); const generateSuggestions = async ( input: SuggestionGeneratorInput, - llm: BaseChatModel, + llm: BaseLLM, ) => { - const chatPrompt = await ChatPromptTemplate.fromMessages([ - new SystemMessage(suggestionGeneratorPrompt), - new HumanMessage(`${formatChatHistoryAsString(input.chatHistory)}`) - ]).formatMessages({}) + const res = await llm.generateObject>({ + messages: [ + { + role: 'system', + content: suggestionGeneratorPrompt, + }, + { + role: 'user', + content: `\n${formatChatHistoryAsString(input.chatHistory)}\n`, + }, + ], + schema, + }); - const res = await llm.invoke(chatPrompt) - - const suggestions = await outputParser.invoke(res) - - return suggestions + return res.suggestions; }; export default generateSuggestions; diff --git a/src/lib/prompts/suggestions/index.ts b/src/lib/prompts/suggestions/index.ts index daa99d4..18922ba 100644 --- a/src/lib/prompts/suggestions/index.ts +++ b/src/lib/prompts/suggestions/index.ts @@ -3,13 +3,15 @@ You are an AI suggestion generator for an AI powered search engine. You will be 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. -Provide these suggestions separated by newlines between the XML tags and . For example: - - -Tell me more about SpaceX and their recent projects -What is the latest news on SpaceX? -Who is the CEO of SpaceX? - +Sample suggestions for a conversation about Elon Musk: +{ + "suggestions": [ + "What are Elon Musk's plans for SpaceX in the next decade?", + "How has Tesla's stock performance been influenced by Elon Musk's leadership?", + "What are the key innovations introduced by Elon Musk in the electric vehicle industry?", + "How does Elon Musk's vision for renewable energy impact global sustainability efforts?" + ] +} Today's date is ${new Date().toISOString()} -`; \ No newline at end of file +`; From 6da6acbcd0261215699933747a6f854f8670974e Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:23:42 +0530 Subject: [PATCH 041/196] feat(agents): update media agents --- src/lib/agents/media/image.ts | 59 +++++++++++++++++---------------- src/lib/agents/media/video.ts | 57 +++++++++++++++---------------- src/lib/prompts/media/image.ts | 41 ++++++++++++----------- src/lib/prompts/media/videos.ts | 41 ++++++++++++----------- 4 files changed, 103 insertions(+), 95 deletions(-) diff --git a/src/lib/agents/media/image.ts b/src/lib/agents/media/image.ts index 648b5ce..f146824 100644 --- a/src/lib/agents/media/image.ts +++ b/src/lib/agents/media/image.ts @@ -1,21 +1,17 @@ /* I don't think can be classified as agents but to keep the structure consistent i guess ill keep it here */ -import { - RunnableSequence, - RunnableMap, - RunnableLambda, -} from '@langchain/core/runnables'; -import { ChatPromptTemplate } from '@langchain/core/prompts'; -import formatChatHistoryAsString from '@/lib/utils/formatHistory'; -import { BaseMessage, HumanMessage, SystemMessage } from '@langchain/core/messages'; -import { StringOutputParser } from '@langchain/core/output_parsers'; import { searchSearxng } from '@/lib/searxng'; -import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import LineOutputParser from '@/lib/outputParsers/lineOutputParser'; -import { imageSearchFewShots, imageSearchPrompt } from '@/lib/prompts/media/image'; +import { + imageSearchFewShots, + imageSearchPrompt, +} from '@/lib/prompts/media/image'; +import BaseLLM from '@/lib/models/base/llm'; +import z from 'zod'; +import { ChatTurnMessage } from '@/lib/types'; +import formatChatHistoryAsString from '@/lib/utils/formatHistory'; type ImageSearchChainInput = { - chatHistory: BaseMessage[]; + chatHistory: ChatTurnMessage[]; query: string; }; @@ -23,27 +19,32 @@ type ImageSearchResult = { img_src: string; url: string; title: string; -} - -const outputParser = new LineOutputParser({ - key: 'query', -}) +}; const searchImages = async ( input: ImageSearchChainInput, - llm: BaseChatModel, + llm: BaseLLM, ) => { - const chatPrompt = await ChatPromptTemplate.fromMessages([ - new SystemMessage(imageSearchPrompt), - ...imageSearchFewShots, - new HumanMessage(`\n${formatChatHistoryAsString(input.chatHistory)}\n\n\n${input.query}\n`) - ]).formatMessages({}) + const schema = z.object({ + query: z.string().describe('The image search query.'), + }); - const res = await llm.invoke(chatPrompt) + const res = await llm.generateObject>({ + messages: [ + { + role: 'system', + content: imageSearchPrompt, + }, + ...imageSearchFewShots, + { + role: 'user', + content: `\n${formatChatHistoryAsString(input.chatHistory)}\n\n\n${input.query}\n`, + }, + ], + schema: schema, + }); - const query = await outputParser.invoke(res) - - const searchRes = await searchSearxng(query!, { + const searchRes = await searchSearxng(res.query, { engines: ['bing images', 'google images'], }); @@ -62,4 +63,4 @@ const searchImages = async ( return images.slice(0, 10); }; -export default searchImages; \ No newline at end of file +export default searchImages; diff --git a/src/lib/agents/media/video.ts b/src/lib/agents/media/video.ts index 60fc04f..feac720 100644 --- a/src/lib/agents/media/video.ts +++ b/src/lib/agents/media/video.ts @@ -1,13 +1,15 @@ -import { ChatPromptTemplate } from '@langchain/core/prompts'; import formatChatHistoryAsString from '@/lib/utils/formatHistory'; -import { BaseMessage, HumanMessage, SystemMessage } from '@langchain/core/messages'; import { searchSearxng } from '@/lib/searxng'; -import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import LineOutputParser from '@/lib/outputParsers/lineOutputParser'; -import { videoSearchFewShots, videoSearchPrompt } from '@/lib/prompts/media/videos'; +import { + videoSearchFewShots, + videoSearchPrompt, +} from '@/lib/prompts/media/videos'; +import { ChatTurnMessage } from '@/lib/types'; +import BaseLLM from '@/lib/models/base/llm'; +import z from 'zod'; type VideoSearchChainInput = { - chatHistory: BaseMessage[]; + chatHistory: ChatTurnMessage[]; query: string; }; @@ -16,39 +18,39 @@ type VideoSearchResult = { url: string; title: string; iframe_src: string; -} - -const outputParser = new LineOutputParser({ - key: 'query', -}); +}; const searchVideos = async ( input: VideoSearchChainInput, - llm: BaseChatModel, + llm: BaseLLM, ) => { - const chatPrompt = await ChatPromptTemplate.fromMessages([ - new SystemMessage(videoSearchPrompt), - ...videoSearchFewShots, - new HumanMessage(`${formatChatHistoryAsString(input.chatHistory)}\n\n\n${input.query}\n`) - ]).formatMessages({}) + const schema = z.object({ + query: z.string().describe('The video search query.'), + }); - const res = await llm.invoke(chatPrompt) + const res = await llm.generateObject>({ + messages: [ + { + role: 'system', + content: videoSearchPrompt, + }, + ...videoSearchFewShots, + { + role: 'user', + content: `\n${formatChatHistoryAsString(input.chatHistory)}\n\n\n${input.query}\n`, + }, + ], + schema: schema, + }); - const query = await outputParser.invoke(res) - - const searchRes = await searchSearxng(query!, { + const searchRes = await searchSearxng(res.query, { engines: ['youtube'], }); const videos: VideoSearchResult[] = []; searchRes.results.forEach((result) => { - if ( - result.thumbnail && - result.url && - result.title && - result.iframe_src - ) { + if (result.thumbnail && result.url && result.title && result.iframe_src) { videos.push({ img_src: result.thumbnail, url: result.url, @@ -59,7 +61,6 @@ const searchVideos = async ( }); return videos.slice(0, 10); - }; export default searchVideos; diff --git a/src/lib/prompts/media/image.ts b/src/lib/prompts/media/image.ts index 5f707c1..d4584cb 100644 --- a/src/lib/prompts/media/image.ts +++ b/src/lib/prompts/media/image.ts @@ -1,26 +1,29 @@ -import { BaseMessageLike } from "@langchain/core/messages"; +import { ChatTurnMessage } from '@/lib/types'; export const imageSearchPrompt = ` You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search the web for images. You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation. -Output only the rephrased query wrapped in an XML element. Do not include any explanation or additional text. +Output only the rephrased query in query key JSON format. Do not include any explanation or additional text. `; -export const imageSearchFewShots: BaseMessageLike[] = [ - [ - 'user', - '\n\n\nWhat is a cat?\n', - ], - ['assistant', 'A cat'], +export const imageSearchFewShots: ChatTurnMessage[] = [ + { + role: 'user', + content: + '\n\n\nWhat is a cat?\n', + }, + { role: 'assistant', content: '{"query":"A cat"}' }, - [ - 'user', - '\n\n\nWhat is a car? How does it work?\n', - ], - ['assistant', 'Car working'], - [ - 'user', - '\n\n\nHow does an AC work?\n', - ], - ['assistant', 'AC working'] -] \ No newline at end of file + { + role: 'user', + content: + '\n\n\nWhat is a car? How does it work?\n', + }, + { role: 'assistant', content: '{"query":"Car working"}' }, + { + role: 'user', + content: + '\n\n\nHow does an AC work?\n', + }, + { role: 'assistant', content: '{"query":"AC working"}' }, +]; diff --git a/src/lib/prompts/media/videos.ts b/src/lib/prompts/media/videos.ts index b4a0d55..adaa7b5 100644 --- a/src/lib/prompts/media/videos.ts +++ b/src/lib/prompts/media/videos.ts @@ -1,25 +1,28 @@ -import { BaseMessageLike } from "@langchain/core/messages"; +import { ChatTurnMessage } from '@/lib/types'; export const videoSearchPrompt = ` You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search Youtube for videos. You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation. -Output only the rephrased query wrapped in an XML element. Do not include any explanation or additional text. +Output only the rephrased query in query key JSON format. Do not include any explanation or additional text. `; -export const videoSearchFewShots: BaseMessageLike[] = [ - [ - 'user', - '\n\n\nHow does a car work?\n', - ], - ['assistant', 'How does a car work?'], - [ - 'user', - '\n\n\nWhat is the theory of relativity?\n', - ], - ['assistant', 'Theory of relativity'], - [ - 'user', - '\n\n\nHow does an AC work?\n', - ], - ['assistant', 'AC working'], -] \ No newline at end of file +export const videoSearchFewShots: ChatTurnMessage[] = [ + { + role: 'user', + content: + '\n\n\nHow does a car work?\n', + }, + { role: 'assistant', content: '{"query":"How does a car work?"}' }, + { + role: 'user', + content: + '\n\n\nWhat is the theory of relativity?\n', + }, + { role: 'assistant', content: '{"query":"Theory of relativity"}' }, + { + role: 'user', + content: + '\n\n\nHow does an AC work?\n', + }, + { role: 'assistant', content: '{"query":"AC working"}' }, +]; From e22a39fd73c7534a6a459e2540c723a79aab59e1 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:24:17 +0530 Subject: [PATCH 042/196] feat(routes): update routes to handle new llm types --- src/app/api/images/route.ts | 13 +-------- src/app/api/search/route.ts | 53 ++++++++++++++++++------------------ src/app/api/uploads/route.ts | 13 +++++++-- src/app/api/videos/route.ts | 13 +-------- 4 files changed, 39 insertions(+), 53 deletions(-) diff --git a/src/app/api/images/route.ts b/src/app/api/images/route.ts index bc62a1d..9cfabb2 100644 --- a/src/app/api/images/route.ts +++ b/src/app/api/images/route.ts @@ -1,7 +1,6 @@ import searchImages from '@/lib/agents/media/image'; import ModelRegistry from '@/lib/models/registry'; import { ModelWithProvider } from '@/lib/models/types'; -import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; interface ImageSearchBody { query: string; @@ -20,19 +19,9 @@ export const POST = async (req: Request) => { body.chatModel.key, ); - const chatHistory = body.chatHistory - .map((msg: any) => { - if (msg.role === 'user') { - return new HumanMessage(msg.content); - } else if (msg.role === 'assistant') { - return new AIMessage(msg.content); - } - }) - .filter((msg) => msg !== undefined) as BaseMessage[]; - const images = await searchImages( { - chatHistory: chatHistory, + chatHistory: body.chatHistory, query: body.query, }, llm, diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts index f737a55..8f357cb 100644 --- a/src/app/api/search/route.ts +++ b/src/app/api/search/route.ts @@ -1,8 +1,8 @@ -import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; -import { MetaSearchAgentType } from '@/lib/search/metaSearchAgent'; -import { searchHandlers } from '@/lib/search'; import ModelRegistry from '@/lib/models/registry'; import { ModelWithProvider } from '@/lib/models/types'; +import SessionManager from '@/lib/session'; +import SearchAgent from '@/lib/agents/search'; +import { ChatTurnMessage } from '@/lib/types'; interface ChatRequestBody { optimizationMode: 'speed' | 'balanced'; @@ -40,27 +40,26 @@ export const POST = async (req: Request) => { ), ]); - const history: BaseMessage[] = body.history.map((msg) => { + const history: ChatTurnMessage[] = body.history.map((msg) => { return msg[0] === 'human' - ? new HumanMessage({ content: msg[1] }) - : new AIMessage({ content: msg[1] }); + ? { role: 'user', content: msg[1] } + : { role: 'assistant', content: msg[1] }; }); - const searchHandler: MetaSearchAgentType = searchHandlers[body.focusMode]; + const session = SessionManager.createSession(); - if (!searchHandler) { - return Response.json({ message: 'Invalid focus mode' }, { status: 400 }); - } + const agent = new SearchAgent(); - const emitter = await searchHandler.searchAndAnswer( - body.query, - history, - llm, - embeddings, - body.optimizationMode, - [], - body.systemInstructions || '', - ); + agent.searchAsync(session, { + chatHistory: history, + config: { + embedding: embeddings, + llm: llm, + sources: ['web', 'discussions', 'academic'], + mode: 'balanced', + }, + followUp: body.query, + }); if (!body.stream) { return new Promise( @@ -71,7 +70,7 @@ export const POST = async (req: Request) => { let message = ''; let sources: any[] = []; - emitter.on('data', (data: string) => { + session.addListener('data', (data: string) => { try { const parsedData = JSON.parse(data); if (parsedData.type === 'response') { @@ -89,11 +88,11 @@ export const POST = async (req: Request) => { } }); - emitter.on('end', () => { + session.addListener('end', () => { resolve(Response.json({ message, sources }, { status: 200 })); }); - emitter.on('error', (error: any) => { + session.addListener('error', (error: any) => { reject( Response.json( { message: 'Search error', error }, @@ -124,14 +123,14 @@ export const POST = async (req: Request) => { ); signal.addEventListener('abort', () => { - emitter.removeAllListeners(); + session.removeAllListeners(); try { controller.close(); - } catch (error) { } + } catch (error) {} }); - emitter.on('data', (data: string) => { + session.addListener('data', (data: string) => { if (signal.aborted) return; try { @@ -162,7 +161,7 @@ export const POST = async (req: Request) => { } }); - emitter.on('end', () => { + session.addListener('end', () => { if (signal.aborted) return; controller.enqueue( @@ -175,7 +174,7 @@ export const POST = async (req: Request) => { controller.close(); }); - emitter.on('error', (error: any) => { + session.addListener('error', (error: any) => { if (signal.aborted) return; controller.error(error); diff --git a/src/app/api/uploads/route.ts b/src/app/api/uploads/route.ts index 2a275f4..dc9c202 100644 --- a/src/app/api/uploads/route.ts +++ b/src/app/api/uploads/route.ts @@ -7,6 +7,7 @@ import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'; import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'; import { Document } from '@langchain/core/documents'; import ModelRegistry from '@/lib/models/registry'; +import { Chunk } from '@/lib/types'; interface FileRes { fileName: string; @@ -87,9 +88,17 @@ export async function POST(req: Request) { }), ); - const embeddings = await model.embedDocuments( - splitted.map((doc) => doc.pageContent), + const chunks: Chunk[] = splitted.map((doc) => { + return { + content: doc.pageContent, + metadata: doc.metadata, + } + }); + + const embeddings = await model.embedChunks( + chunks ); + const embeddingsDataPath = filePath.replace( /\.\w+$/, '-embeddings.json', diff --git a/src/app/api/videos/route.ts b/src/app/api/videos/route.ts index 1417226..0d5e03c 100644 --- a/src/app/api/videos/route.ts +++ b/src/app/api/videos/route.ts @@ -1,7 +1,6 @@ import handleVideoSearch from '@/lib/agents/media/video'; import ModelRegistry from '@/lib/models/registry'; import { ModelWithProvider } from '@/lib/models/types'; -import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; interface VideoSearchBody { query: string; @@ -20,19 +19,9 @@ export const POST = async (req: Request) => { body.chatModel.key, ); - const chatHistory = body.chatHistory - .map((msg: any) => { - if (msg.role === 'user') { - return new HumanMessage(msg.content); - } else if (msg.role === 'assistant') { - return new AIMessage(msg.content); - } - }) - .filter((msg) => msg !== undefined) as BaseMessage[]; - const videos = await handleVideoSearch( { - chatHistory: chatHistory, + chatHistory: body.chatHistory, query: body.query, }, llm, From b7b280637fafc9e86c38f7b9745be5a4a56102ae Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:26:47 +0530 Subject: [PATCH 043/196] feat(providers): update ollama context window, temp --- src/lib/models/providers/ollama/ollamaLLM.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/models/providers/ollama/ollamaLLM.ts b/src/lib/models/providers/ollama/ollamaLLM.ts index 0e64d7b..05869ca 100644 --- a/src/lib/models/providers/ollama/ollamaLLM.ts +++ b/src/lib/models/providers/ollama/ollamaLLM.ts @@ -45,6 +45,7 @@ class OllamaLLM extends BaseLLM { top_p: this.config.options?.topP, temperature: this.config.options?.temperature, num_predict: this.config.options?.maxTokens, + num_ctx: 32000, frequency_penalty: this.config.options?.frequencyPenalty, presence_penalty: this.config.options?.presencePenalty, stop: this.config.options?.stopSequences, @@ -71,6 +72,7 @@ class OllamaLLM extends BaseLLM { options: { top_p: this.config.options?.topP, temperature: this.config.options?.temperature, + num_ctx: 32000, num_predict: this.config.options?.maxTokens, frequency_penalty: this.config.options?.frequencyPenalty, presence_penalty: this.config.options?.presencePenalty, @@ -99,7 +101,7 @@ class OllamaLLM extends BaseLLM { think: false, options: { top_p: this.config.options?.topP, - temperature: 0, + temperature: 0.7, num_predict: this.config.options?.maxTokens, frequency_penalty: this.config.options?.frequencyPenalty, presence_penalty: this.config.options?.presencePenalty, @@ -127,7 +129,7 @@ class OllamaLLM extends BaseLLM { think: false, options: { top_p: this.config.options?.topP, - temperature: this.config.options?.temperature, + temperature: 0.7, num_predict: this.config.options?.maxTokens, frequency_penalty: this.config.options?.frequencyPenalty, presence_penalty: this.config.options?.presencePenalty, From d5f62f2dca6dbdf722dbe2465ae96faaa839a97e Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:46:02 +0530 Subject: [PATCH 044/196] feat(chat): prevent auto-scroll unless message sent --- src/components/Chat.tsx | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index 22e0a48..56c13e6 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -7,11 +7,12 @@ import MessageBoxLoading from './MessageBoxLoading'; import { useChat } from '@/lib/hooks/useChat'; const Chat = () => { - const { sections, chatTurns, loading, messageAppeared } = useChat(); + const { sections, loading, messageAppeared, messages } = useChat(); const [dividerWidth, setDividerWidth] = useState(0); const dividerRef = useRef(null); const messageEnd = useRef(null); + const lastScrolledRef = useRef(0); useEffect(() => { const updateDividerWidth = () => { @@ -22,35 +23,40 @@ const Chat = () => { updateDividerWidth(); + const resizeObserver = new ResizeObserver(() => { + updateDividerWidth(); + }); + + const currentRef = dividerRef.current; + if (currentRef) { + resizeObserver.observe(currentRef); + } + window.addEventListener('resize', updateDividerWidth); return () => { + if (currentRef) { + resizeObserver.unobserve(currentRef); + } + resizeObserver.disconnect(); window.removeEventListener('resize', updateDividerWidth); }; - }, []); + }, [sections.length]); useEffect(() => { const scroll = () => { messageEnd.current?.scrollIntoView({ behavior: 'auto' }); }; - if (chatTurns.length === 1) { - document.title = `${chatTurns[0].content.substring(0, 30)} - Perplexica`; + if (messages.length === 1) { + document.title = `${messages[0].query.substring(0, 30)} - Perplexica`; } - const messageEndBottom = - messageEnd.current?.getBoundingClientRect().bottom ?? 0; - - const distanceFromMessageEnd = window.innerHeight - messageEndBottom; - - if (distanceFromMessageEnd >= -100) { + if (sections.length > lastScrolledRef.current) { scroll(); + lastScrolledRef.current = sections.length; } - - if (chatTurns[chatTurns.length - 1]?.role === 'user') { - scroll(); - } - }, [chatTurns]); + }, [messages]); return (

@@ -58,7 +64,7 @@ const Chat = () => { const isLast = i === sections.length - 1; return ( - + Date: Sun, 23 Nov 2025 19:46:42 +0530 Subject: [PATCH 045/196] feat(app): add initial widgets --- src/components/WeatherWidget.tsx | 2 +- src/components/Widgets/Calculation.tsx | 54 ++ src/components/Widgets/Renderer.tsx | 76 +++ src/components/Widgets/Stock.tsx | 517 ++++++++++++++++++ src/components/Widgets/Weather.tsx | 408 ++++++++++++++ .../classifier/intents/widgetResponse.ts | 40 +- .../search/widgets/calculationWidget.ts | 65 +++ src/lib/agents/search/widgets/index.ts | 4 + src/lib/agents/search/widgets/stockWidget.ts | 412 ++++++++++++++ .../agents/search/widgets/weatherWidget.ts | 205 ++++--- 10 files changed, 1703 insertions(+), 80 deletions(-) create mode 100644 src/components/Widgets/Calculation.tsx create mode 100644 src/components/Widgets/Renderer.tsx create mode 100644 src/components/Widgets/Stock.tsx create mode 100644 src/components/Widgets/Weather.tsx create mode 100644 src/lib/agents/search/widgets/calculationWidget.ts create mode 100644 src/lib/agents/search/widgets/stockWidget.ts diff --git a/src/components/WeatherWidget.tsx b/src/components/WeatherWidget.tsx index a7ebcff..3ba0038 100644 --- a/src/components/WeatherWidget.tsx +++ b/src/components/WeatherWidget.tsx @@ -91,7 +91,7 @@ const WeatherWidget = () => { setData({ temperature: data.temperature, condition: data.condition, - location: location.city, + location: 'Mars', humidity: data.humidity, windSpeed: data.windSpeed, icon: data.icon, diff --git a/src/components/Widgets/Calculation.tsx b/src/components/Widgets/Calculation.tsx new file mode 100644 index 0000000..e671aab --- /dev/null +++ b/src/components/Widgets/Calculation.tsx @@ -0,0 +1,54 @@ +'use client'; + +import { Calculator, Equal } from 'lucide-react'; + +type CalculationWidgetProps = { + expression: string; + result: number; +}; + +const Calculation = ({ expression, result }: CalculationWidgetProps) => { + return ( +
+
+
+ +
+ + Calculation + +
+ +
+
+
+ + Expression + +
+
+ + {expression} + +
+
+ +
+
+ + + Result + +
+
+
+ {result.toLocaleString()} +
+
+
+
+
+ ); +}; + +export default Calculation; diff --git a/src/components/Widgets/Renderer.tsx b/src/components/Widgets/Renderer.tsx new file mode 100644 index 0000000..8456c8f --- /dev/null +++ b/src/components/Widgets/Renderer.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { Widget } from '../ChatWindow'; +import Weather from './Weather'; +import Calculation from './Calculation'; +import Stock from './Stock'; + +const Renderer = ({ widgets }: { widgets: Widget[] }) => { + return widgets.map((widget, index) => { + switch (widget.widgetType) { + case 'weather': + return ( + + ); + case 'calculation_result': + return ( + + ); + case 'stock': + return ( + + ); + default: + return
Unknown widget type: {widget.widgetType}
; + } + }); +}; + +export default Renderer; diff --git a/src/components/Widgets/Stock.tsx b/src/components/Widgets/Stock.tsx new file mode 100644 index 0000000..57fba1a --- /dev/null +++ b/src/components/Widgets/Stock.tsx @@ -0,0 +1,517 @@ +'use client'; + +import { Clock, ArrowUpRight, ArrowDownRight, Minus } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; +import { + createChart, + ColorType, + LineStyle, + BaselineSeries, + LineSeries, +} from 'lightweight-charts'; + +type StockWidgetProps = { + symbol: string; + shortName: string; + longName?: string; + exchange?: string; + currency?: string; + marketState?: string; + regularMarketPrice?: number; + regularMarketChange?: number; + regularMarketChangePercent?: number; + regularMarketPreviousClose?: number; + regularMarketOpen?: number; + regularMarketDayHigh?: number; + regularMarketDayLow?: number; + regularMarketVolume?: number; + averageDailyVolume3Month?: number; + marketCap?: number; + fiftyTwoWeekLow?: number; + fiftyTwoWeekHigh?: number; + trailingPE?: number; + forwardPE?: number; + dividendYield?: number; + earningsPerShare?: number; + website?: string; + postMarketPrice?: number; + postMarketChange?: number; + postMarketChangePercent?: number; + preMarketPrice?: number; + preMarketChange?: number; + preMarketChangePercent?: number; + chartData?: { + '1D'?: { timestamps: number[]; prices: number[] } | null; + '5D'?: { timestamps: number[]; prices: number[] } | null; + '1M'?: { timestamps: number[]; prices: number[] } | null; + '3M'?: { timestamps: number[]; prices: number[] } | null; + '6M'?: { timestamps: number[]; prices: number[] } | null; + '1Y'?: { timestamps: number[]; prices: number[] } | null; + MAX?: { timestamps: number[]; prices: number[] } | null; + } | null; + comparisonData?: Array<{ + ticker: string; + name: string; + chartData: { + '1D'?: { timestamps: number[]; prices: number[] } | null; + '5D'?: { timestamps: number[]; prices: number[] } | null; + '1M'?: { timestamps: number[]; prices: number[] } | null; + '3M'?: { timestamps: number[]; prices: number[] } | null; + '6M'?: { timestamps: number[]; prices: number[] } | null; + '1Y'?: { timestamps: number[]; prices: number[] } | null; + MAX?: { timestamps: number[]; prices: number[] } | null; + }; + }> | null; + error?: string; +}; + +const formatNumber = (num: number | undefined, decimals = 2): string => { + if (num === undefined || num === null) return 'N/A'; + return num.toLocaleString(undefined, { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }); +}; + +const formatLargeNumber = (num: number | undefined): string => { + if (num === undefined || num === null) return 'N/A'; + if (num >= 1e12) return `$${(num / 1e12).toFixed(2)}T`; + if (num >= 1e9) return `$${(num / 1e9).toFixed(2)}B`; + if (num >= 1e6) return `$${(num / 1e6).toFixed(2)}M`; + if (num >= 1e3) return `$${(num / 1e3).toFixed(2)}K`; + return `$${num.toFixed(2)}`; +}; + +const Stock = (props: StockWidgetProps) => { + const [isDarkMode, setIsDarkMode] = useState(false); + const [selectedTimeframe, setSelectedTimeframe] = useState< + '1D' | '5D' | '1M' | '3M' | '6M' | '1Y' | 'MAX' + >('1M'); + const chartContainerRef = useRef(null); + + useEffect(() => { + const checkDarkMode = () => { + setIsDarkMode(document.documentElement.classList.contains('dark')); + }; + + checkDarkMode(); + + const observer = new MutationObserver(checkDarkMode); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class'], + }); + + return () => observer.disconnect(); + }, []); + + useEffect(() => { + const currentChartData = props.chartData?.[selectedTimeframe]; + if ( + !chartContainerRef.current || + !currentChartData || + currentChartData.timestamps.length === 0 + ) { + return; + } + + const chart = createChart(chartContainerRef.current, { + width: chartContainerRef.current.clientWidth, + height: 280, + layout: { + background: { type: ColorType.Solid, color: 'transparent' }, + textColor: isDarkMode ? '#6b7280' : '#9ca3af', + fontSize: 11, + attributionLogo: false, + }, + grid: { + vertLines: { + color: isDarkMode ? '#21262d' : '#e8edf1', + style: LineStyle.Solid, + }, + horzLines: { + color: isDarkMode ? '#21262d' : '#e8edf1', + style: LineStyle.Solid, + }, + }, + crosshair: { + vertLine: { + color: isDarkMode ? '#30363d' : '#d0d7de', + labelVisible: false, + }, + horzLine: { + color: isDarkMode ? '#30363d' : '#d0d7de', + labelVisible: true, + }, + }, + rightPriceScale: { + borderVisible: false, + visible: false, + }, + leftPriceScale: { + borderVisible: false, + visible: true, + }, + timeScale: { + borderVisible: false, + timeVisible: false, + }, + handleScroll: false, + handleScale: false, + }); + + const prices = currentChartData.prices; + let baselinePrice: number; + + if (selectedTimeframe === '1D') { + baselinePrice = props.regularMarketPreviousClose ?? prices[0]; + } else { + baselinePrice = prices[0]; + } + + const baselineSeries = chart.addSeries(BaselineSeries); + + baselineSeries.applyOptions({ + baseValue: { type: 'price', price: baselinePrice }, + topLineColor: isDarkMode ? '#14b8a6' : '#0d9488', + topFillColor1: isDarkMode + ? 'rgba(20, 184, 166, 0.28)' + : 'rgba(13, 148, 136, 0.24)', + topFillColor2: isDarkMode + ? 'rgba(20, 184, 166, 0.05)' + : 'rgba(13, 148, 136, 0.05)', + bottomLineColor: isDarkMode ? '#f87171' : '#dc2626', + bottomFillColor1: isDarkMode + ? 'rgba(248, 113, 113, 0.05)' + : 'rgba(220, 38, 38, 0.05)', + bottomFillColor2: isDarkMode + ? 'rgba(248, 113, 113, 0.28)' + : 'rgba(220, 38, 38, 0.24)', + lineWidth: 2, + crosshairMarkerVisible: true, + crosshairMarkerRadius: 4, + crosshairMarkerBorderColor: '', + crosshairMarkerBackgroundColor: '', + }); + + const data = currentChartData.timestamps.map((timestamp, index) => { + const price = currentChartData.prices[index]; + return { + time: (timestamp / 1000) as any, + value: price, + }; + }); + + baselineSeries.setData(data); + + const comparisonColors = ['#8b5cf6', '#f59e0b', '#ec4899']; + if (props.comparisonData && props.comparisonData.length > 0) { + props.comparisonData.forEach((comp, index) => { + const compChartData = comp.chartData[selectedTimeframe]; + if (compChartData && compChartData.prices.length > 0) { + const compData = compChartData.timestamps.map((timestamp, i) => ({ + time: (timestamp / 1000) as any, + value: compChartData.prices[i], + })); + + const compSeries = chart.addSeries(LineSeries); + compSeries.applyOptions({ + color: comparisonColors[index] || '#6b7280', + lineWidth: 2, + crosshairMarkerVisible: true, + crosshairMarkerRadius: 4, + priceScaleId: 'left', + }); + compSeries.setData(compData); + } + }); + } + + chart.timeScale().fitContent(); + + const handleResize = () => { + if (chartContainerRef.current) { + chart.applyOptions({ + width: chartContainerRef.current.clientWidth, + }); + } + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + chart.remove(); + }; + }, [ + props.chartData, + props.comparisonData, + selectedTimeframe, + isDarkMode, + props.regularMarketPreviousClose, + ]); + + const isPositive = (props.regularMarketChange ?? 0) >= 0; + const isMarketOpen = props.marketState === 'REGULAR'; + const isPreMarket = props.marketState === 'PRE'; + const isPostMarket = props.marketState === 'POST'; + + const displayPrice = isPostMarket + ? props.postMarketPrice ?? props.regularMarketPrice + : isPreMarket + ? props.preMarketPrice ?? props.regularMarketPrice + : props.regularMarketPrice; + + const displayChange = isPostMarket + ? props.postMarketChange ?? props.regularMarketChange + : isPreMarket + ? props.preMarketChange ?? props.regularMarketChange + : props.regularMarketChange; + + const displayChangePercent = isPostMarket + ? props.postMarketChangePercent ?? props.regularMarketChangePercent + : isPreMarket + ? props.preMarketChangePercent ?? props.regularMarketChangePercent + : props.regularMarketChangePercent; + + const changeColor = isPositive + ? 'text-green-600 dark:text-green-400' + : 'text-red-600 dark:text-red-400'; + + if (props.error) { + return ( +
+

+ Error: {props.error} +

+
+ ); + } + + return ( +
+
+
+
+
+ {props.website && ( + {`${props.symbol} { + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> + )} +

+ {props.symbol} +

+ {props.exchange && ( + + {props.exchange} + + )} + {isMarketOpen && ( +
+
+ + Live + +
+ )} + {isPreMarket && ( +
+ + + Pre-Market + +
+ )} + {isPostMarket && ( +
+ + + After Hours + +
+ )} +
+

+ {props.longName || props.shortName} +

+
+ +
+
+ + {props.currency === 'USD' ? '$' : ''} + {formatNumber(displayPrice)} + +
+
+ {isPositive ? ( + + ) : displayChange === 0 ? ( + + ) : ( + + )} + + {displayChange !== undefined && displayChange >= 0 ? '+' : ''} + {formatNumber(displayChange)} + + + ( + {displayChangePercent !== undefined && displayChangePercent >= 0 + ? '+' + : ''} + {formatNumber(displayChangePercent)}%) + +
+
+
+ + {props.chartData && ( +
+
+
+ {(['1D', '5D', '1M', '3M', '6M', '1Y', 'MAX'] as const).map( + (timeframe) => ( + + ), + )} +
+ + {props.comparisonData && props.comparisonData.length > 0 && ( +
+ + {props.symbol} + + {props.comparisonData.map((comp, index) => { + const colors = ['#8b5cf6', '#f59e0b', '#ec4899']; + return ( +
+
+ + {comp.ticker} + +
+ ); + })} +
+ )} +
+ +
+
+
+ +
+
+ + Prev Close + + + ${formatNumber(props.regularMarketPreviousClose)} + +
+
+ + 52W Range + + + ${formatNumber(props.fiftyTwoWeekLow, 2)}-$ + {formatNumber(props.fiftyTwoWeekHigh, 2)} + +
+
+ + Market Cap + + + {formatLargeNumber(props.marketCap)} + +
+
+ + Open + + + ${formatNumber(props.regularMarketOpen)} + +
+
+ + P/E Ratio + + + {props.trailingPE ? formatNumber(props.trailingPE, 2) : 'N/A'} + +
+
+ + Dividend Yield + + + {props.dividendYield + ? `${formatNumber(props.dividendYield * 100, 2)}%` + : 'N/A'} + +
+
+ + Day Range + + + ${formatNumber(props.regularMarketDayLow, 2)}-$ + {formatNumber(props.regularMarketDayHigh, 2)} + +
+
+ + Volume + + + {formatLargeNumber(props.regularMarketVolume)} + +
+
+ + EPS + + + $ + {props.earningsPerShare + ? formatNumber(props.earningsPerShare, 2) + : 'N/A'} + +
+
+
+ )} +
+
+ ); +}; + +export default Stock; diff --git a/src/components/Widgets/Weather.tsx b/src/components/Widgets/Weather.tsx new file mode 100644 index 0000000..749d65b --- /dev/null +++ b/src/components/Widgets/Weather.tsx @@ -0,0 +1,408 @@ +'use client'; + +import { Wind, Droplets, Gauge } from 'lucide-react'; +import { useMemo, useEffect, useState } from 'react'; + +type WeatherWidgetProps = { + location: string; + current: { + time: string; + temperature_2m: number; + relative_humidity_2m: number; + apparent_temperature: number; + is_day: number; + precipitation: number; + weather_code: number; + wind_speed_10m: number; + wind_direction_10m: number; + wind_gusts_10m?: number; + }; + daily: { + time: string[]; + weather_code: number[]; + temperature_2m_max: number[]; + temperature_2m_min: number[]; + precipitation_probability_max: number[]; + }; + timezone: string; +}; + +const getWeatherInfo = (code: number, isDay: boolean, isDarkMode: boolean) => { + const dayNight = isDay ? 'day' : 'night'; + + const weatherMap: Record< + number, + { icon: string; description: string; gradient: string } + > = { + 0: { + icon: `clear-${dayNight}.svg`, + description: 'Clear', + gradient: isDarkMode + ? isDay + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #E8F1FA, #7A9DBF 35%, #4A7BA8 60%, #2F5A88)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #5A6A7E, #3E4E63 40%, #2A3544 65%, #1A2230)' + : isDay + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #DBEAFE 30%, #93C5FD 60%, #60A5FA)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #7B8694, #475569 45%, #334155 70%, #1E293B)', + }, + 1: { + icon: `clear-${dayNight}.svg`, + description: 'Mostly Clear', + gradient: isDarkMode + ? isDay + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #E8F1FA, #7A9DBF 35%, #4A7BA8 60%, #2F5A88)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #5A6A7E, #3E4E63 40%, #2A3544 65%, #1A2230)' + : isDay + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #DBEAFE 30%, #93C5FD 60%, #60A5FA)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #7B8694, #475569 45%, #334155 70%, #1E293B)', + }, + 2: { + icon: `cloudy-1-${dayNight}.svg`, + description: 'Partly Cloudy', + gradient: isDarkMode + ? isDay + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #D4E1ED, #8BA3B8 35%, #617A93 60%, #426070)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #6B7583, #4A5563 40%, #3A4450 65%, #2A3340)' + : isDay + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #E0F2FE 28%, #BFDBFE 58%, #93C5FD)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #8B99AB, #64748B 45%, #475569 70%, #334155)', + }, + 3: { + icon: `cloudy-1-${dayNight}.svg`, + description: 'Cloudy', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #B8C3CF, #758190 38%, #546270 65%, #3D4A58)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #F5F8FA, #CBD5E1 32%, #94A3B8 65%, #64748B)', + }, + 45: { + icon: `fog-${dayNight}.svg`, + description: 'Foggy', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #C5CDD8, #8892A0 38%, #697380 65%, #4F5A68)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #E2E8F0 30%, #CBD5E1 62%, #94A3B8)', + }, + 48: { + icon: `fog-${dayNight}.svg`, + description: 'Rime Fog', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #C5CDD8, #8892A0 38%, #697380 65%, #4F5A68)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #E2E8F0 30%, #CBD5E1 62%, #94A3B8)', + }, + 51: { + icon: `rainy-1-${dayNight}.svg`, + description: 'Light Drizzle', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #B8D4E5, #6FA4C5 35%, #4A85AC 60%, #356A8E)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #E5FBFF, #A5F3FC 28%, #67E8F9 60%, #22D3EE)', + }, + 53: { + icon: `rainy-1-${dayNight}.svg`, + description: 'Drizzle', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #B8D4E5, #6FA4C5 35%, #4A85AC 60%, #356A8E)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #E5FBFF, #A5F3FC 28%, #67E8F9 60%, #22D3EE)', + }, + 55: { + icon: `rainy-2-${dayNight}.svg`, + description: 'Heavy Drizzle', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #A5C5D8, #5E92B0 35%, #3F789D 60%, #2A5F82)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #D4F3FF, #7DD3FC 30%, #38BDF8 62%, #0EA5E9)', + }, + 61: { + icon: `rainy-2-${dayNight}.svg`, + description: 'Light Rain', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #A5C5D8, #5E92B0 35%, #3F789D 60%, #2A5F82)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #D4F3FF, #7DD3FC 30%, #38BDF8 62%, #0EA5E9)', + }, + 63: { + icon: `rainy-2-${dayNight}.svg`, + description: 'Rain', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #8DB3C8, #4D819F 38%, #326A87 65%, #215570)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #B8E8FF, #38BDF8 32%, #0EA5E9 65%, #0284C7)', + }, + 65: { + icon: `rainy-3-${dayNight}.svg`, + description: 'Heavy Rain', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #7BA3B8, #3D6F8A 38%, #295973 65%, #1A455D)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #9CD9F5, #0EA5E9 32%, #0284C7 65%, #0369A1)', + }, + 71: { + icon: `snowy-1-${dayNight}.svg`, + description: 'Light Snow', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #E5F0FA, #9BB5CE 32%, #7496B8 58%, #527A9E)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #F0F9FF 25%, #E0F2FE 55%, #BAE6FD)', + }, + 73: { + icon: `snowy-2-${dayNight}.svg`, + description: 'Snow', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #D4E5F3, #85A1BD 35%, #6584A8 60%, #496A8E)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #FAFEFF, #E0F2FE 28%, #BAE6FD 60%, #7DD3FC)', + }, + 75: { + icon: `snowy-3-${dayNight}.svg`, + description: 'Heavy Snow', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #BDD8EB, #6F92AE 35%, #4F7593 60%, #365A78)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #F0FAFF, #BAE6FD 30%, #7DD3FC 62%, #38BDF8)', + }, + 77: { + icon: `snowy-1-${dayNight}.svg`, + description: 'Snow Grains', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #E5F0FA, #9BB5CE 32%, #7496B8 58%, #527A9E)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #F0F9FF 25%, #E0F2FE 55%, #BAE6FD)', + }, + 80: { + icon: `rainy-2-${dayNight}.svg`, + description: 'Light Showers', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #A5C5D8, #5E92B0 35%, #3F789D 60%, #2A5F82)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #D4F3FF, #7DD3FC 30%, #38BDF8 62%, #0EA5E9)', + }, + 81: { + icon: `rainy-2-${dayNight}.svg`, + description: 'Showers', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #8DB3C8, #4D819F 38%, #326A87 65%, #215570)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #B8E8FF, #38BDF8 32%, #0EA5E9 65%, #0284C7)', + }, + 82: { + icon: `rainy-3-${dayNight}.svg`, + description: 'Heavy Showers', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #7BA3B8, #3D6F8A 38%, #295973 65%, #1A455D)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #9CD9F5, #0EA5E9 32%, #0284C7 65%, #0369A1)', + }, + 85: { + icon: `snowy-2-${dayNight}.svg`, + description: 'Light Snow Showers', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #D4E5F3, #85A1BD 35%, #6584A8 60%, #496A8E)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #FAFEFF, #E0F2FE 28%, #BAE6FD 60%, #7DD3FC)', + }, + 86: { + icon: `snowy-3-${dayNight}.svg`, + description: 'Snow Showers', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #BDD8EB, #6F92AE 35%, #4F7593 60%, #365A78)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #F0FAFF, #BAE6FD 30%, #7DD3FC 62%, #38BDF8)', + }, + 95: { + icon: `scattered-thunderstorms-${dayNight}.svg`, + description: 'Thunderstorm', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #8A95A3, #5F6A7A 38%, #475260 65%, #2F3A48)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #C8D1DD, #94A3B8 32%, #64748B 65%, #475569)', + }, + 96: { + icon: 'severe-thunderstorm.svg', + description: 'Thunderstorm + Hail', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #7A8593, #515C6D 38%, #3A4552 65%, #242D3A)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #B0BBC8, #64748B 32%, #475569 65%, #334155)', + }, + 99: { + icon: 'severe-thunderstorm.svg', + description: 'Severe Thunderstorm', + gradient: isDarkMode + ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #6A7583, #434E5D 40%, #2F3A47 68%, #1C2530)' + : 'radial-gradient(ellipse 150% 100% at 50% 100%, #9BA8B8, #475569 35%, #334155 68%, #1E293B)', + }, + }; + + return weatherMap[code] || weatherMap[0]; +}; + +const Weather = ({ + location, + current, + daily, + timezone, +}: WeatherWidgetProps) => { + const [isDarkMode, setIsDarkMode] = useState(false); + + useEffect(() => { + const checkDarkMode = () => { + setIsDarkMode(document.documentElement.classList.contains('dark')); + }; + + checkDarkMode(); + + const observer = new MutationObserver(checkDarkMode); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class'], + }); + + return () => observer.disconnect(); + }, []); + + const weatherInfo = useMemo( + () => + getWeatherInfo( + current?.weather_code || 0, + current?.is_day === 1, + isDarkMode, + ), + [current?.weather_code, current?.is_day, isDarkMode], + ); + + const forecast = useMemo(() => { + if (!daily?.time || daily.time.length === 0) return []; + + return daily.time.slice(1, 7).map((time, idx) => { + const date = new Date(time); + const dayName = date.toLocaleDateString('en-US', { weekday: 'short' }); + const isDay = true; + const weatherCode = daily.weather_code[idx + 1]; + const info = getWeatherInfo(weatherCode, isDay, isDarkMode); + + return { + day: dayName, + icon: info.icon, + high: Math.round(daily.temperature_2m_max[idx + 1]), + low: Math.round(daily.temperature_2m_min[idx + 1]), + highF: Math.round((daily.temperature_2m_max[idx + 1] * 9) / 5 + 32), + lowF: Math.round((daily.temperature_2m_min[idx + 1] * 9) / 5 + 32), + precipitation: daily.precipitation_probability_max[idx + 1] || 0, + }; + }); + }, [daily, isDarkMode]); + + if (!current || !daily || !daily.time || daily.time.length === 0) { + return ( +
+
+

Weather data unavailable for {location}

+
+
+ ); + } + + return ( +
+
+ +
+
+
+ {weatherInfo.description} +
+
+ + {Math.round(current.temperature_2m)}° + + F C +
+

+ {weatherInfo.description} +

+
+
+
+

+ {Math.round(daily.temperature_2m_max[0])}°{' '} + {Math.round(daily.temperature_2m_min[0])}° +

+
+
+ +
+

{location}

+

+ {new Date(current.time).toLocaleString('en-US', { + weekday: 'short', + hour: 'numeric', + minute: '2-digit', + })} +

+
+ +
+ {forecast.map((day, idx) => ( +
+

{day.day}

+ +
+ {day.high}° + + {day.low}° + +
+ {day.precipitation > 0 && ( +
+ + + {day.precipitation}% + +
+ )} +
+ ))} +
+ +
+
+ +
+

+ Wind +

+

+ {Math.round(current.wind_speed_10m)} km/h +

+
+
+ +
+ +
+

+ Humidity +

+

+ {Math.round(current.relative_humidity_2m)}% +

+
+
+ +
+ +
+

+ Feels Like +

+

+ {Math.round(current.apparent_temperature)}°C +

+
+
+
+
+
+ ); +}; + +export default Weather; diff --git a/src/lib/agents/search/classifier/intents/widgetResponse.ts b/src/lib/agents/search/classifier/intents/widgetResponse.ts index 0cfd58d..a5b0885 100644 --- a/src/lib/agents/search/classifier/intents/widgetResponse.ts +++ b/src/lib/agents/search/classifier/intents/widgetResponse.ts @@ -1,9 +1,45 @@ import { Intent } from '../../types'; +const description = `Use this intent when the user's query can be fully or partially answered using specialized widgets that provide structured, real-time data (weather, stocks, calculations, and more). + +#### When to use: +1. The user is asking for specific information that a widget can provide (current weather, stock prices, mathematical calculations, unit conversions, etc.). +2. A widget can completely answer the query without needing additional web search (use this intent alone and set skipSearch to true). +3. A widget can provide part of the answer, but additional information from web search or other sources is needed (combine with other intents like 'web_search' and set skipSearch to false). + +#### Example use cases: +Note: These are just examples - there are several other widgets available for use depending on the user's query. + +1. "What is the weather in New York?" + - The weather widget can fully answer this query. + - Intent: ['widget_response'] with skipSearch: true + - Widget: [{ type: 'weather', location: 'New York', lat: 0, lon: 0 }] + +2. "What's the weather in San Francisco today? Also tell me some popular events happening there this weekend." + - Weather widget provides current conditions, but events require web search. + - Intent: ['web_search', 'widget_response'] with skipSearch: false + - Widget: [{ type: 'weather', location: 'San Francisco', lat: 0, lon: 0 }] + +3. "Calculate 25% of 480" + - The calculator widget can fully answer this. + - Intent: ['widget_response'] with skipSearch: true + - Widget: [{ type: 'calculator', expression: '25% of 480' }] + +4. "AAPL stock price and recent Apple news" + - Stock widget provides price, but news requires web search. + - Intent: ['web_search', 'widget_response'] with skipSearch: false + - Widget: [{ type: 'stock', symbol: 'AAPL' }] + +5. "What's Tesla's stock doing and how does it compare to competitors?" + - Stock widget provides Tesla's price, but comparison analysis requires web search. + - Intent: ['web_search', 'widget_response'] with skipSearch: false + - Widget: [{ type: 'stock', symbol: 'TSLA' }] + +**IMPORTANT**: Set skipSearch to true ONLY if the widget(s) can completely answer the user's query without any additional information. If the user asks for anything beyond what the widget provides (context, explanations, comparisons, related information), combine this intent with 'web_search' and set skipSearch to false.`; + const widgetResponseIntent: Intent = { name: 'widget_response', - description: - 'Use this intent to respond to user queries using available widgets when the required information can be obtained from them.', + description, requiresSearch: false, enabled: (config) => true, }; diff --git a/src/lib/agents/search/widgets/calculationWidget.ts b/src/lib/agents/search/widgets/calculationWidget.ts new file mode 100644 index 0000000..c613b40 --- /dev/null +++ b/src/lib/agents/search/widgets/calculationWidget.ts @@ -0,0 +1,65 @@ +import z from 'zod'; +import { Widget } from '../types'; +import { evaluate as mathEval } from 'mathjs'; + +const schema = z.object({ + type: z.literal('calculation'), + expression: z + .string() + .describe( + "A valid mathematical expression to be evaluated (e.g., '2 + 2', '3 * (4 + 5)').", + ), +}); + +const calculationWidget: Widget = { + name: 'calculation', + description: `Performs mathematical calculations and evaluates mathematical expressions. Supports arithmetic operations, algebraic equations, functions, and complex mathematical computations. + +**What it provides:** +- Evaluates mathematical expressions and returns computed results +- Handles basic arithmetic (+, -, *, /) +- Supports functions (sqrt, sin, cos, log, etc.) +- Can process complex expressions with parentheses and order of operations + +**When to use:** +- User asks to calculate, compute, or evaluate a mathematical expression +- Questions like "what is X", "calculate Y", "how much is Z" where X/Y/Z are math expressions +- Any request involving numbers and mathematical operations + +**Example call:** +{ + "type": "calculation", + "expression": "25% of 480" +} + +{ + "type": "calculation", + "expression": "sqrt(144) + 5 * 2" +} + +**Important:** The expression must be valid mathematical syntax that can be evaluated by mathjs. Format percentages as "0.25 * 480" or "25% of 480". Do not include currency symbols, units, or non-mathematical text in the expression.`, + schema: schema, + execute: async (params, _) => { + try { + const result = mathEval(params.expression); + + return { + type: 'calculation_result', + data: { + expression: params.expression, + result: result, + }, + }; + } catch (error) { + return { + type: 'calculation_result', + data: { + expression: params.expression, + result: `Error evaluating expression: ${error}`, + }, + }; + } + }, +}; + +export default calculationWidget; diff --git a/src/lib/agents/search/widgets/index.ts b/src/lib/agents/search/widgets/index.ts index 7ddc597..ff18d40 100644 --- a/src/lib/agents/search/widgets/index.ts +++ b/src/lib/agents/search/widgets/index.ts @@ -1,6 +1,10 @@ +import calculationWidget from './calculationWidget'; import WidgetRegistry from './registry'; import weatherWidget from './weatherWidget'; +import stockWidget from './stockWidget'; WidgetRegistry.register(weatherWidget); +WidgetRegistry.register(calculationWidget); +WidgetRegistry.register(stockWidget); export { WidgetRegistry }; diff --git a/src/lib/agents/search/widgets/stockWidget.ts b/src/lib/agents/search/widgets/stockWidget.ts new file mode 100644 index 0000000..b4f8b86 --- /dev/null +++ b/src/lib/agents/search/widgets/stockWidget.ts @@ -0,0 +1,412 @@ +import z from 'zod'; +import { Widget } from '../types'; +import YahooFinance from 'yahoo-finance2'; + +const yf = new YahooFinance({ + suppressNotices: ['yahooSurvey'], +}); + +const schema = z.object({ + type: z.literal('stock'), + ticker: z + .string() + .describe( + "The stock ticker symbol in uppercase (e.g., 'AAPL' for Apple Inc., 'TSLA' for Tesla, 'GOOGL' for Google). Use the primary exchange ticker.", + ), + comparisonTickers: z + .array(z.string()) + .max(3) + .describe( + "Optional array of up to 3 ticker symbols to compare against the base ticker (e.g., ['MSFT', 'GOOGL', 'META']). Charts will show percentage change comparison.", + ), +}); + +const stockWidget: Widget = { + name: 'stock', + description: `Provides comprehensive real-time stock market data and financial information for any publicly traded company. Returns detailed quote data, market status, trading metrics, and company fundamentals. + +You can set skipSearch to true if the stock widget can fully answer the user's query without needing additional web search. + +**What it provides:** +- **Real-time Price Data**: Current price, previous close, open price, day's range (high/low) +- **Market Status**: Whether market is currently open or closed, trading sessions +- **Trading Metrics**: Volume, average volume, bid/ask prices and sizes +- **Performance**: Price changes (absolute and percentage), 52-week high/low range +- **Valuation**: Market capitalization, P/E ratio, earnings per share (EPS) +- **Dividends**: Dividend rate, dividend yield, ex-dividend date +- **Company Info**: Full company name, exchange, currency, sector/industry (when available) +- **Advanced Metrics**: Beta, trailing/forward P/E, book value, price-to-book ratio +- **Charts Data**: Historical price movements for visualization +- **Comparison**: Compare up to 3 stocks side-by-side with percentage-based performance visualization + +**When to use:** +- User asks about a stock price ("What's AAPL stock price?", "How is Tesla doing?") +- Questions about company market performance ("Is Microsoft up or down today?") +- Requests for stock market data, trading info, or company valuation +- Queries about dividends, P/E ratio, market cap, or other financial metrics +- Any stock/equity-related question for a specific company +- Stock comparisons ("Compare AAPL vs MSFT", "How is TSLA doing vs RIVN and LCID?") + +**Example calls:** +{ + "type": "stock", + "ticker": "AAPL" +} + +{ + "type": "stock", + "ticker": "TSLA", + "comparisonTickers": ["RIVN", "LCID"] +} + +{ + "type": "stock", + "ticker": "GOOGL", + "comparisonTickers": ["MSFT", "META", "AMZN"] +} + +**Important:** +- Use the correct ticker symbol (uppercase preferred: AAPL not aapl) +- For companies with multiple share classes, use the most common one (e.g., GOOGL for Google Class A shares) +- The widget works for stocks listed on major exchanges (NYSE, NASDAQ, etc.) +- Returns comprehensive data; the UI will display relevant metrics based on availability +- Market data may be delayed by 15-20 minutes for free data sources during trading hours`, + schema: schema, + execute: async (params, _) => { + try { + const ticker = params.ticker.toUpperCase(); + + const quote: any = await yf.quote(ticker); + + const chartPromises = { + '1D': yf + .chart(ticker, { + period1: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), + period2: new Date(), + interval: '5m', + }) + .catch(() => null), + '5D': yf + .chart(ticker, { + period1: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000), + period2: new Date(), + interval: '15m', + }) + .catch(() => null), + '1M': yf + .chart(ticker, { + period1: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), + interval: '1d', + }) + .catch(() => null), + '3M': yf + .chart(ticker, { + period1: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000), + interval: '1d', + }) + .catch(() => null), + '6M': yf + .chart(ticker, { + period1: new Date(Date.now() - 180 * 24 * 60 * 60 * 1000), + interval: '1d', + }) + .catch(() => null), + '1Y': yf + .chart(ticker, { + period1: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000), + interval: '1d', + }) + .catch(() => null), + MAX: yf + .chart(ticker, { + period1: new Date(Date.now() - 10 * 365 * 24 * 60 * 60 * 1000), + interval: '1wk', + }) + .catch(() => null), + }; + + const charts = await Promise.all([ + chartPromises['1D'], + chartPromises['5D'], + chartPromises['1M'], + chartPromises['3M'], + chartPromises['6M'], + chartPromises['1Y'], + chartPromises['MAX'], + ]); + + const [chart1D, chart5D, chart1M, chart3M, chart6M, chart1Y, chartMAX] = + charts; + + if (!quote) { + throw new Error(`No data found for ticker: ${ticker}`); + } + + let comparisonData: any = null; + if (params.comparisonTickers.length > 0) { + const comparisonPromises = params.comparisonTickers + .slice(0, 3) + .map(async (compTicker) => { + try { + const compQuote = await yf.quote(compTicker); + const compCharts = await Promise.all([ + yf + .chart(compTicker, { + period1: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), + period2: new Date(), + interval: '5m', + }) + .catch(() => null), + yf + .chart(compTicker, { + period1: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000), + period2: new Date(), + interval: '15m', + }) + .catch(() => null), + yf + .chart(compTicker, { + period1: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), + interval: '1d', + }) + .catch(() => null), + yf + .chart(compTicker, { + period1: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000), + interval: '1d', + }) + .catch(() => null), + yf + .chart(compTicker, { + period1: new Date(Date.now() - 180 * 24 * 60 * 60 * 1000), + interval: '1d', + }) + .catch(() => null), + yf + .chart(compTicker, { + period1: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000), + interval: '1d', + }) + .catch(() => null), + yf + .chart(compTicker, { + period1: new Date( + Date.now() - 10 * 365 * 24 * 60 * 60 * 1000, + ), + interval: '1wk', + }) + .catch(() => null), + ]); + return { + ticker: compTicker, + name: compQuote.shortName || compTicker, + charts: compCharts, + }; + } catch (error) { + console.error( + `Failed to fetch comparison ticker ${compTicker}:`, + error, + ); + return null; + } + }); + const compResults = await Promise.all(comparisonPromises); + comparisonData = compResults.filter((r) => r !== null); + } + + const stockData = { + symbol: quote.symbol, + shortName: quote.shortName || quote.longName || ticker, + longName: quote.longName, + exchange: quote.fullExchangeName || quote.exchange, + currency: quote.currency, + quoteType: quote.quoteType, + + marketState: quote.marketState, + regularMarketTime: quote.regularMarketTime, + postMarketTime: quote.postMarketTime, + preMarketTime: quote.preMarketTime, + + regularMarketPrice: quote.regularMarketPrice, + regularMarketChange: quote.regularMarketChange, + regularMarketChangePercent: quote.regularMarketChangePercent, + regularMarketPreviousClose: quote.regularMarketPreviousClose, + regularMarketOpen: quote.regularMarketOpen, + regularMarketDayHigh: quote.regularMarketDayHigh, + regularMarketDayLow: quote.regularMarketDayLow, + + postMarketPrice: quote.postMarketPrice, + postMarketChange: quote.postMarketChange, + postMarketChangePercent: quote.postMarketChangePercent, + preMarketPrice: quote.preMarketPrice, + preMarketChange: quote.preMarketChange, + preMarketChangePercent: quote.preMarketChangePercent, + + regularMarketVolume: quote.regularMarketVolume, + averageDailyVolume3Month: quote.averageDailyVolume3Month, + averageDailyVolume10Day: quote.averageDailyVolume10Day, + bid: quote.bid, + bidSize: quote.bidSize, + ask: quote.ask, + askSize: quote.askSize, + + fiftyTwoWeekLow: quote.fiftyTwoWeekLow, + fiftyTwoWeekHigh: quote.fiftyTwoWeekHigh, + fiftyTwoWeekChange: quote.fiftyTwoWeekChange, + fiftyTwoWeekChangePercent: quote.fiftyTwoWeekChangePercent, + + marketCap: quote.marketCap, + trailingPE: quote.trailingPE, + forwardPE: quote.forwardPE, + priceToBook: quote.priceToBook, + bookValue: quote.bookValue, + earningsPerShare: quote.epsTrailingTwelveMonths, + epsForward: quote.epsForward, + + dividendRate: quote.dividendRate, + dividendYield: quote.dividendYield, + exDividendDate: quote.exDividendDate, + trailingAnnualDividendRate: quote.trailingAnnualDividendRate, + trailingAnnualDividendYield: quote.trailingAnnualDividendYield, + + beta: quote.beta, + + fiftyDayAverage: quote.fiftyDayAverage, + fiftyDayAverageChange: quote.fiftyDayAverageChange, + fiftyDayAverageChangePercent: quote.fiftyDayAverageChangePercent, + twoHundredDayAverage: quote.twoHundredDayAverage, + twoHundredDayAverageChange: quote.twoHundredDayAverageChange, + twoHundredDayAverageChangePercent: + quote.twoHundredDayAverageChangePercent, + + sector: quote.sector, + industry: quote.industry, + website: quote.website, + + chartData: { + '1D': chart1D + ? { + timestamps: chart1D.quotes.map((q: any) => q.date.getTime()), + prices: chart1D.quotes.map((q: any) => q.close), + } + : null, + '5D': chart5D + ? { + timestamps: chart5D.quotes.map((q: any) => q.date.getTime()), + prices: chart5D.quotes.map((q: any) => q.close), + } + : null, + '1M': chart1M + ? { + timestamps: chart1M.quotes.map((q: any) => q.date.getTime()), + prices: chart1M.quotes.map((q: any) => q.close), + } + : null, + '3M': chart3M + ? { + timestamps: chart3M.quotes.map((q: any) => q.date.getTime()), + prices: chart3M.quotes.map((q: any) => q.close), + } + : null, + '6M': chart6M + ? { + timestamps: chart6M.quotes.map((q: any) => q.date.getTime()), + prices: chart6M.quotes.map((q: any) => q.close), + } + : null, + '1Y': chart1Y + ? { + timestamps: chart1Y.quotes.map((q: any) => q.date.getTime()), + prices: chart1Y.quotes.map((q: any) => q.close), + } + : null, + MAX: chartMAX + ? { + timestamps: chartMAX.quotes.map((q: any) => q.date.getTime()), + prices: chartMAX.quotes.map((q: any) => q.close), + } + : null, + }, + comparisonData: comparisonData + ? comparisonData.map((comp: any) => ({ + ticker: comp.ticker, + name: comp.name, + chartData: { + '1D': comp.charts[0] + ? { + timestamps: comp.charts[0].quotes.map((q: any) => + q.date.getTime(), + ), + prices: comp.charts[0].quotes.map((q: any) => q.close), + } + : null, + '5D': comp.charts[1] + ? { + timestamps: comp.charts[1].quotes.map((q: any) => + q.date.getTime(), + ), + prices: comp.charts[1].quotes.map((q: any) => q.close), + } + : null, + '1M': comp.charts[2] + ? { + timestamps: comp.charts[2].quotes.map((q: any) => + q.date.getTime(), + ), + prices: comp.charts[2].quotes.map((q: any) => q.close), + } + : null, + '3M': comp.charts[3] + ? { + timestamps: comp.charts[3].quotes.map((q: any) => + q.date.getTime(), + ), + prices: comp.charts[3].quotes.map((q: any) => q.close), + } + : null, + '6M': comp.charts[4] + ? { + timestamps: comp.charts[4].quotes.map((q: any) => + q.date.getTime(), + ), + prices: comp.charts[4].quotes.map((q: any) => q.close), + } + : null, + '1Y': comp.charts[5] + ? { + timestamps: comp.charts[5].quotes.map((q: any) => + q.date.getTime(), + ), + prices: comp.charts[5].quotes.map((q: any) => q.close), + } + : null, + MAX: comp.charts[6] + ? { + timestamps: comp.charts[6].quotes.map((q: any) => + q.date.getTime(), + ), + prices: comp.charts[6].quotes.map((q: any) => q.close), + } + : null, + }, + })) + : null, + }; + + return { + type: 'stock', + data: stockData, + }; + } catch (error: any) { + return { + type: 'stock', + data: { + error: `Error fetching stock data: ${error.message || error}`, + ticker: params.ticker, + }, + }; + } + }, +}; + +export default stockWidget; diff --git a/src/lib/agents/search/widgets/weatherWidget.ts b/src/lib/agents/search/widgets/weatherWidget.ts index b9d048c..4b2dcf9 100644 --- a/src/lib/agents/search/widgets/weatherWidget.ts +++ b/src/lib/agents/search/widgets/weatherWidget.ts @@ -20,104 +20,155 @@ const WeatherWidgetSchema = z.object({ ), }); -const weatherWidget = { +const weatherWidget: Widget = { name: 'weather', - description: - 'Provides current weather information for a specified location. It can return details such as temperature, humidity, wind speed, and weather conditions. It needs either a location name or latitude/longitude coordinates to function.', + description: `Provides comprehensive current weather information and forecasts for any location worldwide. Returns real-time weather data including temperature, conditions, humidity, wind, and multi-day forecasts. + +You can set skipSearch to true if the weather widget can fully answer the user's query without needing additional web search. + +**What it provides:** +- Current weather conditions (temperature, feels-like, humidity, precipitation) +- Wind speed, direction, and gusts +- Weather codes/conditions (clear, cloudy, rainy, etc.) +- Hourly forecast for next 24 hours +- Daily forecast for next 7 days (high/low temps, precipitation probability) +- Timezone information + +**When to use:** +- User asks about weather in a location ("weather in X", "is it raining in Y") +- Questions about temperature, conditions, or forecast +- Any weather-related query for a specific place + +**Example call:** +{ + "type": "weather", + "location": "San Francisco, CA, USA", + "lat": 0, + "lon": 0 +} + +**Important:** Provide EITHER a location name OR latitude/longitude coordinates, never both. If using location name, set lat/lon to 0. Location should be specific (city, state/region, country) for best results.`, schema: WeatherWidgetSchema, execute: async (params, _) => { - if ( - params.location === '' && - (params.lat === undefined || params.lon === undefined) - ) { - throw new Error( - 'Either location name or both latitude and longitude must be provided.', - ); - } - - if (params.location !== '') { - const openStreetMapUrl = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(params.location)}&format=json&limit=1`; - - const locationRes = await fetch(openStreetMapUrl, { - headers: { - 'User-Agent': 'Perplexica', - 'Content-Type': 'application/json', - }, - }); - - const data = await locationRes.json(); - - const location = data[0]; - - if (!location) { + try { + if ( + params.location === '' && + (params.lat === undefined || params.lon === undefined) + ) { throw new Error( - `Could not find coordinates for location: ${params.location}`, + 'Either location name or both latitude and longitude must be provided.', ); } - const weatherRes = await fetch( - `https://api.open-meteo.com/v1/forecast?latitude=${location.lat}&longitude=${location.lon}¤t_weather=true`, - { + if (params.location !== '') { + const openStreetMapUrl = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(params.location)}&format=json&limit=1`; + + const locationRes = await fetch(openStreetMapUrl, { headers: { 'User-Agent': 'Perplexica', 'Content-Type': 'application/json', }, - }, - ); + }); - const weatherData = await weatherRes.json(); + const data = await locationRes.json(); + + const location = data[0]; + + if (!location) { + throw new Error( + `Could not find coordinates for location: ${params.location}`, + ); + } + + const weatherRes = await fetch( + `https://api.open-meteo.com/v1/forecast?latitude=${location.lat}&longitude=${location.lon}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,rain,showers,snowfall,weather_code,cloud_cover,pressure_msl,surface_pressure,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,precipitation_probability,precipitation,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max&timezone=auto&forecast_days=7`, + { + headers: { + 'User-Agent': 'Perplexica', + 'Content-Type': 'application/json', + }, + }, + ); + + const weatherData = await weatherRes.json(); + + return { + type: 'weather', + data: { + location: params.location, + latitude: location.lat, + longitude: location.lon, + current: weatherData.current, + hourly: { + time: weatherData.hourly.time.slice(0, 24), + temperature_2m: weatherData.hourly.temperature_2m.slice(0, 24), + precipitation_probability: + weatherData.hourly.precipitation_probability.slice(0, 24), + precipitation: weatherData.hourly.precipitation.slice(0, 24), + weather_code: weatherData.hourly.weather_code.slice(0, 24), + }, + daily: weatherData.daily, + timezone: weatherData.timezone, + }, + }; + } else if (params.lat !== undefined && params.lon !== undefined) { + const [weatherRes, locationRes] = await Promise.all([ + fetch( + `https://api.open-meteo.com/v1/forecast?latitude=${params.lat}&longitude=${params.lon}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,rain,showers,snowfall,weather_code,cloud_cover,pressure_msl,surface_pressure,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,precipitation_probability,precipitation,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max&timezone=auto&forecast_days=7`, + { + headers: { + 'User-Agent': 'Perplexica', + 'Content-Type': 'application/json', + }, + }, + ), + fetch( + `https://nominatim.openstreetmap.org/reverse?lat=${params.lat}&lon=${params.lon}&format=json`, + { + headers: { + 'User-Agent': 'Perplexica', + 'Content-Type': 'application/json', + }, + }, + ), + ]); + + const weatherData = await weatherRes.json(); + const locationData = await locationRes.json(); + + return { + type: 'weather', + data: { + location: locationData.display_name, + latitude: params.lat, + longitude: params.lon, + current: weatherData.current, + hourly: { + time: weatherData.hourly.time.slice(0, 24), + temperature_2m: weatherData.hourly.temperature_2m.slice(0, 24), + precipitation_probability: + weatherData.hourly.precipitation_probability.slice(0, 24), + precipitation: weatherData.hourly.precipitation.slice(0, 24), + weather_code: weatherData.hourly.weather_code.slice(0, 24), + }, + daily: weatherData.daily, + timezone: weatherData.timezone, + }, + }; + } - /* this is like a very simple implementation just to see the bacckend works, when we're working on the frontend, we'll return more data i guess? */ return { type: 'weather', - data: { - location: params.location, - latitude: location.lat, - longitude: location.lon, - weather: weatherData.current_weather, - }, + data: null, }; - } else if (params.lat !== undefined && params.lon !== undefined) { - const [weatherRes, locationRes] = await Promise.all([ - fetch( - `https://api.open-meteo.com/v1/forecast?latitude=${params.lat}&longitude=${params.lon}¤t_weather=true`, - { - headers: { - 'User-Agent': 'Perplexica', - 'Content-Type': 'application/json', - }, - }, - ), - fetch( - `https://nominatim.openstreetmap.org/reverse?lat=${params.lat}&lon=${params.lon}&format=json`, - { - headers: { - 'User-Agent': 'Perplexica', - 'Content-Type': 'application/json', - }, - }, - ), - ]); - - const weatherData = await weatherRes.json(); - const locationData = await locationRes.json(); - + } catch (err) { return { type: 'weather', data: { - location: locationData.display_name, - latitude: params.lat, - longitude: params.lon, - weather: weatherData.current_weather, + error: `Error fetching weather data: ${err}`, }, }; } - - return { - type: 'weather', - data: null, - }; }, -} satisfies Widget; - +}; export default weatherWidget; From 7c9258cfc978e2ec311e99999e45d3599e7a9bbd Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:47:11 +0530 Subject: [PATCH 046/196] feat(intents): update intent prompt --- .../classifier/intents/academicSearch.ts | 45 ++++++++++++++++- .../classifier/intents/discussionSearch.ts | 50 +++++++++++++++++-- .../agents/search/classifier/intents/index.ts | 2 + .../search/classifier/intents/registry.ts | 8 +-- .../search/classifier/intents/webSearch.ts | 24 ++++++++- .../search/classifier/intents/writingTask.ts | 46 ++++++++++++++++- 6 files changed, 163 insertions(+), 12 deletions(-) diff --git a/src/lib/agents/search/classifier/intents/academicSearch.ts b/src/lib/agents/search/classifier/intents/academicSearch.ts index b6da377..199f583 100644 --- a/src/lib/agents/search/classifier/intents/academicSearch.ts +++ b/src/lib/agents/search/classifier/intents/academicSearch.ts @@ -1,9 +1,50 @@ import { Intent } from '../../types'; +const description = `Use this intent to search for scholarly articles, research papers, scientific studies, and academic resources when the user explicitly requests credible, peer-reviewed, or authoritative information from academic sources. + +#### When to use: +1. User explicitly mentions academic keywords: research papers, scientific studies, scholarly articles, peer-reviewed, journal articles. +2. User asks for scientific evidence or academic research on a topic. +3. User needs authoritative, citation-worthy sources for research or academic purposes. + +#### When NOT to use: +1. General questions that don't specifically request academic sources - use 'web_search' instead. +2. User just wants general information without specifying academic sources. +3. Casual queries about facts or current events. + +#### Example use cases: +1. "Find scientific papers on climate change effects" + - User explicitly wants scientific papers. + - Intent: ['academic_search'] with skipSearch: false + +2. "What does the research say about meditation benefits?" + - User is asking for research-based information. + - Intent: ['academic_search', 'web_search'] with skipSearch: false + +3. "Show me peer-reviewed articles on CRISPR technology" + - User specifically wants peer-reviewed academic content. + - Intent: ['academic_search'] with skipSearch: false + +4. "I need scholarly sources about renewable energy for my thesis" + - User explicitly needs scholarly/academic sources. + - Intent: ['academic_search'] with skipSearch: false + +5. "Explain quantum computing" (WRONG to use academic_search alone) + - This is a general question, not specifically requesting academic papers. + - Correct intent: ['web_search'] with skipSearch: false + - Could combine: ['web_search', 'academic_search'] if you want both general and academic sources + +6. "What's the latest study on sleep patterns?" + - User mentions "study" - combine academic and web search for comprehensive results. + - Intent: ['academic_search', 'web_search'] with skipSearch: false + +**IMPORTANT**: This intent can be combined with 'web_search' to provide both academic papers and general web information. Always set skipSearch to false when using this intent. + +**NOTE**: This intent is only available if academic search sources are enabled in the configuration.`; + const academicSearchIntent: Intent = { name: 'academic_search', - description: - 'Use this intent to find scholarly articles, research papers, and academic resources when the user is seeking credible and authoritative information on a specific topic.', + description, requiresSearch: true, enabled: (config) => config.sources.includes('academic'), }; diff --git a/src/lib/agents/search/classifier/intents/discussionSearch.ts b/src/lib/agents/search/classifier/intents/discussionSearch.ts index 76b3b01..b7e2cfd 100644 --- a/src/lib/agents/search/classifier/intents/discussionSearch.ts +++ b/src/lib/agents/search/classifier/intents/discussionSearch.ts @@ -1,9 +1,53 @@ import { Intent } from '../../types'; +const description = `Use this intent to search through discussion forums, community boards, and social platforms (Reddit, forums, etc.) when the user explicitly wants opinions, personal experiences, community discussions, or crowd-sourced information. + +#### When to use: +1. User explicitly mentions: Reddit, forums, discussion boards, community opinions, "what do people think", "user experiences". +2. User is asking for opinions, reviews, or personal experiences about a product, service, or topic. +3. User wants to know what communities or people are saying about something. + +#### When NOT to use: +1. General questions that don't specifically ask for opinions or discussions - use 'web_search' instead. +2. User wants factual information or official sources. +3. Casual queries about facts, news, or current events without requesting community input. + +#### Example use cases: +1. "What do people on Reddit think about the new iPhone?" + - User explicitly wants Reddit/community opinions. + - Intent: ['discussions_search'] with skipSearch: false + +2. "User experiences with Tesla Model 3" + - User is asking for personal experiences from users. + - Intent: ['discussions_search'] with skipSearch: false + +3. "Best gaming laptop according to forums" + - User wants forum/community recommendations. + - Intent: ['discussions_search'] with skipSearch: false + +4. "What are people saying about the new AI regulations?" + - User wants community discussions/opinions. + - Intent: ['discussions_search', 'web_search'] with skipSearch: false + +5. "Reviews and user opinions on the Framework laptop" + - Combines user opinions with general reviews. + - Intent: ['discussions_search', 'web_search'] with skipSearch: false + +6. "What's the price of iPhone 15?" (WRONG to use discussions_search) + - This is a factual question, not asking for opinions. + - Correct intent: ['web_search'] with skipSearch: false + +7. "Explain how OAuth works" (WRONG to use discussions_search) + - This is asking for information, not community opinions. + - Correct intent: ['web_search'] with skipSearch: false + +**IMPORTANT**: This intent can be combined with 'web_search' to provide both community discussions and official/factual information. Always set skipSearch to false when using this intent. + +**NOTE**: This intent is only available if discussion search sources are enabled in the configuration.`; + const discussionSearchIntent: Intent = { - name: 'discussion_search', - description: - 'Use this intent to search through discussion forums, community boards, or social media platforms when the user is looking for opinions, experiences, or community-driven information on a specific topic.', + name: 'discussions_search', + description, requiresSearch: true, enabled: (config) => config.sources.includes('discussions'), }; diff --git a/src/lib/agents/search/classifier/intents/index.ts b/src/lib/agents/search/classifier/intents/index.ts index feefd2d..fcab1c7 100644 --- a/src/lib/agents/search/classifier/intents/index.ts +++ b/src/lib/agents/search/classifier/intents/index.ts @@ -1,5 +1,6 @@ import academicSearchIntent from './academicSearch'; import discussionSearchIntent from './discussionSearch'; +import privateSearchIntent from './privateSearch'; import IntentRegistry from './registry'; import webSearchIntent from './webSearch'; import widgetResponseIntent from './widgetResponse'; @@ -10,5 +11,6 @@ IntentRegistry.register(academicSearchIntent); IntentRegistry.register(discussionSearchIntent); IntentRegistry.register(widgetResponseIntent); IntentRegistry.register(writingTaskIntent); +IntentRegistry.register(privateSearchIntent); export { IntentRegistry }; diff --git a/src/lib/agents/search/classifier/intents/registry.ts b/src/lib/agents/search/classifier/intents/registry.ts index bc3464b..4efdbc4 100644 --- a/src/lib/agents/search/classifier/intents/registry.ts +++ b/src/lib/agents/search/classifier/intents/registry.ts @@ -18,10 +18,12 @@ class IntentRegistry { } static getDescriptions(config: { sources: SearchSources[] }): string { - const availableintnets = this.getAvailableIntents(config); + const availableintents = this.getAvailableIntents(config); - return availableintnets - .map((intent) => `${intent.name}: ${intent.description}`) + return availableintents + .map( + (intent) => `-------\n\n###${intent.name}: ${intent.description}\n\n`, + ) .join('\n\n'); } } diff --git a/src/lib/agents/search/classifier/intents/webSearch.ts b/src/lib/agents/search/classifier/intents/webSearch.ts index 9fccd2f..3f795e4 100644 --- a/src/lib/agents/search/classifier/intents/webSearch.ts +++ b/src/lib/agents/search/classifier/intents/webSearch.ts @@ -1,9 +1,29 @@ import { Intent } from '../../types'; +const description = ` +Use this intent to find current information from the web when the user is asking a question or needs up-to-date information that cannot be provided by widgets or other intents. + +#### When to use: +1. Simple user questions about current events, news, weather, or general knowledge that require the latest information and there is no specific better intent to use. +2. When the user explicitly requests information from the web or indicates they want the most recent data (and still there's no other better intent). +3. When no widgets can fully satisfy the user's request for information nor any other specialized search intent applies. + +#### Examples use cases: +1. "What is the weather in San Francisco today? ALso tell me some popular events happening there this weekend." + - In this case, the weather widget can provide the current weather, but for popular events, a web search is needed. So the intent should include a 'web_search' & a 'widget_response'. + +2. "Who won the Oscar for Best Picture in 2024?" + - This is a straightforward question that requires current information from the web. + +3. "Give me the latest news on AI regulations." + - The user is asking for up-to-date news, which necessitates a web search. + +**IMPORTANT**: If this intent is given then skip search should be false. +`; + const webSearchIntent: Intent = { name: 'web_search', - description: - 'Use this intent to find current information from the web when the user is asking a question or needs up-to-date information that cannot be provided by widgets or other intents.', + description: description, requiresSearch: true, enabled: (config) => config.sources.includes('web'), }; diff --git a/src/lib/agents/search/classifier/intents/writingTask.ts b/src/lib/agents/search/classifier/intents/writingTask.ts index 95b5af6..3edc0b3 100644 --- a/src/lib/agents/search/classifier/intents/writingTask.ts +++ b/src/lib/agents/search/classifier/intents/writingTask.ts @@ -1,9 +1,51 @@ import { Intent } from '../../types'; +const description = `Use this intent for simple writing or greeting tasks that do not require any external information or facts. This is ONLY for greetings and straightforward creative writing that needs no factual verification. + +#### When to use: +1. User greetings or simple social interactions (hello, hi, thanks, goodbye). +2. Creative writing tasks that require NO factual information (poems, birthday messages, thank you notes). +3. Simple drafting tasks where the user provides all necessary information. + +#### When NOT to use: +1. ANY question that starts with "what", "how", "why", "when", "where", "who" - these need web_search. +2. Requests for explanations, definitions, or information about anything. +3. Code-related questions or technical help - these need web_search. +4. Writing tasks that require facts, data, or current information. +5. When you're uncertain about any information needed - default to web_search. + +#### Example use cases: +1. "Hello" or "Hi there" + - Simple greeting, no information needed. + - Intent: ['writing_task'] with skipSearch: true + +2. "Write me a birthday message for my friend" + - Creative writing, no facts needed. + - Intent: ['writing_task'] with skipSearch: true + +3. "Draft a thank you email for a job interview" + - Simple writing task, no external information required. + - Intent: ['writing_task'] with skipSearch: true + +4. "What is React?" (WRONG to use writing_task) + - This is a QUESTION asking for information. + - Correct intent: ['web_search'] with skipSearch: false + +5. "How do I fix this error in Python?" (WRONG to use writing_task) + - This is asking for technical help. + - Correct intent: ['web_search'] with skipSearch: false + +6. "Write an email about the latest AI developments" (WRONG to use writing_task alone) + - This requires current information about AI developments. + - Correct intent: ['web_search'] with skipSearch: false + +**CRITICAL RULE**: When in doubt, DO NOT use this intent. Default to web_search. This intent should be rare - only use it for greetings and purely creative writing tasks that need absolutely no facts or information. + +**IMPORTANT**: If this intent is used alone, skipSearch should be true. Never combine this with other search intents unless you're absolutely certain both are needed.`; + const writingTaskIntent: Intent = { name: 'writing_task', - description: - 'Use this intent to assist users with writing tasks such as drafting emails, creating documents, or generating content based on their instructions or greetings.', + description, requiresSearch: false, enabled: (config) => true, }; From 730ee0ff4173355368838c82c900e9bfa8b70f20 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:47:18 +0530 Subject: [PATCH 047/196] feat(intents): add private search --- .../classifier/intents/privateSearch.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/lib/agents/search/classifier/intents/privateSearch.ts diff --git a/src/lib/agents/search/classifier/intents/privateSearch.ts b/src/lib/agents/search/classifier/intents/privateSearch.ts new file mode 100644 index 0000000..8ebc0df --- /dev/null +++ b/src/lib/agents/search/classifier/intents/privateSearch.ts @@ -0,0 +1,47 @@ +import { Intent } from '../../types'; + +const description = `Use this intent to search through the user's uploaded documents or provided web page links when they ask questions about their personal files or specific URLs. + +#### When to use: +1. User explicitly asks about uploaded documents ("tell me about the document I uploaded", "summarize this file"). +2. User provides specific URLs/links and asks questions about them ("tell me about example.com", "what's on this page: url.com"). +3. User references "my documents", "the file I shared", "this link" when files or URLs are available. + +#### When NOT to use: +1. User asks generic questions like "summarize" without providing context or files (later the system will ask what they want summarized). +2. No files have been uploaded and no URLs provided - use web_search or other intents instead. +3. User is asking general questions unrelated to their uploaded content. + +#### Example use cases: +1. "Tell me about the PDF I uploaded" + - Files are uploaded, user wants information from them. + - Intent: ['private_search'] with skipSearch: false + +2. "What's the main point from example.com?" + - User provided a specific URL to analyze. + - Intent: ['private_search'] with skipSearch: false + +3. "Summarize the research paper I shared" + - User references a shared document. + - Intent: ['private_search'] with skipSearch: false + +4. "Summarize" (WRONG to use private_search if no files/URLs) + - No context provided, no files uploaded. + - Correct: Skip this intent, let the answer agent ask what to summarize + +5. "What does my document say about climate change and also search the web for recent updates?" + - Combine private document search with web search. + - Intent: ['private_search', 'web_search'] with skipSearch: false + +**IMPORTANT**: Only use this intent if files are actually uploaded or URLs are explicitly provided in the query. Check the context for uploaded files before selecting this intent. Always set skipSearch to false when using this intent. + +**NOTE**: This intent can be combined with other search intents when the user wants both personal document information and external sources.`; + +const privateSearchIntent: Intent = { + name: 'private_search', + description, + enabled: (config) => true, + requiresSearch: true, +}; + +export default privateSearchIntent; From 8dec689a451ae7698bee42d8cc898b9c7e8ceb5f Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:47:28 +0530 Subject: [PATCH 048/196] feat(prompts): update classifier prompt --- src/lib/prompts/search/classifier.ts | 298 +++++++++++++++------------ 1 file changed, 162 insertions(+), 136 deletions(-) diff --git a/src/lib/prompts/search/classifier.ts b/src/lib/prompts/search/classifier.ts index 40e3203..9301b84 100644 --- a/src/lib/prompts/search/classifier.ts +++ b/src/lib/prompts/search/classifier.ts @@ -4,7 +4,7 @@ export const getClassifierPrompt = (input: { }) => { return ` -You are an expert query classifier for an intelligent search agent. Your task is to analyze user queries and determine the optimal way to answer them—selecting the right intent(s) and widgets. +You are an expert query classifier for an AI-powered search engine. Your task is to analyze user queries and determine the optimal strategy to answer them—selecting the right search intent(s) and widgets that will render in the UI. @@ -12,165 +12,191 @@ Given a conversation history and follow-up question, you must: 1. Determine if search should be skipped (skipSearch: boolean) 2. Generate a standalone, self-contained version of the question (standaloneFollowUp: string) 3. Identify the intent(s) that describe how to fulfill the query (intent: array) -4. Select appropriate widgets (widgets: array) +4. Select appropriate widgets that will enhance the UI response (widgets: array) - -**THE MOST IMPORTANT RULE**: skipSearch should be TRUE only in TWO cases: -1. Widget-only queries (weather, stocks, calculator) -2. Greetings or simple writing tasks (NOT questions) +## Understanding Your Tools -**DEFAULT TO skipSearch: false** for everything else, including: -- Any question ("what is", "how does", "explain", "tell me about") -- Any request for information or facts -- Anything you're unsure about +**Intents** define HOW to find or generate information: +- Different search methods: web search, forum discussions, academic papers, personal documents +- Generation methods: direct response for greetings, creative writing +- Each intent represents a different approach to answering the query +- Multiple intents can be combined for comprehensive answers -Ask yourself: "Is the user ASKING about something or requesting INFORMATION?" -- YES → skipSearch: false (use web_search) -- NO (just greeting or simple writing) → skipSearch: true - +**Widgets** are UI components that render structured, real-time data: +- They display specific types of information (weather forecasts, calculations, stock prices, etc.) +- They provide interactive, visual elements that enhance the text response +- They fetch data independently and render directly in the interface +- They can work alone (widget-only answers) or alongside search results + +**Key distinction:** Intents determine the search/generation strategy, while widgets provide visual data enhancements in the UI. + +## The Philosophy of skipSearch + +Search connects you to external knowledge sources. Skip it only when external knowledge isn't needed. + +**Skip search (TRUE) when:** +- Widgets alone can fully answer the query with their structured data +- Simple greetings or social pleasantries +- Pure creative writing requiring absolutely zero facts + +**Use search (FALSE) when:** +- User is asking a question (what, how, why, when, where, who) +- Any facts, explanations, or information are requested +- Technical help, code, or learning content is needed +- Current events, news, or time-sensitive information required +- Widgets provide partial data but context/explanation needed +- Uncertain - always default to searching + +**Critical rule:** If the user is ASKING about something or requesting INFORMATION, they need search. Question words (what, how, why, explain, tell me) strongly indicate skipSearch should be FALSE. + +## How Intents Work + +Available intent options: +${input.intentDesc} + +**Understanding intent descriptions:** +- Each intent description explains what it does and when to use it +- Read the descriptions carefully to understand their purpose +- Match user needs to the appropriate intent(s) +- Can select multiple intents for comprehensive coverage + +**Selection strategy:** +1. Identify what the user is asking for +2. Review intent descriptions to find matches +3. Select all relevant intents (can combine multiple) +4. If user explicitly mentions a source (Reddit, research papers), use that specific intent +5. Default to general web search for broad questions + +## How Widgets Work + +Available widget options: +${input.widgetDesc} + +**Understanding widget descriptions:** +- Each widget description explains what data it provides and how to use it +- Widgets render as UI components alongside the text response +- They enhance answers with visual, structured information +- Review descriptions to identify applicable widgets + +**Selection strategy:** +1. Identify if query needs any structured/real-time data +2. Check widget descriptions for matches +3. Include ALL applicable widgets (each type only once) +4. Widgets work independently - include them even when also searching + +**Important widget behaviors:** +- If widget fully answers query → skipSearch: TRUE, include widget, use widget_response intent +- If widget provides partial data → skipSearch: FALSE, include widget + appropriate search intent(s) +- Widgets and search intents coexist - they serve different purposes + +## Making Queries Standalone + +Transform follow-up questions to be understandable without conversation history: + +**Replace vague references:** +- "it", "that", "this" → specific subjects from context +- "they", "those" → actual entities being discussed +- "the previous one" → the actual item from history + +**Add necessary context:** +- Include the topic being discussed +- Reference specific subjects mentioned earlier +- Preserve original meaning and scope +- Don't over-elaborate or change intent + +**Example transformations:** +- Context: Discussing React framework +- Follow-up: "How does it work?" → Standalone: "How does React work?" +- Follow-up: "What about hooks?" → Standalone: "What about React hooks?" + +## Critical Decision Framework - Follow this decision tree IN ORDER: -1. **Widget-Only Queries** → skipSearch: TRUE, intent: ['widget_response'] - - Weather queries: "weather in NYC", "temperature in Paris", "is it raining in Seattle" - - Stock queries: "AAPL stock price", "how is Tesla doing", "MSFT stock" - - Calculator queries: "what is 25% of 80", "calculate 15*23", "sqrt(144)" - - These are COMPLETE answers—no search needed +### 1. Widget-Only Queries +**When:** Query can be fully answered by widget data alone +**Then:** skipSearch: TRUE, intent: ['widget_response'], include widget(s) +**Pattern:** Weather requests, calculations, unit conversions, stock prices (when no additional info needed) -2. **Writing/Greeting Tasks** → skipSearch: TRUE, intent: ['writing_task'] - - ONLY for greetings and simple writing: - - Greetings: "hello", "hi", "how are you", "thanks", "goodbye" - - Simple writing needing NO facts: "write a thank you email", "draft a birthday message", "compose a poem" - - NEVER for: questions, "what is X", "how does X work", explanations, definitions, facts, code help - - If user is ASKING about something (not requesting writing), use web_search +### 2. Greeting/Simple Writing Tasks +**When:** Just greetings OR pure creative writing with zero factual requirements +**Then:** skipSearch: TRUE, intent: ['writing_task'] +**Pattern:** "hello", "hi", "write a birthday message", "compose a poem" +**NEVER for:** Questions, explanations, definitions, facts, code help -3. **Image Display Queries** → skipSearch: FALSE, intent: ['image_preview'] - - "Show me images of cats" - - "Pictures of the Eiffel Tower" - - "Visual examples of modern architecture" - - Requests for images to visualize something +### 3. Widget + Additional Information +**When:** Widget provides data but user wants more context/explanation +**Then:** skipSearch: FALSE, intent: ['appropriate_search', 'widget_response'], include widget(s) +**Pattern:** "weather in NYC and things to do", "AAPL stock and recent news" -4. **Widget + Additional Info** → skipSearch: FALSE, intent: ['web_search', 'widget_response'] - - "weather in NYC and best things to do there" - - "AAPL stock and recent Apple news" - - "calculate my mortgage and explain how interest works" +### 4. Pure Search Queries +**When:** No widgets apply, just information/facts needed +**Then:** skipSearch: FALSE, select appropriate search intent(s) +**Strategy:** +- Default to general web search +- Use discussion search when user mentions Reddit, forums, opinions +- Use academic search when user mentions research, papers, studies +- Use private search when user references uploaded files/URLs +- Can combine multiple search intents -5. **Pure Search Queries** → skipSearch: FALSE - - Default to web_search for general questions - - Use discussions_search when user explicitly mentions Reddit, forums, opinions, experiences - - Use academic_search when user explicitly mentions research, papers, studies, scientific - - Can combine multiple search intents when appropriate +### 5. Think Before Setting skipSearch to TRUE +**Ask yourself:** +- Is the user ASKING about something? → FALSE +- Is the user requesting INFORMATION? → FALSE +- Is there ANY factual component? → FALSE +- Am I uncertain? → FALSE (default to search) -6. **Fallback when web_search unavailable** → skipSearch: TRUE, intent: ['writing_task'] or [] - - If no search intents are available and no widgets apply - - Set skipSearch to true and use writing_task or empty intent - +## Intent Selection Rules - -Example 1: Widget-only query -Query: "What is the weather in New York?" -Reasoning: User wants current weather → weather widget provides this completely -Output: skipSearch: true, intent: ['widget_response'], widgets: [weather widget for New York] - -Example 2: Widget-only query -Query: "AAPL stock price" -Reasoning: User wants stock price → stock_ticker widget provides this completely -Output: skipSearch: true, intent: ['widget_response'], widgets: [stock_ticker for AAPL] - -Example 3: Widget + search query -Query: "What's the weather in NYC and what are some good outdoor activities?" -Reasoning: Weather widget handles weather, but outdoor activities need web search -Output: skipSearch: false, intent: ['web_search', 'widget_response'], widgets: [weather widget for NYC] - -Example 4: Pure search query -Query: "What are the latest developments in AI?" -Reasoning: No widget applies, needs current web information -Output: skipSearch: false, intent: ['web_search'], widgets: [] - -Example 5: Writing task (greeting/simple writing only) -Query: "Write me a thank you email for a job interview" -Reasoning: Simple writing task needing no external facts → writing_task -Output: skipSearch: true, intent: ['writing_task'], widgets: [] - -Example 5b: Question about something - ALWAYS needs search -Query: "What is Kimi K2?" -Reasoning: User is ASKING about something → needs web search for accurate info -Output: skipSearch: false, intent: ['web_search'], widgets: [] - -Example 5c: Another question - needs search -Query: "Explain how photosynthesis works" -Reasoning: User is ASKING for explanation → needs web search -Output: skipSearch: false, intent: ['web_search'], widgets: [] - -Example 6: Image display -Query: "Show me images of cats" -Reasoning: User wants to see images → requires image search -Output: skipSearch: false, intent: ['image_preview'], widgets: [] - -Example 7: Multiple search sources -Query: "What does the research say about meditation benefits?" -Reasoning: Benefits from both academic papers and web articles -Output: skipSearch: false, intent: ['academic_search', 'web_search'], widgets: [] - -Example 8: Discussions search -Query: "What do people on Reddit think about the new iPhone?" -Reasoning: User explicitly wants forum/community opinions → discussions_search -Output: skipSearch: false, intent: ['discussions_search'], widgets: [] - -Example 9: Academic search only -Query: "Find scientific papers on climate change effects" -Reasoning: User explicitly wants academic/research papers -Output: skipSearch: false, intent: ['academic_search'], widgets: [] - - - -Transform the follow-up into a self-contained question: -- Include ALL necessary context from chat history -- Replace pronouns (it, they, this, that) with specific nouns -- Replace references ("the previous one", "what you mentioned") with actual content -- Preserve the original complexity—don't over-elaborate simple questions -- The question should be answerable without seeing the conversation - - - Available intents: ${input.intentDesc} -Rules: +**Rules:** - Include at least one intent when applicable -- For questions/information requests: - - Default to web_search unless user explicitly requests another source - - Use discussions_search when user mentions: Reddit, forums, opinions, experiences, "what do people think" - - Use academic_search when user mentions: research, papers, studies, scientific, scholarly - - Can combine intents (e.g., ['academic_search', 'web_search']) -- If web_search is NOT in available intents and query needs search: - - Check if discussions_search or academic_search applies - - If no search intent available and no widgets: use writing_task or empty array [] -- private_search: ONLY when user provides specific URLs/documents -- widget_response: when widgets fully answer the query -- writing_task: ONLY for greetings and simple writing (never for questions) - +- For information requests: default to general web search unless user specifies otherwise +- Use specialized search intents when explicitly requested (discussions, academic, private) +- Can combine multiple intents: ['academic_search', 'web_search'] +- widget_response: when widgets fully satisfy the query +- writing_task: ONLY for greetings and simple creative writing (never for questions) + +## Widget Selection Rules - Available widgets: ${input.widgetDesc} -Rules: +**Rules:** - Include ALL applicable widgets regardless of skipSearch value -- Each widget type can only be included once -- Widgets provide structured, real-time data that enhances any response - +- Each widget type can only be included once per query +- Widgets render in the UI to enhance responses with structured data +- Follow widget descriptions for proper parameter formatting - -Your classification must be precise and consistent: +## Output Format + +Your classification must be valid JSON: +\`\`\`json { "skipSearch": , - "standaloneFollowUp": "", - "intent": [], - "widgets": [] + "standaloneFollowUp": "", + "intent": ["", ""], + "widgets": [ + { + "type": "", + "": "", + "": "" + } + ] } - - `; +\`\`\` + +## Final Reminders + +- **Intents** = HOW to answer (search strategy, generation type) +- **Widgets** = WHAT to display in UI (structured visual data) +- **skipSearch** = Can answer without external search? (widgets alone, greetings, pure creativity) +- **Default to FALSE** = When uncertain, search - better to search unnecessarily than miss information +- **Read descriptions** = Intent and widget descriptions contain all the information you need to select them properly + +Your goal is to understand user intent and route requests through the optimal combination of search methods (intents) and UI enhancements (widgets). Pay close attention to what the user is actually asking for, not just pattern matching keywords. +`; }; From f15802b6889cd9f120bf17193a374c8c0b7779f4 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:48:05 +0530 Subject: [PATCH 049/196] feat(prompts): update research prompt --- src/lib/prompts/search/researcher.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/lib/prompts/search/researcher.ts b/src/lib/prompts/search/researcher.ts index 2c58b9b..f77559f 100644 --- a/src/lib/prompts/search/researcher.ts +++ b/src/lib/prompts/search/researcher.ts @@ -1,6 +1,8 @@ export const getResearcherPrompt = ( actionDesc: string, - mode: 'fast' | 'balanced' | 'deep_research', + mode: 'speed' | 'balanced' | 'quality', + i: number, + maxIteration: number, ) => { const today = new Date().toLocaleDateString('en-US', { year: 'numeric', @@ -10,17 +12,20 @@ export const getResearcherPrompt = ( return ` You are an action orchestrator. Your job is to fulfill user requests by selecting and executing appropriate actions - whether that's searching for information, creating calendar events, sending emails, or any other available action. +You will be shared with the conversation history between user and AI, along with the user's latest follow-up question and your previous actions' results (if any. Note that they're per conversation so if they contain any previous actions it was executed for the last follow up (the one you're currently handling)). Based on this, you must decide the best next action(s) to take to fulfill the user's request. Today's date: ${today} You are operating in "${mode}" mode. ${ - mode === 'fast' + mode === 'speed' ? 'Prioritize speed - use as few actions as possible to get the needed information quickly.' : mode === 'balanced' ? 'Balance speed and depth - use a moderate number of actions to get good information efficiently. Never stop at the first action unless there is no action available or the query is simple.' : 'Conduct deep research - use multiple actions to gather comprehensive information, even if it takes longer.' } +You are currently on iteration ${i + 1} of your research process and have ${maxIteration} total iterations so please take action accordingly. After max iterations, the done action would get called automatically so you don't have to worry about that unless you want to end the research early. + ${actionDesc} @@ -236,6 +241,15 @@ Actions: web_search ["best Italian restaurant near me", "top rated Italian resta Reasoning should be 2-3 natural sentences showing your thought process and plan. Then select and configure the appropriate action(s). + +Always respond in the following JSON format and never deviate from it or output any extra text: +{ + "reasoning": "", + "actions": [ + {"type": "", "param1": "value1", "...": "..."}, + ... + ] +} `; }; From 1b4e883f574bbdfb931827eb5f67024259498b7f Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:48:12 +0530 Subject: [PATCH 050/196] feat(prompts): add writer prompt --- src/lib/prompts/search/writer.ts | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/lib/prompts/search/writer.ts diff --git a/src/lib/prompts/search/writer.ts b/src/lib/prompts/search/writer.ts new file mode 100644 index 0000000..69a99c3 --- /dev/null +++ b/src/lib/prompts/search/writer.ts @@ -0,0 +1,58 @@ +export const getWriterPrompt = (context: string) => { + return ` + You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses. + + Your task is to provide answers that are: + - **Informative and relevant**: Thoroughly address the user's query using the given context. + - **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically. + - **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights. + - **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included. + - **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable. + + ### Formatting Instructions + - **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate. + - **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience. + - **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability. + - **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience. + - **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title. + - **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate. + - **No references or source list at the end**: Do not include a seperate references or sources section at the end of your response. All references are sent to user by the system automatically. + - **Do not give the mapping of citations to sources**: Only use the [number] notation in the text. Never return the mapping of citations to sources. + + ### Citation Requirements + - Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`. + - Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]." + - Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context. + - Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]." + - Always prioritize credibility and accuracy by linking all statements back to their respective context sources. + - Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation. + - Avoid citing widget data but you can use it to directly answer without citation. + - Never return the mapping of citations to sources; only use the [number] notation in the text. Never return a references or sources section seperately. + + ### Widget Data Usage + - Widget data provided in the context can be used directly to answer specific queries (e.g., current weather, stock prices, calculations) without citations. + - The widget data is already displayed to the user in a beautiful format (via cards, tables, etc) just before your response so you don't need to generate a very detailed response for widget data. Provide concise answers for such queries. + - You can also mention that for more information you can look at the widget displayed above. + - For weather data, only provide current weather conditions not forecasts unless explicitly asked for forecasts by the user. + - You don't need to cite widget data you can directly use it to answer the user query. NEVER CITE widget OR (any other notation) TO CITE WIDGET DATA. + + ### Special Instructions + - If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity. + - 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. + - If its a simple query (like weather, calculations, definitions), provide concise answers and not a long article. + + ### 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. + - Provide explanations or historical context as needed to enhance understanding. + - End with a conclusion or overall perspective if relevant. + - For simpler queries like weather, calculations, or definitions, provide concise answers and not a long article. + + + ${context} + + + Current date & time in ISO format (UTC timezone) is: ${new Date().toISOString()}. + `; +}; From ec06a2b9ff0aaa86b5092443a07e753e04292149 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:48:44 +0530 Subject: [PATCH 051/196] feat(researcher): use patching, streaming --- src/lib/agents/search/researcher/index.ts | 170 ++++++++++++++++++---- 1 file changed, 144 insertions(+), 26 deletions(-) diff --git a/src/lib/agents/search/researcher/index.ts b/src/lib/agents/search/researcher/index.ts index 300de72..3eb9478 100644 --- a/src/lib/agents/search/researcher/index.ts +++ b/src/lib/agents/search/researcher/index.ts @@ -8,6 +8,8 @@ import { import { ActionRegistry } from './actions'; import { getResearcherPrompt } from '@/lib/prompts/search/researcher'; import SessionManager from '@/lib/session'; +import { ReasoningResearchBlock } from '@/lib/types'; +import formatChatHistoryAsString from '@/lib/utils/formatHistory'; class Researcher { async research( @@ -17,7 +19,7 @@ class Researcher { let findings: string = ''; let actionOutput: ActionOutput[] = []; let maxIteration = - input.config.mode === 'fast' + input.config.mode === 'speed' ? 1 : input.config.mode === 'balanced' ? 3 @@ -41,45 +43,130 @@ class Researcher { classification: input.classification, }); - for (let i = 0; i < maxIteration; i++) { - const researcherPrompt = getResearcherPrompt(availableActionsDescription); + const researchBlockId = crypto.randomUUID(); - const res = await input.config.llm.generateObject>( - { - messages: [ - { - role: 'system', - content: researcherPrompt, - }, - { - role: 'user', - content: ` - - ${input.classification.standaloneFollowUp} - + session.emitBlock({ + id: researchBlockId, + type: 'research', + data: { + subSteps: [], + }, + }); + + for (let i = 0; i < maxIteration; i++) { + const researcherPrompt = getResearcherPrompt( + availableActionsDescription, + input.config.mode, + i, + maxIteration, + ); + + const actionStream = input.config.llm.streamObject< + z.infer + >({ + messages: [ + { + role: 'system', + content: researcherPrompt, + }, + { + role: 'user', + content: ` + + ${formatChatHistoryAsString(input.chatHistory.slice(-10))} + User: ${input.followUp} (Standalone question: ${input.classification.standaloneFollowUp}) + ${findings} `, - }, - ], - schema, - }, - ); + }, + ], + schema, + }); + const block = session.getBlock(researchBlockId); - if (res.action.type === 'done') { - console.log('Research complete - "done" action selected'); + let reasoningEmitted = false; + let reasoningId = crypto.randomUUID(); + + let finalActionRes: any; + + for await (const partialRes of actionStream) { + try { + if ( + partialRes.reasoning && + !reasoningEmitted && + block && + block.type === 'research' + ) { + reasoningEmitted = true; + block.data.subSteps.push({ + id: reasoningId, + type: 'reasoning', + reasoning: partialRes.reasoning, + }); + session.updateBlock(researchBlockId, [ + { + op: 'replace', + path: '/data/subSteps', + value: block.data.subSteps, + }, + ]); + } else if ( + partialRes.reasoning && + reasoningEmitted && + block && + block.type === 'research' + ) { + const subStepIndex = block.data.subSteps.findIndex( + (step: any) => step.id === reasoningId, + ); + if (subStepIndex !== -1) { + const subStep = block.data.subSteps[ + subStepIndex + ] as ReasoningResearchBlock; + subStep.reasoning = partialRes.reasoning; + session.updateBlock(researchBlockId, [ + { + op: 'replace', + path: '/data/subSteps', + value: block.data.subSteps, + }, + ]); + } + } + + finalActionRes = partialRes; + } catch (e) { + // nothing + } + } + + if (finalActionRes.action.type === 'done') { break; } const actionConfig: ActionConfig = { - type: res.action.type as string, - params: res.action, + type: finalActionRes.action.type as string, + params: finalActionRes.action, }; - findings += 'Reasoning: ' + res.reasoning + '\n'; + const queries = actionConfig.params.queries || []; + if (block && block.type === 'research') { + block.data.subSteps.push({ + id: crypto.randomUUID(), + type: 'searching', + searching: queries, + }); + session.updateBlock(researchBlockId, [ + { op: 'replace', path: '/data/subSteps', value: block.data.subSteps }, + ]); + } + + findings += `\n---\nIteration ${i + 1}:\n`; + findings += 'Reasoning: ' + finalActionRes.reasoning + '\n'; findings += `Executing Action: ${actionConfig.type} with params ${JSON.stringify(actionConfig.params)}\n`; const actionResult = await ActionRegistry.execute( @@ -95,6 +182,21 @@ class Researcher { actionOutput.push(actionResult); if (actionResult.type === 'search_results') { + if (block && block.type === 'research') { + block.data.subSteps.push({ + id: crypto.randomUUID(), + type: 'reading', + reading: actionResult.results, + }); + session.updateBlock(researchBlockId, [ + { + op: 'replace', + path: '/data/subSteps', + value: block.data.subSteps, + }, + ]); + } + findings += actionResult.results .map( (r) => @@ -102,8 +204,24 @@ class Researcher { ) .join('\n'); } + + findings += '\n---------\n'; } + const searchResults = actionOutput.filter( + (a) => a.type === 'search_results', + ); + + session.emit('data', { + type: 'sources', + data: searchResults + .flatMap((a) => a.results) + .map((r) => ({ + content: r.content, + metadata: r.metadata, + })), + }); + return { findings: actionOutput, }; From cba3f43b19bbda5ecb6b996b06556bc5d49713ac Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:49:36 +0530 Subject: [PATCH 052/196] feat(search-agent): add search agent flow --- src/lib/agents/search/index.ts | 67 +++++++++++++++++-- .../search/researcher/actions/webSearch.ts | 5 +- src/lib/agents/search/types.ts | 2 +- 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/src/lib/agents/search/index.ts b/src/lib/agents/search/index.ts index 141ea7f..bacdb88 100644 --- a/src/lib/agents/search/index.ts +++ b/src/lib/agents/search/index.ts @@ -3,6 +3,8 @@ import SessionManager from '@/lib/session'; import Classifier from './classifier'; import { WidgetRegistry } from './widgets'; import Researcher from './researcher'; +import { getWriterPrompt } from '@/lib/prompts/search/writer'; +import fs from 'fs'; class SearchAgent { async searchAsync(session: SessionManager, input: SearchAgentInput) { @@ -15,15 +17,22 @@ class SearchAgent { llm: input.config.llm, }); - session.emit('data', { - type: 'classification', - classification: classification, - }); - const widgetPromise = WidgetRegistry.executeAll(classification.widgets, { llm: input.config.llm, embedding: input.config.embedding, session: session, + }).then((widgetOutputs) => { + widgetOutputs.forEach((o) => { + session.emitBlock({ + id: crypto.randomUUID(), + type: 'widget', + data: { + widgetType: o.type, + params: o.data, + }, + }); + }); + return widgetOutputs; }); let searchPromise: Promise | null = null; @@ -42,6 +51,54 @@ class SearchAgent { widgetPromise, searchPromise, ]); + + session.emit('data', { + type: 'researchComplete', + }); + + const finalContext = + searchResults?.findings + .filter((f) => f.type === 'search_results') + .flatMap((f) => f.results) + .map((f) => `${f.metadata.title}: ${f.content}`) + .join('\n') || ''; + + const widgetContext = widgetOutputs + .map((o) => { + return `${o.type}: ${JSON.stringify(o.data)}`; + }) + .join('\n-------------\n'); + + const finalContextWithWidgets = `${finalContext}\n${widgetContext}`; + + const writerPrompt = getWriterPrompt(finalContextWithWidgets); + + const answerStream = input.config.llm.streamText({ + messages: [ + { + role: 'system', + content: writerPrompt, + }, + ...input.chatHistory, + { + role: 'user', + content: input.followUp, + }, + ], + }); + + let accumulatedText = ''; + + for await (const chunk of answerStream) { + accumulatedText += chunk.contentChunk; + + session.emit('data', { + type: 'response', + data: chunk.contentChunk, + }); + } + + session.emit('end', {}); } } diff --git a/src/lib/agents/search/researcher/actions/webSearch.ts b/src/lib/agents/search/researcher/actions/webSearch.ts index 5ceb2ed..e5ffdd3 100644 --- a/src/lib/agents/search/researcher/actions/webSearch.ts +++ b/src/lib/agents/search/researcher/actions/webSearch.ts @@ -15,9 +15,9 @@ You have to use this action aggressively to find relevant information from the w When this action is present, you must use it to obtain current information from the web. ### How to use: -1. For fast search mode, you can use this action once. Make sure to cover all aspects of the user's query in that single search. +1. For speed search mode, you can use this action once. Make sure to cover all aspects of the user's query in that single search. 2. If you're on quality mode, you'll get to use this action up to two times. Use the first search to gather general information, and the second search to fill in any gaps or get more specific details based on the initial findings. -3. If you're set on Deep research mode, then you will get to use this action multiple times to gather more information. Use your judgment to decide when additional searches are necessary to provide a thorough and accurate response. +3. If you're set on quality mode, then you will get to use this action multiple times to gather more information. Use your judgment to decide when additional searches are necessary to provide a thorough and accurate response. Input: An array of search queries. Make sure the queries are relevant to the user's request and cover different aspects if necessary. You can include a maximum of 3 queries. Make sure the queries are SEO friendly and not sentences rather keywords which can be used to search a search engine like Google, Bing, etc. `; @@ -32,6 +32,7 @@ const webSearchAction: ResearchAction = { const search = async (q: string) => { const res = await searchSearxng(q); + res.results.forEach((r) => { results.push({ content: r.content || r.title, diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts index 0914503..421ee7f 100644 --- a/src/lib/agents/search/types.ts +++ b/src/lib/agents/search/types.ts @@ -10,7 +10,7 @@ export type SearchAgentConfig = { sources: SearchSources[]; llm: BaseLLM; embedding: BaseEmbedding; - mode: 'fast' | 'balanced' | 'deep_research'; + mode: 'speed' | 'balanced' | 'quality'; }; export type SearchAgentInput = { From e0ba476ca442520314e1ed4ff38329890758027a Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:49:54 +0530 Subject: [PATCH 053/196] feat(optimization): enable quality --- src/components/MessageInputActions/Optimization.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/MessageInputActions/Optimization.tsx b/src/components/MessageInputActions/Optimization.tsx index fe04190..1cdded8 100644 --- a/src/components/MessageInputActions/Optimization.tsx +++ b/src/components/MessageInputActions/Optimization.tsx @@ -24,7 +24,7 @@ const OptimizationModes = [ }, { key: 'quality', - title: 'Quality (Soon)', + title: 'Quality', description: 'Get the most thorough and accurate answer', icon: ( { setOptimizationMode(mode.key)} key={i} - disabled={mode.key === 'quality'} className={cn( 'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-1 duration-200 cursor-pointer transition focus:outline-none', optimizationMode === mode.key ? 'bg-light-secondary dark:bg-dark-secondary' : 'hover:bg-light-secondary dark:hover:bg-dark-secondary', - mode.key === 'quality' && 'opacity-50 cursor-not-allowed', )} >
From 956a768a86daca0600c25bf6bae3a2de67d5b08c Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:58:46 +0530 Subject: [PATCH 054/196] feat(app): handle new architecture --- package.json | 3 + src/app/api/chat/route.ts | 315 ++++++---------- src/components/AssistantSteps.tsx | 197 ++++++++++ src/components/ChatWindow.tsx | 31 +- src/components/MessageActions/Copy.tsx | 9 +- src/components/MessageBox.tsx | 94 +++-- src/components/MessageSources.tsx | 4 +- src/components/Navbar.tsx | 133 +++---- src/lib/actions.ts | 12 +- src/lib/agents/search/classifier/index.ts | 5 +- src/lib/hooks/useChat.tsx | 419 ++++++++++++---------- src/lib/models/base/provider.ts | 2 - src/lib/models/providers/openai/index.ts | 2 - yarn.lock | 227 +++++++++++- 14 files changed, 945 insertions(+), 508 deletions(-) create mode 100644 src/components/AssistantSteps.tsx diff --git a/package.json b/package.json index 5752b9f..11a9ce4 100644 --- a/package.json +++ b/package.json @@ -35,9 +35,11 @@ "html-to-text": "^9.0.5", "jspdf": "^3.0.1", "langchain": "^1.0.4", + "lightweight-charts": "^5.0.9", "lucide-react": "^0.363.0", "mammoth": "^1.9.1", "markdown-to-jsx": "^7.7.2", + "mathjs": "^15.1.0", "next": "^15.2.2", "next-themes": "^0.3.0", "ollama": "^0.6.3", @@ -52,6 +54,7 @@ "sonner": "^1.4.41", "tailwind-merge": "^2.2.2", "winston": "^3.17.0", + "yahoo-finance2": "^3.10.2", "yet-another-react-lightbox": "^3.17.2", "zod": "^4.1.12" }, diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 25b8104..2ea0bf5 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,14 +1,10 @@ import crypto from 'crypto'; -import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; -import { EventEmitter } from 'stream'; -import db from '@/lib/db'; -import { chats, messages as messagesSchema } from '@/lib/db/schema'; -import { and, eq, gt } from 'drizzle-orm'; -import { getFileDetails } from '@/lib/utils/files'; -import { searchHandlers } from '@/lib/search'; import { z } from 'zod'; import ModelRegistry from '@/lib/models/registry'; import { ModelWithProvider } from '@/lib/models/types'; +import SearchAgent from '@/lib/agents/search'; +import SessionManager from '@/lib/session'; +import { ChatTurnMessage } from '@/lib/types'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -20,47 +16,25 @@ const messageSchema = z.object({ }); const chatModelSchema: z.ZodType = z.object({ - providerId: z.string({ - errorMap: () => ({ - message: 'Chat model provider id must be provided', - }), - }), - key: z.string({ - errorMap: () => ({ - message: 'Chat model key must be provided', - }), - }), + providerId: z.string({ message: 'Chat model provider id must be provided' }), + key: z.string({ message: 'Chat model key must be provided' }), }); const embeddingModelSchema: z.ZodType = z.object({ providerId: z.string({ - errorMap: () => ({ - message: 'Embedding model provider id must be provided', - }), - }), - key: z.string({ - errorMap: () => ({ - message: 'Embedding model key must be provided', - }), + message: 'Embedding model provider id must be provided', }), + key: z.string({ message: 'Embedding model key must be provided' }), }); const bodySchema = z.object({ message: messageSchema, optimizationMode: z.enum(['speed', 'balanced', 'quality'], { - errorMap: () => ({ - message: 'Optimization mode must be one of: speed, balanced, quality', - }), + message: 'Optimization mode must be one of: speed, balanced, quality', }), focusMode: z.string().min(1, 'Focus mode is required'), history: z - .array( - z.tuple([z.string(), z.string()], { - errorMap: () => ({ - message: 'History items must be tuples of two strings', - }), - }), - ) + .array(z.tuple([z.string(), z.string()])) .optional() .default([]), files: z.array(z.string()).optional().default([]), @@ -78,7 +52,7 @@ const safeValidateBody = (data: unknown) => { if (!result.success) { return { success: false, - error: result.error.errors.map((e) => ({ + error: result.error.issues.map((e: any) => ({ path: e.path.join('.'), message: e.message, })), @@ -91,151 +65,12 @@ const safeValidateBody = (data: unknown) => { }; }; -const handleEmitterEvents = async ( - stream: EventEmitter, - writer: WritableStreamDefaultWriter, - encoder: TextEncoder, - chatId: string, -) => { - let receivedMessage = ''; - const aiMessageId = crypto.randomBytes(7).toString('hex'); - - stream.on('data', (data) => { - const parsedData = JSON.parse(data); - if (parsedData.type === 'response') { - writer.write( - encoder.encode( - JSON.stringify({ - type: 'message', - data: parsedData.data, - messageId: aiMessageId, - }) + '\n', - ), - ); - - receivedMessage += parsedData.data; - } else if (parsedData.type === 'sources') { - writer.write( - encoder.encode( - JSON.stringify({ - type: 'sources', - data: parsedData.data, - messageId: aiMessageId, - }) + '\n', - ), - ); - - const sourceMessageId = crypto.randomBytes(7).toString('hex'); - - db.insert(messagesSchema) - .values({ - chatId: chatId, - messageId: sourceMessageId, - role: 'source', - sources: parsedData.data, - createdAt: new Date().toString(), - }) - .execute(); - } - }); - stream.on('end', () => { - writer.write( - encoder.encode( - JSON.stringify({ - type: 'messageEnd', - }) + '\n', - ), - ); - writer.close(); - - db.insert(messagesSchema) - .values({ - content: receivedMessage, - chatId: chatId, - messageId: aiMessageId, - role: 'assistant', - createdAt: new Date().toString(), - }) - .execute(); - }); - stream.on('error', (data) => { - const parsedData = JSON.parse(data); - writer.write( - encoder.encode( - JSON.stringify({ - type: 'error', - data: parsedData.data, - }), - ), - ); - writer.close(); - }); -}; - -const handleHistorySave = async ( - message: Message, - humanMessageId: string, - focusMode: string, - files: string[], -) => { - const chat = await db.query.chats.findFirst({ - where: eq(chats.id, message.chatId), - }); - - const fileData = files.map(getFileDetails); - - if (!chat) { - await db - .insert(chats) - .values({ - id: message.chatId, - title: message.content, - createdAt: new Date().toString(), - focusMode: focusMode, - files: fileData, - }) - .execute(); - } else if (JSON.stringify(chat.files ?? []) != JSON.stringify(fileData)) { - db.update(chats) - .set({ - files: files.map(getFileDetails), - }) - .where(eq(chats.id, message.chatId)); - } - - const messageExists = await db.query.messages.findFirst({ - where: eq(messagesSchema.messageId, humanMessageId), - }); - - if (!messageExists) { - await db - .insert(messagesSchema) - .values({ - content: message.content, - chatId: message.chatId, - messageId: humanMessageId, - role: 'user', - createdAt: new Date().toString(), - }) - .execute(); - } else { - await db - .delete(messagesSchema) - .where( - and( - gt(messagesSchema.id, messageExists.id), - eq(messagesSchema.chatId, message.chatId), - ), - ) - .execute(); - } -}; - export const POST = async (req: Request) => { try { const reqBody = (await req.json()) as Body; const parseBody = safeValidateBody(reqBody); + if (!parseBody.success) { return Response.json( { message: 'Invalid request body', error: parseBody.error }, @@ -265,48 +100,116 @@ export const POST = async (req: Request) => { ), ]); - const humanMessageId = - message.messageId ?? crypto.randomBytes(7).toString('hex'); - - const history: BaseMessage[] = body.history.map((msg) => { + const history: ChatTurnMessage[] = body.history.map((msg) => { if (msg[0] === 'human') { - return new HumanMessage({ + return { + role: 'user', content: msg[1], - }); + }; } else { - return new AIMessage({ + return { + role: 'assistant', content: msg[1], - }); + }; } }); - const handler = searchHandlers[body.focusMode]; - - if (!handler) { - return Response.json( - { - message: 'Invalid focus mode', - }, - { status: 400 }, - ); - } - - const stream = await handler.searchAndAnswer( - message.content, - history, - llm, - embedding, - body.optimizationMode, - body.files, - body.systemInstructions as string, - ); + const agent = new SearchAgent(); + const session = SessionManager.createSession(); const responseStream = new TransformStream(); const writer = responseStream.writable.getWriter(); const encoder = new TextEncoder(); - handleEmitterEvents(stream, writer, encoder, message.chatId); - handleHistorySave(message, humanMessageId, body.focusMode, body.files); + let receivedMessage = ''; + + session.addListener('data', (data: any) => { + if (data.type === 'response') { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'message', + data: data.data, + }) + '\n', + ), + ); + receivedMessage += data.data; + } else if (data.type === 'sources') { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'sources', + data: data.data, + }) + '\n', + ), + ); + } else if (data.type === 'block') { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'block', + block: data.block, + }) + '\n', + ), + ); + } else if (data.type === 'updateBlock') { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'updateBlock', + blockId: data.blockId, + patch: data.patch, + }) + '\n', + ), + ); + } else if (data.type === 'researchComplete') { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'researchComplete', + }) + '\n', + ), + ); + } + }); + + session.addListener('end', () => { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'messageEnd', + }) + '\n', + ), + ); + writer.close(); + session.removeAllListeners(); + }); + + session.addListener('error', (data: any) => { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'error', + data: data.data, + }) + '\n', + ), + ); + writer.close(); + session.removeAllListeners(); + }); + + agent.searchAsync(session, { + chatHistory: history, + followUp: message.content, + config: { + llm, + embedding: embedding, + sources: ['web'], + mode: body.optimizationMode, + }, + }); + + /* handleHistorySave(message, humanMessageId, body.focusMode, body.files); */ return new Response(responseStream.readable, { headers: { diff --git a/src/components/AssistantSteps.tsx b/src/components/AssistantSteps.tsx new file mode 100644 index 0000000..c688880 --- /dev/null +++ b/src/components/AssistantSteps.tsx @@ -0,0 +1,197 @@ +'use client'; + +import { Brain, Search, FileText, ChevronDown, ChevronUp } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useEffect, useState } from 'react'; +import { ResearchBlock, ResearchBlockSubStep } from '@/lib/types'; +import { useChat } from '@/lib/hooks/useChat'; + +const getStepIcon = (step: ResearchBlockSubStep) => { + if (step.type === 'reasoning') { + return ; + } else if (step.type === 'searching') { + return ; + } else if (step.type === 'reading') { + return ; + } + return null; +}; + +const getStepTitle = ( + step: ResearchBlockSubStep, + isStreaming: boolean, +): string => { + if (step.type === 'reasoning') { + return isStreaming && !step.reasoning ? 'Thinking...' : 'Thinking'; + } else if (step.type === 'searching') { + return `Searching ${step.searching.length} ${step.searching.length === 1 ? 'query' : 'queries'}`; + } else if (step.type === 'reading') { + return `Found ${step.reading.length} ${step.reading.length === 1 ? 'result' : 'results'}`; + } + return 'Processing'; +}; + +const AssistantSteps = ({ + block, + status, +}: { + block: ResearchBlock; + status: 'answering' | 'completed' | 'error'; +}) => { + const [isExpanded, setIsExpanded] = useState(true); + const { researchEnded, loading } = useChat(); + + useEffect(() => { + if (researchEnded) { + setIsExpanded(false); + } else if (status === 'answering') { + setIsExpanded(true); + } + }, [researchEnded, status]); + + if (!block || block.data.subSteps.length === 0) return null; + + return ( +
+ + + + {isExpanded && ( + +
+ {block.data.subSteps.map((step, index) => { + const isLastStep = index === block.data.subSteps.length - 1; + const isStreaming = loading && isLastStep && !researchEnded; + + return ( + + {/* Timeline connector */} +
+
+ {getStepIcon(step)} +
+ {index < block.data.subSteps.length - 1 && ( +
+ )} +
+ + {/* Step content */} +
+ + {getStepTitle(step, isStreaming)} + + + {step.type === 'reasoning' && ( + <> + {step.reasoning && ( +

+ {step.reasoning} +

+ )} + {isStreaming && !step.reasoning && ( +
+
+
+
+
+ )} + + )} + + {step.type === 'searching' && + step.searching.length > 0 && ( +
+ {step.searching.map((query, idx) => ( + + {query} + + ))} +
+ )} + + {step.type === 'reading' && step.reading.length > 0 && ( +
+ {step.reading.slice(0, 4).map((result, idx) => { + const url = result.metadata.url || ''; + const title = result.metadata.title || 'Untitled'; + const domain = url ? new URL(url).hostname : ''; + const faviconUrl = domain + ? `https://s2.googleusercontent.com/s2/favicons?domain=${domain}&sz=128` + : ''; + + return ( + + {faviconUrl && ( + { + e.currentTarget.style.display = 'none'; + }} + /> + )} + {title} + + ); + })} +
+ )} +
+ + ); + })} +
+ + )} + +
+ ); +}; + +export default AssistantSteps; diff --git a/src/components/ChatWindow.tsx b/src/components/ChatWindow.tsx index dc6ab01..9489219 100644 --- a/src/components/ChatWindow.tsx +++ b/src/components/ChatWindow.tsx @@ -1,14 +1,12 @@ 'use client'; -import { Document } from '@langchain/core/documents'; import Navbar from './Navbar'; import Chat from './Chat'; import EmptyChat from './EmptyChat'; -import { Settings } from 'lucide-react'; -import Link from 'next/link'; import NextError from 'next/error'; import { useChat } from '@/lib/hooks/useChat'; import SettingsButtonMobile from './Settings/SettingsButtonMobile'; +import { Block, Chunk } from '@/lib/types'; export interface BaseMessage { chatId: string; @@ -16,20 +14,27 @@ export interface BaseMessage { createdAt: Date; } +export interface Message extends BaseMessage { + backendId: string; + query: string; + responseBlocks: Block[]; + status: 'answering' | 'completed' | 'error'; +} + +export interface UserMessage extends BaseMessage { + role: 'user'; + content: string; +} + export interface AssistantMessage extends BaseMessage { role: 'assistant'; content: string; suggestions?: string[]; } -export interface UserMessage extends BaseMessage { - role: 'user'; - content: string; -} - export interface SourceMessage extends BaseMessage { role: 'source'; - sources: Document[]; + sources: Chunk[]; } export interface SuggestionMessage extends BaseMessage { @@ -37,11 +42,12 @@ export interface SuggestionMessage extends BaseMessage { suggestions: string[]; } -export type Message = +export type LegacyMessage = | AssistantMessage | UserMessage | SourceMessage | SuggestionMessage; + export type ChatTurn = UserMessage | AssistantMessage; export interface File { @@ -50,6 +56,11 @@ export interface File { fileId: string; } +export interface Widget { + widgetType: string; + params: Record; +} + const ChatWindow = () => { const { hasError, notFound, messages } = useChat(); if (hasError) { diff --git a/src/components/MessageActions/Copy.tsx b/src/components/MessageActions/Copy.tsx index f74b9f3..38ed71a 100644 --- a/src/components/MessageActions/Copy.tsx +++ b/src/components/MessageActions/Copy.tsx @@ -15,7 +15,14 @@ const Copy = ({ return (
From b8a7fb936f922e6eeece88213d7406c2e4017bd5 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:50:26 +0530 Subject: [PATCH 070/196] feat(classifier): add `showCalculationWidget` --- src/lib/agents/search/classifier.ts | 3 +++ src/lib/agents/search/types.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/src/lib/agents/search/classifier.ts b/src/lib/agents/search/classifier.ts index b5137a6..5909e35 100644 --- a/src/lib/agents/search/classifier.ts +++ b/src/lib/agents/search/classifier.ts @@ -23,6 +23,9 @@ const schema = z.object({ showStockWidget: z .boolean() .describe('Indicates whether to show the stock widget.'), + showCalculationWidget: z + .boolean() + .describe('Indicates whether to show the calculation widget.'), }), standaloneFollowUp: z .string() diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts index a0b27b4..c30d4f7 100644 --- a/src/lib/agents/search/types.ts +++ b/src/lib/agents/search/types.ts @@ -62,6 +62,7 @@ export type ClassifierOutput = { discussionSearch: boolean; showWeatherWidget: boolean; showStockWidget: boolean; + showCalculationWidget: boolean; }; standaloneFollowUp: string; }; From 1ea348ddb7e451fb7dc9618da0b917f139ce8ae5 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:50:54 +0530 Subject: [PATCH 071/196] feat(classifier-prompt): update and add `showCalculationWidget` --- src/lib/prompts/search/classifier.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/prompts/search/classifier.ts b/src/lib/prompts/search/classifier.ts index d157347..a353ec1 100644 --- a/src/lib/prompts/search/classifier.ts +++ b/src/lib/prompts/search/classifier.ts @@ -31,6 +31,10 @@ NOTE: BY GENERAL KNOWLEDGE WE MEAN INFORMATION THAT IS OBVIOUS, WIDELY KNOWN, OR - Set it to true if the user's query is specifically about current stock prices or stock related information for particular companies. Never use it for a market analysis or news about stock market. - Set it to true for queries like "What's the stock price of [Company]?" or "How is the [Stock] performing today?" or "Show me the stock prices" (Here they mean stocks of companies they are interested in). - If it can fully answer the user query without needing additional search, set skipSearch to true as well. +7. showCalculationWidget (boolean): Decide if displaying a calculation widget would adequately address the user's query. + - Set it to true if the user's query involves mathematical calculations, conversions, or any computation-related tasks. + - Set it to true for queries like "What is 25% of 80?" or "Convert 100 USD to EUR" or "Calculate the square root of 256" or "What is 2 * 3 + 5?" or other mathematical expressions. + - If it can fully answer the user query without needing additional search, set skipSearch to true as well. From dbc2137efbd4a5b1260ea7e01ddc9a47dc2defa4 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:51:17 +0530 Subject: [PATCH 072/196] Revise writer prompt for warmer, conversational tone --- src/lib/prompts/search/writer.ts | 123 +++++++++++++++++++------------ 1 file changed, 76 insertions(+), 47 deletions(-) diff --git a/src/lib/prompts/search/writer.ts b/src/lib/prompts/search/writer.ts index 69a99c3..d564ffb 100644 --- a/src/lib/prompts/search/writer.ts +++ b/src/lib/prompts/search/writer.ts @@ -1,58 +1,87 @@ export const getWriterPrompt = (context: string) => { return ` - You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses. +You are Perplexica, an AI assistant that provides helpful, accurate, and engaging answers. You combine web search results with a warm, conversational tone to deliver responses that feel personal and genuinely useful. - Your task is to provide answers that are: - - **Informative and relevant**: Thoroughly address the user's query using the given context. - - **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically. - - **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights. - - **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included. - - **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable. +## Core Principles - ### Formatting Instructions - - **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate. - - **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience. - - **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability. - - **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience. - - **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title. - - **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate. - - **No references or source list at the end**: Do not include a seperate references or sources section at the end of your response. All references are sent to user by the system automatically. - - **Do not give the mapping of citations to sources**: Only use the [number] notation in the text. Never return the mapping of citations to sources. +**Be warm and conversational**: Write like you're having a friendly conversation with someone curious about the topic. Show genuine interest in helping them understand. Avoid being robotic or overly formal. - ### Citation Requirements - - Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`. - - Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]." - - Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context. - - Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]." - - Always prioritize credibility and accuracy by linking all statements back to their respective context sources. - - Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation. - - Avoid citing widget data but you can use it to directly answer without citation. - - Never return the mapping of citations to sources; only use the [number] notation in the text. Never return a references or sources section seperately. +**Be informative and thorough**: Address the user's query comprehensively using the provided context. Explain concepts clearly and anticipate follow-up questions they might have. - ### Widget Data Usage - - Widget data provided in the context can be used directly to answer specific queries (e.g., current weather, stock prices, calculations) without citations. - - The widget data is already displayed to the user in a beautiful format (via cards, tables, etc) just before your response so you don't need to generate a very detailed response for widget data. Provide concise answers for such queries. - - You can also mention that for more information you can look at the widget displayed above. - - For weather data, only provide current weather conditions not forecasts unless explicitly asked for forecasts by the user. - - You don't need to cite widget data you can directly use it to answer the user query. NEVER CITE widget OR (any other notation) TO CITE WIDGET DATA. +**Be honest and credible**: Cite your sources using [number] notation. If information is uncertain or unavailable, say so transparently. - ### Special Instructions - - If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity. - - 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. - - If its a simple query (like weather, calculations, definitions), provide concise answers and not a long article. +**No emojis**: Keep responses clean and professional. Never use emojis unless the user explicitly requests them. - ### 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. - - Provide explanations or historical context as needed to enhance understanding. - - End with a conclusion or overall perspective if relevant. - - For simpler queries like weather, calculations, or definitions, provide concise answers and not a long article. +## Formatting Guidelines - - ${context} - +**Use Markdown effectively**: +- Use headings (## and ###) to organize longer responses into logical sections +- Use **bold** for key terms and *italics* for emphasis +- Use bullet points and numbered lists to break down complex information +- Use tables when comparing data, features, or options +- Use code blocks for technical content when appropriate - Current date & time in ISO format (UTC timezone) is: ${new Date().toISOString()}. - `; +**Adapt length to the query**: +- Simple questions (weather, calculations, quick facts): Brief, direct answers +- Complex topics: Structured responses with sections, context, and depth +- Always start with the direct answer before expanding into details + +**No main title**: Jump straight into your response without a title heading. + +**No references section**: Never include a "Sources" or "References" section at the end. Citations are handled inline only. + +## Citation Rules + +**Cite all factual claims** using [number] notation corresponding to sources in the context: +- Place citations at the end of the relevant sentence or clause +- Example: "The Great Wall of China stretches over 13,000 miles[1]." +- Use multiple citations when information comes from several sources[1][2] + +**Never cite widget data**: Weather, stock prices, calculations, and other widget data should be stated directly without any citation notation. + +**Never list citation mappings**: Only use [number] in the text. Do not provide a list showing which number corresponds to which source. + +**CRITICAL - No references section**: NEVER include a "Sources", "References", footnotes, or any numbered list at the end of your response that maps citations to their sources. This is strictly forbidden. The system handles source display separately. Your response must end with your final paragraph of content, not a list of sources. + +## Widget Data + +Widget data (weather, stocks, calculations) is displayed to the user in interactive cards above your response. + +**IMPORTANT**: When widget data is present, keep your response VERY brief (2-3 sentences max). The user already sees the detailed data in the widget card. Do NOT repeat all the widget data in your text response. + +For example, for a weather query, just say: +"It's currently -8.7°C in New York with overcast skies. You can see the full details including hourly and daily forecasts in the weather card above." + +**Do NOT**: +- List out all the weather metrics (temperature, humidity, wind, pressure, etc.) +- Provide forecasts unless explicitly asked +- Add citations to widget data +- Repeat information that's already visible in the widget + +## Response Style + +**Opening**: Start with a direct, engaging answer to the question. Get to the point quickly. + +**Body**: Expand with relevant details, context, or explanations. Use formatting to make information scannable and easy to digest. + +**Closing**: For longer responses, summarize key takeaways or suggest related topics they might find interesting. Keep it natural, not formulaic. + +## When Information is Limited + +If you cannot find relevant information, respond honestly: +"I wasn't able to find specific information about this topic. You might want to try rephrasing your question, or I can help you explore related areas." + +Suggest alternative angles or related topics that might be helpful. + + +${context} + + +Current date & time in ISO format (UTC timezone) is: ${new Date().toISOString()}. + +FINAL REMINDERS: +1. DO NOT add a references/sources section at the end. Your response ends with content, not citations. +2. For widget queries (weather, stocks, calculations): Keep it to 2-3 sentences. The widget shows the details. +3. No emojis. +`; }; From 046f1595286f92f8a9245e29fb36224092658886 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:52:40 +0530 Subject: [PATCH 073/196] feat(widgets): use new classifier, implement new widget executor, delete registry --- src/lib/agents/search/index.ts | 18 ++-- .../search/researcher/actions/webSearch.ts | 2 +- src/lib/agents/search/researcher/index.ts | 4 +- src/lib/agents/search/types.ts | 25 ++--- .../search/widgets/calculationWidget.ts | 96 +++++++++---------- src/lib/agents/search/widgets/executor.ts | 36 +++++++ src/lib/agents/search/widgets/index.ts | 10 +- src/lib/agents/search/widgets/registry.ts | 65 ------------- src/lib/agents/search/widgets/stockWidget.ts | 91 +++++++++--------- .../agents/search/widgets/weatherWidget.ts | 79 +++++++++------ 10 files changed, 204 insertions(+), 222 deletions(-) create mode 100644 src/lib/agents/search/widgets/executor.ts delete mode 100644 src/lib/agents/search/widgets/registry.ts diff --git a/src/lib/agents/search/index.ts b/src/lib/agents/search/index.ts index ab12bfe..bc6ff5b 100644 --- a/src/lib/agents/search/index.ts +++ b/src/lib/agents/search/index.ts @@ -1,26 +1,24 @@ import { ResearcherOutput, SearchAgentInput } from './types'; import SessionManager from '@/lib/session'; -import Classifier from './classifier'; -import { WidgetRegistry } from './widgets'; +import { classify } from './classifier'; import Researcher from './researcher'; import { getWriterPrompt } from '@/lib/prompts/search/writer'; -import fs from 'fs'; +import { WidgetExecutor } from './widgets'; class SearchAgent { async searchAsync(session: SessionManager, input: SearchAgentInput) { - const classifier = new Classifier(); - - const classification = await classifier.classify({ + const classification = await classify({ chatHistory: input.chatHistory, enabledSources: input.config.sources, query: input.followUp, llm: input.config.llm, }); - const widgetPromise = WidgetRegistry.executeAll(classification.widgets, { + const widgetPromise = WidgetExecutor.executeAll({ + classification, + chatHistory: input.chatHistory, + followUp: input.followUp, llm: input.config.llm, - embedding: input.config.embedding, - session: session, }).then((widgetOutputs) => { widgetOutputs.forEach((o) => { session.emitBlock({ @@ -37,7 +35,7 @@ class SearchAgent { let searchPromise: Promise | null = null; - if (!classification.skipSearch) { + if (!classification.classification.skipSearch) { const researcher = new Researcher(); searchPromise = researcher.research(session, { chatHistory: input.chatHistory, diff --git a/src/lib/agents/search/researcher/actions/webSearch.ts b/src/lib/agents/search/researcher/actions/webSearch.ts index e5ffdd3..17f5e61 100644 --- a/src/lib/agents/search/researcher/actions/webSearch.ts +++ b/src/lib/agents/search/researcher/actions/webSearch.ts @@ -26,7 +26,7 @@ const webSearchAction: ResearchAction = { name: 'web_search', description: actionDescription, schema: actionSchema, - enabled: (config) => config.classification.intents.includes('web_search'), + enabled: (config) => true, execute: async (input, _) => { let results: Chunk[] = []; diff --git a/src/lib/agents/search/researcher/index.ts b/src/lib/agents/search/researcher/index.ts index 3eb9478..9ca1fed 100644 --- a/src/lib/agents/search/researcher/index.ts +++ b/src/lib/agents/search/researcher/index.ts @@ -61,9 +61,7 @@ class Researcher { maxIteration, ); - const actionStream = input.config.llm.streamObject< - z.infer - >({ + const actionStream = input.config.llm.streamObject({ messages: [ { role: 'system', diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts index c30d4f7..589fa2d 100644 --- a/src/lib/agents/search/types.ts +++ b/src/lib/agents/search/types.ts @@ -19,26 +19,17 @@ export type SearchAgentInput = { config: SearchAgentConfig; }; -export interface Intent { - name: string; - description: string; - requiresSearch: boolean; - enabled: (config: { sources: SearchSources[] }) => boolean; -} - -export type Widget = z.ZodObject> = { - name: string; - description: string; - schema: TSchema; - execute: ( - params: z.infer, - additionalConfig: AdditionalConfig, - ) => Promise; +export type WidgetInput = { + chatHistory: ChatTurnMessage[]; + followUp: string; + classification: ClassifierOutput; + llm: BaseLLM; }; -export type WidgetConfig = { +export type Widget = { type: string; - params: Record; + shouldExecute: (classification: ClassifierOutput) => boolean; + execute: (input: WidgetInput) => Promise; }; export type WidgetOutput = { diff --git a/src/lib/agents/search/widgets/calculationWidget.ts b/src/lib/agents/search/widgets/calculationWidget.ts index 1c1ba51..0026741 100644 --- a/src/lib/agents/search/widgets/calculationWidget.ts +++ b/src/lib/agents/search/widgets/calculationWidget.ts @@ -1,66 +1,66 @@ import z from 'zod'; import { Widget } from '../types'; -import { evaluate as mathEval } from 'mathjs'; +import formatChatHistoryAsString from '@/lib/utils/formatHistory'; +import { exp, evaluate as mathEval } from 'mathjs'; const schema = z.object({ - type: z.literal('calculation'), expression: z .string() - .describe( - "A valid mathematical expression to be evaluated (e.g., '2 + 2', '3 * (4 + 5)').", - ), + .describe('Mathematical expression to calculate or evaluate.'), + notPresent: z + .boolean() + .describe('Whether there is any need for the calculation widget.'), }); -const calculationWidget: Widget = { - name: 'calculation', - description: `Performs mathematical calculations and evaluates mathematical expressions. Supports arithmetic operations, algebraic equations, functions, and complex mathematical computations. +const system = ` + +Assistant is a calculation expression extractor. You will recieve a user follow up and a conversation history. +Your task is to determine if there is a mathematical expression that needs to be calculated or evaluated. If there is, extract the expression and return it. If there is no need for any calculation, set notPresent to true. + -**What it provides:** -- Evaluates mathematical expressions and returns computed results -- Handles basic arithmetic (+, -, *, /) -- Supports functions (sqrt, sin, cos, log, etc.) -- Can process complex expressions with parentheses and order of operations + +Make sure that the extracted expression is valid and can be used to calculate the result with Math JS library (https://mathjs.org/). If the expression is not valid, set notPresent to true. +If you feel like you cannot extract a valid expression, set notPresent to true. + -**When to use:** -- User asks to calculate, compute, or evaluate a mathematical expression -- Questions like "what is X", "calculate Y", "how much is Z" where X/Y/Z are math expressions -- Any request involving numbers and mathematical operations - -**Example call:** + +You must respond in the following JSON format without any extra text, explanations or filler sentences: { - "type": "calculation", - "expression": "25% of 480" + "expression": string, + "notPresent": boolean } + +`; -{ - "type": "calculation", - "expression": "sqrt(144) + 5 * 2" -} - -**Important:** The expression must be valid mathematical syntax that can be evaluated by mathjs. Format percentages as "0.25 * 480" or "25% of 480". Do not include currency symbols, units, or non-mathematical text in the expression.`, - schema: schema, - execute: async (params, _) => { - try { - const result = mathEval(params.expression); - - return { - type: 'calculation_result', - llmContext: `The result of the expression "${params.expression}" is ${result}.`, - data: { - expression: params.expression, - result: result, +const calculationWidget: Widget = { + type: 'calculationWidget', + shouldExecute: (classification) => + classification.classification.showCalculationWidget, + execute: async (input) => { + const output = await input.llm.generateObject({ + messages: [ + { + role: 'system', + content: system, }, - }; - } catch (error) { - return { - type: 'calculation_result', - llmContext: 'Failed to evaluate mathematical expression.', - data: { - expression: params.expression, - result: `Error evaluating expression: ${error}`, + { + role: 'user', + content: `\n${formatChatHistoryAsString(input.chatHistory)}\n\n\n${input.followUp}\n`, }, - }; - } + ], + schema, + }); + + const result = mathEval(output.expression); + + return { + type: 'calculation_result', + llmContext: `The result of the calculation for the expression "${output.expression}" is: ${result}`, + data: { + expression: output.expression, + result, + }, + }; }, }; diff --git a/src/lib/agents/search/widgets/executor.ts b/src/lib/agents/search/widgets/executor.ts new file mode 100644 index 0000000..89f1830 --- /dev/null +++ b/src/lib/agents/search/widgets/executor.ts @@ -0,0 +1,36 @@ +import { Widget, WidgetInput, WidgetOutput } from '../types'; + +class WidgetExecutor { + static widgets = new Map(); + + static register(widget: Widget) { + this.widgets.set(widget.type, widget); + } + + static getWidget(type: string): Widget | undefined { + return this.widgets.get(type); + } + + static async executeAll(input: WidgetInput): Promise { + const results: WidgetOutput[] = []; + + await Promise.all( + Array.from(this.widgets.values()).map(async (widget) => { + try { + if (widget.shouldExecute(input.classification)) { + const output = await widget.execute(input); + if (output) { + results.push(output); + } + } + } catch (e) { + console.log(`Error executing widget ${widget.type}:`, e); + } + }), + ); + + return results; + } +} + +export default WidgetExecutor; diff --git a/src/lib/agents/search/widgets/index.ts b/src/lib/agents/search/widgets/index.ts index ff18d40..9958b0d 100644 --- a/src/lib/agents/search/widgets/index.ts +++ b/src/lib/agents/search/widgets/index.ts @@ -1,10 +1,10 @@ import calculationWidget from './calculationWidget'; -import WidgetRegistry from './registry'; +import WidgetExecutor from './executor'; import weatherWidget from './weatherWidget'; import stockWidget from './stockWidget'; -WidgetRegistry.register(weatherWidget); -WidgetRegistry.register(calculationWidget); -WidgetRegistry.register(stockWidget); +WidgetExecutor.register(weatherWidget); +WidgetExecutor.register(calculationWidget); +WidgetExecutor.register(stockWidget); -export { WidgetRegistry }; +export { WidgetExecutor }; diff --git a/src/lib/agents/search/widgets/registry.ts b/src/lib/agents/search/widgets/registry.ts deleted file mode 100644 index d8ceaba..0000000 --- a/src/lib/agents/search/widgets/registry.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - AdditionalConfig, - SearchAgentConfig, - Widget, - WidgetConfig, - WidgetOutput, -} from '../types'; - -class WidgetRegistry { - private static widgets = new Map(); - - static register(widget: Widget) { - this.widgets.set(widget.name, widget); - } - - static get(name: string): Widget | undefined { - return this.widgets.get(name); - } - - static getAll(): Widget[] { - return Array.from(this.widgets.values()); - } - - static getDescriptions(): string { - return Array.from(this.widgets.values()) - .map((widget) => `${widget.name}: ${widget.description}`) - .join('\n\n'); - } - - static async execute( - name: string, - params: any, - config: AdditionalConfig, - ): Promise { - const widget = this.get(name); - - if (!widget) { - throw new Error(`Widget with name ${name} not found`); - } - - return widget.execute(params, config); - } - - static async executeAll( - widgets: WidgetConfig[], - additionalConfig: AdditionalConfig, - ): Promise { - const results: WidgetOutput[] = []; - - await Promise.all( - widgets.map(async (widgetConfig) => { - const output = await this.execute( - widgetConfig.type, - widgetConfig.params, - additionalConfig, - ); - results.push(output); - }), - ); - - return results; - } -} - -export default WidgetRegistry; diff --git a/src/lib/agents/search/widgets/stockWidget.ts b/src/lib/agents/search/widgets/stockWidget.ts index d728460..4ac2059 100644 --- a/src/lib/agents/search/widgets/stockWidget.ts +++ b/src/lib/agents/search/widgets/stockWidget.ts @@ -1,13 +1,13 @@ import z from 'zod'; import { Widget } from '../types'; import YahooFinance from 'yahoo-finance2'; +import formatChatHistoryAsString from '@/lib/utils/formatHistory'; const yf = new YahooFinance({ suppressNotices: ['yahooSurvey'], }); const schema = z.object({ - type: z.literal('stock'), name: z .string() .describe( @@ -19,60 +19,59 @@ const schema = z.object({ .describe( "Optional array of up to 3 stock names to compare against the base name (e.g., ['Microsoft', 'GOOGL', 'Meta']). Charts will show percentage change comparison.", ), + notPresent: z + .boolean() + .describe('Whether there is no need for the stock widget.'), }); -const stockWidget: Widget = { - name: 'stock', - description: `Provides comprehensive real-time stock market data and financial information for any publicly traded company. Returns detailed quote data, market status, trading metrics, and company fundamentals. +const systemPrompt = ` + +You are a stock ticker/name extractor. You will receive a user follow up and a conversation history. +Your task is to determine if the user is asking about stock information and extract the stock name(s) they want data for. + -You can set skipSearch to true if the stock widget can fully answer the user's query without needing additional web search. + +- If the user is asking about a stock, extract the primary stock name or ticker. +- If the user wants to compare stocks, extract up to 3 comparison stock names in comparisonNames. +- You can use either stock names (e.g., "Nvidia", "Apple") or tickers (e.g., "NVDA", "AAPL"). +- If you cannot determine a valid stock or the query is not stock-related, set notPresent to true. +- If no comparison is needed, set comparisonNames to an empty array. + -**What it provides:** -- **Real-time Price Data**: Current price, previous close, open price, day's range (high/low) -- **Market Status**: Whether market is currently open or closed, trading sessions -- **Trading Metrics**: Volume, average volume, bid/ask prices and sizes -- **Performance**: Price changes (absolute and percentage), 52-week high/low range -- **Valuation**: Market capitalization, P/E ratio, earnings per share (EPS) -- **Dividends**: Dividend rate, dividend yield, ex-dividend date -- **Company Info**: Full company name, exchange, currency, sector/industry (when available) -- **Advanced Metrics**: Beta, trailing/forward P/E, book value, price-to-book ratio -- **Charts Data**: Historical price movements for visualization -- **Comparison**: Compare up to 3 stocks side-by-side with percentage-based performance visualization - -**When to use:** -- User asks about a stock price ("What's AAPL stock price?", "How is Tesla doing?") -- Questions about company market performance ("Is Microsoft up or down today?") -- Requests for stock market data, trading info, or company valuation -- Queries about dividends, P/E ratio, market cap, or other financial metrics -- Any stock/equity-related question for a specific company -- Stock comparisons ("Compare AAPL vs MSFT", "How is TSLA doing vs RIVN and LCID?") - -**Example calls:** + +You must respond in the following JSON format without any extra text, explanations or filler sentences: { - "type": "stock", - "name": "AAPL" + "name": string, + "comparisonNames": string[], + "notPresent": boolean } + +`; -{ - "type": "stock", - "name": "TSLA", - "comparisonNames": ["RIVN", "LCID"] -} +const stockWidget: Widget = { + type: 'stockWidget', + shouldExecute: (classification) => + classification.classification.showStockWidget, + execute: async (input) => { + const output = await input.llm.generateObject({ + messages: [ + { + role: 'system', + content: systemPrompt, + }, + { + role: 'user', + content: `\n${formatChatHistoryAsString(input.chatHistory)}\n\n\n${input.followUp}\n`, + }, + ], + schema, + }); -{ - "type": "stock", - "name": "Google", - "comparisonNames": ["Microsoft", "Meta", "Amazon"] -} + if (output.notPresent) { + return; + } -**Important:** -- You can use both tickers and names (prefer name when you're not aware of the ticker). -- For companies with multiple share classes, use the most common one. -- The widget works for stocks listed on major exchanges (NYSE, NASDAQ, etc.) -- Returns comprehensive data; the UI will display relevant metrics based on availability -- Market data may be delayed by 15-20 minutes for free data sources during trading hours`, - schema: schema, - execute: async (params, _) => { + const params = output; try { const name = params.name; diff --git a/src/lib/agents/search/widgets/weatherWidget.ts b/src/lib/agents/search/widgets/weatherWidget.ts index 2c6d7ab..4739324 100644 --- a/src/lib/agents/search/widgets/weatherWidget.ts +++ b/src/lib/agents/search/widgets/weatherWidget.ts @@ -1,8 +1,8 @@ import z from 'zod'; import { Widget } from '../types'; +import formatChatHistoryAsString from '@/lib/utils/formatHistory'; -const WeatherWidgetSchema = z.object({ - type: z.literal('weather'), +const schema = z.object({ location: z .string() .describe( @@ -18,38 +18,63 @@ const WeatherWidgetSchema = z.object({ .describe( 'Longitude coordinate in decimal degrees (e.g., -74.0060). Only use when location name is empty.', ), + notPresent: z + .boolean() + .describe('Whether there is no need for the weather widget.'), }); -const weatherWidget: Widget = { - name: 'weather', - description: `Provides comprehensive current weather information and forecasts for any location worldwide. Returns real-time weather data including temperature, conditions, humidity, wind, and multi-day forecasts. +const systemPrompt = ` + +You are a location extractor for weather queries. You will receive a user follow up and a conversation history. +Your task is to determine if the user is asking about weather and extract the location they want weather for. + -You can set skipSearch to true if the weather widget can fully answer the user's query without needing additional web search. + +- If the user is asking about weather, extract the location name OR coordinates (never both). +- If using location name, set lat and lon to 0. +- If using coordinates, set location to empty string. +- If you cannot determine a valid location or the query is not weather-related, set notPresent to true. +- Location should be specific (city, state/region, country) for best results. +- You have to give the location so that it can be used to fetch weather data, it cannot be left empty unless notPresent is true. +- Make sure to infer short forms of location names (e.g., "NYC" -> "New York City", "LA" -> "Los Angeles"). + -**What it provides:** -- Current weather conditions (temperature, feels-like, humidity, precipitation) -- Wind speed, direction, and gusts -- Weather codes/conditions (clear, cloudy, rainy, etc.) -- Hourly forecast for next 24 hours -- Daily forecast for next 7 days (high/low temps, precipitation probability) -- Timezone information - -**When to use:** -- User asks about weather in a location ("weather in X", "is it raining in Y") -- Questions about temperature, conditions, or forecast -- Any weather-related query for a specific place - -**Example call:** + +You must respond in the following JSON format without any extra text, explanations or filler sentences: { - "type": "weather", - "location": "San Francisco, CA, USA", - "lat": 0, - "lon": 0 + "location": string, + "lat": number, + "lon": number, + "notPresent": boolean } + +`; + +const weatherWidget: Widget = { + type: 'weatherWidget', + shouldExecute: (classification) => + classification.classification.showWeatherWidget, + execute: async (input) => { + const output = await input.llm.generateObject({ + messages: [ + { + role: 'system', + content: systemPrompt, + }, + { + role: 'user', + content: `\n${formatChatHistoryAsString(input.chatHistory)}\n\n\n${input.followUp}\n`, + }, + ], + schema, + }); + + if (output.notPresent) { + return; + } + + const params = output; -**Important:** Provide EITHER a location name OR latitude/longitude coordinates, never both. If using location name, set lat/lon to 0. Location should be specific (city, state/region, country) for best results.`, - schema: WeatherWidgetSchema, - execute: async (params, _) => { try { if ( params.location === '' && From c35b684dc53b22d4f15b0be48b299b26b1a189a6 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 5 Dec 2025 21:08:37 +0530 Subject: [PATCH 074/196] feat(types): add ToolMessage, Message --- src/lib/types.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/lib/types.ts b/src/lib/types.ts index 9793077..05cf753 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -3,6 +3,15 @@ export type ChatTurnMessage = { content: string; }; +export type ToolMessage = { + role: 'tool'; + id: string; + name: string; + content: string; +}; + +export type Message = ChatTurnMessage | ToolMessage; + export type Chunk = { content: string; metadata: Record; From ee5d9172a422c469e607f7bcfd3484344c5ec3eb Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 5 Dec 2025 21:16:41 +0530 Subject: [PATCH 075/196] feat(models): add tool, tool call --- src/lib/models/types.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/lib/models/types.ts b/src/lib/models/types.ts index 45560d1..54c3e1e 100644 --- a/src/lib/models/types.ts +++ b/src/lib/models/types.ts @@ -37,18 +37,33 @@ type GenerateOptions = { presencePenalty?: number; }; +type Tool = { + name: string; + description: string; + schema: z.ZodObject; +}; + +type ToolCall = { + id?: string; + name: string; + arguments: Record; +}; + type GenerateTextInput = { messages: ChatTurnMessage[]; + tools?: Tool[]; options?: GenerateOptions; }; type GenerateTextOutput = { content: string; + toolCalls: ToolCall[]; additionalInfo?: Record; }; type StreamTextOutput = { contentChunk: string; + toolCallChunk: Partial[]; additionalInfo?: Record; done?: boolean; }; @@ -83,4 +98,6 @@ export type { GenerateObjectInput, GenerateObjectOutput, StreamObjectOutput, + Tool, + ToolCall, }; From 1c0e90c8e018a6ee4f6da349c83792e6211e4175 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 5 Dec 2025 21:17:28 +0530 Subject: [PATCH 076/196] feat(ollama-llm): implement function calling --- src/lib/models/providers/ollama/ollamaLLM.ts | 40 +++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/lib/models/providers/ollama/ollamaLLM.ts b/src/lib/models/providers/ollama/ollamaLLM.ts index 491dfcd..c0028a6 100644 --- a/src/lib/models/providers/ollama/ollamaLLM.ts +++ b/src/lib/models/providers/ollama/ollamaLLM.ts @@ -7,7 +7,7 @@ import { GenerateTextOutput, StreamTextOutput, } from '../../types'; -import { Ollama } from 'ollama'; +import { Ollama, Tool as OllamaTool } from 'ollama'; import { parse } from 'partial-json'; type OllamaConfig = { @@ -36,9 +36,23 @@ class OllamaLLM extends BaseLLM { } async generateText(input: GenerateTextInput): Promise { + const ollamaTools: OllamaTool[] = []; + + input.tools?.forEach((tool) => { + ollamaTools.push({ + type: 'function', + function: { + name: tool.name, + description: tool.description, + parameters: z.toJSONSchema(tool.schema).properties, + }, + }); + }); + const res = await this.ollamaClient.chat({ model: this.config.model, messages: input.messages, + tools: ollamaTools.length > 0 ? ollamaTools : undefined, options: { top_p: input.options?.topP ?? this.config.options?.topP, temperature: @@ -58,6 +72,11 @@ class OllamaLLM extends BaseLLM { return { content: res.message.content, + toolCalls: + res.message.tool_calls?.map((tc) => ({ + name: tc.function.name, + arguments: tc.function.arguments, + })) || [], additionalInfo: { reasoning: res.message.thinking, }, @@ -67,10 +86,24 @@ class OllamaLLM extends BaseLLM { async *streamText( input: GenerateTextInput, ): AsyncGenerator { + const ollamaTools: OllamaTool[] = []; + + input.tools?.forEach((tool) => { + ollamaTools.push({ + type: 'function', + function: { + name: tool.name, + description: tool.description, + parameters: z.toJSONSchema(tool.schema) as any, + }, + }); + }); + const stream = await this.ollamaClient.chat({ model: this.config.model, messages: input.messages, stream: true, + tools: ollamaTools.length > 0 ? ollamaTools : undefined, options: { top_p: input.options?.topP ?? this.config.options?.topP, temperature: @@ -91,6 +124,11 @@ class OllamaLLM extends BaseLLM { for await (const chunk of stream) { yield { contentChunk: chunk.message.content, + toolCallChunk: + chunk.message.tool_calls?.map((tc) => ({ + name: tc.function.name, + arguments: tc.function.arguments, + })) || [], done: chunk.done, additionalInfo: { reasoning: chunk.message.thinking, From 2c61f470882031855a845a6e0fbe564ed275acb2 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 5 Dec 2025 21:17:43 +0530 Subject: [PATCH 077/196] feat(openai-llm): implement function calling --- src/lib/models/providers/openai/openaiLLM.ts | 89 +++++++++++++++++++- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/src/lib/models/providers/openai/openaiLLM.ts b/src/lib/models/providers/openai/openaiLLM.ts index 22f23d4..e1dc531 100644 --- a/src/lib/models/providers/openai/openaiLLM.ts +++ b/src/lib/models/providers/openai/openaiLLM.ts @@ -7,8 +7,16 @@ import { GenerateTextInput, GenerateTextOutput, StreamTextOutput, + ToolCall, } from '../../types'; import { parse } from 'partial-json'; +import z from 'zod'; +import { + ChatCompletionMessageParam, + ChatCompletionTool, + ChatCompletionToolMessageParam, +} from 'openai/resources/index.mjs'; +import { Message } from '@/lib/types'; type OpenAIConfig = { apiKey: string; @@ -29,10 +37,38 @@ class OpenAILLM extends BaseLLM { }); } + convertToOpenAIMessages(messages: Message[]): ChatCompletionMessageParam[] { + return messages.map((msg) => { + if (msg.role === 'tool') { + return { + role: 'tool', + tool_call_id: msg.id, + content: msg.content, + } as ChatCompletionToolMessageParam; + } + + return msg; + }); + } + async generateText(input: GenerateTextInput): Promise { + const openaiTools: ChatCompletionTool[] = []; + + input.tools?.forEach((tool) => { + openaiTools.push({ + type: 'function', + function: { + name: tool.name, + description: tool.description, + parameters: z.toJSONSchema(tool.schema), + }, + }); + }); + const response = await this.openAIClient.chat.completions.create({ model: this.config.model, - messages: input.messages, + tools: openaiTools.length > 0 ? openaiTools : undefined, + messages: this.convertToOpenAIMessages(input.messages), temperature: input.options?.temperature ?? this.config.options?.temperature ?? 1.0, top_p: input.options?.topP ?? this.config.options?.topP, @@ -49,6 +85,18 @@ class OpenAILLM extends BaseLLM { if (response.choices && response.choices.length > 0) { return { content: response.choices[0].message.content!, + toolCalls: + response.choices[0].message.tool_calls + ?.map((tc) => { + if (tc.type === 'function') { + return { + name: tc.function.name, + id: tc.id, + arguments: JSON.parse(tc.function.arguments), + }; + } + }) + .filter((tc) => tc !== undefined) || [], additionalInfo: { finishReason: response.choices[0].finish_reason, }, @@ -61,9 +109,23 @@ class OpenAILLM extends BaseLLM { async *streamText( input: GenerateTextInput, ): AsyncGenerator { + const openaiTools: ChatCompletionTool[] = []; + + input.tools?.forEach((tool) => { + openaiTools.push({ + type: 'function', + function: { + name: tool.name, + description: tool.description, + parameters: z.toJSONSchema(tool.schema), + }, + }); + }); + const stream = await this.openAIClient.chat.completions.create({ model: this.config.model, - messages: input.messages, + messages: this.convertToOpenAIMessages(input.messages), + tools: openaiTools.length > 0 ? openaiTools : undefined, temperature: input.options?.temperature ?? this.config.options?.temperature ?? 1.0, top_p: input.options?.topP ?? this.config.options?.topP, @@ -78,10 +140,33 @@ class OpenAILLM extends BaseLLM { stream: true, }); + let recievedToolCalls: { name: string; id: string; arguments: string }[] = + []; + for await (const chunk of stream) { if (chunk.choices && chunk.choices.length > 0) { + const toolCalls = chunk.choices[0].delta.tool_calls; yield { contentChunk: chunk.choices[0].delta.content || '', + toolCallChunk: + toolCalls?.map((tc) => { + if (tc.type === 'function') { + const call = { + name: tc.function?.name!, + id: tc.id!, + arguments: tc.function?.arguments || '', + }; + recievedToolCalls.push(call); + return { ...call, arguments: parse(call.arguments || '{}') }; + } else { + const existingCall = recievedToolCalls[tc.index]; + existingCall.arguments += tc.function?.arguments || ''; + return { + ...existingCall, + arguments: parse(existingCall.arguments), + }; + } + }) || [], done: chunk.choices[0].finish_reason !== null, additionalInfo: { finishReason: chunk.choices[0].finish_reason, From a548fd694aa3b69efbfbe3ddf950cc91651248a2 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 5 Dec 2025 21:28:15 +0530 Subject: [PATCH 078/196] feat(utils): compute cosine similarity, remove package --- package.json | 1 - src/lib/utils/computeSimilarity.ts | 21 ++++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 11a9ce4..b742559 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "axios": "^1.8.3", "better-sqlite3": "^11.9.1", "clsx": "^2.1.0", - "compute-cosine-similarity": "^1.1.0", "drizzle-orm": "^0.40.1", "framer-motion": "^12.23.24", "html-to-text": "^9.0.5", diff --git a/src/lib/utils/computeSimilarity.ts b/src/lib/utils/computeSimilarity.ts index eda2a59..4cf90c8 100644 --- a/src/lib/utils/computeSimilarity.ts +++ b/src/lib/utils/computeSimilarity.ts @@ -1,7 +1,22 @@ -import cosineSimilarity from 'compute-cosine-similarity'; - const computeSimilarity = (x: number[], y: number[]): number => { - return cosineSimilarity(x, y) as number; + if (x.length !== y.length) + throw new Error('Vectors must be of the same length'); + + let dotProduct = 0; + let normA = 0; + let normB = 0; + + for (let i = 0; i < x.length; i++) { + dotProduct += x[i] * y[i]; + normA += x[i] * x[i]; + normB += y[i] * y[i]; + } + + if (normA === 0 || normB === 0) { + return 0; + } + + return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); }; export default computeSimilarity; From 4fc810d9769af82bce1069a48aa840b0f1747b87 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 5 Dec 2025 21:30:41 +0530 Subject: [PATCH 079/196] feat(calculation-widget): enhance UI --- src/components/Widgets/Calculation.tsx | 32 ++++++++++---------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/src/components/Widgets/Calculation.tsx b/src/components/Widgets/Calculation.tsx index e671aab..54d7ef3 100644 --- a/src/components/Widgets/Calculation.tsx +++ b/src/components/Widgets/Calculation.tsx @@ -9,38 +9,30 @@ type CalculationWidgetProps = { const Calculation = ({ expression, result }: CalculationWidgetProps) => { return ( -
-
-
- -
- - Calculation - -
- -
-
-
- +
+
+
+
+ + Expression
-
+
{expression}
-
-
- - +
+
+ + Result
-
+
{result.toLocaleString()}
From 65ef299d728d58e9e06585e0ec9cf32a11d31258 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 6 Dec 2025 15:22:06 +0530 Subject: [PATCH 080/196] feat(settings): display app version, link --- next.config.mjs | 5 ++ src/components/Settings/SettingsDialogue.tsx | 74 ++++++++++++-------- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/next.config.mjs b/next.config.mjs index 2300ff4..5770f76 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,3 +1,5 @@ +import pkg from './package.json' with { type: 'json' }; + /** @type {import('next').NextConfig} */ const nextConfig = { output: 'standalone', @@ -9,6 +11,9 @@ const nextConfig = { ], }, serverExternalPackages: ['pdf-parse'], + env: { + NEXT_PUBLIC_VERSION: pkg.version, + }, }; export default nextConfig; diff --git a/src/components/Settings/SettingsDialogue.tsx b/src/components/Settings/SettingsDialogue.tsx index ba097a9..f42ce9c 100644 --- a/src/components/Settings/SettingsDialogue.tsx +++ b/src/components/Settings/SettingsDialogue.tsx @@ -3,6 +3,7 @@ import { ArrowLeft, BrainCog, ChevronLeft, + ExternalLink, Search, Sliders, ToggleRight, @@ -115,35 +116,52 @@ const SettingsDialogue = ({
) : (
-
- + +
+ {sections.map((section) => ( + + ))} +
+
+
+

+ Version: {process.env.NEXT_PUBLIC_VERSION}

- -
- {sections.map((section) => ( - - ))} + + GitHub + +
From f2f2af9451e8ae0da6b41824ff103a9a349b8bb1 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 6 Dec 2025 15:24:15 +0530 Subject: [PATCH 081/196] feat(message-input): hide content after input --- src/components/Chat.tsx | 21 ++++++++++++++++----- src/components/MessageInput.tsx | 6 ++---- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index 56c13e6..f0432ca 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -59,7 +59,7 @@ const Chat = () => { }, [messages]); return ( -
+
{sections.map((section, i) => { const isLast = i === sections.length - 1; @@ -80,10 +80,21 @@ const Chat = () => { {loading && !messageAppeared && }
{dividerWidth > 0 && ( -
+
+
+
)} diff --git a/src/components/MessageInput.tsx b/src/components/MessageInput.tsx index d1fc989..c682cf0 100644 --- a/src/components/MessageInput.tsx +++ b/src/components/MessageInput.tsx @@ -2,9 +2,7 @@ import { cn } from '@/lib/utils'; import { ArrowUp } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import TextareaAutosize from 'react-textarea-autosize'; -import Attach from './MessageInputActions/Attach'; import CopilotToggle from './MessageInputActions/Copilot'; -import { File } from './ChatWindow'; import AttachSmall from './MessageInputActions/AttachSmall'; import { useChat } from '@/lib/hooks/useChat'; @@ -64,7 +62,7 @@ const MessageInput = () => { } }} className={cn( - 'bg-light-secondary dark:bg-dark-secondary p-4 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 shadow-sm shadow-light-200/10 dark:shadow-black/20 transition-all duration-200 focus-within:border-light-300 dark:focus-within:border-dark-300', + 'relative bg-light-secondary dark:bg-dark-secondary p-4 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 shadow-sm shadow-light-200/10 dark:shadow-black/20 transition-all duration-200 focus-within:border-light-300 dark:focus-within:border-dark-300', mode === 'multi' ? 'flex-col rounded-2xl' : 'flex-row rounded-full', )} > @@ -103,7 +101,7 @@ const MessageInput = () => { /> From 574b3d55e235db10cb0b68a0288089ae2bb7e53c Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 6 Dec 2025 15:24:46 +0530 Subject: [PATCH 082/196] feat(types): separate user, assistant & system message --- src/lib/types.ts | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/lib/types.ts b/src/lib/types.ts index 05cf753..dfc3b72 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,5 +1,18 @@ -export type ChatTurnMessage = { - role: 'user' | 'assistant' | 'system'; +import { ToolCall } from './models/types'; + +export type SystemMessage = { + role: 'system'; + content: string; +}; + +export type AssistantMessage = { + role: 'assistant'; + content: string; + tool_calls?: ToolCall[]; +}; + +export type UserMessage = { + role: 'user'; content: string; }; @@ -10,7 +23,13 @@ export type ToolMessage = { content: string; }; -export type Message = ChatTurnMessage | ToolMessage; +export type ChatTurnMessage = UserMessage | AssistantMessage; + +export type Message = + | UserMessage + | AssistantMessage + | SystemMessage + | ToolMessage; export type Chunk = { content: string; From e99c8bdd502ccaee8ed4b1038b4fd75d97a2d701 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 6 Dec 2025 15:25:15 +0530 Subject: [PATCH 083/196] feat(models-types): update to use `Message` --- src/lib/models/types.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/models/types.ts b/src/lib/models/types.ts index 54c3e1e..8abefd7 100644 --- a/src/lib/models/types.ts +++ b/src/lib/models/types.ts @@ -1,5 +1,5 @@ import z from 'zod'; -import { ChatTurnMessage } from '../types'; +import { Message } from '../types'; type Model = { name: string; @@ -44,13 +44,13 @@ type Tool = { }; type ToolCall = { - id?: string; + id: string; name: string; arguments: Record; }; type GenerateTextInput = { - messages: ChatTurnMessage[]; + messages: Message[]; tools?: Tool[]; options?: GenerateOptions; }; @@ -63,14 +63,14 @@ type GenerateTextOutput = { type StreamTextOutput = { contentChunk: string; - toolCallChunk: Partial[]; + toolCallChunk: ToolCall[]; additionalInfo?: Record; done?: boolean; }; type GenerateObjectInput = { schema: z.ZodTypeAny; - messages: ChatTurnMessage[]; + messages: Message[]; options?: GenerateOptions; }; From 3c524b0f988648e79ee72e88c96569641acf3c38 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 6 Dec 2025 15:25:48 +0530 Subject: [PATCH 084/196] feat(openai-llm): process assistant message with tool calls --- src/lib/models/providers/openai/openaiLLM.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/lib/models/providers/openai/openaiLLM.ts b/src/lib/models/providers/openai/openaiLLM.ts index e1dc531..a40714e 100644 --- a/src/lib/models/providers/openai/openaiLLM.ts +++ b/src/lib/models/providers/openai/openaiLLM.ts @@ -12,6 +12,7 @@ import { import { parse } from 'partial-json'; import z from 'zod'; import { + ChatCompletionAssistantMessageParam, ChatCompletionMessageParam, ChatCompletionTool, ChatCompletionToolMessageParam, @@ -45,6 +46,22 @@ class OpenAILLM extends BaseLLM { tool_call_id: msg.id, content: msg.content, } as ChatCompletionToolMessageParam; + } else if (msg.role === 'assistant') { + return { + role: 'assistant', + content: msg.content, + ...(msg.tool_calls && + msg.tool_calls.length > 0 && { + tool_calls: msg.tool_calls?.map((tc) => ({ + id: tc.id, + type: 'function', + function: { + name: tc.name, + arguments: JSON.stringify(tc.arguments), + }, + })), + }), + } as ChatCompletionAssistantMessageParam; } return msg; @@ -178,7 +195,7 @@ class OpenAILLM extends BaseLLM { async generateObject(input: GenerateObjectInput): Promise { const response = await this.openAIClient.chat.completions.parse({ - messages: input.messages, + messages: this.convertToOpenAIMessages(input.messages), model: this.config.model, temperature: input.options?.temperature ?? this.config.options?.temperature ?? 1.0, From 4c4c1d19301a28105b9d068f065f1927bab0ab0b Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 6 Dec 2025 15:31:35 +0530 Subject: [PATCH 085/196] feat(ollama-llm): process ollama messages with tool calls --- src/lib/models/providers/ollama/ollamaLLM.ts | 47 +++++++++++++++++--- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/src/lib/models/providers/ollama/ollamaLLM.ts b/src/lib/models/providers/ollama/ollamaLLM.ts index c0028a6..7e53786 100644 --- a/src/lib/models/providers/ollama/ollamaLLM.ts +++ b/src/lib/models/providers/ollama/ollamaLLM.ts @@ -7,8 +7,10 @@ import { GenerateTextOutput, StreamTextOutput, } from '../../types'; -import { Ollama, Tool as OllamaTool } from 'ollama'; +import { Ollama, Tool as OllamaTool, Message as OllamaMessage } from 'ollama'; import { parse } from 'partial-json'; +import crypto from 'crypto'; +import { Message } from '@/lib/types'; type OllamaConfig = { baseURL: string; @@ -35,6 +37,33 @@ class OllamaLLM extends BaseLLM { }); } + convertToOllamaMessages(messages: Message[]): OllamaMessage[] { + return messages.map((msg) => { + if (msg.role === 'tool') { + return { + role: 'tool', + tool_name: msg.name, + content: msg.content, + } as OllamaMessage; + } else if (msg.role === 'assistant') { + return { + role: 'assistant', + content: msg.content, + tool_calls: + msg.tool_calls?.map((tc, i) => ({ + function: { + index: i, + name: tc.name, + arguments: tc.arguments, + }, + })) || [], + }; + } + + return msg; + }); + } + async generateText(input: GenerateTextInput): Promise { const ollamaTools: OllamaTool[] = []; @@ -51,8 +80,11 @@ class OllamaLLM extends BaseLLM { const res = await this.ollamaClient.chat({ model: this.config.model, - messages: input.messages, + messages: this.convertToOllamaMessages(input.messages), tools: ollamaTools.length > 0 ? ollamaTools : undefined, + ...(reasoningModels.find((m) => this.config.model.includes(m)) + ? { think: false } + : {}), options: { top_p: input.options?.topP ?? this.config.options?.topP, temperature: @@ -74,6 +106,7 @@ class OllamaLLM extends BaseLLM { content: res.message.content, toolCalls: res.message.tool_calls?.map((tc) => ({ + id: crypto.randomUUID(), name: tc.function.name, arguments: tc.function.arguments, })) || [], @@ -101,8 +134,11 @@ class OllamaLLM extends BaseLLM { const stream = await this.ollamaClient.chat({ model: this.config.model, - messages: input.messages, + messages: this.convertToOllamaMessages(input.messages), stream: true, + ...(reasoningModels.find((m) => this.config.model.includes(m)) + ? { think: false } + : {}), tools: ollamaTools.length > 0 ? ollamaTools : undefined, options: { top_p: input.options?.topP ?? this.config.options?.topP, @@ -126,6 +162,7 @@ class OllamaLLM extends BaseLLM { contentChunk: chunk.message.content, toolCallChunk: chunk.message.tool_calls?.map((tc) => ({ + id: crypto.randomUUID(), name: tc.function.name, arguments: tc.function.arguments, })) || [], @@ -140,7 +177,7 @@ class OllamaLLM extends BaseLLM { async generateObject(input: GenerateObjectInput): Promise { const response = await this.ollamaClient.chat({ model: this.config.model, - messages: input.messages, + messages: this.convertToOllamaMessages(input.messages), format: z.toJSONSchema(input.schema), ...(reasoningModels.find((m) => this.config.model.includes(m)) ? { think: false } @@ -173,7 +210,7 @@ class OllamaLLM extends BaseLLM { const stream = await this.ollamaClient.chat({ model: this.config.model, - messages: input.messages, + messages: this.convertToOllamaMessages(input.messages), format: z.toJSONSchema(input.schema), stream: true, ...(reasoningModels.find((m) => this.config.model.includes(m)) From 5e3001756b0d17e1808119c4140236d2fae4b680 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 6 Dec 2025 15:31:57 +0530 Subject: [PATCH 086/196] feat(search-types0: add reasoning action --- src/lib/agents/search/types.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts index 589fa2d..e83ac7d 100644 --- a/src/lib/agents/search/types.ts +++ b/src/lib/agents/search/types.ts @@ -84,7 +84,15 @@ export type DoneActionOutput = { type: 'done'; }; -export type ActionOutput = SearchActionOutput | DoneActionOutput; +export type ReasoningResearchAction = { + type: 'reasoning'; + reasoning: string; +}; + +export type ActionOutput = + | SearchActionOutput + | DoneActionOutput + | ReasoningResearchAction; export interface ResearchAction< TSchema extends z.ZodObject = z.ZodObject, @@ -98,8 +106,3 @@ export interface ResearchAction< additionalConfig: AdditionalConfig, ) => Promise; } - -export type ActionConfig = { - type: string; - params: Record; -}; From 8ab675b119099b20423d9a3e5e38cb566f5f7cbf Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 6 Dec 2025 15:37:36 +0530 Subject: [PATCH 087/196] feat(action-registry): use tool types, add tool methods --- .../search/researcher/actions/registry.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/lib/agents/search/researcher/actions/registry.ts b/src/lib/agents/search/researcher/actions/registry.ts index 4172e8b..5a05ac1 100644 --- a/src/lib/agents/search/researcher/actions/registry.ts +++ b/src/lib/agents/search/researcher/actions/registry.ts @@ -1,5 +1,5 @@ +import { Tool, ToolCall } from '@/lib/models/types'; import { - ActionConfig, ActionOutput, AdditionalConfig, ClassifierOutput, @@ -25,6 +25,18 @@ class ActionRegistry { ); } + static getAvailableActionTools(config: { + classification: ClassifierOutput; + }): Tool[] { + const availableActions = this.getAvailableActions(config); + + return availableActions.map((action) => ({ + name: action.name, + description: action.description, + schema: action.schema, + })); + } + static getAvailableActionsDescriptions(config: { classification: ClassifierOutput; }): string { @@ -50,7 +62,7 @@ class ActionRegistry { } static async executeAll( - actions: ActionConfig[], + actions: ToolCall[], additionalConfig: AdditionalConfig, ): Promise { const results: ActionOutput[] = []; @@ -58,8 +70,8 @@ class ActionRegistry { await Promise.all( actions.map(async (actionConfig) => { const output = await this.execute( - actionConfig.type, - actionConfig.params, + actionConfig.name, + actionConfig.arguments, additionalConfig, ); results.push(output); From 97838fd693e3236685fdfa7e6f529d5e2eecf179 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 6 Dec 2025 15:38:07 +0530 Subject: [PATCH 088/196] feat(actions): add plan, update done & web search --- .../agents/search/researcher/actions/done.ts | 6 ++--- .../agents/search/researcher/actions/plan.ts | 26 +++++++++++++++++++ .../search/researcher/actions/webSearch.ts | 14 +++++----- 3 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 src/lib/agents/search/researcher/actions/plan.ts diff --git a/src/lib/agents/search/researcher/actions/done.ts b/src/lib/agents/search/researcher/actions/done.ts index 9ce93ae..bfed3e2 100644 --- a/src/lib/agents/search/researcher/actions/done.ts +++ b/src/lib/agents/search/researcher/actions/done.ts @@ -4,11 +4,9 @@ import { ResearchAction } from '../../types'; const doneAction: ResearchAction = { name: 'done', description: - "Indicates that the research process is complete and no further actions are needed. Use this action when you have gathered sufficient information to answer the user's query.", + 'Only call this after ___plan AND after any other needed tool calls when you truly have enough to answer. Do not call if information is still missing.', enabled: (_) => true, - schema: z.object({ - type: z.literal('done'), - }), + schema: z.object({}), execute: async (params, additionalConfig) => { return { type: 'done', diff --git a/src/lib/agents/search/researcher/actions/plan.ts b/src/lib/agents/search/researcher/actions/plan.ts new file mode 100644 index 0000000..d4d7661 --- /dev/null +++ b/src/lib/agents/search/researcher/actions/plan.ts @@ -0,0 +1,26 @@ +import z from 'zod'; +import { ResearchAction } from '../../types'; + +const schema = z.object({ + plan: z + .string() + .describe( + 'A concise natural-language plan in one short paragraph. Open with a short intent phrase (e.g., "Okay, the user wants to...", "Searching for...", "Looking into...") and lay out the steps you will take.', + ), +}); + +const planAction: ResearchAction = { + name: '___plan', + description: + 'Use this FIRST on every turn to state your plan in natural language before any other action. Keep it short, action-focused, and tailored to the current query.', + schema: schema, + enabled: (_) => true, + execute: async (input, _) => { + return { + type: 'reasoning', + reasoning: input.plan, + }; + }, +}; + +export default planAction; diff --git a/src/lib/agents/search/researcher/actions/webSearch.ts b/src/lib/agents/search/researcher/actions/webSearch.ts index 17f5e61..bf89b5a 100644 --- a/src/lib/agents/search/researcher/actions/webSearch.ts +++ b/src/lib/agents/search/researcher/actions/webSearch.ts @@ -11,22 +11,20 @@ const actionSchema = z.object({ }); const actionDescription = ` -You have to use this action aggressively to find relevant information from the web to answer user queries. You can combine this action with other actions to gather comprehensive data. Always ensure that you provide accurate and up-to-date information by leveraging web search results. -When this action is present, you must use it to obtain current information from the web. +Use immediately after the ___plan call when you need information. Default to using this unless you already have everything needed to finish. Provide 1-3 short, SEO-friendly queries (keywords, not sentences) that cover the user ask. Always prefer current/contextual queries (e.g., include year for news). -### How to use: -1. For speed search mode, you can use this action once. Make sure to cover all aspects of the user's query in that single search. -2. If you're on quality mode, you'll get to use this action up to two times. Use the first search to gather general information, and the second search to fill in any gaps or get more specific details based on the initial findings. -3. If you're set on quality mode, then you will get to use this action multiple times to gather more information. Use your judgment to decide when additional searches are necessary to provide a thorough and accurate response. +For fast mode, you can only use this tool once so make sure to get all needed information in one go. +For balanced and quality modes, you can use this tool multiple times as needed. -Input: An array of search queries. Make sure the queries are relevant to the user's request and cover different aspects if necessary. You can include a maximum of 3 queries. Make sure the queries are SEO friendly and not sentences rather keywords which can be used to search a search engine like Google, Bing, etc. +In quality and balanced mode, first try to gather upper level information with broad queries, then use more specific queries based on what you find to find all information needed. `; const webSearchAction: ResearchAction = { name: 'web_search', description: actionDescription, schema: actionSchema, - enabled: (config) => true, + enabled: (config) => + config.classification.classification.skipSearch === false, execute: async (input, _) => { let results: Chunk[] = []; From 2d82cd65d913a5b668ac68c77ca17715dcaabc50 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 6 Dec 2025 15:38:20 +0530 Subject: [PATCH 089/196] feat(registry): register plan action --- src/lib/agents/search/researcher/actions/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/agents/search/researcher/actions/index.ts b/src/lib/agents/search/researcher/actions/index.ts index 4814b1e..c3790a5 100644 --- a/src/lib/agents/search/researcher/actions/index.ts +++ b/src/lib/agents/search/researcher/actions/index.ts @@ -1,8 +1,10 @@ import doneAction from './done'; +import planAction from './plan'; import ActionRegistry from './registry'; import webSearchAction from './webSearch'; ActionRegistry.register(webSearchAction); ActionRegistry.register(doneAction); +ActionRegistry.register(planAction); export { ActionRegistry }; From 9afea48d313ec2925d199a22b7f512839a2f748b Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 6 Dec 2025 15:38:40 +0530 Subject: [PATCH 090/196] feat(search-agent): use function calling --- src/lib/agents/search/researcher/index.ts | 258 +++++++++++----------- 1 file changed, 131 insertions(+), 127 deletions(-) diff --git a/src/lib/agents/search/researcher/index.ts b/src/lib/agents/search/researcher/index.ts index 9ca1fed..a5dce5a 100644 --- a/src/lib/agents/search/researcher/index.ts +++ b/src/lib/agents/search/researcher/index.ts @@ -1,43 +1,28 @@ -import z from 'zod'; -import { - ActionConfig, - ActionOutput, - ResearcherInput, - ResearcherOutput, -} from '../types'; +import { ActionOutput, ResearcherInput, ResearcherOutput } from '../types'; import { ActionRegistry } from './actions'; import { getResearcherPrompt } from '@/lib/prompts/search/researcher'; import SessionManager from '@/lib/session'; -import { ReasoningResearchBlock } from '@/lib/types'; +import { Message, ReasoningResearchBlock } from '@/lib/types'; import formatChatHistoryAsString from '@/lib/utils/formatHistory'; +import { ToolCall } from '@/lib/models/types'; class Researcher { async research( session: SessionManager, input: ResearcherInput, ): Promise { - let findings: string = ''; let actionOutput: ActionOutput[] = []; let maxIteration = input.config.mode === 'speed' - ? 1 + ? 2 : input.config.mode === 'balanced' - ? 3 + ? 6 : 25; - const availableActions = ActionRegistry.getAvailableActions({ + const availableTools = ActionRegistry.getAvailableActionTools({ classification: input.classification, }); - const schema = z.object({ - reasoning: z - .string() - .describe('The reasoning behind choosing the next action.'), - action: z - .union(availableActions.map((a) => a.schema)) - .describe('The action to be performed next.'), - }); - const availableActionsDescription = ActionRegistry.getAvailableActionsDescriptions({ classification: input.classification, @@ -53,6 +38,18 @@ class Researcher { }, }); + const agentMessageHistory: Message[] = [ + { + role: 'user', + content: ` + + ${formatChatHistoryAsString(input.chatHistory.slice(-10))} + User: ${input.followUp} (Standalone question: ${input.classification.standaloneFollowUp}) + + `, + }, + ]; + for (let i = 0; i < maxIteration; i++) { const researcherPrompt = getResearcherPrompt( availableActionsDescription, @@ -61,27 +58,15 @@ class Researcher { maxIteration, ); - const actionStream = input.config.llm.streamObject({ + const actionStream = input.config.llm.streamText({ messages: [ { role: 'system', content: researcherPrompt, }, - { - role: 'user', - content: ` - - ${formatChatHistoryAsString(input.chatHistory.slice(-10))} - User: ${input.followUp} (Standalone question: ${input.classification.standaloneFollowUp}) - - - - ${findings} - - `, - }, + ...agentMessageHistory, ], - schema, + tools: availableTools, }); const block = session.getBlock(researchBlockId); @@ -89,43 +74,26 @@ class Researcher { let reasoningEmitted = false; let reasoningId = crypto.randomUUID(); - let finalActionRes: any; + let finalToolCalls: ToolCall[] = []; for await (const partialRes of actionStream) { - try { - if ( - partialRes.reasoning && - !reasoningEmitted && - block && - block.type === 'research' - ) { - reasoningEmitted = true; - block.data.subSteps.push({ - id: reasoningId, - type: 'reasoning', - reasoning: partialRes.reasoning, - }); - session.updateBlock(researchBlockId, [ - { - op: 'replace', - path: '/data/subSteps', - value: block.data.subSteps, - }, - ]); - } else if ( - partialRes.reasoning && - reasoningEmitted && - block && - block.type === 'research' - ) { - const subStepIndex = block.data.subSteps.findIndex( - (step: any) => step.id === reasoningId, - ); - if (subStepIndex !== -1) { - const subStep = block.data.subSteps[ - subStepIndex - ] as ReasoningResearchBlock; - subStep.reasoning = partialRes.reasoning; + if (partialRes.toolCallChunk.length > 0) { + partialRes.toolCallChunk.forEach((tc) => { + if ( + tc.name === '___plan' && + tc.arguments['plan'] && + !reasoningEmitted && + block && + block.type === 'research' + ) { + reasoningEmitted = true; + + block.data.subSteps.push({ + id: reasoningId, + type: 'reasoning', + reasoning: tc.arguments['plan'], + }); + session.updateBlock(researchBlockId, [ { op: 'replace', @@ -133,77 +101,118 @@ class Researcher { value: block.data.subSteps, }, ]); - } - } + } else if ( + tc.name === '___plan' && + tc.arguments['plan'] && + reasoningEmitted && + block && + block.type === 'research' + ) { + const subStepIndex = block.data.subSteps.findIndex( + (step: any) => step.id === reasoningId, + ); - finalActionRes = partialRes; - } catch (e) { - // nothing + if (subStepIndex !== -1) { + const subStep = block.data.subSteps[ + subStepIndex + ] as ReasoningResearchBlock; + subStep.reasoning = tc.arguments['plan']; + session.updateBlock(researchBlockId, [ + { + op: 'replace', + path: '/data/subSteps', + value: block.data.subSteps, + }, + ]); + } + } + + const existingIndex = finalToolCalls.findIndex( + (ftc) => ftc.id === tc.id, + ); + + if (existingIndex !== -1) { + finalToolCalls[existingIndex].arguments = tc.arguments; + } else { + finalToolCalls.push(tc); + } + }); } } - if (finalActionRes.action.type === 'done') { + if (finalToolCalls.length === 0) { break; } - const actionConfig: ActionConfig = { - type: finalActionRes.action.type as string, - params: finalActionRes.action, - }; + if (finalToolCalls[finalToolCalls.length - 1].name === 'done') { + break; + } - const queries = actionConfig.params.queries || []; - if (block && block.type === 'research') { + agentMessageHistory.push({ + role: 'assistant', + content: '', + tool_calls: finalToolCalls, + }); + + const searchCalls = finalToolCalls.filter( + (tc) => + tc.name === 'web_search' || + tc.name === 'academic_search' || + tc.name === 'discussion_search', + ); + + if (searchCalls.length > 0 && block && block.type === 'research') { block.data.subSteps.push({ id: crypto.randomUUID(), type: 'searching', - searching: queries, + searching: searchCalls.map((sc) => sc.arguments.queries).flat(), }); + session.updateBlock(researchBlockId, [ - { op: 'replace', path: '/data/subSteps', value: block.data.subSteps }, + { + op: 'replace', + path: '/data/subSteps', + value: block.data.subSteps, + }, ]); } - findings += `\n---\nIteration ${i + 1}:\n`; - findings += 'Reasoning: ' + finalActionRes.reasoning + '\n'; - findings += `Executing Action: ${actionConfig.type} with params ${JSON.stringify(actionConfig.params)}\n`; + const actionResults = await ActionRegistry.executeAll(finalToolCalls, { + llm: input.config.llm, + embedding: input.config.embedding, + session: session, + }); - const actionResult = await ActionRegistry.execute( - actionConfig.type, - actionConfig.params, - { - llm: input.config.llm, - embedding: input.config.embedding, - session: session, - }, + actionOutput.push(...actionResults); + + actionResults.forEach((action, i) => { + agentMessageHistory.push({ + role: 'tool', + id: finalToolCalls[i].id, + name: finalToolCalls[i].name, + content: JSON.stringify(action), + }); + }); + + const searchResults = actionResults.filter( + (a) => a.type === 'search_results', ); - actionOutput.push(actionResult); + if (searchResults.length > 0 && block && block.type === 'research') { + block.data.subSteps.push({ + id: crypto.randomUUID(), + type: 'reading', + reading: searchResults.flatMap((a) => a.results), + }); - if (actionResult.type === 'search_results') { - if (block && block.type === 'research') { - block.data.subSteps.push({ - id: crypto.randomUUID(), - type: 'reading', - reading: actionResult.results, - }); - session.updateBlock(researchBlockId, [ - { - op: 'replace', - path: '/data/subSteps', - value: block.data.subSteps, - }, - ]); - } - - findings += actionResult.results - .map( - (r) => - `Title: ${r.metadata.title}\nURL: ${r.metadata.url}\nContent: ${r.content}\n`, - ) - .join('\n'); + session.updateBlock(researchBlockId, [ + { + op: 'replace', + path: '/data/subSteps', + value: block.data.subSteps, + }, + ]); } - - findings += '\n---------\n'; } const searchResults = actionOutput.filter( @@ -212,12 +221,7 @@ class Researcher { session.emit('data', { type: 'sources', - data: searchResults - .flatMap((a) => a.results) - .map((r) => ({ - content: r.content, - metadata: r.metadata, - })), + data: searchResults.flatMap((a) => a.results), }); return { From a14f3e94643ba7dc2b2b99b0acb78312f0839737 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 6 Dec 2025 15:38:55 +0530 Subject: [PATCH 091/196] feat(prompts): update researcher prompt --- src/lib/prompts/search/researcher.ts | 30 ++++++++++------------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/lib/prompts/search/researcher.ts b/src/lib/prompts/search/researcher.ts index f77559f..08eeaf9 100644 --- a/src/lib/prompts/search/researcher.ts +++ b/src/lib/prompts/search/researcher.ts @@ -37,14 +37,8 @@ NEVER ASSUME - your knowledge may be outdated. When a user asks about something - -Think like a human would. Your reasoning should be natural and show: -- What the user is asking for -- What you need to find out or do -- Your plan to accomplish it - -Keep it to 2-3 natural sentences. - +You never speak your reasoning to the user. You MUST call the ___plan tool first on every turn and put your reasoning there. +The plan must be 2-4 concise sentences, starting with "Okay, the user wants to..." and outlining the steps you will take next. @@ -239,17 +233,13 @@ Actions: web_search ["best Italian restaurant near me", "top rated Italian resta - -Reasoning should be 2-3 natural sentences showing your thought process and plan. Then select and configure the appropriate action(s). - -Always respond in the following JSON format and never deviate from it or output any extra text: -{ - "reasoning": "", - "actions": [ - {"type": "", "param1": "value1", "...": "..."}, - ... - ] -} - + +- NEVER output normal text to the user. ONLY call tools. +- Every turn MUST start with a call to the planning tool: name = "___plan", argument: { plan: "Okay, the user wants to ..." + concise 2-4 sentence plan }. +- Immediately after ___plan, if any information is missing, call \`web_search\` with up to 3 targeted queries. Default to searching unless you are certain you have enough. +- Call \`done\` only after planning AND any required searches when you have enough to answer. +- Do not invent tools. Do not return JSON. Do not echo the plan outside of the tool call. +- If nothing else is needed after planning, call \`done\` immediately after the plan. + `; }; From d0124b9f06a084d1ec4d849c3347b7595969f089 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 6 Dec 2025 18:54:37 +0530 Subject: [PATCH 092/196] feat(actions): add scrape URL action --- package.json | 2 + .../agents/search/researcher/actions/index.ts | 2 + .../search/researcher/actions/scrapeURL.ts | 57 +++++++++++++++++++ yarn.lock | 53 ++++++----------- 4 files changed, 78 insertions(+), 36 deletions(-) create mode 100644 src/lib/agents/search/researcher/actions/scrapeURL.ts diff --git a/package.json b/package.json index b742559..18c5ad8 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "rfc6902": "^5.1.2", "sonner": "^1.4.41", "tailwind-merge": "^2.2.2", + "turndown": "^7.2.2", "winston": "^3.17.0", "yahoo-finance2": "^3.10.2", "yet-another-react-lightbox": "^3.17.2", @@ -65,6 +66,7 @@ "@types/pdf-parse": "^1.1.4", "@types/react": "^18", "@types/react-dom": "^18", + "@types/turndown": "^5.0.6", "autoprefixer": "^10.0.1", "drizzle-kit": "^0.30.5", "eslint": "^8", diff --git a/src/lib/agents/search/researcher/actions/index.ts b/src/lib/agents/search/researcher/actions/index.ts index c3790a5..15c5734 100644 --- a/src/lib/agents/search/researcher/actions/index.ts +++ b/src/lib/agents/search/researcher/actions/index.ts @@ -1,10 +1,12 @@ import doneAction from './done'; import planAction from './plan'; import ActionRegistry from './registry'; +import scrapeURLAction from './scrapeURL'; import webSearchAction from './webSearch'; ActionRegistry.register(webSearchAction); ActionRegistry.register(doneAction); ActionRegistry.register(planAction); +ActionRegistry.register(scrapeURLAction); export { ActionRegistry }; diff --git a/src/lib/agents/search/researcher/actions/scrapeURL.ts b/src/lib/agents/search/researcher/actions/scrapeURL.ts new file mode 100644 index 0000000..0fd3f7b --- /dev/null +++ b/src/lib/agents/search/researcher/actions/scrapeURL.ts @@ -0,0 +1,57 @@ +import z from 'zod'; +import { ResearchAction } from '../../types'; +import { Chunk } from '@/lib/types'; +import TurnDown from 'turndown'; + +const turndownService = new TurnDown(); + +const schema = z.object({ + urls: z.array(z.string()).describe('A list of URLs to scrape content from.'), +}); + +const scrapeURLAction: ResearchAction = { + name: 'scrape_url', + description: + 'Use after __plan to scrape and extract content from the provided URLs. This is useful when you need detailed information from specific web pages or if the user asks you to summarize or analyze content from certain links.', + schema: schema, + enabled: (_) => true, + execute: async (params, additionalConfig) => { + const results: Chunk[] = []; + + await Promise.all( + params.urls.map(async (url) => { + try { + const res = await fetch(url); + const text = await res.text(); + + const title = + text.match(/(.*?)<\/title>/i)?.[1] || `Content from ${url}`; + const markdown = turndownService.turndown(text); + + results.push({ + content: markdown, + metadata: { + url, + title: title, + }, + }); + } catch (error) { + results.push({ + content: `Failed to fetch content from ${url}: ${error}`, + metadata: { + url, + title: `Error fetching ${url}`, + }, + }); + } + }), + ); + + return { + type: 'search_results', + results, + }; + }, +}; + +export default scrapeURLAction; diff --git a/yarn.lock b/yarn.lock index e20ba09..da3e2df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -888,6 +888,11 @@ dependencies: js-tiktoken "^1.0.12" +"@mixmark-io/domino@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@mixmark-io/domino/-/domino-2.2.0.tgz#4e8ec69bf1afeb7a14f0628b7e2c0f35bdb336c3" + integrity sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw== + "@next/env@15.2.2": version "15.2.2" resolved "https://registry.yarnpkg.com/@next/env/-/env-15.2.2.tgz#6345352365a811c523cecf284874ff489b675e59" @@ -1227,6 +1232,11 @@ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== +"@types/turndown@^5.0.6": + version "5.0.6" + resolved "https://registry.yarnpkg.com/@types/turndown/-/turndown-5.0.6.tgz#42a27397298a312d6088f29c0ff4819c518c1ecb" + integrity sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg== + "@types/uuid@^10.0.0": version "10.0.0" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" @@ -1840,32 +1850,6 @@ complex.js@^2.2.5: resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.4.3.tgz#72ee9c303a9b89ebcfeca0d39f74927d38721fce" integrity sha512-UrQVSUur14tNX6tiP4y8T4w4FeJAX3bi2cIv0pu/DTLFNxoq7z2Yh83Vfzztj6Px3X/lubqQ9IrPp7Bpn6p4MQ== -compute-cosine-similarity@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/compute-cosine-similarity/-/compute-cosine-similarity-1.1.0.tgz#0086a06b0239deb90f231f0da894efdc48884609" - integrity sha512-FXhNx0ILLjGi9Z9+lglLzM12+0uoTnYkHm7GiadXDAr0HGVLm25OivUS1B/LPkbzzvlcXz/1EvWg9ZYyJSdhTw== - dependencies: - compute-dot "^1.1.0" - compute-l2norm "^1.1.0" - validate.io-array "^1.0.5" - validate.io-function "^1.0.2" - -compute-dot@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/compute-dot/-/compute-dot-1.1.0.tgz#01a5ba2c7af73b99002acb258459c9576a8232dc" - integrity sha512-L5Ocet4DdMrXboss13K59OK23GXjiSia7+7Ukc7q4Bl+RVpIXK2W9IHMbWDZkh+JUEvJAwOKRaJDiFUa1LTnJg== - dependencies: - validate.io-array "^1.0.3" - validate.io-function "^1.0.2" - -compute-l2norm@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/compute-l2norm/-/compute-l2norm-1.1.0.tgz#bd09131c6b36c8d70c68334e176009a4e0a989ac" - integrity sha512-6EHh1Elj90eU28SXi+h2PLnTQvZmkkHWySpoFz+WOlVNLz3DQoC4ISUHSV9n5jMxPHtKGJ01F4uu2PsXBB8sSg== - dependencies: - validate.io-array "^1.0.3" - validate.io-function "^1.0.2" - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -5368,6 +5352,13 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +turndown@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/turndown/-/turndown-7.2.2.tgz#9557642b54046c5912b3d433f34dd588de455a43" + integrity sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ== + dependencies: + "@mixmark-io/domino" "^2.2.0" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -5546,16 +5537,6 @@ uuid@^9.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== -validate.io-array@^1.0.3, validate.io-array@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/validate.io-array/-/validate.io-array-1.0.6.tgz#5b5a2cafd8f8b85abb2f886ba153f2d93a27774d" - integrity sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg== - -validate.io-function@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/validate.io-function/-/validate.io-function-1.0.2.tgz#343a19802ed3b1968269c780e558e93411c0bad7" - integrity sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ== - web-streams-polyfill@4.0.0-beta.3: version "4.0.0-beta.3" resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz#2898486b74f5156095e473efe989dcf185047a38" From 1c3a5fe275bd02c91ea3d7f5f2dedf6380d4a2c6 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 7 Dec 2025 22:08:46 +0530 Subject: [PATCH 093/196] feat(actions): limit urls & queries to 3 --- .../search/researcher/actions/scrapeURL.ts | 80 ++++++++++--------- .../search/researcher/actions/webSearch.ts | 5 ++ 2 files changed, 46 insertions(+), 39 deletions(-) diff --git a/src/lib/agents/search/researcher/actions/scrapeURL.ts b/src/lib/agents/search/researcher/actions/scrapeURL.ts index 0fd3f7b..023430a 100644 --- a/src/lib/agents/search/researcher/actions/scrapeURL.ts +++ b/src/lib/agents/search/researcher/actions/scrapeURL.ts @@ -6,52 +6,54 @@ import TurnDown from 'turndown'; const turndownService = new TurnDown(); const schema = z.object({ - urls: z.array(z.string()).describe('A list of URLs to scrape content from.'), + urls: z.array(z.string()).describe('A list of URLs to scrape content from.'), }); const scrapeURLAction: ResearchAction<typeof schema> = { - name: 'scrape_url', - description: - 'Use after __plan to scrape and extract content from the provided URLs. This is useful when you need detailed information from specific web pages or if the user asks you to summarize or analyze content from certain links.', - schema: schema, - enabled: (_) => true, - execute: async (params, additionalConfig) => { - const results: Chunk[] = []; + name: 'scrape_url', + description: + 'Use after __plan to scrape and extract content from the provided URLs. This is useful when you need detailed information from specific web pages or if the user asks you to summarize or analyze content from certain links. You can scrape maximum of 3 URLs.', + schema: schema, + enabled: (_) => true, + execute: async (params, additionalConfig) => { + params.urls = params.urls.slice(0, 3); - await Promise.all( - params.urls.map(async (url) => { - try { - const res = await fetch(url); - const text = await res.text(); + const results: Chunk[] = []; - const title = - text.match(/<title>(.*?)<\/title>/i)?.[1] || `Content from ${url}`; - const markdown = turndownService.turndown(text); + await Promise.all( + params.urls.map(async (url) => { + try { + const res = await fetch(url); + const text = await res.text(); - results.push({ - content: markdown, - metadata: { - url, - title: title, - }, - }); - } catch (error) { - results.push({ - content: `Failed to fetch content from ${url}: ${error}`, - metadata: { - url, - title: `Error fetching ${url}`, - }, - }); - } - }), - ); + const title = + text.match(/<title>(.*?)<\/title>/i)?.[1] || `Content from ${url}`; + const markdown = turndownService.turndown(text); - return { - type: 'search_results', - results, - }; - }, + results.push({ + content: markdown, + metadata: { + url, + title: title, + }, + }); + } catch (error) { + results.push({ + content: `Failed to fetch content from ${url}: ${error}`, + metadata: { + url, + title: `Error fetching ${url}`, + }, + }); + } + }), + ); + + return { + type: 'search_results', + results, + }; + }, }; export default scrapeURLAction; diff --git a/src/lib/agents/search/researcher/actions/webSearch.ts b/src/lib/agents/search/researcher/actions/webSearch.ts index bf89b5a..c24c463 100644 --- a/src/lib/agents/search/researcher/actions/webSearch.ts +++ b/src/lib/agents/search/researcher/actions/webSearch.ts @@ -13,7 +13,10 @@ const actionSchema = z.object({ const actionDescription = ` Use immediately after the ___plan call when you need information. Default to using this unless you already have everything needed to finish. Provide 1-3 short, SEO-friendly queries (keywords, not sentences) that cover the user ask. Always prefer current/contextual queries (e.g., include year for news). +You can search maximum of 3 queries at a time. + For fast mode, you can only use this tool once so make sure to get all needed information in one go. + For balanced and quality modes, you can use this tool multiple times as needed. In quality and balanced mode, first try to gather upper level information with broad queries, then use more specific queries based on what you find to find all information needed. @@ -26,6 +29,8 @@ const webSearchAction: ResearchAction<typeof actionSchema> = { enabled: (config) => config.classification.classification.skipSearch === false, execute: async (input, _) => { + input.queries = input.queries.slice(0, 3); + let results: Chunk[] = []; const search = async (q: string) => { From 5174820554d0f8e83a41c295b617a82c7655cb28 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 7 Dec 2025 22:09:11 +0530 Subject: [PATCH 094/196] feat(package): bump next version --- next-env.d.ts | 1 + package.json | 2 +- tsconfig.json | 10 +- yarn.lock | 419 +++++++++++++++++++++++++------------------------- 4 files changed, 223 insertions(+), 209 deletions(-) diff --git a/next-env.d.ts b/next-env.d.ts index 1b3be08..c4b7818 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,5 +1,6 @@ /// <reference types="next" /> /// <reference types="next/image-types/global" /> +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/package.json b/package.json index 18c5ad8..493b4fa 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "mammoth": "^1.9.1", "markdown-to-jsx": "^7.7.2", "mathjs": "^15.1.0", - "next": "^15.2.2", + "next": "^16.0.7", "next-themes": "^0.3.0", "ollama": "^0.6.3", "openai": "^6.9.0", diff --git a/tsconfig.json b/tsconfig.json index 51d0dbc..6749425 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "plugins": [ { @@ -22,6 +22,12 @@ }, "target": "ES2017" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], "exclude": ["node_modules"] } diff --git a/yarn.lock b/yarn.lock index da3e2df..f1de6b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -73,13 +73,6 @@ resolved "https://registry.yarnpkg.com/@drizzle-team/brocli/-/brocli-0.10.2.tgz#9757c006a43daaa6f45512e6cf5fabed36fb9da7" integrity sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w== -"@emnapi/runtime@^1.2.0": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.3.1.tgz#0fcaa575afc31f455fd33534c19381cfce6c6f60" - integrity sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw== - dependencies: - tslib "^2.4.0" - "@emnapi/runtime@^1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.5.0.tgz#9aebfcb9b17195dce3ab53c86787a6b7d058db73" @@ -87,6 +80,13 @@ dependencies: tslib "^2.4.0" +"@emnapi/runtime@^1.7.0": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.7.1.tgz#a73784e23f5d57287369c808197288b52276b791" + integrity sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA== + dependencies: + tslib "^2.4.0" + "@esbuild-kit/core-utils@^3.3.2": version "3.3.2" resolved "https://registry.yarnpkg.com/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz#186b6598a5066f0413471d7c4d45828e399ba96c" @@ -465,13 +465,6 @@ resolved "https://registry.yarnpkg.com/@img/colour/-/colour-1.0.0.tgz#d2fabb223455a793bf3bf9c70de3d28526aa8311" integrity sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw== -"@img/sharp-darwin-arm64@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz#ef5b5a07862805f1e8145a377c8ba6e98813ca08" - integrity sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ== - optionalDependencies: - "@img/sharp-libvips-darwin-arm64" "1.0.4" - "@img/sharp-darwin-arm64@0.34.4": version "0.34.4" resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz#8a0dcac9e621ff533fbf2e830f6a977b38d67a0c" @@ -479,12 +472,12 @@ optionalDependencies: "@img/sharp-libvips-darwin-arm64" "1.2.3" -"@img/sharp-darwin-x64@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz#e03d3451cd9e664faa72948cc70a403ea4063d61" - integrity sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q== +"@img/sharp-darwin-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz#6e0732dcade126b6670af7aa17060b926835ea86" + integrity sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w== optionalDependencies: - "@img/sharp-libvips-darwin-x64" "1.0.4" + "@img/sharp-libvips-darwin-arm64" "1.2.4" "@img/sharp-darwin-x64@0.34.4": version "0.34.4" @@ -493,97 +486,107 @@ optionalDependencies: "@img/sharp-libvips-darwin-x64" "1.2.3" -"@img/sharp-libvips-darwin-arm64@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz#447c5026700c01a993c7804eb8af5f6e9868c07f" - integrity sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg== +"@img/sharp-darwin-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz#19bc1dd6eba6d5a96283498b9c9f401180ee9c7b" + integrity sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw== + optionalDependencies: + "@img/sharp-libvips-darwin-x64" "1.2.4" "@img/sharp-libvips-darwin-arm64@1.2.3": version "1.2.3" resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz#f43c9aa3b74fd307e4318da63ebbe0ed4c34e744" integrity sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw== -"@img/sharp-libvips-darwin-x64@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz#e0456f8f7c623f9dbfbdc77383caa72281d86062" - integrity sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ== +"@img/sharp-libvips-darwin-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz#2894c0cb87d42276c3889942e8e2db517a492c43" + integrity sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g== "@img/sharp-libvips-darwin-x64@1.2.3": version "1.2.3" resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz#c42ff786d4a1f42ef8929dba4a989dd5df6417f0" integrity sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA== -"@img/sharp-libvips-linux-arm64@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz#979b1c66c9a91f7ff2893556ef267f90ebe51704" - integrity sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA== +"@img/sharp-libvips-darwin-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz#e63681f4539a94af9cd17246ed8881734386f8cc" + integrity sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg== "@img/sharp-libvips-linux-arm64@1.2.3": version "1.2.3" resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz#c9073e5c4b629ee417f777db21c552910d84ed77" integrity sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ== -"@img/sharp-libvips-linux-arm@1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz#99f922d4e15216ec205dcb6891b721bfd2884197" - integrity sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g== +"@img/sharp-libvips-linux-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz#b1b288b36864b3bce545ad91fa6dadcf1a4ad318" + integrity sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw== "@img/sharp-libvips-linux-arm@1.2.3": version "1.2.3" resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz#3cbc333fd6b8f224a14d69b03a1dd11df897c799" integrity sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA== +"@img/sharp-libvips-linux-arm@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz#b9260dd1ebe6f9e3bdbcbdcac9d2ac125f35852d" + integrity sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A== + "@img/sharp-libvips-linux-ppc64@1.2.3": version "1.2.3" resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz#68e0e0076299f43d838468675674fabcc7161d16" integrity sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg== -"@img/sharp-libvips-linux-s390x@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz#f8a5eb1f374a082f72b3f45e2fb25b8118a8a5ce" - integrity sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA== +"@img/sharp-libvips-linux-ppc64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz#4b83ecf2a829057222b38848c7b022e7b4d07aa7" + integrity sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA== + +"@img/sharp-libvips-linux-riscv64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz#880b4678009e5a2080af192332b00b0aaf8a48de" + integrity sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA== "@img/sharp-libvips-linux-s390x@1.2.3": version "1.2.3" resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz#7da9ab11a50c0ca905979f0aae14a4ccffab27b2" integrity sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w== -"@img/sharp-libvips-linux-x64@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz#d4c4619cdd157774906e15770ee119931c7ef5e0" - integrity sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw== +"@img/sharp-libvips-linux-s390x@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz#74f343c8e10fad821b38f75ced30488939dc59ec" + integrity sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ== "@img/sharp-libvips-linux-x64@1.2.3": version "1.2.3" resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz#3b162d6b190cf77926819040e09fb15eec42135e" integrity sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg== -"@img/sharp-libvips-linuxmusl-arm64@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz#166778da0f48dd2bded1fa3033cee6b588f0d5d5" - integrity sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA== +"@img/sharp-libvips-linux-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz#df4183e8bd8410f7d61b66859a35edeab0a531ce" + integrity sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw== "@img/sharp-libvips-linuxmusl-arm64@1.2.3": version "1.2.3" resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz#ac99576630dd8e33cb598d7c4586f6e0655912ea" integrity sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw== -"@img/sharp-libvips-linuxmusl-x64@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz#93794e4d7720b077fcad3e02982f2f1c246751ff" - integrity sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw== +"@img/sharp-libvips-linuxmusl-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz#c8d6b48211df67137541007ee8d1b7b1f8ca8e06" + integrity sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw== "@img/sharp-libvips-linuxmusl-x64@1.2.3": version "1.2.3" resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz#93e9495af7bf6c4e0d41dd71d0196c35c3753a1c" integrity sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g== -"@img/sharp-linux-arm64@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz#edb0697e7a8279c9fc829a60fc35644c4839bb22" - integrity sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA== - optionalDependencies: - "@img/sharp-libvips-linux-arm64" "1.0.4" +"@img/sharp-libvips-linuxmusl-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz#be11c75bee5b080cbee31a153a8779448f919f75" + integrity sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg== "@img/sharp-linux-arm64@0.34.4": version "0.34.4" @@ -592,12 +595,12 @@ optionalDependencies: "@img/sharp-libvips-linux-arm64" "1.2.3" -"@img/sharp-linux-arm@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz#422c1a352e7b5832842577dc51602bcd5b6f5eff" - integrity sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ== +"@img/sharp-linux-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz#7aa7764ef9c001f15e610546d42fce56911790cc" + integrity sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg== optionalDependencies: - "@img/sharp-libvips-linux-arm" "1.0.5" + "@img/sharp-libvips-linux-arm64" "1.2.4" "@img/sharp-linux-arm@0.34.4": version "0.34.4" @@ -606,6 +609,13 @@ optionalDependencies: "@img/sharp-libvips-linux-arm" "1.2.3" +"@img/sharp-linux-arm@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz#5fb0c3695dd12522d39c3ff7a6bc816461780a0d" + integrity sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw== + optionalDependencies: + "@img/sharp-libvips-linux-arm" "1.2.4" + "@img/sharp-linux-ppc64@0.34.4": version "0.34.4" resolved "https://registry.yarnpkg.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz#8d5775f6dc7e30ea3a1efa43798b7690bb5cb344" @@ -613,12 +623,19 @@ optionalDependencies: "@img/sharp-libvips-linux-ppc64" "1.2.3" -"@img/sharp-linux-s390x@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz#f5c077926b48e97e4a04d004dfaf175972059667" - integrity sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q== +"@img/sharp-linux-ppc64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz#9c213a81520a20caf66978f3d4c07456ff2e0813" + integrity sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA== optionalDependencies: - "@img/sharp-libvips-linux-s390x" "1.0.4" + "@img/sharp-libvips-linux-ppc64" "1.2.4" + +"@img/sharp-linux-riscv64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz#cdd28182774eadbe04f62675a16aabbccb833f60" + integrity sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw== + optionalDependencies: + "@img/sharp-libvips-linux-riscv64" "1.2.4" "@img/sharp-linux-s390x@0.34.4": version "0.34.4" @@ -627,12 +644,12 @@ optionalDependencies: "@img/sharp-libvips-linux-s390x" "1.2.3" -"@img/sharp-linux-x64@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz#d806e0afd71ae6775cc87f0da8f2d03a7c2209cb" - integrity sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA== +"@img/sharp-linux-s390x@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz#93eac601b9f329bb27917e0e19098c722d630df7" + integrity sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg== optionalDependencies: - "@img/sharp-libvips-linux-x64" "1.0.4" + "@img/sharp-libvips-linux-s390x" "1.2.4" "@img/sharp-linux-x64@0.34.4": version "0.34.4" @@ -641,12 +658,12 @@ optionalDependencies: "@img/sharp-libvips-linux-x64" "1.2.3" -"@img/sharp-linuxmusl-arm64@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz#252975b915894fb315af5deea174651e208d3d6b" - integrity sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g== +"@img/sharp-linux-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz#55abc7cd754ffca5002b6c2b719abdfc846819a8" + integrity sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ== optionalDependencies: - "@img/sharp-libvips-linuxmusl-arm64" "1.0.4" + "@img/sharp-libvips-linux-x64" "1.2.4" "@img/sharp-linuxmusl-arm64@0.34.4": version "0.34.4" @@ -655,12 +672,12 @@ optionalDependencies: "@img/sharp-libvips-linuxmusl-arm64" "1.2.3" -"@img/sharp-linuxmusl-x64@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz#3f4609ac5d8ef8ec7dadee80b560961a60fd4f48" - integrity sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw== +"@img/sharp-linuxmusl-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz#d6515ee971bb62f73001a4829b9d865a11b77086" + integrity sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg== optionalDependencies: - "@img/sharp-libvips-linuxmusl-x64" "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64" "1.2.4" "@img/sharp-linuxmusl-x64@0.34.4": version "0.34.4" @@ -669,12 +686,12 @@ optionalDependencies: "@img/sharp-libvips-linuxmusl-x64" "1.2.3" -"@img/sharp-wasm32@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz#6f44f3283069d935bb5ca5813153572f3e6f61a1" - integrity sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg== - dependencies: - "@emnapi/runtime" "^1.2.0" +"@img/sharp-linuxmusl-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz#d97978aec7c5212f999714f2f5b736457e12ee9f" + integrity sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-x64" "1.2.4" "@img/sharp-wasm32@0.34.4": version "0.34.4" @@ -683,31 +700,43 @@ dependencies: "@emnapi/runtime" "^1.5.0" +"@img/sharp-wasm32@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz#2f15803aa626f8c59dd7c9d0bbc766f1ab52cfa0" + integrity sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw== + dependencies: + "@emnapi/runtime" "^1.7.0" + "@img/sharp-win32-arm64@0.34.4": version "0.34.4" resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz#38e2c8a88826eac647f7c3f99efefb39897a8f5c" integrity sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA== -"@img/sharp-win32-ia32@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz#1a0c839a40c5351e9885628c85f2e5dfd02b52a9" - integrity sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ== +"@img/sharp-win32-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz#3706e9e3ac35fddfc1c87f94e849f1b75307ce0a" + integrity sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g== "@img/sharp-win32-ia32@0.34.4": version "0.34.4" resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz#003a7eb0fdaba600790c3007cfd756e41a9cf749" integrity sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw== -"@img/sharp-win32-x64@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz#56f00962ff0c4e0eb93d34a047d29fa995e3e342" - integrity sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg== +"@img/sharp-win32-ia32@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz#0b71166599b049e032f085fb9263e02f4e4788de" + integrity sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg== "@img/sharp-win32-x64@0.34.4": version "0.34.4" resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz#b19f1f88ace8bfc20784a0ad31767f3438e025d1" integrity sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig== +"@img/sharp-win32-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz#a81ffb00e69267cd0a1d626eaedb8a8430b2b2f8" + integrity sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw== + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -893,10 +922,10 @@ resolved "https://registry.yarnpkg.com/@mixmark-io/domino/-/domino-2.2.0.tgz#4e8ec69bf1afeb7a14f0628b7e2c0f35bdb336c3" integrity sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw== -"@next/env@15.2.2": - version "15.2.2" - resolved "https://registry.yarnpkg.com/@next/env/-/env-15.2.2.tgz#6345352365a811c523cecf284874ff489b675e59" - integrity sha512-yWgopCfA9XDR8ZH3taB5nRKtKJ1Q5fYsTOuYkzIIoS8TJ0UAUKAGF73JnGszbjk2ufAQDj6mDdgsJAFx5CLtYQ== +"@next/env@16.0.7": + version "16.0.7" + resolved "https://registry.yarnpkg.com/@next/env/-/env-16.0.7.tgz#eda56377a865d890d25122257d2b8a85b81d6d3d" + integrity sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw== "@next/eslint-plugin-next@14.1.4": version "14.1.4" @@ -905,45 +934,45 @@ dependencies: glob "10.3.10" -"@next/swc-darwin-arm64@15.2.2": - version "15.2.2" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.2.tgz#d3131279d4815ed7604eba7bf0063e0b8bfd2135" - integrity sha512-HNBRnz+bkZ+KfyOExpUxTMR0Ow8nkkcE6IlsdEa9W/rI7gefud19+Sn1xYKwB9pdCdxIP1lPru/ZfjfA+iT8pw== +"@next/swc-darwin-arm64@16.0.7": + version "16.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.7.tgz#ffe8afc902807e409f16a5f585dc4cb683804931" + integrity sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg== -"@next/swc-darwin-x64@15.2.2": - version "15.2.2" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.2.tgz#8562f6b51177aa30f3a6e95a8b3e0e176a6cb04f" - integrity sha512-mJOUwp7al63tDpLpEFpKwwg5jwvtL1lhRW2fI1Aog0nYCPAhxbJsaZKdoVyPZCy8MYf/iQVNDuk/+i29iLCzIA== +"@next/swc-darwin-x64@16.0.7": + version "16.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.7.tgz#ec5fd15ae391e1af9f152881f559bffaa24524e7" + integrity sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA== -"@next/swc-linux-arm64-gnu@15.2.2": - version "15.2.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.2.tgz#c2466cffc541f39ae4d435087c81f5899d401903" - integrity sha512-5ZZ0Zwy3SgMr7MfWtRE7cQWVssfOvxYfD9O7XHM7KM4nrf5EOeqwq67ZXDgo86LVmffgsu5tPO57EeFKRnrfSQ== +"@next/swc-linux-arm64-gnu@16.0.7": + version "16.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.7.tgz#22be26ee7544f68aa916159b823899b67b09f0a6" + integrity sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww== -"@next/swc-linux-arm64-musl@15.2.2": - version "15.2.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.2.tgz#24ed87fe47325597e986bd01c427efae53d6b677" - integrity sha512-cgKWBuFMLlJ4TWcFHl1KOaVVUAF8vy4qEvX5KsNd0Yj5mhu989QFCq1WjuaEbv/tO1ZpsQI6h/0YR8bLwEi+nA== +"@next/swc-linux-arm64-musl@16.0.7": + version "16.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.7.tgz#1b34103951456e5d6766710e4f16dadbd7745bed" + integrity sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g== -"@next/swc-linux-x64-gnu@15.2.2": - version "15.2.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.2.tgz#0240f44530ca39c1fc899fabe8f33aa35d7e77da" - integrity sha512-c3kWSOSsVL8rcNBBfOq1+/j2PKs2nsMwJUV4icUxRgGBwUOfppeh7YhN5s79enBQFU+8xRgVatFkhHU1QW7yUA== +"@next/swc-linux-x64-gnu@16.0.7": + version "16.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.7.tgz#b45e1fa7aaf844269e0932c4cde49bb6b0817ce9" + integrity sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA== -"@next/swc-linux-x64-musl@15.2.2": - version "15.2.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.2.tgz#cca6ce8e9ec89afaa618673f0f8658682c30ff56" - integrity sha512-PXTW9PLTxdNlVYgPJ0equojcq1kNu5NtwcNjRjHAB+/sdoKZ+X8FBu70fdJFadkxFIGekQTyRvPMFF+SOJaQjw== +"@next/swc-linux-x64-musl@16.0.7": + version "16.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.7.tgz#c5376fd78ca97a911e2bbfb9b26dc623b54edfab" + integrity sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w== -"@next/swc-win32-arm64-msvc@15.2.2": - version "15.2.2" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.2.tgz#08f621602ed53fa21ddf6f656160b9186860be15" - integrity sha512-nG644Es5llSGEcTaXhnGWR/aThM/hIaz0jx4MDg4gWC8GfTCp8eDBWZ77CVuv2ha/uL9Ce+nPTfYkSLG67/sHg== +"@next/swc-win32-arm64-msvc@16.0.7": + version "16.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.7.tgz#2cf27e275ddd8d04f89391a9cdf6790cccb3bf09" + integrity sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q== -"@next/swc-win32-x64-msvc@15.2.2": - version "15.2.2" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.2.tgz#2072d69374f8c944134a5c5a80ce03ff84254cfa" - integrity sha512-52nWy65S/R6/kejz3jpvHAjZDPKIbEQu4x9jDBzmB9jJfuOy5rspjKu4u77+fI4M/WzLXrrQd57hlFGzz1ubcQ== +"@next/swc-win32-x64-msvc@16.0.7": + version "16.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.7.tgz#e9e38fcca752902542c25c527e532af71e9ca812" + integrity sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -1093,11 +1122,6 @@ domhandler "^5.0.3" selderee "^0.11.0" -"@swc/counter@0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" - integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== - "@swc/helpers@0.5.15", "@swc/helpers@^0.5.0": version "0.5.15" resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7" @@ -1664,13 +1688,6 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" -busboy@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" - integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== - dependencies: - streamsearch "^1.1.0" - call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" @@ -1801,7 +1818,7 @@ color-name@^1.0.0, color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.6.0, color-string@^1.9.0: +color-string@^1.6.0: version "1.9.1" resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== @@ -1817,14 +1834,6 @@ color@^3.1.3: color-convert "^1.9.3" color-string "^1.6.0" -color@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" - integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== - dependencies: - color-convert "^2.0.1" - color-string "^1.9.0" - colorspace@1.1.x: version "1.1.4" resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243" @@ -2004,12 +2013,12 @@ dequal@^2.0.3: resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== -detect-libc@^2.0.0, detect-libc@^2.0.3: +detect-libc@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== -detect-libc@^2.1.0: +detect-libc@^2.1.0, detect-libc@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== @@ -3943,28 +3952,26 @@ next-themes@^0.3.0: resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.3.0.tgz#b4d2a866137a67d42564b07f3a3e720e2ff3871a" integrity sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w== -next@^15.2.2: - version "15.2.2" - resolved "https://registry.yarnpkg.com/next/-/next-15.2.2.tgz#e3941a0e0e76cfe1880b57452807489e0546e3a2" - integrity sha512-dgp8Kcx5XZRjMw2KNwBtUzhngRaURPioxoNIVl5BOyJbhi9CUgEtKDO7fx5wh8Z8vOVX1nYZ9meawJoRrlASYA== +next@^16.0.7: + version "16.0.7" + resolved "https://registry.yarnpkg.com/next/-/next-16.0.7.tgz#859045df71599a8883fc7049a65d6d468c56db08" + integrity sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A== dependencies: - "@next/env" "15.2.2" - "@swc/counter" "0.1.3" + "@next/env" "16.0.7" "@swc/helpers" "0.5.15" - busboy "1.6.0" caniuse-lite "^1.0.30001579" postcss "8.4.31" styled-jsx "5.1.6" optionalDependencies: - "@next/swc-darwin-arm64" "15.2.2" - "@next/swc-darwin-x64" "15.2.2" - "@next/swc-linux-arm64-gnu" "15.2.2" - "@next/swc-linux-arm64-musl" "15.2.2" - "@next/swc-linux-x64-gnu" "15.2.2" - "@next/swc-linux-x64-musl" "15.2.2" - "@next/swc-win32-arm64-msvc" "15.2.2" - "@next/swc-win32-x64-msvc" "15.2.2" - sharp "^0.33.5" + "@next/swc-darwin-arm64" "16.0.7" + "@next/swc-darwin-x64" "16.0.7" + "@next/swc-linux-arm64-gnu" "16.0.7" + "@next/swc-linux-arm64-musl" "16.0.7" + "@next/swc-linux-x64-gnu" "16.0.7" + "@next/swc-linux-x64-musl" "16.0.7" + "@next/swc-win32-arm64-msvc" "16.0.7" + "@next/swc-win32-x64-msvc" "16.0.7" + sharp "^0.34.4" node-abi@^3.3.0: version "3.74.0" @@ -4747,7 +4754,7 @@ semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.2, semver@^7.7.2: +semver@^7.3.2, semver@^7.7.2, semver@^7.7.3: version "7.7.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== @@ -4798,35 +4805,6 @@ setimmediate@^1.0.5: resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== -sharp@^0.33.5: - version "0.33.5" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.5.tgz#13e0e4130cc309d6a9497596715240b2ec0c594e" - integrity sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw== - dependencies: - color "^4.2.3" - detect-libc "^2.0.3" - semver "^7.6.3" - optionalDependencies: - "@img/sharp-darwin-arm64" "0.33.5" - "@img/sharp-darwin-x64" "0.33.5" - "@img/sharp-libvips-darwin-arm64" "1.0.4" - "@img/sharp-libvips-darwin-x64" "1.0.4" - "@img/sharp-libvips-linux-arm" "1.0.5" - "@img/sharp-libvips-linux-arm64" "1.0.4" - "@img/sharp-libvips-linux-s390x" "1.0.4" - "@img/sharp-libvips-linux-x64" "1.0.4" - "@img/sharp-libvips-linuxmusl-arm64" "1.0.4" - "@img/sharp-libvips-linuxmusl-x64" "1.0.4" - "@img/sharp-linux-arm" "0.33.5" - "@img/sharp-linux-arm64" "0.33.5" - "@img/sharp-linux-s390x" "0.33.5" - "@img/sharp-linux-x64" "0.33.5" - "@img/sharp-linuxmusl-arm64" "0.33.5" - "@img/sharp-linuxmusl-x64" "0.33.5" - "@img/sharp-wasm32" "0.33.5" - "@img/sharp-win32-ia32" "0.33.5" - "@img/sharp-win32-x64" "0.33.5" - sharp@^0.34.1: version "0.34.4" resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.4.tgz#73c2c5a425e98250b8b927e5537f711da8966e38" @@ -4859,6 +4837,40 @@ sharp@^0.34.1: "@img/sharp-win32-ia32" "0.34.4" "@img/sharp-win32-x64" "0.34.4" +sharp@^0.34.4: + version "0.34.5" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.5.tgz#b6f148e4b8c61f1797bde11a9d1cfebbae2c57b0" + integrity sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg== + dependencies: + "@img/colour" "^1.0.0" + detect-libc "^2.1.2" + semver "^7.7.3" + optionalDependencies: + "@img/sharp-darwin-arm64" "0.34.5" + "@img/sharp-darwin-x64" "0.34.5" + "@img/sharp-libvips-darwin-arm64" "1.2.4" + "@img/sharp-libvips-darwin-x64" "1.2.4" + "@img/sharp-libvips-linux-arm" "1.2.4" + "@img/sharp-libvips-linux-arm64" "1.2.4" + "@img/sharp-libvips-linux-ppc64" "1.2.4" + "@img/sharp-libvips-linux-riscv64" "1.2.4" + "@img/sharp-libvips-linux-s390x" "1.2.4" + "@img/sharp-libvips-linux-x64" "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64" "1.2.4" + "@img/sharp-libvips-linuxmusl-x64" "1.2.4" + "@img/sharp-linux-arm" "0.34.5" + "@img/sharp-linux-arm64" "0.34.5" + "@img/sharp-linux-ppc64" "0.34.5" + "@img/sharp-linux-riscv64" "0.34.5" + "@img/sharp-linux-s390x" "0.34.5" + "@img/sharp-linux-x64" "0.34.5" + "@img/sharp-linuxmusl-arm64" "0.34.5" + "@img/sharp-linuxmusl-x64" "0.34.5" + "@img/sharp-wasm32" "0.34.5" + "@img/sharp-win32-arm64" "0.34.5" + "@img/sharp-win32-ia32" "0.34.5" + "@img/sharp-win32-x64" "0.34.5" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -4970,11 +4982,6 @@ stackblur-canvas@^2.0.0: resolved "https://registry.yarnpkg.com/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz#af931277d0b5096df55e1f91c530043e066989b6" integrity sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ== -streamsearch@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" - integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== - "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" From e329820bc8c750005c7b50a7a4d8f36250498491 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:06:58 +0530 Subject: [PATCH 095/196] feat(package): update lucide-react, framer-motion --- package.json | 4 ++-- yarn.lock | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 493b4fa..1c4b60b 100644 --- a/package.json +++ b/package.json @@ -30,12 +30,12 @@ "better-sqlite3": "^11.9.1", "clsx": "^2.1.0", "drizzle-orm": "^0.40.1", - "framer-motion": "^12.23.24", + "framer-motion": "^12.23.25", "html-to-text": "^9.0.5", "jspdf": "^3.0.1", "langchain": "^1.0.4", "lightweight-charts": "^5.0.9", - "lucide-react": "^0.363.0", + "lucide-react": "^0.556.0", "mammoth": "^1.9.1", "markdown-to-jsx": "^7.7.2", "mathjs": "^15.1.0", diff --git a/yarn.lock b/yarn.lock index f1de6b3..32bcae5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2830,10 +2830,10 @@ fraction.js@^5.2.1: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-5.3.4.tgz#8c0fcc6a9908262df4ed197427bdeef563e0699a" integrity sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ== -framer-motion@^12.23.24: - version "12.23.24" - resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.23.24.tgz#4895b67e880bd2b1089e61fbaa32ae802fc24b8c" - integrity sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w== +framer-motion@^12.23.25: + version "12.23.25" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.23.25.tgz#32d717f8b172c2673f573c0805ecc37d017441b2" + integrity sha512-gUHGl2e4VG66jOcH0JHhuJQr6ZNwrET9g31ZG0xdXzT0CznP7fHX4P8Bcvuc4MiUB90ysNnWX2ukHRIggkl6hQ== dependencies: motion-dom "^12.23.23" motion-utils "^12.23.6" @@ -3750,10 +3750,10 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lucide-react@^0.363.0: - version "0.363.0" - resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.363.0.tgz#2bb1f9d09b830dda86f5118fcd097f87247fe0e3" - integrity sha512-AlsfPCsXQyQx7wwsIgzcKOL9LwC498LIMAo+c0Es5PkHJa33xwmYAkkSoKoJWWWSYQEStqu58/jT4tL2gi32uQ== +lucide-react@^0.556.0: + version "0.556.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.556.0.tgz#aad61a065737aef30322695a11fd21c7542c71aa" + integrity sha512-iOb8dRk7kLaYBZhR2VlV1CeJGxChBgUthpSP8wom9jfj79qovgG6qcSdiy6vkoREKPnbUYzJsCn4o4PtG3Iy+A== mammoth@^1.9.1: version "1.9.1" From d0e71e6482fc2dd5c2a9bf964c51827ce06de157 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:07:16 +0530 Subject: [PATCH 096/196] feat(types): add search_results research block --- src/lib/types.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/lib/types.ts b/src/lib/types.ts index dfc3b72..6606447 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -75,6 +75,12 @@ export type SearchingResearchBlock = { searching: string[]; }; +export type SearchResultsResearchBlock = { + id: string; + type: 'search_results'; + reading: Chunk[]; +}; + export type ReadingResearchBlock = { id: string; type: 'reading'; @@ -84,6 +90,7 @@ export type ReadingResearchBlock = { export type ResearchBlockSubStep = | ReasoningResearchBlock | SearchingResearchBlock + | SearchResultsResearchBlock | ReadingResearchBlock; export type ResearchBlock = { From 331387efa4c87193600562efc5d423b2ed27fc73 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:07:52 +0530 Subject: [PATCH 097/196] feat(search): add better context handling --- src/lib/agents/search/index.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/lib/agents/search/index.ts b/src/lib/agents/search/index.ts index bc6ff5b..91bed9a 100644 --- a/src/lib/agents/search/index.ts +++ b/src/lib/agents/search/index.ts @@ -55,19 +55,20 @@ class SearchAgent { }); const finalContext = - searchResults?.findings - .filter((f) => f.type === 'search_results') - .flatMap((f) => f.results) - .map((f) => `${f.metadata.title}: ${f.content}`) + searchResults?.searchFindings + .map( + (f, index) => + `<result index=${index} title=${f.metadata.title}>${f.content}</result>`, + ) .join('\n') || ''; const widgetContext = widgetOutputs .map((o) => { - return `${o.type}: ${o.llmContext}`; + return `<result>${o.llmContext}</result>`; }) .join('\n-------------\n'); - const finalContextWithWidgets = `<search_results note="These are the search results and you can cite these">${finalContext}</search_results>\n<widgets_result noteForAssistant="Its output is already showed to the user, you can use this information to answer the query but do not CITE this as a souce">${widgetContext}</widgets_result>`; + const finalContextWithWidgets = `<search_results note="These are the search results and assistant can cite these">\n${finalContext}\n</search_results>\n<widgets_result noteForAssistant="Its output is already showed to the user, assistant can use this information to answer the query but do not CITE this as a souce">\n${widgetContext}\n</widgets_result>`; const writerPrompt = getWriterPrompt(finalContextWithWidgets); const answerStream = input.config.llm.streamText({ From 96001a9e263c81684781efa6c2e1d2addc0a866b Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:08:26 +0530 Subject: [PATCH 098/196] feat(assistant-steps): handle reading, search_results --- src/components/AssistantSteps.tsx | 87 ++++++++++++++++++------------- 1 file changed, 50 insertions(+), 37 deletions(-) diff --git a/src/components/AssistantSteps.tsx b/src/components/AssistantSteps.tsx index c688880..b369d1a 100644 --- a/src/components/AssistantSteps.tsx +++ b/src/components/AssistantSteps.tsx @@ -1,6 +1,13 @@ 'use client'; -import { Brain, Search, FileText, ChevronDown, ChevronUp } from 'lucide-react'; +import { + Brain, + Search, + FileText, + ChevronDown, + ChevronUp, + BookSearch, +} from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { useEffect, useState } from 'react'; import { ResearchBlock, ResearchBlockSubStep } from '@/lib/types'; @@ -11,9 +18,12 @@ const getStepIcon = (step: ResearchBlockSubStep) => { return <Brain className="w-4 h-4" />; } else if (step.type === 'searching') { return <Search className="w-4 h-4" />; - } else if (step.type === 'reading') { + } else if (step.type === 'search_results') { return <FileText className="w-4 h-4" />; + } else if (step.type === 'reading') { + return <BookSearch className="w-4 h-4" />; } + return null; }; @@ -25,9 +35,12 @@ const getStepTitle = ( return isStreaming && !step.reasoning ? 'Thinking...' : 'Thinking'; } else if (step.type === 'searching') { return `Searching ${step.searching.length} ${step.searching.length === 1 ? 'query' : 'queries'}`; - } else if (step.type === 'reading') { + } else if (step.type === 'search_results') { return `Found ${step.reading.length} ${step.reading.length === 1 ? 'result' : 'results'}`; + } else if (step.type === 'reading') { + return `Reading ${step.reading.length} ${step.reading.length === 1 ? 'source' : 'sources'}`; } + return 'Processing'; }; @@ -91,10 +104,9 @@ const AssistantSteps = ({ initial={{ opacity: 0, x: -10 }} animate={{ opacity: 1, x: 0 }} transition={{ duration: 0.2, delay: 0 }} - className="flex gap-3" + className="flex gap-2" > - {/* Timeline connector */} - <div className="flex flex-col items-center pt-0.5"> + <div className="flex flex-col items-center -mt-0.5"> <div className={`rounded-full p-1.5 bg-light-100 dark:bg-dark-100 text-black/70 dark:text-white/70 ${isStreaming ? 'animate-pulse' : ''}`} > @@ -105,7 +117,6 @@ const AssistantSteps = ({ )} </div> - {/* Step content */} <div className="flex-1 pb-1"> <span className="text-sm font-medium text-black dark:text-white"> {getStepTitle(step, isStreaming)} @@ -151,37 +162,39 @@ const AssistantSteps = ({ </div> )} - {step.type === 'reading' && step.reading.length > 0 && ( - <div className="flex flex-wrap gap-1.5 mt-1.5"> - {step.reading.slice(0, 4).map((result, idx) => { - const url = result.metadata.url || ''; - const title = result.metadata.title || 'Untitled'; - const domain = url ? new URL(url).hostname : ''; - const faviconUrl = domain - ? `https://s2.googleusercontent.com/s2/favicons?domain=${domain}&sz=128` - : ''; + {(step.type === 'search_results' || + step.type === 'reading') && + step.reading.length > 0 && ( + <div className="flex flex-wrap gap-1.5 mt-1.5"> + {step.reading.slice(0, 4).map((result, idx) => { + const url = result.metadata.url || ''; + const title = result.metadata.title || 'Untitled'; + const domain = url ? new URL(url).hostname : ''; + const faviconUrl = domain + ? `https://s2.googleusercontent.com/s2/favicons?domain=${domain}&sz=128` + : ''; - return ( - <span - key={idx} - className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md text-xs font-medium bg-light-100 dark:bg-dark-100 text-black/70 dark:text-white/70 border border-light-200 dark:border-dark-200" - > - {faviconUrl && ( - <img - src={faviconUrl} - alt="" - className="w-3 h-3 rounded-sm flex-shrink-0" - onError={(e) => { - e.currentTarget.style.display = 'none'; - }} - /> - )} - <span className="line-clamp-1">{title}</span> - </span> - ); - })} - </div> - )} + return ( + <span + key={idx} + className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md text-xs font-medium bg-light-100 dark:bg-dark-100 text-black/70 dark:text-white/70 border border-light-200 dark:border-dark-200" + > + {faviconUrl && ( + <img + src={faviconUrl} + alt="" + className="w-3 h-3 rounded-sm flex-shrink-0" + onError={(e) => { + e.currentTarget.style.display = 'none'; + }} + /> + )} + <span className="line-clamp-1">{title}</span> + </span> + ); + })} + </div> + )} </div> </motion.div> ); From 85f6c3b90138e358330fd891f5036ae26bf3c65d Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:08:52 +0530 Subject: [PATCH 099/196] feat(client-registry): add `getMeasurementUnit` --- src/lib/config/clientRegistry.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/lib/config/clientRegistry.ts b/src/lib/config/clientRegistry.ts index 28d0951..f23d7ad 100644 --- a/src/lib/config/clientRegistry.ts +++ b/src/lib/config/clientRegistry.ts @@ -17,3 +17,13 @@ export const getShowWeatherWidget = () => export const getShowNewsWidget = () => getClientConfig('showNewsWidget', 'true') === 'true'; + +export const getMeasurementUnit = () => { + const value = + getClientConfig('measureUnit') ?? + getClientConfig('measurementUnit', 'metric'); + + if (typeof value !== 'string') return 'metric'; + + return value.toLowerCase(); +}; From 2df6250ba1a58fc1122576dc2908a4e79e63d2b1 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:09:21 +0530 Subject: [PATCH 100/196] feat(weather): respect unit preference --- src/components/Widgets/Weather.tsx | 35 +++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/components/Widgets/Weather.tsx b/src/components/Widgets/Weather.tsx index 1abc78b..159c15e 100644 --- a/src/components/Widgets/Weather.tsx +++ b/src/components/Widgets/Weather.tsx @@ -1,5 +1,6 @@ 'use client'; +import { getMeasurementUnit } from '@/lib/config/clientRegistry'; import { Wind, Droplets, Gauge } from 'lucide-react'; import { useMemo, useEffect, useState } from 'react'; @@ -226,6 +227,20 @@ const Weather = ({ timezone, }: WeatherWidgetProps) => { const [isDarkMode, setIsDarkMode] = useState(false); + const unit = getMeasurementUnit(); + const isImperial = unit === 'imperial'; + const tempUnitLabel = isImperial ? '°F' : '°C'; + const windUnitLabel = isImperial ? 'mph' : 'km/h'; + + const formatTemp = (celsius: number) => { + if (!Number.isFinite(celsius)) return 0; + return Math.round(isImperial ? (celsius * 9) / 5 + 32 : celsius); + }; + + const formatWind = (speedKmh: number) => { + if (!Number.isFinite(speedKmh)) return 0; + return Math.round(isImperial ? speedKmh * 0.621371 : speedKmh); + }; useEffect(() => { const checkDarkMode = () => { @@ -266,14 +281,12 @@ const Weather = ({ return { day: dayName, icon: info.icon, - high: Math.round(daily.temperature_2m_max[idx + 1]), - low: Math.round(daily.temperature_2m_min[idx + 1]), - highF: Math.round((daily.temperature_2m_max[idx + 1] * 9) / 5 + 32), - lowF: Math.round((daily.temperature_2m_min[idx + 1] * 9) / 5 + 32), + high: formatTemp(daily.temperature_2m_max[idx + 1]), + low: formatTemp(daily.temperature_2m_min[idx + 1]), precipitation: daily.precipitation_probability_max[idx + 1] || 0, }; }); - }, [daily, isDarkMode]); + }, [daily, isDarkMode, isImperial]); if (!current || !daily || !daily.time || daily.time.length === 0) { return ( @@ -305,9 +318,9 @@ const Weather = ({ <div> <div className="flex items-baseline gap-1"> <span className="text-4xl font-bold drop-shadow-md"> - {current.temperature_2m}° + {formatTemp(current.temperature_2m)}° </span> - <span className="text-lg">F C</span> + <span className="text-lg">{tempUnitLabel}</span> </div> <p className="text-sm font-medium drop-shadow mt-0.5"> {weatherInfo.description} @@ -316,7 +329,8 @@ const Weather = ({ </div> <div className="text-right"> <p className="text-xs font-medium opacity-90"> - {daily.temperature_2m_max[0]}° {daily.temperature_2m_min[0]}° + {formatTemp(daily.temperature_2m_max[0])}°{' '} + {formatTemp(daily.temperature_2m_min[0])}° </p> </div> </div> @@ -370,7 +384,7 @@ const Weather = ({ Wind </p> <p className="font-semibold"> - {Math.round(current.wind_speed_10m)} km/h + {formatWind(current.wind_speed_10m)} {windUnitLabel} </p> </div> </div> @@ -394,7 +408,8 @@ const Weather = ({ Feels Like </p> <p className="font-semibold"> - {Math.round(current.apparent_temperature)}°C + {formatTemp(current.apparent_temperature)} + {tempUnitLabel} </p> </div> </div> From 8aed9518a239b118a7031029206de55135a8a4dc Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:09:52 +0530 Subject: [PATCH 101/196] feat(researcher): pass research block id --- src/lib/agents/search/researcher/actions/registry.ts | 4 ++-- src/lib/agents/search/types.ts | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/lib/agents/search/researcher/actions/registry.ts b/src/lib/agents/search/researcher/actions/registry.ts index 5a05ac1..7454763 100644 --- a/src/lib/agents/search/researcher/actions/registry.ts +++ b/src/lib/agents/search/researcher/actions/registry.ts @@ -50,7 +50,7 @@ class ActionRegistry { static async execute( name: string, params: any, - additionalConfig: AdditionalConfig, + additionalConfig: AdditionalConfig & { researchBlockId: string }, ) { const action = this.actions.get(name); @@ -63,7 +63,7 @@ class ActionRegistry { static async executeAll( actions: ToolCall[], - additionalConfig: AdditionalConfig, + additionalConfig: AdditionalConfig & { researchBlockId: string }, ): Promise<ActionOutput[]> { const results: ActionOutput[] = []; diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts index e83ac7d..6a6ac69 100644 --- a/src/lib/agents/search/types.ts +++ b/src/lib/agents/search/types.ts @@ -73,6 +73,7 @@ export type ResearcherInput = { export type ResearcherOutput = { findings: ActionOutput[]; + searchFindings: Chunk[]; }; export type SearchActionOutput = { @@ -103,6 +104,8 @@ export interface ResearchAction< enabled: (config: { classification: ClassifierOutput }) => boolean; execute: ( params: z.infer<TSchema>, - additionalConfig: AdditionalConfig, + additionalConfig: AdditionalConfig & { + researchBlockId: string; + }, ) => Promise<ActionOutput>; } From 6016090f1215f5317f3cb35c54451d212e6d29e4 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:10:11 +0530 Subject: [PATCH 102/196] feat(actions): stream results internally --- .../search/researcher/actions/scrapeURL.ts | 76 +++++++++++++++- .../search/researcher/actions/webSearch.ts | 89 ++++++++++++++++--- src/lib/agents/search/researcher/index.ts | 74 ++++++--------- 3 files changed, 179 insertions(+), 60 deletions(-) diff --git a/src/lib/agents/search/researcher/actions/scrapeURL.ts b/src/lib/agents/search/researcher/actions/scrapeURL.ts index 023430a..03dea2f 100644 --- a/src/lib/agents/search/researcher/actions/scrapeURL.ts +++ b/src/lib/agents/search/researcher/actions/scrapeURL.ts @@ -1,7 +1,8 @@ import z from 'zod'; import { ResearchAction } from '../../types'; -import { Chunk } from '@/lib/types'; +import { Chunk, ReadingResearchBlock } from '@/lib/types'; import TurnDown from 'turndown'; +import path from 'path'; const turndownService = new TurnDown(); @@ -12,12 +13,19 @@ const schema = z.object({ const scrapeURLAction: ResearchAction<typeof schema> = { name: 'scrape_url', description: - 'Use after __plan to scrape and extract content from the provided URLs. This is useful when you need detailed information from specific web pages or if the user asks you to summarize or analyze content from certain links. You can scrape maximum of 3 URLs.', + 'Use this tool to scrape and extract content from the provided URLs. This is useful when you the user has asked you to extract or summarize information from specific web pages. You can provide up to 3 URLs at a time. NEVER CALL THIS TOOL EXPLICITLY YOURSELF UNLESS INSTRUCTED TO DO SO BY THE USER.', schema: schema, enabled: (_) => true, execute: async (params, additionalConfig) => { params.urls = params.urls.slice(0, 3); + let readingBlockId = crypto.randomUUID(); + let readingEmitted = false; + + const researchBlock = additionalConfig.session.getBlock( + additionalConfig.researchBlockId, + ); + const results: Chunk[] = []; await Promise.all( @@ -28,6 +36,70 @@ const scrapeURLAction: ResearchAction<typeof schema> = { const title = text.match(/<title>(.*?)<\/title>/i)?.[1] || `Content from ${url}`; + + if ( + !readingEmitted && + researchBlock && + researchBlock.type === 'research' + ) { + readingEmitted = true; + researchBlock.data.subSteps.push({ + id: readingBlockId, + type: 'reading', + reading: [ + { + content: '', + metadata: { + url, + title: title, + }, + }, + ], + }); + + additionalConfig.session.updateBlock( + additionalConfig.researchBlockId, + [ + { + op: 'replace', + path: '/data/subSteps', + value: researchBlock.data.subSteps, + }, + ], + ); + } else if ( + readingEmitted && + researchBlock && + researchBlock.type === 'research' + ) { + const subStepIndex = researchBlock.data.subSteps.findIndex( + (step: any) => step.id === readingBlockId, + ); + + const subStep = researchBlock.data.subSteps[ + subStepIndex + ] as ReadingResearchBlock; + + subStep.reading.push({ + content: '', + metadata: { + url, + title: title, + }, + }); + + additionalConfig.session.updateBlock( + additionalConfig.researchBlockId, + [ + { + op: 'replace', + path: '/data/subSteps', + value: researchBlock.data.subSteps, + }, + ], + ); + } + const markdown = turndownService.turndown(text); results.push({ diff --git a/src/lib/agents/search/researcher/actions/webSearch.ts b/src/lib/agents/search/researcher/actions/webSearch.ts index c24c463..f148155 100644 --- a/src/lib/agents/search/researcher/actions/webSearch.ts +++ b/src/lib/agents/search/researcher/actions/webSearch.ts @@ -1,7 +1,7 @@ import z from 'zod'; import { ResearchAction } from '../../types'; import { searchSearxng } from '@/lib/searxng'; -import { Chunk } from '@/lib/types'; +import { Chunk, SearchResultsResearchBlock } from '@/lib/types'; const actionSchema = z.object({ type: z.literal('web_search'), @@ -28,23 +28,90 @@ const webSearchAction: ResearchAction<typeof actionSchema> = { schema: actionSchema, enabled: (config) => config.classification.classification.skipSearch === false, - execute: async (input, _) => { + execute: async (input, additionalConfig) => { input.queries = input.queries.slice(0, 3); - + + const researchBlock = additionalConfig.session.getBlock( + additionalConfig.researchBlockId, + ); + + if (researchBlock && researchBlock.type === 'research') { + researchBlock.data.subSteps.push({ + id: crypto.randomUUID(), + type: 'searching', + searching: input.queries, + }); + + additionalConfig.session.updateBlock(additionalConfig.researchBlockId, [ + { + op: 'replace', + path: '/data/subSteps', + value: researchBlock.data.subSteps, + }, + ]); + } + + const searchResultsBlockId = crypto.randomUUID(); + let searchResultsEmitted = false; + let results: Chunk[] = []; const search = async (q: string) => { const res = await searchSearxng(q); - res.results.forEach((r) => { - results.push({ - content: r.content || r.title, - metadata: { - title: r.title, - url: r.url, - }, + const resultChunks: Chunk[] = res.results.map((r) => ({ + content: r.content || r.title, + metadata: { + title: r.title, + url: r.url, + }, + })); + + results.push(...resultChunks); + + if ( + !searchResultsEmitted && + researchBlock && + researchBlock.type === 'research' + ) { + searchResultsEmitted = true; + + researchBlock.data.subSteps.push({ + id: searchResultsBlockId, + type: 'search_results', + reading: resultChunks, }); - }); + + additionalConfig.session.updateBlock(additionalConfig.researchBlockId, [ + { + op: 'replace', + path: '/data/subSteps', + value: researchBlock.data.subSteps, + }, + ]); + } else if ( + searchResultsEmitted && + researchBlock && + researchBlock.type === 'research' + ) { + const subStepIndex = researchBlock.data.subSteps.findIndex( + (step) => step.id === searchResultsBlockId, + ); + + const subStep = researchBlock.data.subSteps[ + subStepIndex + ] as SearchResultsResearchBlock; + + subStep.reading.push(...resultChunks); + + additionalConfig.session.updateBlock(additionalConfig.researchBlockId, [ + { + op: 'replace', + path: '/data/subSteps', + value: researchBlock.data.subSteps, + }, + ]); + } }; await Promise.all(input.queries.map(search)); diff --git a/src/lib/agents/search/researcher/index.ts b/src/lib/agents/search/researcher/index.ts index a5dce5a..feb450f 100644 --- a/src/lib/agents/search/researcher/index.ts +++ b/src/lib/agents/search/researcher/index.ts @@ -154,33 +154,11 @@ class Researcher { tool_calls: finalToolCalls, }); - const searchCalls = finalToolCalls.filter( - (tc) => - tc.name === 'web_search' || - tc.name === 'academic_search' || - tc.name === 'discussion_search', - ); - - if (searchCalls.length > 0 && block && block.type === 'research') { - block.data.subSteps.push({ - id: crypto.randomUUID(), - type: 'searching', - searching: searchCalls.map((sc) => sc.arguments.queries).flat(), - }); - - session.updateBlock(researchBlockId, [ - { - op: 'replace', - path: '/data/subSteps', - value: block.data.subSteps, - }, - ]); - } - const actionResults = await ActionRegistry.executeAll(finalToolCalls, { llm: input.config.llm, embedding: input.config.embedding, session: session, + researchBlockId: researchBlockId, }); actionOutput.push(...actionResults); @@ -193,39 +171,41 @@ class Researcher { content: JSON.stringify(action), }); }); - - const searchResults = actionResults.filter( - (a) => a.type === 'search_results', - ); - - if (searchResults.length > 0 && block && block.type === 'research') { - block.data.subSteps.push({ - id: crypto.randomUUID(), - type: 'reading', - reading: searchResults.flatMap((a) => a.results), - }); - - session.updateBlock(researchBlockId, [ - { - op: 'replace', - path: '/data/subSteps', - value: block.data.subSteps, - }, - ]); - } } - const searchResults = actionOutput.filter( - (a) => a.type === 'search_results', - ); + const searchResults = actionOutput + .filter((a) => a.type === 'search_results') + .flatMap((a) => a.results); + + const seenUrls = new Map<string, number>(); + + const filteredSearchResults = searchResults + .map((result, index) => { + if (result.metadata.url && !seenUrls.has(result.metadata.url)) { + seenUrls.set(result.metadata.url, index); + return result; + } else if (result.metadata.url && seenUrls.has(result.metadata.url)) { + const existingIndex = seenUrls.get(result.metadata.url)!; + + const existingResult = searchResults[existingIndex]; + + existingResult.content += `\n\n${result.content}`; + + return undefined; + } + + return result; + }) + .filter((r) => r !== undefined); session.emit('data', { type: 'sources', - data: searchResults.flatMap((a) => a.results), + data: filteredSearchResults, }); return { findings: actionOutput, + searchFindings: filteredSearchResults, }; } } From 3bffc72422c465d2834d8996e9f14aa3d77c820b Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 9 Dec 2025 11:40:40 +0530 Subject: [PATCH 103/196] feat(types): update research action type --- src/lib/agents/search/types.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts index 6a6ac69..f1ae862 100644 --- a/src/lib/agents/search/types.ts +++ b/src/lib/agents/search/types.ts @@ -99,9 +99,10 @@ export interface ResearchAction< TSchema extends z.ZodObject<any> = z.ZodObject<any>, > { name: string; - description: string; schema: z.ZodObject<any>; - enabled: (config: { classification: ClassifierOutput }) => boolean; + getToolDescription: (config: { mode: SearchAgentConfig['mode'] }) => string; + getDescription: (config: { mode: SearchAgentConfig['mode'] }) => string; + enabled: (config: { classification: ClassifierOutput, mode: SearchAgentConfig['mode'] }) => boolean; execute: ( params: z.infer<TSchema>, additionalConfig: AdditionalConfig & { From 01b537ade16d797a5564dce6449480f862f90c4b Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 9 Dec 2025 11:41:55 +0530 Subject: [PATCH 104/196] feat(actions): add tool description, description --- .../agents/search/researcher/actions/done.ts | 13 +++- .../agents/search/researcher/actions/plan.ts | 17 ++++- .../search/researcher/actions/registry.ts | 8 ++- .../search/researcher/actions/scrapeURL.ts | 12 +++- .../search/researcher/actions/webSearch.ts | 70 ++++++++++++++++--- src/lib/agents/search/researcher/index.ts | 3 + 6 files changed, 105 insertions(+), 18 deletions(-) diff --git a/src/lib/agents/search/researcher/actions/done.ts b/src/lib/agents/search/researcher/actions/done.ts index bfed3e2..b19df4f 100644 --- a/src/lib/agents/search/researcher/actions/done.ts +++ b/src/lib/agents/search/researcher/actions/done.ts @@ -1,12 +1,19 @@ import z from 'zod'; import { ResearchAction } from '../../types'; +const actionDescription = ` +Use this action ONLY when you have completed all necessary research and are ready to provide a final answer to the user. This indicates that you have gathered sufficient information from previous steps and are concluding the research process. +YOU MUST CALL THIS ACTION TO SIGNAL COMPLETION; DO NOT OUTPUT FINAL ANSWERS DIRECTLY TO THE USER. +IT WILL BE AUTOMATICALLY TRIGGERED IF MAXIMUM ITERATIONS ARE REACHED SO IF YOU'RE LOW ON ITERATIONS, DON'T CALL IT AND INSTEAD FOCUS ON GATHERING ESSENTIAL INFO FIRST. +` + const doneAction: ResearchAction<any> = { name: 'done', - description: - 'Only call this after ___plan AND after any other needed tool calls when you truly have enough to answer. Do not call if information is still missing.', - enabled: (_) => true, schema: z.object({}), + getToolDescription: () => + 'Only call this after ___plan AND after any other needed tool calls when you truly have enough to answer. Do not call if information is still missing.', + getDescription: () => actionDescription, + enabled: (_) => true, execute: async (params, additionalConfig) => { return { type: 'done', diff --git a/src/lib/agents/search/researcher/actions/plan.ts b/src/lib/agents/search/researcher/actions/plan.ts index d4d7661..dcdd05b 100644 --- a/src/lib/agents/search/researcher/actions/plan.ts +++ b/src/lib/agents/search/researcher/actions/plan.ts @@ -9,12 +9,23 @@ const schema = z.object({ ), }); +const actionDescription = ` +Use thi tool FIRST on every turn to state your plan in natural language before any other action. Keep it short, action-focused, and tailored to the current query. +Make sure to not include reference to any tools or actions you might take, just the plan itself. The user isn't aware about tools, but they love to see your thought process. + +Here are some examples of good plans: +<examples> +- "Okay, the user wants to know the latest advancements in renewable energy. I will start by looking for recent articles and studies on this topic, then summarize the key points." -> "I have gathered enough information to provide a comprehensive answer." +- "The user is asking about the health benefits of a Mediterranean diet. I will search for scientific studies and expert opinions on this diet, then compile the findings into a clear summary." -> "I have gathered information about the Mediterranean diet and its health benefits, I will now look up for any recent studies to ensure the information is current." +<examples> +` + const planAction: ResearchAction<typeof schema> = { name: '___plan', - description: - 'Use this FIRST on every turn to state your plan in natural language before any other action. Keep it short, action-focused, and tailored to the current query.', schema: schema, - enabled: (_) => true, + getToolDescription: () => 'Use this FIRST on every turn to state your plan in natural language before any other action. Keep it short, action-focused, and tailored to the current query.', + getDescription: () => actionDescription, + enabled: (config) => config.mode !== 'speed', execute: async (input, _) => { return { type: 'reasoning', diff --git a/src/lib/agents/search/researcher/actions/registry.ts b/src/lib/agents/search/researcher/actions/registry.ts index 7454763..3a8eda6 100644 --- a/src/lib/agents/search/researcher/actions/registry.ts +++ b/src/lib/agents/search/researcher/actions/registry.ts @@ -4,6 +4,7 @@ import { AdditionalConfig, ClassifierOutput, ResearchAction, + SearchAgentConfig, } from '../../types'; class ActionRegistry { @@ -19,6 +20,7 @@ class ActionRegistry { static getAvailableActions(config: { classification: ClassifierOutput; + mode: SearchAgentConfig['mode']; }): ResearchAction[] { return Array.from( this.actions.values().filter((action) => action.enabled(config)), @@ -27,23 +29,25 @@ class ActionRegistry { static getAvailableActionTools(config: { classification: ClassifierOutput; + mode: SearchAgentConfig['mode']; }): Tool[] { const availableActions = this.getAvailableActions(config); return availableActions.map((action) => ({ name: action.name, - description: action.description, + description: action.getToolDescription({ mode: config.mode }), schema: action.schema, })); } static getAvailableActionsDescriptions(config: { classification: ClassifierOutput; + mode: SearchAgentConfig['mode']; }): string { const availableActions = this.getAvailableActions(config); return availableActions - .map((action) => `------------\n##${action.name}\n${action.description}`) + .map((action) => `<tool name="${action.name}">\n${action.getDescription({ mode: config.mode })}\n</tool>`) .join('\n\n'); } diff --git a/src/lib/agents/search/researcher/actions/scrapeURL.ts b/src/lib/agents/search/researcher/actions/scrapeURL.ts index 03dea2f..3d3173e 100644 --- a/src/lib/agents/search/researcher/actions/scrapeURL.ts +++ b/src/lib/agents/search/researcher/actions/scrapeURL.ts @@ -10,11 +10,19 @@ const schema = z.object({ urls: z.array(z.string()).describe('A list of URLs to scrape content from.'), }); +const actionDescription = ` +Use this tool to scrape and extract content from the provided URLs. This is useful when you the user has asked you to extract or summarize information from specific web pages. You can provide up to 3 URLs at a time. NEVER CALL THIS TOOL EXPLICITLY YOURSELF UNLESS INSTRUCTED TO DO SO BY THE USER. +You should only call this tool when the user has specifically requested information from certain web pages, never call this yourself to get extra information without user instruction. + +For example, if the user says "Please summarize the content of https://example.com/article", you can call this tool with that URL to get the content and then provide the summary or "What does X mean according to https://example.com/page", you can call this tool with that URL to get the content and provide the explanation. +` + const scrapeURLAction: ResearchAction<typeof schema> = { name: 'scrape_url', - description: - 'Use this tool to scrape and extract content from the provided URLs. This is useful when you the user has asked you to extract or summarize information from specific web pages. You can provide up to 3 URLs at a time. NEVER CALL THIS TOOL EXPLICITLY YOURSELF UNLESS INSTRUCTED TO DO SO BY THE USER.', schema: schema, + getToolDescription: () => + 'Use this tool to scrape and extract content from the provided URLs. This is useful when you the user has asked you to extract or summarize information from specific web pages. You can provide up to 3 URLs at a time. NEVER CALL THIS TOOL EXPLICITLY YOURSELF UNLESS INSTRUCTED TO DO SO BY THE USER.', + getDescription: () => actionDescription, enabled: (_) => true, execute: async (params, additionalConfig) => { params.urls = params.urls.slice(0, 3); diff --git a/src/lib/agents/search/researcher/actions/webSearch.ts b/src/lib/agents/search/researcher/actions/webSearch.ts index f148155..04e9702 100644 --- a/src/lib/agents/search/researcher/actions/webSearch.ts +++ b/src/lib/agents/search/researcher/actions/webSearch.ts @@ -10,22 +10,76 @@ const actionSchema = z.object({ .describe('An array of search queries to perform web searches for.'), }); -const actionDescription = ` -Use immediately after the ___plan call when you need information. Default to using this unless you already have everything needed to finish. Provide 1-3 short, SEO-friendly queries (keywords, not sentences) that cover the user ask. Always prefer current/contextual queries (e.g., include year for news). +const speedModePrompt = ` +Use this tool to perform web searches based on the provided queries. This is useful when you need to gather information from the web to answer the user's questions. You can provide up to 3 queries at a time. You will have to use this every single time if this is present and relevant. +You are currently on speed mode, meaning you would only get to call this tool once. Make sure to prioritize the most important queries that are likely to get you the needed information in one go. -You can search maximum of 3 queries at a time. +Your queries should be very targeted and specific to the information you need, avoid broad or generic queries. +Your queries shouldn't be sentences but rather keywords that are SEO friendly and can be used to search the web for information. -For fast mode, you can only use this tool once so make sure to get all needed information in one go. +For example, if the user is asking about the features of a new technology, you might use queries like "GPT-5.1 features", "GPT-5.1 release date", "GPT-5.1 improvements" rather than a broad query like "Tell me about GPT-5.1". -For balanced and quality modes, you can use this tool multiple times as needed. +You can search for 3 queries in one go, make sure to utilize all 3 queries to maximize the information you can gather. If a question is simple, then split your queries to cover different aspects or related topics to get a comprehensive understanding. +If this tool is present and no other tools are more relevant, you MUST use this tool to get the needed information. +` -In quality and balanced mode, first try to gather upper level information with broad queries, then use more specific queries based on what you find to find all information needed. -`; +const balancedModePrompt = ` +Use this tool to perform web searches based on the provided queries. This is useful when you need to gather information from the web to answer the user's questions. You can provide up to 3 queries at a time. You will have to use this every single time if this is present and relevant. + +You can call this tool several times if needed to gather enough information. +Start initially with broader queries to get an overview, then narrow down with more specific queries based on the results you receive. + +Your queries shouldn't be sentences but rather keywords that are SEO friendly and can be used to search the web for information. + +For example if the user is asking about Tesla, your actions should be like: +1. __plan "The user is asking about Tesla. I will start with broader queries to get an overview of Tesla, then narrow down with more specific queries based on the results I receive." then +2. web_search ["Tesla", "Tesla latest news", "Tesla stock price"] then +3. __plan "Based on the previous search results, I will now narrow down my queries to focus on Tesla's recent developments and stock performance." then +4. web_search ["Tesla Q2 2025 earnings", "Tesla new model 2025", "Tesla stock analysis"] then done. +5. __plan "I have gathered enough information to provide a comprehensive answer." +6. done. + +You can search for 3 queries in one go, make sure to utilize all 3 queries to maximize the information you can gather. If a question is simple, then split your queries to cover different aspects or related topics to get a comprehensive understanding. +If this tool is present and no other tools are more relevant, you MUST use this tool to get the needed information. You can call this tools, multiple times as needed. +` + +const qualityModePrompt = ` +Use this tool to perform web searches based on the provided queries. This is useful when you need to gather information from the web to answer the user's questions. You can provide up to 3 queries at a time. You will have to use this every single time if this is present and relevant. + +You have to call this tool several times to gather enough information unless the question is very simple (like greeting questions or basic facts). +Start initially with broader queries to get an overview, then narrow down with more specific queries based on the results you receive. +Never stop before at least 5-6 iterations of searches unless the user question is very simple. + +Your queries shouldn't be sentences but rather keywords that are SEO friendly and can be used to search the web for information. + +You can search for 3 queries in one go, make sure to utilize all 3 queries to maximize the information you can gather. If a question is simple, then split your queries to cover different aspects or related topics to get a comprehensive understanding. +If this tool is present and no other tools are more relevant, you MUST use this tool to get the needed information. You can call this tools, multiple times as needed. +` const webSearchAction: ResearchAction<typeof actionSchema> = { name: 'web_search', - description: actionDescription, schema: actionSchema, + getToolDescription: () => 'Use this tool to perform web searches based on the provided queries. This is useful when you need to gather information from the web to answer the user\'s questions. You can provide up to 3 queries at a time. You will have to use this every single time if this is present and relevant.', + getDescription: (config) => { + let prompt = '' + + switch (config.mode) { + case 'speed': + prompt = speedModePrompt + break; + case 'balanced': + prompt = balancedModePrompt + break; + case 'quality': + prompt = qualityModePrompt + break; + default: + prompt = speedModePrompt + break; + } + + return prompt + }, enabled: (config) => config.classification.classification.skipSearch === false, execute: async (input, additionalConfig) => { diff --git a/src/lib/agents/search/researcher/index.ts b/src/lib/agents/search/researcher/index.ts index feb450f..6762a61 100644 --- a/src/lib/agents/search/researcher/index.ts +++ b/src/lib/agents/search/researcher/index.ts @@ -5,6 +5,7 @@ import SessionManager from '@/lib/session'; import { Message, ReasoningResearchBlock } from '@/lib/types'; import formatChatHistoryAsString from '@/lib/utils/formatHistory'; import { ToolCall } from '@/lib/models/types'; +import fs from 'fs'; class Researcher { async research( @@ -21,11 +22,13 @@ class Researcher { const availableTools = ActionRegistry.getAvailableActionTools({ classification: input.classification, + mode: input.config.mode, }); const availableActionsDescription = ActionRegistry.getAvailableActionsDescriptions({ classification: input.classification, + mode: input.config.mode }); const researchBlockId = crypto.randomUUID(); From fc0c444b6a63118499f40cb29803fc0bce51d504 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 9 Dec 2025 11:42:11 +0530 Subject: [PATCH 105/196] feat(researcher-prompt): add mode based prompts --- src/lib/prompts/search/researcher.ts | 524 +++++++++++++++------------ 1 file changed, 288 insertions(+), 236 deletions(-) diff --git a/src/lib/prompts/search/researcher.ts b/src/lib/prompts/search/researcher.ts index 08eeaf9..3ac5201 100644 --- a/src/lib/prompts/search/researcher.ts +++ b/src/lib/prompts/search/researcher.ts @@ -1,245 +1,297 @@ +const getSpeedPrompt = (actionDesc: string, i: number, maxIteration: number) => { + const today = new Date().toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }) + + return ` + Assistant is an action orchestrator. Your job is to fulfill user requests by selecting and executing the available tools—no free-form replies. + You will be shared with the conversation history between user and an AI, along with the user's latest follow-up question. Based on this, you must use the available tools to fulfill the user's request. + + Today's date: ${today} + + You are currently on iteration ${i + 1} of your research process and have ${maxIteration} total iterations so act efficiently. + When you are finished, you must call the \`done\` tool. Never output text directly. + + <goal> + Fulfill the user's request as quickly as possible using the available tools. + Call tools to gather information or perform tasks as needed. + </goal> + + <core_principle> + Your knowledge is outdated; if you have web search, use it to ground answers even for seemingly basic facts. + </core_principle> + + <examples> + + ## Example 1: Unknown Subject + User: "What is Kimi K2?" + Action: web_search ["Kimi K2", "Kimi K2 AI"] then done. + + ## Example 2: Subject You're Uncertain About + User: "What are the features of GPT-5.1?" + Action: web_search ["GPT-5.1", "GPT-5.1 features", "GPT-5.1 release"] then done. + + ## Example 3: After Tool calls Return Results + User: "What are the features of GPT-5.1?" + [Previous tool calls returned the needed info] + Action: done. + + </examples> + + <available_tools> + ${actionDesc} + </available_tools> + + <mistakes_to_avoid> + +1. **Over-assuming**: Don't assume things exist or don't exist - just look them up + +2. **Verification obsession**: Don't waste tool calls "verifying existence" - just search for the thing directly + +3. **Endless loops**: If 2-3 tool calls don't find something, it probably doesn't exist - report that and move on + +4. **Ignoring task context**: If user wants a calendar event, don't just search - create the event + +5. **Overthinking**: Keep reasoning simple and tool calls focused + +</mistakes_to_avoid> + + <response_protocol> +- NEVER output normal text to the user. ONLY call tools. +- Choose the appropriate tools based on the action descriptions provided above. +- Default to web_search when information is missing or stale; keep queries targeted (max 3 per call). +- Call done when you have gathered enough to answer or performed the required actions. +- Do not invent tools. Do not return JSON. + </response_protocol> + ` +} + +const getBalancedPrompt = (actionDesc: string, i: number, maxIteration: number) => { + const today = new Date().toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }) + + return ` + Assistant is an action orchestrator. Your job is to fulfill user requests by planning briefly and executing the available tools—no free-form replies. + You will be shared with the conversation history between user and an AI, along with the user's latest follow-up question. Based on this, you must use the available tools to fulfill the user's request. + + Today's date: ${today} + + You are currently on iteration ${i + 1} of your research process and have ${maxIteration} total iterations so act efficiently. + When you are finished, you must call the \`done\` tool. Never output text directly. + + <goal> + Fulfill the user's request with concise planning plus focused actions. + You must call the ___plan tool first on every turn to state a short plan. Open with a brief intent phrase (e.g., "Okay, the user wants to...", "Searching for...", "Looking into...") and lay out the steps you will take. Keep it natural language, no tool names. + After planning, use the available tools as needed to gather or act, then finish with done. + </goal> + + <core_principle> + Your knowledge is outdated; if you have web search, use it to ground answers even for seemingly basic facts. + You can call at most 6 tools total per turn: up to 2 reasoning (___plan counts as reasoning), 2-3 information-gathering calls, and 1 done. If you hit the cap, stop after done. + Aim for at least two information-gathering calls when the answer is not already obvious; only skip the second if the question is trivial or you already have sufficient context. + Do not spam searches—pick the most targeted queries. + </core_principle> + + <done_usage> + Call done only after the plan plus the necessary tool calls are completed and you have enough to answer. If you call done early, stop. If you reach the tool cap, call done to conclude. + </done_usage> + + <examples> + + ## Example 1: Unknown Subject + User: "What is Kimi K2?" + Plan: "Okay, the user wants to know about Kimi K2. I will start by looking for what Kimi K2 is and its key details, then summarize the findings." + Action: web_search ["Kimi K2", "Kimi K2 AI"] then done. + + ## Example 2: Subject You're Uncertain About + User: "What are the features of GPT-5.1?" + Plan: "The user is asking about GPT-5.1 features. I will search for current feature and release information, then compile a summary." + Action: web_search ["GPT-5.1", "GPT-5.1 features", "GPT-5.1 release"] then done. + + ## Example 3: After Tool calls Return Results + User: "What are the features of GPT-5.1?" + [Previous tool calls returned the needed info] + Plan: "I have gathered enough information about GPT-5.1 features; I will now wrap up." + Action: done. + + </examples> + + <available_tools> + YOU MUST ALWAYS CALL THE ___plan TOOL FIRST ON EVERY TURN BEFORE ANY OTHER ACTION. IF YOU DO NOT CALL IT, THE TOOL CALL WILL BE IGNORED. + ${actionDesc} + </available_tools> + + <mistakes_to_avoid> + +1. **Over-assuming**: Don't assume things exist or don't exist - just look them up + +2. **Verification obsession**: Don't waste tool calls "verifying existence" - just search for the thing directly + +3. **Endless loops**: If 2-3 tool calls don't find something, it probably doesn't exist - report that and move on + +4. **Ignoring task context**: If user wants a calendar event, don't just search - create the event + +5. **Overthinking**: Keep reasoning simple and tool calls focused + +6. **Skipping the plan**: Always call ___plan first to outline your approach before other actions + +</mistakes_to_avoid> + + <response_protocol> +- NEVER output normal text to the user. ONLY call tools. +- Start with ___plan: open with intent phrase ("Okay, the user wants to...", "Looking into...", etc.) and lay out steps. No tool names. +- Choose tools based on the action descriptions provided above. +- Default to web_search when information is missing or stale; keep queries targeted (max 3 per call). +- Use at most 6 tool calls total (___plan + 2-3 info calls + optional extra reasoning if needed + done). If done is called early, stop. +- Do not stop after a single information-gathering call unless the task is trivial or prior results already cover the answer. +- Call done only after you have the needed info or actions completed; do not call it early. +- Do not invent tools. Do not return JSON. + </response_protocol> + ` +} + +const getQualityPrompt = (actionDesc: string, i: number, maxIteration: number) => { + const today = new Date().toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }) + + return ` + Assistant is a deep-research orchestrator. Your job is to fulfill user requests with the most thorough, comprehensive research possible—no free-form replies. + You will be shared with the conversation history between user and an AI, along with the user's latest follow-up question. Based on this, you must use the available tools to fulfill the user's request with depth and rigor. + + Today's date: ${today} + + You are currently on iteration ${i + 1} of your research process and have ${maxIteration} total iterations. Use every iteration wisely to gather comprehensive information. + When you are finished, you must call the \`done\` tool. Never output text directly. + + <goal> + Conduct the deepest, most thorough research possible. Leave no stone unturned. + Follow an iterative plan-act loop: call ___plan first to outline your next step, then call the appropriate tool(s) to gather info or take action, then call ___plan again to reflect on results and decide the next step. Repeat until you have exhaustive coverage. + Open each plan with a brief intent phrase (e.g., "Okay, the user wants to know about...", "From the results, it looks like...", "Now I need to dig into...") and describe what you'll do next. Keep it natural language, no tool names. + Finish with done only when you have comprehensive, multi-angle information. + </goal> + + <core_principle> + Your knowledge is outdated; always use the available tools to ground answers. + This is DEEP RESEARCH mode—be exhaustive. Explore multiple angles: definitions, features, comparisons, recent news, expert opinions, use cases, limitations, and alternatives. + You can call up to 10 tools total per turn. Use an iterative loop: ___plan → tool call(s) → ___plan → tool call(s) → ... → done. + Never settle for surface-level answers. If results hint at more depth, plan your next step and follow up. Cross-reference information from multiple queries. + </core_principle> + + <done_usage> + Call done only after you have gathered comprehensive, multi-angle information. Do not call done early—exhaust your research budget first. If you reach the tool cap, call done to conclude. + </done_usage> + + <examples> + + ## Example 1: Unknown Subject - Deep Dive + User: "What is Kimi K2?" + Plan: "Okay, the user wants to know about Kimi K2. I'll start by finding out what it is and its key capabilities." + [calls info-gathering tool] + Plan: "From the results, Kimi K2 is an AI model by Moonshot. Now I need to dig into how it compares to competitors and any recent news." + [calls info-gathering tool] + Plan: "Got comparison info. Let me also check for limitations or critiques to give a balanced view." + [calls info-gathering tool] + Plan: "I now have comprehensive coverage—definition, capabilities, comparisons, and critiques. Wrapping up." + Action: done. + + ## Example 2: Feature Research - Comprehensive + User: "What are the features of GPT-5.1?" + Plan: "The user wants comprehensive GPT-5.1 feature information. I'll start with core features and specs." + [calls info-gathering tool] + Plan: "Got the basics. Now I should look into how it compares to GPT-4 and benchmark performance." + [calls info-gathering tool] + Plan: "Good comparison data. Let me also gather use cases and expert opinions for depth." + [calls info-gathering tool] + Plan: "I have exhaustive coverage across features, comparisons, benchmarks, and reviews. Done." + Action: done. + + ## Example 3: Iterative Refinement + User: "Tell me about quantum computing applications in healthcare." + Plan: "Okay, the user wants to know about quantum computing in healthcare. I'll start with an overview of current applications." + [calls info-gathering tool] + Plan: "Results mention drug discovery and diagnostics. Let me dive deeper into drug discovery use cases." + [calls info-gathering tool] + Plan: "Now I'll explore the diagnostics angle and any recent breakthroughs." + [calls info-gathering tool] + Plan: "Comprehensive coverage achieved. Wrapping up." + Action: done. + + </examples> + + <available_tools> + YOU MUST ALWAYS CALL THE ___plan TOOL FIRST ON EVERY TURN BEFORE ANY OTHER ACTION. IF YOU DO NOT CALL IT, THE TOOL CALL WILL BE IGNORED. + ${actionDesc} + </available_tools> + + <research_strategy> + For any topic, consider searching: + 1. **Core definition/overview** - What is it? + 2. **Features/capabilities** - What can it do? + 3. **Comparisons** - How does it compare to alternatives? + 4. **Recent news/updates** - What's the latest? + 5. **Reviews/opinions** - What do experts say? + 6. **Use cases** - How is it being used? + 7. **Limitations/critiques** - What are the downsides? + </research_strategy> + + <mistakes_to_avoid> + +1. **Shallow research**: Don't stop after one or two searches—dig deeper from multiple angles + +2. **Over-assuming**: Don't assume things exist or don't exist - just look them up + +3. **Missing perspectives**: Search for both positive and critical viewpoints + +4. **Ignoring follow-ups**: If results hint at interesting sub-topics, explore them + +5. **Premature done**: Don't call done until you've exhausted reasonable research avenues + +6. **Skipping the plan**: Always call ___plan first to outline your research strategy + +</mistakes_to_avoid> + + <response_protocol> +- NEVER output normal text to the user. ONLY call tools. +- Follow an iterative loop: ___plan → tool call(s) → ___plan → tool call(s) → ... → done. +- Each ___plan should reflect on previous results (if any) and state the next research step. No tool names in the plan. +- Choose tools based on the action descriptions provided above—use whatever tools are available to accomplish the task. +- Aim for 4-7 information-gathering calls covering different angles; cross-reference and follow up on interesting leads. +- Call done only after comprehensive, multi-angle research is complete. +- Do not invent tools. Do not return JSON. + </response_protocol> + ` +} + export const getResearcherPrompt = ( actionDesc: string, mode: 'speed' | 'balanced' | 'quality', i: number, maxIteration: number, ) => { - const today = new Date().toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - }); + let prompt = '' - return ` -You are an action orchestrator. Your job is to fulfill user requests by selecting and executing appropriate actions - whether that's searching for information, creating calendar events, sending emails, or any other available action. -You will be shared with the conversation history between user and AI, along with the user's latest follow-up question and your previous actions' results (if any. Note that they're per conversation so if they contain any previous actions it was executed for the last follow up (the one you're currently handling)). Based on this, you must decide the best next action(s) to take to fulfill the user's request. - -Today's date: ${today} - -You are operating in "${mode}" mode. ${ - mode === 'speed' - ? 'Prioritize speed - use as few actions as possible to get the needed information quickly.' - : mode === 'balanced' - ? 'Balance speed and depth - use a moderate number of actions to get good information efficiently. Never stop at the first action unless there is no action available or the query is simple.' - : 'Conduct deep research - use multiple actions to gather comprehensive information, even if it takes longer.' + switch (mode) { + case 'speed': + prompt = getSpeedPrompt(actionDesc, i, maxIteration) + break + case 'balanced': + prompt = getBalancedPrompt(actionDesc, i, maxIteration) + break + case 'quality': + prompt = getQualityPrompt(actionDesc, i, maxIteration) + break + default: + prompt = getSpeedPrompt(actionDesc, i, maxIteration) + break } -You are currently on iteration ${i + 1} of your research process and have ${maxIteration} total iterations so please take action accordingly. After max iterations, the done action would get called automatically so you don't have to worry about that unless you want to end the research early. - -<available_actions> -${actionDesc} -</available_actions> - -<core_principle> - -NEVER ASSUME - your knowledge may be outdated. When a user asks about something you're not certain about, go find out. Don't assume it exists or doesn't exist - just look it up directly. - -</core_principle> - -<reasoning_approach> -You never speak your reasoning to the user. You MUST call the ___plan tool first on every turn and put your reasoning there. -The plan must be 2-4 concise sentences, starting with "Okay, the user wants to..." and outlining the steps you will take next. -</reasoning_approach> - -<examples> - -## Example 1: Unknown Subject - -User: "What is Kimi K2?" - -Good reasoning: -"I'm not sure what Kimi K2 is - could be an AI model, a product, or something else. Let me look it up to find out what it actually is and get the relevant details." - -Actions: web_search ["Kimi K2", "Kimi K2 AI"] - -## Example 2: Subject You're Uncertain About - -User: "What are the features of GPT-5.1?" - -Good reasoning: -"I don't have current information on GPT-5.1 - my knowledge might be outdated. Let me look up GPT-5.1 to see what's available and what features it has." - -Actions: web_search ["GPT-5.1", "GPT-5.1 features", "GPT-5.1 release"] - -Bad reasoning (wastes time on verification): -"GPT-5.1 might not exist based on my knowledge. I need to verify if it exists first before looking for features." - -## Example 3: After Actions Return Results - -User: "What are the features of GPT-5.1?" -[Previous actions returned information about GPT-5.1] - -Good reasoning: -"Got the information I needed about GPT-5.1. The results cover its features and capabilities - I can now provide a complete answer." - -Action: done - -## Example 4: Ambiguous Query - -User: "Tell me about Mercury" - -Good reasoning: -"Mercury could refer to several things - the planet, the element, or something else. I'll look up both main interpretations to give a useful answer." - -Actions: web_search ["Mercury planet facts", "Mercury element"] - -## Example 5: Current Events - -User: "What's happening with AI regulation?" - -Good reasoning: -"I need current news on AI regulation developments. Let me find the latest updates on this topic." - -Actions: web_search ["AI regulation news 2024", "AI regulation bill latest"] - -## Example 6: Technical Query - -User: "How do I set up authentication in Next.js 14?" - -Good reasoning: -"This is a technical implementation question. I'll find the current best practices and documentation for Next.js 14 authentication." - -Actions: web_search ["Next.js 14 authentication guide", "NextAuth.js App Router"] - -## Example 7: Comparison Query - -User: "Prisma vs Drizzle - which should I use?" - -Good reasoning: -"Need to find factual comparisons between these ORMs - performance, features, trade-offs. Let me gather objective information." - -Actions: web_search ["Prisma vs Drizzle comparison 2024", "Drizzle ORM performance"] - -## Example 8: Fact-Check - -User: "Is it true you only use 10% of your brain?" - -Good reasoning: -"This is a common claim that needs scientific verification. Let me find what the actual research says about this." - -Actions: web_search ["10 percent brain myth science", "brain usage neuroscience"] - -## Example 9: Recent Product - -User: "What are the specs of MacBook Pro M4?" - -Good reasoning: -"I need current information on the MacBook Pro M4. Let me look up the latest specs and details." - -Actions: web_search ["MacBook Pro M4 specs", "MacBook Pro M4 specifications Apple"] - -## Example 10: Multi-Part Query - -User: "Population of Tokyo vs New York?" - -Good reasoning: -"Need current population stats for both cities. I'll look up the comparison data." - -Actions: web_search ["Tokyo population 2024", "Tokyo vs New York population"] - -## Example 11: Calendar Task - -User: "Add a meeting with John tomorrow at 3pm" - -Good reasoning: -"This is a calendar task. I have all the details - meeting with John, tomorrow, 3pm. I'll create the event." - -Action: create_calendar_event with the provided details - -## Example 12: Email Task - -User: "Send an email to sarah@company.com about the project update" - -Good reasoning: -"Need to send an email. I have the recipient but need to compose appropriate content about the project update." - -Action: send_email to sarah@company.com with project update content - -## Example 13: Multi-Step Task - -User: "What's the weather in Tokyo and add a reminder to pack an umbrella if it's rainy" - -Good reasoning: -"Two things here - first I need to check Tokyo's weather, then based on that I might need to create a reminder. Let me start with the weather lookup." - -Actions: web_search ["Tokyo weather today forecast"] - -## Example 14: Research Then Act - -User: "Find the best Italian restaurant near me and make a reservation for 7pm" - -Good reasoning: -"I need to first find top Italian restaurants in the area, then make a reservation. Let me start by finding the options." - -Actions: web_search ["best Italian restaurant near me", "top rated Italian restaurants"] - -</examples> - -<action_guidelines> - -## For Information Queries: -- Just look it up - don't overthink whether something exists -- Use 1-3 targeted queries -- Done when you have useful information to answer with - -## For Task Execution: -- Calendar, email, reminders: execute directly with the provided details -- If details are missing, note what you need - -## For Multi-Step Requests: -- Break it down logically -- Complete one part before moving to the next -- Some tasks require information before you can act - -## When to Select "done": -- You have the information needed to answer -- You've completed the requested task -- Further actions would be redundant - -</action_guidelines> - -<query_formulation> - -**General subjects:** -- ["subject name", "subject name + context"] - -**Current events:** -- Include year: "topic 2024", "topic latest news" - -**Technical topics:** -- Include versions: "framework v14 guide" -- Add context: "documentation", "tutorial", "how to" - -**Comparisons:** -- "X vs Y comparison", "X vs Y benchmarks" - -**Keep it simple:** -- 1-3 actions per iteration -- Don't over-complicate queries - -</query_formulation> - -<mistakes_to_avoid> - -1. **Over-assuming**: Don't assume things exist or don't exist - just look them up - -2. **Verification obsession**: Don't waste actions "verifying existence" - just search for the thing directly - -3. **Endless loops**: If 2-3 actions don't find something, it probably doesn't exist - report that and move on - -4. **Ignoring task context**: If user wants a calendar event, don't just search - create the event - -5. **Overthinking**: Keep reasoning simple and action-focused - -</mistakes_to_avoid> - -<response_protocol> -- NEVER output normal text to the user. ONLY call tools. -- Every turn MUST start with a call to the planning tool: name = "___plan", argument: { plan: "Okay, the user wants to ..." + concise 2-4 sentence plan }. -- Immediately after ___plan, if any information is missing, call \`web_search\` with up to 3 targeted queries. Default to searching unless you are certain you have enough. -- Call \`done\` only after planning AND any required searches when you have enough to answer. -- Do not invent tools. Do not return JSON. Do not echo the plan outside of the tool call. -- If nothing else is needed after planning, call \`done\` immediately after the plan. -</response_protocol> -`; -}; + return prompt +}; From 0cfa01422c39ca2d9fc93313b79123e73c5482f9 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 12 Dec 2025 23:56:34 +0530 Subject: [PATCH 106/196] Create fileSearch.ts --- src/lib/agents/search/researcher/actions/fileSearch.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/lib/agents/search/researcher/actions/fileSearch.ts diff --git a/src/lib/agents/search/researcher/actions/fileSearch.ts b/src/lib/agents/search/researcher/actions/fileSearch.ts new file mode 100644 index 0000000..0905e5d --- /dev/null +++ b/src/lib/agents/search/researcher/actions/fileSearch.ts @@ -0,0 +1,10 @@ +import z from "zod"; +import { ResearchAction } from "../../types"; + +const schema = z.object({ + queries: z.array(z.string()).describe("A list of queries to search in files."), +}) + +const fileSearhAction: ResearchAction<typeof schema> = { + name: "file_search", +} \ No newline at end of file From 13ae0b9451c1f1ad68536b3e04db6965344bed98 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:14:29 +0530 Subject: [PATCH 107/196] feat(package): remove langchain, other unused packages --- package.json | 22 +- yarn.lock | 1152 ++++++++++++++------------------------------------ 2 files changed, 321 insertions(+), 853 deletions(-) diff --git a/package.json b/package.json index 1c4b60b..52ce2c5 100644 --- a/package.json +++ b/package.json @@ -14,26 +14,16 @@ "@headlessui/react": "^2.2.0", "@headlessui/tailwindcss": "^0.2.2", "@huggingface/transformers": "^3.7.5", - "@iarna/toml": "^2.2.5", "@icons-pack/react-simple-icons": "^12.3.0", - "@langchain/anthropic": "^1.0.1", - "@langchain/community": "^1.0.3", - "@langchain/core": "^1.0.5", - "@langchain/google-genai": "^1.0.1", - "@langchain/groq": "^1.0.1", - "@langchain/langgraph": "^1.0.1", - "@langchain/ollama": "^1.0.1", - "@langchain/openai": "^1.1.1", - "@langchain/textsplitters": "^1.0.0", "@tailwindcss/typography": "^0.5.12", + "@types/jspdf": "^2.0.0", "axios": "^1.8.3", "better-sqlite3": "^11.9.1", "clsx": "^2.1.0", "drizzle-orm": "^0.40.1", "framer-motion": "^12.23.25", - "html-to-text": "^9.0.5", - "jspdf": "^3.0.1", - "langchain": "^1.0.4", + "js-tiktoken": "^1.0.21", + "jspdf": "^3.0.4", "lightweight-charts": "^5.0.9", "lucide-react": "^0.556.0", "mammoth": "^1.9.1", @@ -41,10 +31,11 @@ "mathjs": "^15.1.0", "next": "^16.0.7", "next-themes": "^0.3.0", + "officeparser": "^5.2.2", "ollama": "^0.6.3", "openai": "^6.9.0", "partial-json": "^0.1.7", - "pdf-parse": "^1.1.1", + "pdf-parse": "^2.4.5", "react": "^18", "react-dom": "^18", "react-text-to-speech": "^0.14.5", @@ -53,15 +44,12 @@ "sonner": "^1.4.41", "tailwind-merge": "^2.2.2", "turndown": "^7.2.2", - "winston": "^3.17.0", "yahoo-finance2": "^3.10.2", "yet-another-react-lightbox": "^3.17.2", "zod": "^4.1.12" }, "devDependencies": { "@types/better-sqlite3": "^7.6.12", - "@types/html-to-text": "^9.0.4", - "@types/jspdf": "^2.0.0", "@types/node": "^24.8.1", "@types/pdf-parse": "^1.1.4", "@types/react": "^18", diff --git a/yarn.lock b/yarn.lock index 32bcae5..f0c27ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,19 +12,7 @@ resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== -"@anthropic-ai/sdk@^0.65.0": - version "0.65.0" - resolved "https://registry.yarnpkg.com/@anthropic-ai/sdk/-/sdk-0.65.0.tgz#3f464fe2029eacf8e7e7fb8197579d00c8ca7502" - integrity sha512-zIdPOcrCVEI8t3Di40nH4z9EoeyGZfXbYSvWdDLsB/KkaSYMnEgC7gmcgWu83g2NTn1ZTpbMvpdttWDGGIk6zw== - dependencies: - json-schema-to-ts "^3.1.1" - -"@babel/runtime@^7.12.5", "@babel/runtime@^7.26.7": - version "7.27.3" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.3.tgz#10491113799fb8d77e1d9273384d5d68deeea8f6" - integrity sha512-7EYtGezsdiDMyY80+65EzwiGmcJqpmcZCojSXaRgdrBaGtWTgDZKq69cPIVped6MkIM78cTQ2GOiEYjwOlG4xw== - -"@babel/runtime@^7.18.3", "@babel/runtime@^7.26.10": +"@babel/runtime@^7.12.5", "@babel/runtime@^7.26.10", "@babel/runtime@^7.28.4": version "7.28.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== @@ -36,25 +24,6 @@ dependencies: regenerator-runtime "^0.14.0" -"@cfworker/json-schema@^4.0.2": - version "4.1.1" - resolved "https://registry.yarnpkg.com/@cfworker/json-schema/-/json-schema-4.1.1.tgz#4a2a3947ee9fa7b7c24be981422831b8674c3be6" - integrity sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og== - -"@colors/colors@1.6.0", "@colors/colors@^1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" - integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== - -"@dabh/diagnostics@^2.0.2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" - integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== - dependencies: - colorspace "1.1.x" - enabled "2.0.x" - kuler "^2.0.0" - "@deno/shim-deno-test@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@deno/shim-deno-test/-/shim-deno-test-0.5.0.tgz#7d5dd221c736d182e587b8fd9bfca49b4dc0aa79" @@ -396,11 +365,6 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62" integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig== -"@google/generative-ai@^0.24.0": - version "0.24.1" - resolved "https://registry.yarnpkg.com/@google/generative-ai/-/generative-ai-0.24.1.tgz#634a3c06f8ea7a6125c1b0d6c1e66bb11afb52c9" - integrity sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q== - "@headlessui/react@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-2.2.0.tgz#a8e32f0899862849a1ce1615fa280e7891431ab7" @@ -450,11 +414,6 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== -"@iarna/toml@^2.2.5": - version "2.2.5" - resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c" - integrity sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg== - "@icons-pack/react-simple-icons@^12.3.0": version "12.3.0" resolved "https://registry.yarnpkg.com/@icons-pack/react-simple-icons/-/react-simple-icons-12.3.0.tgz#2089c8226842611d751fc86fa93fea053076db77" @@ -788,140 +747,143 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@langchain/anthropic@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@langchain/anthropic/-/anthropic-1.0.1.tgz#a9f836b11ecbce282fc2afb8d707c52fd37711c4" - integrity sha512-yVKePAT+nNHtybyyPlWqiq6lqcoDlIuMgL9B4WMEU5gbmzL170iodiqcgcZNFQLOC1V2wCOzywq6Zr0kB24AFg== - dependencies: - "@anthropic-ai/sdk" "^0.65.0" - -"@langchain/classic@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@langchain/classic/-/classic-1.0.3.tgz#92482cb4cb8692407b4ecde0df312f035934472f" - integrity sha512-XyoaiJSi4y7SzrZMCb3DdDfC+M3gqIQpVH2cOCh9xQf4244jNrncpLXF/MwOJYWxzTsjfcCAHIbFJ0kSH5nqmg== - dependencies: - "@langchain/openai" "1.1.1" - "@langchain/textsplitters" "1.0.0" - handlebars "^4.7.8" - js-yaml "^4.1.0" - jsonpointer "^5.0.1" - openapi-types "^12.1.3" - p-retry "4" - uuid "^10.0.0" - yaml "^2.2.1" - zod "^3.25.76 || ^4" - optionalDependencies: - langsmith "^0.3.64" - -"@langchain/community@^1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@langchain/community/-/community-1.0.3.tgz#278c82eee22ff37b120e182b07b7c23ffc6786ab" - integrity sha512-86L7qooSY8Fh5Sf2Tu/X8PvDJqvEXohyZUGusuv0XtnWGivwtecBm0vEbVPkLF07I2ZMtyAGzHJOblbveq6Nmg== - dependencies: - "@langchain/classic" "1.0.3" - "@langchain/openai" "1.1.1" - binary-extensions "^2.2.0" - flat "^5.0.2" - js-yaml "^4.1.0" - math-expression-evaluator "^2.0.0" - uuid "^10.0.0" - zod "^3.25.76 || ^4" - -"@langchain/core@^1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@langchain/core/-/core-1.0.5.tgz#1e20ecce80fa4d0b979ea05b24b879b8357d8092" - integrity sha512-9Hy/b9+j+mm0Bhnm8xD9B0KpBYTidroLrDHdbrHoMC2DqXoY2umvi1M3M/9D744qsMSaIMP0ZwFcy5YbqI/dGw== - dependencies: - "@cfworker/json-schema" "^4.0.2" - ansi-styles "^5.0.0" - camelcase "6" - decamelize "1.2.0" - js-tiktoken "^1.0.12" - langsmith "^0.3.64" - mustache "^4.2.0" - p-queue "^6.6.2" - p-retry "4" - uuid "^10.0.0" - zod "^3.25.76 || ^4" - -"@langchain/google-genai@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@langchain/google-genai/-/google-genai-1.0.1.tgz#3601645f652f24e3beb55acc61878070b49c24ed" - integrity sha512-a9Bzaswp1P+eA2V8hAWSBypqjxmH+/zhOY1TBdalQuPQBTRH35jBMVgX3CTTAheAzBUGQtlDD4/dR9tyemDbhw== - dependencies: - "@google/generative-ai" "^0.24.0" - uuid "^11.1.0" - -"@langchain/groq@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@langchain/groq/-/groq-1.0.1.tgz#7ec8822cd2f29eef4ae0f9c20f67268d1924ab96" - integrity sha512-vDQzv6A3mjG0/W/7vL4Iq+dnmhSbMHln+b7Rna810trjZzfNPZhAP6omqZyzCKIqjsQYUH4ODLnSUCNiarfYsQ== - dependencies: - groq-sdk "^0.19.0" - -"@langchain/langgraph-checkpoint@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.0.tgz#ece2ede439d0d0b0b532c4be7817fd5029afe4f8" - integrity sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A== - dependencies: - uuid "^10.0.0" - -"@langchain/langgraph-sdk@~1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@langchain/langgraph-sdk/-/langgraph-sdk-1.0.0.tgz#16faca6cc426432dee9316428d0aecd94e5b7989" - integrity sha512-g25ti2W7Dl5wUPlNK+0uIGbeNFqf98imhHlbdVVKTTkDYLhi/pI1KTgsSSkzkeLuBIfvt2b0q6anQwCs7XBlbw== - dependencies: - p-queue "^6.6.2" - p-retry "4" - uuid "^9.0.0" - -"@langchain/langgraph@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-1.0.0.tgz#de22851d61d6a3987b0e0983c961a2c3c45408a5" - integrity sha512-2k7Thk5qH0zMhP+sbNKwQZl6zSwxKtrsFmHGOEvUUMjH6Rb9n5IMxoF5WTJlP/CGMO9ulI+5UefNz3rOfjuPBg== - dependencies: - "@langchain/langgraph-checkpoint" "^1.0.0" - "@langchain/langgraph-sdk" "~1.0.0" - uuid "^10.0.0" - -"@langchain/langgraph@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-1.0.1.tgz#d0be714653e8a27665f86ea795c5c34189455406" - integrity sha512-7y8OTDLrHrpJ55Y5x7c7zU2BbqNllXwxM106Xrd+NaQB5CpEb4hbUfIwe4XmhhscKPwvhXAq3tjeUxw9MCiurQ== - dependencies: - "@langchain/langgraph-checkpoint" "^1.0.0" - "@langchain/langgraph-sdk" "~1.0.0" - uuid "^10.0.0" - -"@langchain/ollama@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@langchain/ollama/-/ollama-1.0.1.tgz#c63ac6db65110beef4020a5e2b167ad0bc678d33" - integrity sha512-Pe32hhTpMvnRlNFJxkdu6r1QzsONGz5uvoLiMU1TpgAUu7EyKr2osymlgjBLqDe2vMKUmqHb+yWRH0IppDBUOg== - dependencies: - ollama "^0.5.12" - uuid "^10.0.0" - -"@langchain/openai@1.1.1", "@langchain/openai@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@langchain/openai/-/openai-1.1.1.tgz#67ddcf54ee7ac402f6b75b4b9e25447e78c56a93" - integrity sha512-0kUaXejo/sn6QAohWHDaAUapC4CJRkJIajGaWfJC+llSqpDBnmBE1oHg1M2fi1OCeP+ns9SxB6BTsq4Qbiqmig== - dependencies: - js-tiktoken "^1.0.12" - openai "^6.9.0" - zod "^3.25.76 || ^4" - -"@langchain/textsplitters@1.0.0", "@langchain/textsplitters@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@langchain/textsplitters/-/textsplitters-1.0.0.tgz#1fe78562b9bf74b0a88f13d443cb3c79f4d28331" - integrity sha512-L1gOwOJXeM+6MKzrj9shSsDyH32j898jgqvVArOjdge2zLyY+Mv4aOuyAAxbPyaFdQXlxKfa9xjqIUyv8TzrqA== - dependencies: - js-tiktoken "^1.0.12" - "@mixmark-io/domino@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@mixmark-io/domino/-/domino-2.2.0.tgz#4e8ec69bf1afeb7a14f0628b7e2c0f35bdb336c3" integrity sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw== +"@napi-rs/canvas-android-arm64@0.1.80": + version "0.1.80" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz#2779ca5c8aaeb46c85eb72d29f1eb34efd46fb45" + integrity sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ== + +"@napi-rs/canvas-android-arm64@0.1.84": + version "0.1.84" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.84.tgz#7b476e3003be0aca08ab27962fd0d6e803939bec" + integrity sha512-pdvuqvj3qtwVryqgpAGornJLV6Ezpk39V6wT4JCnRVGy8I3Tk1au8qOalFGrx/r0Ig87hWslysPpHBxVpBMIww== + +"@napi-rs/canvas-darwin-arm64@0.1.80": + version "0.1.80" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz#638eaa2d0a2a373c7d15748743182718dcd95c4b" + integrity sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ== + +"@napi-rs/canvas-darwin-arm64@0.1.84": + version "0.1.84" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.84.tgz#0f131722f9f66316cea5f5ed7cfb9ad1290683cd" + integrity sha512-A8IND3Hnv0R6abc6qCcCaOCujTLMmGxtucMTZ5vbQUrEN/scxi378MyTLtyWg+MRr6bwQJ6v/orqMS9datIcww== + +"@napi-rs/canvas-darwin-x64@0.1.80": + version "0.1.80" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz#bd6bc048dbd4b02b9620d9d07117ed93e6970978" + integrity sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA== + +"@napi-rs/canvas-darwin-x64@0.1.84": + version "0.1.84" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.84.tgz#e6ab8c534172d8a8d434fa090da8a205359d8769" + integrity sha512-AUW45lJhYWwnA74LaNeqhvqYKK/2hNnBBBl03KRdqeCD4tKneUSrxUqIv8d22CBweOvrAASyKN3W87WO2zEr/A== + +"@napi-rs/canvas-linux-arm-gnueabihf@0.1.80": + version "0.1.80" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz#ce6bfbeb19d9234c42df5c384e5989aa7d734789" + integrity sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ== + +"@napi-rs/canvas-linux-arm-gnueabihf@0.1.84": + version "0.1.84" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.84.tgz#5898daa3050a8ba4619c1d6cea3a3217d46c5ffd" + integrity sha512-8zs5ZqOrdgs4FioTxSBrkl/wHZB56bJNBqaIsfPL4ZkEQCinOkrFF7xIcXiHiKp93J3wUtbIzeVrhTIaWwqk+A== + +"@napi-rs/canvas-linux-arm64-gnu@0.1.80": + version "0.1.80" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz#3b7a7832fef763826fa5fb740d5757204e52607d" + integrity sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw== + +"@napi-rs/canvas-linux-arm64-gnu@0.1.84": + version "0.1.84" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.84.tgz#fbbde94c04278259f1f40b4c199dfd9f95c82e66" + integrity sha512-i204vtowOglJUpbAFWU5mqsJgH0lVpNk/Ml4mQtB4Lndd86oF+Otr6Mr5KQnZHqYGhlSIKiU2SYnUbhO28zGQA== + +"@napi-rs/canvas-linux-arm64-musl@0.1.80": + version "0.1.80" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz#d8ccd91f31d70760628623cd575134ada17690a3" + integrity sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg== + +"@napi-rs/canvas-linux-arm64-musl@0.1.84": + version "0.1.84" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.84.tgz#8b02c46c5dbb0a58de87885c61ca1681b1199697" + integrity sha512-VyZq0EEw+OILnWk7G3ZgLLPaz1ERaPP++jLjeyLMbFOF+Tr4zHzWKiKDsEV/cT7btLPZbVoR3VX+T9/QubnURQ== + +"@napi-rs/canvas-linux-riscv64-gnu@0.1.80": + version "0.1.80" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz#927a3b859a0e3c691beaf52a19bc4736c4ffc9b8" + integrity sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw== + +"@napi-rs/canvas-linux-riscv64-gnu@0.1.84": + version "0.1.84" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.84.tgz#787c9c207f69aaa51b852c7063a6eed2985b7fca" + integrity sha512-PSMTh8DiThvLRsbtc/a065I/ceZk17EXAATv9uNvHgkgo7wdEfTh2C3aveNkBMGByVO3tvnvD5v/YFtZL07cIg== + +"@napi-rs/canvas-linux-x64-gnu@0.1.80": + version "0.1.80" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz#25c0416bcedd6fadc15295e9afa8d9697232050c" + integrity sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA== + +"@napi-rs/canvas-linux-x64-gnu@0.1.84": + version "0.1.84" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.84.tgz#fb6eaea81ce679575b5004bc2177ce2d9222642b" + integrity sha512-N1GY3noO1oqgEo3rYQIwY44kfM11vA0lDbN0orTOHfCSUZTUyiYCY0nZ197QMahZBm1aR/vYgsWpV74MMMDuNA== + +"@napi-rs/canvas-linux-x64-musl@0.1.80": + version "0.1.80" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz#de85d644e09120a60996bbe165cc2efee804551b" + integrity sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg== + +"@napi-rs/canvas-linux-x64-musl@0.1.84": + version "0.1.84" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.84.tgz#54a37352a0be7f2a7218b6f11d8ae9b5cdbb3d6c" + integrity sha512-vUZmua6ADqTWyHyei81aXIt9wp0yjeNwTH0KdhdeoBb6azHmFR8uKTukZMXfLCC3bnsW0t4lW7K78KNMknmtjg== + +"@napi-rs/canvas-win32-x64-msvc@0.1.80": + version "0.1.80" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz#6bb95885d9377912d71f1372fc1916fb54d6ef0a" + integrity sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg== + +"@napi-rs/canvas-win32-x64-msvc@0.1.84": + version "0.1.84" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.84.tgz#e0039b89f8e04287c77bd1fb5e6fa671d6c9d3c8" + integrity sha512-YSs8ncurc1xzegUMNnQUTYrdrAuaXdPMOa+iYYyAxydOtg0ppV386hyYMsy00Yip1NlTgLCseRG4sHSnjQx6og== + +"@napi-rs/canvas@0.1.80": + version "0.1.80" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas/-/canvas-0.1.80.tgz#53615bea56fd94e07331ab13caa7a39efc4914ab" + integrity sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww== + optionalDependencies: + "@napi-rs/canvas-android-arm64" "0.1.80" + "@napi-rs/canvas-darwin-arm64" "0.1.80" + "@napi-rs/canvas-darwin-x64" "0.1.80" + "@napi-rs/canvas-linux-arm-gnueabihf" "0.1.80" + "@napi-rs/canvas-linux-arm64-gnu" "0.1.80" + "@napi-rs/canvas-linux-arm64-musl" "0.1.80" + "@napi-rs/canvas-linux-riscv64-gnu" "0.1.80" + "@napi-rs/canvas-linux-x64-gnu" "0.1.80" + "@napi-rs/canvas-linux-x64-musl" "0.1.80" + "@napi-rs/canvas-win32-x64-msvc" "0.1.80" + +"@napi-rs/canvas@^0.1.80", "@napi-rs/canvas@^0.1.81": + version "0.1.84" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas/-/canvas-0.1.84.tgz#514ba5518b9fc34c67b6569c8767036a1d6622be" + integrity sha512-88FTNFs4uuiFKP0tUrPsEXhpe9dg7za9ILZJE08pGdUveMIDeana1zwfVkqRHJDPJFAmGY3dXmJ99dzsy57YnA== + optionalDependencies: + "@napi-rs/canvas-android-arm64" "0.1.84" + "@napi-rs/canvas-darwin-arm64" "0.1.84" + "@napi-rs/canvas-darwin-x64" "0.1.84" + "@napi-rs/canvas-linux-arm-gnueabihf" "0.1.84" + "@napi-rs/canvas-linux-arm64-gnu" "0.1.84" + "@napi-rs/canvas-linux-arm64-musl" "0.1.84" + "@napi-rs/canvas-linux-riscv64-gnu" "0.1.84" + "@napi-rs/canvas-linux-x64-gnu" "0.1.84" + "@napi-rs/canvas-linux-x64-musl" "0.1.84" + "@napi-rs/canvas-win32-x64-msvc" "0.1.84" + "@next/env@16.0.7": version "16.0.7" resolved "https://registry.yarnpkg.com/@next/env/-/env-16.0.7.tgz#eda56377a865d890d25122257d2b8a85b81d6d3d" @@ -1114,14 +1076,6 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.1.tgz#7ca168b6937818e9a74b47ac4e2112b2e1a024cf" integrity sha512-S3Kq8e7LqxkA9s7HKLqXGTGck1uwis5vAXan3FnU5yw1Ec5hsSGnq4s/UCaSqABPOnOTg7zASLyst7+ohgWexg== -"@selderee/plugin-htmlparser2@^0.11.0": - version "0.11.0" - resolved "https://registry.yarnpkg.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz#d5b5e29a7ba6d3958a1972c7be16f4b2c188c517" - integrity sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ== - dependencies: - domhandler "^5.0.3" - selderee "^0.11.0" - "@swc/helpers@0.5.15", "@swc/helpers@^0.5.0": version "0.5.15" resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7" @@ -1151,6 +1105,11 @@ resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.10.9.tgz#55710c92b311fdaa8d8c66682a0dbdd684bc77c4" integrity sha512-kBknKOKzmeR7lN+vSadaKWXaLS0SZZG+oqpQ/k80Q6g9REn6zRHS/ZYdrIzHnpHgy/eWs00SujveUN/GJT2qTw== +"@tokenizer/token@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276" + integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A== + "@types/better-sqlite3@^7.6.12": version "7.6.12" resolved "https://registry.yarnpkg.com/@types/better-sqlite3/-/better-sqlite3-7.6.12.tgz#e5712d46d71097dcc2775c0b068072eadc15deb7" @@ -1158,11 +1117,6 @@ dependencies: "@types/node" "*" -"@types/html-to-text@^9.0.4": - version "9.0.4" - resolved "https://registry.yarnpkg.com/@types/html-to-text/-/html-to-text-9.0.4.tgz#4a83dd8ae8bfa91457d0b1ffc26f4d0537eff58c" - integrity sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ== - "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -1175,14 +1129,6 @@ dependencies: jspdf "*" -"@types/node-fetch@^2.6.4": - version "2.6.13" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.13.tgz#e0c9b7b5edbdb1b50ce32c127e85e880872d56ee" - integrity sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw== - dependencies: - "@types/node" "*" - form-data "^4.0.4" - "@types/node@*": version "20.12.5" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.5.tgz#74c4f31ab17955d0b5808cdc8fd2839526ad00b3" @@ -1197,13 +1143,6 @@ dependencies: undici-types "~6.20.0" -"@types/node@^18.11.18": - version "18.19.127" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.127.tgz#7c2e47fa79ad7486134700514d4a975c4607f09d" - integrity sha512-gSjxjrnKXML/yo0BO099uPixMqfpJU0TKYjpfLU7TrtA2WWDki412Np/RSTPRil1saKBhvVVKzVx/p/6p94nVA== - dependencies: - undici-types "~5.26.4" - "@types/node@^24.8.1": version "24.8.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-24.8.1.tgz#74c8ae00b045a0a351f2837ec00f25dfed0053be" @@ -1211,6 +1150,11 @@ dependencies: undici-types "~7.14.0" +"@types/pako@^2.0.3": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/pako/-/pako-2.0.4.tgz#c3575ef8125e176c345fa0e7b301c1db41170c15" + integrity sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw== + "@types/pdf-parse@^1.1.4": version "1.1.4" resolved "https://registry.yarnpkg.com/@types/pdf-parse/-/pdf-parse-1.1.4.tgz#21a539efd2f16009d08aeed3350133948b5d7ed1" @@ -1241,16 +1185,6 @@ "@types/prop-types" "*" csstype "^3.0.2" -"@types/retry@0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" - integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== - -"@types/triple-beam@^1.3.2": - version "1.3.5" - resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c" - integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw== - "@types/trusted-types@^2.0.7": version "2.0.7" resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" @@ -1261,11 +1195,6 @@ resolved "https://registry.yarnpkg.com/@types/turndown/-/turndown-5.0.6.tgz#42a27397298a312d6088f29c0ff4819c518c1ecb" integrity sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg== -"@types/uuid@^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" - integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ== - "@typescript-eslint/parser@^5.4.2 || ^6.0.0": version "6.21.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" @@ -1317,6 +1246,11 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== +"@xmldom/xmldom@^0.8.10": + version "0.8.11" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.11.tgz#b79de2d67389734c57c52595f7a7305e30c2d608" + integrity sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw== + "@xmldom/xmldom@^0.8.6": version "0.8.10" resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99" @@ -1339,13 +1273,6 @@ acorn@^8.9.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== -agentkeepalive@^4.2.1: - version "4.6.0" - resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a" - integrity sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ== - dependencies: - humanize-ms "^1.2.1" - ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -1373,11 +1300,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" - integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== - ansi-styles@^6.1.0: version "6.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" @@ -1529,21 +1451,11 @@ ast-types-flow@^0.0.8: resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== -async@^3.2.3: - version "3.2.6" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" - integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - autoprefixer@^10.0.1: version "10.4.19" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f" @@ -1607,7 +1519,7 @@ better-sqlite3@^11.9.1: bindings "^1.5.0" prebuild-install "^7.1.1" -binary-extensions@^2.0.0, binary-extensions@^2.2.0: +binary-extensions@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== @@ -1670,10 +1582,10 @@ browserslist@^4.23.0: node-releases "^2.0.14" update-browserslist-db "^1.0.13" -btoa@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" - integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== buffer-from@^1.0.0: version "1.1.2" @@ -1688,13 +1600,13 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" -call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" - integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" + base64-js "^1.3.1" + ieee754 "^1.2.1" call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: version "1.0.7" @@ -1717,11 +1629,6 @@ camelcase-css@^2.0.1: resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== -camelcase@6: - version "6.3.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" - integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== - caniuse-lite@^1.0.30001579: version "1.0.30001705" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001705.tgz#dc3510bcdef261444ca944b7be9c8d0bb7fafeef" @@ -1746,7 +1653,7 @@ canvg@^3.0.11: stackblur-canvas "^2.0.0" svg-pathdata "^6.0.3" -chalk@^4.0.0, chalk@^4.1.2: +chalk@^4.0.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1794,13 +1701,6 @@ clsx@^2.1.0: resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb" integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg== -color-convert@^1.9.3: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -1808,40 +1708,11 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - -color-name@^1.0.0, color-name@~1.1.4: +color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.6.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" - integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color@^3.1.3: - version "3.2.1" - resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" - integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== - dependencies: - color-convert "^1.9.3" - color-string "^1.6.0" - -colorspace@1.1.x: - version "1.1.4" - resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243" - integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== - dependencies: - color "^3.1.3" - text-hex "1.0.x" - combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -1864,17 +1735,20 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -console-table-printer@^2.12.1: - version "2.14.6" - resolved "https://registry.yarnpkg.com/console-table-printer/-/console-table-printer-2.14.6.tgz#edfe0bf311fa2701922ed509443145ab51e06436" - integrity sha512-MCBl5HNVaFuuHW6FGbL/4fB7N/ormCy+tQ+sxTrF6QtSbSNETvPuOVbkJBhzDgYhvjWGrTma4eYJa37ZuoQsPw== +concat-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== dependencies: - simple-wcswidth "^1.0.1" + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.0.2" + typedarray "^0.0.6" core-js@^3.6.0, core-js@^3.8.3: - version "3.42.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.42.0.tgz#edbe91f78ac8cfb6df8d997e74d368a68082fe37" - integrity sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g== + version "3.47.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.47.0.tgz#436ef07650e191afeb84c24481b298bd60eb4a17" + integrity sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg== core-util-is@~1.0.0: version "1.0.3" @@ -1939,7 +1813,7 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" -debug@^3.1.0, debug@^3.2.7: +debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -1953,11 +1827,6 @@ debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: dependencies: ms "2.1.2" -decamelize@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== - decimal.js@^10.4.3: version "10.6.0" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a" @@ -1980,11 +1849,6 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -deepmerge@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" - integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== - define-data-property@^1.0.1, define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" @@ -2064,43 +1928,13 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-serializer@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" - integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.2" - entities "^4.2.0" - -domelementtype@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" - integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== - -domhandler@^5.0.2, domhandler@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" - integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== - dependencies: - domelementtype "^2.3.0" - dompurify@^3.2.4: - version "3.2.6" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.6.tgz#ca040a6ad2b88e2a92dc45f38c79f84a714a1cad" - integrity sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ== + version "3.3.1" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.3.1.tgz#c7e1ddebfe3301eacd6c0c12a4af284936dbbb86" + integrity sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q== optionalDependencies: "@types/trusted-types" "^2.0.7" -domutils@^3.0.1: - version "3.2.2" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" - integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== - dependencies: - dom-serializer "^2.0.0" - domelementtype "^2.3.0" - domhandler "^5.0.3" - drizzle-kit@^0.30.5: version "0.30.5" resolved "https://registry.yarnpkg.com/drizzle-kit/-/drizzle-kit-0.30.5.tgz#fce34a8ca9f82da5212f31c266e407c78523166f" @@ -2124,15 +1958,6 @@ duck@^0.1.12: dependencies: underscore "^1.13.1" -dunder-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" - integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== - dependencies: - call-bind-apply-helpers "^1.0.1" - es-errors "^1.3.0" - gopd "^1.2.0" - eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -2153,11 +1978,6 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== -enabled@2.0.x: - version "2.0.0" - resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" - integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== - end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -2173,11 +1993,6 @@ enhanced-resolve@^5.12.0: graceful-fs "^4.2.4" tapable "^2.2.0" -entities@^4.2.0, entities@^4.4.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" - integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== - env-paths@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-3.0.0.tgz#2f1e89c2f6dbd3408e1b1711dd82d62e317f58da" @@ -2242,11 +2057,6 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" -es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" @@ -2279,13 +2089,6 @@ es-object-atoms@^1.0.0: dependencies: es-errors "^1.3.0" -es-object-atoms@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" - integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== - dependencies: - es-errors "^1.3.0" - es-set-tostringtag@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" @@ -2295,16 +2098,6 @@ es-set-tostringtag@^2.0.3: has-tostringtag "^1.0.2" hasown "^2.0.1" -es-set-tostringtag@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" - integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== - dependencies: - es-errors "^1.3.0" - get-intrinsic "^1.2.6" - has-tostringtag "^1.0.2" - hasown "^2.0.2" - es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" @@ -2623,10 +2416,10 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== -eventemitter3@^4.0.4: - version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== expand-template@^2.0.3: version "2.0.3" @@ -2664,6 +2457,15 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-png@^6.2.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/fast-png/-/fast-png-6.4.0.tgz#807fc353ccab060d09151b7d082786e02d8e92d6" + integrity sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q== + dependencies: + "@types/pako" "^2.0.3" + iobuffer "^5.3.2" + pako "^2.1.0" + fastq@^1.6.0: version "1.17.1" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" @@ -2671,11 +2473,6 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -fecha@^4.2.0: - version "4.2.3" - resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" - integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== - "fetch-mock-cache@npm:fetch-mock-cache@^2.1.3": version "2.3.1" resolved "https://registry.yarnpkg.com/fetch-mock-cache/-/fetch-mock-cache-2.3.1.tgz#1018f5fc2f91cf2511abcea8a5e3a3b05e2d02bf" @@ -2696,6 +2493,15 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +file-type@^16.5.4: + version "16.5.4" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-16.5.4.tgz#474fb4f704bee427681f98dd390058a172a6c2fd" + integrity sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw== + dependencies: + readable-web-to-node-stream "^3.0.0" + strtok3 "^6.2.4" + token-types "^4.1.1" + file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -2747,11 +2553,6 @@ flat-cache@^3.0.4: keyv "^4.5.3" rimraf "^3.0.2" -flat@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" - integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== - flatbuffers@^25.1.24: version "25.9.23" resolved "https://registry.yarnpkg.com/flatbuffers/-/flatbuffers-25.9.23.tgz#346811557fe9312ab5647535e793c761e9c81eb1" @@ -2762,11 +2563,6 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== -fn.name@1.x.x: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" - integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== - follow-redirects@^1.15.6: version "1.15.9" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" @@ -2787,11 +2583,6 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" -form-data-encoder@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040" - integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A== - form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -2801,25 +2592,6 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4" - integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - es-set-tostringtag "^2.1.0" - hasown "^2.0.2" - mime-types "^2.1.12" - -formdata-node@^4.3.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/formdata-node/-/formdata-node-4.4.1.tgz#23f6a5cb9cb55315912cbec4ff7b0f59bbd191e2" - integrity sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ== - dependencies: - node-domexception "1.0.0" - web-streams-polyfill "4.0.0-beta.3" - fraction.js@^4.3.7: version "4.3.7" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" @@ -2897,30 +2669,6 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" -get-intrinsic@^1.2.6: - version "1.3.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" - integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== - dependencies: - call-bind-apply-helpers "^1.0.2" - es-define-property "^1.0.1" - es-errors "^1.3.0" - es-object-atoms "^1.1.1" - function-bind "^1.1.2" - get-proto "^1.0.1" - gopd "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - math-intrinsics "^1.1.0" - -get-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" - integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== - dependencies: - dunder-proto "^1.0.1" - es-object-atoms "^1.0.0" - get-symbol-description@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" @@ -3050,11 +2798,6 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - graceful-fs@^4.2.4: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -3065,36 +2808,11 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== -groq-sdk@^0.19.0: - version "0.19.0" - resolved "https://registry.yarnpkg.com/groq-sdk/-/groq-sdk-0.19.0.tgz#564ce018172dc3e2e2793398e0227a035a357d09" - integrity sha512-vdh5h7ORvwvOvutA80dKF81b0gPWHxu6K/GOJBOM0n6p6CSqAVLhFfeS79Ef0j/yCycDR09jqY7jkYz9dLiS6w== - dependencies: - "@types/node" "^18.11.18" - "@types/node-fetch" "^2.6.4" - abort-controller "^3.0.0" - agentkeepalive "^4.2.1" - form-data-encoder "1.7.2" - formdata-node "^4.3.2" - node-fetch "^2.6.7" - guid-typescript@^1.0.9: version "1.0.9" resolved "https://registry.yarnpkg.com/guid-typescript/-/guid-typescript-1.0.9.tgz#e35f77003535b0297ea08548f5ace6adb1480ddc" integrity sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ== -handlebars@^4.7.8: - version "4.7.8" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9" - integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ== - dependencies: - minimist "^1.2.5" - neo-async "^2.6.2" - source-map "^0.6.1" - wordwrap "^1.0.0" - optionalDependencies: - uglify-js "^3.1.4" - has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -3122,11 +2840,6 @@ has-symbols@^1.0.2, has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== -has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" @@ -3141,17 +2854,6 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: dependencies: function-bind "^1.1.2" -html-to-text@^9.0.5: - version "9.0.5" - resolved "https://registry.yarnpkg.com/html-to-text/-/html-to-text-9.0.5.tgz#6149a0f618ae7a0db8085dca9bbf96d32bb8368d" - integrity sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg== - dependencies: - "@selderee/plugin-htmlparser2" "^0.11.0" - deepmerge "^4.3.1" - dom-serializer "^2.0.0" - htmlparser2 "^8.0.2" - selderee "^0.11.0" - html2canvas@^1.0.0-rc.5: version "1.4.1" resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" @@ -3160,23 +2862,6 @@ html2canvas@^1.0.0-rc.5: css-line-break "^2.1.0" text-segmentation "^1.0.3" -htmlparser2@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" - integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.3" - domutils "^3.0.1" - entities "^4.4.0" - -humanize-ms@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" - integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== - dependencies: - ms "^2.0.0" - humanize-url@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/humanize-url/-/humanize-url-2.1.1.tgz#1be3dc2b8a23ee28fdf9db95b22962b3eb5e4683" @@ -3184,7 +2869,7 @@ humanize-url@^2.1.1: dependencies: normalize-url "^4.5.1" -ieee754@^1.1.13: +ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -3239,6 +2924,11 @@ internal-slot@^1.0.7: hasown "^2.0.0" side-channel "^1.0.4" +iobuffer@^5.3.2: + version "5.4.0" + resolved "https://registry.yarnpkg.com/iobuffer/-/iobuffer-5.4.0.tgz#f85dff957fd0579257472f0a4cfe5ed3430e63e1" + integrity sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA== + is-array-buffer@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" @@ -3247,11 +2937,6 @@ is-array-buffer@^3.0.4: call-bind "^1.0.2" get-intrinsic "^1.2.1" -is-arrayish@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" - integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== - is-async-function@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" @@ -3385,11 +3070,6 @@ is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: dependencies: call-bind "^1.0.7" -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" @@ -3481,10 +3161,10 @@ jiti@^1.21.0: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== -js-tiktoken@^1.0.12: - version "1.0.20" - resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.20.tgz#fa2733bf147acaf1bdcf9ab8a878e79c581c95f2" - integrity sha512-Xlaqhhs8VfCd6Sh7a1cFkZHQbYTLCwVJJWiHVxBYzLPxW0XsoxBy1hitmjkdIjD3Aon5BXLHFwU5O8WUx6HH+A== +js-tiktoken@^1.0.21: + version "1.0.21" + resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.21.tgz#368a9957591a30a62997dd0c4cf30866f00f8221" + integrity sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g== dependencies: base64-js "^1.5.1" @@ -3505,14 +3185,6 @@ json-buffer@3.0.1: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== -json-schema-to-ts@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz#81f3acaf5a34736492f6f5f51870ef9ece1ca853" - integrity sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g== - dependencies: - "@babel/runtime" "^7.18.3" - ts-algebra "^2.0.0" - json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -3540,19 +3212,13 @@ json5@^1.0.2: dependencies: minimist "^1.2.0" -jsonpointer@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" - integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== - -jspdf@*, jspdf@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/jspdf/-/jspdf-3.0.1.tgz#d81e1964f354f60412516eb2449ea2cccd4d2a3b" - integrity sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg== +jspdf@*, jspdf@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/jspdf/-/jspdf-3.0.4.tgz#f9ad24751eaf3c8a758eccab6f621d723d4b32b6" + integrity sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ== dependencies: - "@babel/runtime" "^7.26.7" - atob "^2.1.2" - btoa "^1.2.1" + "@babel/runtime" "^7.28.4" + fast-png "^6.2.0" fflate "^0.8.1" optionalDependencies: canvg "^3.0.11" @@ -3587,48 +3253,6 @@ keyv@^4.5.3: dependencies: json-buffer "3.0.1" -kuler@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" - integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== - -langchain@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/langchain/-/langchain-1.0.4.tgz#c4fa22d927f41d56c356ecfccea5c08ae7b682ef" - integrity sha512-g7z2kKvnXOecybbVGHfI2ZmdmP309mxC1FYlq6WC/7RsKgX5MwY9gBjwK16mpKOaozOD9QCo1Ia7o2UcUBRb9Q== - dependencies: - "@langchain/langgraph" "^1.0.0" - "@langchain/langgraph-checkpoint" "^1.0.0" - langsmith "~0.3.74" - uuid "^10.0.0" - zod "^3.25.76 || ^4" - -langsmith@^0.3.64: - version "0.3.74" - resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.3.74.tgz#014d31a9ff7530b54f0d797502abd512ce8fb6fb" - integrity sha512-ZuW3Qawz8w88XcuCRH91yTp6lsdGuwzRqZ5J0Hf5q/AjMz7DwcSv0MkE6V5W+8hFMI850QZN2Wlxwm3R9lHlZg== - dependencies: - "@types/uuid" "^10.0.0" - chalk "^4.1.2" - console-table-printer "^2.12.1" - p-queue "^6.6.2" - p-retry "4" - semver "^7.6.3" - uuid "^10.0.0" - -langsmith@~0.3.74: - version "0.3.79" - resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.3.79.tgz#6c845644da26e7fdd8e9b80706091669fc43bda4" - integrity sha512-j5uiAsyy90zxlxaMuGjb7EdcL51Yx61SpKfDOI1nMPBbemGju+lf47he4e59Hp5K63CY8XWgFP42WeZ+zuIU4Q== - dependencies: - "@types/uuid" "^10.0.0" - chalk "^4.1.2" - console-table-printer "^2.12.1" - p-queue "^6.6.2" - p-retry "4" - semver "^7.6.3" - uuid "^10.0.0" - language-subtag-registry@^0.3.20: version "0.3.22" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" @@ -3641,11 +3265,6 @@ language-tags@^1.0.9: dependencies: language-subtag-registry "^0.3.20" -leac@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/leac/-/leac-0.6.0.tgz#dcf136e382e666bd2475f44a1096061b70dc0912" - integrity sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg== - levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -3705,18 +3324,6 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -logform@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/logform/-/logform-2.7.0.tgz#cfca97528ef290f2e125a08396805002b2d060d1" - integrity sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ== - dependencies: - "@colors/colors" "1.6.0" - "@types/triple-beam" "^1.3.2" - fecha "^4.2.0" - ms "^2.1.1" - safe-stable-stringify "^2.3.1" - triple-beam "^1.3.0" - long@^5.0.0, long@^5.2.3: version "5.3.2" resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" @@ -3783,16 +3390,6 @@ matcher@^3.0.0: dependencies: escape-string-regexp "^4.0.0" -math-expression-evaluator@^2.0.0: - version "2.0.7" - resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-2.0.7.tgz#dc99a80ce2bf7f9b7df878126feb5c506c1fdf5f" - integrity sha512-uwliJZ6BPHRq4eiqNWxZBDzKUiS5RIynFFcgchqhBOloVLVBpZpNG8jRYkedLcBvhph8TnRyWEuxPqiQcwIdog== - -math-intrinsics@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" - integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== - mathjs@^15.1.0: version "15.1.0" resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-15.1.0.tgz#e910f626c5d66ff1902eb69c3b2e7b7d010fc39e" @@ -3859,7 +3456,7 @@ minimatch@^9.0.1: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -3903,16 +3500,11 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.0.0, ms@^2.1.1: +ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -mustache@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64" - integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== - mz@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" @@ -3942,11 +3534,6 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -neo-async@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - next-themes@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.3.0.tgz#b4d2a866137a67d42564b07f3a3e720e2ff3871a" @@ -3980,23 +3567,11 @@ node-abi@^3.3.0: dependencies: semver "^7.3.5" -node-domexception@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" - integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== - node-ensure@^0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7" integrity sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw== -node-fetch@^2.6.7: - version "2.7.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" - integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== - dependencies: - whatwg-url "^5.0.0" - node-releases@^2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" @@ -4093,12 +3668,17 @@ object.values@^1.1.6, object.values@^1.1.7: define-properties "^1.2.1" es-object-atoms "^1.0.0" -ollama@^0.5.12: - version "0.5.16" - resolved "https://registry.yarnpkg.com/ollama/-/ollama-0.5.16.tgz#cb695b4aab6f6c07236a04b3aee40160f4f9e892" - integrity sha512-OEbxxOIUZtdZgOaTPAULo051F5y+Z1vosxEYOoABPnQKeW7i4O8tJNlxCB+xioyoorVqgjkdj+TA1f1Hy2ug/w== +officeparser@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/officeparser/-/officeparser-5.2.2.tgz#75efff96424f97787af0d3fa0bafe2b63832a418" + integrity sha512-5JrV1CZFqTv/27fXy2bcf+3g6BpDZiJ3XoSRW3fb2i2EFex0DduqjTxiU2RsJ08WBsk4Hp0nZoGi9ZtHMZFaPA== dependencies: - whatwg-fetch "^3.6.20" + "@xmldom/xmldom" "^0.8.10" + concat-stream "^2.0.0" + file-type "^16.5.4" + node-ensure "^0.0.0" + pdfjs-dist "^5.3.31" + yauzl "^3.1.3" ollama@^0.6.3: version "0.6.3" @@ -4114,13 +3694,6 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -one-time@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" - integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== - dependencies: - fn.name "1.x.x" - onnxruntime-common@1.21.0: version "1.21.0" resolved "https://registry.yarnpkg.com/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz#a81d4191d418acbbff2546a954cc2cc23eeb09f8" @@ -4157,11 +3730,6 @@ openai@^6.9.0: resolved "https://registry.yarnpkg.com/openai/-/openai-6.9.0.tgz#acd15b2233c42b165981f3de8f4cfce27f844fce" integrity sha512-n2sJRYmM+xfJ0l3OfH8eNnIyv3nQY7L08gZQu3dw6wSdfPtKAk92L83M2NIP5SS8Cl/bsBBG3yKzEOjkx0O+7A== -openapi-types@^12.1.3: - version "12.1.3" - resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3" - integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw== - option@~0.2.1: version "0.2.4" resolved "https://registry.yarnpkg.com/option/-/option-0.2.4.tgz#fd475cdf98dcabb3cb397a3ba5284feb45edbfe4" @@ -4179,11 +3747,6 @@ optionator@^0.9.3: prelude-ls "^1.2.1" type-check "^0.4.0" -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== - p-limit@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" @@ -4198,28 +3761,10 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-queue@^6.6.2: - version "6.6.2" - resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" - integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== - dependencies: - eventemitter3 "^4.0.4" - p-timeout "^3.2.0" - -p-retry@4: - version "4.6.2" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" - integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== - dependencies: - "@types/retry" "0.12.0" - retry "^0.13.1" - -p-timeout@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" - integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== - dependencies: - p-finally "^1.0.0" +pako@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== pako@~1.0.2: version "1.0.11" @@ -4233,14 +3778,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parseley@^0.12.0: - version "0.12.1" - resolved "https://registry.yarnpkg.com/parseley/-/parseley-0.12.1.tgz#4afd561d50215ebe259e3e7a853e62f600683aef" - integrity sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw== - dependencies: - leac "^0.6.0" - peberminta "^0.9.0" - partial-json@^0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/partial-json/-/partial-json-0.1.7.tgz#b735a89edb3e25f231a3c4caeaae71dc9f578605" @@ -4279,18 +3816,37 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pdf-parse@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/pdf-parse/-/pdf-parse-1.1.1.tgz#745e07408679548b3995ff896fd38e96e19d14a7" - integrity sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A== +pdf-parse@^2.4.5: + version "2.4.5" + resolved "https://registry.yarnpkg.com/pdf-parse/-/pdf-parse-2.4.5.tgz#fcbf9774d985a7f573e899c22618ab53a508a9f3" + integrity sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg== dependencies: - debug "^3.1.0" - node-ensure "^0.0.0" + "@napi-rs/canvas" "0.1.80" + pdfjs-dist "5.4.296" -peberminta@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/peberminta/-/peberminta-0.9.0.tgz#8ec9bc0eb84b7d368126e71ce9033501dca2a352" - integrity sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ== +pdfjs-dist@5.4.296: + version "5.4.296" + resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz#b1aa7ded8828f29537bc7cc99c1343c8b3a5d2d6" + integrity sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q== + optionalDependencies: + "@napi-rs/canvas" "^0.1.80" + +pdfjs-dist@^5.3.31: + version "5.4.449" + resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-5.4.449.tgz#acc2b4fafd595981280d0314e151445eb9f9c00b" + integrity sha512-CegnUaT0QwAyQMS+7o2POr4wWUNNe8VaKKlcuoRHeYo98cVnqPpwOXNSx6Trl6szH02JrRcsPgletV6GmF3LtQ== + optionalDependencies: + "@napi-rs/canvas" "^0.1.81" + +peek-readable@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.1.0.tgz#4ece1111bf5c2ad8867c314c81356847e8a62e72" + integrity sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg== + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== performance-now@^2.1.0: version "2.1.0" @@ -4430,6 +3986,11 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" @@ -4550,7 +4111,7 @@ read-cache@^1.0.0: dependencies: pify "^2.3.0" -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.2: +readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -4559,6 +4120,17 @@ readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.2: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.7.0.tgz#cedbd8a1146c13dfff8dab14068028d58c15ac91" + integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" @@ -4572,6 +4144,13 @@ readable-stream@~2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-web-to-node-stream@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz#392ba37707af5bf62d725c36c1b5d6ef4119eefc" + integrity sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw== + dependencies: + readable-stream "^4.7.0" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -4645,11 +4224,6 @@ resolve@^2.0.0-next.5: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -retry@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" - integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== - reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -4720,11 +4294,6 @@ safe-regex-test@^1.0.3: es-errors "^1.3.0" is-regex "^1.1.4" -safe-stable-stringify@^2.3.1: - version "2.5.0" - resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" - integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== - scheduler@^0.23.0: version "0.23.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" @@ -4737,13 +4306,6 @@ seedrandom@^3.0.5: resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== -selderee@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/selderee/-/selderee-0.11.0.tgz#6af0c7983e073ad3e35787ffe20cefd9daf0ec8a" - integrity sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA== - dependencies: - parseley "^0.12.0" - semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" @@ -4759,7 +4321,7 @@ semver@^7.3.2, semver@^7.7.2, semver@^7.7.3: resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== -semver@^7.3.5, semver@^7.6.2, semver@^7.6.3: +semver@^7.3.5, semver@^7.6.2: version "7.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== @@ -4917,18 +4479,6 @@ simple-get@^4.0.0: once "^1.3.1" simple-concat "^1.0.0" -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" - integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== - dependencies: - is-arrayish "^0.3.1" - -simple-wcswidth@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz#66722f37629d5203f9b47c5477b1225b85d6525b" - integrity sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw== - slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -4957,7 +4507,7 @@ source-map-support@^0.5.21: buffer-from "^1.0.0" source-map "^0.6.0" -source-map@^0.6.0, source-map@^0.6.1: +source-map@^0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -4972,11 +4522,6 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -stack-trace@0.0.x: - version "0.0.10" - resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" - integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== - stackblur-canvas@^2.0.0: version "2.7.0" resolved "https://registry.yarnpkg.com/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz#af931277d0b5096df55e1f91c530043e066989b6" @@ -5055,7 +4600,7 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -string_decoder@^1.1.1: +string_decoder@^1.1.1, string_decoder@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -5112,6 +4657,14 @@ strip-outer@^1.0.1: dependencies: escape-string-regexp "^1.0.2" +strtok3@^6.2.4: + version "6.3.0" + resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-6.3.0.tgz#358b80ffe6d5d5620e19a073aa78ce947a90f9a0" + integrity sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw== + dependencies: + "@tokenizer/token" "^0.3.0" + peek-readable "^4.1.0" + styled-jsx@5.1.6: version "5.1.6" resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.6.tgz#83b90c077e6c6a80f7f5e8781d0f311b2fe41499" @@ -5226,11 +4779,6 @@ tar@^7.0.1: minizlib "^3.1.0" yallist "^5.0.0" -text-hex@1.0.x: - version "1.0.0" - resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" - integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== - text-segmentation@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943" @@ -5281,6 +4829,14 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +token-types@^4.1.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/token-types/-/token-types-4.2.1.tgz#0f897f03665846982806e138977dbe72d44df753" + integrity sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ== + dependencies: + "@tokenizer/token" "^0.3.0" + ieee754 "^1.2.1" + "tough-cookie-file-store@npm:tough-cookie-file-store@^2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/tough-cookie-file-store/-/tough-cookie-file-store-2.0.3.tgz#788f7a6fe5cd8f61a1afb71b2f0b964ebf914b80" @@ -5305,11 +4861,6 @@ tough-cookie@^4.0.0: dependencies: tldts "^6.1.32" -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - trim-repeated@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" @@ -5317,16 +4868,6 @@ trim-repeated@^1.0.0: dependencies: escape-string-regexp "^1.0.2" -triple-beam@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984" - integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg== - -ts-algebra@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ts-algebra/-/ts-algebra-2.0.0.tgz#4e3e0953878f26518fce7f6bb115064a65388b7a" - integrity sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw== - ts-api-utils@^1.0.1: version "1.3.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" @@ -5432,16 +4973,16 @@ typed-function@^4.2.1: resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-4.2.1.tgz#19aa51847aa2dea9ef5e7fb7641c060179a74426" integrity sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA== +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + typescript@^5.9.3: version "5.9.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== -uglify-js@^3.1.4: - version "3.19.3" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" - integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== - unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" @@ -5529,44 +5070,11 @@ utrie@^1.0.2: dependencies: base64-arraybuffer "^1.0.2" -uuid@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" - integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== - -uuid@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" - integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== - -uuid@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== - -web-streams-polyfill@4.0.0-beta.3: - version "4.0.0-beta.3" - resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz#2898486b74f5156095e473efe989dcf185047a38" - integrity sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug== - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - whatwg-fetch@^3.6.20: version "3.6.20" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" @@ -5631,37 +5139,6 @@ which@^4.0.0: dependencies: isexe "^3.1.1" -winston-transport@^4.9.0: - version "4.9.0" - resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.9.0.tgz#3bba345de10297654ea6f33519424560003b3bf9" - integrity sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A== - dependencies: - logform "^2.7.0" - readable-stream "^3.6.2" - triple-beam "^1.3.0" - -winston@^3.17.0: - version "3.17.0" - resolved "https://registry.yarnpkg.com/winston/-/winston-3.17.0.tgz#74b8665ce9b4ea7b29d0922cfccf852a08a11423" - integrity sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw== - dependencies: - "@colors/colors" "^1.6.0" - "@dabh/diagnostics" "^2.0.2" - async "^3.2.3" - is-stream "^2.0.0" - logform "^2.7.0" - one-time "^1.0.0" - readable-stream "^3.4.0" - safe-stable-stringify "^2.3.1" - stack-trace "0.0.x" - triple-beam "^1.3.0" - winston-transport "^4.9.0" - -wordwrap@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" - integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== - "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -5711,16 +5188,19 @@ yallist@^5.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-5.0.0.tgz#00e2de443639ed0d78fd87de0d27469fbcffb533" integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw== -yaml@^2.2.1: - version "2.8.0" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.0.tgz#15f8c9866211bdc2d3781a0890e44d4fa1a5fff6" - integrity sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ== - yaml@^2.3.4: version "2.4.1" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.1.tgz#2e57e0b5e995292c25c75d2658f0664765210eed" integrity sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg== +yauzl@^3.1.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-3.2.0.tgz#7b6cb548f09a48a6177ea0be8ece48deb7da45c0" + integrity sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w== + dependencies: + buffer-crc32 "~0.2.3" + pend "~1.2.0" + yet-another-react-lightbox@^3.17.2: version "3.17.2" resolved "https://registry.yarnpkg.com/yet-another-react-lightbox/-/yet-another-react-lightbox-3.17.2.tgz#00474b83189ec4d81302792211ca31ffb808554c" @@ -5731,7 +5211,7 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -"zod@^3.25.76 || ^4", zod@^4.1.12: +zod@^4.1.12: version "4.1.12" resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.12.tgz#64f1ea53d00eab91853195653b5af9eee68970f0" integrity sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ== From ff4cf98b50fc681c0dd98892b75c9057c2b70292 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:15:29 +0530 Subject: [PATCH 108/196] feat(media-search): fix type errors --- src/lib/agents/media/image.ts | 2 +- src/lib/agents/media/video.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/agents/media/image.ts b/src/lib/agents/media/image.ts index f146824..b04d532 100644 --- a/src/lib/agents/media/image.ts +++ b/src/lib/agents/media/image.ts @@ -29,7 +29,7 @@ const searchImages = async ( query: z.string().describe('The image search query.'), }); - const res = await llm.generateObject<z.infer<typeof schema>>({ + const res = await llm.generateObject<typeof schema>({ messages: [ { role: 'system', diff --git a/src/lib/agents/media/video.ts b/src/lib/agents/media/video.ts index feac720..c8f19b6 100644 --- a/src/lib/agents/media/video.ts +++ b/src/lib/agents/media/video.ts @@ -28,7 +28,7 @@ const searchVideos = async ( query: z.string().describe('The video search query.'), }); - const res = await llm.generateObject<z.infer<typeof schema>>({ + const res = await llm.generateObject<typeof schema>({ messages: [ { role: 'system', From 8d1b04e05f0d8a4e6c1392542c4a943eee38d107 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:15:47 +0530 Subject: [PATCH 109/196] feat(search-agent): use `index + 1` to fix zero errors --- src/lib/agents/search/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/agents/search/index.ts b/src/lib/agents/search/index.ts index 91bed9a..26fc13d 100644 --- a/src/lib/agents/search/index.ts +++ b/src/lib/agents/search/index.ts @@ -58,7 +58,7 @@ class SearchAgent { searchResults?.searchFindings .map( (f, index) => - `<result index=${index} title=${f.metadata.title}>${f.content}</result>`, + `<result index=${index + 1} title=${f.metadata.title}>${f.content}</result>`, ) .join('\n') || ''; From 0b9e193ed1198a413b258265658fac5279812f68 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:16:21 +0530 Subject: [PATCH 110/196] feat(actions): rename plan to reasoning --- src/lib/agents/search/researcher/actions/done.ts | 4 ++-- src/lib/agents/search/researcher/actions/plan.ts | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/lib/agents/search/researcher/actions/done.ts b/src/lib/agents/search/researcher/actions/done.ts index b19df4f..3c3e5f3 100644 --- a/src/lib/agents/search/researcher/actions/done.ts +++ b/src/lib/agents/search/researcher/actions/done.ts @@ -5,13 +5,13 @@ const actionDescription = ` Use this action ONLY when you have completed all necessary research and are ready to provide a final answer to the user. This indicates that you have gathered sufficient information from previous steps and are concluding the research process. YOU MUST CALL THIS ACTION TO SIGNAL COMPLETION; DO NOT OUTPUT FINAL ANSWERS DIRECTLY TO THE USER. IT WILL BE AUTOMATICALLY TRIGGERED IF MAXIMUM ITERATIONS ARE REACHED SO IF YOU'RE LOW ON ITERATIONS, DON'T CALL IT AND INSTEAD FOCUS ON GATHERING ESSENTIAL INFO FIRST. -` +`; const doneAction: ResearchAction<any> = { name: 'done', schema: z.object({}), getToolDescription: () => - 'Only call this after ___plan AND after any other needed tool calls when you truly have enough to answer. Do not call if information is still missing.', + 'Only call this after 0_reasoning AND after any other needed tool calls when you truly have enough to answer. Do not call if information is still missing.', getDescription: () => actionDescription, enabled: (_) => true, execute: async (params, additionalConfig) => { diff --git a/src/lib/agents/search/researcher/actions/plan.ts b/src/lib/agents/search/researcher/actions/plan.ts index dcdd05b..05f2e13 100644 --- a/src/lib/agents/search/researcher/actions/plan.ts +++ b/src/lib/agents/search/researcher/actions/plan.ts @@ -10,7 +10,7 @@ const schema = z.object({ }); const actionDescription = ` -Use thi tool FIRST on every turn to state your plan in natural language before any other action. Keep it short, action-focused, and tailored to the current query. +Use this tool FIRST on every turn to state your plan in natural language before any other action. Keep it short, action-focused, and tailored to the current query. Make sure to not include reference to any tools or actions you might take, just the plan itself. The user isn't aware about tools, but they love to see your thought process. Here are some examples of good plans: @@ -18,12 +18,15 @@ Here are some examples of good plans: - "Okay, the user wants to know the latest advancements in renewable energy. I will start by looking for recent articles and studies on this topic, then summarize the key points." -> "I have gathered enough information to provide a comprehensive answer." - "The user is asking about the health benefits of a Mediterranean diet. I will search for scientific studies and expert opinions on this diet, then compile the findings into a clear summary." -> "I have gathered information about the Mediterranean diet and its health benefits, I will now look up for any recent studies to ensure the information is current." <examples> -` + +YOU CAN NEVER CALL ANY OTHER TOOL BEFORE CALLING THIS ONE FIRST, IF YOU DO, THAT CALL WOULD BE IGNORED. +`; const planAction: ResearchAction<typeof schema> = { - name: '___plan', + name: '0_reasoning', schema: schema, - getToolDescription: () => 'Use this FIRST on every turn to state your plan in natural language before any other action. Keep it short, action-focused, and tailored to the current query.', + getToolDescription: () => + 'Use this FIRST on every turn to state your plan in natural language before any other action. Keep it short, action-focused, and tailored to the current query.', getDescription: () => actionDescription, enabled: (config) => config.mode !== 'speed', execute: async (input, _) => { From 0688630863e96619932c8e391c159fe2df2a29e9 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:17:02 +0530 Subject: [PATCH 111/196] feat(actions): update web search action to use reasoning --- .../search/researcher/actions/webSearch.ts | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/lib/agents/search/researcher/actions/webSearch.ts b/src/lib/agents/search/researcher/actions/webSearch.ts index 04e9702..5eb3af4 100644 --- a/src/lib/agents/search/researcher/actions/webSearch.ts +++ b/src/lib/agents/search/researcher/actions/webSearch.ts @@ -21,7 +21,7 @@ For example, if the user is asking about the features of a new technology, you m You can search for 3 queries in one go, make sure to utilize all 3 queries to maximize the information you can gather. If a question is simple, then split your queries to cover different aspects or related topics to get a comprehensive understanding. If this tool is present and no other tools are more relevant, you MUST use this tool to get the needed information. -` +`; const balancedModePrompt = ` Use this tool to perform web searches based on the provided queries. This is useful when you need to gather information from the web to answer the user's questions. You can provide up to 3 queries at a time. You will have to use this every single time if this is present and relevant. @@ -32,16 +32,16 @@ Start initially with broader queries to get an overview, then narrow down with m Your queries shouldn't be sentences but rather keywords that are SEO friendly and can be used to search the web for information. For example if the user is asking about Tesla, your actions should be like: -1. __plan "The user is asking about Tesla. I will start with broader queries to get an overview of Tesla, then narrow down with more specific queries based on the results I receive." then +1. 0_reasoning "The user is asking about Tesla. I will start with broader queries to get an overview of Tesla, then narrow down with more specific queries based on the results I receive." then 2. web_search ["Tesla", "Tesla latest news", "Tesla stock price"] then -3. __plan "Based on the previous search results, I will now narrow down my queries to focus on Tesla's recent developments and stock performance." then +3. 0_reasoning "Based on the previous search results, I will now narrow down my queries to focus on Tesla's recent developments and stock performance." then 4. web_search ["Tesla Q2 2025 earnings", "Tesla new model 2025", "Tesla stock analysis"] then done. -5. __plan "I have gathered enough information to provide a comprehensive answer." +5. 0_reasoning "I have gathered enough information to provide a comprehensive answer." 6. done. You can search for 3 queries in one go, make sure to utilize all 3 queries to maximize the information you can gather. If a question is simple, then split your queries to cover different aspects or related topics to get a comprehensive understanding. If this tool is present and no other tools are more relevant, you MUST use this tool to get the needed information. You can call this tools, multiple times as needed. -` +`; const qualityModePrompt = ` Use this tool to perform web searches based on the provided queries. This is useful when you need to gather information from the web to answer the user's questions. You can provide up to 3 queries at a time. You will have to use this every single time if this is present and relevant. @@ -54,31 +54,32 @@ Your queries shouldn't be sentences but rather keywords that are SEO friendly an You can search for 3 queries in one go, make sure to utilize all 3 queries to maximize the information you can gather. If a question is simple, then split your queries to cover different aspects or related topics to get a comprehensive understanding. If this tool is present and no other tools are more relevant, you MUST use this tool to get the needed information. You can call this tools, multiple times as needed. -` +`; const webSearchAction: ResearchAction<typeof actionSchema> = { name: 'web_search', schema: actionSchema, - getToolDescription: () => 'Use this tool to perform web searches based on the provided queries. This is useful when you need to gather information from the web to answer the user\'s questions. You can provide up to 3 queries at a time. You will have to use this every single time if this is present and relevant.', + getToolDescription: () => + "Use this tool to perform web searches based on the provided queries. This is useful when you need to gather information from the web to answer the user's questions. You can provide up to 3 queries at a time. You will have to use this every single time if this is present and relevant.", getDescription: (config) => { - let prompt = '' + let prompt = ''; switch (config.mode) { case 'speed': - prompt = speedModePrompt + prompt = speedModePrompt; break; case 'balanced': - prompt = balancedModePrompt + prompt = balancedModePrompt; break; case 'quality': - prompt = qualityModePrompt + prompt = qualityModePrompt; break; default: - prompt = speedModePrompt + prompt = speedModePrompt; break; } - return prompt + return prompt; }, enabled: (config) => config.classification.classification.skipSearch === false, From c7c327a7bb468519da90f493b9a1da116b7df412 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:17:51 +0530 Subject: [PATCH 112/196] feat(utils): add token based text splitting --- src/lib/utils/splitText.ts | 74 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/lib/utils/splitText.ts diff --git a/src/lib/utils/splitText.ts b/src/lib/utils/splitText.ts new file mode 100644 index 0000000..796bf4b --- /dev/null +++ b/src/lib/utils/splitText.ts @@ -0,0 +1,74 @@ +import { getEncoding } from 'js-tiktoken'; + +const splitRegex = /(?<=\. |\n|! |\? |; |:\s|\d+\.\s|- |\* )/g; + +const enc = getEncoding('cl100k_base'); + +const getTokenCount = (text: string): number => { + try { + return enc.encode(text).length; + } catch { + return Math.ceil(text.length / 4); + } +}; + +export const splitText = ( + text: string, + maxTokens = 512, + overlapTokens = 64, +): string[] => { + const segments = text.split(splitRegex).filter(Boolean); + + if (segments.length === 0) { + return []; + } + + const segmentTokenCounts = segments.map(getTokenCount); + + const result: string[] = []; + + let chunkStart = 0; + + while (chunkStart < segments.length) { + let chunkEnd = chunkStart; + let currentTokenCount = 0; + + while (chunkEnd < segments.length && currentTokenCount < maxTokens) { + if (currentTokenCount + segmentTokenCounts[chunkEnd] > maxTokens) { + break; + } + + currentTokenCount += segmentTokenCounts[chunkEnd]; + chunkEnd++; + } + + let overlapBeforeStart = Math.max(0, chunkStart - 1); + let overlapBeforeTokenCount = 0; + + while (overlapBeforeStart >= 0 && overlapBeforeTokenCount < overlapTokens) { + if ( + overlapBeforeTokenCount + segmentTokenCounts[overlapBeforeStart] > + overlapTokens + ) { + break; + } + + overlapBeforeTokenCount += segmentTokenCounts[overlapBeforeStart]; + overlapBeforeStart--; + } + + const overlapStartIndex = Math.max(0, overlapBeforeStart + 1); + + const overlapBeforeContent = segments + .slice(overlapStartIndex, chunkStart) + .join(''); + + const chunkContent = segments.slice(chunkStart, chunkEnd).join(''); + + result.push(overlapBeforeContent + chunkContent); + + chunkStart = chunkEnd; + } + + return result; +}; From 6473e51fde89937917929fbe98690f0dca7af1fa Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:18:07 +0530 Subject: [PATCH 113/196] feat(uploads): add uploads manager --- src/lib/uploads/manager.ts | 217 +++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 src/lib/uploads/manager.ts diff --git a/src/lib/uploads/manager.ts b/src/lib/uploads/manager.ts new file mode 100644 index 0000000..81ef28e --- /dev/null +++ b/src/lib/uploads/manager.ts @@ -0,0 +1,217 @@ +import path from "path"; +import BaseEmbedding from "../models/base/embedding" +import crypto from "crypto" +import fs from 'fs'; +import { splitText } from "../utils/splitText"; +import { PDFParse } from 'pdf-parse'; +import officeParser from 'officeparser' +import { Chunk } from "../types"; + +const supportedMimeTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain'] as const + +type SupportedMimeType = typeof supportedMimeTypes[number]; + +type UploadManagerParams = { + embeddingModel: BaseEmbedding<any>; +} + +type RecordedFile = { + id: string; + name: string; + filePath: string; + contentPath: string; + uploadedAt: string; +} + +type FileRes = { + fileName: string; + fileExtension: string; + fileId: string; +} + +class UploadManager { + private embeddingModel: BaseEmbedding<any>; + static uploadsDir = path.join(process.cwd(), 'data', 'uploads'); + static uploadedFilesRecordPath = path.join(this.uploadsDir, 'uploaded_files.json'); + + constructor(private params: UploadManagerParams) { + this.embeddingModel = params.embeddingModel; + + if (!fs.existsSync(UploadManager.uploadsDir)) { + fs.mkdirSync(UploadManager.uploadsDir, { recursive: true }); + } + + if (!fs.existsSync(UploadManager.uploadedFilesRecordPath)) { + const data = { + files: [] + } + + fs.writeFileSync(UploadManager.uploadedFilesRecordPath, JSON.stringify(data, null, 2)); + } + } + + private static getRecordedFiles(): RecordedFile[] { + const data = fs.readFileSync(UploadManager.uploadedFilesRecordPath, 'utf-8'); + return JSON.parse(data).files; + } + + private static addNewRecordedFile(fileRecord: RecordedFile) { + const currentData = this.getRecordedFiles() + + currentData.push(fileRecord); + + fs.writeFileSync(UploadManager.uploadedFilesRecordPath, JSON.stringify({ files: currentData }, null, 2)); + } + + static getFile(fileId: string): RecordedFile | null { + const recordedFiles = this.getRecordedFiles(); + + return recordedFiles.find(f => f.id === fileId) || null; + } + + static getFileChunks(fileId: string): { content: string; embedding: number[] }[] { + try { + const recordedFile = this.getFile(fileId); + + if (!recordedFile) { + throw new Error(`File with ID ${fileId} not found`); + } + + const contentData = JSON.parse(fs.readFileSync(recordedFile.contentPath, 'utf-8')) + + return contentData.chunks; + } catch (err) { + console.log('Error getting file chunks:', err); + return []; + } + } + + private async extractContentAndEmbed(filePath: string, fileType: SupportedMimeType): Promise<string> { + switch (fileType) { + case 'text/plain': + const content = fs.readFileSync(filePath, 'utf-8'); + + const splittedText = splitText(content, 256, 64) + const embeddings = await this.embeddingModel.embedText(splittedText) + + if (embeddings.length !== splittedText.length) { + throw new Error('Embeddings and text chunks length mismatch'); + } + + const contentPath = filePath.split('.').slice(0, -1).join('.') + '.content.json'; + + const data = { + chunks: splittedText.map((text, i) => { + return { + content: text, + embedding: embeddings[i], + } + }) + } + + fs.writeFileSync(contentPath, JSON.stringify(data, null, 2)); + + return contentPath; + case 'application/pdf': + const pdfBuffer = fs.readFileSync(filePath); + + const parser = new PDFParse({ + data: pdfBuffer + }) + + const pdfText = await parser.getText().then(res => res.text) + + const pdfSplittedText = splitText(pdfText, 256, 64) + const pdfEmbeddings = await this.embeddingModel.embedText(pdfSplittedText) + + if (pdfEmbeddings.length !== pdfSplittedText.length) { + throw new Error('Embeddings and text chunks length mismatch'); + } + + const pdfContentPath = filePath.split('.').slice(0, -1).join('.') + '.content.json'; + + const pdfData = { + chunks: pdfSplittedText.map((text, i) => { + return { + content: text, + embedding: pdfEmbeddings[i], + } + }) + } + + fs.writeFileSync(pdfContentPath, JSON.stringify(pdfData, null, 2)); + + return pdfContentPath; + case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': + const docBuffer = fs.readFileSync(filePath); + + const docText = await officeParser.parseOfficeAsync(docBuffer) + + const docSplittedText = splitText(docText, 256, 64) + const docEmbeddings = await this.embeddingModel.embedText(docSplittedText) + + if (docEmbeddings.length !== docSplittedText.length) { + throw new Error('Embeddings and text chunks length mismatch'); + } + + const docContentPath = filePath.split('.').slice(0, -1).join('.') + '.content.json'; + + const docData = { + chunks: docSplittedText.map((text, i) => { + return { + content: text, + embedding: docEmbeddings[i], + } + }) + } + + fs.writeFileSync(docContentPath, JSON.stringify(docData, null, 2)); + + return docContentPath; + default: + throw new Error(`Unsupported file type: ${fileType}`); + } + } + + async processFiles(files: File[]): Promise<FileRes[]> { + const processedFiles: FileRes[] = []; + + await Promise.all(files.map(async (file) => { + if (!(supportedMimeTypes as unknown as string[]).includes(file.type)) { + throw new Error(`File type ${file.type} not supported`); + } + + const fileId = crypto.randomBytes(16).toString('hex'); + + const fileExtension = file.name.split('.').pop(); + const fileName = `${crypto.randomBytes(16).toString('hex')}.${fileExtension}`; + const filePath = path.join(UploadManager.uploadsDir, fileName); + + const buffer = Buffer.from(await file.arrayBuffer()) + + fs.writeFileSync(filePath, buffer); + + const contentFilePath = await this.extractContentAndEmbed(filePath, file.type as SupportedMimeType); + + const fileRecord: RecordedFile = { + id: fileId, + name: file.name, + filePath: filePath, + contentPath: contentFilePath, + uploadedAt: new Date().toISOString(), + } + + UploadManager.addNewRecordedFile(fileRecord); + + processedFiles.push({ + fileExtension: fileExtension || '', + fileId, + fileName: file.name + }); + })) + + return processedFiles; + } +} + +export default UploadManager; \ No newline at end of file From aeb90cb137ef1795ef058e285e6e2738ebe7f65c Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:18:33 +0530 Subject: [PATCH 114/196] feat(uploads): add uploads store with reciprocal rerank fusion --- src/lib/uploads/store.ts | 122 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 src/lib/uploads/store.ts diff --git a/src/lib/uploads/store.ts b/src/lib/uploads/store.ts new file mode 100644 index 0000000..29afeb0 --- /dev/null +++ b/src/lib/uploads/store.ts @@ -0,0 +1,122 @@ +import BaseEmbedding from "../models/base/embedding"; +import UploadManager from "./manager"; +import computeSimilarity from "../utils/computeSimilarity"; +import { Chunk } from "../types"; +import { hashObj } from "../serverUtils"; +import fs from 'fs'; + +type UploadStoreParams = { + embeddingModel: BaseEmbedding<any>; + fileIds: string[]; +} + +type StoreRecord = { + embedding: number[]; + content: string; + fileId: string; + metadata: Record<string, any> +} + +class UploadStore { + embeddingModel: BaseEmbedding<any>; + fileIds: string[]; + records: StoreRecord[] = []; + + constructor(private params: UploadStoreParams) { + this.embeddingModel = params.embeddingModel; + this.fileIds = params.fileIds; + this.initializeStore() + } + + initializeStore() { + this.fileIds.forEach((fileId) => { + const file = UploadManager.getFile(fileId) + + if (!file) { + throw new Error(`File with ID ${fileId} not found`); + } + + const chunks = UploadManager.getFileChunks(fileId); + + this.records.push(...chunks.map((chunk) => ({ + embedding: chunk.embedding, + content: chunk.content, + fileId: fileId, + metadata: { + fileName: file.name, + title: file.name, + url: `file_id://${file.id}`, + } + }))) + }) + } + + async query(queries: string[], topK: number): Promise<Chunk[]> { + const queryEmbeddings = await this.embeddingModel.embedText(queries) + + const results: { chunk: Chunk; score: number; }[][] = []; + const hashResults: string[][] = [] + + await Promise.all(queryEmbeddings.map(async (query) => { + const similarities = this.records.map((record, idx) => { + return { + chunk: { + content: record.content, + metadata: { + ...record.metadata, + fileId: record.fileId, + } + }, + score: computeSimilarity(query, record.embedding) + } as { chunk: Chunk; score: number; }; + }).sort((a, b) => b.score - a.score) + + results.push(similarities) + hashResults.push(similarities.map(s => hashObj(s))) + })) + + const chunkMap: Map<string, Chunk> = new Map(); + const scoreMap: Map<string, number> = new Map(); + const k = 60; + + for (let i = 0; i < results.length; i++) { + for (let j = 0; j < results[i].length; j++) { + const chunkHash = hashResults[i][j] + + chunkMap.set(chunkHash, results[i][j].chunk); + scoreMap.set(chunkHash, (scoreMap.get(chunkHash) || 0) + results[i][j].score / (j + 1 + k)); + } + } + + const finalResults = Array.from(scoreMap.entries()) + .sort((a, b) => b[1] - a[1]) + .map(([chunkHash, _score]) => { + return chunkMap.get(chunkHash)!; + }) + + return finalResults.slice(0, topK); + } + + static getFileData(fileIds: string[]): { fileName: string; initialContent: string }[] { + const filesData: { fileName: string; initialContent: string }[] = []; + + fileIds.forEach((fileId) => { + const file = UploadManager.getFile(fileId) + + if (!file) { + throw new Error(`File with ID ${fileId} not found`); + } + + const chunks = UploadManager.getFileChunks(fileId); + + filesData.push({ + fileName: file.name, + initialContent: chunks.slice(0, 3).map(c => c.content).join('\n---\n'), + }) + }) + + return filesData + } +} + +export default UploadStore \ No newline at end of file From 86ea3cde7e8e286d072e35af3e4988b0f058cfa8 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:18:48 +0530 Subject: [PATCH 115/196] feat(types): add upload research blocks --- src/lib/types.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/lib/types.ts b/src/lib/types.ts index 6606447..b78a07b 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -87,11 +87,25 @@ export type ReadingResearchBlock = { reading: Chunk[]; }; +export type UploadSearchingResearchBlock = { + id: string; + type: 'upload_searching'; + queries: string[]; +}; + +export type UploadSearchResultsResearchBlock = { + id: string; + type: 'upload_search_results'; + results: Chunk[]; +}; + export type ResearchBlockSubStep = | ReasoningResearchBlock | SearchingResearchBlock | SearchResultsResearchBlock - | ReadingResearchBlock; + | ReadingResearchBlock + | UploadSearchingResearchBlock + | UploadSearchResultsResearchBlock; export type ResearchBlock = { id: string; From fd745577d69e494d6092acdea1525cadbb5c380d Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:19:06 +0530 Subject: [PATCH 116/196] feat(writer-prompt): revert to old prompt to fix length issues --- src/lib/prompts/search/writer.ts | 109 ++++++++++--------------------- 1 file changed, 34 insertions(+), 75 deletions(-) diff --git a/src/lib/prompts/search/writer.ts b/src/lib/prompts/search/writer.ts index d564ffb..584ed0e 100644 --- a/src/lib/prompts/search/writer.ts +++ b/src/lib/prompts/search/writer.ts @@ -1,87 +1,46 @@ export const getWriterPrompt = (context: string) => { return ` -You are Perplexica, an AI assistant that provides helpful, accurate, and engaging answers. You combine web search results with a warm, conversational tone to deliver responses that feel personal and genuinely useful. +You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses. -## Core Principles + Your task is to provide answers that are: + - **Informative and relevant**: Thoroughly address the user's query using the given context. + - **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically. + - **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights. + - **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included. + - **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable. -**Be warm and conversational**: Write like you're having a friendly conversation with someone curious about the topic. Show genuine interest in helping them understand. Avoid being robotic or overly formal. + ### Formatting Instructions + - **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate. + - **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience. + - **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability. + - **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience. + - **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title. + - **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate. -**Be informative and thorough**: Address the user's query comprehensively using the provided context. Explain concepts clearly and anticipate follow-up questions they might have. + ### Citation Requirements + - Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`. + - Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]." + - Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context. + - Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]." + - Always prioritize credibility and accuracy by linking all statements back to their respective context sources. + - Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation. -**Be honest and credible**: Cite your sources using [number] notation. If information is uncertain or unavailable, say so transparently. + ### Special Instructions + - If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity. + - 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. -**No emojis**: Keep responses clean and professional. Never use emojis unless the user explicitly requests them. -## Formatting Guidelines + ### 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. + - Provide explanations or historical context as needed to enhance understanding. + - End with a conclusion or overall perspective if relevant. -**Use Markdown effectively**: -- Use headings (## and ###) to organize longer responses into logical sections -- Use **bold** for key terms and *italics* for emphasis -- Use bullet points and numbered lists to break down complex information -- Use tables when comparing data, features, or options -- Use code blocks for technical content when appropriate + <context> + ${context} + </context> -**Adapt length to the query**: -- Simple questions (weather, calculations, quick facts): Brief, direct answers -- Complex topics: Structured responses with sections, context, and depth -- Always start with the direct answer before expanding into details - -**No main title**: Jump straight into your response without a title heading. - -**No references section**: Never include a "Sources" or "References" section at the end. Citations are handled inline only. - -## Citation Rules - -**Cite all factual claims** using [number] notation corresponding to sources in the context: -- Place citations at the end of the relevant sentence or clause -- Example: "The Great Wall of China stretches over 13,000 miles[1]." -- Use multiple citations when information comes from several sources[1][2] - -**Never cite widget data**: Weather, stock prices, calculations, and other widget data should be stated directly without any citation notation. - -**Never list citation mappings**: Only use [number] in the text. Do not provide a list showing which number corresponds to which source. - -**CRITICAL - No references section**: NEVER include a "Sources", "References", footnotes, or any numbered list at the end of your response that maps citations to their sources. This is strictly forbidden. The system handles source display separately. Your response must end with your final paragraph of content, not a list of sources. - -## Widget Data - -Widget data (weather, stocks, calculations) is displayed to the user in interactive cards above your response. - -**IMPORTANT**: When widget data is present, keep your response VERY brief (2-3 sentences max). The user already sees the detailed data in the widget card. Do NOT repeat all the widget data in your text response. - -For example, for a weather query, just say: -"It's currently -8.7°C in New York with overcast skies. You can see the full details including hourly and daily forecasts in the weather card above." - -**Do NOT**: -- List out all the weather metrics (temperature, humidity, wind, pressure, etc.) -- Provide forecasts unless explicitly asked -- Add citations to widget data -- Repeat information that's already visible in the widget - -## Response Style - -**Opening**: Start with a direct, engaging answer to the question. Get to the point quickly. - -**Body**: Expand with relevant details, context, or explanations. Use formatting to make information scannable and easy to digest. - -**Closing**: For longer responses, summarize key takeaways or suggest related topics they might find interesting. Keep it natural, not formulaic. - -## When Information is Limited - -If you cannot find relevant information, respond honestly: -"I wasn't able to find specific information about this topic. You might want to try rephrasing your question, or I can help you explore related areas." - -Suggest alternative angles or related topics that might be helpful. - -<context> -${context} -</context> - -Current date & time in ISO format (UTC timezone) is: ${new Date().toISOString()}. - -FINAL REMINDERS: -1. DO NOT add a references/sources section at the end. Your response ends with content, not citations. -2. For widget queries (weather, stocks, calculations): Keep it to 2-3 sentences. The widget shows the details. -3. No emojis. + Current date & time in ISO format (UTC timezone) is: ${new Date().toISOString()}. `; }; From 56e47d6c392d8d2311ad36e09263fce80922d134 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:19:38 +0530 Subject: [PATCH 117/196] feat(ollama-llm): use hash to generate id --- src/lib/models/providers/ollama/ollamaLLM.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lib/models/providers/ollama/ollamaLLM.ts b/src/lib/models/providers/ollama/ollamaLLM.ts index 7e53786..0b88393 100644 --- a/src/lib/models/providers/ollama/ollamaLLM.ts +++ b/src/lib/models/providers/ollama/ollamaLLM.ts @@ -161,8 +161,13 @@ class OllamaLLM extends BaseLLM<OllamaConfig> { yield { contentChunk: chunk.message.content, toolCallChunk: - chunk.message.tool_calls?.map((tc) => ({ - id: crypto.randomUUID(), + chunk.message.tool_calls?.map((tc, i) => ({ + id: crypto + .createHash('sha256') + .update( + `${i}-${tc.function.name}`, + ) /* Ollama currently doesn't return a tool call ID so we're creating one based on the index and tool call name */ + .digest('hex'), name: tc.function.name, arguments: tc.function.arguments, })) || [], From 3949748bbd51775455c6c2ee527784017cc61361 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:19:52 +0530 Subject: [PATCH 118/196] feat(suggestions-agent): fix type errors --- src/lib/agents/suggestions/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/agents/suggestions/index.ts b/src/lib/agents/suggestions/index.ts index 050eac7..15b3598 100644 --- a/src/lib/agents/suggestions/index.ts +++ b/src/lib/agents/suggestions/index.ts @@ -19,7 +19,7 @@ const generateSuggestions = async ( input: SuggestionGeneratorInput, llm: BaseLLM<any>, ) => { - const res = await llm.generateObject<z.infer<typeof schema>>({ + const res = await llm.generateObject<typeof schema>({ messages: [ { role: 'system', From 40b25a487b7ef0ceaf628dacf00d037dca9c7ee8 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:20:26 +0530 Subject: [PATCH 119/196] feat(uploads): update to use new manager --- src/app/api/uploads/route.ts | 112 ++++------------------------------- 1 file changed, 10 insertions(+), 102 deletions(-) diff --git a/src/app/api/uploads/route.ts b/src/app/api/uploads/route.ts index dc9c202..9cac0f7 100644 --- a/src/app/api/uploads/route.ts +++ b/src/app/api/uploads/route.ts @@ -1,40 +1,16 @@ import { NextResponse } from 'next/server'; -import fs from 'fs'; -import path from 'path'; -import crypto from 'crypto'; -import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'; -import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'; -import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'; -import { Document } from '@langchain/core/documents'; import ModelRegistry from '@/lib/models/registry'; -import { Chunk } from '@/lib/types'; - -interface FileRes { - fileName: string; - fileExtension: string; - fileId: string; -} - -const uploadDir = path.join(process.cwd(), 'uploads'); - -if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }); -} - -const splitter = new RecursiveCharacterTextSplitter({ - chunkSize: 500, - chunkOverlap: 100, -}); +import UploadManager from '@/lib/uploads/manager'; export async function POST(req: Request) { try { const formData = await req.formData(); const files = formData.getAll('files') as File[]; - const embedding_model = formData.get('embedding_model_key') as string; - const embedding_model_provider = formData.get('embedding_model_provider_id') as string; + const embeddingModel = formData.get('embedding_model_key') as string; + const embeddingModelProvider = formData.get('embedding_model_provider_id') as string; - if (!embedding_model || !embedding_model_provider) { + if (!embeddingModel || !embeddingModelProvider) { return NextResponse.json( { message: 'Missing embedding model or provider' }, { status: 400 }, @@ -43,81 +19,13 @@ export async function POST(req: Request) { const registry = new ModelRegistry(); - const model = await registry.loadEmbeddingModel(embedding_model_provider, embedding_model); + const model = await registry.loadEmbeddingModel(embeddingModelProvider, embeddingModel); + + const uploadManager = new UploadManager({ + embeddingModel: model, + }) - const processedFiles: FileRes[] = []; - - await Promise.all( - files.map(async (file: any) => { - const fileExtension = file.name.split('.').pop(); - if (!['pdf', 'docx', 'txt'].includes(fileExtension!)) { - return NextResponse.json( - { message: 'File type not supported' }, - { status: 400 }, - ); - } - - const uniqueFileName = `${crypto.randomBytes(16).toString('hex')}.${fileExtension}`; - const filePath = path.join(uploadDir, uniqueFileName); - - const buffer = Buffer.from(await file.arrayBuffer()); - fs.writeFileSync(filePath, new Uint8Array(buffer)); - - let docs: any[] = []; - if (fileExtension === 'pdf') { - const loader = new PDFLoader(filePath); - docs = await loader.load(); - } else if (fileExtension === 'docx') { - const loader = new DocxLoader(filePath); - docs = await loader.load(); - } else if (fileExtension === 'txt') { - const text = fs.readFileSync(filePath, 'utf-8'); - docs = [ - new Document({ pageContent: text, metadata: { title: file.name } }), - ]; - } - - const splitted = await splitter.splitDocuments(docs); - - const extractedDataPath = filePath.replace(/\.\w+$/, '-extracted.json'); - fs.writeFileSync( - extractedDataPath, - JSON.stringify({ - title: file.name, - contents: splitted.map((doc) => doc.pageContent), - }), - ); - - const chunks: Chunk[] = splitted.map((doc) => { - return { - content: doc.pageContent, - metadata: doc.metadata, - } - }); - - const embeddings = await model.embedChunks( - chunks - ); - - const embeddingsDataPath = filePath.replace( - /\.\w+$/, - '-embeddings.json', - ); - fs.writeFileSync( - embeddingsDataPath, - JSON.stringify({ - title: file.name, - embeddings, - }), - ); - - processedFiles.push({ - fileName: file.name, - fileExtension: fileExtension, - fileId: uniqueFileName.replace(/\.\w+$/, ''), - }); - }), - ); + const processedFiles = await uploadManager.processFiles(files); return NextResponse.json({ files: processedFiles, From 8d471ac40e5d3836f592b8b074c19a088dc0aa96 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:21:22 +0530 Subject: [PATCH 120/196] feat(registry): update to send fileIds --- .../search/researcher/actions/registry.ts | 18 +++++++++++++++--- src/lib/agents/search/types.ts | 8 +++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/lib/agents/search/researcher/actions/registry.ts b/src/lib/agents/search/researcher/actions/registry.ts index 3a8eda6..8e0530c 100644 --- a/src/lib/agents/search/researcher/actions/registry.ts +++ b/src/lib/agents/search/researcher/actions/registry.ts @@ -20,6 +20,7 @@ class ActionRegistry { static getAvailableActions(config: { classification: ClassifierOutput; + fileIds: string[]; mode: SearchAgentConfig['mode']; }): ResearchAction[] { return Array.from( @@ -29,6 +30,7 @@ class ActionRegistry { static getAvailableActionTools(config: { classification: ClassifierOutput; + fileIds: string[]; mode: SearchAgentConfig['mode']; }): Tool[] { const availableActions = this.getAvailableActions(config); @@ -42,19 +44,26 @@ class ActionRegistry { static getAvailableActionsDescriptions(config: { classification: ClassifierOutput; + fileIds: string[]; mode: SearchAgentConfig['mode']; }): string { const availableActions = this.getAvailableActions(config); return availableActions - .map((action) => `<tool name="${action.name}">\n${action.getDescription({ mode: config.mode })}\n</tool>`) + .map( + (action) => + `<tool name="${action.name}">\n${action.getDescription({ mode: config.mode })}\n</tool>`, + ) .join('\n\n'); } static async execute( name: string, params: any, - additionalConfig: AdditionalConfig & { researchBlockId: string }, + additionalConfig: AdditionalConfig & { + researchBlockId: string; + fileIds: string[]; + }, ) { const action = this.actions.get(name); @@ -67,7 +76,10 @@ class ActionRegistry { static async executeAll( actions: ToolCall[], - additionalConfig: AdditionalConfig & { researchBlockId: string }, + additionalConfig: AdditionalConfig & { + researchBlockId: string; + fileIds: string[]; + }, ): Promise<ActionOutput[]> { const results: ActionOutput[] = []; diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts index f1ae862..0733de3 100644 --- a/src/lib/agents/search/types.ts +++ b/src/lib/agents/search/types.ts @@ -8,6 +8,7 @@ export type SearchSources = 'web' | 'discussions' | 'academic'; export type SearchAgentConfig = { sources: SearchSources[]; + fileIds: string[]; llm: BaseLLM<any>; embedding: BaseEmbedding<any>; mode: 'speed' | 'balanced' | 'quality'; @@ -102,11 +103,16 @@ export interface ResearchAction< schema: z.ZodObject<any>; getToolDescription: (config: { mode: SearchAgentConfig['mode'] }) => string; getDescription: (config: { mode: SearchAgentConfig['mode'] }) => string; - enabled: (config: { classification: ClassifierOutput, mode: SearchAgentConfig['mode'] }) => boolean; + enabled: (config: { + classification: ClassifierOutput; + fileIds: string[]; + mode: SearchAgentConfig['mode']; + }) => boolean; execute: ( params: z.infer<TSchema>, additionalConfig: AdditionalConfig & { researchBlockId: string; + fileIds: string[]; }, ) => Promise<ActionOutput>; } From 1f3bf8da32385991b84968211f917517867b0ee1 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:21:44 +0530 Subject: [PATCH 121/196] feat(researcher): use reasoning --- src/lib/agents/search/researcher/index.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/lib/agents/search/researcher/index.ts b/src/lib/agents/search/researcher/index.ts index 6762a61..334ba74 100644 --- a/src/lib/agents/search/researcher/index.ts +++ b/src/lib/agents/search/researcher/index.ts @@ -5,7 +5,6 @@ import SessionManager from '@/lib/session'; import { Message, ReasoningResearchBlock } from '@/lib/types'; import formatChatHistoryAsString from '@/lib/utils/formatHistory'; import { ToolCall } from '@/lib/models/types'; -import fs from 'fs'; class Researcher { async research( @@ -22,13 +21,15 @@ class Researcher { const availableTools = ActionRegistry.getAvailableActionTools({ classification: input.classification, + fileIds: input.config.fileIds, mode: input.config.mode, }); const availableActionsDescription = ActionRegistry.getAvailableActionsDescriptions({ classification: input.classification, - mode: input.config.mode + fileIds: input.config.fileIds, + mode: input.config.mode, }); const researchBlockId = crypto.randomUUID(); @@ -59,6 +60,7 @@ class Researcher { input.config.mode, i, maxIteration, + input.config.fileIds, ); const actionStream = input.config.llm.streamText({ @@ -83,7 +85,7 @@ class Researcher { if (partialRes.toolCallChunk.length > 0) { partialRes.toolCallChunk.forEach((tc) => { if ( - tc.name === '___plan' && + tc.name === '0_reasoning' && tc.arguments['plan'] && !reasoningEmitted && block && @@ -105,7 +107,7 @@ class Researcher { }, ]); } else if ( - tc.name === '___plan' && + tc.name === '0_reasoning' && tc.arguments['plan'] && reasoningEmitted && block && @@ -162,6 +164,7 @@ class Researcher { embedding: input.config.embedding, session: session, researchBlockId: researchBlockId, + fileIds: input.config.fileIds, }); actionOutput.push(...actionResults); From 748ee4d3c2b74bab077beac12f4213916c981e55 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:22:17 +0530 Subject: [PATCH 122/196] feat(actions): add uploads search action --- .../search/researcher/actions/fileSearch.ts | 10 -- .../agents/search/researcher/actions/index.ts | 2 + .../researcher/actions/uploadsSearch.ts | 102 ++++++++++++++++++ 3 files changed, 104 insertions(+), 10 deletions(-) delete mode 100644 src/lib/agents/search/researcher/actions/fileSearch.ts create mode 100644 src/lib/agents/search/researcher/actions/uploadsSearch.ts diff --git a/src/lib/agents/search/researcher/actions/fileSearch.ts b/src/lib/agents/search/researcher/actions/fileSearch.ts deleted file mode 100644 index 0905e5d..0000000 --- a/src/lib/agents/search/researcher/actions/fileSearch.ts +++ /dev/null @@ -1,10 +0,0 @@ -import z from "zod"; -import { ResearchAction } from "../../types"; - -const schema = z.object({ - queries: z.array(z.string()).describe("A list of queries to search in files."), -}) - -const fileSearhAction: ResearchAction<typeof schema> = { - name: "file_search", -} \ No newline at end of file diff --git a/src/lib/agents/search/researcher/actions/index.ts b/src/lib/agents/search/researcher/actions/index.ts index 15c5734..6d7e58f 100644 --- a/src/lib/agents/search/researcher/actions/index.ts +++ b/src/lib/agents/search/researcher/actions/index.ts @@ -2,11 +2,13 @@ import doneAction from './done'; import planAction from './plan'; import ActionRegistry from './registry'; import scrapeURLAction from './scrapeURL'; +import uploadsSearchAction from './uploadsSearch'; import webSearchAction from './webSearch'; ActionRegistry.register(webSearchAction); ActionRegistry.register(doneAction); ActionRegistry.register(planAction); ActionRegistry.register(scrapeURLAction); +ActionRegistry.register(uploadsSearchAction); export { ActionRegistry }; diff --git a/src/lib/agents/search/researcher/actions/uploadsSearch.ts b/src/lib/agents/search/researcher/actions/uploadsSearch.ts new file mode 100644 index 0000000..8195063 --- /dev/null +++ b/src/lib/agents/search/researcher/actions/uploadsSearch.ts @@ -0,0 +1,102 @@ +import z from 'zod'; +import { ResearchAction } from '../../types'; +import UploadStore from '@/lib/uploads/store'; + +const schema = z.object({ + queries: z + .array(z.string()) + .describe( + 'A list of queries to search in user uploaded files. Can be a maximum of 3 queries.', + ), +}); + +const uploadsSearchAction: ResearchAction<typeof schema> = { + name: 'uploads_search', + enabled: (config) => + (config.classification.classification.personalSearch && + config.fileIds.length > 0) || + config.fileIds.length > 0, + schema, + getToolDescription: () => + `Use this tool to perform searches over the user's uploaded files. This is useful when you need to gather information from the user's documents to answer their questions. You can provide up to 3 queries at a time. You will have to use this every single time if this is present and relevant.`, + getDescription: () => ` + Use this tool to perform searches over the user's uploaded files. This is useful when you need to gather information from the user's documents to answer their questions. You can provide up to 3 queries at a time. You will have to use this every single time if this is present and relevant. + Always ensure that the queries you use are directly relevant to the user's request and pertain to the content of their uploaded files. + + For example, if the user says "Please find information about X in my uploaded documents", you can call this tool with a query related to X to retrieve the relevant information from their files. + Never use this tool to search the web or for information that is not contained within the user's uploaded files. + `, + execute: async (input, additionalConfig) => { + input.queries = input.queries.slice(0, 3); + + const researchBlock = additionalConfig.session.getBlock( + additionalConfig.researchBlockId, + ); + + if (researchBlock && researchBlock.type === 'research') { + researchBlock.data.subSteps.push({ + id: crypto.randomUUID(), + type: 'upload_searching', + queries: input.queries, + }); + + additionalConfig.session.updateBlock(additionalConfig.researchBlockId, [ + { + op: 'replace', + path: '/data/subSteps', + value: researchBlock.data.subSteps, + }, + ]); + } + + const uploadStore = new UploadStore({ + embeddingModel: additionalConfig.embedding, + fileIds: additionalConfig.fileIds, + }); + + const results = await uploadStore.query(input.queries, 10); + + const seenIds = new Map<string, number>(); + + const filteredSearchResults = results + .map((result, index) => { + if (result.metadata.url && !seenIds.has(result.metadata.url)) { + seenIds.set(result.metadata.url, index); + return result; + } else if (result.metadata.url && seenIds.has(result.metadata.url)) { + const existingIndex = seenIds.get(result.metadata.url)!; + const existingResult = results[existingIndex]; + + existingResult.content += `\n\n${result.content}`; + + return undefined; + } + + return result; + }) + .filter((r) => r !== undefined); + + if (researchBlock && researchBlock.type === 'research') { + researchBlock.data.subSteps.push({ + id: crypto.randomUUID(), + type: 'upload_search_results', + results: filteredSearchResults, + }); + + additionalConfig.session.updateBlock(additionalConfig.researchBlockId, [ + { + op: 'replace', + path: '/data/subSteps', + value: researchBlock.data.subSteps, + }, + ]); + } + + return { + type: 'search_results', + results: filteredSearchResults, + }; + }, +}; + +export default uploadsSearchAction; From a6ff94d03022366eb595fdaf38727fd72556a529 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:22:41 +0530 Subject: [PATCH 123/196] feat(api): update to use fileIds --- src/app/api/chat/route.ts | 2 +- src/app/api/search/route.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 2ea0bf5..23104e0 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,4 +1,3 @@ -import crypto from 'crypto'; import { z } from 'zod'; import ModelRegistry from '@/lib/models/registry'; import { ModelWithProvider } from '@/lib/models/types'; @@ -206,6 +205,7 @@ export const POST = async (req: Request) => { embedding: embedding, sources: ['web'], mode: body.optimizationMode, + fileIds: body.files, }, }); diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts index 8f357cb..530a7e7 100644 --- a/src/app/api/search/route.ts +++ b/src/app/api/search/route.ts @@ -57,6 +57,7 @@ export const POST = async (req: Request) => { llm: llm, sources: ['web', 'discussions', 'academic'], mode: 'balanced', + fileIds: [] }, followUp: body.query, }); From 60675955e450397e2421341974e04c9472500aa2 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:23:08 +0530 Subject: [PATCH 124/196] feat(researcher-prompt): add user uploaded files --- src/lib/prompts/search/researcher.ts | 173 ++++++++++++++++++--------- 1 file changed, 115 insertions(+), 58 deletions(-) diff --git a/src/lib/prompts/search/researcher.ts b/src/lib/prompts/search/researcher.ts index 3ac5201..d402ad5 100644 --- a/src/lib/prompts/search/researcher.ts +++ b/src/lib/prompts/search/researcher.ts @@ -1,9 +1,17 @@ -const getSpeedPrompt = (actionDesc: string, i: number, maxIteration: number) => { +import BaseEmbedding from '@/lib/models/base/embedding'; +import UploadStore from '@/lib/uploads/store'; + +const getSpeedPrompt = ( + actionDesc: string, + i: number, + maxIteration: number, + fileDesc: string, +) => { const today = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', - }) + }); return ` Assistant is an action orchestrator. Your job is to fulfill user requests by selecting and executing the available tools—no free-form replies. @@ -65,18 +73,33 @@ const getSpeedPrompt = (actionDesc: string, i: number, maxIteration: number) => - Call done when you have gathered enough to answer or performed the required actions. - Do not invent tools. Do not return JSON. </response_protocol> - ` -} -const getBalancedPrompt = (actionDesc: string, i: number, maxIteration: number) => { + ${ + fileDesc.length > 0 + ? `<user_uploaded_files> + The user has uploaded the following files which may be relevant to their request: + ${fileDesc} + You can use the uploaded files search tool to look for information within these documents if needed. + </user_uploaded_files>` + : '' + } + `; +}; + +const getBalancedPrompt = ( + actionDesc: string, + i: number, + maxIteration: number, + fileDesc: string, +) => { const today = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', - }) + }); return ` - Assistant is an action orchestrator. Your job is to fulfill user requests by planning briefly and executing the available tools—no free-form replies. + Assistant is an action orchestrator. Your job is to fulfill user requests by reasoning briefly and executing the available tools—no free-form replies. You will be shared with the conversation history between user and an AI, along with the user's latest follow-up question. Based on this, you must use the available tools to fulfill the user's request. Today's date: ${today} @@ -85,44 +108,43 @@ const getBalancedPrompt = (actionDesc: string, i: number, maxIteration: number) When you are finished, you must call the \`done\` tool. Never output text directly. <goal> - Fulfill the user's request with concise planning plus focused actions. - You must call the ___plan tool first on every turn to state a short plan. Open with a brief intent phrase (e.g., "Okay, the user wants to...", "Searching for...", "Looking into...") and lay out the steps you will take. Keep it natural language, no tool names. - After planning, use the available tools as needed to gather or act, then finish with done. + Fulfill the user's request with concise reasoning plus focused actions. + You must call the 0_reasoning tool before every tool call in this assistant turn. Alternate: 0_reasoning → tool → 0_reasoning → tool ... and finish with 0_reasoning → done. Open each 0_reasoning with a brief intent phrase (e.g., "Okay, the user wants to...", "Searching for...", "Looking into...") and lay out your reasoning for the next step. Keep it natural language, no tool names. </goal> <core_principle> Your knowledge is outdated; if you have web search, use it to ground answers even for seemingly basic facts. - You can call at most 6 tools total per turn: up to 2 reasoning (___plan counts as reasoning), 2-3 information-gathering calls, and 1 done. If you hit the cap, stop after done. + You can call at most 6 tools total per turn: up to 2 reasoning (0_reasoning counts as reasoning), 2-3 information-gathering calls, and 1 done. If you hit the cap, stop after done. Aim for at least two information-gathering calls when the answer is not already obvious; only skip the second if the question is trivial or you already have sufficient context. Do not spam searches—pick the most targeted queries. </core_principle> <done_usage> - Call done only after the plan plus the necessary tool calls are completed and you have enough to answer. If you call done early, stop. If you reach the tool cap, call done to conclude. + Call done only after the reasoning plus the necessary tool calls are completed and you have enough to answer. If you call done early, stop. If you reach the tool cap, call done to conclude. </done_usage> <examples> ## Example 1: Unknown Subject User: "What is Kimi K2?" - Plan: "Okay, the user wants to know about Kimi K2. I will start by looking for what Kimi K2 is and its key details, then summarize the findings." - Action: web_search ["Kimi K2", "Kimi K2 AI"] then done. + Reason: "Okay, the user wants to know about Kimi K2. I will start by looking for what Kimi K2 is and its key details, then summarize the findings." + Action: web_search ["Kimi K2", "Kimi K2 AI"] then reasoning then done. ## Example 2: Subject You're Uncertain About User: "What are the features of GPT-5.1?" - Plan: "The user is asking about GPT-5.1 features. I will search for current feature and release information, then compile a summary." - Action: web_search ["GPT-5.1", "GPT-5.1 features", "GPT-5.1 release"] then done. + Reason: "The user is asking about GPT-5.1 features. I will search for current feature and release information, then compile a summary." + Action: web_search ["GPT-5.1", "GPT-5.1 features", "GPT-5.1 release"] then reasoning then done. ## Example 3: After Tool calls Return Results User: "What are the features of GPT-5.1?" [Previous tool calls returned the needed info] - Plan: "I have gathered enough information about GPT-5.1 features; I will now wrap up." + Reason: "I have gathered enough information about GPT-5.1 features; I will now wrap up." Action: done. </examples> <available_tools> - YOU MUST ALWAYS CALL THE ___plan TOOL FIRST ON EVERY TURN BEFORE ANY OTHER ACTION. IF YOU DO NOT CALL IT, THE TOOL CALL WILL BE IGNORED. + YOU MUST CALL 0_reasoning BEFORE EVERY TOOL CALL IN THIS ASSISTANT TURN. IF YOU DO NOT CALL IT, THE TOOL CALL WILL BE IGNORED. ${actionDesc} </available_tools> @@ -138,29 +160,44 @@ const getBalancedPrompt = (actionDesc: string, i: number, maxIteration: number) 5. **Overthinking**: Keep reasoning simple and tool calls focused -6. **Skipping the plan**: Always call ___plan first to outline your approach before other actions +6. **Skipping the reasoning step**: Always call 0_reasoning first to outline your approach before other actions </mistakes_to_avoid> <response_protocol> - NEVER output normal text to the user. ONLY call tools. -- Start with ___plan: open with intent phrase ("Okay, the user wants to...", "Looking into...", etc.) and lay out steps. No tool names. +- Start with 0_reasoning and call 0_reasoning before every tool call (including done): open with intent phrase ("Okay, the user wants to...", "Looking into...", etc.) and lay out your reasoning for the next step. No tool names. - Choose tools based on the action descriptions provided above. - Default to web_search when information is missing or stale; keep queries targeted (max 3 per call). -- Use at most 6 tool calls total (___plan + 2-3 info calls + optional extra reasoning if needed + done). If done is called early, stop. +- Use at most 6 tool calls total (0_reasoning + 2-3 info calls + 0_reasoning + done). If done is called early, stop. - Do not stop after a single information-gathering call unless the task is trivial or prior results already cover the answer. - Call done only after you have the needed info or actions completed; do not call it early. - Do not invent tools. Do not return JSON. </response_protocol> - ` -} -const getQualityPrompt = (actionDesc: string, i: number, maxIteration: number) => { + ${ + fileDesc.length > 0 + ? `<user_uploaded_files> + The user has uploaded the following files which may be relevant to their request: + ${fileDesc} + You can use the uploaded files search tool to look for information within these documents if needed. + </user_uploaded_files>` + : '' + } + `; +}; + +const getQualityPrompt = ( + actionDesc: string, + i: number, + maxIteration: number, + fileDesc: string, +) => { const today = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', - }) + }); return ` Assistant is a deep-research orchestrator. Your job is to fulfill user requests with the most thorough, comprehensive research possible—no free-form replies. @@ -173,16 +210,16 @@ const getQualityPrompt = (actionDesc: string, i: number, maxIteration: number) = <goal> Conduct the deepest, most thorough research possible. Leave no stone unturned. - Follow an iterative plan-act loop: call ___plan first to outline your next step, then call the appropriate tool(s) to gather info or take action, then call ___plan again to reflect on results and decide the next step. Repeat until you have exhaustive coverage. - Open each plan with a brief intent phrase (e.g., "Okay, the user wants to know about...", "From the results, it looks like...", "Now I need to dig into...") and describe what you'll do next. Keep it natural language, no tool names. + Follow an iterative reason-act loop: call 0_reasoning before every tool call to outline the next step, then call the tool, then 0_reasoning again to reflect and decide the next step. Repeat until you have exhaustive coverage. + Open each 0_reasoning with a brief intent phrase (e.g., "Okay, the user wants to know about...", "From the results, it looks like...", "Now I need to dig into...") and describe what you'll do next. Keep it natural language, no tool names. Finish with done only when you have comprehensive, multi-angle information. </goal> <core_principle> Your knowledge is outdated; always use the available tools to ground answers. This is DEEP RESEARCH mode—be exhaustive. Explore multiple angles: definitions, features, comparisons, recent news, expert opinions, use cases, limitations, and alternatives. - You can call up to 10 tools total per turn. Use an iterative loop: ___plan → tool call(s) → ___plan → tool call(s) → ... → done. - Never settle for surface-level answers. If results hint at more depth, plan your next step and follow up. Cross-reference information from multiple queries. + You can call up to 10 tools total per turn. Use an iterative loop: 0_reasoning → tool call(s) → 0_reasoning → tool call(s) → ... → 0_reasoning → done. + Never settle for surface-level answers. If results hint at more depth, reason about your next step and follow up. Cross-reference information from multiple queries. </core_principle> <done_usage> @@ -193,41 +230,41 @@ const getQualityPrompt = (actionDesc: string, i: number, maxIteration: number) = ## Example 1: Unknown Subject - Deep Dive User: "What is Kimi K2?" - Plan: "Okay, the user wants to know about Kimi K2. I'll start by finding out what it is and its key capabilities." + Reason: "Okay, the user wants to know about Kimi K2. I'll start by finding out what it is and its key capabilities." [calls info-gathering tool] - Plan: "From the results, Kimi K2 is an AI model by Moonshot. Now I need to dig into how it compares to competitors and any recent news." + Reason: "From the results, Kimi K2 is an AI model by Moonshot. Now I need to dig into how it compares to competitors and any recent news." [calls info-gathering tool] - Plan: "Got comparison info. Let me also check for limitations or critiques to give a balanced view." + Reason: "Got comparison info. Let me also check for limitations or critiques to give a balanced view." [calls info-gathering tool] - Plan: "I now have comprehensive coverage—definition, capabilities, comparisons, and critiques. Wrapping up." + Reason: "I now have comprehensive coverage—definition, capabilities, comparisons, and critiques. Wrapping up." Action: done. ## Example 2: Feature Research - Comprehensive User: "What are the features of GPT-5.1?" - Plan: "The user wants comprehensive GPT-5.1 feature information. I'll start with core features and specs." + Reason: "The user wants comprehensive GPT-5.1 feature information. I'll start with core features and specs." [calls info-gathering tool] - Plan: "Got the basics. Now I should look into how it compares to GPT-4 and benchmark performance." + Reason: "Got the basics. Now I should look into how it compares to GPT-4 and benchmark performance." [calls info-gathering tool] - Plan: "Good comparison data. Let me also gather use cases and expert opinions for depth." + Reason: "Good comparison data. Let me also gather use cases and expert opinions for depth." [calls info-gathering tool] - Plan: "I have exhaustive coverage across features, comparisons, benchmarks, and reviews. Done." + Reason: "I have exhaustive coverage across features, comparisons, benchmarks, and reviews. Done." Action: done. ## Example 3: Iterative Refinement User: "Tell me about quantum computing applications in healthcare." - Plan: "Okay, the user wants to know about quantum computing in healthcare. I'll start with an overview of current applications." + Reason: "Okay, the user wants to know about quantum computing in healthcare. I'll start with an overview of current applications." [calls info-gathering tool] - Plan: "Results mention drug discovery and diagnostics. Let me dive deeper into drug discovery use cases." + Reason: "Results mention drug discovery and diagnostics. Let me dive deeper into drug discovery use cases." [calls info-gathering tool] - Plan: "Now I'll explore the diagnostics angle and any recent breakthroughs." + Reason: "Now I'll explore the diagnostics angle and any recent breakthroughs." [calls info-gathering tool] - Plan: "Comprehensive coverage achieved. Wrapping up." + Reason: "Comprehensive coverage achieved. Wrapping up." Action: done. </examples> <available_tools> - YOU MUST ALWAYS CALL THE ___plan TOOL FIRST ON EVERY TURN BEFORE ANY OTHER ACTION. IF YOU DO NOT CALL IT, THE TOOL CALL WILL BE IGNORED. + YOU MUST CALL 0_reasoning BEFORE EVERY TOOL CALL IN THIS ASSISTANT TURN. IF YOU DO NOT CALL IT, THE TOOL CALL WILL BE IGNORED. ${actionDesc} </available_tools> @@ -254,44 +291,64 @@ const getQualityPrompt = (actionDesc: string, i: number, maxIteration: number) = 5. **Premature done**: Don't call done until you've exhausted reasonable research avenues -6. **Skipping the plan**: Always call ___plan first to outline your research strategy +6. **Skipping the reasoning step**: Always call 0_reasoning first to outline your research strategy </mistakes_to_avoid> <response_protocol> - NEVER output normal text to the user. ONLY call tools. -- Follow an iterative loop: ___plan → tool call(s) → ___plan → tool call(s) → ... → done. -- Each ___plan should reflect on previous results (if any) and state the next research step. No tool names in the plan. +- Follow an iterative loop: 0_reasoning → tool call → 0_reasoning → tool call → ... → 0_reasoning → done. +- Each 0_reasoning should reflect on previous results (if any) and state the next research step. No tool names in the reasoning. - Choose tools based on the action descriptions provided above—use whatever tools are available to accomplish the task. - Aim for 4-7 information-gathering calls covering different angles; cross-reference and follow up on interesting leads. - Call done only after comprehensive, multi-angle research is complete. - Do not invent tools. Do not return JSON. </response_protocol> - ` -} + + ${ + fileDesc.length > 0 + ? `<user_uploaded_files> + The user has uploaded the following files which may be relevant to their request: + ${fileDesc} + You can use the uploaded files search tool to look for information within these documents if needed. + </user_uploaded_files>` + : '' + } + `; +}; export const getResearcherPrompt = ( actionDesc: string, mode: 'speed' | 'balanced' | 'quality', i: number, maxIteration: number, + fileIds: string[], ) => { - let prompt = '' + let prompt = ''; + + const filesData = UploadStore.getFileData(fileIds); + + const fileDesc = filesData + .map( + (f) => + `<file><name>${f.fileName}</name><initial_content>${f.initialContent}</initial_content></file>`, + ) + .join('\n'); switch (mode) { case 'speed': - prompt = getSpeedPrompt(actionDesc, i, maxIteration) - break + prompt = getSpeedPrompt(actionDesc, i, maxIteration, fileDesc); + break; case 'balanced': - prompt = getBalancedPrompt(actionDesc, i, maxIteration) - break + prompt = getBalancedPrompt(actionDesc, i, maxIteration, fileDesc); + break; case 'quality': - prompt = getQualityPrompt(actionDesc, i, maxIteration) - break + prompt = getQualityPrompt(actionDesc, i, maxIteration, fileDesc); + break; default: - prompt = getSpeedPrompt(actionDesc, i, maxIteration) - break + prompt = getSpeedPrompt(actionDesc, i, maxIteration, fileDesc); + break; } - return prompt -}; + return prompt; +}; From a99702d837a5b334a2ac9914cb5cc3fb1d9fe966 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:23:39 +0530 Subject: [PATCH 125/196] feat(app): update UI to handle uploads --- src/components/AssistantSteps.tsx | 61 ++++++++++++++++++++++++++++++- src/components/MessageSources.tsx | 6 ++- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/components/AssistantSteps.tsx b/src/components/AssistantSteps.tsx index b369d1a..6c84ec6 100644 --- a/src/components/AssistantSteps.tsx +++ b/src/components/AssistantSteps.tsx @@ -16,9 +16,12 @@ import { useChat } from '@/lib/hooks/useChat'; const getStepIcon = (step: ResearchBlockSubStep) => { if (step.type === 'reasoning') { return <Brain className="w-4 h-4" />; - } else if (step.type === 'searching') { + } else if (step.type === 'searching' || step.type === 'upload_searching') { return <Search className="w-4 h-4" />; - } else if (step.type === 'search_results') { + } else if ( + step.type === 'search_results' || + step.type === 'upload_search_results' + ) { return <FileText className="w-4 h-4" />; } else if (step.type === 'reading') { return <BookSearch className="w-4 h-4" />; @@ -39,6 +42,10 @@ const getStepTitle = ( return `Found ${step.reading.length} ${step.reading.length === 1 ? 'result' : 'results'}`; } else if (step.type === 'reading') { return `Reading ${step.reading.length} ${step.reading.length === 1 ? 'source' : 'sources'}`; + } else if (step.type === 'upload_searching') { + return 'Scanning your uploaded documents'; + } else if (step.type === 'upload_search_results') { + return `Reading ${step.results.length} ${step.results.length === 1 ? 'document' : 'documents'}`; } return 'Processing'; @@ -195,6 +202,56 @@ const AssistantSteps = ({ })} </div> )} + + {step.type === 'upload_searching' && + step.queries.length > 0 && ( + <div className="flex flex-wrap gap-1.5 mt-1.5"> + {step.queries.map((query, idx) => ( + <span + key={idx} + className="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium bg-light-100 dark:bg-dark-100 text-black/70 dark:text-white/70 border border-light-200 dark:border-dark-200" + > + {query} + </span> + ))} + </div> + )} + + {step.type === 'upload_search_results' && + step.results.length > 0 && ( + <div className="mt-1.5 space-y-2"> + {step.results.slice(0, 4).map((result, idx) => { + const title = + (result.metadata && + (result.metadata.title || + result.metadata.fileName)) || + 'Untitled document'; + const snippet = (result.content || '') + .replace(/\s+/g, ' ') + .trim() + .slice(0, 220); + + return ( + <div + key={idx} + className="flex gap-3 items-start rounded-lg border border-light-200 dark:border-dark-200 bg-light-100 dark:bg-dark-100 p-2" + > + <div className="mt-0.5 h-10 w-10 rounded-md bg-cyan-100 text-cyan-800 dark:bg-sky-500 dark:text-cyan-50 flex items-center justify-center"> + <FileText className="w-5 h-5" /> + </div> + <div className="flex-1 min-w-0"> + <div className="text-sm font-semibold text-black dark:text-white line-clamp-1"> + {title} + </div> + <div className="text-xs text-black/70 dark:text-white/70 mt-0.5 leading-relaxed line-clamp-3"> + {snippet || 'No preview available.'} + </div> + </div> + </div> + ); + })} + </div> + )} </div> </motion.div> ); diff --git a/src/components/MessageSources.tsx b/src/components/MessageSources.tsx index ea5fa79..a1db27a 100644 --- a/src/components/MessageSources.tsx +++ b/src/components/MessageSources.tsx @@ -37,7 +37,7 @@ const MessageSources = ({ sources }: { sources: Chunk[] }) => { </p> <div className="flex flex-row items-center justify-between"> <div className="flex flex-row items-center space-x-1"> - {source.metadata.url === 'File' ? ( + {source.metadata.url.includes('file_id://') ? ( <div className="bg-dark-200 hover:bg-dark-100 transition duration-200 flex items-center justify-center w-6 h-6 rounded-full"> <File size={12} className="text-white/70" /> </div> @@ -51,7 +51,9 @@ const MessageSources = ({ sources }: { sources: Chunk[] }) => { /> )} <p className="text-xs text-black/50 dark:text-white/50 overflow-hidden whitespace-nowrap text-ellipsis"> - {source.metadata.url.replace(/.+\/\/|www.|\..+/g, '')} + {source.metadata.url.includes('file_id://') + ? 'Uploaded File' + : source.metadata.url.replace(/.+\/\/|www.|\..+/g, '')} </p> </div> <div className="flex flex-row items-center space-x-1 text-black/50 dark:text-white/50 text-xs"> From 3d1d164f685e06fb14b514b5b52930c819f74ddd Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:23:54 +0530 Subject: [PATCH 126/196] feat(app): lint & beautify --- src/app/api/suggestions/route.ts | 1 - src/lib/agents/search/researcher/actions/scrapeURL.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/api/suggestions/route.ts b/src/app/api/suggestions/route.ts index 0c70cba..07432d6 100644 --- a/src/app/api/suggestions/route.ts +++ b/src/app/api/suggestions/route.ts @@ -1,7 +1,6 @@ import generateSuggestions from '@/lib/agents/suggestions'; import ModelRegistry from '@/lib/models/registry'; import { ModelWithProvider } from '@/lib/models/types'; -import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; interface SuggestionsGenerationBody { chatHistory: any[]; diff --git a/src/lib/agents/search/researcher/actions/scrapeURL.ts b/src/lib/agents/search/researcher/actions/scrapeURL.ts index 3d3173e..c702a70 100644 --- a/src/lib/agents/search/researcher/actions/scrapeURL.ts +++ b/src/lib/agents/search/researcher/actions/scrapeURL.ts @@ -15,7 +15,7 @@ Use this tool to scrape and extract content from the provided URLs. This is usef You should only call this tool when the user has specifically requested information from certain web pages, never call this yourself to get extra information without user instruction. For example, if the user says "Please summarize the content of https://example.com/article", you can call this tool with that URL to get the content and then provide the summary or "What does X mean according to https://example.com/page", you can call this tool with that URL to get the content and provide the explanation. -` +`; const scrapeURLAction: ResearchAction<typeof schema> = { name: 'scrape_url', From ca4809f0f25b1b83761b91e51c73a32c49595f98 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 14 Dec 2025 19:32:09 +0530 Subject: [PATCH 127/196] Update manager.ts --- src/lib/uploads/manager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/uploads/manager.ts b/src/lib/uploads/manager.ts index 81ef28e..57a8101 100644 --- a/src/lib/uploads/manager.ts +++ b/src/lib/uploads/manager.ts @@ -91,7 +91,7 @@ class UploadManager { case 'text/plain': const content = fs.readFileSync(filePath, 'utf-8'); - const splittedText = splitText(content, 256, 64) + const splittedText = splitText(content, 512, 128) const embeddings = await this.embeddingModel.embedText(splittedText) if (embeddings.length !== splittedText.length) { @@ -121,7 +121,7 @@ class UploadManager { const pdfText = await parser.getText().then(res => res.text) - const pdfSplittedText = splitText(pdfText, 256, 64) + const pdfSplittedText = splitText(pdfText, 512, 128) const pdfEmbeddings = await this.embeddingModel.embedText(pdfSplittedText) if (pdfEmbeddings.length !== pdfSplittedText.length) { @@ -147,7 +147,7 @@ class UploadManager { const docText = await officeParser.parseOfficeAsync(docBuffer) - const docSplittedText = splitText(docText, 256, 64) + const docSplittedText = splitText(docText, 512, 128) const docEmbeddings = await this.embeddingModel.embedText(docSplittedText) if (docEmbeddings.length !== docSplittedText.length) { From a3065d58ef5266d67a0cfb8c8ac083803377a0f7 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:46:11 +0530 Subject: [PATCH 128/196] feat(package): add motion, react tooltip, phosphor icons --- package.json | 5 +- yarn.lock | 380 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 380 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 52ce2c5..ba1162f 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,14 @@ "@headlessui/tailwindcss": "^0.2.2", "@huggingface/transformers": "^3.7.5", "@icons-pack/react-simple-icons": "^12.3.0", + "@phosphor-icons/react": "^2.1.10", + "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/typography": "^0.5.12", "@types/jspdf": "^2.0.0", "axios": "^1.8.3", "better-sqlite3": "^11.9.1", "clsx": "^2.1.0", "drizzle-orm": "^0.40.1", - "framer-motion": "^12.23.25", "js-tiktoken": "^1.0.21", "jspdf": "^3.0.4", "lightweight-charts": "^5.0.9", @@ -29,6 +30,7 @@ "mammoth": "^1.9.1", "markdown-to-jsx": "^7.7.2", "mathjs": "^15.1.0", + "motion": "^12.23.26", "next": "^16.0.7", "next-themes": "^0.3.0", "officeparser": "^5.2.2", @@ -38,6 +40,7 @@ "pdf-parse": "^2.4.5", "react": "^18", "react-dom": "^18", + "react-syntax-highlighter": "^16.1.0", "react-text-to-speech": "^0.14.5", "react-textarea-autosize": "^8.5.3", "rfc6902": "^5.1.2", diff --git a/yarn.lock b/yarn.lock index f0c27ab..1dbec13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -336,6 +336,13 @@ dependencies: "@floating-ui/utils" "^0.2.8" +"@floating-ui/core@^1.7.3": + version "1.7.3" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.3.tgz#462d722f001e23e46d86fd2bd0d21b7693ccb8b7" + integrity sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w== + dependencies: + "@floating-ui/utils" "^0.2.10" + "@floating-ui/dom@^1.0.0": version "1.6.12" resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.12.tgz#6333dcb5a8ead3b2bf82f33d6bc410e95f54e556" @@ -344,6 +351,21 @@ "@floating-ui/core" "^1.6.0" "@floating-ui/utils" "^0.2.8" +"@floating-ui/dom@^1.7.4": + version "1.7.4" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.4.tgz#ee667549998745c9c3e3e84683b909c31d6c9a77" + integrity sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA== + dependencies: + "@floating-ui/core" "^1.7.3" + "@floating-ui/utils" "^0.2.10" + +"@floating-ui/react-dom@^2.0.0": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.6.tgz#189f681043c1400561f62972f461b93f01bf2231" + integrity sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw== + dependencies: + "@floating-ui/dom" "^1.7.4" + "@floating-ui/react-dom@^2.1.2": version "2.1.2" resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.2.tgz#a1349bbf6a0e5cb5ded55d023766f20a4d439a31" @@ -360,6 +382,11 @@ "@floating-ui/utils" "^0.2.8" tabbable "^6.0.0" +"@floating-ui/utils@^0.2.10": + version "0.2.10" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c" + integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ== + "@floating-ui/utils@^0.2.8": version "0.2.8" resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62" @@ -962,6 +989,11 @@ resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.9.2.tgz#217a5d349f3655b8e286be447e0ed1eae063a78f" integrity sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog== +"@phosphor-icons/react@^2.1.10": + version "2.1.10" + resolved "https://registry.yarnpkg.com/@phosphor-icons/react/-/react-2.1.10.tgz#3a97ec5b7a4b8d53afeb29125bc17e74ed2daf92" + integrity sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA== + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -1020,6 +1052,168 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@radix-ui/primitive@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.3.tgz#e2dbc13bdc5e4168f4334f75832d7bdd3e2de5ba" + integrity sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg== + +"@radix-ui/react-arrow@1.1.7": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz#e14a2657c81d961598c5e72b73dd6098acc04f09" + integrity sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w== + dependencies: + "@radix-ui/react-primitive" "2.1.3" + +"@radix-ui/react-compose-refs@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30" + integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg== + +"@radix-ui/react-context@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36" + integrity sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA== + +"@radix-ui/react-dismissable-layer@1.1.11": + version "1.1.11" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz#e33ab6f6bdaa00f8f7327c408d9f631376b88b37" + integrity sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg== + dependencies: + "@radix-ui/primitive" "1.1.3" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-escape-keydown" "1.1.1" + +"@radix-ui/react-id@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.1.tgz#1404002e79a03fe062b7e3864aa01e24bd1471f7" + integrity sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.1" + +"@radix-ui/react-popper@1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.8.tgz#a79f39cdd2b09ab9fb50bf95250918422c4d9602" + integrity sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw== + dependencies: + "@floating-ui/react-dom" "^2.0.0" + "@radix-ui/react-arrow" "1.1.7" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-use-rect" "1.1.1" + "@radix-ui/react-use-size" "1.1.1" + "@radix-ui/rect" "1.1.1" + +"@radix-ui/react-portal@1.1.9": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz#14c3649fe48ec474ac51ed9f2b9f5da4d91c4472" + integrity sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ== + dependencies: + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-layout-effect" "1.1.1" + +"@radix-ui/react-presence@1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.5.tgz#5d8f28ac316c32f078afce2996839250c10693db" + integrity sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-use-layout-effect" "1.1.1" + +"@radix-ui/react-primitive@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz#db9b8bcff49e01be510ad79893fb0e4cda50f1bc" + integrity sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ== + dependencies: + "@radix-ui/react-slot" "1.2.3" + +"@radix-ui/react-slot@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz#502d6e354fc847d4169c3bc5f189de777f68cfe1" + integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + +"@radix-ui/react-tooltip@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz#3f50267e25bccfc9e20bb3036bfd9ab4c2c30c2c" + integrity sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg== + dependencies: + "@radix-ui/primitive" "1.1.3" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-dismissable-layer" "1.1.11" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-popper" "1.2.8" + "@radix-ui/react-portal" "1.1.9" + "@radix-ui/react-presence" "1.1.5" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-slot" "1.2.3" + "@radix-ui/react-use-controllable-state" "1.2.2" + "@radix-ui/react-visually-hidden" "1.2.3" + +"@radix-ui/react-use-callback-ref@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz#62a4dba8b3255fdc5cc7787faeac1c6e4cc58d40" + integrity sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg== + +"@radix-ui/react-use-controllable-state@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz#905793405de57d61a439f4afebbb17d0645f3190" + integrity sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg== + dependencies: + "@radix-ui/react-use-effect-event" "0.0.2" + "@radix-ui/react-use-layout-effect" "1.1.1" + +"@radix-ui/react-use-effect-event@0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz#090cf30d00a4c7632a15548512e9152217593907" + integrity sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.1" + +"@radix-ui/react-use-escape-keydown@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz#b3fed9bbea366a118f40427ac40500aa1423cc29" + integrity sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g== + dependencies: + "@radix-ui/react-use-callback-ref" "1.1.1" + +"@radix-ui/react-use-layout-effect@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz#0c4230a9eed49d4589c967e2d9c0d9d60a23971e" + integrity sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ== + +"@radix-ui/react-use-rect@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz#01443ca8ed071d33023c1113e5173b5ed8769152" + integrity sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w== + dependencies: + "@radix-ui/rect" "1.1.1" + +"@radix-ui/react-use-size@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz#6de276ffbc389a537ffe4316f5b0f24129405b37" + integrity sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.1" + +"@radix-ui/react-visually-hidden@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz#a8c38c8607735dc9f05c32f87ab0f9c2b109efbf" + integrity sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug== + dependencies: + "@radix-ui/react-primitive" "2.1.3" + +"@radix-ui/rect@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.1.tgz#78244efe12930c56fd255d7923865857c41ac8cb" + integrity sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw== + "@react-aria/focus@^3.17.1": version "3.18.4" resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.18.4.tgz#a6e95896bc8680d1b5bcd855e983fc2c195a1a55" @@ -1117,6 +1311,13 @@ dependencies: "@types/node" "*" +"@types/hast@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + dependencies: + "@types/unist" "*" + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -1160,6 +1361,11 @@ resolved "https://registry.yarnpkg.com/@types/pdf-parse/-/pdf-parse-1.1.4.tgz#21a539efd2f16009d08aeed3350133948b5d7ed1" integrity sha512-+gbBHbNCVGGYw1S9lAIIvrHW47UYOhMIFUsJcMkMrzy1Jf0vulBN3XQIjPgnoOXveMuHnF3b57fXROnY/Or7eg== +"@types/prismjs@^1.0.0": + version "1.26.5" + resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.5.tgz#72499abbb4c4ec9982446509d2f14fb8483869d6" + integrity sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ== + "@types/prop-types@*": version "15.7.12" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" @@ -1195,6 +1401,16 @@ resolved "https://registry.yarnpkg.com/@types/turndown/-/turndown-5.0.6.tgz#42a27397298a312d6088f29c0ff4819c518c1ecb" integrity sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg== +"@types/unist@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + +"@types/unist@^2.0.0": + version "2.0.11" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" + integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== + "@typescript-eslint/parser@^5.4.2 || ^6.0.0": version "6.21.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" @@ -1661,6 +1877,21 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +character-entities-legacy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" + integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + +character-entities@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== + +character-reference-invalid@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" + integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== + chokidar@^3.5.3: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -1720,6 +1951,11 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +comma-separated-tokens@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" + integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + commander@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" @@ -1832,6 +2068,13 @@ decimal.js@^10.4.3: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a" integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg== +decode-named-character-reference@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz#25c32ae6dd5e21889549d40f676030e9514cc0ed" + integrity sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q== + dependencies: + character-entities "^2.0.0" + decompress-response@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" @@ -2473,6 +2716,13 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fault@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13" + integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA== + dependencies: + format "^0.2.0" + "fetch-mock-cache@npm:fetch-mock-cache@^2.1.3": version "2.3.1" resolved "https://registry.yarnpkg.com/fetch-mock-cache/-/fetch-mock-cache-2.3.1.tgz#1018f5fc2f91cf2511abcea8a5e3a3b05e2d02bf" @@ -2592,6 +2842,11 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +format@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" + integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== + fraction.js@^4.3.7: version "4.3.7" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" @@ -2602,10 +2857,10 @@ fraction.js@^5.2.1: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-5.3.4.tgz#8c0fcc6a9908262df4ed197427bdeef563e0699a" integrity sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ== -framer-motion@^12.23.25: - version "12.23.25" - resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.23.25.tgz#32d717f8b172c2673f573c0805ecc37d017441b2" - integrity sha512-gUHGl2e4VG66jOcH0JHhuJQr6ZNwrET9g31ZG0xdXzT0CznP7fHX4P8Bcvuc4MiUB90ysNnWX2ukHRIggkl6hQ== +framer-motion@^12.23.26: + version "12.23.26" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.23.26.tgz#2a684e9b156118e1c4989d7fc9327def83480391" + integrity sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA== dependencies: motion-dom "^12.23.23" motion-utils "^12.23.6" @@ -2854,6 +3109,34 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: dependencies: function-bind "^1.1.2" +hast-util-parse-selector@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27" + integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A== + dependencies: + "@types/hast" "^3.0.0" + +hastscript@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-9.0.1.tgz#dbc84bef6051d40084342c229c451cd9dc567dff" + integrity sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w== + dependencies: + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + hast-util-parse-selector "^4.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + +highlight.js@^10.4.1, highlight.js@~10.7.0: + version "10.7.3" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" + integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== + +highlightjs-vue@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz#fdfe97fbea6354e70ee44e3a955875e114db086d" + integrity sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA== + html2canvas@^1.0.0-rc.5: version "1.4.1" resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" @@ -2929,6 +3212,19 @@ iobuffer@^5.3.2: resolved "https://registry.yarnpkg.com/iobuffer/-/iobuffer-5.4.0.tgz#f85dff957fd0579257472f0a4cfe5ed3430e63e1" integrity sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA== +is-alphabetical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" + integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== + +is-alphanumerical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" + integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== + dependencies: + is-alphabetical "^2.0.0" + is-decimal "^2.0.0" + is-array-buffer@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" @@ -2992,6 +3288,11 @@ is-date-object@^1.0.1, is-date-object@^1.0.5: dependencies: has-tostringtag "^1.0.0" +is-decimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" + integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -3023,6 +3324,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-hexadecimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" + integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== + is-map@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" @@ -3345,6 +3651,14 @@ lop@^0.4.2: option "~0.2.1" underscore "^1.13.1" +lowlight@^1.17.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.20.0.tgz#ddb197d33462ad0d93bf19d17b6c301aa3941888" + integrity sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw== + dependencies: + fault "^1.0.0" + highlight.js "~10.7.0" + lru-cache@^10.2.0: version "10.2.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" @@ -3495,6 +3809,14 @@ motion-utils@^12.23.6: resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.23.6.tgz#fafef80b4ea85122dd0d6c599a0c63d72881f312" integrity sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ== +motion@^12.23.26: + version "12.23.26" + resolved "https://registry.yarnpkg.com/motion/-/motion-12.23.26.tgz#7309d3f13df43795b774fa98821c6ee5d6fab4f1" + integrity sha512-Ll8XhVxY8LXMVYTCfme27WH2GjBrCIzY4+ndr5QKxsK+YwCtOi2B/oBi5jcIbik5doXuWT/4KKDOVAZJkeY5VQ== + dependencies: + framer-motion "^12.23.26" + tslib "^2.4.0" + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -3778,6 +4100,19 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-entities@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.2.tgz#61d46f5ed28e4ee62e9ddc43d6b010188443f159" + integrity sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw== + dependencies: + "@types/unist" "^2.0.0" + character-entities-legacy "^3.0.0" + character-reference-invalid "^2.0.0" + decode-named-character-reference "^1.0.0" + is-alphanumerical "^2.0.0" + is-decimal "^2.0.0" + is-hexadecimal "^2.0.0" + partial-json@^0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/partial-json/-/partial-json-0.1.7.tgz#b735a89edb3e25f231a3c4caeaae71dc9f578605" @@ -3981,6 +4316,11 @@ prettier@^3.2.5: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== +prismjs@^1.30.0: + version "1.30.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.30.0.tgz#d9709969d9d4e16403f6f348c63553b19f0975a9" + integrity sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -4000,6 +4340,11 @@ prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" +property-information@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-7.1.0.tgz#b622e8646e02b580205415586b40804d3e8bfd5d" + integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ== + protobufjs@^7.2.4: version "7.5.4" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.4.tgz#885d31fe9c4b37f25d1bb600da30b1c5b37d286a" @@ -4083,6 +4428,18 @@ react-is@^16.13.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-syntax-highlighter@^16.1.0: + version "16.1.0" + resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-16.1.0.tgz#ebe0bb5ae7a3540859212cedafd767f0189c516c" + integrity sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg== + dependencies: + "@babel/runtime" "^7.28.4" + highlight.js "^10.4.1" + highlightjs-vue "^1.0.0" + lowlight "^1.17.0" + prismjs "^1.30.0" + refractor "^5.0.0" + react-text-to-speech@^0.14.5: version "0.14.5" resolved "https://registry.yarnpkg.com/react-text-to-speech/-/react-text-to-speech-0.14.5.tgz#f918786ab283311535682011045bd49777193300" @@ -4171,6 +4528,16 @@ reflect.getprototypeof@^1.0.4: globalthis "^1.0.3" which-builtin-type "^1.1.3" +refractor@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/refractor/-/refractor-5.0.0.tgz#85daf0448a6d947f5361796eb22c31733d61d904" + integrity sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw== + dependencies: + "@types/hast" "^3.0.0" + "@types/prismjs" "^1.0.0" + hastscript "^9.0.0" + parse-entities "^4.0.0" + regenerator-runtime@^0.13.7: version "0.13.11" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" @@ -4512,6 +4879,11 @@ source-map@^0.6.0: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + sprintf-js@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" From b90b92079b25e7246041dee050c25b4ce2dcba92 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:46:46 +0530 Subject: [PATCH 129/196] feat(chat-route): accept sources --- src/app/api/chat/route.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 23104e0..b5d4ead 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -4,6 +4,7 @@ import { ModelWithProvider } from '@/lib/models/types'; import SearchAgent from '@/lib/agents/search'; import SessionManager from '@/lib/session'; import { ChatTurnMessage } from '@/lib/types'; +import { SearchSources } from '@/lib/agents/search/types'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -31,7 +32,7 @@ const bodySchema = z.object({ optimizationMode: z.enum(['speed', 'balanced', 'quality'], { message: 'Optimization mode must be one of: speed, balanced, quality', }), - focusMode: z.string().min(1, 'Focus mode is required'), + sources: z.array(z.string()).optional().default([]), history: z .array(z.tuple([z.string(), z.string()])) .optional() @@ -42,7 +43,6 @@ const bodySchema = z.object({ systemInstructions: z.string().nullable().optional().default(''), }); -type Message = z.infer<typeof messageSchema>; type Body = z.infer<typeof bodySchema>; const safeValidateBody = (data: unknown) => { @@ -203,7 +203,7 @@ export const POST = async (req: Request) => { config: { llm, embedding: embedding, - sources: ['web'], + sources: body.sources as SearchSources[], mode: body.optimizationMode, fileIds: body.files, }, From e6c8a0aa6f48d583de6a61322ba4f9a5a1f00d54 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:47:01 +0530 Subject: [PATCH 130/196] Add antialiased class to body element --- src/app/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e9fd8c7..535a0e0 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -34,7 +34,7 @@ export default function RootLayout({ return ( <html className="h-full" lang="en" suppressHydrationWarning> - <body className={cn('h-full', montserrat.className)}> + <body className={cn('h-full antialiased', montserrat.className)}> <ThemeProvider> {setupComplete ? ( <ChatProvider> From bd3c5f895a776430ee3ce681c9f4088e9f0b056b Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:47:21 +0530 Subject: [PATCH 131/196] feat(message-input-actions): remove copilot, focus selector --- .../MessageInputActions/Copilot.tsx | 43 ------ src/components/MessageInputActions/Focus.tsx | 123 ------------------ 2 files changed, 166 deletions(-) delete mode 100644 src/components/MessageInputActions/Copilot.tsx delete mode 100644 src/components/MessageInputActions/Focus.tsx diff --git a/src/components/MessageInputActions/Copilot.tsx b/src/components/MessageInputActions/Copilot.tsx deleted file mode 100644 index 5a3e476..0000000 --- a/src/components/MessageInputActions/Copilot.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { cn } from '@/lib/utils'; -import { Switch } from '@headlessui/react'; - -const CopilotToggle = ({ - copilotEnabled, - setCopilotEnabled, -}: { - copilotEnabled: boolean; - setCopilotEnabled: (enabled: boolean) => void; -}) => { - return ( - <div className="group flex flex-row items-center space-x-1 active:scale-95 duration-200 transition cursor-pointer"> - <Switch - checked={copilotEnabled} - onChange={setCopilotEnabled} - className="bg-light-secondary dark:bg-dark-secondary border border-light-200/70 dark:border-dark-200 relative inline-flex h-5 w-10 sm:h-6 sm:w-11 items-center rounded-full" - > - <span className="sr-only">Copilot</span> - <span - className={cn( - copilotEnabled - ? 'translate-x-6 bg-[#24A0ED]' - : 'translate-x-1 bg-black/50 dark:bg-white/50', - 'inline-block h-3 w-3 sm:h-4 sm:w-4 transform rounded-full transition-all duration-200', - )} - /> - </Switch> - <p - onClick={() => setCopilotEnabled(!copilotEnabled)} - className={cn( - 'text-xs font-medium transition-colors duration-150 ease-in-out', - copilotEnabled - ? 'text-[#24A0ED]' - : 'text-black/50 dark:text-white/50 group-hover:text-black dark:group-hover:text-white', - )} - > - Copilot - </p> - </div> - ); -}; - -export default CopilotToggle; diff --git a/src/components/MessageInputActions/Focus.tsx b/src/components/MessageInputActions/Focus.tsx deleted file mode 100644 index 58b1a39..0000000 --- a/src/components/MessageInputActions/Focus.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { - BadgePercent, - ChevronDown, - Globe, - Pencil, - ScanEye, - SwatchBook, -} from 'lucide-react'; -import { cn } from '@/lib/utils'; -import { - Popover, - PopoverButton, - PopoverPanel, - Transition, -} from '@headlessui/react'; -import { SiReddit, SiYoutube } from '@icons-pack/react-simple-icons'; -import { Fragment } from 'react'; -import { useChat } from '@/lib/hooks/useChat'; - -const focusModes = [ - { - key: 'webSearch', - title: 'All', - description: 'Searches across all of the internet', - icon: <Globe size={16} />, - }, - { - key: 'academicSearch', - title: 'Academic', - description: 'Search in published academic papers', - icon: <SwatchBook size={16} />, - }, - { - key: 'writingAssistant', - title: 'Writing', - description: 'Chat without searching the web', - icon: <Pencil size={16} />, - }, - { - key: 'wolframAlphaSearch', - title: 'Wolfram Alpha', - description: 'Computational knowledge engine', - icon: <BadgePercent size={16} />, - }, - { - key: 'youtubeSearch', - title: 'Youtube', - description: 'Search and watch videos', - icon: <SiYoutube className="h-[16px] w-auto mr-0.5" />, - }, - { - key: 'redditSearch', - title: 'Reddit', - description: 'Search for discussions and opinions', - icon: <SiReddit className="h-[16px] w-auto mr-0.5" />, - }, -]; - -const Focus = () => { - const { focusMode, setFocusMode } = useChat(); - - return ( - <Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg"> - <PopoverButton - type="button" - className="active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none headless-open:text-black dark:headless-open:text-white text-black/50 dark:text-white/50 active:scale-95 transition duration-200 hover:text-black dark:hover:text-white" - > - {focusMode !== 'webSearch' ? ( - <div className="flex flex-row items-center space-x-1"> - {focusModes.find((mode) => mode.key === focusMode)?.icon} - </div> - ) : ( - <div className="flex flex-row items-center space-x-1"> - <Globe size={16} /> - </div> - )} - </PopoverButton> - <Transition - as={Fragment} - enter="transition ease-out duration-150" - enterFrom="opacity-0 translate-y-1" - enterTo="opacity-100 translate-y-0" - leave="transition ease-in duration-150" - leaveFrom="opacity-100 translate-y-0" - leaveTo="opacity-0 translate-y-1" - > - <PopoverPanel className="absolute z-10 w-64 md:w-[500px] -right-4"> - <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto"> - {focusModes.map((mode, i) => ( - <PopoverButton - onClick={() => setFocusMode(mode.key)} - key={i} - className={cn( - 'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-2 duration-200 cursor-pointer transition focus:outline-none', - focusMode === mode.key - ? 'bg-light-secondary dark:bg-dark-secondary' - : 'hover:bg-light-secondary dark:hover:bg-dark-secondary', - )} - > - <div - className={cn( - 'flex flex-row items-center space-x-1', - focusMode === mode.key - ? 'text-[#24A0ED]' - : 'text-black dark:text-white', - )} - > - {mode.icon} - <p className="text-sm font-medium">{mode.title}</p> - </div> - <p className="text-black/70 dark:text-white/70 text-xs"> - {mode.description} - </p> - </PopoverButton> - ))} - </div> - </PopoverPanel> - </Transition> - </Popover> - ); -}; - -export default Focus; From 9dd670f46aded0c53d4b5f6f2f3f2e7c24a6a111 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:47:38 +0530 Subject: [PATCH 132/196] feat(chat-hook): handle sources --- src/lib/hooks/useChat.tsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/lib/hooks/useChat.tsx b/src/lib/hooks/useChat.tsx index 34c7095..6330c80 100644 --- a/src/lib/hooks/useChat.tsx +++ b/src/lib/hooks/useChat.tsx @@ -34,7 +34,7 @@ type ChatContext = { chatHistory: [string, string][]; files: File[]; fileIds: string[]; - focusMode: string; + sources: string[]; chatId: string | undefined; optimizationMode: string; isMessagesLoaded: boolean; @@ -48,7 +48,7 @@ type ChatContext = { researchEnded: boolean; setResearchEnded: (ended: boolean) => void; setOptimizationMode: (mode: string) => void; - setFocusMode: (mode: string) => void; + setSources: (sources: string[]) => void; setFiles: (files: File[]) => void; setFileIds: (fileIds: string[]) => void; sendMessage: ( @@ -176,7 +176,7 @@ const loadMessages = async ( setMessages: (messages: Message[]) => void, setIsMessagesLoaded: (loaded: boolean) => void, setChatHistory: (history: [string, string][]) => void, - setFocusMode: (mode: string) => void, + setSources: (sources: string[]) => void, setNotFound: (notFound: boolean) => void, setFiles: (files: File[]) => void, setFileIds: (fileIds: string[]) => void, @@ -234,7 +234,7 @@ const loadMessages = async ( setFileIds(files.map((file: File) => file.fileId)); setChatHistory(history); - setFocusMode(data.chat.focusMode); + setSources(data.chat.sources); setIsMessagesLoaded(true); }; @@ -243,7 +243,7 @@ export const chatContext = createContext<ChatContext>({ chatId: '', fileIds: [], files: [], - focusMode: '', + sources: [], hasError: false, isMessagesLoaded: false, isReady: false, @@ -260,7 +260,7 @@ export const chatContext = createContext<ChatContext>({ sendMessage: async () => {}, setFileIds: () => {}, setFiles: () => {}, - setFocusMode: () => {}, + setSources: () => {}, setOptimizationMode: () => {}, setChatModelProvider: () => {}, setEmbeddingModelProvider: () => {}, @@ -286,7 +286,7 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { const [files, setFiles] = useState<File[]>([]); const [fileIds, setFileIds] = useState<string[]>([]); - const [focusMode, setFocusMode] = useState('webSearch'); + const [sources, setSources] = useState<string[]>(['web']); const [optimizationMode, setOptimizationMode] = useState('speed'); const [isMessagesLoaded, setIsMessagesLoaded] = useState(false); @@ -436,7 +436,7 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { setMessages, setIsMessagesLoaded, setChatHistory, - setFocusMode, + setSources, setNotFound, setFiles, setFileIds, @@ -722,7 +722,7 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { }, chatId: chatId!, files: fileIds, - focusMode: focusMode, + sources: sources, optimizationMode: optimizationMode, history: rewrite ? chatHistory.slice(0, messageIndex === -1 ? undefined : messageIndex) @@ -774,7 +774,7 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { chatHistory, files, fileIds, - focusMode, + sources, chatId, hasError, isMessagesLoaded, @@ -785,7 +785,7 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { optimizationMode, setFileIds, setFiles, - setFocusMode, + setSources, setOptimizationMode, rewrite, sendMessage, From f9cc97ffb5e51e67393f5a94d118d1ad38ae43cc Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:47:48 +0530 Subject: [PATCH 133/196] feat(message-input-actions): add sources --- .../MessageInputActions/Sources.tsx | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/components/MessageInputActions/Sources.tsx diff --git a/src/components/MessageInputActions/Sources.tsx b/src/components/MessageInputActions/Sources.tsx new file mode 100644 index 0000000..e996b6e --- /dev/null +++ b/src/components/MessageInputActions/Sources.tsx @@ -0,0 +1,93 @@ +import { useChat } from '@/lib/hooks/useChat'; +import { + Popover, + PopoverButton, + PopoverPanel, + Switch, +} from '@headlessui/react'; +import { + GlobeIcon, + GraduationCapIcon, + NetworkIcon, +} from '@phosphor-icons/react'; +import { AnimatePresence, motion } from 'motion/react'; + +const sourcesList = [ + { + name: 'Web', + key: 'web', + icon: <GlobeIcon className="h-[16px] w-auto" />, + }, + { + name: 'Academic', + key: 'academic', + icon: <GraduationCapIcon className="h-[16px] w-auto" />, + }, + { + name: 'Social', + key: 'social', + icon: <NetworkIcon className="h-[16px] w-auto" />, + }, +]; + +const Sources = () => { + const { sources, setSources } = useChat(); + + return ( + <Popover className="relative"> + {({ open }) => ( + <> + <PopoverButton className="flex items-center justify-center active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none text-black/50 dark:text-white/50 active:scale-95 transition duration-200 hover:text-black dark:hover:text-white"> + <GlobeIcon className="h-[18px] w-auto" /> + </PopoverButton> + <AnimatePresence> + {open && ( + <PopoverPanel + static + className="absolute z-10 w-64 md:w-[225px] right-0" + > + <motion.div + initial={{ opacity: 0, scale: 0.9 }} + animate={{ opacity: 1, scale: 1 }} + exit={{ opacity: 0, scale: 0.9 }} + transition={{ duration: 0.1, ease: 'easeOut' }} + className="origin-top-right flex flex-col bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-1 max-h-[200px] md:max-h-none overflow-y-auto shadow-md shadow-dark-50" + > + {sourcesList.map((source, i) => ( + <div + key={i} + className="flex flex-row justify-between hover:bg-white-100 hover:dark:bg-dark-100 rounded-md py-3 px-2 cursor-pointer" + onClick={() => { + if (!sources.includes(source.key)) { + setSources([...sources, source.key]); + } else { + setSources(sources.filter((s) => s !== source.key)); + } + }} + > + <div className="flex flex-row space-x-1.5 text-white/80"> + {source.icon} + <p className="text-xs">{source.name}</p> + </div> + <Switch + checked={sources.includes(source.key)} + className="group relative flex h-4 w-7 shrink-0 cursor-pointer rounded-full bg-white/10 p-0.5 duration-200 ease-in-out focus:outline-none transition-colors disabled:opacity-60 disabled:cursor-not-allowed data-[checked]:bg-sky-500" + > + <span + aria-hidden="true" + className="pointer-events-none inline-block size-3 translate-x-[1px] group-data-[checked]:translate-x-3 rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out" + /> + </Switch> + </div> + ))} + </motion.div> + </PopoverPanel> + )} + </AnimatePresence> + </> + )} + </Popover> + ); +}; + +export default Sources; From 4bdb90e1501bd15bc62baa454d3d89e2b0373d15 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:48:14 +0530 Subject: [PATCH 134/196] feat(message-input-actions): update to use motion, improve animations --- .../MessageInputActions/ChatModelSelector.tsx | 232 +++++++++--------- .../MessageInputActions/Optimization.tsx | 72 +++--- 2 files changed, 156 insertions(+), 148 deletions(-) diff --git a/src/components/MessageInputActions/ChatModelSelector.tsx b/src/components/MessageInputActions/ChatModelSelector.tsx index ddad581..4d75166 100644 --- a/src/components/MessageInputActions/ChatModelSelector.tsx +++ b/src/components/MessageInputActions/ChatModelSelector.tsx @@ -2,15 +2,11 @@ import { Cpu, Loader2, Search } from 'lucide-react'; import { cn } from '@/lib/utils'; -import { - Popover, - PopoverButton, - PopoverPanel, - Transition, -} from '@headlessui/react'; -import { Fragment, useEffect, useMemo, useState } from 'react'; +import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'; +import { useEffect, useMemo, useState } from 'react'; import { MinimalProvider } from '@/lib/models/types'; import { useChat } from '@/lib/hooks/useChat'; +import { AnimatePresence, motion } from 'motion/react'; const ModelSelector = () => { const [providers, setProviders] = useState<MinimalProvider[]>([]); @@ -79,119 +75,127 @@ const ModelSelector = () => { return ( <Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg"> - <PopoverButton - type="button" - className="active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none headless-open:text-black dark:headless-open:text-white text-black/50 dark:text-white/50 active:scale-95 transition duration-200 hover:text-black dark:hover:text-white" - > - <Cpu size={16} className="text-sky-500" /> - </PopoverButton> - <Transition - as={Fragment} - enter="transition ease-out duration-100" - enterFrom="opacity-0 translate-y-1" - enterTo="opacity-100 translate-y-0" - leave="transition ease-in duration-100" - leaveFrom="opacity-100 translate-y-0" - leaveTo="opacity-0 translate-y-1" - > - <PopoverPanel className="absolute z-10 w-[230px] sm:w-[270px] md:w-[300px] -right-4"> - <div className="bg-light-primary dark:bg-dark-primary max-h-[300px] sm:max-w-none border rounded-lg border-light-200 dark:border-dark-200 w-full flex flex-col shadow-lg overflow-hidden"> - <div className="p-4 border-b border-light-200 dark:border-dark-200"> - <div className="relative"> - <Search - size={16} - className="absolute left-3 top-1/2 -translate-y-1/2 text-black/40 dark:text-white/40" - /> - <input - type="text" - placeholder="Search models..." - value={searchQuery} - onChange={(e) => setSearchQuery(e.target.value)} - className="w-full pl-9 pr-3 py-2 bg-light-secondary dark:bg-dark-secondary rounded-lg placeholder:text-sm text-sm text-black dark:text-white placeholder:text-black/40 dark:placeholder:text-white/40 focus:outline-none focus:ring-2 focus:ring-sky-500/20 border border-transparent focus:border-sky-500/30 transition duration-200" - /> - </div> - </div> + {({ open }) => ( + <> + <PopoverButton + type="button" + className="active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none headless-open:text-black dark:headless-open:text-white text-black/50 dark:text-white/50 active:scale-95 transition duration-200 hover:text-black dark:hover:text-white" + > + <Cpu size={16} className="text-sky-500" /> + </PopoverButton> + <AnimatePresence> + {open && ( + <PopoverPanel + className="absolute z-10 w-[230px] sm:w-[270px] md:w-[300px] right-0" + static + > + <motion.div + initial={{ opacity: 0, scale: 0.9 }} + animate={{ opacity: 1, scale: 1 }} + exit={{ opacity: 0, scale: 0.9 }} + transition={{ duration: 0.1, ease: 'easeOut' }} + className="origin-top-right bg-light-primary dark:bg-dark-primary max-h-[300px] sm:max-w-none border rounded-lg border-light-200 dark:border-dark-200 w-full flex flex-col shadow-lg overflow-hidden" + > + <div className="p-2 border-b border-light-200 dark:border-dark-200"> + <div className="relative"> + <Search + size={16} + className="absolute left-3 top-1/2 -translate-y-1/2 text-black/40 dark:text-white/40" + /> + <input + type="text" + placeholder="Search models..." + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + className="w-full pl-8 pr-3 py-2 bg-light-secondary dark:bg-dark-secondary rounded-lg placeholder:text-xs placeholder:-translate-y-[1.5px] text-xs text-black dark:text-white placeholder:text-black/40 dark:placeholder:text-white/40 focus:outline-none border border-transparent transition duration-200" + /> + </div> + </div> - <div className="max-h-[320px] overflow-y-auto"> - {isLoading ? ( - <div className="flex items-center justify-center py-16"> - <Loader2 - className="animate-spin text-black/40 dark:text-white/40" - size={24} - /> - </div> - ) : filteredProviders.length === 0 ? ( - <div className="text-center py-16 px-4 text-black/60 dark:text-white/60 text-sm"> - {searchQuery - ? 'No models found' - : 'No chat models configured'} - </div> - ) : ( - <div className="flex flex-col"> - {filteredProviders.map((provider, providerIndex) => ( - <div key={provider.id}> - <div className="px-4 py-2.5 sticky top-0 bg-light-primary dark:bg-dark-primary border-b border-light-200/50 dark:border-dark-200/50"> - <p className="text-xs text-black/50 dark:text-white/50 uppercase tracking-wider"> - {provider.name} - </p> + <div className="max-h-[320px] overflow-y-auto"> + {isLoading ? ( + <div className="flex items-center justify-center py-16"> + <Loader2 + className="animate-spin text-black/40 dark:text-white/40" + size={24} + /> </div> - - <div className="flex flex-col px-2 py-2 space-y-0.5"> - {provider.chatModels.map((model) => ( - <button - key={model.key} - onClick={() => - handleModelSelect(provider.id, model.key) - } - type="button" - className={cn( - 'px-3 py-2 flex items-center justify-between text-start duration-200 cursor-pointer transition rounded-lg group', - chatModelProvider?.providerId === provider.id && - chatModelProvider?.key === model.key - ? 'bg-light-secondary dark:bg-dark-secondary' - : 'hover:bg-light-secondary dark:hover:bg-dark-secondary', - )} - > - <div className="flex items-center space-x-2.5 min-w-0 flex-1"> - <Cpu - size={15} - className={cn( - 'shrink-0', - chatModelProvider?.providerId === - provider.id && - chatModelProvider?.key === model.key - ? 'text-sky-500' - : 'text-black/50 dark:text-white/50 group-hover:text-black/70 group-hover:dark:text-white/70', - )} - /> - <p - className={cn( - 'text-sm truncate', - chatModelProvider?.providerId === - provider.id && - chatModelProvider?.key === model.key - ? 'text-sky-500 font-medium' - : 'text-black/70 dark:text-white/70 group-hover:text-black dark:group-hover:text-white', - )} - > - {model.name} + ) : filteredProviders.length === 0 ? ( + <div className="text-center py-16 px-4 text-black/60 dark:text-white/60 text-sm"> + {searchQuery + ? 'No models found' + : 'No chat models configured'} + </div> + ) : ( + <div className="flex flex-col"> + {filteredProviders.map((provider, providerIndex) => ( + <div key={provider.id}> + <div className="px-4 py-2.5 sticky top-0 bg-light-primary dark:bg-dark-primary border-b border-light-200/50 dark:border-dark-200/50"> + <p className="text-xs text-black/50 dark:text-white/50 uppercase tracking-wider"> + {provider.name} </p> </div> - </button> + + <div className="flex flex-col px-2 py-2 space-y-0.5"> + {provider.chatModels.map((model) => ( + <button + key={model.key} + onClick={() => + handleModelSelect(provider.id, model.key) + } + type="button" + className={cn( + 'px-3 py-2 flex items-center justify-between text-start duration-200 cursor-pointer transition rounded-lg group', + chatModelProvider?.providerId === + provider.id && + chatModelProvider?.key === model.key + ? 'bg-light-secondary dark:bg-dark-secondary' + : 'hover:bg-light-secondary dark:hover:bg-dark-secondary', + )} + > + <div className="flex items-center space-x-2.5 min-w-0 flex-1"> + <Cpu + size={15} + className={cn( + 'shrink-0', + chatModelProvider?.providerId === + provider.id && + chatModelProvider?.key === model.key + ? 'text-sky-500' + : 'text-black/50 dark:text-white/50 group-hover:text-black/70 group-hover:dark:text-white/70', + )} + /> + <p + className={cn( + 'text-xs truncate', + chatModelProvider?.providerId === + provider.id && + chatModelProvider?.key === model.key + ? 'text-sky-500 font-medium' + : 'text-black/70 dark:text-white/70 group-hover:text-black dark:group-hover:text-white', + )} + > + {model.name} + </p> + </div> + </button> + ))} + </div> + + {providerIndex < filteredProviders.length - 1 && ( + <div className="h-px bg-light-200 dark:bg-dark-200" /> + )} + </div> ))} </div> - - {providerIndex < filteredProviders.length - 1 && ( - <div className="h-px bg-light-200 dark:bg-dark-200" /> - )} - </div> - ))} - </div> - )} - </div> - </div> - </PopoverPanel> - </Transition> + )} + </div> + </motion.div> + </PopoverPanel> + )} + </AnimatePresence> + </> + )} </Popover> ); }; diff --git a/src/components/MessageInputActions/Optimization.tsx b/src/components/MessageInputActions/Optimization.tsx index 1cdded8..7b87e6c 100644 --- a/src/components/MessageInputActions/Optimization.tsx +++ b/src/components/MessageInputActions/Optimization.tsx @@ -8,6 +8,7 @@ import { } from '@headlessui/react'; import { Fragment } from 'react'; import { useChat } from '@/lib/hooks/useChat'; +import { AnimatePresence, motion } from 'motion/react'; const OptimizationModes = [ { @@ -60,40 +61,43 @@ const Optimization = () => { /> </div> </PopoverButton> - <Transition - as={Fragment} - enter="transition ease-out duration-150" - enterFrom="opacity-0 translate-y-1" - enterTo="opacity-100 translate-y-0" - leave="transition ease-in duration-150" - leaveFrom="opacity-100 translate-y-0" - leaveTo="opacity-0 translate-y-1" - > - <PopoverPanel className="absolute z-10 w-64 md:w-[250px] left-0"> - <div className="flex flex-col gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto"> - {OptimizationModes.map((mode, i) => ( - <PopoverButton - onClick={() => setOptimizationMode(mode.key)} - key={i} - className={cn( - 'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-1 duration-200 cursor-pointer transition focus:outline-none', - optimizationMode === mode.key - ? 'bg-light-secondary dark:bg-dark-secondary' - : 'hover:bg-light-secondary dark:hover:bg-dark-secondary', - )} - > - <div className="flex flex-row items-center space-x-1 text-black dark:text-white"> - {mode.icon} - <p className="text-sm font-medium">{mode.title}</p> - </div> - <p className="text-black/70 dark:text-white/70 text-xs"> - {mode.description} - </p> - </PopoverButton> - ))} - </div> - </PopoverPanel> - </Transition> + <AnimatePresence> + {open && ( + <PopoverPanel + className="absolute z-10 w-64 md:w-[250px] left-0" + static + > + <motion.div + initial={{ opacity: 0, scale: 0.9 }} + animate={{ opacity: 1, scale: 1 }} + exit={{ opacity: 0, scale: 0.9 }} + transition={{ duration: 0.1, ease: 'easeOut' }} + className="origin-top-left flex flex-col space-y-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-2 max-h-[200px] md:max-h-none overflow-y-auto" + > + {OptimizationModes.map((mode, i) => ( + <PopoverButton + onClick={() => setOptimizationMode(mode.key)} + key={i} + className={cn( + 'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-1 duration-200 cursor-pointer transition focus:outline-none', + optimizationMode === mode.key + ? 'bg-light-secondary dark:bg-dark-secondary' + : 'hover:bg-light-secondary dark:hover:bg-dark-secondary', + )} + > + <div className="flex flex-row items-center space-x-1 text-black dark:text-white"> + {mode.icon} + <p className="text-xs font-medium">{mode.title}</p> + </div> + <p className="text-black/70 dark:text-white/70 text-xs"> + {mode.description} + </p> + </PopoverButton> + ))} + </motion.div> + </PopoverPanel> + )} + </AnimatePresence> </> )} </Popover> From 9b2c229e9c631cf2d28ce88bb064587e156f92c1 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:48:32 +0530 Subject: [PATCH 135/196] feat(message-input): remove copilot toggle --- src/components/MessageInput.tsx | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/src/components/MessageInput.tsx b/src/components/MessageInput.tsx index c682cf0..0776d1d 100644 --- a/src/components/MessageInput.tsx +++ b/src/components/MessageInput.tsx @@ -2,7 +2,6 @@ import { cn } from '@/lib/utils'; import { ArrowUp } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import TextareaAutosize from 'react-textarea-autosize'; -import CopilotToggle from './MessageInputActions/Copilot'; import AttachSmall from './MessageInputActions/AttachSmall'; import { useChat } from '@/lib/hooks/useChat'; @@ -78,11 +77,16 @@ const MessageInput = () => { placeholder="Ask a follow-up" /> {mode === 'single' && ( - <div className="flex flex-row items-center space-x-4"> - <CopilotToggle - copilotEnabled={copilotEnabled} - setCopilotEnabled={setCopilotEnabled} - /> + <button + disabled={message.trim().length === 0 || loading} + className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#e0e0dc79] dark:disabled:bg-[#ececec21] rounded-full p-2" + > + <ArrowUp className="bg-background" size={17} /> + </button> + )} + {mode === 'multi' && ( + <div className="flex flex-row items-center justify-between w-full pt-2"> + <AttachSmall /> <button disabled={message.trim().length === 0 || loading} className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#e0e0dc79] dark:disabled:bg-[#ececec21] rounded-full p-2" @@ -91,23 +95,6 @@ const MessageInput = () => { </button> </div> )} - {mode === 'multi' && ( - <div className="flex flex-row items-center justify-between w-full pt-2"> - <AttachSmall /> - <div className="flex flex-row items-center space-x-4"> - <CopilotToggle - copilotEnabled={copilotEnabled} - setCopilotEnabled={setCopilotEnabled} - /> - <button - disabled={message.trim().length === 0 || loading} - className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#e0e0dc79] dark:disabled:bg-[#ececec21] rounded-full p-2" - > - <ArrowUp className="bg-background" size={17} /> - </button> - </div> - </div> - )} </form> ); }; From 249889f55a4874816621c4cb86afc67312b2b108 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:49:11 +0530 Subject: [PATCH 136/196] feat(actions-registry): add sources, update web search to become active on web --- src/lib/agents/search/researcher/actions/registry.ts | 4 ++++ src/lib/agents/search/researcher/actions/webSearch.ts | 1 + src/lib/agents/search/researcher/index.ts | 2 ++ src/lib/agents/search/types.ts | 1 + 4 files changed, 8 insertions(+) diff --git a/src/lib/agents/search/researcher/actions/registry.ts b/src/lib/agents/search/researcher/actions/registry.ts index 8e0530c..a8de513 100644 --- a/src/lib/agents/search/researcher/actions/registry.ts +++ b/src/lib/agents/search/researcher/actions/registry.ts @@ -5,6 +5,7 @@ import { ClassifierOutput, ResearchAction, SearchAgentConfig, + SearchSources, } from '../../types'; class ActionRegistry { @@ -22,6 +23,7 @@ class ActionRegistry { classification: ClassifierOutput; fileIds: string[]; mode: SearchAgentConfig['mode']; + sources: SearchSources[]; }): ResearchAction[] { return Array.from( this.actions.values().filter((action) => action.enabled(config)), @@ -32,6 +34,7 @@ class ActionRegistry { classification: ClassifierOutput; fileIds: string[]; mode: SearchAgentConfig['mode']; + sources: SearchSources[]; }): Tool[] { const availableActions = this.getAvailableActions(config); @@ -46,6 +49,7 @@ class ActionRegistry { classification: ClassifierOutput; fileIds: string[]; mode: SearchAgentConfig['mode']; + sources: SearchSources[]; }): string { const availableActions = this.getAvailableActions(config); diff --git a/src/lib/agents/search/researcher/actions/webSearch.ts b/src/lib/agents/search/researcher/actions/webSearch.ts index 5eb3af4..3c712a2 100644 --- a/src/lib/agents/search/researcher/actions/webSearch.ts +++ b/src/lib/agents/search/researcher/actions/webSearch.ts @@ -82,6 +82,7 @@ const webSearchAction: ResearchAction<typeof actionSchema> = { return prompt; }, enabled: (config) => + config.sources.includes('web') && config.classification.classification.skipSearch === false, execute: async (input, additionalConfig) => { input.queries = input.queries.slice(0, 3); diff --git a/src/lib/agents/search/researcher/index.ts b/src/lib/agents/search/researcher/index.ts index 334ba74..814b0e3 100644 --- a/src/lib/agents/search/researcher/index.ts +++ b/src/lib/agents/search/researcher/index.ts @@ -23,6 +23,7 @@ class Researcher { classification: input.classification, fileIds: input.config.fileIds, mode: input.config.mode, + sources: input.config.sources, }); const availableActionsDescription = @@ -30,6 +31,7 @@ class Researcher { classification: input.classification, fileIds: input.config.fileIds, mode: input.config.mode, + sources: input.config.sources, }); const researchBlockId = crypto.randomUUID(); diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts index 0733de3..04cbe19 100644 --- a/src/lib/agents/search/types.ts +++ b/src/lib/agents/search/types.ts @@ -107,6 +107,7 @@ export interface ResearchAction< classification: ClassifierOutput; fileIds: string[]; mode: SearchAgentConfig['mode']; + sources: SearchSources[]; }) => boolean; execute: ( params: z.infer<TSchema>, From 1961e4e7070cf31a37c80b4d7b5241466e4d943d Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:49:26 +0530 Subject: [PATCH 137/196] feat(empty-chat-message-input): use sources --- src/components/EmptyChatMessageInput.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/EmptyChatMessageInput.tsx b/src/components/EmptyChatMessageInput.tsx index 770c647..6d159f9 100644 --- a/src/components/EmptyChatMessageInput.tsx +++ b/src/components/EmptyChatMessageInput.tsx @@ -1,7 +1,7 @@ import { ArrowRight } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import TextareaAutosize from 'react-textarea-autosize'; -import Focus from './MessageInputActions/Focus'; +import Sources from './MessageInputActions/Sources'; import Optimization from './MessageInputActions/Optimization'; import Attach from './MessageInputActions/Attach'; import { useChat } from '@/lib/hooks/useChat'; @@ -68,8 +68,8 @@ const EmptyChatMessageInput = () => { <Optimization /> <div className="flex flex-row items-center space-x-2"> <div className="flex flex-row items-center space-x-1"> + <Sources /> <ModelSelector /> - <Focus /> <Attach /> </div> <button From 6e086953b1155233f57ffd1bd880ad78cb1c64ca Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 17 Dec 2025 21:17:08 +0530 Subject: [PATCH 138/196] feat(agents): add academic and social search --- src/lib/agents/search/researcher/actions/academicSearch.ts | 0 src/lib/agents/search/researcher/actions/socialSearch.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/lib/agents/search/researcher/actions/academicSearch.ts create mode 100644 src/lib/agents/search/researcher/actions/socialSearch.ts diff --git a/src/lib/agents/search/researcher/actions/academicSearch.ts b/src/lib/agents/search/researcher/actions/academicSearch.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/agents/search/researcher/actions/socialSearch.ts b/src/lib/agents/search/researcher/actions/socialSearch.ts new file mode 100644 index 0000000..e69de29 From 491136822f605ca2061257d55f1c720013b1d0b5 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 17 Dec 2025 21:17:21 +0530 Subject: [PATCH 139/196] feat(app): lint & beautify --- src/app/api/search/route.ts | 2 +- src/lib/actions.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts index 530a7e7..dc1d0b8 100644 --- a/src/app/api/search/route.ts +++ b/src/app/api/search/route.ts @@ -57,7 +57,7 @@ export const POST = async (req: Request) => { llm: llm, sources: ['web', 'discussions', 'academic'], mode: 'balanced', - fileIds: [] + fileIds: [], }, followUp: body.query, }); diff --git a/src/lib/actions.ts b/src/lib/actions.ts index f9d04b2..c2855c0 100644 --- a/src/lib/actions.ts +++ b/src/lib/actions.ts @@ -1,5 +1,3 @@ -import { Message } from '@/components/ChatWindow'; - export const getSuggestions = async (chatHistory: [string, string][]) => { const chatTurns = chatHistory.map(([role, content]) => { if (role === 'human') { From 473a04b6a59bd448bee2541187fc94f5f71b83d9 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Thu, 18 Dec 2025 13:56:04 +0530 Subject: [PATCH 140/196] feat(suggestions): prevent icon from shrinking --- src/components/MessageBox.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/MessageBox.tsx b/src/components/MessageBox.tsx index 4f5766d..0179255 100644 --- a/src/components/MessageBox.tsx +++ b/src/components/MessageBox.tsx @@ -218,10 +218,10 @@ const MessageBox = ({ className="group w-full py-4 text-left transition-colors duration-200" > <div className="flex items-center justify-between gap-3"> - <div className="flex flex-row space-x-3 items-center "> + <div className="flex flex-row space-x-3 items-center"> <CornerDownRight - size={17} - className="group-hover:text-sky-400 transition-colors duration-200" + size={15} + className="group-hover:text-sky-400 transition-colors duration-200 flex-shrink-0" /> <p className="text-sm text-black/70 dark:text-white/70 group-hover:text-sky-400 transition-colors duration-200 leading-relaxed"> {suggestion} From 5511a276d4e5e5ea22bebd881101cb1c38d627bb Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Thu, 18 Dec 2025 13:56:08 +0530 Subject: [PATCH 141/196] Update Sources.tsx --- src/components/MessageInputActions/Sources.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MessageInputActions/Sources.tsx b/src/components/MessageInputActions/Sources.tsx index e996b6e..9a98239 100644 --- a/src/components/MessageInputActions/Sources.tsx +++ b/src/components/MessageInputActions/Sources.tsx @@ -25,7 +25,7 @@ const sourcesList = [ }, { name: 'Social', - key: 'social', + key: 'discussions', icon: <NetworkIcon className="h-[16px] w-auto" />, }, ]; From ac183a90e8e392d1930a81ee3bf062ec5afcb3bb Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Thu, 18 Dec 2025 13:56:26 +0530 Subject: [PATCH 142/196] feat(academic-search): add academic search --- .../researcher/actions/academicSearch.ts | 129 ++++++++++++++++++ .../agents/search/researcher/actions/index.ts | 2 + 2 files changed, 131 insertions(+) diff --git a/src/lib/agents/search/researcher/actions/academicSearch.ts b/src/lib/agents/search/researcher/actions/academicSearch.ts index e69de29..72e1f4b 100644 --- a/src/lib/agents/search/researcher/actions/academicSearch.ts +++ b/src/lib/agents/search/researcher/actions/academicSearch.ts @@ -0,0 +1,129 @@ +import z from 'zod'; +import { ResearchAction } from '../../types'; +import { Chunk, SearchResultsResearchBlock } from '@/lib/types'; +import { searchSearxng } from '@/lib/searxng'; + +const schema = z.object({ + queries: z.array(z.string()).describe('List of academic search queries'), +}); + +const academicSearchDescription = ` +Use this tool to perform academic searches for scholarly articles, papers, and research studies relevant to the user's query. Provide a list of concise search queries that will help gather comprehensive academic information on the topic at hand. +You can provide up to 3 queries at a time. Make sure the queries are specific and relevant to the user's needs. + +For example, if the user is interested in recent advancements in renewable energy, your queries could be: +1. "Recent advancements in renewable energy 2024" +2. "Cutting-edge research on solar power technologies" +3. "Innovations in wind energy systems" + +If this tool is present and no other tools are more relevant, you MUST use this tool to get the needed academic information. +`; + +const academicSearchAction: ResearchAction<typeof schema> = { + name: 'academic_search', + schema: schema, + getDescription: () => academicSearchDescription, + getToolDescription: () => + "Use this tool to perform academic searches for scholarly articles, papers, and research studies relevant to the user's query. Provide a list of concise search queries that will help gather comprehensive academic information on the topic at hand.", + enabled: (config) => + config.sources.includes('academic') && + config.classification.classification.skipSearch === false && + config.classification.classification.academicSearch === true, + execute: async (input, additionalConfig) => { + input.queries = input.queries.slice(0, 3); + + const researchBlock = additionalConfig.session.getBlock( + additionalConfig.researchBlockId, + ); + + if (researchBlock && researchBlock.type === 'research') { + researchBlock.data.subSteps.push({ + type: 'searching', + id: crypto.randomUUID(), + searching: input.queries, + }); + + additionalConfig.session.updateBlock(additionalConfig.researchBlockId, [ + { + op: 'replace', + path: '/data/subSteps', + value: researchBlock.data.subSteps, + }, + ]); + } + + const searchResultsBlockId = crypto.randomUUID(); + let searchResultsEmitted = false; + + let results: Chunk[] = []; + + const search = async (q: string) => { + const res = await searchSearxng(q, { + engines: ['arxiv', 'google scholar', 'pubmed'], + }); + + const resultChunks: Chunk[] = res.results.map((r) => ({ + content: r.content || r.title, + metadata: { + title: r.title, + url: r.url, + }, + })); + + results.push(...resultChunks); + + if ( + !searchResultsEmitted && + researchBlock && + researchBlock.type === 'research' + ) { + searchResultsEmitted = true; + + researchBlock.data.subSteps.push({ + id: searchResultsBlockId, + type: 'search_results', + reading: resultChunks, + }); + + additionalConfig.session.updateBlock(additionalConfig.researchBlockId, [ + { + op: 'replace', + path: '/data/subSteps', + value: researchBlock.data.subSteps, + }, + ]); + } else if ( + searchResultsEmitted && + researchBlock && + researchBlock.type === 'research' + ) { + const subStepIndex = researchBlock.data.subSteps.findIndex( + (step) => step.id === searchResultsBlockId, + ); + + const subStep = researchBlock.data.subSteps[ + subStepIndex + ] as SearchResultsResearchBlock; + + subStep.reading.push(...resultChunks); + + additionalConfig.session.updateBlock(additionalConfig.researchBlockId, [ + { + op: 'replace', + path: '/data/subSteps', + value: researchBlock.data.subSteps, + }, + ]); + } + }; + + await Promise.all(input.queries.map(search)); + + return { + type: 'search_results', + results, + }; + }, +}; + +export default academicSearchAction; diff --git a/src/lib/agents/search/researcher/actions/index.ts b/src/lib/agents/search/researcher/actions/index.ts index 6d7e58f..90559b9 100644 --- a/src/lib/agents/search/researcher/actions/index.ts +++ b/src/lib/agents/search/researcher/actions/index.ts @@ -1,3 +1,4 @@ +import academicSearchAction from './academicSearch'; import doneAction from './done'; import planAction from './plan'; import ActionRegistry from './registry'; @@ -10,5 +11,6 @@ ActionRegistry.register(doneAction); ActionRegistry.register(planAction); ActionRegistry.register(scrapeURLAction); ActionRegistry.register(uploadsSearchAction); +ActionRegistry.register(academicSearchAction); export { ActionRegistry }; From 604774ef6e3154f0020a63f3f9f31f6676bd2194 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Thu, 18 Dec 2025 13:56:39 +0530 Subject: [PATCH 143/196] feat(social-search): add social search --- .../agents/search/researcher/actions/index.ts | 2 + .../search/researcher/actions/socialSearch.ts | 129 ++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/src/lib/agents/search/researcher/actions/index.ts b/src/lib/agents/search/researcher/actions/index.ts index 90559b9..8864c08 100644 --- a/src/lib/agents/search/researcher/actions/index.ts +++ b/src/lib/agents/search/researcher/actions/index.ts @@ -3,6 +3,7 @@ import doneAction from './done'; import planAction from './plan'; import ActionRegistry from './registry'; import scrapeURLAction from './scrapeURL'; +import socialSearchAction from './socialSearch'; import uploadsSearchAction from './uploadsSearch'; import webSearchAction from './webSearch'; @@ -12,5 +13,6 @@ ActionRegistry.register(planAction); ActionRegistry.register(scrapeURLAction); ActionRegistry.register(uploadsSearchAction); ActionRegistry.register(academicSearchAction); +ActionRegistry.register(socialSearchAction); export { ActionRegistry }; diff --git a/src/lib/agents/search/researcher/actions/socialSearch.ts b/src/lib/agents/search/researcher/actions/socialSearch.ts index e69de29..16468ab 100644 --- a/src/lib/agents/search/researcher/actions/socialSearch.ts +++ b/src/lib/agents/search/researcher/actions/socialSearch.ts @@ -0,0 +1,129 @@ +import z from 'zod'; +import { ResearchAction } from '../../types'; +import { Chunk, SearchResultsResearchBlock } from '@/lib/types'; +import { searchSearxng } from '@/lib/searxng'; + +const schema = z.object({ + queries: z.array(z.string()).describe('List of social search queries'), +}); + +const socialSearchDescription = ` +Use this tool to perform social media searches for relevant posts, discussions, and trends related to the user's query. Provide a list of concise search queries that will help gather comprehensive social media information on the topic at hand. +You can provide up to 3 queries at a time. Make sure the queries are specific and relevant to the user's needs. + +For example, if the user is interested in public opinion on electric vehicles, your queries could be: +1. "Electric vehicles public opinion 2024" +2. "Social media discussions on EV adoption" +3. "Trends in electric vehicle usage" + +If this tool is present and no other tools are more relevant, you MUST use this tool to get the needed social media information. +`; + +const socialSearchAction: ResearchAction<typeof schema> = { + name: 'social_search', + schema: schema, + getDescription: () => socialSearchDescription, + getToolDescription: () => + "Use this tool to perform social media searches for relevant posts, discussions, and trends related to the user's query. Provide a list of concise search queries that will help gather comprehensive social media information on the topic at hand.", + enabled: (config) => + config.sources.includes('discussions') && + config.classification.classification.skipSearch === false && + config.classification.classification.discussionSearch === true, + execute: async (input, additionalConfig) => { + input.queries = input.queries.slice(0, 3); + + const researchBlock = additionalConfig.session.getBlock( + additionalConfig.researchBlockId, + ); + + if (researchBlock && researchBlock.type === 'research') { + researchBlock.data.subSteps.push({ + type: 'searching', + id: crypto.randomUUID(), + searching: input.queries, + }); + + additionalConfig.session.updateBlock(additionalConfig.researchBlockId, [ + { + op: 'replace', + path: '/data/subSteps', + value: researchBlock.data.subSteps, + }, + ]); + } + + const searchResultsBlockId = crypto.randomUUID(); + let searchResultsEmitted = false; + + let results: Chunk[] = []; + + const search = async (q: string) => { + const res = await searchSearxng(q, { + engines: ['reddit'], + }); + + const resultChunks: Chunk[] = res.results.map((r) => ({ + content: r.content || r.title, + metadata: { + title: r.title, + url: r.url, + }, + })); + + results.push(...resultChunks); + + if ( + !searchResultsEmitted && + researchBlock && + researchBlock.type === 'research' + ) { + searchResultsEmitted = true; + + researchBlock.data.subSteps.push({ + id: searchResultsBlockId, + type: 'search_results', + reading: resultChunks, + }); + + additionalConfig.session.updateBlock(additionalConfig.researchBlockId, [ + { + op: 'replace', + path: '/data/subSteps', + value: researchBlock.data.subSteps, + }, + ]); + } else if ( + searchResultsEmitted && + researchBlock && + researchBlock.type === 'research' + ) { + const subStepIndex = researchBlock.data.subSteps.findIndex( + (step) => step.id === searchResultsBlockId, + ); + + const subStep = researchBlock.data.subSteps[ + subStepIndex + ] as SearchResultsResearchBlock; + + subStep.reading.push(...resultChunks); + + additionalConfig.session.updateBlock(additionalConfig.researchBlockId, [ + { + op: 'replace', + path: '/data/subSteps', + value: researchBlock.data.subSteps, + }, + ]); + } + }; + + await Promise.all(input.queries.map(search)); + + return { + type: 'search_results', + results, + }; + }, +}; + +export default socialSearchAction; From 64683e3dec1ed5c6e46de4c09f5d07c2f21c707f Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:25:56 +0530 Subject: [PATCH 144/196] feat(assistant-steps): improve style --- src/components/AssistantSteps.tsx | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/components/AssistantSteps.tsx b/src/components/AssistantSteps.tsx index 6c84ec6..46461e9 100644 --- a/src/components/AssistantSteps.tsx +++ b/src/components/AssistantSteps.tsx @@ -182,8 +182,10 @@ const AssistantSteps = ({ : ''; return ( - <span + <a key={idx} + href={url} + target="_blank" className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md text-xs font-medium bg-light-100 dark:bg-dark-100 text-black/70 dark:text-white/70 border border-light-200 dark:border-dark-200" > {faviconUrl && ( @@ -197,7 +199,7 @@ const AssistantSteps = ({ /> )} <span className="line-clamp-1">{title}</span> - </span> + </a> ); })} </div> @@ -219,33 +221,26 @@ const AssistantSteps = ({ {step.type === 'upload_search_results' && step.results.length > 0 && ( - <div className="mt-1.5 space-y-2"> + <div className="mt-1.5 grid gap-3 lg:grid-cols-3"> {step.results.slice(0, 4).map((result, idx) => { const title = (result.metadata && (result.metadata.title || result.metadata.fileName)) || 'Untitled document'; - const snippet = (result.content || '') - .replace(/\s+/g, ' ') - .trim() - .slice(0, 220); return ( <div key={idx} - className="flex gap-3 items-start rounded-lg border border-light-200 dark:border-dark-200 bg-light-100 dark:bg-dark-100 p-2" + className="flex flex-row space-x-3 rounded-lg border border-light-200 dark:border-dark-200 bg-light-100 dark:bg-dark-100 p-2 cursor-pointer" > <div className="mt-0.5 h-10 w-10 rounded-md bg-cyan-100 text-cyan-800 dark:bg-sky-500 dark:text-cyan-50 flex items-center justify-center"> <FileText className="w-5 h-5" /> </div> - <div className="flex-1 min-w-0"> - <div className="text-sm font-semibold text-black dark:text-white line-clamp-1"> + <div className="flex flex-col justify-center"> + <p className="text-[13px] text-black dark:text-white line-clamp-1"> {title} - </div> - <div className="text-xs text-black/70 dark:text-white/70 mt-0.5 leading-relaxed line-clamp-3"> - {snippet || 'No preview available.'} - </div> + </p> </div> </div> ); From a82b605c70b85704bb77a31d3d5be399e04f16aa Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:26:13 +0530 Subject: [PATCH 145/196] feat(citation): move to message renderer --- src/components/{ => MessageRenderer}/Citation.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/components/{ => MessageRenderer}/Citation.tsx (100%) diff --git a/src/components/Citation.tsx b/src/components/MessageRenderer/Citation.tsx similarity index 100% rename from src/components/Citation.tsx rename to src/components/MessageRenderer/Citation.tsx From 21cb0f5fd967e743486515a9f40fa44007afb5bd Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:26:38 +0530 Subject: [PATCH 146/196] feat(app): add syntax highlighter --- package.json | 1 + yarn.lock | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/package.json b/package.json index ba1162f..a735c5d 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@types/pdf-parse": "^1.1.4", "@types/react": "^18", "@types/react-dom": "^18", + "@types/react-syntax-highlighter": "^15.5.13", "@types/turndown": "^5.0.6", "autoprefixer": "^10.0.1", "drizzle-kit": "^0.30.5", diff --git a/yarn.lock b/yarn.lock index 1dbec13..7efced6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1383,6 +1383,13 @@ dependencies: "@types/react" "*" +"@types/react-syntax-highlighter@^15.5.13": + version "15.5.13" + resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz#c5baf62a3219b3bf28d39cfea55d0a49a263d1f2" + integrity sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@^18": version "18.2.74" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.74.tgz#2d52eb80e4e7c4ea8812c89181d6d590b53f958c" From fdee29c93ef3bb0ee312326d06cb849c3d05fedf Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:26:51 +0530 Subject: [PATCH 147/196] feat(renderers): add code block --- .../CodeBlock/CodeBlockDarkTheme.ts | 102 ++++++++++++++++++ .../CodeBlock/CodeBlockLightTheme.ts | 102 ++++++++++++++++++ .../MessageRenderer/CodeBlock/index.tsx | 64 +++++++++++ 3 files changed, 268 insertions(+) create mode 100644 src/components/MessageRenderer/CodeBlock/CodeBlockDarkTheme.ts create mode 100644 src/components/MessageRenderer/CodeBlock/CodeBlockLightTheme.ts create mode 100644 src/components/MessageRenderer/CodeBlock/index.tsx diff --git a/src/components/MessageRenderer/CodeBlock/CodeBlockDarkTheme.ts b/src/components/MessageRenderer/CodeBlock/CodeBlockDarkTheme.ts new file mode 100644 index 0000000..0a9d6a4 --- /dev/null +++ b/src/components/MessageRenderer/CodeBlock/CodeBlockDarkTheme.ts @@ -0,0 +1,102 @@ +import type { CSSProperties } from 'react'; + +const darkTheme = { + 'hljs-comment': { + color: '#8b949e', + }, + 'hljs-quote': { + color: '#8b949e', + }, + 'hljs-variable': { + color: '#ff7b72', + }, + 'hljs-template-variable': { + color: '#ff7b72', + }, + 'hljs-tag': { + color: '#ff7b72', + }, + 'hljs-name': { + color: '#ff7b72', + }, + 'hljs-selector-id': { + color: '#ff7b72', + }, + 'hljs-selector-class': { + color: '#ff7b72', + }, + 'hljs-regexp': { + color: '#ff7b72', + }, + 'hljs-deletion': { + color: '#ff7b72', + }, + 'hljs-number': { + color: '#f2cc60', + }, + 'hljs-built_in': { + color: '#f2cc60', + }, + 'hljs-builtin-name': { + color: '#f2cc60', + }, + 'hljs-literal': { + color: '#f2cc60', + }, + 'hljs-type': { + color: '#f2cc60', + }, + 'hljs-params': { + color: '#f2cc60', + }, + 'hljs-meta': { + color: '#f2cc60', + }, + 'hljs-link': { + color: '#f2cc60', + }, + 'hljs-attribute': { + color: '#58a6ff', + }, + 'hljs-string': { + color: '#7ee787', + }, + 'hljs-symbol': { + color: '#7ee787', + }, + 'hljs-bullet': { + color: '#7ee787', + }, + 'hljs-addition': { + color: '#7ee787', + }, + 'hljs-title': { + color: '#79c0ff', + }, + 'hljs-section': { + color: '#79c0ff', + }, + 'hljs-keyword': { + color: '#c297ff', + }, + 'hljs-selector-tag': { + color: '#c297ff', + }, + hljs: { + display: 'block', + overflowX: 'auto', + background: '#0d1117', + color: '#c9d1d9', + padding: '0.75em', + border: '1px solid #21262d', + borderRadius: '10px', + }, + 'hljs-emphasis': { + fontStyle: 'italic', + }, + 'hljs-strong': { + fontWeight: 'bold', + }, +} satisfies Record<string, CSSProperties>; + +export default darkTheme; diff --git a/src/components/MessageRenderer/CodeBlock/CodeBlockLightTheme.ts b/src/components/MessageRenderer/CodeBlock/CodeBlockLightTheme.ts new file mode 100644 index 0000000..758dbac --- /dev/null +++ b/src/components/MessageRenderer/CodeBlock/CodeBlockLightTheme.ts @@ -0,0 +1,102 @@ +import type { CSSProperties } from 'react'; + +const lightTheme = { + 'hljs-comment': { + color: '#6e7781', + }, + 'hljs-quote': { + color: '#6e7781', + }, + 'hljs-variable': { + color: '#d73a49', + }, + 'hljs-template-variable': { + color: '#d73a49', + }, + 'hljs-tag': { + color: '#d73a49', + }, + 'hljs-name': { + color: '#d73a49', + }, + 'hljs-selector-id': { + color: '#d73a49', + }, + 'hljs-selector-class': { + color: '#d73a49', + }, + 'hljs-regexp': { + color: '#d73a49', + }, + 'hljs-deletion': { + color: '#d73a49', + }, + 'hljs-number': { + color: '#b08800', + }, + 'hljs-built_in': { + color: '#b08800', + }, + 'hljs-builtin-name': { + color: '#b08800', + }, + 'hljs-literal': { + color: '#b08800', + }, + 'hljs-type': { + color: '#b08800', + }, + 'hljs-params': { + color: '#b08800', + }, + 'hljs-meta': { + color: '#b08800', + }, + 'hljs-link': { + color: '#b08800', + }, + 'hljs-attribute': { + color: '#0a64ae', + }, + 'hljs-string': { + color: '#22863a', + }, + 'hljs-symbol': { + color: '#22863a', + }, + 'hljs-bullet': { + color: '#22863a', + }, + 'hljs-addition': { + color: '#22863a', + }, + 'hljs-title': { + color: '#005cc5', + }, + 'hljs-section': { + color: '#005cc5', + }, + 'hljs-keyword': { + color: '#6f42c1', + }, + 'hljs-selector-tag': { + color: '#6f42c1', + }, + hljs: { + display: 'block', + overflowX: 'auto', + background: '#ffffff', + color: '#24292f', + padding: '0.75em', + border: '1px solid #e8edf1', + borderRadius: '10px', + }, + 'hljs-emphasis': { + fontStyle: 'italic', + }, + 'hljs-strong': { + fontWeight: 'bold', + }, +} satisfies Record<string, CSSProperties>; + +export default lightTheme; diff --git a/src/components/MessageRenderer/CodeBlock/index.tsx b/src/components/MessageRenderer/CodeBlock/index.tsx new file mode 100644 index 0000000..493a0d0 --- /dev/null +++ b/src/components/MessageRenderer/CodeBlock/index.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { CheckIcon, CopyIcon } from '@phosphor-icons/react'; +import React, { useEffect, useMemo, useState } from 'react'; +import { useTheme } from 'next-themes'; +import SyntaxHighlighter from 'react-syntax-highlighter'; +import darkTheme from './CodeBlockDarkTheme'; +import lightTheme from './CodeBlockLightTheme'; + +const CodeBlock = ({ + language, + children, +}: { + language: string; + children: React.ReactNode; +}) => { + const { resolvedTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + + const [copied, setCopied] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + const syntaxTheme = useMemo(() => { + if (!mounted) return lightTheme; + return resolvedTheme === 'dark' ? darkTheme : lightTheme; + }, [mounted, resolvedTheme]); + + return ( + <div className="relative"> + <button + className="absolute top-2 right-2 p-1" + onClick={() => { + navigator.clipboard.writeText(children as string); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }} + > + {copied ? ( + <CheckIcon + size={16} + className="absolute top-2 right-2 text-black/70 dark:text-white/70" + /> + ) : ( + <CopyIcon + size={16} + className="absolute top-2 right-2 transition duration-200 text-black/70 dark:text-white/70 hover:text-gray-800/70 hover:dark:text-gray-300/70" + /> + )} + </button> + <SyntaxHighlighter + language={language} + style={syntaxTheme} + showInlineLineNumbers + > + {children as string} + </SyntaxHighlighter> + </div> + ); +}; + +export default CodeBlock; From 6e2345bd2d56fc136d3397c26e409b03a024ed7c Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:27:55 +0530 Subject: [PATCH 148/196] feat(message-box): update `markdown2jsx` overrides to render codeblock --- src/components/MessageBox.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/components/MessageBox.tsx b/src/components/MessageBox.tsx index 0179255..93c26a6 100644 --- a/src/components/MessageBox.tsx +++ b/src/components/MessageBox.tsx @@ -12,7 +12,7 @@ import { Plus, CornerDownRight, } from 'lucide-react'; -import Markdown, { MarkdownToJSX } from 'markdown-to-jsx'; +import Markdown, { MarkdownToJSX, RuleType } from 'markdown-to-jsx'; import Copy from './MessageActions/Copy'; import Rewrite from './MessageActions/Rewrite'; import MessageSources from './MessageSources'; @@ -21,10 +21,11 @@ import SearchVideos from './SearchVideos'; import { useSpeech } from 'react-text-to-speech'; import ThinkBox from './ThinkBox'; import { useChat, Section } from '@/lib/hooks/useChat'; -import Citation from './Citation'; +import Citation from './MessageRenderer/Citation'; import AssistantSteps from './AssistantSteps'; import { ResearchBlock } from '@/lib/types'; import Renderer from './Widgets/Renderer'; +import CodeBlock from './MessageRenderer/CodeBlock'; const ThinkTagProcessor = ({ children, @@ -67,6 +68,21 @@ const MessageBox = ({ const { speechStatus, start, stop } = useSpeech({ text: speechMessage }); const markdownOverrides: MarkdownToJSX.Options = { + renderRule(next, node, renderChildren, state) { + if (node.type === RuleType.codeInline) { + return `\`${node.text}\``; + } + + if (node.type === RuleType.codeBlock) { + return ( + <CodeBlock key={state.key} language={node.lang || ''}> + {node.text} + </CodeBlock> + ); + } + + return next(); + }, overrides: { think: { component: ThinkTagProcessor, From 85273493a01797291dd088d5d28307bb63203f2c Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:35:13 +0530 Subject: [PATCH 149/196] feat(copy): fix type mismatch --- src/components/MessageActions/Copy.tsx | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/components/MessageActions/Copy.tsx b/src/components/MessageActions/Copy.tsx index 38ed71a..f2509f4 100644 --- a/src/components/MessageActions/Copy.tsx +++ b/src/components/MessageActions/Copy.tsx @@ -2,6 +2,7 @@ import { Check, ClipboardList } from 'lucide-react'; import { Message } from '../ChatWindow'; import { useState } from 'react'; import { Section } from '@/lib/hooks/useChat'; +import { SourceBlock } from '@/lib/types'; const Copy = ({ section, @@ -15,15 +16,24 @@ const Copy = ({ return ( <button onClick={() => { + const sources = section.message.responseBlocks.filter( + (b) => b.type === 'source' && b.data.length > 0, + ) as SourceBlock[]; + const contentToCopy = `${initialMessage}${ - section?.message.responseBlocks.filter((b) => b.type === 'source') - ?.length > 0 && - `\n\nCitations:\n${section.message.responseBlocks - .filter((b) => b.type === 'source') - ?.map((source: any, i: any) => `[${i + 1}] ${source.metadata.url}`) + sources.length > 0 && + `\n\nCitations:\n${sources + .map((source) => source.data) + .flat() + .map( + (s, i) => + `[${i + 1}] ${s.metadata.url.startsWith('file_id://') ? s.metadata.fileName || 'Uploaded File' : s.metadata.url}`, + ) .join(`\n`)}` }`; + navigator.clipboard.writeText(contentToCopy); + setCopied(true); setTimeout(() => setCopied(false), 1000); }} From 300cfa35c7fa825f6fd4da305b18f42314e36549 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:45:46 +0530 Subject: [PATCH 150/196] Update Optimization.tsx --- src/components/MessageInputActions/Optimization.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/MessageInputActions/Optimization.tsx b/src/components/MessageInputActions/Optimization.tsx index 7b87e6c..4433fcd 100644 --- a/src/components/MessageInputActions/Optimization.tsx +++ b/src/components/MessageInputActions/Optimization.tsx @@ -85,9 +85,16 @@ const Optimization = () => { : 'hover:bg-light-secondary dark:hover:bg-dark-secondary', )} > - <div className="flex flex-row items-center space-x-1 text-black dark:text-white"> - {mode.icon} - <p className="text-xs font-medium">{mode.title}</p> + <div className="flex flex-row justify-between w-full text-black dark:text-white"> + <div className='flex flex-row space-x-1'> + {mode.icon} + <p className="text-xs font-medium">{mode.title}</p> + </div> + {mode.key === 'quality' && ( + <span className='bg-violet-500/70 dark:bg-violet-500/40 border border-violet-500 px-1 rounded-full text-[10px] text-white'> + Beta + </span> + )} </div> <p className="text-black/70 dark:text-white/70 text-xs"> {mode.description} From 94a24d40582242a47745cae41ca91eddaf686688 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 21 Dec 2025 19:56:43 +0530 Subject: [PATCH 151/196] feat(message-input): add overflow to prevent blocked popovers --- src/components/MessageInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MessageInput.tsx b/src/components/MessageInput.tsx index 0776d1d..56054eb 100644 --- a/src/components/MessageInput.tsx +++ b/src/components/MessageInput.tsx @@ -61,7 +61,7 @@ const MessageInput = () => { } }} className={cn( - 'relative bg-light-secondary dark:bg-dark-secondary p-4 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 shadow-sm shadow-light-200/10 dark:shadow-black/20 transition-all duration-200 focus-within:border-light-300 dark:focus-within:border-dark-300', + 'relative bg-light-secondary dark:bg-dark-secondary p-4 flex items-center overflow-visible border border-light-200 dark:border-dark-200 shadow-sm shadow-light-200/10 dark:shadow-black/20 transition-all duration-200 focus-within:border-light-300 dark:focus-within:border-dark-300', mode === 'multi' ? 'flex-col rounded-2xl' : 'flex-row rounded-full', )} > From 13d6bcf1136355d7dbdd3949847cac9263073910 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:58:30 +0530 Subject: [PATCH 152/196] Update Optimization.tsx --- src/components/MessageInputActions/Optimization.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MessageInputActions/Optimization.tsx b/src/components/MessageInputActions/Optimization.tsx index 4433fcd..bc56ef2 100644 --- a/src/components/MessageInputActions/Optimization.tsx +++ b/src/components/MessageInputActions/Optimization.tsx @@ -91,7 +91,7 @@ const Optimization = () => { <p className="text-xs font-medium">{mode.title}</p> </div> {mode.key === 'quality' && ( - <span className='bg-violet-500/70 dark:bg-violet-500/40 border border-violet-500 px-1 rounded-full text-[10px] text-white'> + <span className='bg-sky-500/70 dark:bg-sky-500/40 border border-sky-600 px-1 rounded-full text-[10px] text-white'> Beta </span> )} From 68a9e048ac174220ae904c9952b00553e4738792 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:11:38 +0530 Subject: [PATCH 153/196] feat(schema): change focusMode to sources --- src/lib/db/schema.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 50dd14c..6017d9f 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -1,6 +1,7 @@ import { sql } from 'drizzle-orm'; import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core'; import { Block } from '../types'; +import { SearchSources } from '../agents/search/types'; export const messages = sqliteTable('messages', { id: integer('id').primaryKey(), @@ -26,7 +27,11 @@ export const chats = sqliteTable('chats', { id: text('id').primaryKey(), title: text('title').notNull(), createdAt: text('createdAt').notNull(), - focusMode: text('focusMode').notNull(), + sources: text('sources', { + mode: 'json', + }) + .$type<SearchSources[]>() + .default(sql`'[]'`), files: text('files', { mode: 'json' }) .$type<DBFile[]>() .default(sql`'[]'`), From 216332fb20fd511a76d90340c009ea0361e1587d Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:12:15 +0530 Subject: [PATCH 154/196] feat(session): add subscribe method, getAllBlocks --- src/lib/session.ts | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/lib/session.ts b/src/lib/session.ts index e5a9bc1..e675821 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -67,15 +67,32 @@ class SessionManager { } } - addListener(event: string, listener: (data: any) => void) { - this.emitter.addListener(event, listener); + getAllBlocks() { + return Array.from(this.blocks.values()); } - replay() { - for (const { event, data } of this.events) { - /* Using emitter directly to avoid infinite loop */ - this.emitter.emit(event, data); + subscribe(listener: (event: string, data: any) => void): () => void { + const currentEventsLength = this.events.length; + + const handler = (event: string) => (data: any) => listener(event, data); + const dataHandler = handler('data'); + const endHandler = handler('end'); + const errorHandler = handler('error'); + + this.emitter.on('data', dataHandler); + this.emitter.on('end', endHandler); + this.emitter.on('error', errorHandler); + + for (let i = 0; i < currentEventsLength; i++) { + const { event, data } = this.events[i]; + listener(event, data); } + + return () => { + this.emitter.off('data', dataHandler); + this.emitter.off('end', endHandler); + this.emitter.off('error', errorHandler); + }; } } From 8ba64be446f0c63d488ebcae693798c8e6cf3920 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:12:56 +0530 Subject: [PATCH 155/196] feat(session): fix sessions getting disregarded due to reload --- src/lib/session.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lib/session.ts b/src/lib/session.ts index e675821..4f74330 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -2,8 +2,14 @@ import { EventEmitter } from 'stream'; import { applyPatch } from 'rfc6902'; import { Block } from './types'; +const sessions = + (global as any)._sessionManagerSessions || new Map<string, SessionManager>(); +if (process.env.NODE_ENV !== 'production') { + (global as any)._sessionManagerSessions = sessions; +} + class SessionManager { - private static sessions = new Map<string, SessionManager>(); + private static sessions: Map<string, SessionManager> = sessions; readonly id: string; private blocks = new Map<string, Block>(); private events: { event: string; data: any }[] = []; From 0e176e0b781bb9d6d5a583fbd8511d644cf9b85b Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:14:02 +0530 Subject: [PATCH 156/196] feat(chat-route): add history saving, disconnect on abort, use subscribe method --- src/app/api/chat/route.ts | 161 ++++++++++++++++++++++---------------- 1 file changed, 93 insertions(+), 68 deletions(-) diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index b5d4ead..67270ce 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -5,6 +5,10 @@ import SearchAgent from '@/lib/agents/search'; import SessionManager from '@/lib/session'; import { ChatTurnMessage } from '@/lib/types'; import { SearchSources } from '@/lib/agents/search/types'; +import db from '@/lib/db'; +import { eq } from 'drizzle-orm'; +import { chats } from '@/lib/db/schema'; +import UploadManager from '@/lib/uploads/manager'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -64,6 +68,38 @@ const safeValidateBody = (data: unknown) => { }; }; +const ensureChatExists = async (input: { + id: string; + sources: SearchSources[]; + query: string; + fileIds: string[]; +}) => { + try { + const exists = await db.query.chats + .findFirst({ + where: eq(chats.id, input.id), + }) + .execute(); + + if (!exists) { + await db.insert(chats).values({ + id: input.id, + createdAt: new Date().toISOString(), + sources: input.sources, + title: input.query, + files: input.fileIds.map((id) => { + return { + fileId: id, + name: UploadManager.getFile(id)?.name || 'Uploaded File', + }; + }), + }); + } + } catch (err) { + console.error('Failed to check/save chat:', err); + } +}; + export const POST = async (req: Request) => { try { const reqBody = (await req.json()) as Body; @@ -120,86 +156,65 @@ export const POST = async (req: Request) => { const writer = responseStream.writable.getWriter(); const encoder = new TextEncoder(); - let receivedMessage = ''; - - session.addListener('data', (data: any) => { - if (data.type === 'response') { + const disconnect = session.subscribe((event: string, data: any) => { + if (event === 'data') { + if (data.type === 'block') { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'block', + block: data.block, + }) + '\n', + ), + ); + } else if (data.type === 'updateBlock') { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'updateBlock', + blockId: data.blockId, + patch: data.patch, + }) + '\n', + ), + ); + } else if (data.type === 'researchComplete') { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'researchComplete', + }) + '\n', + ), + ); + } + } else if (event === 'end') { writer.write( encoder.encode( JSON.stringify({ - type: 'message', + type: 'messageEnd', + }) + '\n', + ), + ); + writer.close(); + session.removeAllListeners(); + } else if (event === 'error') { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'error', data: data.data, }) + '\n', ), ); - receivedMessage += data.data; - } else if (data.type === 'sources') { - writer.write( - encoder.encode( - JSON.stringify({ - type: 'sources', - data: data.data, - }) + '\n', - ), - ); - } else if (data.type === 'block') { - writer.write( - encoder.encode( - JSON.stringify({ - type: 'block', - block: data.block, - }) + '\n', - ), - ); - } else if (data.type === 'updateBlock') { - writer.write( - encoder.encode( - JSON.stringify({ - type: 'updateBlock', - blockId: data.blockId, - patch: data.patch, - }) + '\n', - ), - ); - } else if (data.type === 'researchComplete') { - writer.write( - encoder.encode( - JSON.stringify({ - type: 'researchComplete', - }) + '\n', - ), - ); + writer.close(); + session.removeAllListeners(); } }); - session.addListener('end', () => { - writer.write( - encoder.encode( - JSON.stringify({ - type: 'messageEnd', - }) + '\n', - ), - ); - writer.close(); - session.removeAllListeners(); - }); - - session.addListener('error', (data: any) => { - writer.write( - encoder.encode( - JSON.stringify({ - type: 'error', - data: data.data, - }) + '\n', - ), - ); - writer.close(); - session.removeAllListeners(); - }); - agent.searchAsync(session, { chatHistory: history, followUp: message.content, + chatId: body.message.chatId, + messageId: body.message.messageId, config: { llm, embedding: embedding, @@ -209,7 +224,17 @@ export const POST = async (req: Request) => { }, }); - /* handleHistorySave(message, humanMessageId, body.focusMode, body.files); */ + ensureChatExists({ + id: body.message.chatId, + sources: body.sources as SearchSources[], + fileIds: body.files, + query: body.message.content, + }); + + req.signal.addEventListener('abort', () => { + disconnect(); + writer.close(); + }); return new Response(responseStream.readable, { headers: { From f278eb8bf11b1c220a770740f33e0893f7f1a68e Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:15:02 +0530 Subject: [PATCH 157/196] feat(routes): add reconnect route --- src/app/api/reconnect/[id]/route.ts | 93 +++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/app/api/reconnect/[id]/route.ts diff --git a/src/app/api/reconnect/[id]/route.ts b/src/app/api/reconnect/[id]/route.ts new file mode 100644 index 0000000..08be11b --- /dev/null +++ b/src/app/api/reconnect/[id]/route.ts @@ -0,0 +1,93 @@ +import SessionManager from '@/lib/session'; + +export const POST = async ( + req: Request, + { params }: { params: Promise<{ id: string }> }, +) => { + try { + const { id } = await params; + + const session = SessionManager.getSession(id); + + if (!session) { + return Response.json({ message: 'Session not found' }, { status: 404 }); + } + + const responseStream = new TransformStream(); + const writer = responseStream.writable.getWriter(); + const encoder = new TextEncoder(); + + const disconnect = session.subscribe((event, data) => { + if (event === 'data') { + if (data.type === 'block') { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'block', + block: data.block, + }) + '\n', + ), + ); + } else if (data.type === 'updateBlock') { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'updateBlock', + blockId: data.blockId, + patch: data.patch, + }) + '\n', + ), + ); + } else if (data.type === 'researchComplete') { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'researchComplete', + }) + '\n', + ), + ); + } + } else if (event === 'end') { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'messageEnd', + }) + '\n', + ), + ); + writer.close(); + disconnect(); + } else if (event === 'error') { + writer.write( + encoder.encode( + JSON.stringify({ + type: 'error', + data: data.data, + }) + '\n', + ), + ); + writer.close(); + disconnect(); + } + }); + + req.signal.addEventListener('abort', () => { + disconnect(); + writer.close(); + }); + + return new Response(responseStream.readable, { + headers: { + 'Content-Type': 'text/event-stream', + Connection: 'keep-alive', + 'Cache-Control': 'no-cache, no-transform', + }, + }); + } catch (err) { + console.error('Error in reconnecting to session stream: ', err); + return Response.json( + { message: 'An error has occurred.' }, + { status: 500 }, + ); + } +}; From a6d4f47130a69b9562fb787222c6840629592134 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:15:32 +0530 Subject: [PATCH 158/196] feat(search-agent): save history --- src/lib/agents/search/index.ts | 79 +++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/src/lib/agents/search/index.ts b/src/lib/agents/search/index.ts index 26fc13d..1ecfe51 100644 --- a/src/lib/agents/search/index.ts +++ b/src/lib/agents/search/index.ts @@ -4,9 +4,53 @@ import { classify } from './classifier'; import Researcher from './researcher'; import { getWriterPrompt } from '@/lib/prompts/search/writer'; import { WidgetExecutor } from './widgets'; +import db from '@/lib/db'; +import { chats, messages } from '@/lib/db/schema'; +import { and, eq, gt } from 'drizzle-orm'; +import { TextBlock } from '@/lib/types'; class SearchAgent { async searchAsync(session: SessionManager, input: SearchAgentInput) { + const exists = await db.query.messages.findFirst({ + where: and( + eq(messages.chatId, input.chatId), + eq(messages.messageId, input.messageId), + ), + }); + + if (!exists) { + await db.insert(messages).values({ + chatId: input.chatId, + messageId: input.messageId, + backendId: session.id, + query: input.followUp, + createdAt: new Date().toISOString(), + status: 'answering', + responseBlocks: [], + }); + } else { + await db + .delete(messages) + .where( + and(eq(messages.chatId, input.chatId), gt(messages.id, exists.id)), + ) + .execute(); + await db + .update(messages) + .set({ + status: 'answering', + backendId: session.id, + responseBlocks: [], + }) + .where( + and( + eq(messages.chatId, input.chatId), + eq(messages.messageId, input.messageId), + ), + ) + .execute(); + } + const classification = await classify({ chatHistory: input.chatHistory, enabledSources: input.config.sources, @@ -85,18 +129,41 @@ class SearchAgent { ], }); - let accumulatedText = ''; + const block: TextBlock = { + id: crypto.randomUUID(), + type: 'text', + data: '', + }; + + session.emitBlock(block); for await (const chunk of answerStream) { - accumulatedText += chunk.contentChunk; + block.data += chunk.contentChunk; - session.emit('data', { - type: 'response', - data: chunk.contentChunk, - }); + session.updateBlock(block.id, [ + { + op: 'replace', + path: '/data', + value: block.data, + }, + ]); } session.emit('end', {}); + + await db + .update(messages) + .set({ + status: 'completed', + responseBlocks: session.getAllBlocks(), + }) + .where( + and( + eq(messages.chatId, input.chatId), + eq(messages.messageId, input.messageId), + ), + ) + .execute(); } } From 8520ea6fe5b53c7c26d49da97ec00c3de28d4d00 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:15:42 +0530 Subject: [PATCH 159/196] feat(researcher): emit sources as block --- src/lib/agents/search/researcher/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/agents/search/researcher/index.ts b/src/lib/agents/search/researcher/index.ts index 814b0e3..f01b6fb 100644 --- a/src/lib/agents/search/researcher/index.ts +++ b/src/lib/agents/search/researcher/index.ts @@ -206,8 +206,9 @@ class Researcher { }) .filter((r) => r !== undefined); - session.emit('data', { - type: 'sources', + session.emitBlock({ + id: crypto.randomUUID(), + type: 'source', data: filteredSearchResults, }); From 5847379db0469aa5e71b9184eb0dda066901bab3 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:15:46 +0530 Subject: [PATCH 160/196] Update types.ts --- src/lib/agents/search/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts index 04cbe19..ac511e6 100644 --- a/src/lib/agents/search/types.ts +++ b/src/lib/agents/search/types.ts @@ -18,6 +18,8 @@ export type SearchAgentInput = { chatHistory: ChatTurnMessage[]; followUp: string; config: SearchAgentConfig; + chatId: string; + messageId: string; }; export type WidgetInput = { From 5f040346504d89829160db2c651ff50b7363d1d7 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:17:19 +0530 Subject: [PATCH 161/196] feat(chat-hook): handle reconnect --- src/lib/hooks/useChat.tsx | 208 ++++++++++++++++++++------------------ 1 file changed, 108 insertions(+), 100 deletions(-) diff --git a/src/lib/hooks/useChat.tsx b/src/lib/hooks/useChat.tsx index 6330c80..4389400 100644 --- a/src/lib/hooks/useChat.tsx +++ b/src/lib/hooks/useChat.tsx @@ -401,6 +401,50 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { }); }, [messages]); + const checkReconnect = async () => { + if (messages.length > 0) { + const lastMsg = messages[messages.length - 1]; + + if (lastMsg.status === 'answering') { + setLoading(true); + setResearchEnded(false); + setMessageAppeared(false); + + const res = await fetch(`/api/reconnect/${lastMsg.backendId}`, { + method: 'POST', + }); + + if (!res.body) throw new Error('No response body'); + + const reader = res.body?.getReader(); + const decoder = new TextDecoder('utf-8'); + + let partialChunk = ''; + + const messageHandler = getMessageHandler(lastMsg); + + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + partialChunk += decoder.decode(value, { stream: true }); + + try { + const messages = partialChunk.split('\n'); + for (const msg of messages) { + if (!msg.trim()) continue; + const json = JSON.parse(msg); + messageHandler(json); + } + partialChunk = ''; + } catch (error) { + console.warn('Incomplete JSON, waiting for next chunk...'); + } + } + } + } + }; + useEffect(() => { checkConfig( setChatModelProvider, @@ -454,13 +498,22 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { }, [messages]); useEffect(() => { - if (isMessagesLoaded && isConfigReady) { + if (isMessagesLoaded && isConfigReady && newChatCreated) { setIsReady(true); console.debug(new Date(), 'app:ready'); + } else if (isMessagesLoaded && isConfigReady && !newChatCreated) { + checkReconnect() + .then(() => { + setIsReady(true); + console.debug(new Date(), 'app:ready'); + }) + .catch((err) => { + console.error('Error during reconnect:', err); + }); } else { setIsReady(false); } - }, [isMessagesLoaded, isConfigReady]); + }, [isMessagesLoaded, isConfigReady, newChatCreated]); const rewrite = (messageId: string) => { const index = messages.findIndex((msg) => msg.messageId === messageId); @@ -488,38 +541,10 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [isConfigReady, isReady, initialMessage]); - const sendMessage: ChatContext['sendMessage'] = async ( - message, - messageId, - rewrite = false, - ) => { - if (loading || !message) return; - setLoading(true); - setResearchEnded(false); - setMessageAppeared(false); + const getMessageHandler = (message: Message) => { + const messageId = message.messageId; - if (messages.length <= 1) { - window.history.replaceState(null, '', `/c/${chatId}`); - } - - messageId = messageId ?? crypto.randomBytes(7).toString('hex'); - const backendId = crypto.randomBytes(20).toString('hex'); - - const newMessage: Message = { - messageId, - chatId: chatId!, - backendId, - query: message, - responseBlocks: [], - status: 'answering', - createdAt: new Date(), - }; - - setMessages((prevMessages) => [...prevMessages, newMessage]); - - const receivedTextRef = { current: '' }; - - const messageHandler = async (data: any) => { + return async (data: any) => { if (data.type === 'error') { toast.error(data.data); setLoading(false); @@ -536,7 +561,7 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { if (data.type === 'researchComplete') { setResearchEnded(true); if ( - newMessage.responseBlocks.find( + message.responseBlocks.find( (b) => b.type === 'source' && b.data.length > 0, ) ) { @@ -556,6 +581,13 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { return msg; }), ); + + if ( + (data.block.type === 'source' && data.block.data.length > 0) || + data.block.type === 'text' + ) { + setMessageAppeared(true); + } } if (data.type === 'updateBlock') { @@ -577,72 +609,19 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { ); } - if (data.type === 'sources') { - const sourceBlock: Block = { - id: crypto.randomBytes(7).toString('hex'), - type: 'source', - data: data.data, - }; - - setMessages((prev) => - prev.map((msg) => { - if (msg.messageId === messageId) { - return { - ...msg, - responseBlocks: [...msg.responseBlocks, sourceBlock], - }; - } - return msg; - }), - ); - if (data.data.length > 0) { - setMessageAppeared(true); - } - } - - if (data.type === 'message') { - receivedTextRef.current += data.data; - - setMessages((prev) => - prev.map((msg) => { - if (msg.messageId === messageId) { - const existingTextBlockIndex = msg.responseBlocks.findIndex( - (b) => b.type === 'text', - ); - - if (existingTextBlockIndex >= 0) { - const updatedBlocks = [...msg.responseBlocks]; - const existingBlock = updatedBlocks[ - existingTextBlockIndex - ] as Block & { type: 'text' }; - updatedBlocks[existingTextBlockIndex] = { - ...existingBlock, - data: existingBlock.data + data.data, - }; - return { ...msg, responseBlocks: updatedBlocks }; - } else { - const textBlock: Block = { - id: crypto.randomBytes(7).toString('hex'), - type: 'text', - data: data.data, - }; - return { - ...msg, - responseBlocks: [...msg.responseBlocks, textBlock], - }; - } - } - return msg; - }), - ); - setMessageAppeared(true); - } - if (data.type === 'messageEnd') { + const currentMsg = messagesRef.current.find( + (msg) => msg.messageId === messageId, + ); + const newHistory: [string, string][] = [ ...chatHistory, - ['human', message], - ['assistant', receivedTextRef.current], + ['human', message.query], + [ + 'assistant', + currentMsg?.responseBlocks.find((b) => b.type === 'text')?.data || + '', + ], ]; setChatHistory(newHistory); @@ -672,9 +651,6 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { } // Check if there are sources and no suggestions - const currentMsg = messagesRef.current.find( - (msg) => msg.messageId === messageId, - ); const hasSourceBlocks = currentMsg?.responseBlocks.some( (block) => block.type === 'source' && block.data.length > 0, @@ -705,6 +681,36 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { } } }; + }; + + const sendMessage: ChatContext['sendMessage'] = async ( + message, + messageId, + rewrite = false, + ) => { + if (loading || !message) return; + setLoading(true); + setResearchEnded(false); + setMessageAppeared(false); + + if (messages.length <= 1) { + window.history.replaceState(null, '', `/c/${chatId}`); + } + + messageId = messageId ?? crypto.randomBytes(7).toString('hex'); + const backendId = crypto.randomBytes(20).toString('hex'); + + const newMessage: Message = { + messageId, + chatId: chatId!, + backendId, + query: message, + responseBlocks: [], + status: 'answering', + createdAt: new Date(), + }; + + setMessages((prevMessages) => [...prevMessages, newMessage]); const messageIndex = messages.findIndex((m) => m.messageId === messageId); @@ -746,6 +752,8 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { let partialChunk = ''; + const messageHandler = getMessageHandler(newMessage); + while (true) { const { value, done } = await reader.read(); if (done) break; From c3b74a3fd0eb86b82851dcda6ff19f2a44e3dba0 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:17:56 +0530 Subject: [PATCH 162/196] feat(assistant-steps): only open last comp --- src/components/AssistantSteps.tsx | 10 +++++++--- src/components/MessageBox.tsx | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/AssistantSteps.tsx b/src/components/AssistantSteps.tsx index 46461e9..c715a92 100644 --- a/src/components/AssistantSteps.tsx +++ b/src/components/AssistantSteps.tsx @@ -54,17 +54,21 @@ const getStepTitle = ( const AssistantSteps = ({ block, status, + isLast, }: { block: ResearchBlock; status: 'answering' | 'completed' | 'error'; + isLast: boolean; }) => { - const [isExpanded, setIsExpanded] = useState(true); + const [isExpanded, setIsExpanded] = useState( + isLast && status === 'answering' ? true : false, + ); const { researchEnded, loading } = useChat(); useEffect(() => { - if (researchEnded) { + if (researchEnded && isLast) { setIsExpanded(false); - } else if (status === 'answering') { + } else if (status === 'answering' && isLast) { setIsExpanded(true); } }, [researchEnded, status]); diff --git a/src/components/MessageBox.tsx b/src/components/MessageBox.tsx index 93c26a6..3ea7909 100644 --- a/src/components/MessageBox.tsx +++ b/src/components/MessageBox.tsx @@ -131,6 +131,7 @@ const MessageBox = ({ <AssistantSteps block={researchBlock} status={section.message.status} + isLast={isLast} /> </div> ))} From 2c65bd916b1e1faab04679a1f4a24114e89fe6fc Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:29:14 +0530 Subject: [PATCH 163/196] feat(chat-hook): set ready before reconnecting --- src/lib/hooks/useChat.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/lib/hooks/useChat.tsx b/src/lib/hooks/useChat.tsx index 4389400..3559b7c 100644 --- a/src/lib/hooks/useChat.tsx +++ b/src/lib/hooks/useChat.tsx @@ -269,6 +269,7 @@ export const chatContext = createContext<ChatContext>({ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { const params: { chatId: string } = useParams(); + const searchParams = useSearchParams(); const initialMessage = searchParams.get('q'); @@ -402,6 +403,9 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { }, [messages]); const checkReconnect = async () => { + setIsReady(true); + console.debug(new Date(), 'app:ready'); + if (messages.length > 0) { const lastMsg = messages[messages.length - 1]; @@ -503,13 +507,6 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { console.debug(new Date(), 'app:ready'); } else if (isMessagesLoaded && isConfigReady && !newChatCreated) { checkReconnect() - .then(() => { - setIsReady(true); - console.debug(new Date(), 'app:ready'); - }) - .catch((err) => { - console.error('Error during reconnect:', err); - }); } else { setIsReady(false); } From b706434bac23ba0db8f67baaaf2be918e58a4cd1 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:56:15 +0530 Subject: [PATCH 164/196] feat(chat-window): display only when ready --- src/components/ChatWindow.tsx | 41 +++++++---------------------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/src/components/ChatWindow.tsx b/src/components/ChatWindow.tsx index 9489219..19cc542 100644 --- a/src/components/ChatWindow.tsx +++ b/src/components/ChatWindow.tsx @@ -6,7 +6,8 @@ import EmptyChat from './EmptyChat'; import NextError from 'next/error'; import { useChat } from '@/lib/hooks/useChat'; import SettingsButtonMobile from './Settings/SettingsButtonMobile'; -import { Block, Chunk } from '@/lib/types'; +import { Block } from '@/lib/types'; +import Loader from './ui/Loader'; export interface BaseMessage { chatId: string; @@ -21,35 +22,6 @@ export interface Message extends BaseMessage { status: 'answering' | 'completed' | 'error'; } -export interface UserMessage extends BaseMessage { - role: 'user'; - content: string; -} - -export interface AssistantMessage extends BaseMessage { - role: 'assistant'; - content: string; - suggestions?: string[]; -} - -export interface SourceMessage extends BaseMessage { - role: 'source'; - sources: Chunk[]; -} - -export interface SuggestionMessage extends BaseMessage { - role: 'suggestion'; - suggestions: string[]; -} - -export type LegacyMessage = - | AssistantMessage - | UserMessage - | SourceMessage - | SuggestionMessage; - -export type ChatTurn = UserMessage | AssistantMessage; - export interface File { fileName: string; fileExtension: string; @@ -62,7 +34,8 @@ export interface Widget { } const ChatWindow = () => { - const { hasError, notFound, messages } = useChat(); + const { hasError, notFound, messages, isReady } = useChat(); + if (hasError) { return ( <div className="relative"> @@ -78,7 +51,7 @@ const ChatWindow = () => { ); } - return notFound ? ( + return isReady ? notFound ? ( <NextError statusCode={404} /> ) : ( <div> @@ -91,7 +64,9 @@ const ChatWindow = () => { <EmptyChat /> )} </div> - ); + ) : <div className='flex items-center justify-center min-h-screen w-full'> + <Loader /> + </div>; }; export default ChatWindow; From ea18c13326795ed24ecfa3227c3783b78e9ce1b0 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 23 Dec 2025 18:38:25 +0530 Subject: [PATCH 165/196] feat(app): remove uploads --- uploads/.gitignore | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 uploads/.gitignore diff --git a/uploads/.gitignore b/uploads/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/uploads/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore From b47f522bf29331e7c61160c2355cf50750028fad Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 23 Dec 2025 18:40:30 +0530 Subject: [PATCH 166/196] feat(app): update guide for run command --- README.md | 6 +++--- docker-compose.yaml | 2 ++ docs/installation/UPDATING.md | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index dc654df..75b741d 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ There are mainly 2 ways of installing Perplexica - With Docker, Without Docker. Perplexica can be easily run using Docker. Simply run the following command: ```bash -docker run -d -p 3000:3000 -v perplexica-data:/home/perplexica/data -v perplexica-uploads:/home/perplexica/uploads --name perplexica itzcrazykns1337/perplexica:latest +docker run -d -p 3000:3000 -v perplexica-data:/home/perplexica/data --name perplexica itzcrazykns1337/perplexica:latest ``` This will pull and start the Perplexica container with the bundled SearxNG search engine. Once running, open your browser and navigate to http://localhost:3000. You can then configure your settings (API keys, models, etc.) directly in the setup screen. @@ -93,7 +93,7 @@ This will pull and start the Perplexica container with the bundled SearxNG searc If you already have SearxNG running, you can use the slim version of Perplexica: ```bash -docker run -d -p 3000:3000 -e SEARXNG_API_URL=http://your-searxng-url:8080 -v perplexica-data:/home/perplexica/data -v perplexica-uploads:/home/perplexica/uploads --name perplexica itzcrazykns1337/perplexica:slim-latest +docker run -d -p 3000:3000 -e SEARXNG_API_URL=http://your-searxng-url:8080 -v perplexica-data:/home/perplexica/data --name perplexica itzcrazykns1337/perplexica:slim-latest ``` **Important**: Make sure your SearxNG instance has: @@ -120,7 +120,7 @@ If you prefer to build from source or need more control: ```bash docker build -t perplexica . - docker run -d -p 3000:3000 -v perplexica-data:/home/perplexica/data -v perplexica-uploads:/home/perplexica/uploads --name perplexica perplexica + docker run -d -p 3000:3000 -v perplexica-data:/home/perplexica/data --name perplexica perplexica ``` 5. Access Perplexica at http://localhost:3000 and configure your settings in the setup screen. diff --git a/docker-compose.yaml b/docker-compose.yaml index 50b6785..e2c245d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,6 +1,8 @@ services: perplexica: image: itzcrazykns1337/perplexica:latest + build: + context: . ports: - '3000:3000' volumes: diff --git a/docs/installation/UPDATING.md b/docs/installation/UPDATING.md index 0603671..4f2be75 100644 --- a/docs/installation/UPDATING.md +++ b/docs/installation/UPDATING.md @@ -10,7 +10,7 @@ Simply pull the latest image and restart your container: docker pull itzcrazykns1337/perplexica:latest docker stop perplexica docker rm perplexica -docker run -d -p 3000:3000 -v perplexica-data:/home/perplexica/data -v perplexica-uploads:/home/perplexica/uploads --name perplexica itzcrazykns1337/perplexica:latest +docker run -d -p 3000:3000 -v perplexica-data:/home/perplexica/data --name perplexica itzcrazykns1337/perplexica:latest ``` For slim version: @@ -19,7 +19,7 @@ For slim version: docker pull itzcrazykns1337/perplexica:slim-latest docker stop perplexica docker rm perplexica -docker run -d -p 3000:3000 -e SEARXNG_API_URL=http://your-searxng-url:8080 -v perplexica-data:/home/perplexica/data -v perplexica-uploads:/home/perplexica/uploads --name perplexica itzcrazykns1337/perplexica:slim-latest +docker run -d -p 3000:3000 -e SEARXNG_API_URL=http://your-searxng-url:8080 -v perplexica-data:/home/perplexica/data --name perplexica itzcrazykns1337/perplexica:slim-latest ``` Once updated, go to http://localhost:3000 and verify the latest changes. Your settings are preserved automatically. From 24c32ed88194af49905b8c5484d1edb3f85398b7 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 23 Dec 2025 18:53:40 +0530 Subject: [PATCH 167/196] feat(app): enhance attach transition --- src/components/MessageInputActions/Attach.tsx | 161 ++++++++-------- .../MessageInputActions/AttachSmall.tsx | 172 +++++++++--------- 2 files changed, 173 insertions(+), 160 deletions(-) diff --git a/src/components/MessageInputActions/Attach.tsx b/src/components/MessageInputActions/Attach.tsx index fbc2e7e..84d7152 100644 --- a/src/components/MessageInputActions/Attach.tsx +++ b/src/components/MessageInputActions/Attach.tsx @@ -16,6 +16,8 @@ import { } from 'lucide-react'; import { Fragment, useRef, useState } from 'react'; import { useChat } from '@/lib/hooks/useChat'; +import { AnimatePresence } from 'motion/react'; +import { motion } from 'framer-motion'; const Attach = () => { const { files, setFiles, setFileIds, fileIds } = useChat(); @@ -53,86 +55,95 @@ const Attach = () => { return loading ? ( <div className="active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none text-black/50 dark:text-white/50 transition duration-200"> - <LoaderCircle size={16} className="text-sky-400 animate-spin" /> + <LoaderCircle size={16} className="text-sky-500 animate-spin" /> </div> ) : files.length > 0 ? ( <Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg"> - <PopoverButton - type="button" - className="active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none headless-open:text-black dark:headless-open:text-white text-black/50 dark:text-white/50 active:scale-95 transition duration-200 hover:text-black dark:hover:text-white" - > - <File size={16} className="text-sky-400" /> - </PopoverButton> - <Transition - as={Fragment} - enter="transition ease-out duration-150" - enterFrom="opacity-0 translate-y-1" - enterTo="opacity-100 translate-y-0" - leave="transition ease-in duration-150" - leaveFrom="opacity-100 translate-y-0" - leaveTo="opacity-0 translate-y-1" - > - <PopoverPanel className="absolute z-10 w-64 md:w-[350px] right-0"> - <div className="bg-light-primary dark:bg-dark-primary border rounded-md border-light-200 dark:border-dark-200 w-full max-h-[200px] md:max-h-none overflow-y-auto flex flex-col"> - <div className="flex flex-row items-center justify-between px-3 py-2"> - <h4 className="text-black dark:text-white font-medium text-sm"> - Attached files - </h4> - <div className="flex flex-row items-center space-x-4"> - <button - type="button" - onClick={() => fileInputRef.current.click()} - className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200 focus:outline-none" + {({ open }) => ( + <> + <PopoverButton + type="button" + className="active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none headless-open:text-black dark:headless-open:text-white text-black/50 dark:text-white/50 active:scale-95 transition duration-200 hover:text-black dark:hover:text-white" + > + <File size={16} className="text-sky-500" /> + </PopoverButton> + <AnimatePresence> + {open && ( + <PopoverPanel + className="absolute z-10 w-64 md:w-[350px] right-0" + static + > + <motion.div + initial={{ opacity: 0, scale: 0.9 }} + animate={{ opacity: 1, scale: 1 }} + exit={{ opacity: 0, scale: 0.9 }} + transition={{ duration: 0.1, ease: 'easeOut' }} + className="origin-top-right bg-light-primary dark:bg-dark-primary border rounded-md border-light-200 dark:border-dark-200 w-full max-h-[200px] md:max-h-none overflow-y-auto flex flex-col" > - <input - type="file" - onChange={handleChange} - ref={fileInputRef} - accept=".pdf,.docx,.txt" - multiple - hidden - /> - <Plus size={16} /> - <p className="text-xs">Add</p> - </button> - <button - onClick={() => { - setFiles([]); - setFileIds([]); - }} - className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200 focus:outline-none" - > - <Trash size={14} /> - <p className="text-xs">Clear</p> - </button> - </div> - </div> - <div className="h-[0.5px] mx-2 bg-white/10" /> - <div className="flex flex-col items-center"> - {files.map((file, i) => ( - <div - key={i} - className="flex flex-row items-center justify-start w-full space-x-3 p-3" - > - <div className="bg-light-100 dark:bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md"> - <File - size={16} - className="text-black/70 dark:text-white/70" - /> + <div className="flex flex-row items-center justify-between px-3 py-2"> + <h4 className="text-black/70 dark:text-white/70 text-sm"> + Attached files + </h4> + <div className="flex flex-row items-center space-x-4"> + <button + type="button" + onClick={() => fileInputRef.current.click()} + className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200 focus:outline-none" + > + <input + type="file" + onChange={handleChange} + ref={fileInputRef} + accept=".pdf,.docx,.txt" + multiple + hidden + /> + <Plus size={16} /> + <p className="text-xs">Add</p> + </button> + <button + onClick={() => { + setFiles([]); + setFileIds([]); + }} + className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200 focus:outline-none" + > + <Trash size={13} /> + <p className="text-xs">Clear</p> + </button> + </div> </div> - <p className="text-black/70 dark:text-white/70 text-sm"> - {file.fileName.length > 25 - ? file.fileName.replace(/\.\w+$/, '').substring(0, 25) + - '...' + - file.fileExtension - : file.fileName} - </p> - </div> - ))} - </div> - </div> - </PopoverPanel> - </Transition> + <div className="h-[0.5px] mx-2 bg-white/10" /> + <div className="flex flex-col items-center"> + {files.map((file, i) => ( + <div + key={i} + className="flex flex-row items-center justify-start w-full space-x-3 p-3" + > + <div className="bg-light-100 dark:bg-dark-100 flex items-center justify-center w-9 h-9 rounded-md"> + <File + size={16} + className="text-black/70 dark:text-white/70" + /> + </div> + <p className="text-black/70 dark:text-white/70 text-xs"> + {file.fileName.length > 25 + ? file.fileName + .replace(/\.\w+$/, '') + .substring(0, 25) + + '...' + + file.fileExtension + : file.fileName} + </p> + </div> + ))} + </div> + </motion.div> + </PopoverPanel> + )} + </AnimatePresence> + </> + )} </Popover> ) : ( <button diff --git a/src/components/MessageInputActions/AttachSmall.tsx b/src/components/MessageInputActions/AttachSmall.tsx index ca18f4e..828ac79 100644 --- a/src/components/MessageInputActions/AttachSmall.tsx +++ b/src/components/MessageInputActions/AttachSmall.tsx @@ -1,21 +1,14 @@ -import { cn } from '@/lib/utils'; import { Popover, PopoverButton, PopoverPanel, Transition, } from '@headlessui/react'; -import { - CopyPlus, - File, - LoaderCircle, - Paperclip, - Plus, - Trash, -} from 'lucide-react'; +import { File, LoaderCircle, Paperclip, Plus, Trash } from 'lucide-react'; import { Fragment, useRef, useState } from 'react'; -import { File as FileType } from '../ChatWindow'; import { useChat } from '@/lib/hooks/useChat'; +import { AnimatePresence } from 'motion/react'; +import { motion } from 'framer-motion'; const AttachSmall = () => { const { files, setFiles, setFileIds, fileIds } = useChat(); @@ -53,86 +46,95 @@ const AttachSmall = () => { return loading ? ( <div className="flex flex-row items-center justify-between space-x-1 p-1 "> - <LoaderCircle size={20} className="text-sky-400 animate-spin" /> + <LoaderCircle size={20} className="text-sky-500 animate-spin" /> </div> ) : files.length > 0 ? ( <Popover className="max-w-[15rem] md:max-w-md lg:max-w-lg"> - <PopoverButton - type="button" - className="flex flex-row items-center justify-between space-x-1 p-1 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white" - > - <File size={20} className="text-sky-400" /> - </PopoverButton> - <Transition - as={Fragment} - enter="transition ease-out duration-150" - enterFrom="opacity-0 translate-y-1" - enterTo="opacity-100 translate-y-0" - leave="transition ease-in duration-150" - leaveFrom="opacity-100 translate-y-0" - leaveTo="opacity-0 translate-y-1" - > - <PopoverPanel className="absolute z-10 w-64 md:w-[350px] bottom-14 -ml-3"> - <div className="bg-light-primary dark:bg-dark-primary border rounded-md border-light-200 dark:border-dark-200 w-full max-h-[200px] md:max-h-none overflow-y-auto flex flex-col"> - <div className="flex flex-row items-center justify-between px-3 py-2"> - <h4 className="text-black dark:text-white font-medium text-sm"> - Attached files - </h4> - <div className="flex flex-row items-center space-x-4"> - <button - type="button" - onClick={() => fileInputRef.current.click()} - className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200" + {({ open }) => ( + <> + <PopoverButton + type="button" + className="flex flex-row items-center justify-between space-x-1 p-1 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white" + > + <File size={20} className="text-sky-500" /> + </PopoverButton> + <AnimatePresence> + {open && ( + <PopoverPanel + className="absolute z-10 w-64 md:w-[350px] bottom-14" + static + > + <motion.div + initial={{ opacity: 0, scale: 0.9 }} + animate={{ opacity: 1, scale: 1 }} + exit={{ opacity: 0, scale: 0.9 }} + transition={{ duration: 0.1, ease: 'easeOut' }} + className="origin-bottom-left bg-light-primary dark:bg-dark-primary border rounded-md border-light-200 dark:border-dark-200 w-full max-h-[200px] md:max-h-none overflow-y-auto flex flex-col" > - <input - type="file" - onChange={handleChange} - ref={fileInputRef} - accept=".pdf,.docx,.txt" - multiple - hidden - /> - <Plus size={18} /> - <p className="text-xs">Add</p> - </button> - <button - onClick={() => { - setFiles([]); - setFileIds([]); - }} - className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200" - > - <Trash size={14} /> - <p className="text-xs">Clear</p> - </button> - </div> - </div> - <div className="h-[0.5px] mx-2 bg-white/10" /> - <div className="flex flex-col items-center"> - {files.map((file, i) => ( - <div - key={i} - className="flex flex-row items-center justify-start w-full space-x-3 p-3" - > - <div className="bg-light-100 dark:bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md"> - <File - size={16} - className="text-black/70 dark:text-white/70" - /> + <div className="flex flex-row items-center justify-between px-3 py-2"> + <h4 className="text-black/70 dark:text-white/70 font-medium text-sm"> + Attached files + </h4> + <div className="flex flex-row items-center space-x-4"> + <button + type="button" + onClick={() => fileInputRef.current.click()} + className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200" + > + <input + type="file" + onChange={handleChange} + ref={fileInputRef} + accept=".pdf,.docx,.txt" + multiple + hidden + /> + <Plus size={16} /> + <p className="text-xs">Add</p> + </button> + <button + onClick={() => { + setFiles([]); + setFileIds([]); + }} + className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200" + > + <Trash size={13} /> + <p className="text-xs">Clear</p> + </button> + </div> </div> - <p className="text-black/70 dark:text-white/70 text-sm"> - {file.fileName.length > 25 - ? file.fileName.replace(/\.\w+$/, '').substring(0, 25) + - '...' + - file.fileExtension - : file.fileName} - </p> - </div> - ))} - </div> - </div> - </PopoverPanel> - </Transition> + <div className="h-[0.5px] mx-2 bg-white/10" /> + <div className="flex flex-col items-center"> + {files.map((file, i) => ( + <div + key={i} + className="flex flex-row items-center justify-start w-full space-x-3 p-3" + > + <div className="bg-light-100 dark:bg-dark-100 flex items-center justify-center w-9 h-9 rounded-md"> + <File + size={16} + className="text-black/70 dark:text-white/70" + /> + </div> + <p className="text-black/70 dark:text-white/70 text-xs"> + {file.fileName.length > 25 + ? file.fileName + .replace(/\.\w+$/, '') + .substring(0, 25) + + '...' + + file.fileExtension + : file.fileName} + </p> + </div> + ))} + </div> + </motion.div> + </PopoverPanel> + )} + </AnimatePresence> + </> + )} </Popover> ) : ( <button From cf95ea0af71dc3f9125a2b306fec63303d02c384 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 23 Dec 2025 18:54:01 +0530 Subject: [PATCH 168/196] feat(app): lint & beautify --- src/app/c/[chatId]/page.tsx | 7 +--- src/components/ChatWindow.tsx | 32 +++++++++++-------- .../MessageInputActions/Optimization.tsx | 4 +-- src/lib/hooks/useChat.tsx | 4 +-- 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/app/c/[chatId]/page.tsx b/src/app/c/[chatId]/page.tsx index 39b93f0..06cd823 100644 --- a/src/app/c/[chatId]/page.tsx +++ b/src/app/c/[chatId]/page.tsx @@ -1,10 +1,5 @@ 'use client'; import ChatWindow from '@/components/ChatWindow'; -import React from 'react'; -const Page = () => { - return <ChatWindow />; -}; - -export default Page; +export default ChatWindow; diff --git a/src/components/ChatWindow.tsx b/src/components/ChatWindow.tsx index 19cc542..a2a9f67 100644 --- a/src/components/ChatWindow.tsx +++ b/src/components/ChatWindow.tsx @@ -51,22 +51,26 @@ const ChatWindow = () => { ); } - return isReady ? notFound ? ( - <NextError statusCode={404} /> + return isReady ? ( + notFound ? ( + <NextError statusCode={404} /> + ) : ( + <div> + {messages.length > 0 ? ( + <> + <Navbar /> + <Chat /> + </> + ) : ( + <EmptyChat /> + )} + </div> + ) ) : ( - <div> - {messages.length > 0 ? ( - <> - <Navbar /> - <Chat /> - </> - ) : ( - <EmptyChat /> - )} + <div className="flex items-center justify-center min-h-screen w-full"> + <Loader /> </div> - ) : <div className='flex items-center justify-center min-h-screen w-full'> - <Loader /> - </div>; + ); }; export default ChatWindow; diff --git a/src/components/MessageInputActions/Optimization.tsx b/src/components/MessageInputActions/Optimization.tsx index bc56ef2..2f0cd82 100644 --- a/src/components/MessageInputActions/Optimization.tsx +++ b/src/components/MessageInputActions/Optimization.tsx @@ -86,12 +86,12 @@ const Optimization = () => { )} > <div className="flex flex-row justify-between w-full text-black dark:text-white"> - <div className='flex flex-row space-x-1'> + <div className="flex flex-row space-x-1"> {mode.icon} <p className="text-xs font-medium">{mode.title}</p> </div> {mode.key === 'quality' && ( - <span className='bg-sky-500/70 dark:bg-sky-500/40 border border-sky-600 px-1 rounded-full text-[10px] text-white'> + <span className="bg-sky-500/70 dark:bg-sky-500/40 border border-sky-600 px-1 rounded-full text-[10px] text-white"> Beta </span> )} diff --git a/src/lib/hooks/useChat.tsx b/src/lib/hooks/useChat.tsx index 3559b7c..fdb5743 100644 --- a/src/lib/hooks/useChat.tsx +++ b/src/lib/hooks/useChat.tsx @@ -269,7 +269,7 @@ export const chatContext = createContext<ChatContext>({ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { const params: { chatId: string } = useParams(); - + const searchParams = useSearchParams(); const initialMessage = searchParams.get('q'); @@ -506,7 +506,7 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => { setIsReady(true); console.debug(new Date(), 'app:ready'); } else if (isMessagesLoaded && isConfigReady && !newChatCreated) { - checkReconnect() + checkReconnect(); } else { setIsReady(false); } From eca66f0b5fdc0497b93ac57770f392ac78e7baa5 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:53:09 +0530 Subject: [PATCH 169/196] feat(writer): add system instructions, send response block on response --- src/app/api/chat/route.ts | 1 + src/lib/agents/search/index.ts | 48 +++++++++++++++++++++----------- src/lib/prompts/search/writer.ts | 12 ++++++-- 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 67270ce..6362ebc 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -221,6 +221,7 @@ export const POST = async (req: Request) => { sources: body.sources as SearchSources[], mode: body.optimizationMode, fileIds: body.files, + systemInstructions: body.systemInstructions || 'None', }, }); diff --git a/src/lib/agents/search/index.ts b/src/lib/agents/search/index.ts index 1ecfe51..8591832 100644 --- a/src/lib/agents/search/index.ts +++ b/src/lib/agents/search/index.ts @@ -114,7 +114,11 @@ class SearchAgent { const finalContextWithWidgets = `<search_results note="These are the search results and assistant can cite these">\n${finalContext}\n</search_results>\n<widgets_result noteForAssistant="Its output is already showed to the user, assistant can use this information to answer the query but do not CITE this as a souce">\n${widgetContext}\n</widgets_result>`; - const writerPrompt = getWriterPrompt(finalContextWithWidgets); + const writerPrompt = getWriterPrompt( + finalContextWithWidgets, + input.config.systemInstructions, + input.config.mode, + ); const answerStream = input.config.llm.streamText({ messages: [ { @@ -129,24 +133,36 @@ class SearchAgent { ], }); - const block: TextBlock = { - id: crypto.randomUUID(), - type: 'text', - data: '', - }; - - session.emitBlock(block); + let responseBlockId = ''; for await (const chunk of answerStream) { - block.data += chunk.contentChunk; + if (!responseBlockId) { + const block: TextBlock = { + id: crypto.randomUUID(), + type: 'text', + data: chunk.contentChunk, + }; - session.updateBlock(block.id, [ - { - op: 'replace', - path: '/data', - value: block.data, - }, - ]); + session.emitBlock(block); + + responseBlockId = block.id; + } else { + const block = session.getBlock(responseBlockId) as TextBlock | null; + + if (!block) { + continue; + } + + block.data += chunk.contentChunk; + + session.updateBlock(block.id, [ + { + op: 'replace', + path: '/data', + value: block.data, + }, + ]); + } } session.emit('end', {}); diff --git a/src/lib/prompts/search/writer.ts b/src/lib/prompts/search/writer.ts index 584ed0e..02ec6de 100644 --- a/src/lib/prompts/search/writer.ts +++ b/src/lib/prompts/search/writer.ts @@ -1,4 +1,8 @@ -export const getWriterPrompt = (context: string) => { +export const getWriterPrompt = ( + context: string, + systemInstructions: string, + mode: 'speed' | 'balanced' | 'quality', +) => { return ` You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses. @@ -29,7 +33,11 @@ You are Perplexica, an AI model skilled in web search and crafting detailed, eng - If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity. - 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. - + ${mode === 'quality' ? "- YOU ARE CURRENTLY SET IN QUALITY MODE, GENERATE VERY DEEP, DETAILED AND COMPREHENSIVE RESPONSES USING THE FULL CONTEXT PROVIDED. ASSISTANT'S RESPONSES SHALL NOT BE LESS THAN AT LEAST 2000 WORDS, COVER EVERYTHING AND FRAME IT LIKE A RESEARCH REPORT." : ''} + + ### User instructions + These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines. + ${systemInstructions} ### Example Output - Begin with a brief introduction summarizing the event or query topic. From 53697bb42eb78d9f297de078881b26e4c79500cd Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:53:35 +0530 Subject: [PATCH 170/196] feat(classifier-prompt): add calculation widget --- src/lib/prompts/search/classifier.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/prompts/search/classifier.ts b/src/lib/prompts/search/classifier.ts index a353ec1..770b86d 100644 --- a/src/lib/prompts/search/classifier.ts +++ b/src/lib/prompts/search/classifier.ts @@ -55,7 +55,8 @@ You must respond in the following JSON format without any extra text, explanatio "academicSearch": boolean, "discussionSearch": boolean, "showWeatherWidget": boolean, - "showStockWidget": boolean + "showStockWidget": boolean, + "showCalculationWidget": boolean, }, "standaloneFollowUp": string } From dc74e7174ff14bb4cc227458e337ffa200f006da Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:54:49 +0530 Subject: [PATCH 171/196] feat(researcher): rename `0_reasoning` to `__reasoning_preamble` to comply with provider guidelines --- .../agents/search/researcher/actions/done.ts | 2 +- .../agents/search/researcher/actions/plan.ts | 2 +- .../search/researcher/actions/webSearch.ts | 6 ++--- src/lib/agents/search/researcher/index.ts | 4 +-- src/lib/prompts/search/researcher.ts | 26 +++++++++---------- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/lib/agents/search/researcher/actions/done.ts b/src/lib/agents/search/researcher/actions/done.ts index 3c3e5f3..d2c4ed6 100644 --- a/src/lib/agents/search/researcher/actions/done.ts +++ b/src/lib/agents/search/researcher/actions/done.ts @@ -11,7 +11,7 @@ const doneAction: ResearchAction<any> = { name: 'done', schema: z.object({}), getToolDescription: () => - 'Only call this after 0_reasoning AND after any other needed tool calls when you truly have enough to answer. Do not call if information is still missing.', + 'Only call this after __reasoning_preamble AND after any other needed tool calls when you truly have enough to answer. Do not call if information is still missing.', getDescription: () => actionDescription, enabled: (_) => true, execute: async (params, additionalConfig) => { diff --git a/src/lib/agents/search/researcher/actions/plan.ts b/src/lib/agents/search/researcher/actions/plan.ts index 05f2e13..d89d64c 100644 --- a/src/lib/agents/search/researcher/actions/plan.ts +++ b/src/lib/agents/search/researcher/actions/plan.ts @@ -23,7 +23,7 @@ YOU CAN NEVER CALL ANY OTHER TOOL BEFORE CALLING THIS ONE FIRST, IF YOU DO, THAT `; const planAction: ResearchAction<typeof schema> = { - name: '0_reasoning', + name: '__reasoning_preamble', schema: schema, getToolDescription: () => 'Use this FIRST on every turn to state your plan in natural language before any other action. Keep it short, action-focused, and tailored to the current query.', diff --git a/src/lib/agents/search/researcher/actions/webSearch.ts b/src/lib/agents/search/researcher/actions/webSearch.ts index 3c712a2..4d60b79 100644 --- a/src/lib/agents/search/researcher/actions/webSearch.ts +++ b/src/lib/agents/search/researcher/actions/webSearch.ts @@ -32,11 +32,11 @@ Start initially with broader queries to get an overview, then narrow down with m Your queries shouldn't be sentences but rather keywords that are SEO friendly and can be used to search the web for information. For example if the user is asking about Tesla, your actions should be like: -1. 0_reasoning "The user is asking about Tesla. I will start with broader queries to get an overview of Tesla, then narrow down with more specific queries based on the results I receive." then +1. __reasoning_preamble "The user is asking about Tesla. I will start with broader queries to get an overview of Tesla, then narrow down with more specific queries based on the results I receive." then 2. web_search ["Tesla", "Tesla latest news", "Tesla stock price"] then -3. 0_reasoning "Based on the previous search results, I will now narrow down my queries to focus on Tesla's recent developments and stock performance." then +3. __reasoning_preamble "Based on the previous search results, I will now narrow down my queries to focus on Tesla's recent developments and stock performance." then 4. web_search ["Tesla Q2 2025 earnings", "Tesla new model 2025", "Tesla stock analysis"] then done. -5. 0_reasoning "I have gathered enough information to provide a comprehensive answer." +5. __reasoning_preamble "I have gathered enough information to provide a comprehensive answer." 6. done. You can search for 3 queries in one go, make sure to utilize all 3 queries to maximize the information you can gather. If a question is simple, then split your queries to cover different aspects or related topics to get a comprehensive understanding. diff --git a/src/lib/agents/search/researcher/index.ts b/src/lib/agents/search/researcher/index.ts index f01b6fb..d653281 100644 --- a/src/lib/agents/search/researcher/index.ts +++ b/src/lib/agents/search/researcher/index.ts @@ -87,7 +87,7 @@ class Researcher { if (partialRes.toolCallChunk.length > 0) { partialRes.toolCallChunk.forEach((tc) => { if ( - tc.name === '0_reasoning' && + tc.name === '__reasoning_preamble' && tc.arguments['plan'] && !reasoningEmitted && block && @@ -109,7 +109,7 @@ class Researcher { }, ]); } else if ( - tc.name === '0_reasoning' && + tc.name === '__reasoning_preamble' && tc.arguments['plan'] && reasoningEmitted && block && diff --git a/src/lib/prompts/search/researcher.ts b/src/lib/prompts/search/researcher.ts index d402ad5..537d488 100644 --- a/src/lib/prompts/search/researcher.ts +++ b/src/lib/prompts/search/researcher.ts @@ -109,12 +109,12 @@ const getBalancedPrompt = ( <goal> Fulfill the user's request with concise reasoning plus focused actions. - You must call the 0_reasoning tool before every tool call in this assistant turn. Alternate: 0_reasoning → tool → 0_reasoning → tool ... and finish with 0_reasoning → done. Open each 0_reasoning with a brief intent phrase (e.g., "Okay, the user wants to...", "Searching for...", "Looking into...") and lay out your reasoning for the next step. Keep it natural language, no tool names. + You must call the __reasoning_preamble tool before every tool call in this assistant turn. Alternate: __reasoning_preamble → tool → __reasoning_preamble → tool ... and finish with __reasoning_preamble → done. Open each __reasoning_preamble with a brief intent phrase (e.g., "Okay, the user wants to...", "Searching for...", "Looking into...") and lay out your reasoning for the next step. Keep it natural language, no tool names. </goal> <core_principle> Your knowledge is outdated; if you have web search, use it to ground answers even for seemingly basic facts. - You can call at most 6 tools total per turn: up to 2 reasoning (0_reasoning counts as reasoning), 2-3 information-gathering calls, and 1 done. If you hit the cap, stop after done. + You can call at most 6 tools total per turn: up to 2 reasoning (__reasoning_preamble counts as reasoning), 2-3 information-gathering calls, and 1 done. If you hit the cap, stop after done. Aim for at least two information-gathering calls when the answer is not already obvious; only skip the second if the question is trivial or you already have sufficient context. Do not spam searches—pick the most targeted queries. </core_principle> @@ -144,7 +144,7 @@ const getBalancedPrompt = ( </examples> <available_tools> - YOU MUST CALL 0_reasoning BEFORE EVERY TOOL CALL IN THIS ASSISTANT TURN. IF YOU DO NOT CALL IT, THE TOOL CALL WILL BE IGNORED. + YOU MUST CALL __reasoning_preamble BEFORE EVERY TOOL CALL IN THIS ASSISTANT TURN. IF YOU DO NOT CALL IT, THE TOOL CALL WILL BE IGNORED. ${actionDesc} </available_tools> @@ -160,16 +160,16 @@ const getBalancedPrompt = ( 5. **Overthinking**: Keep reasoning simple and tool calls focused -6. **Skipping the reasoning step**: Always call 0_reasoning first to outline your approach before other actions +6. **Skipping the reasoning step**: Always call __reasoning_preamble first to outline your approach before other actions </mistakes_to_avoid> <response_protocol> - NEVER output normal text to the user. ONLY call tools. -- Start with 0_reasoning and call 0_reasoning before every tool call (including done): open with intent phrase ("Okay, the user wants to...", "Looking into...", etc.) and lay out your reasoning for the next step. No tool names. +- Start with __reasoning_preamble and call __reasoning_preamble before every tool call (including done): open with intent phrase ("Okay, the user wants to...", "Looking into...", etc.) and lay out your reasoning for the next step. No tool names. - Choose tools based on the action descriptions provided above. - Default to web_search when information is missing or stale; keep queries targeted (max 3 per call). -- Use at most 6 tool calls total (0_reasoning + 2-3 info calls + 0_reasoning + done). If done is called early, stop. +- Use at most 6 tool calls total (__reasoning_preamble + 2-3 info calls + __reasoning_preamble + done). If done is called early, stop. - Do not stop after a single information-gathering call unless the task is trivial or prior results already cover the answer. - Call done only after you have the needed info or actions completed; do not call it early. - Do not invent tools. Do not return JSON. @@ -210,15 +210,15 @@ const getQualityPrompt = ( <goal> Conduct the deepest, most thorough research possible. Leave no stone unturned. - Follow an iterative reason-act loop: call 0_reasoning before every tool call to outline the next step, then call the tool, then 0_reasoning again to reflect and decide the next step. Repeat until you have exhaustive coverage. - Open each 0_reasoning with a brief intent phrase (e.g., "Okay, the user wants to know about...", "From the results, it looks like...", "Now I need to dig into...") and describe what you'll do next. Keep it natural language, no tool names. + Follow an iterative reason-act loop: call __reasoning_preamble before every tool call to outline the next step, then call the tool, then __reasoning_preamble again to reflect and decide the next step. Repeat until you have exhaustive coverage. + Open each __reasoning_preamble with a brief intent phrase (e.g., "Okay, the user wants to know about...", "From the results, it looks like...", "Now I need to dig into...") and describe what you'll do next. Keep it natural language, no tool names. Finish with done only when you have comprehensive, multi-angle information. </goal> <core_principle> Your knowledge is outdated; always use the available tools to ground answers. This is DEEP RESEARCH mode—be exhaustive. Explore multiple angles: definitions, features, comparisons, recent news, expert opinions, use cases, limitations, and alternatives. - You can call up to 10 tools total per turn. Use an iterative loop: 0_reasoning → tool call(s) → 0_reasoning → tool call(s) → ... → 0_reasoning → done. + You can call up to 10 tools total per turn. Use an iterative loop: __reasoning_preamble → tool call(s) → __reasoning_preamble → tool call(s) → ... → __reasoning_preamble → done. Never settle for surface-level answers. If results hint at more depth, reason about your next step and follow up. Cross-reference information from multiple queries. </core_principle> @@ -264,7 +264,7 @@ const getQualityPrompt = ( </examples> <available_tools> - YOU MUST CALL 0_reasoning BEFORE EVERY TOOL CALL IN THIS ASSISTANT TURN. IF YOU DO NOT CALL IT, THE TOOL CALL WILL BE IGNORED. + YOU MUST CALL __reasoning_preamble BEFORE EVERY TOOL CALL IN THIS ASSISTANT TURN. IF YOU DO NOT CALL IT, THE TOOL CALL WILL BE IGNORED. ${actionDesc} </available_tools> @@ -291,14 +291,14 @@ const getQualityPrompt = ( 5. **Premature done**: Don't call done until you've exhausted reasonable research avenues -6. **Skipping the reasoning step**: Always call 0_reasoning first to outline your research strategy +6. **Skipping the reasoning step**: Always call __reasoning_preamble first to outline your research strategy </mistakes_to_avoid> <response_protocol> - NEVER output normal text to the user. ONLY call tools. -- Follow an iterative loop: 0_reasoning → tool call → 0_reasoning → tool call → ... → 0_reasoning → done. -- Each 0_reasoning should reflect on previous results (if any) and state the next research step. No tool names in the reasoning. +- Follow an iterative loop: __reasoning_preamble → tool call → __reasoning_preamble → tool call → ... → __reasoning_preamble → done. +- Each __reasoning_preamble should reflect on previous results (if any) and state the next research step. No tool names in the reasoning. - Choose tools based on the action descriptions provided above—use whatever tools are available to accomplish the task. - Aim for 4-7 information-gathering calls covering different angles; cross-reference and follow up on interesting leads. - Call done only after comprehensive, multi-angle research is complete. From 6ae885e0ed334ce9610613f2866aa71d3eb0ede1 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:55:07 +0530 Subject: [PATCH 172/196] feat(steps): display after loading animation --- src/components/MessageBox.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MessageBox.tsx b/src/components/MessageBox.tsx index 3ea7909..19e3546 100644 --- a/src/components/MessageBox.tsx +++ b/src/components/MessageBox.tsx @@ -136,8 +136,6 @@ const MessageBox = ({ </div> ))} - {section.widgets.length > 0 && <Renderer widgets={section.widgets} />} - {isLast && loading && !researchEnded && @@ -152,6 +150,8 @@ const MessageBox = ({ </div> )} + {section.widgets.length > 0 && <Renderer widgets={section.widgets} />} + <div className="flex flex-col space-y-2"> {sources.length > 0 && ( <div className="flex flex-row items-center space-x-2"> From d4c276ab931537eb496fd5862bbcd80dce0347e2 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:55:12 +0530 Subject: [PATCH 173/196] Update types.ts --- src/lib/agents/search/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts index ac511e6..64c967e 100644 --- a/src/lib/agents/search/types.ts +++ b/src/lib/agents/search/types.ts @@ -12,6 +12,7 @@ export type SearchAgentConfig = { llm: BaseLLM<any>; embedding: BaseEmbedding<any>; mode: 'speed' | 'balanced' | 'quality'; + systemInstructions: string; }; export type SearchAgentInput = { From c4349f3d5c5a5433f89a2efe331399cda0f12930 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:55:32 +0530 Subject: [PATCH 174/196] feat(providers): add gemini --- .../providers/gemini/geminiEmbedding.ts | 5 + src/lib/models/providers/gemini/geminiLLM.ts | 5 + src/lib/models/providers/gemini/index.ts | 144 ++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 src/lib/models/providers/gemini/geminiEmbedding.ts create mode 100644 src/lib/models/providers/gemini/geminiLLM.ts create mode 100644 src/lib/models/providers/gemini/index.ts diff --git a/src/lib/models/providers/gemini/geminiEmbedding.ts b/src/lib/models/providers/gemini/geminiEmbedding.ts new file mode 100644 index 0000000..0054853 --- /dev/null +++ b/src/lib/models/providers/gemini/geminiEmbedding.ts @@ -0,0 +1,5 @@ +import OpenAIEmbedding from '../openai/openaiEmbedding'; + +class GeminiEmbedding extends OpenAIEmbedding {} + +export default GeminiEmbedding; diff --git a/src/lib/models/providers/gemini/geminiLLM.ts b/src/lib/models/providers/gemini/geminiLLM.ts new file mode 100644 index 0000000..0a0d4ff --- /dev/null +++ b/src/lib/models/providers/gemini/geminiLLM.ts @@ -0,0 +1,5 @@ +import OpenAILLM from '../openai/openaiLLM'; + +class GeminiLLM extends OpenAILLM {} + +export default GeminiLLM; diff --git a/src/lib/models/providers/gemini/index.ts b/src/lib/models/providers/gemini/index.ts new file mode 100644 index 0000000..2eb92cd --- /dev/null +++ b/src/lib/models/providers/gemini/index.ts @@ -0,0 +1,144 @@ +import { UIConfigField } from '@/lib/config/types'; +import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry'; +import { Model, ModelList, ProviderMetadata } from '../../types'; +import GeminiEmbedding from './geminiEmbedding'; +import BaseEmbedding from '../../base/embedding'; +import BaseModelProvider from '../../base/provider'; +import BaseLLM from '../../base/llm'; +import GeminiLLM from './geminiLLM'; + +interface GeminiConfig { + apiKey: string; +} + +const providerConfigFields: UIConfigField[] = [ + { + type: 'password', + name: 'API Key', + key: 'apiKey', + description: 'Your Gemini API key', + required: true, + placeholder: 'Gemini API Key', + env: 'GEMINI_API_KEY', + scope: 'server', + }, +]; + +class GeminiProvider extends BaseModelProvider<GeminiConfig> { + constructor(id: string, name: string, config: GeminiConfig) { + super(id, name, config); + } + + async getDefaultModels(): Promise<ModelList> { + const res = await fetch( + `https://generativelanguage.googleapis.com/v1beta/models?key=${this.config.apiKey}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + const data = await res.json(); + + let defaultEmbeddingModels: Model[] = []; + let defaultChatModels: Model[] = []; + + data.models.forEach((m: any) => { + if ( + m.supportedGenerationMethods.some( + (genMethod: string) => + genMethod === 'embedText' || genMethod === 'embedContent', + ) + ) { + defaultEmbeddingModels.push({ + key: m.name, + name: m.displayName, + }); + } else if (m.supportedGenerationMethods.includes('generateContent')) { + defaultChatModels.push({ + key: m.name, + name: m.displayName, + }); + } + }); + + return { + embedding: defaultEmbeddingModels, + chat: defaultChatModels, + }; + } + + async getModelList(): Promise<ModelList> { + const defaultModels = await this.getDefaultModels(); + const configProvider = getConfiguredModelProviderById(this.id)!; + + return { + embedding: [ + ...defaultModels.embedding, + ...configProvider.embeddingModels, + ], + chat: [...defaultModels.chat, ...configProvider.chatModels], + }; + } + + async loadChatModel(key: string): Promise<BaseLLM<any>> { + const modelList = await this.getModelList(); + + const exists = modelList.chat.find((m) => m.key === key); + + if (!exists) { + throw new Error( + 'Error Loading Gemini Chat Model. Invalid Model Selected', + ); + } + + return new GeminiLLM({ + apiKey: this.config.apiKey, + model: key, + baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', + }); + } + + async loadEmbeddingModel(key: string): Promise<BaseEmbedding<any>> { + const modelList = await this.getModelList(); + const exists = modelList.embedding.find((m) => m.key === key); + + if (!exists) { + throw new Error( + 'Error Loading Gemini Embedding Model. Invalid Model Selected.', + ); + } + + return new GeminiEmbedding({ + apiKey: this.config.apiKey, + model: key, + baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', + }); + } + + static parseAndValidate(raw: any): GeminiConfig { + if (!raw || typeof raw !== 'object') + throw new Error('Invalid config provided. Expected object'); + if (!raw.apiKey) + throw new Error('Invalid config provided. API key must be provided'); + + return { + apiKey: String(raw.apiKey), + }; + } + + static getProviderConfigFields(): UIConfigField[] { + return providerConfigFields; + } + + static getProviderMetadata(): ProviderMetadata { + return { + key: 'gemini', + name: 'Gemini', + }; + } +} + +export default GeminiProvider; From 9ce17edd4a58ce986f1646812e6d64c1cca3bee5 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:55:42 +0530 Subject: [PATCH 175/196] feat(providers): add groq --- src/lib/models/providers/groq/groqLLM.ts | 5 + src/lib/models/providers/groq/index.ts | 113 +++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 src/lib/models/providers/groq/groqLLM.ts create mode 100644 src/lib/models/providers/groq/index.ts diff --git a/src/lib/models/providers/groq/groqLLM.ts b/src/lib/models/providers/groq/groqLLM.ts new file mode 100644 index 0000000..dfcb294 --- /dev/null +++ b/src/lib/models/providers/groq/groqLLM.ts @@ -0,0 +1,5 @@ +import OpenAILLM from '../openai/openaiLLM'; + +class GroqLLM extends OpenAILLM {} + +export default GroqLLM; diff --git a/src/lib/models/providers/groq/index.ts b/src/lib/models/providers/groq/index.ts new file mode 100644 index 0000000..f50323e --- /dev/null +++ b/src/lib/models/providers/groq/index.ts @@ -0,0 +1,113 @@ +import { UIConfigField } from '@/lib/config/types'; +import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry'; +import { Model, ModelList, ProviderMetadata } from '../../types'; +import BaseEmbedding from '../../base/embedding'; +import BaseModelProvider from '../../base/provider'; +import BaseLLM from '../../base/llm'; +import GroqLLM from './groqLLM'; + +interface GroqConfig { + apiKey: string; +} + +const providerConfigFields: UIConfigField[] = [ + { + type: 'password', + name: 'API Key', + key: 'apiKey', + description: 'Your Groq API key', + required: true, + placeholder: 'Groq API Key', + env: 'GROQ_API_KEY', + scope: 'server', + }, +]; + +class GroqProvider extends BaseModelProvider<GroqConfig> { + constructor(id: string, name: string, config: GroqConfig) { + super(id, name, config); + } + + async getDefaultModels(): Promise<ModelList> { + const res = await fetch(`https://api.groq.com/openai/v1/models`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.config.apiKey}`, + }, + }); + + const data = await res.json(); + + const defaultChatModels: Model[] = []; + + data.data.forEach((m: any) => { + defaultChatModels.push({ + key: m.id, + name: m.id, + }); + }); + + return { + embedding: [], + chat: defaultChatModels, + }; + } + + async getModelList(): Promise<ModelList> { + const defaultModels = await this.getDefaultModels(); + const configProvider = getConfiguredModelProviderById(this.id)!; + + return { + embedding: [ + ...defaultModels.embedding, + ...configProvider.embeddingModels, + ], + chat: [...defaultModels.chat, ...configProvider.chatModels], + }; + } + + async loadChatModel(key: string): Promise<BaseLLM<any>> { + const modelList = await this.getModelList(); + + const exists = modelList.chat.find((m) => m.key === key); + + if (!exists) { + throw new Error('Error Loading Groq Chat Model. Invalid Model Selected'); + } + + return new GroqLLM({ + apiKey: this.config.apiKey, + model: key, + baseURL: 'https://api.groq.com/openai/v1', + }); + } + + async loadEmbeddingModel(key: string): Promise<BaseEmbedding<any>> { + throw new Error('Groq Provider does not support embedding models.'); + } + + static parseAndValidate(raw: any): GroqConfig { + if (!raw || typeof raw !== 'object') + throw new Error('Invalid config provided. Expected object'); + if (!raw.apiKey) + throw new Error('Invalid config provided. API key must be provided'); + + return { + apiKey: String(raw.apiKey), + }; + } + + static getProviderConfigFields(): UIConfigField[] { + return providerConfigFields; + } + + static getProviderMetadata(): ProviderMetadata { + return { + key: 'groq', + name: 'Groq', + }; + } +} + +export default GroqProvider; From 86a43086cce9f3ce35be745de2ec444a740d2c69 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:55:56 +0530 Subject: [PATCH 176/196] feat(providers): add transformers --- .../models/providers/transformers/index.ts | 88 +++++++++++++++++++ .../transformers/transformerEmbedding.ts | 43 +++++++++ 2 files changed, 131 insertions(+) create mode 100644 src/lib/models/providers/transformers/index.ts create mode 100644 src/lib/models/providers/transformers/transformerEmbedding.ts diff --git a/src/lib/models/providers/transformers/index.ts b/src/lib/models/providers/transformers/index.ts new file mode 100644 index 0000000..e60e94f --- /dev/null +++ b/src/lib/models/providers/transformers/index.ts @@ -0,0 +1,88 @@ +import { UIConfigField } from '@/lib/config/types'; +import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry'; +import { Model, ModelList, ProviderMetadata } from '../../types'; +import BaseModelProvider from '../../base/provider'; +import BaseLLM from '../../base/llm'; +import BaseEmbedding from '../../base/embedding'; +import TransformerEmbedding from './transformerEmbedding'; + +interface TransformersConfig {} + +const defaultEmbeddingModels: Model[] = [ + { + name: 'all-MiniLM-L6-v2', + key: 'Xenova/all-MiniLM-L6-v2', + }, + { + name: 'mxbai-embed-large-v1', + key: 'mixedbread-ai/mxbai-embed-large-v1', + }, + { + name: 'nomic-embed-text-v1', + key: 'Xenova/nomic-embed-text-v1', + }, +]; + +const providerConfigFields: UIConfigField[] = []; + +class TransformersProvider extends BaseModelProvider<TransformersConfig> { + constructor(id: string, name: string, config: TransformersConfig) { + super(id, name, config); + } + + async getDefaultModels(): Promise<ModelList> { + return { + embedding: [...defaultEmbeddingModels], + chat: [], + }; + } + + async getModelList(): Promise<ModelList> { + const defaultModels = await this.getDefaultModels(); + const configProvider = getConfiguredModelProviderById(this.id)!; + + return { + embedding: [ + ...defaultModels.embedding, + ...configProvider.embeddingModels, + ], + chat: [], + }; + } + + async loadChatModel(key: string): Promise<BaseLLM<any>> { + throw new Error('Transformers Provider does not support chat models.'); + } + + async loadEmbeddingModel(key: string): Promise<BaseEmbedding<any>> { + const modelList = await this.getModelList(); + const exists = modelList.embedding.find((m) => m.key === key); + + if (!exists) { + throw new Error( + 'Error Loading OpenAI Embedding Model. Invalid Model Selected.', + ); + } + + return new TransformerEmbedding({ + model: key, + }); + } + + static parseAndValidate(raw: any): TransformersConfig { + return {}; + } + + static getProviderConfigFields(): UIConfigField[] { + return providerConfigFields; + } + + static getProviderMetadata(): ProviderMetadata { + return { + key: 'transformers', + name: 'Transformers', + }; + } +} + +export default TransformersProvider; diff --git a/src/lib/models/providers/transformers/transformerEmbedding.ts b/src/lib/models/providers/transformers/transformerEmbedding.ts new file mode 100644 index 0000000..b3f43f0 --- /dev/null +++ b/src/lib/models/providers/transformers/transformerEmbedding.ts @@ -0,0 +1,43 @@ +import { Chunk } from '@/lib/types'; +import BaseEmbedding from '../../base/embedding'; +import { FeatureExtractionPipeline, pipeline } from '@huggingface/transformers'; + +type TransformerConfig = { + model: string; +}; + +class TransformerEmbedding extends BaseEmbedding<TransformerConfig> { + private pipelinePromise: Promise<FeatureExtractionPipeline> | null = null; + + constructor(protected config: TransformerConfig) { + super(config); + } + + async embedText(texts: string[]): Promise<number[][]> { + return this.embed(texts); + } + + async embedChunks(chunks: Chunk[]): Promise<number[][]> { + return this.embed(chunks.map((c) => c.content)); + } + + async embed(texts: string[]): Promise<number[][]> { + if (!this.pipelinePromise) { + this.pipelinePromise = (async () => { + const transformers = await import('@huggingface/transformers'); + return (await transformers.pipeline( + 'feature-extraction', + this.config.model, + )) as unknown as FeatureExtractionPipeline; + })(); + } + + const pipeline = await this.pipelinePromise; + + const output = await pipeline(texts, { pooling: 'mean', normalize: true }); + + return output.tolist() as number[][]; + } +} + +export default TransformerEmbedding; From d40fcd57d90504b821a8ad2eb1ba59fb434ebac2 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:56:11 +0530 Subject: [PATCH 177/196] feat(ollama): add nemotron to thinking list --- src/lib/models/providers/ollama/ollamaLLM.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/models/providers/ollama/ollamaLLM.ts b/src/lib/models/providers/ollama/ollamaLLM.ts index 0b88393..9bf5139 100644 --- a/src/lib/models/providers/ollama/ollamaLLM.ts +++ b/src/lib/models/providers/ollama/ollamaLLM.ts @@ -24,6 +24,7 @@ const reasoningModels = [ 'qwen3', 'deepseek-v3.1', 'magistral', + 'nemotron-3-nano', ]; class OllamaLLM extends BaseLLM<OllamaConfig> { From fe2c1b821096eb88eb73db1caa69533d1bffc91e Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:56:24 +0530 Subject: [PATCH 178/196] feat(providers): update index map --- src/lib/models/providers/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/models/providers/index.ts b/src/lib/models/providers/index.ts index 6e508e1..2c7ef2c 100644 --- a/src/lib/models/providers/index.ts +++ b/src/lib/models/providers/index.ts @@ -2,10 +2,16 @@ import { ModelProviderUISection } from '@/lib/config/types'; import { ProviderConstructor } from '../base/provider'; import OpenAIProvider from './openai'; import OllamaProvider from './ollama'; +import GeminiProvider from './gemini'; +import TransformersProvider from './transformers'; +import GroqProvider from './groq'; export const providers: Record<string, ProviderConstructor<any>> = { openai: OpenAIProvider, ollama: OllamaProvider, + gemini: GeminiProvider, + transformers: TransformersProvider, + groq: GroqProvider, }; export const getModelProvidersUIConfigSection = From e1afcbb78797e899f392edb4db66c5c2f340ed99 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:56:43 +0530 Subject: [PATCH 179/196] feat(package): add google genai & bump transformers --- package.json | 3 +- yarn.lock | 233 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 224 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index a735c5d..1040261 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,10 @@ "format:write": "prettier . --write" }, "dependencies": { + "@google/genai": "^1.34.0", "@headlessui/react": "^2.2.0", "@headlessui/tailwindcss": "^0.2.2", - "@huggingface/transformers": "^3.7.5", + "@huggingface/transformers": "^3.8.1", "@icons-pack/react-simple-icons": "^12.3.0", "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-tooltip": "^1.2.8", diff --git a/yarn.lock b/yarn.lock index 7efced6..c647d92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -392,6 +392,14 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62" integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig== +"@google/genai@^1.34.0": + version "1.34.0" + resolved "https://registry.yarnpkg.com/@google/genai/-/genai-1.34.0.tgz#8a6a85c2c7eb94afbb1a999967e828cae43ee6dd" + integrity sha512-vu53UMPvjmb7PGzlYu6Tzxso8Dfhn+a7eQFaS2uNemVtDZKwzSpJ5+ikqBbXplF7RGB1STcVDqCkPvquiwb2sw== + dependencies: + google-auth-library "^10.3.0" + ws "^8.18.0" + "@headlessui/react@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-2.2.0.tgz#a8e32f0899862849a1ce1615fa280e7891431ab7" @@ -407,17 +415,17 @@ resolved "https://registry.yarnpkg.com/@headlessui/tailwindcss/-/tailwindcss-0.2.2.tgz#8ebde73fabca72d48636ea56ae790209dc5f0d49" integrity sha512-xNe42KjdyA4kfUKLLPGzME9zkH7Q3rOZ5huFihWNWOQFxnItxPB3/67yBI8/qBfY8nwBRx5GHn4VprsoluVMGw== -"@huggingface/jinja@^0.5.1": - version "0.5.1" - resolved "https://registry.yarnpkg.com/@huggingface/jinja/-/jinja-0.5.1.tgz#081d334ddcf6237f65561ae3d665bb713a8ff74f" - integrity sha512-yUZLld4lrM9iFxHCwFQ7D1HW2MWMwSbeB7WzWqFYDWK+rEb+WldkLdAJxUPOmgICMHZLzZGVcVjFh3w/YGubng== +"@huggingface/jinja@^0.5.3": + version "0.5.3" + resolved "https://registry.yarnpkg.com/@huggingface/jinja/-/jinja-0.5.3.tgz#8182a2d6fc9f84c1539fa12c219a5915cdc41264" + integrity sha512-asqfZ4GQS0hD876Uw4qiUb7Tr/V5Q+JZuo2L+BtdrD4U40QU58nIRq3ZSgAzJgT874VLjhGVacaYfrdpXtEvtA== -"@huggingface/transformers@^3.7.5": - version "3.7.5" - resolved "https://registry.yarnpkg.com/@huggingface/transformers/-/transformers-3.7.5.tgz#63d6b5792c74904168959561c6a05a05d680092f" - integrity sha512-5jvrIwHyRXfOKVaGKYvUZM6ZjJKQXWeKzIOdKBE5pdzPSNzTwBNx5NdWcGElf4Ddv7Dl2mWsvJh+G5RnCUxMmA== +"@huggingface/transformers@^3.8.1": + version "3.8.1" + resolved "https://registry.yarnpkg.com/@huggingface/transformers/-/transformers-3.8.1.tgz#317da003865322396796173223eeaaf0f9723f0a" + integrity sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA== dependencies: - "@huggingface/jinja" "^0.5.1" + "@huggingface/jinja" "^0.5.3" onnxruntime-node "1.21.0" onnxruntime-web "1.22.0-dev.20250409-89f8206ba4" sharp "^0.34.1" @@ -1496,6 +1504,11 @@ acorn@^8.9.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== +agent-base@^7.1.2: + version "7.1.4" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8" + integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== + ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -1729,7 +1742,7 @@ base64-arraybuffer@^1.0.2: resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== -base64-js@^1.3.1, base64-js@^1.5.1: +base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -1742,6 +1755,11 @@ better-sqlite3@^11.9.1: bindings "^1.5.0" prebuild-install "^7.1.1" +bignumber.js@^9.0.0: + version "9.3.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.1.tgz#759c5aaddf2ffdc4f154f7b493e1c8770f88c4d7" + integrity sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ== + binary-extensions@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" @@ -1810,6 +1828,11 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== +buffer-equal-constant-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -2029,6 +2052,11 @@ damerau-levenshtein@^1.0.8: resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== + data-view-buffer@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" @@ -2056,6 +2084,13 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" +debug@4: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -2213,6 +2248,13 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + electron-to-chromium@^1.4.668: version "1.4.729" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.729.tgz#8477d21e2a50993781950885b2731d92ad532c00" @@ -2676,6 +2718,11 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== +extend@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + fancy-canvas@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fancy-canvas/-/fancy-canvas-2.1.0.tgz#44b40e40419ad8ef8304df365e4276767e918552" @@ -2730,6 +2777,14 @@ fault@^1.0.0: dependencies: format "^0.2.0" +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + "fetch-mock-cache@npm:fetch-mock-cache@^2.1.3": version "2.3.1" resolved "https://registry.yarnpkg.com/fetch-mock-cache/-/fetch-mock-cache-2.3.1.tgz#1018f5fc2f91cf2511abcea8a5e3a3b05e2d02bf" @@ -2854,6 +2909,13 @@ format@^0.2.0: resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + fraction.js@^4.3.7: version "4.3.7" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" @@ -2908,6 +2970,25 @@ functions-have-names@^1.2.3: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +gaxios@^7.0.0: + version "7.1.3" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-7.1.3.tgz#c5312f4254abc1b8ab53aef30c22c5229b80b1e1" + integrity sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ== + dependencies: + extend "^3.0.2" + https-proxy-agent "^7.0.1" + node-fetch "^3.3.2" + rimraf "^5.0.1" + +gcp-metadata@^8.0.0: + version "8.1.2" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-8.1.2.tgz#e62e3373ddf41fc727ccc31c55c687b798bee898" + integrity sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg== + dependencies: + gaxios "^7.0.0" + google-logging-utils "^1.0.0" + json-bigint "^1.0.0" + gel@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/gel/-/gel-2.0.1.tgz#587d89db54351c2d436de981d136481e37d06a7a" @@ -2995,6 +3076,18 @@ glob@^10.3.10: minipass "^7.0.4" path-scurry "^1.10.2" +glob@^10.3.7: + version "10.5.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c" + integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + glob@^7.1.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -3053,6 +3146,24 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +google-auth-library@^10.3.0: + version "10.5.0" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-10.5.0.tgz#3f0ebd47173496b91d2868f572bb8a8180c4b561" + integrity sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w== + dependencies: + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + gaxios "^7.0.0" + gcp-metadata "^8.0.0" + google-logging-utils "^1.0.0" + gtoken "^8.0.0" + jws "^4.0.0" + +google-logging-utils@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/google-logging-utils/-/google-logging-utils-1.1.3.tgz#17b71f1f95d266d2ddd356b8f00178433f041b17" + integrity sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA== + gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -3070,6 +3181,14 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +gtoken@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-8.0.0.tgz#d67a0e346dd441bfb54ad14040ddc3b632886575" + integrity sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw== + dependencies: + gaxios "^7.0.0" + jws "^4.0.0" + guid-typescript@^1.0.9: version "1.0.9" resolved "https://registry.yarnpkg.com/guid-typescript/-/guid-typescript-1.0.9.tgz#e35f77003535b0297ea08548f5ace6adb1480ddc" @@ -3152,6 +3271,14 @@ html2canvas@^1.0.0-rc.5: css-line-break "^2.1.0" text-segmentation "^1.0.3" +https-proxy-agent@^7.0.1: + version "7.0.6" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== + dependencies: + agent-base "^7.1.2" + debug "4" + humanize-url@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/humanize-url/-/humanize-url-2.1.1.tgz#1be3dc2b8a23ee28fdf9db95b22962b3eb5e4683" @@ -3464,6 +3591,15 @@ jackspeak@^2.3.5, jackspeak@^2.3.6: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + javascript-natural-sort@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59" @@ -3493,6 +3629,13 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" @@ -3559,6 +3702,23 @@ jszip@^3.7.1: readable-stream "~2.3.6" setimmediate "^1.0.5" +jwa@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804" + integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg== + dependencies: + buffer-equal-constant-time "^1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.1.tgz#07edc1be8fac20e677b283ece261498bd38f0690" + integrity sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA== + dependencies: + jwa "^2.0.1" + safe-buffer "^5.0.1" + keyv@^4.5.3: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -3777,6 +3937,13 @@ minimatch@^9.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -3829,7 +3996,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.1: +ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -3896,11 +4063,25 @@ node-abi@^3.3.0: dependencies: semver "^7.3.5" +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + node-ensure@^0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7" integrity sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw== +node-fetch@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" + integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + node-releases@^2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" @@ -4090,6 +4271,11 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + pako@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" @@ -4153,6 +4339,14 @@ path-scurry@^1.10.1, path-scurry@^1.10.2: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -4620,6 +4814,13 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" +rimraf@^5.0.1: + version "5.0.10" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.10.tgz#23b9843d3dc92db71f96e1a2ce92e39fd2a8221c" + integrity sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ== + dependencies: + glob "^10.3.7" + roarr@^2.15.3: version "2.15.4" resolved "https://registry.yarnpkg.com/roarr/-/roarr-2.15.4.tgz#f5fe795b7b838ccfe35dc608e0282b9eba2e7afd" @@ -5449,6 +5650,11 @@ utrie@^1.0.2: dependencies: base64-arraybuffer "^1.0.2" +web-streams-polyfill@^3.0.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" + integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== + whatwg-fetch@^3.6.20: version "3.6.20" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" @@ -5541,6 +5747,11 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== +ws@^8.18.0: + version "8.18.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" + integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== + xmlbuilder@^10.0.0: version "10.1.1" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-10.1.1.tgz#8cae6688cc9b38d850b7c8d3c0a4161dcaf475b0" From a2f2e17bbb8aed212b7e8edc98a1f84bd4dfc48e Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:12:22 +0530 Subject: [PATCH 180/196] feat(providers): add lemonade --- src/lib/models/providers/index.ts | 2 + src/lib/models/providers/lemonade/index.ts | 152 ++++++++++++++++++ .../providers/lemonade/lemonadeEmbedding.ts | 5 + .../models/providers/lemonade/lemonadeLLM.ts | 5 + 4 files changed, 164 insertions(+) create mode 100644 src/lib/models/providers/lemonade/index.ts create mode 100644 src/lib/models/providers/lemonade/lemonadeEmbedding.ts create mode 100644 src/lib/models/providers/lemonade/lemonadeLLM.ts diff --git a/src/lib/models/providers/index.ts b/src/lib/models/providers/index.ts index 2c7ef2c..b3dc550 100644 --- a/src/lib/models/providers/index.ts +++ b/src/lib/models/providers/index.ts @@ -5,6 +5,7 @@ import OllamaProvider from './ollama'; import GeminiProvider from './gemini'; import TransformersProvider from './transformers'; import GroqProvider from './groq'; +import LemonadeProvider from './lemonade'; export const providers: Record<string, ProviderConstructor<any>> = { openai: OpenAIProvider, @@ -12,6 +13,7 @@ export const providers: Record<string, ProviderConstructor<any>> = { gemini: GeminiProvider, transformers: TransformersProvider, groq: GroqProvider, + lemonade: LemonadeProvider }; export const getModelProvidersUIConfigSection = diff --git a/src/lib/models/providers/lemonade/index.ts b/src/lib/models/providers/lemonade/index.ts new file mode 100644 index 0000000..582363a --- /dev/null +++ b/src/lib/models/providers/lemonade/index.ts @@ -0,0 +1,152 @@ +import { BaseChatModel } from '@langchain/core/language_models/chat_models'; +import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai'; +import { Embeddings } from '@langchain/core/embeddings'; +import { UIConfigField } from '@/lib/config/types'; +import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry'; +import BaseModelProvider from '../../base/provider'; +import { Model, ModelList, ProviderMetadata } from '../../types'; +import BaseLLM from '../../base/llm'; +import LemonadeLLM from './lemonadeLLM'; +import BaseEmbedding from '../../base/embedding'; +import LemonadeEmbedding from './lemonadeEmbedding'; + +interface LemonadeConfig { + baseURL: string; + apiKey?: string; +} + +const providerConfigFields: UIConfigField[] = [ + { + type: 'string', + name: 'Base URL', + key: 'baseURL', + description: 'The base URL for Lemonade API', + required: true, + placeholder: 'https://api.lemonade.ai/v1', + env: 'LEMONADE_BASE_URL', + scope: 'server', + }, + { + type: 'password', + name: 'API Key', + key: 'apiKey', + description: 'Your Lemonade API key (optional)', + required: false, + placeholder: 'Lemonade API Key', + env: 'LEMONADE_API_KEY', + scope: 'server', + }, +]; + +class LemonadeProvider extends BaseModelProvider<LemonadeConfig> { + constructor(id: string, name: string, config: LemonadeConfig) { + super(id, name, config); + } + + async getDefaultModels(): Promise<ModelList> { + try { + const res = await fetch(`${this.config.baseURL}/models`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...this.config.apiKey ? {'Authorization': `Bearer ${this.config.apiKey}`} : {} + }, + }); + + const data = await res.json(); + + const models: Model[] = data.data.filter((m: any) => m.recipe === 'llamacpp').map((m: any) => { + return { + name: m.id, + key: m.id, + }; + }); + + return { + embedding: models, + chat: models, + }; + } catch (err) { + if (err instanceof TypeError) { + throw new Error( + 'Error connecting to Lemonade API. Please ensure the base URL is correct and the service is available.', + ); + } + + throw err; + } + } + + async getModelList(): Promise<ModelList> { + const defaultModels = await this.getDefaultModels(); + const configProvider = getConfiguredModelProviderById(this.id)!; + + return { + embedding: [ + ...defaultModels.embedding, + ...configProvider.embeddingModels, + ], + chat: [...defaultModels.chat, ...configProvider.chatModels], + }; + } + + async loadChatModel(key: string): Promise<BaseLLM<any>> { + const modelList = await this.getModelList(); + + const exists = modelList.chat.find((m) => m.key === key); + + if (!exists) { + throw new Error( + 'Error Loading Lemonade Chat Model. Invalid Model Selected', + ); + } + + return new LemonadeLLM({ + apiKey: this.config.apiKey || 'not-needed', + model: key, + baseURL: this.config.baseURL, + }); + } + + async loadEmbeddingModel(key: string): Promise<BaseEmbedding<any>> { + const modelList = await this.getModelList(); + const exists = modelList.embedding.find((m) => m.key === key); + + if (!exists) { + throw new Error( + 'Error Loading Lemonade Embedding Model. Invalid Model Selected.', + ); + } + + return new LemonadeEmbedding({ + apiKey: this.config.apiKey || 'not-needed', + model: key, + baseURL: this.config.baseURL, + }); + } + + static parseAndValidate(raw: any): LemonadeConfig { + if (!raw || typeof raw !== 'object') + throw new Error('Invalid config provided. Expected object'); + if (!raw.baseURL) + throw new Error('Invalid config provided. Base URL must be provided'); + + return { + baseURL: String(raw.baseURL), + apiKey: raw.apiKey ? String(raw.apiKey) : undefined, + }; + } + + static getProviderConfigFields(): UIConfigField[] { + return providerConfigFields; + } + + static getProviderMetadata(): ProviderMetadata { + return { + key: 'lemonade', + name: 'Lemonade', + }; + } +} + +export default LemonadeProvider; \ No newline at end of file diff --git a/src/lib/models/providers/lemonade/lemonadeEmbedding.ts b/src/lib/models/providers/lemonade/lemonadeEmbedding.ts new file mode 100644 index 0000000..a8e2155 --- /dev/null +++ b/src/lib/models/providers/lemonade/lemonadeEmbedding.ts @@ -0,0 +1,5 @@ +import OpenAIEmbedding from "../openai/openaiEmbedding"; + +class LemonadeEmbedding extends OpenAIEmbedding {} + +export default LemonadeEmbedding; \ No newline at end of file diff --git a/src/lib/models/providers/lemonade/lemonadeLLM.ts b/src/lib/models/providers/lemonade/lemonadeLLM.ts new file mode 100644 index 0000000..d207e13 --- /dev/null +++ b/src/lib/models/providers/lemonade/lemonadeLLM.ts @@ -0,0 +1,5 @@ +import OpenAILLM from "../openai/openaiLLM"; + +class LemonadeLLM extends OpenAILLM {} + +export default LemonadeLLM; \ No newline at end of file From 0a9641a110aaad4c6a6d193e7f2977d98e954779 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:24:06 +0530 Subject: [PATCH 181/196] feat(providers): add anthropic --- .../providers/anthropic/anthropicLLM.ts | 5 + src/lib/models/providers/anthropic/index.ts | 115 ++++++++++++++++++ src/lib/models/providers/index.ts | 4 +- src/lib/models/providers/lemonade/index.ts | 3 - 4 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 src/lib/models/providers/anthropic/anthropicLLM.ts create mode 100644 src/lib/models/providers/anthropic/index.ts diff --git a/src/lib/models/providers/anthropic/anthropicLLM.ts b/src/lib/models/providers/anthropic/anthropicLLM.ts new file mode 100644 index 0000000..9d660d6 --- /dev/null +++ b/src/lib/models/providers/anthropic/anthropicLLM.ts @@ -0,0 +1,5 @@ +import OpenAILLM from "../openai/openaiLLM"; + +class AnthropicLLM extends OpenAILLM {} + +export default AnthropicLLM; \ No newline at end of file diff --git a/src/lib/models/providers/anthropic/index.ts b/src/lib/models/providers/anthropic/index.ts new file mode 100644 index 0000000..2278ca2 --- /dev/null +++ b/src/lib/models/providers/anthropic/index.ts @@ -0,0 +1,115 @@ +import { UIConfigField } from '@/lib/config/types'; +import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry'; +import { Model, ModelList, ProviderMetadata } from '../../types'; +import BaseEmbedding from '../../base/embedding'; +import BaseModelProvider from '../../base/provider'; +import BaseLLM from '../../base/llm'; +import AnthropicLLM from './anthropicLLM'; + +interface AnthropicConfig { + apiKey: string; +} + +const providerConfigFields: UIConfigField[] = [ + { + type: 'password', + name: 'API Key', + key: 'apiKey', + description: 'Your Anthropic API key', + required: true, + placeholder: 'Anthropic API Key', + env: 'ANTHROPIC_API_KEY', + scope: 'server', + }, +]; + +class AnthropicProvider extends BaseModelProvider<AnthropicConfig> { + constructor(id: string, name: string, config: AnthropicConfig) { + super(id, name, config); + } + + async getDefaultModels(): Promise<ModelList> { + const res = await fetch('https://api.anthropic.com/v1/models?limit=999', { + method: 'GET', + headers: { + 'x-api-key': this.config.apiKey, + 'anthropic-version': '2023-06-01', + 'Content-type': 'application/json', + }, + }); + + if (!res.ok) { + throw new Error(`Failed to fetch Anthropic models: ${res.statusText}`); + } + + const data = (await res.json()).data; + + const models: Model[] = data.map((m: any) => { + return { + key: m.id, + name: m.display_name, + }; + }); + + return { + embedding: [], + chat: models, + }; + } + + async getModelList(): Promise<ModelList> { + const defaultModels = await this.getDefaultModels(); + const configProvider = getConfiguredModelProviderById(this.id)!; + + return { + embedding: [], + chat: [...defaultModels.chat, ...configProvider.chatModels], + }; + } + + async loadChatModel(key: string): Promise<BaseLLM<any>> { + const modelList = await this.getModelList(); + + const exists = modelList.chat.find((m) => m.key === key); + + if (!exists) { + throw new Error( + 'Error Loading Anthropic Chat Model. Invalid Model Selected', + ); + } + + return new AnthropicLLM({ + apiKey: this.config.apiKey, + model: key, + baseURL: 'https://api.anthropic.com/v1' + }); + } + + async loadEmbeddingModel(key: string): Promise<BaseEmbedding<any>> { + throw new Error('Anthropic provider does not support embedding models.'); + } + + static parseAndValidate(raw: any): AnthropicConfig { + if (!raw || typeof raw !== 'object') + throw new Error('Invalid config provided. Expected object'); + if (!raw.apiKey) + throw new Error('Invalid config provided. API key must be provided'); + + return { + apiKey: String(raw.apiKey), + }; + } + + static getProviderConfigFields(): UIConfigField[] { + return providerConfigFields; + } + + static getProviderMetadata(): ProviderMetadata { + return { + key: 'anthropic', + name: 'Anthropic', + }; + } +} + +export default AnthropicProvider; \ No newline at end of file diff --git a/src/lib/models/providers/index.ts b/src/lib/models/providers/index.ts index b3dc550..c53cb71 100644 --- a/src/lib/models/providers/index.ts +++ b/src/lib/models/providers/index.ts @@ -6,6 +6,7 @@ import GeminiProvider from './gemini'; import TransformersProvider from './transformers'; import GroqProvider from './groq'; import LemonadeProvider from './lemonade'; +import AnthropicProvider from './anthropic'; export const providers: Record<string, ProviderConstructor<any>> = { openai: OpenAIProvider, @@ -13,7 +14,8 @@ export const providers: Record<string, ProviderConstructor<any>> = { gemini: GeminiProvider, transformers: TransformersProvider, groq: GroqProvider, - lemonade: LemonadeProvider + lemonade: LemonadeProvider, + anthropic: AnthropicProvider }; export const getModelProvidersUIConfigSection = diff --git a/src/lib/models/providers/lemonade/index.ts b/src/lib/models/providers/lemonade/index.ts index 582363a..12f5391 100644 --- a/src/lib/models/providers/lemonade/index.ts +++ b/src/lib/models/providers/lemonade/index.ts @@ -1,6 +1,3 @@ -import { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai'; -import { Embeddings } from '@langchain/core/embeddings'; import { UIConfigField } from '@/lib/config/types'; import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry'; import BaseModelProvider from '../../base/provider'; From 452180356d57c6e1a7fdc10006de92a5686c0cb1 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:47:56 +0530 Subject: [PATCH 182/196] feat(library): enhance ui & ux --- src/app/library/page.tsx | 198 ++++++++++++++++++++++++++------------- 1 file changed, 131 insertions(+), 67 deletions(-) diff --git a/src/app/library/page.tsx b/src/app/library/page.tsx index 9c40b2b..3eb923e 100644 --- a/src/app/library/page.tsx +++ b/src/app/library/page.tsx @@ -1,8 +1,8 @@ 'use client'; import DeleteChat from '@/components/DeleteChat'; -import { cn, formatTimeDifference } from '@/lib/utils'; -import { BookOpenText, ClockIcon, Delete, ScanEye } from 'lucide-react'; +import { formatTimeDifference } from '@/lib/utils'; +import { BookOpenText, ClockIcon, FileText, Globe2Icon } from 'lucide-react'; import Link from 'next/link'; import { useEffect, useState } from 'react'; @@ -10,7 +10,8 @@ export interface Chat { id: string; title: string; createdAt: string; - focusMode: string; + sources: string[]; + files: { fileId: string; name: string }[]; } const Page = () => { @@ -37,74 +38,137 @@ const Page = () => { fetchChats(); }, []); - return loading ? ( - <div className="flex flex-row items-center justify-center min-h-screen"> - <svg - aria-hidden="true" - className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]" - viewBox="0 0 100 101" - fill="none" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M100 50.5908C100.003 78.2051 78.1951 100.003 50.5908 100C22.9765 99.9972 0.997224 78.018 1 50.4037C1.00281 22.7993 22.8108 0.997224 50.4251 1C78.0395 1.00281 100.018 22.8108 100 50.4251ZM9.08164 50.594C9.06312 73.3997 27.7909 92.1272 50.5966 92.1457C73.4023 92.1642 92.1298 73.4365 92.1483 50.6308C92.1669 27.8251 73.4392 9.0973 50.6335 9.07878C27.8278 9.06026 9.10003 27.787 9.08164 50.594Z" - fill="currentColor" - /> - <path - d="M93.9676 39.0409C96.393 38.4037 97.8624 35.9116 96.9801 33.5533C95.1945 28.8227 92.871 24.3692 90.0681 20.348C85.6237 14.1775 79.4473 9.36872 72.0454 6.45794C64.6435 3.54717 56.3134 2.65431 48.3133 3.89319C45.869 4.27179 44.3768 6.77534 45.014 9.20079C45.6512 11.6262 48.1343 13.0956 50.5786 12.717C56.5073 11.8281 62.5542 12.5399 68.0406 14.7911C73.527 17.0422 78.2187 20.7487 81.5841 25.4923C83.7976 28.5886 85.4467 32.059 86.4416 35.7474C87.1273 38.1189 89.5423 39.6781 91.9676 39.0409Z" - fill="currentFill" - /> - </svg> - </div> - ) : ( + return ( <div> - <div className="flex flex-col pt-4"> - <div className="flex items-center"> - <BookOpenText /> - <h1 className="text-3xl font-medium p-2">Library</h1> - </div> - <hr className="border-t border-[#2B2C2C] my-4 w-full" /> - </div> - {chats.length === 0 && ( - <div className="flex flex-row items-center justify-center min-h-screen"> - <p className="text-black/70 dark:text-white/70 text-sm"> - No chats found. - </p> - </div> - )} - {chats.length > 0 && ( - <div className="flex flex-col pb-20 lg:pb-2"> - {chats.map((chat, i) => ( - <div - className={cn( - 'flex flex-col space-y-4 py-6', - i !== chats.length - 1 - ? 'border-b border-white-200 dark:border-dark-200' - : '', - )} - key={i} - > - <Link - href={`/c/${chat.id}`} - className="text-black dark:text-white lg:text-xl font-medium truncate transition duration-200 hover:text-[#24A0ED] dark:hover:text-[#24A0ED] cursor-pointer" + <div className="flex flex-col pt-10 border-b border-light-200/20 dark:border-dark-200/20 pb-6 px-2"> + <div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-3"> + <div className="flex items-center justify-center"> + <BookOpenText size={45} className="mb-2.5" /> + <div className="flex flex-col"> + <h1 + className="text-5xl font-normal p-2 pb-0" + style={{ fontFamily: 'PP Editorial, serif' }} > - {chat.title} - </Link> - <div className="flex flex-row items-center justify-between w-full"> - <div className="flex flex-row items-center space-x-1 lg:space-x-1.5 text-black/70 dark:text-white/70"> - <ClockIcon size={15} /> - <p className="text-xs"> - {formatTimeDifference(new Date(), chat.createdAt)} Ago - </p> - </div> - <DeleteChat - chatId={chat.id} - chats={chats} - setChats={setChats} - /> + Library + </h1> + <div className="px-2 text-sm text-black/60 dark:text-white/60 text-center lg:text-left"> + Past chats, sources, and uploads. </div> </div> - ))} + </div> + + <div className="flex items-center justify-center lg:justify-end gap-2 text-xs text-black/60 dark:text-white/60"> + <span className="inline-flex items-center gap-1 rounded-full border border-black/20 dark:border-white/20 px-2 py-0.5"> + <BookOpenText size={14} /> + {loading + ? 'Loading…' + : `${chats.length} ${chats.length === 1 ? 'chat' : 'chats'}`} + </span> + </div> + </div> + </div> + + {loading ? ( + <div className="flex flex-row items-center justify-center min-h-[60vh]"> + <svg + aria-hidden="true" + className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]" + viewBox="0 0 100 101" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M100 50.5908C100.003 78.2051 78.1951 100.003 50.5908 100C22.9765 99.9972 0.997224 78.018 1 50.4037C1.00281 22.7993 22.8108 0.997224 50.4251 1C78.0395 1.00281 100.018 22.8108 100 50.4251ZM9.08164 50.594C9.06312 73.3997 27.7909 92.1272 50.5966 92.1457C73.4023 92.1642 92.1298 73.4365 92.1483 50.6308C92.1669 27.8251 73.4392 9.0973 50.6335 9.07878C27.8278 9.06026 9.10003 27.787 9.08164 50.594Z" + fill="currentColor" + /> + <path + d="M93.9676 39.0409C96.393 38.4037 97.8624 35.9116 96.9801 33.5533C95.1945 28.8227 92.871 24.3692 90.0681 20.348C85.6237 14.1775 79.4473 9.36872 72.0454 6.45794C64.6435 3.54717 56.3134 2.65431 48.3133 3.89319C45.869 4.27179 44.3768 6.77534 45.014 9.20079C45.6512 11.6262 48.1343 13.0956 50.5786 12.717C56.5073 11.8281 62.5542 12.5399 68.0406 14.7911C73.527 17.0422 78.2187 20.7487 81.5841 25.4923C83.7976 28.5886 85.4467 32.059 86.4416 35.7474C87.1273 38.1189 89.5423 39.6781 91.9676 39.0409Z" + fill="currentFill" + /> + </svg> + </div> + ) : chats.length === 0 ? ( + <div className="flex flex-col items-center justify-center min-h-[70vh] px-2 text-center"> + <div className="flex items-center justify-center w-12 h-12 rounded-2xl border border-light-200 dark:border-dark-200 bg-light-secondary dark:bg-dark-secondary"> + <BookOpenText className="text-black/70 dark:text-white/70" /> + </div> + <p className="mt-2 text-black/70 dark:text-white/70 text-sm"> + No chats found. + </p> + <p className="mt-1 text-black/70 dark:text-white/70 text-sm"> + <Link href="/" className="text-sky-400"> + Start a new chat + </Link>{' '} + to see it listed here. + </p> + </div> + ) : ( + <div className="pt-6 pb-28 px-2"> + <div className="rounded-2xl border border-light-200 dark:border-dark-200 overflow-hidden bg-light-primary dark:bg-dark-primary"> + {chats.map((chat, index) => { + const sourcesLabel = + chat.sources.length === 0 + ? null + : chat.sources.length <= 2 + ? chat.sources + .map((s) => s.charAt(0).toUpperCase() + s.slice(1)) + .join(', ') + : `${chat.sources + .slice(0, 2) + .map((s) => s.charAt(0).toUpperCase() + s.slice(1)) + .join(', ')} + ${chat.sources.length - 2}`; + + return ( + <div + key={chat.id} + className={ + 'group flex flex-col gap-2 p-4 hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors duration-200 ' + + (index !== chats.length - 1 + ? 'border-b border-light-200 dark:border-dark-200' + : '') + } + > + <div className="flex items-start justify-between gap-3"> + <Link + href={`/c/${chat.id}`} + className="flex-1 text-black dark:text-white text-base lg:text-lg font-medium leading-snug line-clamp-2 group-hover:text-[#24A0ED] transition duration-200" + title={chat.title} + > + {chat.title} + </Link> + <div className="pt-0.5 shrink-0"> + <DeleteChat + chatId={chat.id} + chats={chats} + setChats={setChats} + /> + </div> + </div> + + <div className="flex flex-wrap items-center gap-2 text-black/70 dark:text-white/70"> + <span className="inline-flex items-center gap-1 text-xs"> + <ClockIcon size={14} /> + {formatTimeDifference(new Date(), chat.createdAt)} Ago + </span> + + {sourcesLabel && ( + <span className="inline-flex items-center gap-1 text-xs border border-black/20 dark:border-white/20 rounded-full px-2 py-0.5"> + <Globe2Icon size={14} /> + {sourcesLabel} + </span> + )} + {chat.files.length > 0 && ( + <span className="inline-flex items-center gap-1 text-xs border border-black/20 dark:border-white/20 rounded-full px-2 py-0.5"> + <FileText size={14} /> + {chat.files.length}{' '} + {chat.files.length === 1 ? 'file' : 'files'} + </span> + )} + </div> + </div> + ); + })} + </div> </div> )} </div> From f5e054f6ea64d9a3a7ebde2e95d0eef26a0de2e0 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:48:16 +0530 Subject: [PATCH 183/196] feat(chat): fix hidden input --- src/components/Chat.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index f0432ca..1c95d26 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -59,7 +59,7 @@ const Chat = () => { }, [messages]); return ( - <div className="flex flex-col space-y-6 pt-8 pb-28 sm:mx-4 md:mx-8"> + <div className="flex flex-col space-y-6 pt-8 pb-44 lg:pb-28 sm:mx-4 md:mx-8"> {sections.map((section, i) => { const isLast = i === sections.length - 1; @@ -80,7 +80,7 @@ const Chat = () => { {loading && !messageAppeared && <MessageBoxLoading />} <div ref={messageEnd} className="h-0" /> {dividerWidth > 0 && ( - <div className="bottom-6 fixed z-40" style={{ width: dividerWidth }}> + <div className="fixed z-40 bottom-24 lg:bottom-6" style={{ width: dividerWidth }}> <div className="pointer-events-none absolute -bottom-6 left-0 right-0 h-[calc(100%+24px+24px)] dark:hidden" style={{ From 60dd7a81080749ac72733ee26184556f907b241d Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:24:07 +0530 Subject: [PATCH 184/196] feat(ui): fix theming issues --- src/components/MessageInputActions/Sources.tsx | 8 ++++---- src/components/Settings/SettingsField.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/MessageInputActions/Sources.tsx b/src/components/MessageInputActions/Sources.tsx index 9a98239..2652d58 100644 --- a/src/components/MessageInputActions/Sources.tsx +++ b/src/components/MessageInputActions/Sources.tsx @@ -51,12 +51,12 @@ const Sources = () => { animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.9 }} transition={{ duration: 0.1, ease: 'easeOut' }} - className="origin-top-right flex flex-col bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-1 max-h-[200px] md:max-h-none overflow-y-auto shadow-md shadow-dark-50" + className="origin-top-right flex flex-col bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-1 max-h-[200px] md:max-h-none overflow-y-auto shadow-lg" > {sourcesList.map((source, i) => ( <div key={i} - className="flex flex-row justify-between hover:bg-white-100 hover:dark:bg-dark-100 rounded-md py-3 px-2 cursor-pointer" + className="flex flex-row justify-between hover:bg-light-100 hover:dark:bg-dark-100 rounded-md py-3 px-2 cursor-pointer" onClick={() => { if (!sources.includes(source.key)) { setSources([...sources, source.key]); @@ -65,13 +65,13 @@ const Sources = () => { } }} > - <div className="flex flex-row space-x-1.5 text-white/80"> + <div className="flex flex-row space-x-1.5 text-black/80 dark:text-white/80"> {source.icon} <p className="text-xs">{source.name}</p> </div> <Switch checked={sources.includes(source.key)} - className="group relative flex h-4 w-7 shrink-0 cursor-pointer rounded-full bg-white/10 p-0.5 duration-200 ease-in-out focus:outline-none transition-colors disabled:opacity-60 disabled:cursor-not-allowed data-[checked]:bg-sky-500" + className="group relative flex h-4 w-7 shrink-0 cursor-pointer rounded-full bg-light-200 dark:bg-white/10 p-0.5 duration-200 ease-in-out focus:outline-none transition-colors disabled:opacity-60 disabled:cursor-not-allowed data-[checked]:bg-sky-500 dark:data-[checked]:bg-sky-500" > <span aria-hidden="true" diff --git a/src/components/Settings/SettingsField.tsx b/src/components/Settings/SettingsField.tsx index 843ce7c..447ce1c 100644 --- a/src/components/Settings/SettingsField.tsx +++ b/src/components/Settings/SettingsField.tsx @@ -310,7 +310,7 @@ const SettingsSwitch = ({ checked={isChecked} onChange={handleSave} disabled={loading} - className="group relative flex h-6 w-12 shrink-0 cursor-pointer rounded-full bg-white/10 p-1 duration-200 ease-in-out focus:outline-none transition-colors disabled:opacity-60 disabled:cursor-not-allowed data-[checked]:bg-sky-500" + className="group relative flex h-6 w-12 shrink-0 cursor-pointer rounded-full bg-light-200 dark:bg-white/10 p-1 duration-200 ease-in-out focus:outline-none transition-colors disabled:opacity-60 disabled:cursor-not-allowed data-[checked]:bg-sky-500 dark:data-[checked]:bg-sky-500" > <span aria-hidden="true" From ae132ebee807e416bbde5e8ef247f1013db73d4f Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Thu, 25 Dec 2025 18:58:33 +0530 Subject: [PATCH 185/196] feat(app): lint & beautify --- .../providers/anthropic/anthropicLLM.ts | 4 +- src/lib/models/providers/anthropic/index.ts | 4 +- src/lib/models/providers/index.ts | 2 +- src/lib/models/providers/lemonade/index.ts | 240 +++++++++--------- .../providers/lemonade/lemonadeEmbedding.ts | 4 +- .../models/providers/lemonade/lemonadeLLM.ts | 4 +- 6 files changed, 131 insertions(+), 127 deletions(-) diff --git a/src/lib/models/providers/anthropic/anthropicLLM.ts b/src/lib/models/providers/anthropic/anthropicLLM.ts index 9d660d6..a020de6 100644 --- a/src/lib/models/providers/anthropic/anthropicLLM.ts +++ b/src/lib/models/providers/anthropic/anthropicLLM.ts @@ -1,5 +1,5 @@ -import OpenAILLM from "../openai/openaiLLM"; +import OpenAILLM from '../openai/openaiLLM'; class AnthropicLLM extends OpenAILLM {} -export default AnthropicLLM; \ No newline at end of file +export default AnthropicLLM; diff --git a/src/lib/models/providers/anthropic/index.ts b/src/lib/models/providers/anthropic/index.ts index 2278ca2..0342647 100644 --- a/src/lib/models/providers/anthropic/index.ts +++ b/src/lib/models/providers/anthropic/index.ts @@ -81,7 +81,7 @@ class AnthropicProvider extends BaseModelProvider<AnthropicConfig> { return new AnthropicLLM({ apiKey: this.config.apiKey, model: key, - baseURL: 'https://api.anthropic.com/v1' + baseURL: 'https://api.anthropic.com/v1', }); } @@ -112,4 +112,4 @@ class AnthropicProvider extends BaseModelProvider<AnthropicConfig> { } } -export default AnthropicProvider; \ No newline at end of file +export default AnthropicProvider; diff --git a/src/lib/models/providers/index.ts b/src/lib/models/providers/index.ts index c53cb71..ef52f4b 100644 --- a/src/lib/models/providers/index.ts +++ b/src/lib/models/providers/index.ts @@ -15,7 +15,7 @@ export const providers: Record<string, ProviderConstructor<any>> = { transformers: TransformersProvider, groq: GroqProvider, lemonade: LemonadeProvider, - anthropic: AnthropicProvider + anthropic: AnthropicProvider, }; export const getModelProvidersUIConfigSection = diff --git a/src/lib/models/providers/lemonade/index.ts b/src/lib/models/providers/lemonade/index.ts index 12f5391..d31676b 100644 --- a/src/lib/models/providers/lemonade/index.ts +++ b/src/lib/models/providers/lemonade/index.ts @@ -8,142 +8,146 @@ import BaseEmbedding from '../../base/embedding'; import LemonadeEmbedding from './lemonadeEmbedding'; interface LemonadeConfig { - baseURL: string; - apiKey?: string; + baseURL: string; + apiKey?: string; } const providerConfigFields: UIConfigField[] = [ - { - type: 'string', - name: 'Base URL', - key: 'baseURL', - description: 'The base URL for Lemonade API', - required: true, - placeholder: 'https://api.lemonade.ai/v1', - env: 'LEMONADE_BASE_URL', - scope: 'server', - }, - { - type: 'password', - name: 'API Key', - key: 'apiKey', - description: 'Your Lemonade API key (optional)', - required: false, - placeholder: 'Lemonade API Key', - env: 'LEMONADE_API_KEY', - scope: 'server', - }, + { + type: 'string', + name: 'Base URL', + key: 'baseURL', + description: 'The base URL for Lemonade API', + required: true, + placeholder: 'https://api.lemonade.ai/v1', + env: 'LEMONADE_BASE_URL', + scope: 'server', + }, + { + type: 'password', + name: 'API Key', + key: 'apiKey', + description: 'Your Lemonade API key (optional)', + required: false, + placeholder: 'Lemonade API Key', + env: 'LEMONADE_API_KEY', + scope: 'server', + }, ]; class LemonadeProvider extends BaseModelProvider<LemonadeConfig> { - constructor(id: string, name: string, config: LemonadeConfig) { - super(id, name, config); - } + constructor(id: string, name: string, config: LemonadeConfig) { + super(id, name, config); + } - async getDefaultModels(): Promise<ModelList> { - try { - const res = await fetch(`${this.config.baseURL}/models`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - ...this.config.apiKey ? {'Authorization': `Bearer ${this.config.apiKey}`} : {} - }, - }); + async getDefaultModels(): Promise<ModelList> { + try { + const res = await fetch(`${this.config.baseURL}/models`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...(this.config.apiKey + ? { Authorization: `Bearer ${this.config.apiKey}` } + : {}), + }, + }); - const data = await res.json(); + const data = await res.json(); - const models: Model[] = data.data.filter((m: any) => m.recipe === 'llamacpp').map((m: any) => { - return { - name: m.id, - key: m.id, - }; - }); - - return { - embedding: models, - chat: models, - }; - } catch (err) { - if (err instanceof TypeError) { - throw new Error( - 'Error connecting to Lemonade API. Please ensure the base URL is correct and the service is available.', - ); - } - - throw err; - } - } - - async getModelList(): Promise<ModelList> { - const defaultModels = await this.getDefaultModels(); - const configProvider = getConfiguredModelProviderById(this.id)!; - - return { - embedding: [ - ...defaultModels.embedding, - ...configProvider.embeddingModels, - ], - chat: [...defaultModels.chat, ...configProvider.chatModels], - }; - } - - async loadChatModel(key: string): Promise<BaseLLM<any>> { - const modelList = await this.getModelList(); - - const exists = modelList.chat.find((m) => m.key === key); - - if (!exists) { - throw new Error( - 'Error Loading Lemonade Chat Model. Invalid Model Selected', - ); - } - - return new LemonadeLLM({ - apiKey: this.config.apiKey || 'not-needed', - model: key, - baseURL: this.config.baseURL, + const models: Model[] = data.data + .filter((m: any) => m.recipe === 'llamacpp') + .map((m: any) => { + return { + name: m.id, + key: m.id, + }; }); + + return { + embedding: models, + chat: models, + }; + } catch (err) { + if (err instanceof TypeError) { + throw new Error( + 'Error connecting to Lemonade API. Please ensure the base URL is correct and the service is available.', + ); + } + + throw err; + } + } + + async getModelList(): Promise<ModelList> { + const defaultModels = await this.getDefaultModels(); + const configProvider = getConfiguredModelProviderById(this.id)!; + + return { + embedding: [ + ...defaultModels.embedding, + ...configProvider.embeddingModels, + ], + chat: [...defaultModels.chat, ...configProvider.chatModels], + }; + } + + async loadChatModel(key: string): Promise<BaseLLM<any>> { + const modelList = await this.getModelList(); + + const exists = modelList.chat.find((m) => m.key === key); + + if (!exists) { + throw new Error( + 'Error Loading Lemonade Chat Model. Invalid Model Selected', + ); } - async loadEmbeddingModel(key: string): Promise<BaseEmbedding<any>> { - const modelList = await this.getModelList(); - const exists = modelList.embedding.find((m) => m.key === key); + return new LemonadeLLM({ + apiKey: this.config.apiKey || 'not-needed', + model: key, + baseURL: this.config.baseURL, + }); + } - if (!exists) { - throw new Error( - 'Error Loading Lemonade Embedding Model. Invalid Model Selected.', - ); - } + async loadEmbeddingModel(key: string): Promise<BaseEmbedding<any>> { + const modelList = await this.getModelList(); + const exists = modelList.embedding.find((m) => m.key === key); - return new LemonadeEmbedding({ - apiKey: this.config.apiKey || 'not-needed', - model: key, - baseURL: this.config.baseURL, - }); + if (!exists) { + throw new Error( + 'Error Loading Lemonade Embedding Model. Invalid Model Selected.', + ); } - static parseAndValidate(raw: any): LemonadeConfig { - if (!raw || typeof raw !== 'object') - throw new Error('Invalid config provided. Expected object'); - if (!raw.baseURL) - throw new Error('Invalid config provided. Base URL must be provided'); + return new LemonadeEmbedding({ + apiKey: this.config.apiKey || 'not-needed', + model: key, + baseURL: this.config.baseURL, + }); + } - return { - baseURL: String(raw.baseURL), - apiKey: raw.apiKey ? String(raw.apiKey) : undefined, - }; - } + static parseAndValidate(raw: any): LemonadeConfig { + if (!raw || typeof raw !== 'object') + throw new Error('Invalid config provided. Expected object'); + if (!raw.baseURL) + throw new Error('Invalid config provided. Base URL must be provided'); - static getProviderConfigFields(): UIConfigField[] { - return providerConfigFields; - } + return { + baseURL: String(raw.baseURL), + apiKey: raw.apiKey ? String(raw.apiKey) : undefined, + }; + } - static getProviderMetadata(): ProviderMetadata { - return { - key: 'lemonade', - name: 'Lemonade', - }; - } + static getProviderConfigFields(): UIConfigField[] { + return providerConfigFields; + } + + static getProviderMetadata(): ProviderMetadata { + return { + key: 'lemonade', + name: 'Lemonade', + }; + } } -export default LemonadeProvider; \ No newline at end of file +export default LemonadeProvider; diff --git a/src/lib/models/providers/lemonade/lemonadeEmbedding.ts b/src/lib/models/providers/lemonade/lemonadeEmbedding.ts index a8e2155..7d720f8 100644 --- a/src/lib/models/providers/lemonade/lemonadeEmbedding.ts +++ b/src/lib/models/providers/lemonade/lemonadeEmbedding.ts @@ -1,5 +1,5 @@ -import OpenAIEmbedding from "../openai/openaiEmbedding"; +import OpenAIEmbedding from '../openai/openaiEmbedding'; class LemonadeEmbedding extends OpenAIEmbedding {} -export default LemonadeEmbedding; \ No newline at end of file +export default LemonadeEmbedding; diff --git a/src/lib/models/providers/lemonade/lemonadeLLM.ts b/src/lib/models/providers/lemonade/lemonadeLLM.ts index d207e13..bfd3e28 100644 --- a/src/lib/models/providers/lemonade/lemonadeLLM.ts +++ b/src/lib/models/providers/lemonade/lemonadeLLM.ts @@ -1,5 +1,5 @@ -import OpenAILLM from "../openai/openaiLLM"; +import OpenAILLM from '../openai/openaiLLM'; class LemonadeLLM extends OpenAILLM {} -export default LemonadeLLM; \ No newline at end of file +export default LemonadeLLM; From edba47aed86a7ad86e26299214a5e862f7dcd918 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Fri, 26 Dec 2025 14:51:24 +0530 Subject: [PATCH 186/196] feat(db): add migration scripts --- drizzle/0002_daffy_wrecker.sql | 16 +--- src/lib/db/migrate.ts | 155 +++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 15 deletions(-) diff --git a/drizzle/0002_daffy_wrecker.sql b/drizzle/0002_daffy_wrecker.sql index 78b6685..1520a65 100644 --- a/drizzle/0002_daffy_wrecker.sql +++ b/drizzle/0002_daffy_wrecker.sql @@ -1,15 +1 @@ -PRAGMA foreign_keys=OFF;--> statement-breakpoint -CREATE TABLE `__new_messages` ( - `id` integer PRIMARY KEY NOT NULL, - `messageId` text NOT NULL, - `chatId` text NOT NULL, - `backendId` text NOT NULL, - `query` text NOT NULL, - `createdAt` text NOT NULL, - `responseBlocks` text DEFAULT '[]', - `status` text DEFAULT 'answering' -); ---> statement-breakpoint -DROP TABLE `messages`;--> statement-breakpoint -ALTER TABLE `__new_messages` RENAME TO `messages`;--> statement-breakpoint -PRAGMA foreign_keys=ON; \ No newline at end of file +/* do nothing */ \ No newline at end of file diff --git a/src/lib/db/migrate.ts b/src/lib/db/migrate.ts index 5e2c374..e0efb7c 100644 --- a/src/lib/db/migrate.ts +++ b/src/lib/db/migrate.ts @@ -45,6 +45,7 @@ fs.readdirSync(migrationsFolder) const already = db .prepare('SELECT 1 FROM ran_migrations WHERE name = ?') .get(migrationName); + if (already) { console.log(`Skipping already-applied migration: ${file}`); return; @@ -113,6 +114,160 @@ fs.readdirSync(migrationsFolder) db.exec('DROP TABLE messages;'); db.exec('ALTER TABLE messages_with_sources RENAME TO messages;'); + } else if (migrationName === '0002') { + /* Migrate chat */ + db.exec(` + CREATE TABLE IF NOT EXISTS chats_new ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + createdAt TEXT NOT NULL, + sources TEXT DEFAULT '[]', + files TEXT DEFAULT '[]' + ); + `); + + const chats = db + .prepare('SELECT id, title, createdAt, files FROM chats') + .all(); + + const insertChat = db.prepare(` + INSERT INTO chats_new (id, title, createdAt, sources, files) + VALUES (?, ?, ?, ?, ?) + `); + + chats.forEach((chat: any) => { + let files = chat.files; + while (typeof files === 'string') { + files = JSON.parse(files || '[]'); + } + + insertChat.run( + chat.id, + chat.title, + chat.createdAt, + '["web"]', + JSON.stringify(files), + ); + }); + + db.exec('DROP TABLE chats;'); + db.exec('ALTER TABLE chats_new RENAME TO chats;'); + + /* Migrate messages */ + + db.exec(` + CREATE TABLE IF NOT EXISTS messages_new ( + id INTEGER PRIMARY KEY, + messageId TEXT NOT NULL, + chatId TEXT NOT NULL, + backendId TEXT NOT NULL, + query TEXT NOT NULL, + createdAt TEXT NOT NULL, + responseBlocks TEXT DEFAULT '[]', + status TEXT DEFAULT 'answering' + ); + `); + + const messages = db + .prepare( + 'SELECT id, messageId, chatId, type, content, createdAt, sources FROM messages ORDER BY id ASC', + ) + .all(); + + const insertMessage = db.prepare(` + INSERT INTO messages_new (messageId, chatId, backendId, query, createdAt, responseBlocks, status) + VALUES (?, ?, ?, ?, ?, ?, ?) + `); + + let currentMessageData: { + sources?: any[]; + response?: string; + query?: string; + messageId?: string; + chatId?: string; + createdAt?: string; + } = {}; + let lastCompleted = true; + + messages.forEach((msg: any) => { + if (msg.type === 'user' && lastCompleted) { + currentMessageData = {}; + currentMessageData.messageId = msg.messageId; + currentMessageData.chatId = msg.chatId; + currentMessageData.query = msg.content; + currentMessageData.createdAt = msg.createdAt; + lastCompleted = false; + } else if (msg.type === 'source' && !lastCompleted) { + let sources = msg.sources; + + while (typeof sources === 'string') { + sources = JSON.parse(sources || '[]'); + } + + currentMessageData.sources = sources; + } else if (msg.type === 'assistant' && !lastCompleted) { + currentMessageData.response = msg.content; + insertMessage.run( + currentMessageData.messageId, + currentMessageData.chatId, + `${currentMessageData.messageId}-backend`, + currentMessageData.query, + currentMessageData.createdAt, + JSON.stringify([ + { + id: crypto.randomUUID(), + type: 'text', + data: currentMessageData.response || '', + }, + ...(currentMessageData.sources && + currentMessageData.sources.length > 0 + ? [ + { + id: crypto.randomUUID(), + type: 'source', + data: currentMessageData.sources, + }, + ] + : []), + ]), + 'completed', + ); + + lastCompleted = true; + } else if (msg.type === 'user' && !lastCompleted) { + /* Message wasn't completed so we'll just create the record with empty response */ + insertMessage.run( + currentMessageData.messageId, + currentMessageData.chatId, + `${currentMessageData.messageId}-backend`, + currentMessageData.query, + currentMessageData.createdAt, + JSON.stringify([ + { + id: crypto.randomUUID(), + type: 'text', + data: '', + }, + ...(currentMessageData.sources && + currentMessageData.sources.length > 0 + ? [ + { + id: crypto.randomUUID(), + type: 'source', + data: currentMessageData.sources, + }, + ] + : []), + ]), + 'completed', + ); + + lastCompleted = true; + } + }); + + db.exec('DROP TABLE messages;'); + db.exec('ALTER TABLE messages_new RENAME TO messages;'); } else { // Execute each statement separately statements.forEach((stmt) => { From a31a4ab295c7dff61b74deebefb006568088eac8 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 27 Dec 2025 13:07:42 +0530 Subject: [PATCH 187/196] feat(agents): add api search agent --- src/lib/agents/search/api.ts | 99 ++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/lib/agents/search/api.ts diff --git a/src/lib/agents/search/api.ts b/src/lib/agents/search/api.ts new file mode 100644 index 0000000..924bc68 --- /dev/null +++ b/src/lib/agents/search/api.ts @@ -0,0 +1,99 @@ +import { ResearcherOutput, SearchAgentInput } from './types'; +import SessionManager from '@/lib/session'; +import { classify } from './classifier'; +import Researcher from './researcher'; +import { getWriterPrompt } from '@/lib/prompts/search/writer'; +import { WidgetExecutor } from './widgets'; + +class APISearchAgent { + async searchAsync(session: SessionManager, input: SearchAgentInput) { + const classification = await classify({ + chatHistory: input.chatHistory, + enabledSources: input.config.sources, + query: input.followUp, + llm: input.config.llm, + }); + + const widgetPromise = WidgetExecutor.executeAll({ + classification, + chatHistory: input.chatHistory, + followUp: input.followUp, + llm: input.config.llm, + }); + + let searchPromise: Promise<ResearcherOutput> | null = null; + + if (!classification.classification.skipSearch) { + const researcher = new Researcher(); + searchPromise = researcher.research(SessionManager.createSession(), { + chatHistory: input.chatHistory, + followUp: input.followUp, + classification: classification, + config: input.config, + }); + } + + const [widgetOutputs, searchResults] = await Promise.all([ + widgetPromise, + searchPromise, + ]); + + if (searchResults) { + session.emit('data', { + type: 'searchResults', + data: searchResults.searchFindings, + }); + } + + session.emit('data', { + type: 'researchComplete', + }); + + const finalContext = + searchResults?.searchFindings + .map( + (f, index) => + `<result index=${index + 1} title=${f.metadata.title}>${f.content}</result>`, + ) + .join('\n') || ''; + + const widgetContext = widgetOutputs + .map((o) => { + return `<result>${o.llmContext}</result>`; + }) + .join('\n-------------\n'); + + const finalContextWithWidgets = `<search_results note="These are the search results and assistant can cite these">\n${finalContext}\n</search_results>\n<widgets_result noteForAssistant="Its output is already showed to the user, assistant can use this information to answer the query but do not CITE this as a souce">\n${widgetContext}\n</widgets_result>`; + + const writerPrompt = getWriterPrompt( + finalContextWithWidgets, + input.config.systemInstructions, + input.config.mode, + ); + + const answerStream = input.config.llm.streamText({ + messages: [ + { + role: 'system', + content: writerPrompt, + }, + ...input.chatHistory, + { + role: 'user', + content: input.followUp, + }, + ], + }); + + for await (const chunk of answerStream) { + session.emit('data', { + type: 'response', + data: chunk.contentChunk, + }); + } + + session.emit('end', {}); + } +} + +export default APISearchAgent; From 50ca7ac73ab4b53bb895926763eac1ef19173388 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 27 Dec 2025 13:07:59 +0530 Subject: [PATCH 188/196] feat(api): update search api & related documentation --- docs/API/SEARCH.md | 25 +++--- src/app/api/search/route.ts | 149 +++++++++++++++++++----------------- 2 files changed, 90 insertions(+), 84 deletions(-) diff --git a/docs/API/SEARCH.md b/docs/API/SEARCH.md index 04f11ef..0c35a81 100644 --- a/docs/API/SEARCH.md +++ b/docs/API/SEARCH.md @@ -57,7 +57,7 @@ Use the `id` field as the `providerId` and the `key` field from the models array ### Request -The API accepts a JSON object in the request body, where you define the focus mode, chat models, embedding models, and your query. +The API accepts a JSON object in the request body, where you define the enabled search `sources`, chat models, embedding models, and your query. #### Request Body Structure @@ -72,7 +72,7 @@ The API accepts a JSON object in the request body, where you define the focus mo "key": "text-embedding-3-large" }, "optimizationMode": "speed", - "focusMode": "webSearch", + "sources": ["web"], "query": "What is Perplexica", "history": [ ["human", "Hi, how are you?"], @@ -87,24 +87,25 @@ The API accepts a JSON object in the request body, where you define the focus mo ### Request Parameters -- **`chatModel`** (object, optional): Defines the chat model to be used for the query. To get available providers and models, send a GET request to `http://localhost:3000/api/providers`. +- **`chatModel`** (object, required): Defines the chat model to be used for the query. To get available providers and models, send a GET request to `http://localhost:3000/api/providers`. - `providerId` (string): The UUID of the provider. You can get this from the `/api/providers` endpoint response. - `key` (string): The model key/identifier (e.g., `gpt-4o-mini`, `llama3.1:latest`). Use the `key` value from the provider's `chatModels` array, not the display name. -- **`embeddingModel`** (object, optional): Defines the embedding model for similarity-based searching. To get available providers and models, send a GET request to `http://localhost:3000/api/providers`. +- **`embeddingModel`** (object, required): Defines the embedding model for similarity-based searching. To get available providers and models, send a GET request to `http://localhost:3000/api/providers`. - `providerId` (string): The UUID of the embedding provider. You can get this from the `/api/providers` endpoint response. - `key` (string): The embedding model key (e.g., `text-embedding-3-large`, `nomic-embed-text`). Use the `key` value from the provider's `embeddingModels` array, not the display name. -- **`focusMode`** (string, required): Specifies which focus mode to use. Available modes: +- **`sources`** (array, required): Which search sources to enable. Available values: - - `webSearch`, `academicSearch`, `writingAssistant`, `wolframAlphaSearch`, `youtubeSearch`, `redditSearch`. + - `web`, `academic`, `discussions`. - **`optimizationMode`** (string, optional): Specifies the optimization mode to control the balance between performance and quality. Available modes: - `speed`: Prioritize speed and return the fastest answer. - `balanced`: Provide a balanced answer with good speed and reasonable quality. + - `quality`: Prioritize answer quality (may be slower). - **`query`** (string, required): The search query or question. @@ -132,14 +133,14 @@ The response from the API includes both the final message and the sources used t "message": "Perplexica is an innovative, open-source AI-powered search engine designed to enhance the way users search for information online. Here are some key features and characteristics of Perplexica:\n\n- **AI-Powered Technology**: It utilizes advanced machine learning algorithms to not only retrieve information but also to understand the context and intent behind user queries, providing more relevant results [1][5].\n\n- **Open-Source**: Being open-source, Perplexica offers flexibility and transparency, allowing users to explore its functionalities without the constraints of proprietary software [3][10].", "sources": [ { - "pageContent": "Perplexica is an innovative, open-source AI-powered search engine designed to enhance the way users search for information online.", + "content": "Perplexica is an innovative, open-source AI-powered search engine designed to enhance the way users search for information online.", "metadata": { "title": "What is Perplexica, and how does it function as an AI-powered search ...", "url": "https://askai.glarity.app/search/What-is-Perplexica--and-how-does-it-function-as-an-AI-powered-search-engine" } }, { - "pageContent": "Perplexica is an open-source AI-powered search tool that dives deep into the internet to find precise answers.", + "content": "Perplexica is an open-source AI-powered search tool that dives deep into the internet to find precise answers.", "metadata": { "title": "Sahar Mor's Post", "url": "https://www.linkedin.com/posts/sahar-mor_a-new-open-source-project-called-perplexica-activity-7204489745668694016-ncja" @@ -158,7 +159,7 @@ Example of streamed response objects: ``` {"type":"init","data":"Stream connected"} -{"type":"sources","data":[{"pageContent":"...","metadata":{"title":"...","url":"..."}},...]} +{"type":"sources","data":[{"content":"...","metadata":{"title":"...","url":"..."}},...]} {"type":"response","data":"Perplexica is an "} {"type":"response","data":"innovative, open-source "} {"type":"response","data":"AI-powered search engine..."} @@ -174,9 +175,9 @@ Clients should process each line as a separate JSON object. The different messag ### Fields in the Response -- **`message`** (string): The search result, generated based on the query and focus mode. +- **`message`** (string): The search result, generated based on the query and enabled `sources`. - **`sources`** (array): A list of sources that were used to generate the search result. Each source includes: - - `pageContent`: A snippet of the relevant content from the source. + - `content`: A snippet of the relevant content from the source. - `metadata`: Metadata about the source, including: - `title`: The title of the webpage. - `url`: The URL of the webpage. @@ -185,5 +186,5 @@ Clients should process each line as a separate JSON object. The different messag If an error occurs during the search process, the API will return an appropriate error message with an HTTP status code. -- **400**: If the request is malformed or missing required fields (e.g., no focus mode or query). +- **400**: If the request is malformed or missing required fields (e.g., no `sources` or `query`). - **500**: If an internal server error occurs during the search. diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts index dc1d0b8..0991268 100644 --- a/src/app/api/search/route.ts +++ b/src/app/api/search/route.ts @@ -1,12 +1,13 @@ import ModelRegistry from '@/lib/models/registry'; import { ModelWithProvider } from '@/lib/models/types'; import SessionManager from '@/lib/session'; -import SearchAgent from '@/lib/agents/search'; import { ChatTurnMessage } from '@/lib/types'; +import { SearchSources } from '@/lib/agents/search/types'; +import APISearchAgent from '@/lib/agents/search/api'; interface ChatRequestBody { - optimizationMode: 'speed' | 'balanced'; - focusMode: string; + optimizationMode: 'speed' | 'balanced' | 'quality'; + sources: SearchSources[]; chatModel: ModelWithProvider; embeddingModel: ModelWithProvider; query: string; @@ -19,15 +20,15 @@ export const POST = async (req: Request) => { try { const body: ChatRequestBody = await req.json(); - if (!body.focusMode || !body.query) { + if (!body.sources || !body.query) { return Response.json( - { message: 'Missing focus mode or query' }, + { message: 'Missing sources or query' }, { status: 400 }, ); } body.history = body.history || []; - body.optimizationMode = body.optimizationMode || 'balanced'; + body.optimizationMode = body.optimizationMode || 'speed'; body.stream = body.stream || false; const registry = new ModelRegistry(); @@ -48,18 +49,21 @@ export const POST = async (req: Request) => { const session = SessionManager.createSession(); - const agent = new SearchAgent(); + const agent = new APISearchAgent(); agent.searchAsync(session, { chatHistory: history, config: { embedding: embeddings, llm: llm, - sources: ['web', 'discussions', 'academic'], - mode: 'balanced', + sources: body.sources, + mode: body.optimizationMode, fileIds: [], + systemInstructions: body.systemInstructions || '', }, followUp: body.query, + chatId: crypto.randomUUID(), + messageId: crypto.randomUUID(), }); if (!body.stream) { @@ -71,36 +75,37 @@ export const POST = async (req: Request) => { let message = ''; let sources: any[] = []; - session.addListener('data', (data: string) => { - try { - const parsedData = JSON.parse(data); - if (parsedData.type === 'response') { - message += parsedData.data; - } else if (parsedData.type === 'sources') { - sources = parsedData.data; + session.subscribe((event: string, data: Record<string, any>) => { + if (event === 'data') { + try { + if (data.type === 'response') { + message += data.data; + } else if (data.type === 'searchResults') { + sources = data.data; + } + } catch (error) { + reject( + Response.json( + { message: 'Error parsing data' }, + { status: 500 }, + ), + ); } - } catch (error) { + } + + if (event === 'end') { + resolve(Response.json({ message, sources }, { status: 200 })); + } + + if (event === 'error') { reject( Response.json( - { message: 'Error parsing data' }, + { message: 'Search error', error: data }, { status: 500 }, ), ); } }); - - session.addListener('end', () => { - resolve(Response.json({ message, sources }, { status: 200 })); - }); - - session.addListener('error', (error: any) => { - reject( - Response.json( - { message: 'Search error', error }, - { status: 500 }, - ), - ); - }); }, ); } @@ -131,54 +136,54 @@ export const POST = async (req: Request) => { } catch (error) {} }); - session.addListener('data', (data: string) => { - if (signal.aborted) return; + session.subscribe((event: string, data: Record<string, any>) => { + if (event === 'data') { + if (signal.aborted) return; - try { - const parsedData = JSON.parse(data); - - if (parsedData.type === 'response') { - controller.enqueue( - encoder.encode( - JSON.stringify({ - type: 'response', - data: parsedData.data, - }) + '\n', - ), - ); - } else if (parsedData.type === 'sources') { - sources = parsedData.data; - controller.enqueue( - encoder.encode( - JSON.stringify({ - type: 'sources', - data: sources, - }) + '\n', - ), - ); + try { + if (data.type === 'response') { + controller.enqueue( + encoder.encode( + JSON.stringify({ + type: 'response', + data: data.data, + }) + '\n', + ), + ); + } else if (data.type === 'searchResults') { + sources = data.data; + controller.enqueue( + encoder.encode( + JSON.stringify({ + type: 'sources', + data: sources, + }) + '\n', + ), + ); + } + } catch (error) { + controller.error(error); } - } catch (error) { - controller.error(error); } - }); - session.addListener('end', () => { - if (signal.aborted) return; + if (event === 'end') { + if (signal.aborted) return; - controller.enqueue( - encoder.encode( - JSON.stringify({ - type: 'done', - }) + '\n', - ), - ); - controller.close(); - }); + controller.enqueue( + encoder.encode( + JSON.stringify({ + type: 'done', + }) + '\n', + ), + ); + controller.close(); + } - session.addListener('error', (error: any) => { - if (signal.aborted) return; + if (event === 'error') { + if (signal.aborted) return; - controller.error(error); + controller.error(data); + } }); }, cancel() { From cc183cd0cd3da05b11dabf034ad46573692b84fd Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 27 Dec 2025 13:08:28 +0530 Subject: [PATCH 189/196] feat(readme): update features & upcoming features --- README.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 75b741d..49b1fc5 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,11 @@ Want to know more about its architecture and how it works? You can read it [here 🤖 **Support for all major AI providers** - Use local LLMs through Ollama or connect to OpenAI, Anthropic Claude, Google Gemini, Groq, and more. Mix and match models based on your needs. -⚡ **Smart search modes** - Choose Balanced Mode for everyday searches, Fast Mode when you need quick answers, or wait for Quality Mode (coming soon) for deep research. +⚡ **Smart search modes** - Choose Speed Mode when you need quick answers, Balanced Mode for everyday searches, or Quality Mode for deep research. -🎯 **Six specialized focus modes** - Get better results with modes designed for specific tasks: Academic papers, YouTube videos, Reddit discussions, Wolfram Alpha calculations, writing assistance, or general web search. +🧭 **Pick your sources** - Search the web, discussions, or academic papers. More sources and integrations are in progress. + +🧩 **Widgets** - Helpful UI cards that show up when relevant, like weather, calculations, stock prices, and other quick lookups. 🔍 **Web search powered by SearxNG** - Access multiple search engines while keeping your identity private. Support for Tavily and Exa coming soon for even better results. @@ -237,13 +239,8 @@ Perplexica runs on Next.js and handles all API requests. It works right away on ## Upcoming Features -- [x] Add settings page -- [x] Adding support for local LLMs -- [x] History Saving features -- [x] Introducing various Focus Modes -- [x] Adding API support -- [x] Adding Discover -- [ ] Finalizing Copilot Mode +- [] Adding more widgets, integrations, search sources +- [] Adding authentication ## Support Us From 5a44319d854847ab8795450e87a7b81291ee3984 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 27 Dec 2025 13:09:01 +0530 Subject: [PATCH 190/196] feat(guides): update contributing guides --- CONTRIBUTING.md | 62 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04416e1..6b4ce5b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,33 +11,63 @@ Perplexica's codebase is organized as follows: - **UI Components and Pages**: - **Components (`src/components`)**: Reusable UI components. - **Pages and Routes (`src/app`)**: Next.js app directory structure with page components. - - Main app routes include: home (`/`), chat (`/c`), discover (`/discover`), library (`/library`), and settings (`/settings`). - - **API Routes (`src/app/api`)**: API endpoints implemented with Next.js API routes. - - `/api/chat`: Handles chat interactions. - - `/api/search`: Provides direct access to Perplexica's search capabilities. - - Other endpoints for models, files, and suggestions. + - Main app routes include: home (`/`), chat (`/c`), discover (`/discover`), and library (`/library`). + - **API Routes (`src/app/api`)**: Server endpoints implemented with Next.js route handlers. - **Backend Logic (`src/lib`)**: Contains all the backend functionality including search, database, and API logic. - - The search functionality is present inside `src/lib/search` directory. - - All of the focus modes are implemented using the Meta Search Agent class in `src/lib/search/metaSearchAgent.ts`. + - The search system lives in `src/lib/agents/search`. + - The search pipeline is split into classification, research, widgets, and writing. - Database functionality is in `src/lib/db`. - - Chat model and embedding model providers are managed in `src/lib/providers`. - - Prompt templates and LLM chain definitions are in `src/lib/prompts` and `src/lib/chains` respectively. + - Chat model and embedding model providers are in `src/lib/models/providers`, and models are loaded via `src/lib/models/registry.ts`. + - Prompt templates are in `src/lib/prompts`. + - SearXNG integration is in `src/lib/searxng.ts`. + - Upload search lives in `src/lib/uploads`. + +### Where to make changes + +If you are not sure where to start, use this section as a map. + +- **Search behavior and reasoning** + + - `src/lib/agents/search` contains the core chat and search pipeline. + - `classifier.ts` decides whether research is needed and what should run. + - `researcher/` gathers information in the background. + +- **Add or change a search capability** + + - Research tools (web, academic, discussions, uploads, scraping) live in `src/lib/agents/search/researcher/actions`. + - Tools are registered in `src/lib/agents/search/researcher/actions/index.ts`. + +- **Add or change widgets** + + - Widgets live in `src/lib/agents/search/widgets`. + - Widgets run in parallel with research and show structured results in the UI. + +- **Model integrations** + + - Providers live in `src/lib/models/providers`. + - Add new providers there and wire them into the model registry so they show up in the app. + +- **Architecture docs** + - High level overview: `docs/architecture/README.md` + - High level flow: `docs/architecture/WORKING.md` ## API Documentation -Perplexica exposes several API endpoints for programmatic access, including: +Perplexica includes API documentation for programmatic access. -- **Search API**: Access Perplexica's advanced search capabilities directly via the `/api/search` endpoint. For detailed documentation, see `docs/api/search.md`. +- **Search API**: For detailed documentation, see `docs/API/SEARCH.md`. ## Setting Up Your Environment Before diving into coding, setting up your local environment is key. Here's what you need to do: -1. In the root directory, locate the `sample.config.toml` file. -2. Rename it to `config.toml` and fill in the necessary configuration fields. -3. Run `npm install` to install all dependencies. -4. Run `npm run db:migrate` to set up the local sqlite database. -5. Use `npm run dev` to start the application in development mode. +1. Run `npm install` to install all dependencies. +2. Use `npm run dev` to start the application in development mode. +3. Open http://localhost:3000 and complete the setup in the UI (API keys, models, search backend URL, etc.). + +Database migrations are applied automatically on startup. + +For full installation options (Docker and non Docker), see the installation guide in the repository README. **Please note**: Docker configurations are present for setting up production environments, whereas `npm run dev` is used for development purposes. From 65fdecb122c1d42d974de73c98cb52141ae5705a Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 27 Dec 2025 13:09:11 +0530 Subject: [PATCH 191/196] feat(docs): update architecture docs --- docs/architecture/README.md | 43 ++++++++++++++++---- docs/architecture/WORKING.md | 77 ++++++++++++++++++++++++++++++------ 2 files changed, 100 insertions(+), 20 deletions(-) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 5732471..5593b37 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -1,11 +1,38 @@ -# Perplexica's Architecture +# Perplexica Architecture -Perplexica's architecture consists of the following key components: +Perplexica is a Next.js application that combines an AI chat experience with search. -1. **User Interface**: A web-based interface that allows users to interact with Perplexica for searching images, videos, and much more. -2. **Agent/Chains**: These components predict Perplexica's next actions, understand user queries, and decide whether a web search is necessary. -3. **SearXNG**: A metadata search engine used by Perplexica to search the web for sources. -4. **LLMs (Large Language Models)**: Utilized by agents and chains for tasks like understanding content, writing responses, and citing sources. Examples include Claude, GPTs, etc. -5. **Embedding Models**: To improve the accuracy of search results, embedding models re-rank the results using similarity search algorithms such as cosine similarity and dot product distance. +For a high level flow, see [WORKING.md](WORKING.md). For deeper implementation details, see [CONTRIBUTING.md](../../CONTRIBUTING.md). -For a more detailed explanation of how these components work together, see [WORKING.md](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/architecture/WORKING.md). +## Key components + +1. **User Interface** + + - A web based UI that lets users chat, search, and view citations. + +2. **API Routes** + + - `POST /api/chat` powers the chat UI. + - `POST /api/search` provides a programmatic search endpoint. + - `GET /api/providers` lists available providers and model keys. + +3. **Agents and Orchestration** + + - The system classifies the question first. + - It can run research and widgets in parallel. + - It generates the final answer and includes citations. + +4. **Search Backend** + + - A meta search backend is used to fetch relevant web results when research is enabled. + +5. **LLMs (Large Language Models)** + + - Used for classification, writing answers, and producing citations. + +6. **Embedding Models** + + - Used for semantic search over user uploaded files. + +7. **Storage** + - Chats and messages are stored so conversations can be reloaded. diff --git a/docs/architecture/WORKING.md b/docs/architecture/WORKING.md index 6bad4f9..af29b90 100644 --- a/docs/architecture/WORKING.md +++ b/docs/architecture/WORKING.md @@ -1,19 +1,72 @@ -# How does Perplexica work? +# How Perplexica Works -Curious about how Perplexica works? Don't worry, we'll cover it here. Before we begin, make sure you've read about the architecture of Perplexica to ensure you understand what it's made up of. Haven't read it? You can read it [here](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/architecture/README.md). +This is a high level overview of how Perplexica answers a question. -We'll understand how Perplexica works by taking an example of a scenario where a user asks: "How does an A.C. work?". We'll break down the process into steps to make it easier to understand. The steps are as follows: +If you want a component level overview, see [README.md](README.md). -1. The message is sent to the `/api/chat` route where it invokes the chain. The chain will depend on your focus mode. For this example, let's assume we use the "webSearch" focus mode. -2. The chain is now invoked; first, the message is passed to another chain where it first predicts (using the chat history and the question) whether there is a need for sources and searching the web. If there is, it will generate a query (in accordance with the chat history) for searching the web that we'll take up later. If not, the chain will end there, and then the answer generator chain, also known as the response generator, will be started. -3. The query returned by the first chain is passed to SearXNG to search the web for information. -4. After the information is retrieved, it is based on keyword-based search. We then convert the information into embeddings and the query as well, then we perform a similarity search to find the most relevant sources to answer the query. -5. After all this is done, the sources are passed to the response generator. This chain takes all the chat history, the query, and the sources. It generates a response that is streamed to the UI. +If you want implementation details, see [CONTRIBUTING.md](../../CONTRIBUTING.md). -## How are the answers cited? +## What happens when you ask a question -The LLMs are prompted to do so. We've prompted them so well that they cite the answers themselves, and using some UI magic, we display it to the user. +When you send a message in the UI, the app calls `POST /api/chat`. -## Image and Video Search +At a high level, we do three things: -Image and video searches are conducted in a similar manner. A query is always generated first, then we search the web for images and videos that match the query. These results are then returned to the user. +1. Classify the question and decide what to do next. +2. Run research and widgets in parallel. +3. Write the final answer and include citations. + +## Classification + +Before searching or answering, we run a classification step. + +This step decides things like: + +- Whether we should do research for this question +- Whether we should show any widgets +- How to rewrite the question into a clearer standalone form + +## Widgets + +Widgets are small, structured helpers that can run alongside research. + +Examples include weather, stocks, and simple calculations. + +If a widget is relevant, we show it in the UI while the answer is still being generated. + +Widgets are helpful context for the answer, but they are not part of what the model should cite. + +## Research + +If research is needed, we gather information in the background while widgets can run. + +Depending on configuration, research may include web lookup and searching user uploaded files. + +## Answer generation + +Once we have enough context, the chat model generates the final response. + +You can control the tradeoff between speed and quality using `optimizationMode`: + +- `speed` +- `balanced` +- `quality` + +## How citations work + +We prompt the model to cite the references it used. The UI then renders those citations alongside the supporting links. + +## Search API + +If you are integrating Perplexica into another product, you can call `POST /api/search`. + +It returns: + +- `message`: the generated answer +- `sources`: supporting references used for the answer + +You can also enable streaming by setting `stream: true`. + +## Image and video search + +Image and video search use separate endpoints (`POST /api/images` and `POST /api/videos`). We generate a focused query using the chat model, then fetch matching results from a search backend. From b5ba8c48c0c9a6537db6a3cfcbb6e61c47a8ede8 Mon Sep 17 00:00:00 2001 From: Kushagra Srivastava <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 27 Dec 2025 13:14:46 +0530 Subject: [PATCH 192/196] Update src/components/WeatherWidget.tsx Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- src/components/WeatherWidget.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/WeatherWidget.tsx b/src/components/WeatherWidget.tsx index 3ba0038..a7ebcff 100644 --- a/src/components/WeatherWidget.tsx +++ b/src/components/WeatherWidget.tsx @@ -91,7 +91,7 @@ const WeatherWidget = () => { setData({ temperature: data.temperature, condition: data.condition, - location: 'Mars', + location: location.city, humidity: data.humidity, windSpeed: data.windSpeed, icon: data.icon, From 6919ad1a0f30bc29a792055b8ced04a140633db9 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 27 Dec 2025 13:24:35 +0530 Subject: [PATCH 193/196] feat(app): address review --- drizzle/meta/0002_snapshot.json | 4 ++-- src/lib/agents/search/widgets/calculationWidget.ts | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json index 2890f66..feb820c 100644 --- a/drizzle/meta/0002_snapshot.json +++ b/drizzle/meta/0002_snapshot.json @@ -28,8 +28,8 @@ "notNull": true, "autoincrement": false }, - "focusMode": { - "name": "focusMode", + "sources": { + "name": "sources", "type": "text", "primaryKey": false, "notNull": true, diff --git a/src/lib/agents/search/widgets/calculationWidget.ts b/src/lib/agents/search/widgets/calculationWidget.ts index 0026741..3e28015 100644 --- a/src/lib/agents/search/widgets/calculationWidget.ts +++ b/src/lib/agents/search/widgets/calculationWidget.ts @@ -51,6 +51,10 @@ const calculationWidget: Widget = { schema, }); + if (output.notPresent) { + return; + } + const result = mathEval(output.expression); return { From ec5ff6f4a8c8329d409a6eb9548664e6a0ac8c55 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 27 Dec 2025 13:26:07 +0530 Subject: [PATCH 194/196] Update plan.ts --- src/lib/agents/search/researcher/actions/plan.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/agents/search/researcher/actions/plan.ts b/src/lib/agents/search/researcher/actions/plan.ts index d89d64c..32ea623 100644 --- a/src/lib/agents/search/researcher/actions/plan.ts +++ b/src/lib/agents/search/researcher/actions/plan.ts @@ -17,7 +17,7 @@ Here are some examples of good plans: <examples> - "Okay, the user wants to know the latest advancements in renewable energy. I will start by looking for recent articles and studies on this topic, then summarize the key points." -> "I have gathered enough information to provide a comprehensive answer." - "The user is asking about the health benefits of a Mediterranean diet. I will search for scientific studies and expert opinions on this diet, then compile the findings into a clear summary." -> "I have gathered information about the Mediterranean diet and its health benefits, I will now look up for any recent studies to ensure the information is current." -<examples> +</examples> YOU CAN NEVER CALL ANY OTHER TOOL BEFORE CALLING THIS ONE FIRST, IF YOU DO, THAT CALL WOULD BE IGNORED. `; From 9620e63e3f7fecb50edb0907195b6fcc73314b5b Mon Sep 17 00:00:00 2001 From: Kushagra Srivastava <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 27 Dec 2025 13:29:43 +0530 Subject: [PATCH 195/196] Update src/components/MessageActions/Copy.tsx Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- src/components/MessageActions/Copy.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/components/MessageActions/Copy.tsx b/src/components/MessageActions/Copy.tsx index f2509f4..7a3d518 100644 --- a/src/components/MessageActions/Copy.tsx +++ b/src/components/MessageActions/Copy.tsx @@ -21,15 +21,16 @@ const Copy = ({ ) as SourceBlock[]; const contentToCopy = `${initialMessage}${ - sources.length > 0 && - `\n\nCitations:\n${sources - .map((source) => source.data) - .flat() - .map( - (s, i) => - `[${i + 1}] ${s.metadata.url.startsWith('file_id://') ? s.metadata.fileName || 'Uploaded File' : s.metadata.url}`, - ) - .join(`\n`)}` + sources.length > 0 + ? `\n\nCitations:\n${sources + .map((source) => source.data) + .flat() + .map( + (s, i) => + `[${i + 1}] ${s.metadata.url.startsWith('file_id://') ? s.metadata.fileName || 'Uploaded File' : s.metadata.url}`, + ) + .join(`\n`)}` + : '' }`; navigator.clipboard.writeText(contentToCopy); From 7f3f88196412ddd3a2d28e0aff13063bcadf0431 Mon Sep 17 00:00:00 2001 From: Kushagra Srivastava <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 27 Dec 2025 13:32:20 +0530 Subject: [PATCH 196/196] Update src/components/Navbar.tsx Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- src/components/Navbar.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index ae32685..6d3e77c 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -205,8 +205,9 @@ const Navbar = () => { useEffect(() => { if (sections.length > 0 && sections[0].message) { const newTitle = - sections[0].message.query.substring(0, 30) + '...' || - 'New Conversation'; + sections[0].message.query.length > 30 + ? `${sections[0].message.query.substring(0, 30).trim()}...` + : sections[0].message.query || 'New Conversation'; setTitle(newTitle); const newTimeAgo = formatTimeDifference(