Guided-flow prompt
Source: platform/apps/app/src/lib/chat/flow/prompt.ts · rendered from main on every deploy — edit in the repo, not here
/** * Guided-flow engine — system-prompt guidance (ADR-0169). * * A pure fragment appended to the persona system prompt when a flow is active. * It tells the model which slots to collect, what is already known (so it does * not re-ask), to persist via `updateFlow`, and to keep grounding + consistency. * Localised th/en (the reply language follows the user's message); default Thai. * * This guidance is prompt IP — it lives in the SYSTEM prompt (never sent to the * client) and its signature terms are in the output-leak scanner (guardrails.ts), * so an extraction attempt terminates the stream. */
import type { DraftKind, FlowSpec, FlowState } from './types.js';
type Loc = 'th' | 'en';
const DRAFT_LABEL: Record<Loc, Record<DraftKind, string>> = { th: { reservation: 'ใบจอง', itinerary: 'แผนการเดินทาง', cart: 'รายการสินค้าแนะนำ', lead: 'ข้อมูลผู้สนใจ', generic: 'ผลลัพธ์', }, en: { reservation: 'booking', itinerary: 'itinerary', cart: 'recommended cart', lead: 'inquiry summary', generic: 'result', },};
const T: Record< Loc, { header: (l: string) => string; intro: (l: string) => string; rule: string; missing: (slots: string[], first: string) => string; known: (k: string[]) => string; consistency: string; anchor: (preview: string) => string; delivery: (l: string) => string; }> = { th: { header: (l) => `## โหมดงานทีละขั้น: "${l}"`, intro: (l) => `คุณกำลังช่วยลูกค้าทำ "${l}" ทีละขั้น และต้องใช้เครื่องมือ updateFlow เพื่อจดจำความคืบหน้า`, rule: '**กติกาสำคัญ (ต้องทำทุกครั้ง):** ก่อนพิมพ์คำตอบให้ผู้ใช้ ให้เรียก updateFlow ก่อนเสมอ เมื่อผู้ใช้เพิ่งให้ค่าใดๆ ให้ส่งใน slots (เฉพาะค่าที่เพิ่งได้) และเมื่อคุณกำลังจะเสนอผลสรุป ให้ส่งใน draft ด้วย ถ้าไม่มีข้อมูลใหม่จึงไม่ต้องเรียก', missing: (slots, first) => `ยังต้องถามให้ครบก่อนเสนอผลสรุป: ${slots.join(', ')} — **รอบนี้ห้ามเสนอแพ็กเกจ/ราคา หรือบรรยายภาพรวมยาวๆ** ให้ถามเรื่อง "${first}" เป็นคำถามสั้นๆ ข้อเดียว และถ้ามีตัวเลือกที่ชัดเจน ให้ลงท้ายด้วยบรรทัดขึ้นต้น "BUTTONS:" ตามด้วยตัวเลือกคั่นด้วย "|" เพื่อให้ลูกค้าแตะเลือกได้`, known: (k) => `ข้อมูลที่บันทึกแล้ว: ${k.join(' · ')}`, consistency: '**ความสอดคล้อง:** แนะนำ/สรุปโดยยึดจากความต้องการที่ลูกค้าให้มาเป็นหลัก และคงคำแนะนำให้ตรงกันตลอดบทสนทนา อย่าสลับตัวเลือก/แพ็กเกจไปมาตามผลค้นหาในแต่ละรอบ เว้นแต่ผู้ใช้ขอเปลี่ยนเอง', anchor: (p) => `คุณเคยเสนอไว้แล้วว่า: "${p}" — ต่อยอด/ปรับจากอันนี้ อย่าเปลี่ยนไปเป็นตัวเลือกอื่น`, delivery: (l) => `**การเสนอผลสรุป:** ทุกครั้งที่คุณกำลังจะเสนอ "${l}" / คำแนะนำ / แพ็กเกจ / ข้อสรุปใดๆ ให้เรียก updateFlow พร้อมฟิลด์ draft (เป็น object ที่มีฟิลด์สำคัญ เช่น ตัวเลือกที่แนะนำ ราคา รายละเอียด) **ก่อน** พิมพ์คำตอบเสมอ — ห้ามสรุปหรือแนะนำให้ผู้ใช้โดยไม่บันทึก draft ก่อน สรุปจากฐานความรู้ที่ค้นได้เท่านั้น ห้ามแต่งข้อมูล`, }, en: { header: (l) => `## Step-by-step task mode: "${l}"`, intro: (l) => `You are helping the customer build a "${l}" step by step, and you MUST use the updateFlow tool to record progress.`, rule: '**Critical rule (every turn):** Before you write your reply, call updateFlow first — when the user just gave any value, send it in `slots` (only what is new); when you are about to present a summary, include it in `draft`. If there is nothing new, do not call it.', missing: (slots, first) => `Still needed before you summarise: ${slots.join(', ')} — **this turn, do NOT propose a package/price or a long overview.** Ask ONE short question about "${first}", and if there are clear choices, end with a line starting "BUTTONS:" followed by options separated by "|" so the customer can tap.`, known: (k) => `Already recorded: ${k.join(' · ')}`, consistency: "**Consistency:** Recommend/summarise from the requirements the customer gave, and keep the recommendation the same throughout — do not switch options/packages based on each turn's search results unless the user asks.", anchor: (p) => `You already proposed: "${p}" — build on / refine this; do not switch to a different option.`, delivery: (l) => `**Presenting the summary:** Whenever you are about to propose the "${l}" / a recommendation / a package / any conclusion, call updateFlow with a \`draft\` field (an object with key fields, e.g. the recommended option, price, details) **before** writing your reply — never summarise or recommend without recording the draft first. Use only retrieved knowledge; never invent.`, },};
export function buildFlowGuidance(spec: FlowSpec, state: FlowState, locale: string = 'th'): string { const L: Loc = locale.toLowerCase().startsWith('en') ? 'en' : 'th'; const t = T[L]; const draftLabel = DRAFT_LABEL[L][spec.draftKind]; const isFilled = (key: string) => state.slots[key] !== undefined && state.slots[key] !== null;
const known = spec.slots .filter((s) => isFilled(s.key)) .map((s) => `${s.key}=${String(state.slots[s.key])}`); const missingRequired = spec.slots .filter((s) => s.required && !isFilled(s.key)) .map((s) => s.key);
const lines: string[] = [t.header(draftLabel), t.intro(draftLabel), t.rule]; if (missingRequired.length > 0) { lines.push(t.missing(missingRequired, missingRequired[0])); } if (known.length > 0) { lines.push(t.known(known)); } lines.push(t.consistency); if (state.draft) { const preview = ( typeof (state.draft as Record<string, unknown>).summary === 'string' ? ((state.draft as Record<string, unknown>).summary as string) : JSON.stringify(state.draft) ).slice(0, 300); lines.push(t.anchor(preview)); } lines.push(t.delivery(draftLabel)); return lines.join('\n');}