feat(search-agent): add researcher, research actions

This commit is contained in:
ItzCrazyKns
2025-11-21 23:57:29 +05:30
parent 0df0114e76
commit 08feb18197
6 changed files with 313 additions and 5 deletions

View File

@@ -0,0 +1,19 @@
import z from 'zod';
import { ResearchAction } from '../../types';
const doneAction: ResearchAction<any> = {
name: 'done',
description:
"Indicates that the research process is complete and no further actions are needed. Use this action when you have gathered sufficient information to answer the user's query.",
enabled: (_) => true,
schema: z.object({
type: z.literal('done'),
}),
execute: async (params, additionalConfig) => {
return {
type: 'done',
};
},
};
export default doneAction;

View File

@@ -0,0 +1,8 @@
import doneAction from './done';
import ActionRegistry from './registry';
import webSearchAction from './webSearch';
ActionRegistry.register(webSearchAction);
ActionRegistry.register(doneAction);
export { ActionRegistry };

View File

@@ -0,0 +1,73 @@
import {
ActionConfig,
ActionOutput,
AdditionalConfig,
ClassifierOutput,
ResearchAction,
} from '../../types';
class ActionRegistry {
private static actions: Map<string, ResearchAction> = new Map();
static register(action: ResearchAction<any>) {
this.actions.set(action.name, action);
}
static get(name: string): ResearchAction | undefined {
return this.actions.get(name);
}
static getAvailableActions(config: {
classification: ClassifierOutput;
}): ResearchAction[] {
return Array.from(
this.actions.values().filter((action) => action.enabled(config)),
);
}
static getAvailableActionsDescriptions(config: {
classification: ClassifierOutput;
}): string {
const availableActions = this.getAvailableActions(config);
return availableActions
.map((action) => `------------\n##${action.name}\n${action.description}`)
.join('\n\n');
}
static async execute(
name: string,
params: any,
additionalConfig: AdditionalConfig,
) {
const action = this.actions.get(name);
if (!action) {
throw new Error(`Action with name ${name} not found`);
}
return action.execute(params, additionalConfig);
}
static async executeAll(
actions: ActionConfig[],
additionalConfig: AdditionalConfig,
): Promise<ActionOutput[]> {
const results: ActionOutput[] = [];
await Promise.all(
actions.map(async (actionConfig) => {
const output = await this.execute(
actionConfig.type,
actionConfig.params,
additionalConfig,
);
results.push(output);
}),
);
return results;
}
}
export default ActionRegistry;

View File

@@ -0,0 +1,54 @@
import z from 'zod';
import { ResearchAction } from '../../types';
import { searchSearxng } from '@/lib/searxng';
const actionSchema = z.object({
type: z.literal('web_search'),
queries: z
.array(z.string())
.describe('An array of search queries to perform web searches for.'),
});
const actionDescription = `
You have to use this action aggressively to find relevant information from the web to answer user queries. You can combine this action with other actions to gather comprehensive data. Always ensure that you provide accurate and up-to-date information by leveraging web search results.
When this action is present, you must use it to obtain current information from the web.
### How to use:
1. For fast search mode, you can use this action once. Make sure to cover all aspects of the user's query in that single search.
2. If you're on quality mode, you'll get to use this action up to two times. Use the first search to gather general information, and the second search to fill in any gaps or get more specific details based on the initial findings.
3. If you're set on Deep research mode, then you will get to use this action multiple times to gather more information. Use your judgment to decide when additional searches are necessary to provide a thorough and accurate response.
Input: An array of search queries. Make sure the queries are relevant to the user's request and cover different aspects if necessary. You can include a maximum of 3 queries. Make sure the queries are SEO friendly and not sentences rather keywords which can be used to search a search engine like Google, Bing, etc.
`;
const webSearchAction: ResearchAction<typeof actionSchema> = {
name: 'web_search',
description: actionDescription,
schema: actionSchema,
enabled: (config) => config.classification.intents.includes('web_search'),
execute: async (input, _) => {
let results: Chunk[] = [];
const search = async (q: string) => {
const res = await searchSearxng(q);
res.results.forEach((r) => {
results.push({
content: r.content || r.title,
metadata: {
title: r.title,
url: r.url,
},
});
});
};
await Promise.all(input.queries.map(search));
return {
type: 'search_results',
results,
};
},
};
export default webSearchAction;

View File

@@ -0,0 +1,113 @@
import z from 'zod';
import {
ActionConfig,
ActionOutput,
ResearcherInput,
ResearcherOutput,
} from '../types';
import { ActionRegistry } from './actions';
import { getResearcherPrompt } from '@/lib/prompts/search/researcher';
import SessionManager from '@/lib/session';
class Researcher {
async research(
session: SessionManager,
input: ResearcherInput,
): Promise<ResearcherOutput> {
let findings: string = '';
let actionOutput: ActionOutput[] = [];
let maxIteration =
input.config.mode === 'fast'
? 1
: input.config.mode === 'balanced'
? 3
: 25;
const availableActions = ActionRegistry.getAvailableActions({
classification: input.classification,
});
const schema = z.object({
reasoning: z
.string()
.describe('The reasoning behind choosing the next action.'),
action: z
.union(availableActions.map((a) => a.schema))
.describe('The action to be performed next.'),
});
const availableActionsDescription =
ActionRegistry.getAvailableActionsDescriptions({
classification: input.classification,
});
for (let i = 0; i < maxIteration; i++) {
const researcherPrompt = getResearcherPrompt(availableActionsDescription);
const res = await input.config.llm.generateObject<z.infer<typeof schema>>(
{
messages: [
{
role: 'system',
content: researcherPrompt,
},
{
role: 'user',
content: `
<research_query>
${input.classification.standaloneFollowUp}
</research_query>
<previous_actions>
${findings}
</previous_actions>
`,
},
],
schema,
},
);
if (res.action.type === 'done') {
console.log('Research complete - "done" action selected');
break;
}
const actionConfig: ActionConfig = {
type: res.action.type as string,
params: res.action,
};
findings += 'Reasoning: ' + res.reasoning + '\n';
findings += `Executing Action: ${actionConfig.type} with params ${JSON.stringify(actionConfig.params)}\n`;
const actionResult = await ActionRegistry.execute(
actionConfig.type,
actionConfig.params,
{
llm: input.config.llm,
embedding: input.config.embedding,
session: session,
},
);
actionOutput.push(actionResult);
if (actionResult.type === 'search_results') {
findings += actionResult.results
.map(
(r) =>
`Title: ${r.metadata.title}\nURL: ${r.metadata.url}\nContent: ${r.content}\n`,
)
.join('\n');
}
}
return {
findings: actionOutput,
};
}
}
export default Researcher;

View File

@@ -1,7 +1,7 @@
import { EventEmitter } from 'stream';
import z from 'zod';
import BaseLLM from '../../models/base/llm';
import BaseEmbedding from '@/lib/models/base/embedding';
import SessionManager from '@/lib/session';
export type SearchSources = 'web' | 'discussions' | 'academic';
@@ -9,10 +9,11 @@ export type SearchAgentConfig = {
sources: SearchSources[];
llm: BaseLLM<any>;
embedding: BaseEmbedding<any>;
mode: 'fast' | 'balanced' | 'deep_research';
};
export type SearchAgentInput = {
chatHistory: Message[];
chatHistory: ChatTurnMessage[];
followUp: string;
config: SearchAgentConfig;
};
@@ -48,7 +49,7 @@ export type ClassifierInput = {
llm: BaseLLM<any>;
enabledSources: SearchSources[];
query: string;
chatHistory: Message[];
chatHistory: ChatTurnMessage[];
};
export type ClassifierOutput = {
@@ -60,6 +61,46 @@ export type ClassifierOutput = {
export type AdditionalConfig = {
llm: BaseLLM<any>;
embedding: BaseLLM<any>;
emitter: EventEmitter;
embedding: BaseEmbedding<any>;
session: SessionManager;
};
export type ResearcherInput = {
chatHistory: ChatTurnMessage[];
followUp: string;
classification: ClassifierOutput;
config: SearchAgentConfig;
};
export type ResearcherOutput = {
findings: ActionOutput[];
};
export type SearchActionOutput = {
type: 'search_results';
results: Chunk[];
};
export type DoneActionOutput = {
type: 'done';
};
export type ActionOutput = SearchActionOutput | DoneActionOutput;
export interface ResearchAction<
TSchema extends z.ZodObject<any> = z.ZodObject<any>,
> {
name: string;
description: string;
schema: z.ZodObject<any>;
enabled: (config: { classification: ClassifierOutput }) => boolean;
execute: (
params: z.infer<TSchema>,
additionalConfig: AdditionalConfig,
) => Promise<ActionOutput>;
}
export type ActionConfig = {
type: string;
params: Record<string, any>;
};