/** * BrainBank — Query Decomposer * * Uses Haiku to break complex/ambiguous natural language queries into * 2-3 focused sub-queries that each target a specific aspect of the * original intent. This improves embedding coverage for multi-concept * queries where a single embedding vector can't capture all facets. * * Example: * Input: "user availability shiftStatus signIn signOut available unavailable" * Output: [ * "user signIn signOut authentication session methods", * "shiftStatus isAvailable availability entity fields", * "responder availability state management workflow" * ] * * Latency: ~300-500ms (single Haiku call). * Cost: ~0.0001$ per decomposition. */ const DEFAULT_MODEL = 'claude-haiku-4-5-20251001'; const _debug = !!process.env.BRAINBANK_DEBUG; function dbg(msg: string): void { if (_debug) process.stderr.write(`[query-decomposer] ${msg}\n`); } /** Minimum word count to trigger decomposition. Short queries pass through. */ const MIN_WORDS_FOR_DECOMPOSITION = 6; export interface QueryDecomposerOptions { /** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var. */ apiKey?: string; /** Model to use. Default: claude-haiku-4-5-20251001 */ model?: string; } export class QueryDecomposer { private readonly _apiKey: string; private readonly _model: string; constructor(options: QueryDecomposerOptions = {}) { this._apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY ?? ''; this._model = options.model ?? DEFAULT_MODEL; } /** * Decompose a complex query into 2-3 focused sub-queries. * Returns the original query + generated sub-queries. * For simple queries (< 6 words), returns just the original. */ async decompose(query: string): Promise { // Always include original query const words = query.trim().split(/\s+/); if (words.length < MIN_WORDS_FOR_DECOMPOSITION || !this._apiKey) { dbg(`Skip decomposition: ${words.length} words (min: ${MIN_WORDS_FOR_DECOMPOSITION})`); return [query]; } try { const prompt = `You are a code search query optimizer. Given a complex search query, decompose it into 2-3 focused sub-queries that each target a DIFFERENT aspect of the original intent.\n\n` + `Original query: "${query}"\n\n` + `Rules:\n` + `- Each sub-query should be 4-8 words, mixing natural language with code identifiers\n` + `- Sub-queries should be COMPLEMENTARY, not overlapping\n` + `- Preserve code identifiers (camelCase, PascalCase) exactly as written\n` + `- Focus on: (1) the core action/method, (2) the data/entity, (3) the workflow/context\n` + `- Return ONLY a JSON array of strings. No explanation.\n\n` + `Example:\n` + `Query: "offer lifecycle arrived started admin behalf responder availability"\n` + `["offer state machine arrived started transition", "admin acting behalf responder proxy", "responder availability isAvailable update"]\n\n` + `Respond with ONLY the JSON array:`; const response = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': this._apiKey, 'anthropic-version': '2023-06-01', }, body: JSON.stringify({ model: this._model, max_tokens: 256, messages: [{ role: 'user', content: prompt }], }), }); if (!response.ok) { dbg(`API error: ${response.status} — using original query only`); return [query]; } const data = await response.json() as { content: { type: string; text: string }[]; }; const text = data.content?.[0]?.text ?? ''; dbg(`Raw response: ${text}`); // Extract JSON array from response (may be wrapped in ```json ... ```) const match = text.match(/\[[\s\S]*?\]/); if (!match) { dbg(`No JSON array found — using original query only`); return [query]; } const subQueries = JSON.parse(match[0]) as string[]; if (!Array.isArray(subQueries) || subQueries.length === 0) { return [query]; } // Cap at 3 sub-queries + always include original const result = [query, ...subQueries.slice(0, 3)]; dbg(`Decomposed into ${result.length} queries: ${JSON.stringify(result)}`); return result; } catch (err) { dbg(`Error: ${err instanceof Error ? err.message : String(err)} — using original query only`); return [query]; } } /** Check if the decomposer is available (has API key). */ get available(): boolean { return !!this._apiKey; } }