Follow-up query rewrite
Source: platform/apps/app/src/lib/rag/query-rewrite.ts · rendered from main on every deploy — edit in the repo, not here
/** * Query rewriting for multi-turn chat (ADR-0017 §2). * * Takes conversation history + current query and rewrites the query * as a standalone question that captures all necessary context. * This enables the existing RAG pipeline to work without modification. */
import { generateText } from 'ai';
interface HistoryMessage { role: 'user' | 'assistant'; content: string;}
interface RewriteResult { rewritten: string; wasRewritten: boolean;}
/** * Rewrite a follow-up query into a standalone question using conversation history. * Returns the original query unchanged if: * - No history exists (first turn) * - The query is already self-contained (heuristic) * - LLM call fails (graceful fallback) */export async function rewriteQuery( history: HistoryMessage[], currentQuery: string, apiKey: string, locale: string, gw?: { AI_GATEWAY_URL?: string; AI_GATEWAY_TOKEN?: string; ANTHROPIC_API_KEY?: string },): Promise<RewriteResult> { // First turn or very short history — no rewrite needed if (history.length === 0) { return { rewritten: currentQuery, wasRewritten: false }; }
// Heuristic: skip rewrite for trivial follow-ups that don't reference context const referencePatterns = locale === 'th' ? /(?:นั้น|นี้|มัน|เรื่องนี้|ข้อนั้น|ข้างต้น|ดังกล่าว|อันไหน|ตรงนั้น|แล้ว.*ล่ะ|เพิ่มเติม)/ : /(?:\b(?:it|this|that|these|those|they|them|its|their|above|previous|mentioned|said|the same|more about|what about|how about)\b)/i;
// Short queries without references are standalone (e.g., "ค่าธรรมเนียม", "ISO 9001") if (!referencePatterns.test(currentQuery) && currentQuery.length > 15) { return { rewritten: currentQuery, wasRewritten: false }; }
// Very short acknowledgements — no rewrite value const trivialPatterns = /^(ใช่|ไม่|ครับ|ค่ะ|ok|yes|no|thanks|ขอบคุณ|ดี|โอเค)\s*[.!?]*$/i; if (trivialPatterns.test(currentQuery.trim())) { return { rewritten: currentQuery, wasRewritten: false }; }
// Build compact history (last 6 messages, truncated) const recentHistory = history.slice(-6).map((m) => ({ role: m.role, content: m.content.slice(0, 300), }));
const historyText = recentHistory .map((m) => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`) .join('\n');
const systemPrompt = locale === 'th' ? `เขียนคำถามของผู้ใช้ใหม่ให้เป็นคำถามที่สมบูรณ์ในตัวเอง โดยรวมบริบทจากประวัติสนทนา ตอบเฉพาะคำถามที่เขียนใหม่เท่านั้น ไม่ต้องอธิบายเพิ่ม` : `Rewrite the user's latest question as a standalone query that captures all necessary context from the conversation history. Output only the rewritten query, nothing else.`;
try { const { createLlmProvider } = await import('../chat/llm-provider.js'); const { chatModel } = createLlmProvider(apiKey, gw);
const { text } = await generateText({ model: chatModel, system: systemPrompt, prompt: `Conversation:\n${historyText}\n\nLatest question: ${currentQuery}`, maxOutputTokens: 150, temperature: 0, });
const rewritten = text.trim(); if (!rewritten || rewritten.length < 3) { return { rewritten: currentQuery, wasRewritten: false }; }
return { rewritten, wasRewritten: true }; } catch { // LLM failed — use original query (graceful degradation) return { rewritten: currentQuery, wasRewritten: false }; }}