alexhancock/mcp-crate-cleanup (#4885)
This commit is contained in:
@@ -149,7 +149,7 @@ impl CursorAgentProvider {
|
||||
MessageContent::ToolRequest(tool_request) => {
|
||||
if let Ok(tool_call) = &tool_request.tool_call {
|
||||
full_prompt.push_str(&format!(
|
||||
"Tool Use: {} with args: {}\n",
|
||||
"Tool Use: {} with args: {:?}\n",
|
||||
tool_call.name, tool_call.arguments
|
||||
));
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@ use crate::model::ModelConfig;
|
||||
use crate::providers::base::Usage;
|
||||
use crate::providers::errors::ProviderError;
|
||||
use anyhow::{anyhow, Result};
|
||||
use mcp_core::ToolCall;
|
||||
use rmcp::model::{ErrorCode, ErrorData, Role, Tool};
|
||||
use rmcp::model::{object, CallToolRequestParam, ErrorCode, ErrorData, Role, Tool};
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashSet;
|
||||
|
||||
@@ -230,19 +229,24 @@ pub fn response_to_message(response: &Value) -> Result<Message> {
|
||||
let name = block
|
||||
.get(NAME_FIELD)
|
||||
.and_then(|n| n.as_str())
|
||||
.ok_or_else(|| anyhow!("Missing tool_use name"))?;
|
||||
.ok_or_else(|| anyhow!("Missing tool_use name"))?
|
||||
.to_string();
|
||||
let input = block
|
||||
.get(INPUT_FIELD)
|
||||
.ok_or_else(|| anyhow!("Missing tool_use input"))?;
|
||||
|
||||
let tool_call = ToolCall::new(name, input.clone());
|
||||
let tool_call = CallToolRequestParam {
|
||||
name: name.into(),
|
||||
arguments: Some(object(input.clone())),
|
||||
};
|
||||
message = message.with_tool_request(id, Ok(tool_call));
|
||||
}
|
||||
Some(THINKING_TYPE) => {
|
||||
let thinking = block
|
||||
.get(THINKING_TYPE)
|
||||
.and_then(|t| t.as_str())
|
||||
.ok_or_else(|| anyhow!("Missing thinking content"))?;
|
||||
.ok_or_else(|| anyhow!("Missing thinking content"))?
|
||||
.to_string();
|
||||
let signature = block
|
||||
.get(SIGNATURE_FIELD)
|
||||
.and_then(|s| s.as_str())
|
||||
@@ -589,7 +593,8 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
let tool_call = ToolCall::new(&name, parsed_args);
|
||||
let tool_call = CallToolRequestParam{ name: name.into(), arguments: Some(object(parsed_args)) };
|
||||
|
||||
let mut message = Message::new(
|
||||
rmcp::model::Role::Assistant,
|
||||
chrono::Utc::now().timestamp(),
|
||||
@@ -750,7 +755,7 @@ mod tests {
|
||||
if let MessageContent::ToolRequest(tool_request) = &message.content[0] {
|
||||
let tool_call = tool_request.tool_call.as_ref().unwrap();
|
||||
assert_eq!(tool_call.name, "calculator");
|
||||
assert_eq!(tool_call.arguments, json!({"expression": "2 + 2"}));
|
||||
assert_eq!(tool_call.arguments, Some(object!({"expression": "2 + 2"})));
|
||||
} else {
|
||||
panic!("Expected ToolRequest content");
|
||||
}
|
||||
@@ -992,7 +997,10 @@ mod tests {
|
||||
let messages = vec![
|
||||
Message::assistant().with_tool_request(
|
||||
"tool_1",
|
||||
Ok(ToolCall::new("calculator", json!({"expression": "2 + 2"}))),
|
||||
Ok(CallToolRequestParam {
|
||||
name: "calculator".into(),
|
||||
arguments: Some(object!({"expression": "2 + 2"})),
|
||||
}),
|
||||
),
|
||||
Message::user().with_tool_response(
|
||||
"tool_1",
|
||||
|
||||
@@ -2,13 +2,16 @@ use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::mcp_utils::ToolResult;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use aws_sdk_bedrockruntime::types as bedrock;
|
||||
use aws_smithy_types::{Document, Number};
|
||||
use base64::Engine;
|
||||
use chrono::Utc;
|
||||
use mcp_core::{ToolCall, ToolResult};
|
||||
use rmcp::model::{Content, ErrorCode, ErrorData, RawContent, ResourceContents, Role, Tool};
|
||||
use rmcp::model::{
|
||||
object, CallToolRequestParam, Content, ErrorCode, ErrorData, RawContent, ResourceContents,
|
||||
Role, Tool,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::super::base::Usage;
|
||||
@@ -57,7 +60,7 @@ pub fn to_bedrock_message_content(content: &MessageContent) -> Result<bedrock::C
|
||||
bedrock::ToolUseBlock::builder()
|
||||
.tool_use_id(tool_use_id)
|
||||
.name(call.name.to_string())
|
||||
.input(to_bedrock_json(&call.arguments))
|
||||
.input(to_bedrock_json(&Value::from(call.arguments.clone())))
|
||||
.build()
|
||||
} else {
|
||||
bedrock::ToolUseBlock::builder()
|
||||
@@ -72,7 +75,7 @@ pub fn to_bedrock_message_content(content: &MessageContent) -> Result<bedrock::C
|
||||
bedrock::ToolUseBlock::builder()
|
||||
.tool_use_id(tool_use_id)
|
||||
.name(call.name.to_string())
|
||||
.input(to_bedrock_json(&call.arguments))
|
||||
.input(to_bedrock_json(&Value::from(call.arguments.clone())))
|
||||
.build()
|
||||
} else {
|
||||
bedrock::ToolUseBlock::builder()
|
||||
@@ -282,10 +285,10 @@ pub fn from_bedrock_content_block(block: &bedrock::ContentBlock) -> Result<Messa
|
||||
bedrock::ContentBlock::Text(text) => MessageContent::text(text),
|
||||
bedrock::ContentBlock::ToolUse(tool_use) => MessageContent::tool_request(
|
||||
tool_use.tool_use_id.to_string(),
|
||||
Ok(ToolCall::new(
|
||||
tool_use.name.to_string(),
|
||||
from_bedrock_json(&tool_use.input)?,
|
||||
)),
|
||||
Ok(CallToolRequestParam {
|
||||
name: tool_use.name.clone().into(),
|
||||
arguments: Some(object(from_bedrock_json(&tool_use.input.clone())?)),
|
||||
}),
|
||||
),
|
||||
bedrock::ContentBlock::ToolResult(tool_res) => MessageContent::tool_response(
|
||||
tool_res.tool_use_id.to_string(),
|
||||
|
||||
@@ -5,9 +5,9 @@ use crate::providers::utils::{
|
||||
sanitize_function_name, ImageFormat,
|
||||
};
|
||||
use anyhow::{anyhow, Error};
|
||||
use mcp_core::ToolCall;
|
||||
use rmcp::model::{
|
||||
AnnotateAble, Content, ErrorCode, ErrorData, RawContent, ResourceContents, Role, Tool,
|
||||
object, AnnotateAble, CallToolRequestParam, Content, ErrorCode, ErrorData, RawContent,
|
||||
ResourceContents, Role, Tool,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
@@ -102,6 +102,12 @@ fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<Data
|
||||
match &request.tool_call {
|
||||
Ok(tool_call) => {
|
||||
let sanitized_name = sanitize_function_name(&tool_call.name);
|
||||
let arguments_str = match &tool_call.arguments {
|
||||
Some(args) => {
|
||||
serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string())
|
||||
}
|
||||
None => "{}".to_string(),
|
||||
};
|
||||
|
||||
let tool_calls = converted.tool_calls.get_or_insert_default();
|
||||
tool_calls.push(json!({
|
||||
@@ -109,7 +115,7 @@ fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<Data
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": sanitized_name,
|
||||
"arguments": tool_call.arguments.to_string(),
|
||||
"arguments": arguments_str,
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -286,6 +292,7 @@ pub fn format_tools(tools: &[Tool]) -> anyhow::Result<Vec<Value>> {
|
||||
}
|
||||
|
||||
/// Convert Databricks' API response to internal Message format
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn response_to_message(response: &Value) -> anyhow::Result<Message> {
|
||||
let original = &response["choices"][0]["message"];
|
||||
let mut content = Vec::new();
|
||||
@@ -373,7 +380,10 @@ pub fn response_to_message(response: &Value) -> anyhow::Result<Message> {
|
||||
Ok(params) => {
|
||||
content.push(MessageContent::tool_request(
|
||||
id,
|
||||
Ok(ToolCall::new(&function_name, params)),
|
||||
Ok(CallToolRequestParam {
|
||||
name: function_name.into(),
|
||||
arguments: Some(object(params)),
|
||||
}),
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -771,7 +781,10 @@ mod tests {
|
||||
Message::user().with_text("How are you?"),
|
||||
Message::assistant().with_tool_request(
|
||||
"tool1",
|
||||
Ok(ToolCall::new("example", json!({"param1": "value1"}))),
|
||||
Ok(CallToolRequestParam {
|
||||
name: "example".into(),
|
||||
arguments: Some(object!({"param1": "value1"})),
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -807,7 +820,10 @@ mod tests {
|
||||
fn test_format_messages_multiple_content() -> anyhow::Result<()> {
|
||||
let mut messages = vec![Message::assistant().with_tool_request(
|
||||
"tool1",
|
||||
Ok(ToolCall::new("example", json!({"param1": "value1"}))),
|
||||
Ok(CallToolRequestParam {
|
||||
name: "example".into(),
|
||||
arguments: Some(object!({"param1": "value1"})),
|
||||
}),
|
||||
)];
|
||||
|
||||
// Get the ID from the tool request to use in the response
|
||||
@@ -956,7 +972,7 @@ mod tests {
|
||||
if let MessageContent::ToolRequest(request) = &message.content[0] {
|
||||
let tool_call = request.tool_call.as_ref().unwrap();
|
||||
assert_eq!(tool_call.name, "example_fn");
|
||||
assert_eq!(tool_call.arguments, json!({"param": "value"}));
|
||||
assert_eq!(tool_call.arguments, Some(object!({"param": "value"})));
|
||||
} else {
|
||||
panic!("Expected ToolRequest content");
|
||||
}
|
||||
@@ -1027,7 +1043,7 @@ mod tests {
|
||||
if let MessageContent::ToolRequest(request) = &message.content[0] {
|
||||
let tool_call = request.tool_call.as_ref().unwrap();
|
||||
assert_eq!(tool_call.name, "example_fn");
|
||||
assert_eq!(tool_call.arguments, json!({}));
|
||||
assert_eq!(tool_call.arguments, Some(object!({})));
|
||||
} else {
|
||||
panic!("Expected ToolRequest content");
|
||||
}
|
||||
@@ -1226,4 +1242,65 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_messages_tool_request_with_none_arguments() -> anyhow::Result<()> {
|
||||
// Test that tool calls with None arguments are formatted as "{}" string
|
||||
let message = Message::assistant().with_tool_request(
|
||||
"tool1",
|
||||
Ok(CallToolRequestParam {
|
||||
name: "test_tool".into(),
|
||||
arguments: None, // This is the key case the fix addresses
|
||||
}),
|
||||
);
|
||||
|
||||
let spec = format_messages(&[message], &ImageFormat::OpenAi);
|
||||
let as_value = serde_json::to_value(spec)?;
|
||||
let spec_array = as_value.as_array().unwrap();
|
||||
|
||||
assert_eq!(spec_array.len(), 1);
|
||||
assert_eq!(spec_array[0]["role"], "assistant");
|
||||
assert!(spec_array[0]["tool_calls"].is_array());
|
||||
|
||||
let tool_call = &spec_array[0]["tool_calls"][0];
|
||||
assert_eq!(tool_call["id"], "tool1");
|
||||
assert_eq!(tool_call["type"], "function");
|
||||
assert_eq!(tool_call["function"]["name"], "test_tool");
|
||||
// This should be the string "{}", not null
|
||||
assert_eq!(tool_call["function"]["arguments"], "{}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_messages_tool_request_with_some_arguments() -> anyhow::Result<()> {
|
||||
// Test that tool calls with Some arguments are properly JSON-serialized
|
||||
let message = Message::assistant().with_tool_request(
|
||||
"tool1",
|
||||
Ok(CallToolRequestParam {
|
||||
name: "test_tool".into(),
|
||||
arguments: Some(object!({"param": "value", "number": 42})),
|
||||
}),
|
||||
);
|
||||
|
||||
let spec = format_messages(&[message], &ImageFormat::OpenAi);
|
||||
let as_value = serde_json::to_value(spec)?;
|
||||
let spec_array = as_value.as_array().unwrap();
|
||||
|
||||
assert_eq!(spec_array.len(), 1);
|
||||
assert_eq!(spec_array[0]["role"], "assistant");
|
||||
assert!(spec_array[0]["tool_calls"].is_array());
|
||||
|
||||
let tool_call = &spec_array[0]["tool_calls"][0];
|
||||
assert_eq!(tool_call["id"], "tool1");
|
||||
assert_eq!(tool_call["type"], "function");
|
||||
assert_eq!(tool_call["function"]["name"], "test_tool");
|
||||
// This should be a JSON string representation
|
||||
let args_str = tool_call["function"]["arguments"].as_str().unwrap();
|
||||
let parsed_args: Value = serde_json::from_str(args_str)?;
|
||||
assert_eq!(parsed_args["param"], "value");
|
||||
assert_eq!(parsed_args["number"], 42);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ use crate::providers::base::Usage;
|
||||
use crate::providers::errors::ProviderError;
|
||||
use crate::providers::utils::{is_valid_function_name, sanitize_function_name};
|
||||
use anyhow::Result;
|
||||
use mcp_core::ToolCall;
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use rmcp::model::{AnnotateAble, ErrorCode, ErrorData, RawContent, Role, Tool};
|
||||
use rmcp::model::{
|
||||
object, AnnotateAble, CallToolRequestParam, ErrorCode, ErrorData, RawContent, Role, Tool,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::conversation::message::{Message, MessageContent};
|
||||
@@ -43,12 +44,14 @@ pub fn format_messages(messages: &[Message]) -> Vec<Value> {
|
||||
"name".to_string(),
|
||||
json!(sanitize_function_name(&tool_call.name)),
|
||||
);
|
||||
if tool_call.arguments.is_object()
|
||||
&& !tool_call.arguments.as_object().unwrap().is_empty()
|
||||
{
|
||||
function_call_part
|
||||
.insert("args".to_string(), tool_call.arguments.clone());
|
||||
|
||||
if let Some(args) = &tool_call.arguments {
|
||||
if !args.is_empty() {
|
||||
function_call_part
|
||||
.insert("args".to_string(), args.clone().into());
|
||||
}
|
||||
}
|
||||
|
||||
parts.push(json!({
|
||||
"functionCall": function_call_part
|
||||
}));
|
||||
@@ -269,7 +272,10 @@ pub fn response_to_message(response: Value) -> Result<Message> {
|
||||
if let Some(params) = parameters {
|
||||
content.push(MessageContent::tool_request(
|
||||
id,
|
||||
Ok(ToolCall::new(&name, params.clone())),
|
||||
Ok(CallToolRequestParam {
|
||||
name: name.into(),
|
||||
arguments: Some(object(params.clone())),
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -341,6 +347,7 @@ pub fn create_request(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::conversation::message::Message;
|
||||
use rmcp::model::CallToolRequestParam;
|
||||
use rmcp::{model::Content, object};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -348,7 +355,7 @@ mod tests {
|
||||
Message::new(role, 0, vec![MessageContent::text(text.to_string())])
|
||||
}
|
||||
|
||||
fn set_up_tool_request_message(id: &str, tool_call: ToolCall) -> Message {
|
||||
fn set_up_tool_request_message(id: &str, tool_call: CallToolRequestParam) -> Message {
|
||||
Message::new(
|
||||
Role::User,
|
||||
0,
|
||||
@@ -356,14 +363,14 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
fn set_up_tool_confirmation_message(id: &str, tool_call: ToolCall) -> Message {
|
||||
fn set_up_tool_confirmation_message(id: &str, tool_call: CallToolRequestParam) -> Message {
|
||||
Message::new(
|
||||
Role::User,
|
||||
0,
|
||||
vec![MessageContent::tool_confirmation_request(
|
||||
id.to_string(),
|
||||
tool_call.name.clone(),
|
||||
tool_call.arguments.clone(),
|
||||
tool_call.name.to_string().clone(),
|
||||
tool_call.arguments.unwrap_or_default().clone(),
|
||||
Some("goose would like to call the above tool. Allow? (y/n):".to_string()),
|
||||
)],
|
||||
)
|
||||
@@ -415,10 +422,19 @@ mod tests {
|
||||
"param1": "value1"
|
||||
});
|
||||
let messages = vec![
|
||||
set_up_tool_request_message("id", ToolCall::new("tool_name", arguments.clone())),
|
||||
set_up_tool_request_message(
|
||||
"id",
|
||||
CallToolRequestParam {
|
||||
name: "tool_name".into(),
|
||||
arguments: Some(object(arguments.clone())),
|
||||
},
|
||||
),
|
||||
set_up_tool_confirmation_message(
|
||||
"id2",
|
||||
ToolCall::new("tool_name_2", arguments.clone()),
|
||||
CallToolRequestParam {
|
||||
name: "tool_name_2".into(),
|
||||
arguments: Some(object(arguments.clone())),
|
||||
},
|
||||
),
|
||||
];
|
||||
let payload = format_messages(&messages);
|
||||
@@ -780,7 +796,14 @@ mod tests {
|
||||
assert_eq!(message.content.len(), 1);
|
||||
if let Ok(tool_call) = &message.content[0].as_tool_request().unwrap().tool_call {
|
||||
assert_eq!(tool_call.name, "valid_name");
|
||||
assert_eq!(tool_call.arguments["param"], "value");
|
||||
assert_eq!(
|
||||
tool_call
|
||||
.arguments
|
||||
.as_ref()
|
||||
.and_then(|args| args.get("param"))
|
||||
.and_then(|v| v.as_str()),
|
||||
Some("value")
|
||||
);
|
||||
} else {
|
||||
panic!("Expected valid tool request");
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ use crate::providers::utils::{
|
||||
use anyhow::{anyhow, Error};
|
||||
use async_stream::try_stream;
|
||||
use futures::Stream;
|
||||
use mcp_core::ToolCall;
|
||||
use rmcp::model::{
|
||||
AnnotateAble, Content, ErrorCode, ErrorData, RawContent, ResourceContents, Role, Tool,
|
||||
object, AnnotateAble, CallToolRequestParam, Content, ErrorCode, ErrorData, RawContent,
|
||||
ResourceContents, Role, Tool,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
@@ -104,6 +104,13 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
|
||||
MessageContent::ToolRequest(request) => match &request.tool_call {
|
||||
Ok(tool_call) => {
|
||||
let sanitized_name = sanitize_function_name(&tool_call.name);
|
||||
let arguments_str = match &tool_call.arguments {
|
||||
Some(args) => {
|
||||
serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string())
|
||||
}
|
||||
None => "{}".to_string(),
|
||||
};
|
||||
|
||||
let tool_calls = converted
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
@@ -115,7 +122,7 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": sanitized_name,
|
||||
"arguments": tool_call.arguments.to_string(),
|
||||
"arguments": arguments_str,
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -209,6 +216,13 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
|
||||
MessageContent::FrontendToolRequest(request) => match &request.tool_call {
|
||||
Ok(tool_call) => {
|
||||
let sanitized_name = sanitize_function_name(&tool_call.name);
|
||||
let arguments_str = match &tool_call.arguments {
|
||||
Some(args) => {
|
||||
serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string())
|
||||
}
|
||||
None => "{}".to_string(),
|
||||
};
|
||||
|
||||
let tool_calls = converted
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
@@ -220,7 +234,7 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": sanitized_name,
|
||||
"arguments": tool_call.arguments.to_string(),
|
||||
"arguments": arguments_str,
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -316,7 +330,10 @@ pub fn response_to_message(response: &Value) -> anyhow::Result<Message> {
|
||||
Ok(params) => {
|
||||
content.push(MessageContent::tool_request(
|
||||
id,
|
||||
Ok(ToolCall::new(&function_name, params)),
|
||||
Ok(CallToolRequestParam {
|
||||
name: function_name.into(),
|
||||
arguments: Some(object(params)),
|
||||
}),
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -515,7 +532,7 @@ where
|
||||
Ok(params) => {
|
||||
MessageContent::tool_request(
|
||||
id.clone(),
|
||||
Ok(ToolCall::new(function_name.clone(), params)),
|
||||
Ok(CallToolRequestParam { name: function_name.clone().into(), arguments: Some(object(params)) }),
|
||||
)
|
||||
},
|
||||
Err(e) => {
|
||||
@@ -821,7 +838,10 @@ mod tests {
|
||||
Message::user().with_text("How are you?"),
|
||||
Message::assistant().with_tool_request(
|
||||
"tool1",
|
||||
Ok(ToolCall::new("example", json!({"param1": "value1"}))),
|
||||
Ok(CallToolRequestParam {
|
||||
name: "example".into(),
|
||||
arguments: Some(object!({"param1": "value1"})),
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -855,7 +875,10 @@ mod tests {
|
||||
fn test_format_messages_multiple_content() -> anyhow::Result<()> {
|
||||
let mut messages = vec![Message::assistant().with_tool_request(
|
||||
"tool1",
|
||||
Ok(ToolCall::new("example", json!({"param1": "value1"}))),
|
||||
Ok(CallToolRequestParam {
|
||||
name: "example".into(),
|
||||
arguments: Some(object!({"param1": "value1"})),
|
||||
}),
|
||||
)];
|
||||
|
||||
// Get the ID from the tool request to use in the response
|
||||
@@ -1000,7 +1023,7 @@ mod tests {
|
||||
if let MessageContent::ToolRequest(request) = &message.content[0] {
|
||||
let tool_call = request.tool_call.as_ref().unwrap();
|
||||
assert_eq!(tool_call.name, "example_fn");
|
||||
assert_eq!(tool_call.arguments, json!({"param": "value"}));
|
||||
assert_eq!(tool_call.arguments, Some(object!({"param": "value"})));
|
||||
} else {
|
||||
panic!("Expected ToolRequest content");
|
||||
}
|
||||
@@ -1071,7 +1094,7 @@ mod tests {
|
||||
if let MessageContent::ToolRequest(request) = &message.content[0] {
|
||||
let tool_call = request.tool_call.as_ref().unwrap();
|
||||
assert_eq!(tool_call.name, "example_fn");
|
||||
assert_eq!(tool_call.arguments, json!({}));
|
||||
assert_eq!(tool_call.arguments, Some(object!({})));
|
||||
} else {
|
||||
panic!("Expected ToolRequest content");
|
||||
}
|
||||
@@ -1079,6 +1102,120 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_messages_tool_request_with_none_arguments() -> anyhow::Result<()> {
|
||||
// Test that tool calls with None arguments are formatted as "{}" string
|
||||
let message = Message::assistant().with_tool_request(
|
||||
"tool1",
|
||||
Ok(CallToolRequestParam {
|
||||
name: "test_tool".into(),
|
||||
arguments: None, // This is the key case the fix addresses
|
||||
}),
|
||||
);
|
||||
|
||||
let spec = format_messages(&[message], &ImageFormat::OpenAi);
|
||||
|
||||
assert_eq!(spec.len(), 1);
|
||||
assert_eq!(spec[0]["role"], "assistant");
|
||||
assert!(spec[0]["tool_calls"].is_array());
|
||||
|
||||
let tool_call = &spec[0]["tool_calls"][0];
|
||||
assert_eq!(tool_call["id"], "tool1");
|
||||
assert_eq!(tool_call["type"], "function");
|
||||
assert_eq!(tool_call["function"]["name"], "test_tool");
|
||||
// This should be the string "{}", not null
|
||||
assert_eq!(tool_call["function"]["arguments"], "{}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_messages_tool_request_with_some_arguments() -> anyhow::Result<()> {
|
||||
// Test that tool calls with Some arguments are properly JSON-serialized
|
||||
let message = Message::assistant().with_tool_request(
|
||||
"tool1",
|
||||
Ok(CallToolRequestParam {
|
||||
name: "test_tool".into(),
|
||||
arguments: Some(object!({"param": "value", "number": 42})),
|
||||
}),
|
||||
);
|
||||
|
||||
let spec = format_messages(&[message], &ImageFormat::OpenAi);
|
||||
|
||||
assert_eq!(spec.len(), 1);
|
||||
assert_eq!(spec[0]["role"], "assistant");
|
||||
assert!(spec[0]["tool_calls"].is_array());
|
||||
|
||||
let tool_call = &spec[0]["tool_calls"][0];
|
||||
assert_eq!(tool_call["id"], "tool1");
|
||||
assert_eq!(tool_call["type"], "function");
|
||||
assert_eq!(tool_call["function"]["name"], "test_tool");
|
||||
// This should be a JSON string representation
|
||||
let args_str = tool_call["function"]["arguments"].as_str().unwrap();
|
||||
let parsed_args: Value = serde_json::from_str(args_str)?;
|
||||
assert_eq!(parsed_args["param"], "value");
|
||||
assert_eq!(parsed_args["number"], 42);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_messages_frontend_tool_request_with_none_arguments() -> anyhow::Result<()> {
|
||||
// Test that FrontendToolRequest with None arguments are formatted as "{}" string
|
||||
let message = Message::assistant().with_frontend_tool_request(
|
||||
"frontend_tool1",
|
||||
Ok(CallToolRequestParam {
|
||||
name: "frontend_test_tool".into(),
|
||||
arguments: None, // This is the key case the fix addresses
|
||||
}),
|
||||
);
|
||||
|
||||
let spec = format_messages(&[message], &ImageFormat::OpenAi);
|
||||
|
||||
assert_eq!(spec.len(), 1);
|
||||
assert_eq!(spec[0]["role"], "assistant");
|
||||
assert!(spec[0]["tool_calls"].is_array());
|
||||
|
||||
let tool_call = &spec[0]["tool_calls"][0];
|
||||
assert_eq!(tool_call["id"], "frontend_tool1");
|
||||
assert_eq!(tool_call["type"], "function");
|
||||
assert_eq!(tool_call["function"]["name"], "frontend_test_tool");
|
||||
// This should be the string "{}", not null
|
||||
assert_eq!(tool_call["function"]["arguments"], "{}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_messages_frontend_tool_request_with_some_arguments() -> anyhow::Result<()> {
|
||||
// Test that FrontendToolRequest with Some arguments are properly JSON-serialized
|
||||
let message = Message::assistant().with_frontend_tool_request(
|
||||
"frontend_tool1",
|
||||
Ok(CallToolRequestParam {
|
||||
name: "frontend_test_tool".into(),
|
||||
arguments: Some(object!({"action": "click", "element": "button"})),
|
||||
}),
|
||||
);
|
||||
|
||||
let spec = format_messages(&[message], &ImageFormat::OpenAi);
|
||||
|
||||
assert_eq!(spec.len(), 1);
|
||||
assert_eq!(spec[0]["role"], "assistant");
|
||||
assert!(spec[0]["tool_calls"].is_array());
|
||||
|
||||
let tool_call = &spec[0]["tool_calls"][0];
|
||||
assert_eq!(tool_call["id"], "frontend_tool1");
|
||||
assert_eq!(tool_call["type"], "function");
|
||||
assert_eq!(tool_call["function"]["name"], "frontend_test_tool");
|
||||
// This should be a JSON string representation
|
||||
let args_str = tool_call["function"]["arguments"].as_str().unwrap();
|
||||
let parsed_args: Value = serde_json::from_str(args_str)?;
|
||||
assert_eq!(parsed_args["action"], "click");
|
||||
assert_eq!(parsed_args["element"], "button");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_request_gpt_4o() -> anyhow::Result<()> {
|
||||
// Test default medium reasoning effort for O3 model
|
||||
|
||||
@@ -3,8 +3,8 @@ use crate::model::ModelConfig;
|
||||
use crate::providers::base::Usage;
|
||||
use crate::providers::errors::ProviderError;
|
||||
use anyhow::{anyhow, Result};
|
||||
use mcp_core::tool::ToolCall;
|
||||
use rmcp::model::{Role, Tool};
|
||||
use rmcp::model::{object, CallToolRequestParam, Role, Tool};
|
||||
use rmcp::object;
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashSet;
|
||||
|
||||
@@ -181,16 +181,22 @@ pub fn parse_streaming_response(sse_data: &str) -> Result<Message> {
|
||||
}
|
||||
|
||||
// Add tool use if complete
|
||||
if let (Some(id), Some(name)) = (&tool_use_id, &tool_name) {
|
||||
if let Some((id, name)) = tool_use_id.zip(tool_name) {
|
||||
if !tool_input.is_empty() {
|
||||
let input_value = serde_json::from_str::<Value>(&tool_input)
|
||||
.unwrap_or_else(|_| Value::String(tool_input.clone()));
|
||||
let tool_call = ToolCall::new(name, input_value);
|
||||
message = message.with_tool_request(id, Ok(tool_call));
|
||||
} else if tool_name.is_some() {
|
||||
let tool_call = CallToolRequestParam {
|
||||
name: name.into(),
|
||||
arguments: Some(object(input_value)),
|
||||
};
|
||||
message = message.with_tool_request(&id, Ok(tool_call));
|
||||
} else {
|
||||
// Tool with no input - use empty object
|
||||
let tool_call = ToolCall::new(name, Value::Object(serde_json::Map::new()));
|
||||
message = message.with_tool_request(id, Ok(tool_call));
|
||||
let tool_call = CallToolRequestParam {
|
||||
name: name.into(),
|
||||
arguments: Some(object!({})),
|
||||
};
|
||||
message = message.with_tool_request(&id, Ok(tool_call));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,14 +244,18 @@ pub fn response_to_message(response: &Value) -> Result<Message> {
|
||||
let name = content
|
||||
.get("name")
|
||||
.and_then(|n| n.as_str())
|
||||
.ok_or_else(|| anyhow!("Missing tool_use name"))?;
|
||||
.ok_or_else(|| anyhow!("Missing tool_use name"))?
|
||||
.to_string();
|
||||
|
||||
let input = content
|
||||
.get("input")
|
||||
.ok_or_else(|| anyhow!("Missing tool input"))?
|
||||
.clone();
|
||||
|
||||
let tool_call = ToolCall::new(name, input);
|
||||
let tool_call = CallToolRequestParam {
|
||||
name: name.into(),
|
||||
arguments: Some(object(input)),
|
||||
};
|
||||
message = message.with_tool_request(id, Ok(tool_call));
|
||||
}
|
||||
Some("thinking") => {
|
||||
@@ -425,7 +435,7 @@ mod tests {
|
||||
if let MessageContent::ToolRequest(tool_request) = &message.content[0] {
|
||||
let tool_call = tool_request.tool_call.as_ref().unwrap();
|
||||
assert_eq!(tool_call.name, "calculator");
|
||||
assert_eq!(tool_call.arguments, json!({"expression": "2 + 2"}));
|
||||
assert_eq!(tool_call.arguments, Some(object!({"expression": "2 + 2"})));
|
||||
} else {
|
||||
panic!("Expected ToolRequest content");
|
||||
}
|
||||
@@ -536,7 +546,7 @@ data: {"id":"a9537c2c-2017-4906-9817-2456168d89fa","model":"claude-sonnet-4-2025
|
||||
if let MessageContent::ToolRequest(tool_request) = &message.content[1] {
|
||||
let tool_call = tool_request.tool_call.as_ref().unwrap();
|
||||
assert_eq!(tool_call.name, "get_stock_price");
|
||||
assert_eq!(tool_call.arguments, json!({"symbol": "NVDA"}));
|
||||
assert_eq!(tool_call.arguments, Some(object!({"symbol": "NVDA"})));
|
||||
assert_eq!(tool_request.id, "tooluse_FB_nOElDTAOKa-YnVWI5Uw");
|
||||
} else {
|
||||
panic!("Expected ToolRequest content second");
|
||||
@@ -679,10 +689,12 @@ data: {"id":"a9537c2c-2017-4906-9817-2456168d89fa","model":"claude-sonnet-4-2025
|
||||
#[test]
|
||||
fn test_message_formatting_skips_tool_requests() {
|
||||
use crate::conversation::message::Message;
|
||||
use mcp_core::tool::ToolCall;
|
||||
|
||||
// Create a conversation with text, tool requests, and tool responses
|
||||
let tool_call = ToolCall::new("calculator", json!({"expression": "2 + 2"}));
|
||||
let tool_call = CallToolRequestParam {
|
||||
name: "calculator".into(),
|
||||
arguments: Some(object!({"expression": "2 + 2"})),
|
||||
};
|
||||
|
||||
let messages = vec![
|
||||
Message::user().with_text("Calculate 2 + 2"),
|
||||
|
||||
@@ -38,9 +38,8 @@ use crate::conversation::Conversation;
|
||||
use crate::model::ModelConfig;
|
||||
use crate::providers::formats::openai::create_request;
|
||||
use anyhow::Result;
|
||||
use mcp_core::tool::ToolCall;
|
||||
use reqwest::Client;
|
||||
use rmcp::model::{RawContent, Tool};
|
||||
use rmcp::model::{object, CallToolRequestParam, RawContent, Tool};
|
||||
use serde_json::{json, Value};
|
||||
use std::ops::Deref;
|
||||
use std::time::Duration;
|
||||
@@ -60,7 +59,7 @@ pub trait ToolInterpreter {
|
||||
&self,
|
||||
content: &str,
|
||||
tools: &[Tool],
|
||||
) -> Result<Vec<ToolCall>, ProviderError>;
|
||||
) -> Result<Vec<CallToolRequestParam>, ProviderError>;
|
||||
}
|
||||
|
||||
/// Ollama-specific implementation of the ToolInterpreter trait
|
||||
@@ -198,7 +197,9 @@ impl OllamaInterpreter {
|
||||
Ok(response_json)
|
||||
}
|
||||
|
||||
fn process_interpreter_response(response: &Value) -> Result<Vec<ToolCall>, ProviderError> {
|
||||
fn process_interpreter_response(
|
||||
response: &Value,
|
||||
) -> Result<Vec<CallToolRequestParam>, ProviderError> {
|
||||
let mut tool_calls = Vec::new();
|
||||
tracing::info!(
|
||||
"Tool interpreter response is {}",
|
||||
@@ -219,12 +220,14 @@ impl OllamaInterpreter {
|
||||
&& item.get("name").is_some()
|
||||
&& item.get("arguments").is_some()
|
||||
{
|
||||
// Create ToolCall directly from the JSON data
|
||||
let name = item["name"].as_str().unwrap_or_default().to_string();
|
||||
let arguments = item["arguments"].clone();
|
||||
|
||||
// Add the tool call to our result vector
|
||||
tool_calls.push(ToolCall::new(name, arguments));
|
||||
tool_calls.push(CallToolRequestParam {
|
||||
name: name.into(),
|
||||
arguments: Some(object(arguments)),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,7 +245,7 @@ impl ToolInterpreter for OllamaInterpreter {
|
||||
&self,
|
||||
last_assistant_msg: &str,
|
||||
tools: &[Tool],
|
||||
) -> Result<Vec<ToolCall>, ProviderError> {
|
||||
) -> Result<Vec<CallToolRequestParam>, ProviderError> {
|
||||
if tools.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ use super::retry::ProviderRetry;
|
||||
use super::utils::map_http_error_to_provider_error;
|
||||
use crate::conversation::message::{Message, MessageContent};
|
||||
use crate::impl_provider_default;
|
||||
use crate::mcp_utils::ToolResult;
|
||||
use crate::model::ModelConfig;
|
||||
use mcp_core::{ToolCall, ToolResult};
|
||||
use rmcp::model::{Role, Tool};
|
||||
use rmcp::model::{object, CallToolRequestParam, Role, Tool};
|
||||
|
||||
// ---------- Capability Flags ----------
|
||||
#[derive(Debug)]
|
||||
@@ -339,12 +339,19 @@ impl Provider for VeniceProvider {
|
||||
.iter()
|
||||
.filter_map(|tr| {
|
||||
if let ToolResult::Ok(tool_call) = &tr.tool_call {
|
||||
// Safely convert arguments to a JSON string
|
||||
let args_str = tool_call
|
||||
.arguments
|
||||
.as_ref() // borrow the Option contents
|
||||
.map(|map| serde_json::to_string(map).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Log tool call details for debugging
|
||||
tracing::debug!(
|
||||
"Tool call conversion: id={}, name={}, args_len={}",
|
||||
tr.id,
|
||||
tool_call.name,
|
||||
tool_call.arguments.to_string().len()
|
||||
args_str.len()
|
||||
);
|
||||
|
||||
// Convert to Venice format
|
||||
@@ -353,7 +360,7 @@ impl Provider for VeniceProvider {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_call.name,
|
||||
"arguments": tool_call.arguments.to_string()
|
||||
"arguments": args_str
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
@@ -453,8 +460,10 @@ impl Provider for VeniceProvider {
|
||||
function["arguments"].clone()
|
||||
};
|
||||
|
||||
// Create a ToolCall using the function name and arguments
|
||||
let tool_call = ToolCall { name, arguments };
|
||||
let tool_call = CallToolRequestParam {
|
||||
name: name.into(),
|
||||
arguments: Some(object(arguments)),
|
||||
};
|
||||
|
||||
// Create a ToolRequest MessageContent
|
||||
let tool_request = MessageContent::tool_request(id, ToolResult::Ok(tool_call));
|
||||
|
||||
Reference in New Issue
Block a user