Skip to main content

adk_ui/tools/
render_table.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_table tool
12#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
13pub struct RenderTableParams {
14    /// Table title
15    #[serde(default)]
16    pub title: Option<String>,
17    /// Column definitions
18    pub columns: Vec<ColumnDef>,
19    /// Row data - array of objects with keys matching accessor_key
20    #[serde(default)]
21    pub data: Vec<HashMap<String, Value>>,
22    /// Optional data-model binding. Literal `data` remains the fallback.
23    #[serde(default)]
24    pub data_source: Option<DataSource>,
25    #[serde(default)]
26    pub sortable: bool,
27    #[serde(default)]
28    pub page_size: Option<u32>,
29    #[serde(default)]
30    pub striped: bool,
31    /// Optional protocol output configuration.
32    #[serde(flatten)]
33    pub protocol: LegacyProtocolOptions,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
37pub struct ColumnDef {
38    /// Column header text
39    pub header: String,
40    /// Key to access data from row objects
41    pub accessor_key: String,
42    #[serde(default = "default_column_sortable")]
43    pub sortable: bool,
44}
45
46fn default_column_sortable() -> bool {
47    true
48}
49
50/// Tool for rendering data tables.
51///
52/// Creates tabular displays for structured data with customizable columns.
53/// Supports optional title, sorting, pagination, and striped styling.
54///
55/// # Example JSON Parameters
56///
57/// ```json
58/// {
59///   "title": "User List",
60///   "columns": [
61///     { "header": "Name", "accessor_key": "name" },
62///     { "header": "Email", "accessor_key": "email" },
63///     { "header": "Role", "accessor_key": "role" }
64///   ],
65///   "data": [
66///     { "name": "Alice", "email": "alice@example.com", "role": "Admin" },
67///     { "name": "Bob", "email": "bob@example.com", "role": "User" }
68///   ]
69/// }
70/// ```
71pub struct RenderTableTool;
72
73impl RenderTableTool {
74    pub fn new() -> Self {
75        Self
76    }
77}
78
79impl Default for RenderTableTool {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85#[async_trait]
86impl Tool for RenderTableTool {
87    fn name(&self) -> &str {
88        "render_table"
89    }
90
91    fn description(&self) -> &str {
92        r#"Render a data table. Output example:
93┌───────┬─────────────────────┬───────┐
94│ Name  │ Email               │ Role  │
95├───────┼─────────────────────┼───────┤
96│ Alice │ alice@example.com   │ Admin │
97│ Bob   │ bob@example.com     │ User  │
98└───────┴─────────────────────┴───────┘
99Set sortable=true for clickable column headers. Set page_size for pagination. Set striped=true for alternating row colors."#
100    }
101
102    fn parameters_schema(&self) -> Option<Value> {
103        Some(super::generate_gemini_schema::<RenderTableParams>())
104    }
105
106    async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
107        let params: RenderTableParams = serde_json::from_value(args)
108            .map_err(|e| crate::compat::AdkError::tool(format!("Invalid parameters: {}", e)))?;
109        if params.page_size == Some(0) {
110            return Err(crate::compat::AdkError::tool(
111                "Invalid parameters: page_size must be greater than zero".to_string(),
112            ));
113        }
114        if let Some(source) = &params.data_source
115            && !source.binding_path.starts_with('/')
116        {
117            return Err(crate::compat::AdkError::tool(
118                "Invalid parameters: data_source.binding_path must be an absolute JSON Pointer"
119                    .to_string(),
120            ));
121        }
122        let protocol_options = params.protocol.clone();
123
124        let columns: Vec<TableColumn> = params
125            .columns
126            .into_iter()
127            .map(|c| TableColumn {
128                header: c.header,
129                accessor_key: c.accessor_key,
130                sortable: c.sortable,
131            })
132            .collect();
133
134        let mut components = Vec::new();
135
136        // Add title if provided
137        if let Some(title) = params.title {
138            components.push(Component::Text(Text {
139                id: None,
140                content: title,
141                variant: TextVariant::H3,
142            }));
143        }
144
145        components.push(Component::Table(Table {
146            id: None,
147            columns,
148            data: params.data,
149            data_source: params.data_source,
150            sortable: params.sortable,
151            page_size: params.page_size,
152            striped: params.striped,
153        }));
154
155        let ui = UiResponse::new(components);
156        let surface_id = protocol_options.resolved_surface_id("table");
157        let output = render_ui_response_with_protocol(ui, &protocol_options, "table")?;
158        let surface_ref = crate::surface_runtime::next_surface_ref(&ctx, surface_id);
159        crate::surface_runtime::record_surface_ref(&ctx, &surface_ref);
160        Ok(output)
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use serde_json::json;
168
169    #[test]
170    fn parameters_expose_table_behavior() {
171        let params: RenderTableParams = serde_json::from_value(json!({
172            "columns": [{ "header": "Name", "accessor_key": "name", "sortable": false }],
173            "data": [],
174            "sortable": true,
175            "page_size": 25,
176            "striped": true
177        }))
178        .unwrap();
179        assert!(params.sortable);
180        assert_eq!(params.page_size, Some(25));
181        assert!(params.striped);
182        assert!(!params.columns[0].sortable);
183    }
184}