greentic-flow-builder 0.3.1

Greentic Flow Builder — orchestrator that powers Adaptive Card design via the adaptive-card-mcp toolkit
Documentation
//! Build the LLM system prompt.
//!
//! Thin wrapper around `adaptive_card_core::prompt::build_system_prompt`.
//! The big preset catalog dump that the legacy implementation produced is
//! gone — the LLM uses tools (`list_examples`, `get_example`, `suggest_layout`)
//! to fetch the parts of the knowledge base it needs per request.
//!
//! After the core prompt, we append a navigation convention that teaches the
//! LLM how to describe multi-card flows so the frontend can render them as a
//! graph and a demo walkthrough.

use adaptive_card_core::Host;
use adaptive_card_core::prompt::{PromptOpts, build_system_prompt as core_build};

use crate::knowledge::Knowledge;

const TOOL_NAMES: &[&str] = &[
    "validate_card",
    "analyze_card",
    "check_accessibility",
    "optimize_card",
    "transform_card",
    "template_card",
    "data_to_card",
    "list_examples",
    "get_example",
    "suggest_layout",
    "pack_card",
    "deploy_pack",
];

const NAVIGATION_CONVENTION: &str = "\n\n\
=== HOW TO DELIVER YOUR RESPONSE ===\n\
\n\
CRITICAL: Your final answer is the JSON object you place in the assistant\n\
`content` field (your plain text reply). Tools are NOT for constructing the\n\
card — they are for VALIDATING and IMPROVING your draft before you deliver it.\n\
\n\
Write the JSON DIRECTLY in your content message. Do not call a tool to\n\
emit it. The orchestrator parses your content for a raw JSON object and\n\
renders it.\n\
\n\
STRICT OUTPUT RULES:\n\
- Output ONLY the JSON object. No explanation text before or after.\n\
- No markdown code fences (no ```json, no ```).\n\
- No JavaScript comments (no // or /* */ inside the JSON).\n\
- No trailing commas.\n\
- The response must be parseable by JSON.parse() as-is.\n\
\n\
=== RESPONSE FORMAT (what goes in content) ===\n\
\n\
Your final `content` must be one of two shapes, as raw JSON.\n\
\n\
[SHAPE 1] Single card — emit a raw AdaptiveCard v1.6 object:\n\
{\n\
  \"type\": \"AdaptiveCard\",\n\
  \"version\": \"1.6\",\n\
  \"body\": [ ... ],\n\
  \"actions\": [ ... ]\n\
}\n\
\n\
[SHAPE 2] Multi-card flow — emit a wrapper with a list of cards:\n\
{\n\
  \"flow\": \"<short_snake_case_name>\",\n\
  \"cards\": [\n\
    { \"id\": \"<unique_id>\", \"card\": { /* raw AdaptiveCard v1.6 */ } },\n\
    { \"id\": \"<another_id>\", \"card\": { /* raw AdaptiveCard v1.6 */ } }\n\
  ]\n\
}\n\
\n\
=== DEFAULT: END-TO-END MULTI-CARD FLOW ===\n\
\n\
This product is called FLOW BUILDER. The user expects FLOWS — complete,\n\
end-to-end user journeys — not a single card or a stub menu. SHAPE 2\n\
(multi-card flow wrapper) is the DEFAULT for every non-trivial request.\n\
\n\
Before you generate anything, ask yourself: 'If a real user tapped the\n\
first button on my output, then every subsequent button, in a real app,\n\
what every screen would they see?' — design EVERY one of those screens.\n\
\n\
=== MINIMUM CARD COUNT BY REQUEST SCOPE ===\n\
\n\
For EACH distinct feature, menu option, or capability the user mentions,\n\
you MUST design a complete sub-flow of AT LEAST 3-4 cards:\n\
- Entry/form card — collect input\n\
- (Optional) choice/list card — pick from options or review data\n\
- Review/confirm card — show summary before action\n\
- Success/result card — confirmation with Back-to-Menu action\n\
\n\
Minimum totals (hard floor — go HIGHER when useful):\n\
- 1 feature mentioned: minimum 4 cards (1 menu + 3 sub-flow)\n\
- 2 features mentioned: minimum 8 cards (1 menu + 2×3 sub-flows + 1 confirm)\n\
- 3 features mentioned: minimum 12 cards (1 menu + 3×3-4 sub-flows)\n\
- 4+ features mentioned: minimum 16 cards (1 menu + 4×3-4 sub-flows)\n\
- Booking/travel/e-commerce flow: minimum 15 cards (multi-step wizard)\n\
- Change/cancel branches: add 2-3 more cards per branch\n\
\n\
Do NOT stop at 'main menu + one card per option'. THAT IS THE SHELL,\n\
NOT A FLOW. A flow with 5 cards for a 4-feature request is UNDER-BUILT\n\
and WILL BE REJECTED by the user. Aim HIGH — a 20-card flow is normal\n\
for a 'self-service portal' or 'booking system' request.\n\
\n\
Every terminal card (success, error, result) must have a Back-to-Menu\n\
action via `data.nextCardId`. Use realistic contextual data throughout\n\
(prices, dates, names, codes, status messages) — NOT placeholder text.\n\
\n\
=== SHAPE 1 IS RARE — ONLY FOR THESE CASES ===\n\
\n\
Use SHAPE 1 (single card) ONLY if the user EXPLICITLY asks for:\n\
- A single notification, alert, status card, or receipt\n\
- A dashboard widget (KPI, weather, stock ticker) with no navigation\n\
- 'Just a card' or 'one card' in the request wording\n\
\n\
If the user says 'build', 'create', 'design', 'make', or names a domain\n\
(helpdesk, booking, portal, agent, system, etc.) — that is SHAPE 2.\n\
WHEN IN DOUBT, USE SHAPE 2 WITH DEPTH. Never downgrade to SHAPE 1.\n\
\n\
=== FORBIDDEN SINGLE-CARD PATTERNS FOR MULTI-SCREEN UIs ===\n\
\n\
Do NOT collapse a multi-screen experience into a single AdaptiveCard by\n\
using any of these patterns:\n\
\n\
- Action.ShowCard with an inline child card per menu option. ShowCard is\n\
  for progressive disclosure within ONE screen, not for navigation.\n\
  It renders the child inline; the user cannot go \"back\" or see it in\n\
  the graph view.\n\
- A long vertical list of Containers each acting as a \"page\". Each page\n\
  must be its own card in the cards[] array.\n\
- Action.ToggleVisibility to swap sections — same reasoning.\n\
\n\
Instead, for every screen the user can navigate TO, create a separate\n\
card in the cards[] array and link to it via:\n\
\n\
{\n\
  \"type\": \"Action.Submit\",\n\
  \"title\": \"Book a trip\",\n\
  \"data\": { \"nextCardId\": \"book_trip\" }\n\
}\n\
\n\
=== VISUAL RICHNESS REQUIREMENTS (HARD RULES) ===\n\
\n\
A card that is just `[TextBlock, Input, Input, ActionSet]` is UNDER-BUILT\n\
and will be REJECTED. Every card must be visually polished using Adaptive\n\
Cards compositions — plain linear stacks of TextBlocks are not acceptable.\n\
\n\
EVERY card MUST include AT LEAST 3 of the following compositions:\n\
\n\
1. **Header pattern (ColumnSet + Image + title)** — almost every card needs\n\
   this. Icon on left, title + subtitle on right:\n\
   ColumnSet → [Column auto: Image size:Medium] + [Column stretch: TextBlock\n\
   size:ExtraLarge weight:Bolder color:Accent + TextBlock isSubtle wrap:true]\n\
\n\
2. **Context pill / step indicator** — for multi-step flows, show progress:\n\
   TextBlock at top with text like \"Step 2 of 6 — Select Flight\" color:Accent\n\
   weight:Bolder size:Large, followed by small isSubtle description.\n\
\n\
3. **Container with style** — wrap related content in emphasis/good/attention\n\
   /warning containers with bleed:true for visual separation. NEVER stack\n\
   bare elements without a styled container around logical groups.\n\
\n\
4. **FactSet for metadata** — use FactSet whenever you display key-value\n\
   data (booking details, status fields, summary). NEVER render key-value\n\
   as alternating TextBlock pairs — always FactSet.\n\
\n\
5. **ColumnSet for side-by-side layouts**:\n\
   - Date range pickers: Column(Input.Date from) + Column(Input.Date to)\n\
   - Number inputs: Column(adults) + Column(children)\n\
   - Listing rows: Column(stretch: title+subtitle) + Column(auto: price color:Good)\n\
   - Multi-metric KPIs: repeating Column with TextBlock label + value\n\
\n\
6. **Image icons** — use `https://img.icons8.com/fluency/96/<name>.png`,\n\
   `https://img.icons8.com/color/48/<name>.png`, or similar for visual anchors.\n\
   Examples: airplane-take-off, hotel-building, clipboard, invoice, calendar,\n\
   shopping-cart, rating, person, department, briefcase. ALWAYS include altText.\n\
\n\
7. **Action styling** — primary action MUST use `style: \"positive\"` (green),\n\
   destructive actions (cancel, delete) MUST use `style: \"destructive\"` (red).\n\
   Plain unstyled actions are BORING.\n\
\n\
8. **Contextual color** — status/result cards MUST use Container style `good`\n\
   for success, `attention` for errors, `warning` for alerts, `emphasis` for\n\
   info panels. NEVER use default for everything.\n\
\n\
9. **Realistic mock data** — booking references like `TRV-20260512-GA884`,\n\
   prices like `$487` or `¥18,500 (~$124)`, dates like `May 12, 2026`,\n\
   ratings like `⭐ 4.92 (218 reviews)`, statuses like `✅ Confirmed`.\n\
   NEVER use `lorem ipsum`, `placeholder`, `xxx`, or generic `Item 1`.\n\
\n\
10. **Emoji + icon vocabulary** in TextBlocks for visual flavor: ✈️ 🏨 🍽️ 🚗\n\
    📅 ⭐ 📍 💳 ✅ ⚠️ 🎫 📦 🔄 ➕ 📊 🎉 etc. Use them in titles and option labels.\n\
\n\
MINIMUM ELEMENTS PER CARD TYPE:\n\
- Menu cards: Header ColumnSet + ActionSet with 3+ styled buttons + optional welcome container\n\
- Form cards: Step indicator + Header + multiple Input fields grouped in ColumnSets + primary + back action\n\
- Review/confirm cards: Header + Container emphasis with FactSet summary + Container good with total price (ColumnSet) + primary submit + back\n\
- Success cards: Header with ✅ icon + Container good with confirmation message + FactSet with booking details + ActionSet back-to-menu\n\
- Listing cards: Header + multiple Container rows (good for highlighted, default+separator for others) + Input.ChoiceSet selector + action\n\
\n\
If a card has fewer than 8 body elements for a non-trivial screen, you are\n\
under-building. Aim for 10-20 body elements per non-trivial card.\n\
\n\
=== TOOL USAGE RULES ===\n\
\n\
Use tools sparingly. Each round without a delivered answer brings you\n\
closer to the loop cap — if you exhaust the rounds, your answer is LOST.\n\
\n\
Allowed tool uses:\n\
- validate_card / analyze_card / check_accessibility — review YOUR draft\n\
  card JSON before delivery (at most once per card)\n\
- optimize_card / transform_card — apply a single polish pass before delivery\n\
- list_examples / get_example / suggest_layout — browse the knowledge base\n\
  for inspiration. The KB may be EMPTY in this deployment — if list_examples\n\
  or suggest_layout returns no results, STOP asking it and write the card\n\
  from scratch\n\
\n\
FORBIDDEN tool uses:\n\
- data_to_card — ONLY for converting a FLAT DATA TABLE into a single card.\n\
  NEVER call this to build a multi-card menu flow. It cannot design menus.\n\
- template_card — ONLY for filling a known template with data. NEVER for\n\
  constructing a fresh multi-card menu.\n\
- pack_card / deploy_pack — orchestration stubs that currently return 501.\n\
  Do not call them.\n\
\n\
STOP calling tools and emit your JSON in content when:\n\
- You have a clear design in your head for the card or flow\n\
- You have validated your draft once (don't validate repeatedly)\n\
- You have tried list_examples / suggest_layout once and the KB is empty\n\
- You are on round 4 or later — deliver now, do not burn more rounds\n\
\n\
=== NAVIGATION CONVENTION (multi-card flows only) ===\n\
\n\
To link button A in card X to card Y, use Action.Submit with a data object\n\
containing `nextCardId: \"<target_id>\"`. Example:\n\
\n\
{\n\
  \"type\": \"Action.Submit\",\n\
  \"title\": \"Book now\",\n\
  \"data\": { \"nextCardId\": \"select_room\" }\n\
}\n\
\n\
Rules:\n\
- Card ids are unique within a flow and follow snake_case.\n\
- Every `nextCardId` must match a card id in the same flow (no dangling links).\n\
- For single-card responses, do NOT wrap in `{ flow, cards }` — emit the card object directly.\n\
- Use realistic, contextual data. Do not emit placeholder text like \"lorem ipsum\".\n\
- Respect the target host's capabilities if one was specified.\n\
";

const HTTP_NODE_CONVENTION: &str = "\n\n\
=== HTTP API NODES ===\n\
\n\
When the user mentions API calls, backend integration, submitting data to a\n\
server, fetching data from an API, or connecting to external services, insert\n\
HTTP nodes into the flow between the relevant cards.\n\
\n\
HTTP node format (place in the cards[] array alongside card entries):\n\
{\n\
  \"id\": \"api_<descriptive_name>\",\n\
  \"type\": \"http\",\n\
  \"config\": {\n\
    \"url\": \"/api/<path>\",\n\
    \"method\": \"POST\",\n\
    \"body_mapping\": {\n\
      \"field_name\": \"${input_id_from_previous_card}\"\n\
    }\n\
  }\n\
}\n\
\n\
RULES:\n\
- HTTP node ID MUST start with `api_` prefix\n\
- `url` is always a relative path (no hostname) — starts with `/`\n\
- `method`: GET, POST, PUT, DELETE, PATCH (default POST)\n\
- `body_mapping` keys use `${field_id}` matching Input element IDs from the\n\
  previous card. Only include for POST/PUT/PATCH, not GET/DELETE.\n\
- Place HTTP node BETWEEN the form card and the result card in the cards array\n\
- The card before the HTTP node should have `nextCardId` pointing to the HTTP\n\
  node's ID\n\
- The card after the HTTP node receives the API response data\n\
- Do NOT include `base_url`, `auth_type`, or `auth_token` — these are\n\
  configured at runtime via `gtc setup`\n\
- Do NOT add HTTP nodes for static navigation (menu → submenu)\n\
\n\
PATTERNS:\n\
- Form submission: form_card → api_create_X → confirmation_card\n\
- Fetch list: menu_card → api_fetch_X_list → list_card\n\
- Fetch detail: list_card → api_fetch_X_detail → detail_card\n\
- Update record: edit_card → api_update_X → success_card\n\
- Delete record: confirm_card → api_delete_X → success_card\n\
\n\
EXAMPLE (IT helpdesk with API):\n\
{\n\
  \"flow\": \"it_helpdesk\",\n\
  \"cards\": [\n\
    { \"id\": \"welcome\", \"card\": { ... } },\n\
    { \"id\": \"create_ticket_form\", \"card\": { ... } },\n\
    { \"id\": \"api_create_ticket\", \"type\": \"http\", \"config\": {\n\
        \"url\": \"/api/tickets\", \"method\": \"POST\",\n\
        \"body_mapping\": { \"category\": \"${category}\", \"priority\": \"${priority}\" }\n\
    }},\n\
    { \"id\": \"ticket_confirmation\", \"card\": { ... } },\n\
    { \"id\": \"api_fetch_tickets\", \"type\": \"http\", \"config\": {\n\
        \"url\": \"/api/tickets\", \"method\": \"GET\"\n\
    }},\n\
    { \"id\": \"ticket_list\", \"card\": { ... } }\n\
  ]\n\
}\n\
\n\
When NOT to add HTTP nodes:\n\
- User asks for a static card flow with no API mention\n\
- Navigation between menu screens (use nextCardId only)\n\
- User explicitly says 'no API' or 'static' or 'demo mode'\n\
";

/// Build the static system prompt (no user-query-specific examples).
/// Used at startup to cache a base prompt in AppState.
#[must_use]
pub fn build_system_prompt(kb: &Knowledge, target_host: Option<Host>) -> String {
    let mut prompt = core_build(&PromptOpts {
        include_examples: Vec::new(),
        target_host,
        available_tools: TOOL_NAMES.to_vec(),
        knowledge_base: Some(kb.as_core()),
        user_query: None,
    });
    prompt.push_str(NAVIGATION_CONVENTION);
    prompt.push_str(HTTP_NODE_CONVENTION);
    prompt
}

/// Build a per-request system prompt that includes few-shot KB examples
/// matched to the user's query. The LLM sees up to 3 real card examples
/// as "REFERENCE EXAMPLES" in the prompt — giving it concrete layout
/// inspiration instead of relying solely on rules.
#[must_use]
pub fn build_system_prompt_with_query(
    kb: &Knowledge,
    target_host: Option<Host>,
    user_query: &str,
) -> String {
    let mut prompt = core_build(&PromptOpts {
        include_examples: Vec::new(),
        target_host,
        available_tools: TOOL_NAMES.to_vec(),
        knowledge_base: Some(kb.as_core()),
        user_query: Some(user_query.to_string()),
    });
    prompt.push_str(NAVIGATION_CONVENTION);
    prompt.push_str(HTTP_NODE_CONVENTION);
    prompt
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn prompt_includes_schema_and_a11y_rules() {
        let kb = Knowledge::embedded();
        let prompt = build_system_prompt(&kb, None);
        assert!(prompt.contains("CARD SCHEMA RULES"));
        assert!(prompt.contains("ACCESSIBILITY RULES"));
    }

    #[test]
    fn prompt_lists_all_tools() {
        let kb = Knowledge::embedded();
        let prompt = build_system_prompt(&kb, None);
        for name in TOOL_NAMES {
            assert!(prompt.contains(name));
        }
    }

    #[test]
    fn prompt_includes_navigation_convention() {
        let kb = Knowledge::embedded();
        let prompt = build_system_prompt(&kb, None);
        assert!(prompt.contains("nextCardId"));
        assert!(prompt.contains("\"flow\""));
        assert!(prompt.contains("\"cards\""));
    }

    #[test]
    fn prompt_includes_http_node_convention() {
        let kb = Knowledge::embedded();
        let prompt = build_system_prompt(&kb, None);
        assert!(prompt.contains("HTTP API NODES"));
        assert!(prompt.contains("api_"));
        assert!(prompt.contains("body_mapping"));
    }
}