greentic-flow-builder 0.4.0

Greentic Flow Builder — orchestrator that powers Adaptive Card design via the adaptive-card-mcp toolkit
Documentation
//! Tool definitions + dispatcher for OpenAI function calling.
//!
//! Exposes the full 12-tool surface that the chat loop can invoke:
//! 10 core adaptive-card tools (validate, analyze, accessibility, suggest,
//! optimize, transform, template, data_to_card, list_examples, get_example)
//! plus 2 orchestration stubs (pack_card, deploy_pack).
//!
//! The module is split into:
//! - `defs` — static `TOOL_DEFS` with JSON Schema for each tool
//! - `dispatch` — async dispatch function that fans out to the right
//!   `adaptive_card_core` or `orchestrate::*` handler per tool name

mod defs;
mod dispatch;

pub use defs::TOOL_DEFS;
pub use dispatch::dispatch;

#[cfg(test)]
mod tests {
    use super::*;
    use crate::knowledge::Knowledge;
    use serde_json::{Value, json};
    use std::sync::Arc;

    fn empty_kb() -> Arc<Knowledge> {
        Arc::new(Knowledge::embedded())
    }

    #[tokio::test]
    async fn dispatch_validate_card_returns_report() {
        let kb = empty_kb();
        let args = json!({
            "card": {
                "type": "AdaptiveCard", "version": "1.6",
                "speak": "Hi",
                "body": [{"type": "TextBlock", "text": "Hello", "wrap": true}]
            }
        })
        .to_string();
        let result = dispatch(&kb, "validate_card", &args).await;
        let parsed: Value = serde_json::from_str(&result).unwrap();
        assert_eq!(parsed["valid"], true);
    }

    #[tokio::test]
    async fn dispatch_unknown_tool_returns_error() {
        let kb = empty_kb();
        let result = dispatch(&kb, "no_such_tool", "{}").await;
        let parsed: Value = serde_json::from_str(&result).unwrap();
        assert!(parsed["error"].is_string());
    }

    #[test]
    fn tool_defs_has_twelve_tools() {
        assert_eq!(TOOL_DEFS.len(), 12);
    }

    #[test]
    fn all_tool_names_unique() {
        let mut names: Vec<&'static str> = TOOL_DEFS.iter().map(|t| t.function.name).collect();
        names.sort_unstable();
        let original_len = names.len();
        names.dedup();
        assert_eq!(names.len(), original_len);
    }

    #[tokio::test]
    async fn dispatch_optimize_card_adds_speak() {
        let kb = empty_kb();
        let args = json!({
            "card": {
                "type": "AdaptiveCard",
                "version": "1.6",
                "body": [{"type": "Image", "url": "x.png"}]
            },
            "accessibility": true
        })
        .to_string();
        let result = dispatch(&kb, "optimize_card", &args).await;
        let v: Value = serde_json::from_str(&result).unwrap();
        assert!(v["card"]["speak"].is_string());
    }

    #[tokio::test]
    async fn dispatch_transform_card_to_outlook() {
        let kb = empty_kb();
        let args = json!({
            "card": {
                "type": "AdaptiveCard",
                "version": "1.6",
                "body": [],
                "actions": [{"type": "Action.Execute", "title": "OK", "verb": "save"}]
            },
            "target_host": "outlook"
        })
        .to_string();
        let result = dispatch(&kb, "transform_card", &args).await;
        let v: Value = serde_json::from_str(&result).unwrap();
        assert_eq!(v["card"]["version"], "1.4");
        assert_eq!(v["card"]["actions"][0]["type"], "Action.Submit");
    }

    #[tokio::test]
    async fn dispatch_pack_card_returns_ui_message() {
        let kb = empty_kb();
        let args = json!({ "name": "test", "cards": [] }).to_string();
        let result = dispatch(&kb, "pack_card", &args).await;
        let v: Value = serde_json::from_str(&result).unwrap();
        assert!(v["error"].as_str().unwrap().contains("UI"));
    }
}