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 = ({
+ );
+};
+
+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',
)}
>