diff --git a/src/lib/agents/search/classifier/index.ts b/src/lib/agents/search/classifier/index.ts new file mode 100644 index 0000000..9b2bf6b --- /dev/null +++ b/src/lib/agents/search/classifier/index.ts @@ -0,0 +1,72 @@ +import z from 'zod'; +import { ClassifierInput, ClassifierOutput } from '../types'; +import { WidgetRegistry } from '../widgets'; +import { IntentRegistry } from './intents'; +import { getClassifierPrompt } from '@/lib/prompts/search/classifier'; +import formatChatHistoryAsString from '@/lib/utils/formatHistory'; + +class Classifier { + async classify(input: ClassifierInput): Promise { + const availableIntents = IntentRegistry.getAvailableIntents({ + sources: input.enabledSources, + }); + const availableWidgets = WidgetRegistry.getAll(); + + const classificationSchema = z.object({ + skipSearch: z + .boolean() + .describe( + 'Set to true to SKIP search. Skip ONLY when: (1) widgets alone fully answer the query (e.g., weather, stocks, calculator), (2) simple greetings or writing tasks (NOT questions). Set to false for ANY question or information request.', + ), + standaloneFollowUp: z + .string() + .describe( + 'A self-contained, context-independent reformulation of the user\'s question. Must include all necessary context from chat history, replace pronouns with specific nouns, and be clear enough to answer without seeing the conversation. Keep the same complexity as the original question.', + ), + intents: z + .array(z.enum(availableIntents.map((i) => i.name))) + .describe( + 'The intent(s) that best describe how to fulfill the user\'s query. Can include multiple intents (e.g., [\'web_search\', \'widget_response\'] for \'weather in NYC and recent news\'). Always include at least one intent when applicable.', + ), + widgets: z + .array(z.union(availableWidgets.map((w) => w.schema))) + .describe( + 'Widgets that can display structured data to answer (fully or partially) the query. Include all applicable widgets regardless of skipSearch value.', + ), + }); + + const classifierPrompt = getClassifierPrompt({ + intentDesc: IntentRegistry.getDescriptions({ + sources: input.enabledSources, + }), + widgetDesc: WidgetRegistry.getDescriptions(), + }); + + const res = await input.llm.generateObject< + z.infer + >({ + messages: [ + { + role: 'system', + content: classifierPrompt, + }, + { + role: 'user', + content: `${formatChatHistoryAsString(input.chatHistory)}\n\n${input.query}`, + }, + ], + schema: classificationSchema, + }); + + res.widgets = res.widgets.map((widgetConfig) => { + return { + type: widgetConfig.type, + params: widgetConfig, + }; + }); + + return res as ClassifierOutput; + } +} + +export default Classifier; diff --git a/src/lib/agents/search/classifier/intents/academicSearch.ts b/src/lib/agents/search/classifier/intents/academicSearch.ts new file mode 100644 index 0000000..b6da377 --- /dev/null +++ b/src/lib/agents/search/classifier/intents/academicSearch.ts @@ -0,0 +1,11 @@ +import { Intent } from '../../types'; + +const academicSearchIntent: Intent = { + name: 'academic_search', + description: + 'Use this intent to find scholarly articles, research papers, and academic resources when the user is seeking credible and authoritative information on a specific topic.', + requiresSearch: true, + enabled: (config) => config.sources.includes('academic'), +}; + +export default academicSearchIntent; diff --git a/src/lib/agents/search/classifier/intents/discussionSearch.ts b/src/lib/agents/search/classifier/intents/discussionSearch.ts new file mode 100644 index 0000000..76b3b01 --- /dev/null +++ b/src/lib/agents/search/classifier/intents/discussionSearch.ts @@ -0,0 +1,11 @@ +import { Intent } from '../../types'; + +const discussionSearchIntent: Intent = { + name: 'discussion_search', + description: + 'Use this intent to search through discussion forums, community boards, or social media platforms when the user is looking for opinions, experiences, or community-driven information on a specific topic.', + requiresSearch: true, + enabled: (config) => config.sources.includes('discussions'), +}; + +export default discussionSearchIntent; diff --git a/src/lib/agents/search/classifier/intents/index.ts b/src/lib/agents/search/classifier/intents/index.ts new file mode 100644 index 0000000..feefd2d --- /dev/null +++ b/src/lib/agents/search/classifier/intents/index.ts @@ -0,0 +1,14 @@ +import academicSearchIntent from './academicSearch'; +import discussionSearchIntent from './discussionSearch'; +import IntentRegistry from './registry'; +import webSearchIntent from './webSearch'; +import widgetResponseIntent from './widgetResponse'; +import writingTaskIntent from './writingTask'; + +IntentRegistry.register(webSearchIntent); +IntentRegistry.register(academicSearchIntent); +IntentRegistry.register(discussionSearchIntent); +IntentRegistry.register(widgetResponseIntent); +IntentRegistry.register(writingTaskIntent); + +export { IntentRegistry }; diff --git a/src/lib/agents/search/classifier/intents/registry.ts b/src/lib/agents/search/classifier/intents/registry.ts new file mode 100644 index 0000000..bc3464b --- /dev/null +++ b/src/lib/agents/search/classifier/intents/registry.ts @@ -0,0 +1,29 @@ +import { Intent, SearchAgentConfig, SearchSources } from '../../types'; + +class IntentRegistry { + private static intents = new Map(); + + static register(intent: Intent) { + this.intents.set(intent.name, intent); + } + + static get(name: string): Intent | undefined { + return this.intents.get(name); + } + + static getAvailableIntents(config: { sources: SearchSources[] }): Intent[] { + return Array.from( + this.intents.values().filter((intent) => intent.enabled(config)), + ); + } + + static getDescriptions(config: { sources: SearchSources[] }): string { + const availableintnets = this.getAvailableIntents(config); + + return availableintnets + .map((intent) => `${intent.name}: ${intent.description}`) + .join('\n\n'); + } +} + +export default IntentRegistry; diff --git a/src/lib/agents/search/classifier/intents/webSearch.ts b/src/lib/agents/search/classifier/intents/webSearch.ts new file mode 100644 index 0000000..9fccd2f --- /dev/null +++ b/src/lib/agents/search/classifier/intents/webSearch.ts @@ -0,0 +1,11 @@ +import { Intent } from '../../types'; + +const webSearchIntent: Intent = { + name: 'web_search', + description: + 'Use this intent to find current information from the web when the user is asking a question or needs up-to-date information that cannot be provided by widgets or other intents.', + requiresSearch: true, + enabled: (config) => config.sources.includes('web'), +}; + +export default webSearchIntent; diff --git a/src/lib/agents/search/classifier/intents/widgetResponse.ts b/src/lib/agents/search/classifier/intents/widgetResponse.ts new file mode 100644 index 0000000..0cfd58d --- /dev/null +++ b/src/lib/agents/search/classifier/intents/widgetResponse.ts @@ -0,0 +1,11 @@ +import { Intent } from '../../types'; + +const widgetResponseIntent: Intent = { + name: 'widget_response', + description: + 'Use this intent to respond to user queries using available widgets when the required information can be obtained from them.', + requiresSearch: false, + enabled: (config) => true, +}; + +export default widgetResponseIntent; diff --git a/src/lib/agents/search/classifier/intents/writingTask.ts b/src/lib/agents/search/classifier/intents/writingTask.ts new file mode 100644 index 0000000..95b5af6 --- /dev/null +++ b/src/lib/agents/search/classifier/intents/writingTask.ts @@ -0,0 +1,11 @@ +import { Intent } from '../../types'; + +const writingTaskIntent: Intent = { + name: 'writing_task', + description: + 'Use this intent to assist users with writing tasks such as drafting emails, creating documents, or generating content based on their instructions or greetings.', + requiresSearch: false, + enabled: (config) => true, +}; + +export default writingTaskIntent; diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts index c65d940..72d7ecf 100644 --- a/src/lib/agents/search/types.ts +++ b/src/lib/agents/search/types.ts @@ -53,6 +53,7 @@ export type ClassifierInput = { export type ClassifierOutput = { skipSearch: boolean; + standaloneFollowUp: string; intents: string[]; widgets: WidgetConfig[]; };