greentic-flow-builder 0.1.0

AI-powered Adaptive Card flow builder with visual graph editor and demo runner
Documentation
//! 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 &registry.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(&registry.themes.names().join(", "));

    prompt.push_str("\n\nPRIMITIVES (for compose mode):\n");
    for primitive in &registry.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
}