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:
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user