Skip to main content

adk_ui/tools/
render_chart.rs

1use crate::compat::{Result, Tool, ToolContext};
2use crate::schema::*;
3use crate::tools::{LegacyProtocolOptions, render_ui_response_with_protocol};
4use async_trait::async_trait;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::collections::HashMap;
9use std::sync::Arc;
10
11/// Parameters for the render_chart tool
12#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
13pub struct RenderChartParams {
14    /// Chart title
15    #[serde(default)]
16    pub title: Option<String>,
17    /// Chart type: bar, line, area, pie, scatter, or sparkline
18    #[serde(rename = "type", default = "default_chart_type")]
19    pub chart_type: String,
20    /// Data points - array of objects with x_key and y_key values
21    #[serde(default)]
22    pub data: Vec<HashMap<String, Value>>,
23    #[serde(default)]
24    pub data_source: Option<DataSource>,
25    /// Key for x-axis values
26    pub x_key: String,
27    /// Keys for y-axis values (can be multiple for multi-series)
28    pub y_keys: Vec<String>,
29    #[serde(default)]
30    pub x_type: ChartXType,
31    #[serde(default)]
32    pub time_format: Option<String>,
33    #[serde(default)]
34    pub window: Option<u32>,
35    #[serde(default)]
36    pub x_label: Option<String>,
37    #[serde(default)]
38    pub y_label: Option<String>,
39    #[serde(default = "default_show_legend")]
40    pub show_legend: bool,
41    #[serde(default)]
42    pub colors: Option<Vec<String>>,
43    /// Optional protocol output configuration.
44    #[serde(flatten)]
45    pub protocol: LegacyProtocolOptions,
46}
47
48fn default_chart_type() -> String {
49    "bar".to_string()
50}
51
52fn default_show_legend() -> bool {
53    true
54}
55
56/// Tool for rendering charts and data visualizations.
57///
58/// Creates interactive charts to display data trends, comparisons, and distributions.
59/// Supports multiple chart types and customizable axis labels, legends, and colors.
60///
61/// # Chart Types
62///
63/// - `bar`: Vertical bar chart (default)
64/// - `line`: Line chart for trends
65/// - `area`: Filled area chart
66/// - `pie`: Pie chart for distributions
67///
68/// # Example JSON Parameters
69///
70/// ```json
71/// {
72///   "title": "Monthly Sales",
73///   "type": "line",
74///   "data": [
75///     { "month": "Jan", "sales": 100 },
76///     { "month": "Feb", "sales": 150 },
77///     { "month": "Mar", "sales": 120 }
78///   ],
79///   "x_key": "month",
80///   "y_keys": ["sales"]
81/// }
82/// ```
83pub struct RenderChartTool;
84
85impl RenderChartTool {
86    pub fn new() -> Self {
87        Self
88    }
89}
90
91impl Default for RenderChartTool {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97#[async_trait]
98impl Tool for RenderChartTool {
99    fn name(&self) -> &str {
100        "render_chart"
101    }
102
103    fn description(&self) -> &str {
104        "Render a chart to visualize data. Supports bar, line, area, and pie charts. Use this for showing trends, comparisons, or distributions."
105    }
106
107    fn parameters_schema(&self) -> Option<Value> {
108        Some(super::generate_gemini_schema::<RenderChartParams>())
109    }
110
111    async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
112        let params: RenderChartParams = serde_json::from_value(args)
113            .map_err(|e| crate::compat::AdkError::tool(format!("Invalid parameters: {}", e)))?;
114        if params.window == Some(0) {
115            return Err(crate::compat::AdkError::tool(
116                "Invalid parameters: window must be greater than zero".to_string(),
117            ));
118        }
119        if let Some(source) = &params.data_source
120            && !source.binding_path.starts_with('/')
121        {
122            return Err(crate::compat::AdkError::tool(
123                "Invalid parameters: data_source.binding_path must be an absolute JSON Pointer"
124                    .to_string(),
125            ));
126        }
127        let protocol_options = params.protocol.clone();
128
129        let kind = match params.chart_type.as_str() {
130            "line" => ChartKind::Line,
131            "area" => ChartKind::Area,
132            "pie" => ChartKind::Pie,
133            "scatter" => ChartKind::Scatter,
134            "sparkline" => ChartKind::Sparkline,
135            _ => ChartKind::Bar,
136        };
137
138        let ui = UiResponse::new(vec![Component::Chart(Chart {
139            id: None,
140            title: params.title,
141            kind,
142            data: params.data,
143            data_source: params.data_source,
144            x_key: params.x_key,
145            y_keys: params.y_keys,
146            x_type: params.x_type,
147            time_format: params.time_format,
148            window: params.window,
149            x_label: params.x_label,
150            y_label: params.y_label,
151            show_legend: params.show_legend,
152            colors: params.colors,
153        })]);
154
155        let surface_id = protocol_options.resolved_surface_id("chart");
156        let output = render_ui_response_with_protocol(ui, &protocol_options, "chart")?;
157        let surface_ref = crate::surface_runtime::next_surface_ref(&ctx, surface_id);
158        crate::surface_runtime::record_surface_ref(&ctx, &surface_ref);
159        Ok(output)
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use serde_json::json;
167
168    #[test]
169    fn parameters_expose_time_series_behavior() {
170        let params: RenderChartParams = serde_json::from_value(json!({
171            "type": "sparkline",
172            "x_key": "at",
173            "y_keys": ["latency"],
174            "x_type": "time",
175            "time_format": "time",
176            "window": 60,
177            "x_label": "Observed",
178            "y_label": "Milliseconds",
179            "show_legend": false,
180            "colors": ["#123456"]
181        }))
182        .unwrap();
183        assert!(matches!(params.x_type, ChartXType::Time));
184        assert_eq!(params.window, Some(60));
185        assert!(!params.show_legend);
186    }
187}