//! Builds the OpenAI system prompt from the loaded template registry.
//!
//! The prompt lists available presets (grouped by category), themes, and
//! primitives so the LLM knows which preset to pick and what data shape to
//! produce.
use crate::registry::TemplateRegistry;
/// Construct the system prompt for the chat endpoint.
pub fn build_system_prompt(registry: &TemplateRegistry) -> String {
let mut prompt = String::from(
"You are an Adaptive Card flow builder. Given a user request, \
pick the best preset and generate data for it.\n\n\
AVAILABLE PRESETS (grouped by category):\n",
);
for category in ®istry.categories {
prompt.push_str(&format!("\n{}:\n", category.display_name.to_uppercase()));
for preset_name in &category.presets {
if let Some(preset) = registry.get_preset(preset_name) {
prompt.push_str(&format!("- {}: {}\n", preset.name, preset.description));
}
}
}
prompt.push_str("\nTHEMES: ");
prompt.push_str(®istry.themes.names().join(", "));
prompt.push_str("\n\nPRIMITIVES (for compose mode):\n");
for primitive in ®istry.primitives_manifest.primitives {
let props: Vec<String> = primitive
.props
.iter()
.map(|(name, prop)| {
if prop.required {
name.clone()
} else {
format!("{name}?")
}
})
.collect();
prompt.push_str(&format!(
"- {} {{ {} }}\n",
primitive.name,
props.join(", ")
));
}
prompt.push_str(
"\n\nRESPONSE FORMAT — pick the right shape:\n\n\
[SHAPE 1] Single card:\n\
{\"preset\": \"preset-name\", \"theme\": \"default\", \"data\": { ...preset data... }}\n\n\
[SHAPE 2] Multi-card flow (REQUIRED card structure — EVERY card MUST have id, preset, and data):\n\
{\n \"flow\": \"flow-name\",\n \"theme\": \"default\",\n \"cards\": [\n {\n \"id\": \"step-1\",\n \"preset\": \"menu-card\",\n \"data\": { ...preset data... },\n \"actions\": [{ \"title\": \"Next\", \"goto\": \"step-2\" }]\n },\n {\n \"id\": \"step-2\",\n \"preset\": \"form-input\",\n \"data\": { ... },\n \"actions\": [{ \"title\": \"Continue\", \"goto\": \"step-3\" }]\n },\n {\n \"id\": \"step-3\",\n \"preset\": \"confirm-dialog\",\n \"data\": { ... }\n }\n ]\n}\n\n\
[SHAPE 3] Custom composition (when no preset fits):\n\
{\"preset\": \"compose\", \"theme\": \"default\", \"sections\": [{\"primitive\": \"name\", ...data...}]}\n\n\
CRITICAL RULES (violating these causes errors):\n\
- Respond with ONLY a valid JSON object. NO markdown fences, NO explanation text.\n\
- For multi-card flows, EVERY card object MUST contain:\n\
* \"id\" (short kebab-case string, e.g. \"welcome\", \"book-trip\")\n\
* \"preset\" (name of the preset from the list above)\n\
* \"data\" (object matching the preset's schema)\n\
- Every card except the LAST one MUST have \"actions\" at CARD level (not inside data):\n\
\"actions\": [{ \"title\": \"...\", \"goto\": \"next-card-id\" }]\n\
- \"actions\" is ALWAYS at card level, NEVER inside \"data\".\n\
- Card ids must be UNIQUE within a flow.\n\
- \"goto\" values in actions must match another card's \"id\" in the same flow.\n\
- For menu-card preset: items[].id should match other card ids so navigation works.\n\
- Use realistic, contextual data (not placeholder text like 'foo' or 'lorem ipsum').\n\
- Match multilingual requests (Indonesian, English) via preset tags.\n\
- Omit optional fields entirely rather than leaving them empty.\n",
);
prompt
}