1use crate::schema::*;
2use adk_core::{Result, Tool, ToolContext};
3use async_trait::async_trait;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::collections::HashMap;
8use std::sync::Arc;
9
10#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
22pub struct DashboardSection {
23 pub title: String,
25 #[serde(rename = "type")]
27 pub section_type: String,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub stats: Option<Vec<StatItem>>,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub text: Option<String>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub message: Option<String>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub severity: Option<String>,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub columns: Option<Vec<ColumnSpec>>,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub rows: Option<Vec<HashMap<String, Value>>>,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub chart_type: Option<String>,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub data: Option<Vec<HashMap<String, Value>>>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub x_key: Option<String>,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub y_keys: Option<Vec<String>>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub pairs: Option<Vec<KeyValueItem>>,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub items: Option<Vec<String>>,
64 #[serde(default)]
66 pub ordered: bool,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub code: Option<String>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub language: Option<String>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
76pub struct StatItem {
77 pub label: String,
79 pub value: String,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub status: Option<String>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
87pub struct ColumnSpec {
88 pub header: String,
90 pub key: String,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
96pub struct KeyValueItem {
97 pub key: String,
99 pub value: String,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
105pub struct RenderLayoutParams {
106 pub title: String,
108 #[serde(default)]
110 pub description: Option<String>,
111 pub sections: Vec<DashboardSection>,
113 #[serde(default)]
115 pub theme: Option<String>,
116}
117
118pub struct RenderLayoutTool;
161
162impl RenderLayoutTool {
163 pub fn new() -> Self {
164 Self
165 }
166}
167
168impl Default for RenderLayoutTool {
169 fn default() -> Self {
170 Self::new()
171 }
172}
173
174#[async_trait]
175impl Tool for RenderLayoutTool {
176 fn name(&self) -> &str {
177 "render_layout"
178 }
179
180 fn description(&self) -> &str {
181 r#"Render a dashboard layout with multiple sections. Output example:
182┌─────────────────────────────────────────────┐
183│ System Status │
184├─────────────────────────────────────────────┤
185│ CPU: 45% ✓ │ Memory: 78% ⚠ │ Disk: 92% ✗ │
186├─────────────────────────────────────────────┤
187│ [Chart: Usage over time] │
188├─────────────────────────────────────────────┤
189│ Region: us-east-1 │ Version: 1.2.3 │
190└─────────────────────────────────────────────┘
191Section types: stats (label/value/status), table, chart, alert, text, key_value, list, code_block."#
192 }
193
194 fn parameters_schema(&self) -> Option<Value> {
195 Some(super::generate_gemini_schema::<RenderLayoutParams>())
196 }
197
198 async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
199 let params: RenderLayoutParams = serde_json::from_value(args.clone()).map_err(|e| {
200 adk_core::AdkError::Tool(format!("Invalid parameters: {}. Got: {}", e, args))
201 })?;
202
203 let mut components = Vec::new();
204
205 components.push(Component::Text(Text {
207 id: None,
208 content: params.title,
209 variant: TextVariant::H2,
210 }));
211
212 if let Some(desc) = params.description {
214 components.push(Component::Text(Text {
215 id: None,
216 content: desc,
217 variant: TextVariant::Caption,
218 }));
219 }
220
221 for section in params.sections {
223 let section_component = build_section_component(section);
224 components.push(section_component);
225 }
226
227 let mut ui = UiResponse::new(components);
228
229 if let Some(theme_str) = params.theme {
231 let theme = match theme_str.to_lowercase().as_str() {
232 "dark" => Theme::Dark,
233 "system" => Theme::System,
234 _ => Theme::Light,
235 };
236 ui = ui.with_theme(theme);
237 }
238
239 serde_json::to_value(ui)
240 .map_err(|e| adk_core::AdkError::Tool(format!("Failed to serialize UI: {}", e)))
241 }
242}
243
244fn build_section_component(section: DashboardSection) -> Component {
245 let mut card_content: Vec<Component> = Vec::new();
246
247 match section.section_type.as_str() {
248 "stats" => {
249 if let Some(stats) = section.stats {
250 for stat in stats {
252 let status_indicator = match stat.status.as_deref() {
253 Some("operational") | Some("ok") | Some("success") => "🟢 ",
254 Some("degraded") | Some("warning") => "🟡 ",
255 Some("down") | Some("error") | Some("outage") => "🔴 ",
256 _ => "",
257 };
258 card_content.push(Component::Text(Text {
259 id: None,
260 content: format!("{}{}: {}", status_indicator, stat.label, stat.value),
261 variant: TextVariant::Body,
262 }));
263 }
264 }
265 }
266 "text" => {
267 if let Some(text) = section.text {
268 card_content.push(Component::Text(Text {
269 id: None,
270 content: text,
271 variant: TextVariant::Body,
272 }));
273 }
274 }
275 "alert" => {
276 let variant = match section.severity.as_deref() {
277 Some("success") => AlertVariant::Success,
278 Some("warning") => AlertVariant::Warning,
279 Some("error") => AlertVariant::Error,
280 _ => AlertVariant::Info,
281 };
282 return Component::Alert(Alert {
283 id: None,
284 title: section.title,
285 description: section.message,
286 variant,
287 });
288 }
289 "table" => {
290 if let (Some(cols), Some(rows)) = (section.columns, section.rows) {
291 let table_columns: Vec<TableColumn> = cols
292 .into_iter()
293 .map(|c| TableColumn { header: c.header, accessor_key: c.key, sortable: true })
294 .collect();
295 card_content.push(Component::Table(Table {
296 id: None,
297 columns: table_columns,
298 data: rows,
299 sortable: false,
300 page_size: None,
301 striped: false,
302 }));
303 }
304 }
305 "chart" => {
306 if let (Some(data), Some(x), Some(y)) = (section.data, section.x_key, section.y_keys) {
307 let kind = match section.chart_type.as_deref() {
308 Some("line") => ChartKind::Line,
309 Some("area") => ChartKind::Area,
310 Some("pie") => ChartKind::Pie,
311 _ => ChartKind::Bar,
312 };
313 card_content.push(Component::Chart(Chart {
314 id: None,
315 title: None,
316 kind,
317 data,
318 x_key: x,
319 y_keys: y,
320 x_label: None,
321 y_label: None,
322 show_legend: true,
323 colors: None,
324 }));
325 }
326 }
327 "key_value" => {
328 if let Some(pairs) = section.pairs {
329 let kv_pairs: Vec<KeyValuePair> = pairs
330 .into_iter()
331 .map(|p| KeyValuePair { key: p.key, value: p.value })
332 .collect();
333 card_content.push(Component::KeyValue(KeyValue { id: None, pairs: kv_pairs }));
334 }
335 }
336 "list" => {
337 if let Some(items) = section.items {
338 card_content.push(Component::List(List {
339 id: None,
340 items,
341 ordered: section.ordered,
342 }));
343 }
344 }
345 "code_block" => {
346 if let Some(code) = section.code {
347 card_content.push(Component::CodeBlock(CodeBlock {
348 id: None,
349 code,
350 language: section.language,
351 }));
352 }
353 }
354 _ => {
355 card_content.push(Component::Text(Text {
357 id: None,
358 content: format!("Unknown section type: {}", section.section_type),
359 variant: TextVariant::Caption,
360 }));
361 }
362 }
363
364 if card_content.is_empty() {
366 card_content.push(Component::Text(Text {
367 id: None,
368 content: "(No content)".to_string(),
369 variant: TextVariant::Caption,
370 }));
371 }
372
373 Component::Card(Card {
374 id: None,
375 title: Some(section.title),
376 description: None,
377 content: card_content,
378 footer: None,
379 })
380}