fix: simplify donut chart schema for OpenAI compatibility (#8303)

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
This commit is contained in:
Matt Van Horn
2026-04-10 14:10:03 -07:00
committed by GitHub
parent 9858f828f1
commit 00a4d056fb
+57 -63
View File
@@ -277,16 +277,6 @@ pub struct SingleDonutChart {
pub labels: Option<Vec<String>>, pub labels: Option<Vec<String>>,
} }
/// Donut chart data — a single chart object or an array of chart objects
#[derive(Debug, Serialize, Deserialize, rmcp::schemars::JsonSchema)]
#[serde(untagged)]
pub enum DonutChartData {
/// Single donut chart
Single(SingleDonutChart),
/// Multiple donut charts
Multiple(Vec<SingleDonutChart>),
}
impl SingleDonutChart { impl SingleDonutChart {
fn validate(&self) -> Result<(), ErrorData> { fn validate(&self) -> Result<(), ErrorData> {
if self.values.is_empty() { if self.values.is_empty() {
@@ -305,28 +295,21 @@ impl SingleDonutChart {
} }
} }
impl DonutChartData { fn validate_donut_charts(charts: &[SingleDonutChart]) -> Result<(), ErrorData> {
fn validate(&self) -> Result<(), ErrorData> { if charts.is_empty() {
match self { return Err(validation_err("charts array must not be empty"));
DonutChartData::Single(chart) => chart.validate(),
DonutChartData::Multiple(charts) => {
if charts.is_empty() {
return Err(validation_err("charts array must not be empty"));
}
for chart in charts {
chart.validate()?;
}
Ok(())
}
}
} }
for chart in charts {
chart.validate()?;
}
Ok(())
} }
/// Parameters for render_donut tool /// Parameters for render_donut tool
#[derive(Debug, Serialize, Deserialize, rmcp::schemars::JsonSchema)] #[derive(Debug, Serialize, Deserialize, rmcp::schemars::JsonSchema)]
pub struct RenderDonutParams { pub struct RenderDonutParams {
/// The chart data (single chart object or array of chart objects) /// The chart data as an array of chart objects. Use a single-element array for one chart.
pub data: DonutChartData, pub data: Vec<SingleDonutChart>,
} }
/// Treemap node structure /// Treemap node structure
@@ -987,7 +970,8 @@ Example:
#[tool( #[tool(
name = "render_donut", name = "render_donut",
description = r#"show pie or donut charts for categorical data visualization. description = r#"show pie or donut charts for categorical data visualization.
Supports single or multiple charts in a grid layout. Supports one or more charts in a grid layout.
The `data` field must always be an array; pass a single-element array for one chart.
Each chart object must contain: Each chart object must contain:
- values: Array of numbers OR objects with 'label' and 'value' - values: Array of numbers OR objects with 'label' and 'value'
@@ -996,20 +980,24 @@ Each chart object must contain:
- labels: Optional array of labels (required when values are plain numbers) - labels: Optional array of labels (required when values are plain numbers)
Example single chart (labeled values): Example single chart (labeled values):
{ [
"values": [ {
{"label": "Marketing", "value": 25000}, "values": [
{"label": "Development", "value": 35000} {"label": "Marketing", "value": 25000},
], {"label": "Development", "value": 35000}
"title": "Budget" ],
} "title": "Budget"
}
]
Example single chart (parallel arrays): Example single chart (parallel arrays):
{ [
"values": [45000, 38000], {
"labels": ["Product A", "Product B"], "values": [45000, 38000],
"type": "pie" "labels": ["Product A", "Product B"],
} "type": "pie"
}
]
Example multiple charts (array of chart objects): Example multiple charts (array of chart objects):
[ [
@@ -1023,7 +1011,7 @@ Example multiple charts (array of chart objects):
params: Parameters<RenderDonutParams>, params: Parameters<RenderDonutParams>,
) -> Result<CallToolResult, ErrorData> { ) -> Result<CallToolResult, ErrorData> {
let inner = params.0; let inner = params.0;
inner.data.validate()?; validate_donut_charts(&inner.data)?;
let data = validate_data_param( let data = validate_data_param(
&serde_json::to_value(inner).map_err(|e| { &serde_json::to_value(inner).map_err(|e| {
ErrorData::new( ErrorData::new(
@@ -1035,15 +1023,21 @@ Example multiple charts (array of chart objects):
true, true,
)?; )?;
let text_fallback = if data.is_array() { let charts = data.as_array().ok_or_else(|| {
let count = data.as_array().map(|a| a.len()).unwrap_or(0); ErrorData::new(
format!("donut/pie chart: {} chart(s)", count) ErrorCode::INVALID_PARAMS,
} else { "The 'data' parameter must be an array.".to_string(),
let title = data None,
)
})?;
let text_fallback = if charts.len() == 1 {
let title = charts[0]
.get("title") .get("title")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or("Untitled"); .unwrap_or("Untitled");
format!("donut/pie chart: \"{}\"", title) format!("donut/pie chart: \"{}\"", title)
} else {
format!("donut/pie chart: {} chart(s)", charts.len())
}; };
let mut result = CallToolResult::structured(data); let mut result = CallToolResult::structured(data);
@@ -1566,7 +1560,7 @@ mod tests {
async fn test_render_donut() { async fn test_render_donut() {
let router = AutoVisualiserRouter::new(); let router = AutoVisualiserRouter::new();
let params = Parameters(RenderDonutParams { let params = Parameters(RenderDonutParams {
data: DonutChartData::Single(SingleDonutChart { data: vec![SingleDonutChart {
values: vec![ values: vec![
DonutDataItem::Number(30.0), DonutDataItem::Number(30.0),
DonutDataItem::Number(40.0), DonutDataItem::Number(40.0),
@@ -1575,7 +1569,7 @@ mod tests {
labels: Some(vec!["A".to_string(), "B".to_string(), "C".to_string()]), labels: Some(vec!["A".to_string(), "B".to_string(), "C".to_string()]),
title: None, title: None,
chart_type: None, chart_type: None,
}), }],
}); });
let result = router.render_donut(params).await; let result = router.render_donut(params).await;
@@ -1721,14 +1715,14 @@ mod donut_format_tests {
#[test] #[test]
fn labeled_values_single_chart() { fn labeled_values_single_chart() {
// {"data": {"values": [{"label": "A", "value": 10}, ...]}} // {"data": [{"values": [{"label": "A", "value": 10}, ...]}]}
let input = json!({ let input = json!({
"data": { "data": [{
"values": [ "values": [
{"label": "A", "value": 10}, {"label": "A", "value": 10},
{"label": "B", "value": 20} {"label": "B", "value": 20}
] ]
} }]
}); });
let result = round_trip(input); let result = round_trip(input);
assert!( assert!(
@@ -1740,12 +1734,12 @@ mod donut_format_tests {
#[test] #[test]
fn parallel_arrays_single_chart() { fn parallel_arrays_single_chart() {
// {"data": {"values": [10, 20], "labels": ["A", "B"]}} // {"data": [{"values": [10, 20], "labels": ["A", "B"]}]}
let input = json!({ let input = json!({
"data": { "data": [{
"values": [10, 20], "values": [10, 20],
"labels": ["A", "B"] "labels": ["A", "B"]
} }]
}); });
let result = round_trip(input); let result = round_trip(input);
assert!( assert!(
@@ -1775,14 +1769,14 @@ mod donut_format_tests {
#[test] #[test]
fn labeled_values_with_title_and_type() { fn labeled_values_with_title_and_type() {
let input = json!({ let input = json!({
"data": { "data": [{
"values": [ "values": [
{"label": "Marketing", "value": 25000}, {"label": "Marketing", "value": 25000},
{"label": "Development", "value": 35000} {"label": "Development", "value": 35000}
], ],
"title": "Budget", "title": "Budget",
"type": "pie" "type": "pie"
} }]
}); });
let result = round_trip(input); let result = round_trip(input);
assert!( assert!(
@@ -1910,31 +1904,31 @@ mod validation_tests {
#[test] #[test]
fn donut_rejects_empty_values() { fn donut_rejects_empty_values() {
let data = DonutChartData::Single(SingleDonutChart { let data = vec![SingleDonutChart {
values: vec![], values: vec![],
title: None, title: None,
chart_type: None, chart_type: None,
labels: None, labels: None,
}); }];
assert!(data.validate().is_err()); assert!(validate_donut_charts(&data).is_err());
} }
#[test] #[test]
fn donut_rejects_mismatched_labels() { fn donut_rejects_mismatched_labels() {
let data = DonutChartData::Single(SingleDonutChart { let data = vec![SingleDonutChart {
values: vec![DonutDataItem::Number(10.0), DonutDataItem::Number(20.0)], values: vec![DonutDataItem::Number(10.0), DonutDataItem::Number(20.0)],
title: None, title: None,
chart_type: None, chart_type: None,
labels: Some(vec!["A".into()]), // 1 label but 2 values labels: Some(vec!["A".into()]), // 1 label but 2 values
}); }];
let err = data.validate().unwrap_err(); let err = validate_donut_charts(&data).unwrap_err();
assert!(err.message.contains("labels")); assert!(err.message.contains("labels"));
} }
#[test] #[test]
fn donut_rejects_empty_multiple() { fn donut_rejects_empty_multiple() {
let data = DonutChartData::Multiple(vec![]); let data: Vec<SingleDonutChart> = vec![];
assert!(data.validate().is_err()); assert!(validate_donut_charts(&data).is_err());
} }
#[test] #[test]