feat(search): add classifier

This commit is contained in:
ItzCrazyKns
2025-11-20 14:55:24 +05:30
parent 8b515201f3
commit 036b44611f
9 changed files with 171 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
import z from 'zod';
import { ClassifierInput, ClassifierOutput } from '../types';
import { WidgetRegistry } from '../widgets';
import { IntentRegistry } from './intents';
import { getClassifierPrompt } from '@/lib/prompts/search/classifier';
import formatChatHistoryAsString from '@/lib/utils/formatHistory';
class Classifier {
async classify(input: ClassifierInput): Promise<ClassifierOutput> {
const availableIntents = IntentRegistry.getAvailableIntents({
sources: input.enabledSources,
});
const availableWidgets = WidgetRegistry.getAll();
const classificationSchema = z.object({
skipSearch: z
.boolean()
.describe(
'Set to true to SKIP search. Skip ONLY when: (1) widgets alone fully answer the query (e.g., weather, stocks, calculator), (2) simple greetings or writing tasks (NOT questions). Set to false for ANY question or information request.',
),
standaloneFollowUp: z
.string()
.describe(
'A self-contained, context-independent reformulation of the user\'s question. Must include all necessary context from chat history, replace pronouns with specific nouns, and be clear enough to answer without seeing the conversation. Keep the same complexity as the original question.',
),
intents: z
.array(z.enum(availableIntents.map((i) => i.name)))
.describe(
'The intent(s) that best describe how to fulfill the user\'s query. Can include multiple intents (e.g., [\'web_search\', \'widget_response\'] for \'weather in NYC and recent news\'). Always include at least one intent when applicable.',
),
widgets: z
.array(z.union(availableWidgets.map((w) => w.schema)))
.describe(
'Widgets that can display structured data to answer (fully or partially) the query. Include all applicable widgets regardless of skipSearch value.',
),
});
const classifierPrompt = getClassifierPrompt({
intentDesc: IntentRegistry.getDescriptions({
sources: input.enabledSources,
}),
widgetDesc: WidgetRegistry.getDescriptions(),
});
const res = await input.llm.generateObject<
z.infer<typeof classificationSchema>
>({
messages: [
{
role: 'system',
content: classifierPrompt,
},
{
role: 'user',
content: `<conversation>${formatChatHistoryAsString(input.chatHistory)}</conversation>\n\n<query>${input.query}</query>`,
},
],
schema: classificationSchema,
});
res.widgets = res.widgets.map((widgetConfig) => {
return {
type: widgetConfig.type,
params: widgetConfig,
};
});
return res as ClassifierOutput;
}
}
export default Classifier;

View File

@@ -0,0 +1,11 @@
import { Intent } from '../../types';
const academicSearchIntent: Intent = {
name: 'academic_search',
description:
'Use this intent to find scholarly articles, research papers, and academic resources when the user is seeking credible and authoritative information on a specific topic.',
requiresSearch: true,
enabled: (config) => config.sources.includes('academic'),
};
export default academicSearchIntent;

View File

@@ -0,0 +1,11 @@
import { Intent } from '../../types';
const discussionSearchIntent: Intent = {
name: 'discussion_search',
description:
'Use this intent to search through discussion forums, community boards, or social media platforms when the user is looking for opinions, experiences, or community-driven information on a specific topic.',
requiresSearch: true,
enabled: (config) => config.sources.includes('discussions'),
};
export default discussionSearchIntent;

View File

@@ -0,0 +1,14 @@
import academicSearchIntent from './academicSearch';
import discussionSearchIntent from './discussionSearch';
import IntentRegistry from './registry';
import webSearchIntent from './webSearch';
import widgetResponseIntent from './widgetResponse';
import writingTaskIntent from './writingTask';
IntentRegistry.register(webSearchIntent);
IntentRegistry.register(academicSearchIntent);
IntentRegistry.register(discussionSearchIntent);
IntentRegistry.register(widgetResponseIntent);
IntentRegistry.register(writingTaskIntent);
export { IntentRegistry };

View File

@@ -0,0 +1,29 @@
import { Intent, SearchAgentConfig, SearchSources } from '../../types';
class IntentRegistry {
private static intents = new Map<string, Intent>();
static register(intent: Intent) {
this.intents.set(intent.name, intent);
}
static get(name: string): Intent | undefined {
return this.intents.get(name);
}
static getAvailableIntents(config: { sources: SearchSources[] }): Intent[] {
return Array.from(
this.intents.values().filter((intent) => intent.enabled(config)),
);
}
static getDescriptions(config: { sources: SearchSources[] }): string {
const availableintnets = this.getAvailableIntents(config);
return availableintnets
.map((intent) => `${intent.name}: ${intent.description}`)
.join('\n\n');
}
}
export default IntentRegistry;

View File

@@ -0,0 +1,11 @@
import { Intent } from '../../types';
const webSearchIntent: Intent = {
name: 'web_search',
description:
'Use this intent to find current information from the web when the user is asking a question or needs up-to-date information that cannot be provided by widgets or other intents.',
requiresSearch: true,
enabled: (config) => config.sources.includes('web'),
};
export default webSearchIntent;

View File

@@ -0,0 +1,11 @@
import { Intent } from '../../types';
const widgetResponseIntent: Intent = {
name: 'widget_response',
description:
'Use this intent to respond to user queries using available widgets when the required information can be obtained from them.',
requiresSearch: false,
enabled: (config) => true,
};
export default widgetResponseIntent;

View File

@@ -0,0 +1,11 @@
import { Intent } from '../../types';
const writingTaskIntent: Intent = {
name: 'writing_task',
description:
'Use this intent to assist users with writing tasks such as drafting emails, creating documents, or generating content based on their instructions or greetings.',
requiresSearch: false,
enabled: (config) => true,
};
export default writingTaskIntent;

View File

@@ -53,6 +53,7 @@ export type ClassifierInput = {
export type ClassifierOutput = {
skipSearch: boolean;
standaloneFollowUp: string;
intents: string[];
widgets: WidgetConfig[];
};