greentic-flow-builder 0.2.0

Greentic Flow Builder — orchestrator that powers Adaptive Card design via the adaptive-card-mcp toolkit
Documentation
//! Async dispatcher mapping tool names to their implementations.

use crate::knowledge::Knowledge;
use adaptive_card_core::{CardVersion, Host, Presentation, check_accessibility, validate_card};
use serde_json::{Value, json};
use std::sync::Arc;

/// Invoke a tool by name with its raw JSON-encoded arguments, returning a
/// JSON-encoded result string (either the tool's normal output or an
/// `{"error":"..."}` payload).
pub async fn dispatch(kb: &Arc<Knowledge>, name: &str, args: &str) -> String {
    match dispatch_inner(kb, name, args).await {
        Ok(value) => serde_json::to_string(&value)
            .unwrap_or_else(|e| format!(r#"{{"error":"serialize: {e}"}}"#)),
        Err(e) => json!({ "error": e.to_string() }).to_string(),
    }
}

async fn dispatch_inner(kb: &Arc<Knowledge>, name: &str, args: &str) -> anyhow::Result<Value> {
    let parsed: Value = serde_json::from_str(args)
        .map_err(|e| anyhow::anyhow!("invalid tool arguments JSON: {e}"))?;

    match name {
        "validate_card" => {
            let card = parsed
                .get("card")
                .ok_or_else(|| anyhow::anyhow!("missing required 'card'"))?;
            let host = parsed
                .get("host")
                .and_then(Value::as_str)
                .and_then(Host::from_str);
            Ok(serde_json::to_value(validate_card(card, host))?)
        }
        "analyze_card" => {
            let card = parsed
                .get("card")
                .ok_or_else(|| anyhow::anyhow!("missing required 'card'"))?;
            Ok(serde_json::to_value(adaptive_card_core::analyze_card(
                card,
            ))?)
        }
        "check_accessibility" => {
            let card = parsed
                .get("card")
                .ok_or_else(|| anyhow::anyhow!("missing required 'card'"))?;
            Ok(serde_json::to_value(check_accessibility(card))?)
        }
        "suggest_layout" => {
            let query = parsed
                .get("query")
                .and_then(Value::as_str)
                .ok_or_else(|| anyhow::anyhow!("missing required 'query'"))?;
            let limit = parsed
                .get("limit")
                .and_then(Value::as_u64)
                .map(|n| n as usize)
                .unwrap_or(5);
            Ok(serde_json::to_value(kb.suggest(query, limit))?)
        }
        "optimize_card" => {
            let card = parsed
                .get("card")
                .cloned()
                .ok_or_else(|| anyhow::anyhow!("missing required 'card'"))?;
            let opts = adaptive_card_core::OptimizeOpts {
                accessibility: parsed
                    .get("accessibility")
                    .and_then(Value::as_bool)
                    .unwrap_or(false),
                performance: parsed
                    .get("performance")
                    .and_then(Value::as_bool)
                    .unwrap_or(false),
                modernize: parsed
                    .get("modernize")
                    .and_then(Value::as_bool)
                    .unwrap_or(false),
                target_host: parsed
                    .get("target_host")
                    .and_then(Value::as_str)
                    .and_then(Host::from_str),
            };
            Ok(serde_json::to_value(adaptive_card_core::optimize_card(
                card, &opts,
            ))?)
        }
        "transform_card" => {
            let card = parsed
                .get("card")
                .cloned()
                .ok_or_else(|| anyhow::anyhow!("missing required 'card'"))?;
            let target = adaptive_card_core::TransformTarget {
                version: parsed
                    .get("target_version")
                    .and_then(Value::as_str)
                    .and_then(CardVersion::parse),
                host: parsed
                    .get("target_host")
                    .and_then(Value::as_str)
                    .and_then(Host::from_str),
                strict: parsed
                    .get("strict")
                    .and_then(Value::as_bool)
                    .unwrap_or(false),
            };
            Ok(serde_json::to_value(adaptive_card_core::transform_card(
                card, &target,
            )?)?)
        }
        "template_card" => {
            let card = parsed
                .get("card")
                .cloned()
                .ok_or_else(|| anyhow::anyhow!("missing required 'card'"))?;
            Ok(serde_json::to_value(adaptive_card_core::template_card(
                card,
            ))?)
        }
        "data_to_card" => {
            let data = parsed
                .get("data")
                .ok_or_else(|| anyhow::anyhow!("missing required 'data'"))?;
            let host = parsed
                .get("host")
                .and_then(Value::as_str)
                .and_then(Host::from_str)
                .unwrap_or(Host::Generic);
            let presentation = parsed
                .get("presentation")
                .and_then(Value::as_str)
                .and_then(|s| match s {
                    "table" => Some(Presentation::Table),
                    "factset" => Some(Presentation::FactSet),
                    "list" => Some(Presentation::List),
                    "chart" => Some(Presentation::Chart),
                    "auto" => Some(Presentation::Auto),
                    _ => None,
                });
            let title = parsed
                .get("title")
                .and_then(Value::as_str)
                .map(String::from);
            let opts = adaptive_card_core::DataToCardOpts {
                title,
                presentation,
                host,
            };
            Ok(adaptive_card_core::data_to_card(data, &opts)?)
        }
        "list_examples" => {
            let category = parsed.get("category").and_then(Value::as_str);
            let limit = parsed
                .get("limit")
                .and_then(Value::as_u64)
                .map(|n| n as usize)
                .unwrap_or(20);
            let entries: Vec<Value> = kb
                .all()
                .iter()
                .filter(|e| category.is_none_or(|c| e.category == c))
                .take(limit)
                .map(|e| {
                    json!({
                        "id": e.id,
                        "title": e.title,
                        "category": e.category,
                        "tags": e.tags,
                        "complexity": format!("{:?}", e.complexity).to_lowercase(),
                    })
                })
                .collect();
            Ok(json!({ "examples": entries }))
        }
        "get_example" => {
            let id = parsed
                .get("id")
                .and_then(Value::as_str)
                .ok_or_else(|| anyhow::anyhow!("missing required 'id'"))?;
            let entry = kb
                .by_id(id)
                .ok_or_else(|| anyhow::anyhow!("knowledge entry not found: {id}"))?;
            Ok(serde_json::to_value(entry)?)
        }
        "pack_card" => {
            // pack_card is triggered via the UI Pack button, not via LLM tool calls.
            // The prompt marks this tool as FORBIDDEN for LLM use.
            Ok(json!({
                "error": "pack_card is not available as an LLM tool. Use the Pack button in the UI instead."
            }))
        }
        "deploy_pack" => {
            // deploy_pack is triggered via the UI, not via LLM tool calls.
            Ok(json!({
                "error": "deploy_pack is not available as an LLM tool. Use the Pack button with 'Build .gtbundle' option in the UI instead."
            }))
        }
        other => anyhow::bail!("unknown tool: {other}"),
    }
}