fix: match shape of the calltoolresult schema (#377)
BREAKING CHANGE: makes the `content` field non-optional
This commit is contained in:
parent
81ff3159f9
commit
a47cbaac4b
5 changed files with 26 additions and 29 deletions
|
|
@ -1257,8 +1257,7 @@ pub type CreateElicitationRequest =
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct CallToolResult {
|
pub struct CallToolResult {
|
||||||
/// The content returned by the tool (text, images, etc.)
|
/// The content returned by the tool (text, images, etc.)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
pub content: Vec<Content>,
|
||||||
pub content: Option<Vec<Content>>,
|
|
||||||
/// An optional JSON object that represents the structured result of the tool call
|
/// An optional JSON object that represents the structured result of the tool call
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub structured_content: Option<Value>,
|
pub structured_content: Option<Value>,
|
||||||
|
|
@ -1271,7 +1270,7 @@ impl CallToolResult {
|
||||||
/// Create a successful tool result with unstructured content
|
/// Create a successful tool result with unstructured content
|
||||||
pub fn success(content: Vec<Content>) -> Self {
|
pub fn success(content: Vec<Content>) -> Self {
|
||||||
CallToolResult {
|
CallToolResult {
|
||||||
content: Some(content),
|
content,
|
||||||
structured_content: None,
|
structured_content: None,
|
||||||
is_error: Some(false),
|
is_error: Some(false),
|
||||||
}
|
}
|
||||||
|
|
@ -1279,7 +1278,7 @@ impl CallToolResult {
|
||||||
/// Create an error tool result with unstructured content
|
/// Create an error tool result with unstructured content
|
||||||
pub fn error(content: Vec<Content>) -> Self {
|
pub fn error(content: Vec<Content>) -> Self {
|
||||||
CallToolResult {
|
CallToolResult {
|
||||||
content: Some(content),
|
content,
|
||||||
structured_content: None,
|
structured_content: None,
|
||||||
is_error: Some(true),
|
is_error: Some(true),
|
||||||
}
|
}
|
||||||
|
|
@ -1300,7 +1299,7 @@ impl CallToolResult {
|
||||||
/// ```
|
/// ```
|
||||||
pub fn structured(value: Value) -> Self {
|
pub fn structured(value: Value) -> Self {
|
||||||
CallToolResult {
|
CallToolResult {
|
||||||
content: Some(vec![Content::text(value.to_string())]),
|
content: vec![Content::text(value.to_string())],
|
||||||
structured_content: Some(value),
|
structured_content: Some(value),
|
||||||
is_error: Some(false),
|
is_error: Some(false),
|
||||||
}
|
}
|
||||||
|
|
@ -1325,7 +1324,7 @@ impl CallToolResult {
|
||||||
/// ```
|
/// ```
|
||||||
pub fn structured_error(value: Value) -> Self {
|
pub fn structured_error(value: Value) -> Self {
|
||||||
CallToolResult {
|
CallToolResult {
|
||||||
content: Some(vec![Content::text(value.to_string())]),
|
content: vec![Content::text(value.to_string())],
|
||||||
structured_content: Some(value),
|
structured_content: Some(value),
|
||||||
is_error: Some(true),
|
is_error: Some(true),
|
||||||
}
|
}
|
||||||
|
|
@ -1341,10 +1340,10 @@ impl CallToolResult {
|
||||||
where
|
where
|
||||||
T: DeserializeOwned,
|
T: DeserializeOwned,
|
||||||
{
|
{
|
||||||
let raw_text = match (self.structured_content, &self.content) {
|
let raw_text = match (self.structured_content, &self.content.first()) {
|
||||||
(Some(value), _) => return serde_json::from_value(value),
|
(Some(value), _) => return serde_json::from_value(value),
|
||||||
(None, Some(contents)) => {
|
(None, Some(contents)) => {
|
||||||
if let Some(text) = contents.first().and_then(|c| c.as_text()) {
|
if let Some(text) = contents.as_text() {
|
||||||
let text = &text.text;
|
let text = &text.text;
|
||||||
Some(text)
|
Some(text)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1379,13 +1378,13 @@ impl<'de> Deserialize<'de> for CallToolResult {
|
||||||
|
|
||||||
let helper = CallToolResultHelper::deserialize(deserializer)?;
|
let helper = CallToolResultHelper::deserialize(deserializer)?;
|
||||||
let result = CallToolResult {
|
let result = CallToolResult {
|
||||||
content: helper.content,
|
content: helper.content.unwrap_or_default(),
|
||||||
structured_content: helper.structured_content,
|
structured_content: helper.structured_content,
|
||||||
is_error: helper.is_error,
|
is_error: helper.is_error,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate mutual exclusivity
|
// Validate mutual exclusivity
|
||||||
if result.content.is_none() && result.structured_content.is_none() {
|
if result.content.is_empty() && result.structured_content.is_none() {
|
||||||
return Err(serde::de::Error::custom(
|
return Err(serde::de::Error::custom(
|
||||||
"CallToolResult must have either content or structured_content",
|
"CallToolResult must have either content or structured_content",
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -304,10 +304,7 @@
|
||||||
"properties": {
|
"properties": {
|
||||||
"content": {
|
"content": {
|
||||||
"description": "The content returned by the tool (text, images, etc.)",
|
"description": "The content returned by the tool (text, images, etc.)",
|
||||||
"type": [
|
"type": "array",
|
||||||
"array",
|
|
||||||
"null"
|
|
||||||
],
|
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/Annotated"
|
"$ref": "#/definitions/Annotated"
|
||||||
}
|
}
|
||||||
|
|
@ -322,7 +319,10 @@
|
||||||
"structuredContent": {
|
"structuredContent": {
|
||||||
"description": "An optional JSON object that represents the structured result of the tool call"
|
"description": "An optional JSON object that represents the structured result of the tool call"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"required": [
|
||||||
|
"content"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"CancelledNotificationMethod": {
|
"CancelledNotificationMethod": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
|
||||||
|
|
@ -122,10 +122,10 @@ async fn test_structured_content_in_call_result() {
|
||||||
|
|
||||||
let result = CallToolResult::structured(structured_data.clone());
|
let result = CallToolResult::structured(structured_data.clone());
|
||||||
|
|
||||||
assert!(result.content.is_some());
|
assert!(!result.content.is_empty());
|
||||||
assert!(result.structured_content.is_some());
|
assert!(result.structured_content.is_some());
|
||||||
|
|
||||||
let contents = result.content.unwrap();
|
let contents = result.content;
|
||||||
|
|
||||||
assert_eq!(contents.len(), 1);
|
assert_eq!(contents.len(), 1);
|
||||||
|
|
||||||
|
|
@ -150,10 +150,10 @@ async fn test_structured_error_in_call_result() {
|
||||||
|
|
||||||
let result = CallToolResult::structured_error(error_data.clone());
|
let result = CallToolResult::structured_error(error_data.clone());
|
||||||
|
|
||||||
assert!(result.content.is_some());
|
assert!(!result.content.is_empty());
|
||||||
assert!(result.structured_content.is_some());
|
assert!(result.structured_content.is_some());
|
||||||
|
|
||||||
let contents = result.content.unwrap();
|
let contents = result.content;
|
||||||
|
|
||||||
assert_eq!(contents.len(), 1);
|
assert_eq!(contents.len(), 1);
|
||||||
|
|
||||||
|
|
@ -217,10 +217,10 @@ async fn test_structured_return_conversion() {
|
||||||
|
|
||||||
// Tools which return structured content should also return a serialized version as
|
// Tools which return structured content should also return a serialized version as
|
||||||
// Content::text for backwards compatibility.
|
// Content::text for backwards compatibility.
|
||||||
assert!(call_result.content.is_some());
|
assert!(!call_result.content.is_empty());
|
||||||
assert!(call_result.structured_content.is_some());
|
assert!(call_result.structured_content.is_some());
|
||||||
|
|
||||||
let contents = call_result.content.unwrap();
|
let contents = call_result.content;
|
||||||
|
|
||||||
assert_eq!(contents.len(), 1);
|
assert_eq!(contents.len(), 1);
|
||||||
|
|
||||||
|
|
@ -278,5 +278,5 @@ async fn test_output_schema_requires_structured_content() {
|
||||||
|
|
||||||
// Verify it has structured_content and content
|
// Verify it has structured_content and content
|
||||||
assert!(call_result.structured_content.is_some());
|
assert!(call_result.structured_content.is_some());
|
||||||
assert!(call_result.content.is_some());
|
assert!(!call_result.content.is_empty());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -301,8 +301,7 @@ async fn test_optional_i64_field_with_null_input() -> anyhow::Result<()> {
|
||||||
|
|
||||||
let result_text = result
|
let result_text = result
|
||||||
.content
|
.content
|
||||||
.as_ref()
|
.first()
|
||||||
.and_then(|contents| contents.first())
|
|
||||||
.and_then(|content| content.raw.as_text())
|
.and_then(|content| content.raw.as_text())
|
||||||
.map(|text| text.text.as_str())
|
.map(|text| text.text.as_str())
|
||||||
.expect("Expected text content");
|
.expect("Expected text content");
|
||||||
|
|
@ -330,8 +329,7 @@ async fn test_optional_i64_field_with_null_input() -> anyhow::Result<()> {
|
||||||
|
|
||||||
let some_result_text = some_result
|
let some_result_text = some_result
|
||||||
.content
|
.content
|
||||||
.as_ref()
|
.first()
|
||||||
.and_then(|contents| contents.first())
|
|
||||||
.and_then(|content| content.raw.as_text())
|
.and_then(|content| content.raw.as_text())
|
||||||
.map(|text| text.text.as_str())
|
.map(|text| text.text.as_str())
|
||||||
.expect("Expected text content");
|
.expect("Expected text content");
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,8 @@ impl ChatSession {
|
||||||
if result.is_error.is_some_and(|b| b) {
|
if result.is_error.is_some_and(|b| b) {
|
||||||
self.messages
|
self.messages
|
||||||
.push(Message::user("tool call failed, mcp call error"));
|
.push(Message::user("tool call failed, mcp call error"));
|
||||||
} else if let Some(contents) = &result.content {
|
} else {
|
||||||
contents.iter().for_each(|content| {
|
result.content.iter().for_each(|content| {
|
||||||
if let Some(content_text) = content.as_text() {
|
if let Some(content_text) = content.as_text() {
|
||||||
let json_result = serde_json::from_str::<serde_json::Value>(
|
let json_result = serde_json::from_str::<serde_json::Value>(
|
||||||
&content_text.text,
|
&content_text.text,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue