Skip to main content

adk_ui/tools/
render_kit.rs

1use crate::compat::{Result, Tool, ToolContext};
2use crate::kit::{KitArtifacts, KitGenerator, KitSpec};
3use crate::tools::LegacyProtocolOptions;
4use async_trait::async_trait;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use serde_json::{Value, json};
8use std::sync::Arc;
9
10/// Parameters for the render_kit tool.
11#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
12pub struct RenderKitParams {
13    #[serde(flatten)]
14    pub spec: KitSpec,
15    /// Optional output format; "json" (default) or "catalog_only"
16    #[serde(default)]
17    pub output: Option<String>,
18    /// Optional protocol output configuration.
19    #[serde(flatten)]
20    pub protocol: LegacyProtocolOptions,
21}
22
23/// Tool for generating a reviewable UI kit draft.
24pub struct RenderKitTool;
25
26impl RenderKitTool {
27    pub fn new() -> Self {
28        Self
29    }
30}
31
32impl Default for RenderKitTool {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38#[async_trait]
39impl Tool for RenderKitTool {
40    fn name(&self) -> &str {
41        "render_kit"
42    }
43
44    fn description(&self) -> &str {
45        "Generate a versioned brand-kit draft from a KitSpec. Returns a manifest, supported component catalog, semantic tokens, templates, agent constraints, and scoped theme CSS. Drafts must be reviewed before runtime approval."
46    }
47
48    fn parameters_schema(&self) -> Option<Value> {
49        Some(super::generate_gemini_schema::<RenderKitParams>())
50    }
51
52    async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
53        let params: RenderKitParams = serde_json::from_value(args.clone()).map_err(|e| {
54            crate::compat::AdkError::tool(format!("Invalid parameters: {}. Got: {}", e, args))
55        })?;
56
57        let generator = KitGenerator::new();
58        let artifacts = generator.generate(&params.spec);
59        if let Err(errors) = artifacts.manifest.validate() {
60            let details = errors
61                .iter()
62                .map(ToString::to_string)
63                .collect::<Vec<_>>()
64                .join("; ");
65            return Err(crate::compat::AdkError::tool(format!(
66                "Generated kit failed validation: {details}"
67            )));
68        }
69        let payload = format_output(&artifacts, params.output.as_deref());
70
71        let surface_id = params.protocol.resolved_surface_id("kit");
72        let output = match params.protocol.protocol {
73            Some(protocol) => {
74                let protocol = serde_json::to_value(protocol).unwrap_or_else(|_| json!("a2ui"));
75                json!({
76                    "protocol": protocol,
77                    "surface_id": surface_id.clone(),
78                    "payload": payload
79                })
80            }
81            None => payload,
82        };
83        let surface_ref = crate::surface_runtime::next_surface_ref(&ctx, surface_id);
84        crate::surface_runtime::record_surface_ref(&ctx, &surface_ref);
85        Ok(output)
86    }
87}
88
89fn format_output(artifacts: &KitArtifacts, output: Option<&str>) -> Value {
90    match output {
91        Some("catalog_only") => artifacts.catalog.clone(),
92        _ => json!({
93            "manifest": artifacts.manifest,
94            "catalog": artifacts.catalog,
95            "tokens": artifacts.tokens,
96            "templates": artifacts.templates,
97            "agent_context": artifacts.agent_context,
98            "theme_css": artifacts.theme_css,
99        }),
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use crate::compat::{Content, EventActions, ReadonlyContext};
107    use async_trait::async_trait;
108    use std::sync::{Arc, Mutex};
109
110    struct TestContext {
111        content: Content,
112        actions: Mutex<EventActions>,
113    }
114
115    impl TestContext {
116        fn new() -> Self {
117            Self {
118                content: Content::new("user"),
119                actions: Mutex::new(EventActions::default()),
120            }
121        }
122    }
123
124    #[async_trait]
125    impl ReadonlyContext for TestContext {
126        fn invocation_id(&self) -> &str {
127            "test"
128        }
129        fn agent_name(&self) -> &str {
130            "test"
131        }
132        fn user_id(&self) -> &str {
133            "user"
134        }
135        fn app_name(&self) -> &str {
136            "app"
137        }
138        fn session_id(&self) -> &str {
139            "session"
140        }
141        fn branch(&self) -> &str {
142            ""
143        }
144        fn user_content(&self) -> &Content {
145            &self.content
146        }
147    }
148
149    #[async_trait]
150    impl crate::compat::CallbackContext for TestContext {
151        fn artifacts(&self) -> Option<Arc<dyn crate::compat::Artifacts>> {
152            None
153        }
154    }
155
156    #[async_trait]
157    impl ToolContext for TestContext {
158        fn function_call_id(&self) -> &str {
159            "call-123"
160        }
161        fn actions(&self) -> EventActions {
162            self.actions.lock().unwrap().clone()
163        }
164        fn set_actions(&self, actions: EventActions) {
165            *self.actions.lock().unwrap() = actions;
166        }
167        async fn search_memory(&self, _query: &str) -> Result<Vec<crate::compat::MemoryEntry>> {
168            Ok(vec![])
169        }
170    }
171
172    #[tokio::test]
173    async fn render_kit_emits_catalog() {
174        let tool = RenderKitTool::new();
175        let args = serde_json::json!({
176            "name": "Fintech Pro",
177            "version": "0.1.0",
178            "brand": { "vibe": "trustworthy", "industry": "fintech" },
179            "colors": { "primary": "#2F6BFF" },
180            "typography": { "family": "Source Sans 3" },
181            "assets": [{
182                "id": "primary-logo",
183                "kind": "logo",
184                "uri": "/assets/fintech-pro.svg",
185                "mimeType": "image/svg+xml",
186                "alt": "Fintech Pro"
187            }],
188            "provenance": {
189                "source": "imported",
190                "sourceUri": "https://brand.example.com/design-system"
191            },
192            "templates": ["auth_login"]
193        });
194
195        let ctx: Arc<dyn ToolContext> = Arc::new(TestContext::new());
196        let value = tool.execute(ctx, args).await.unwrap();
197        assert_eq!(value["manifest"]["status"], "draft");
198        assert!(value.get("catalog").is_some());
199        assert!(value.get("tokens").is_some());
200        assert!(value["catalog"]["components"]["Card"].is_object());
201        assert_eq!(value["manifest"]["assets"][0]["id"], "primary-logo");
202    }
203
204    #[tokio::test]
205    async fn render_kit_emits_protocol_envelope() {
206        let tool = RenderKitTool::new();
207        let args = serde_json::json!({
208            "name": "Fintech Pro",
209            "version": "0.1.0",
210            "brand": { "vibe": "trustworthy", "industry": "fintech" },
211            "colors": { "primary": "#2F6BFF" },
212            "typography": { "family": "Source Sans 3" },
213            "protocol": "mcp_apps"
214        });
215
216        let ctx: Arc<dyn ToolContext> = Arc::new(TestContext::new());
217        let value = tool.execute(ctx, args).await.unwrap();
218        assert_eq!(value["protocol"], "mcp_apps");
219        assert!(value["payload"]["catalog"].is_object());
220    }
221}