Tool reply meta (#6074)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
use crate::mcp_utils::ToolResult;
|
||||
use chrono::Utc;
|
||||
use rmcp::model::{
|
||||
AnnotateAble, CallToolRequestParam, Content, ImageContent, JsonObject, PromptMessage,
|
||||
PromptMessageContent, PromptMessageRole, RawContent, RawImageContent, RawTextContent,
|
||||
ResourceContents, Role, TextContent,
|
||||
AnnotateAble, CallToolRequestParam, CallToolResult, Content, ImageContent, JsonObject,
|
||||
PromptMessage, PromptMessageContent, PromptMessageRole, RawContent, RawImageContent,
|
||||
RawTextContent, ResourceContents, Role, TextContent,
|
||||
};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::collections::HashSet;
|
||||
@@ -88,7 +88,7 @@ pub struct ToolResponse {
|
||||
pub id: String,
|
||||
#[serde(with = "tool_result_serde")]
|
||||
#[schema(value_type = Object)]
|
||||
pub tool_result: ToolResult<Vec<Content>>,
|
||||
pub tool_result: ToolResult<CallToolResult>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
@@ -190,7 +190,7 @@ impl fmt::Display for MessageContent {
|
||||
f,
|
||||
"[ToolResponse: {}]",
|
||||
match &r.tool_result {
|
||||
Ok(contents) => format!("{} content item(s)", contents.len()),
|
||||
Ok(result) => format!("{} content item(s)", result.content.len()),
|
||||
Err(e) => format!("Error: {e}"),
|
||||
}
|
||||
),
|
||||
@@ -266,7 +266,7 @@ impl MessageContent {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn tool_response<S: Into<String>>(id: S, tool_result: ToolResult<Vec<Content>>) -> Self {
|
||||
pub fn tool_response<S: Into<String>>(id: S, tool_result: ToolResult<CallToolResult>) -> Self {
|
||||
MessageContent::ToolResponse(ToolResponse {
|
||||
id: id.into(),
|
||||
tool_result,
|
||||
@@ -380,8 +380,9 @@ impl MessageContent {
|
||||
|
||||
pub fn as_tool_response_text(&self) -> Option<String> {
|
||||
if let Some(tool_response) = self.as_tool_response() {
|
||||
if let Ok(contents) = &tool_response.tool_result {
|
||||
let texts: Vec<String> = contents
|
||||
if let Ok(result) = &tool_response.tool_result {
|
||||
let texts: Vec<String> = result
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|content| content.as_text().map(|t| t.text.to_string()))
|
||||
.collect();
|
||||
@@ -644,7 +645,7 @@ impl Message {
|
||||
pub fn with_tool_response<S: Into<String>>(
|
||||
self,
|
||||
id: S,
|
||||
result: ToolResult<Vec<Content>>,
|
||||
result: ToolResult<CallToolResult>,
|
||||
) -> Self {
|
||||
self.with_content(MessageContent::tool_response(id, result))
|
||||
}
|
||||
|
||||
@@ -555,7 +555,15 @@ mod tests {
|
||||
arguments: Some(object!({"query": "rust programming"})),
|
||||
}),
|
||||
),
|
||||
Message::user().with_tool_response("search_1", Ok(vec![])),
|
||||
Message::user().with_tool_response(
|
||||
"search_1",
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
),
|
||||
Message::assistant().with_text("Based on the search results, here's what I found..."),
|
||||
];
|
||||
|
||||
@@ -592,7 +600,15 @@ mod tests {
|
||||
Message::user().with_text("Another user message"),
|
||||
Message::assistant()
|
||||
.with_text("Response")
|
||||
.with_tool_response("orphan_1", Ok(vec![])), // Wrong role
|
||||
.with_tool_response(
|
||||
"orphan_1",
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
), // Wrong role
|
||||
Message::assistant().with_thinking("Let me think", "sig"),
|
||||
Message::user()
|
||||
.with_tool_request(
|
||||
@@ -642,7 +658,15 @@ mod tests {
|
||||
}),
|
||||
),
|
||||
Message::user(),
|
||||
Message::user().with_tool_response("wrong_id", Ok(vec![])),
|
||||
Message::user().with_tool_response(
|
||||
"wrong_id",
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
),
|
||||
Message::assistant().with_tool_request(
|
||||
"search_2",
|
||||
Ok(CallToolRequestParam {
|
||||
@@ -687,7 +711,12 @@ mod tests {
|
||||
.with_tool_request("toolu_bdrk_01KgDYHs4fAodi22NqxRzmwx", Ok(CallToolRequestParam { name: "developer__shell".into(), arguments: Some(object!({"command": "wc slack.yaml"})) })),
|
||||
|
||||
Message::user()
|
||||
.with_tool_response("toolu_bdrk_01KgDYHs4fAodi22NqxRzmwx", Ok(vec![])),
|
||||
.with_tool_response("toolu_bdrk_01KgDYHs4fAodi22NqxRzmwx", Ok(rmcp::model::CallToolResult {
|
||||
content: vec![],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
})),
|
||||
|
||||
Message::assistant()
|
||||
.with_text("I ran `ls -la` in the current directory and found several files. Looking at the file sizes, I can see that both `slack.yaml` and `subrecipes.yaml` are 0 bytes (the smallest files). I ran a word count on `slack.yaml` which shows: **0 lines**, **0 words**, **0 characters**"),
|
||||
@@ -718,7 +747,15 @@ mod tests {
|
||||
arguments: Some(object!({})),
|
||||
}),
|
||||
),
|
||||
Message::user().with_tool_response("search_1", Ok(vec![])),
|
||||
Message::user().with_tool_response(
|
||||
"search_1",
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
),
|
||||
Message::user().with_text("Thanks!"),
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user