Skip to content

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

platform/apps/app/src/lib/rag/query-rewrite.ts
/**
* 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 };
}
}