draft: use rust messages in typescript (#1393)
This commit is contained in:
@@ -10,9 +10,8 @@ use bytes::Bytes;
|
||||
use futures::{stream::StreamExt, Stream};
|
||||
use goose::message::{Message, MessageContent};
|
||||
|
||||
use mcp_core::{content::Content, role::Role};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use mcp_core::role::Role;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
pin::Pin,
|
||||
@@ -23,33 +22,13 @@ use tokio::sync::mpsc;
|
||||
use tokio::time::timeout;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
|
||||
// Types matching the incoming JSON structure
|
||||
// Direct message serialization for the chat request
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ChatRequest {
|
||||
messages: Vec<IncomingMessage>,
|
||||
messages: Vec<Message>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct IncomingMessage {
|
||||
role: String,
|
||||
content: String,
|
||||
#[serde(default)]
|
||||
#[serde(rename = "toolInvocations")]
|
||||
tool_invocations: Vec<ToolInvocation>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ToolInvocation {
|
||||
state: String,
|
||||
#[serde(rename = "toolCallId")]
|
||||
tool_call_id: String,
|
||||
#[serde(rename = "toolName")]
|
||||
tool_name: String,
|
||||
args: Value,
|
||||
result: Option<Vec<Content>>,
|
||||
}
|
||||
|
||||
// Custom SSE response type that implements the Vercel AI SDK protocol
|
||||
// Custom SSE response type for streaming messages
|
||||
pub struct SseResponse {
|
||||
rx: ReceiverStream<String>,
|
||||
}
|
||||
@@ -79,188 +58,32 @@ impl IntoResponse for SseResponse {
|
||||
.header("Content-Type", "text/event-stream")
|
||||
.header("Cache-Control", "no-cache")
|
||||
.header("Connection", "keep-alive")
|
||||
.header("x-vercel-ai-data-stream", "v1")
|
||||
.body(body)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
// Convert incoming messages to our internal Message type
|
||||
fn convert_messages(incoming: Vec<IncomingMessage>) -> Vec<Message> {
|
||||
let mut messages = Vec::new();
|
||||
|
||||
for msg in incoming {
|
||||
match msg.role.as_str() {
|
||||
"user" => {
|
||||
messages.push(Message::user().with_text(msg.content));
|
||||
}
|
||||
"assistant" => {
|
||||
// First handle any tool invocations - each represents a complete request/response cycle
|
||||
for tool in msg.tool_invocations {
|
||||
if tool.state == "result" {
|
||||
// Add the original tool request from assistant
|
||||
let tool_call = mcp_core::tool::ToolCall {
|
||||
name: tool.tool_name,
|
||||
arguments: tool.args,
|
||||
};
|
||||
messages.push(
|
||||
Message::assistant()
|
||||
.with_tool_request(tool.tool_call_id.clone(), Ok(tool_call)),
|
||||
);
|
||||
|
||||
// Add the tool response from user
|
||||
if let Some(result) = &tool.result {
|
||||
messages.push(
|
||||
Message::user()
|
||||
.with_tool_response(tool.tool_call_id, Ok(result.clone())),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then add the assistant's text response after tool interactions
|
||||
if !msg.content.is_empty() {
|
||||
messages.push(Message::assistant().with_text(msg.content));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("Unknown role: {}", msg.role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messages
|
||||
// Message event types for SSE streaming
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(tag = "type")]
|
||||
enum MessageEvent {
|
||||
Message { message: Message },
|
||||
Error { error: String },
|
||||
Finish { reason: String },
|
||||
}
|
||||
|
||||
// Protocol-specific message formatting
|
||||
struct ProtocolFormatter;
|
||||
|
||||
impl ProtocolFormatter {
|
||||
fn format_text(text: &str) -> String {
|
||||
let encoded_text = serde_json::to_string(text).unwrap_or_else(|_| String::new());
|
||||
format!("0:{}\n", encoded_text)
|
||||
}
|
||||
|
||||
fn format_tool_call(id: &str, name: &str, args: &Value) -> String {
|
||||
// Tool calls start with "9:"
|
||||
let tool_call = json!({
|
||||
"toolCallId": id,
|
||||
"toolName": name,
|
||||
"args": args
|
||||
});
|
||||
format!("9:{}\n", tool_call)
|
||||
}
|
||||
|
||||
fn format_tool_response(id: &str, result: &Vec<Content>) -> String {
|
||||
// Tool responses start with "a:"
|
||||
let response = json!({
|
||||
"toolCallId": id,
|
||||
"result": result,
|
||||
});
|
||||
format!("a:{}\n", response)
|
||||
}
|
||||
|
||||
fn format_error(error: &str) -> String {
|
||||
// Error messages start with "3:" in the new protocol.
|
||||
let encoded_error = serde_json::to_string(error).unwrap_or_else(|_| String::new());
|
||||
format!("3:{}\n", encoded_error)
|
||||
}
|
||||
|
||||
fn format_finish(reason: &str) -> String {
|
||||
// Finish messages start with "d:"
|
||||
let finish = json!({
|
||||
"finishReason": reason,
|
||||
"usage": {
|
||||
"promptTokens": 0,
|
||||
"completionTokens": 0
|
||||
}
|
||||
});
|
||||
format!("d:{}\n", finish)
|
||||
}
|
||||
}
|
||||
|
||||
async fn stream_message(
|
||||
message: Message,
|
||||
// Stream a message as an SSE event
|
||||
async fn stream_event(
|
||||
event: MessageEvent,
|
||||
tx: &mpsc::Sender<String>,
|
||||
) -> Result<(), mpsc::error::SendError<String>> {
|
||||
match message.role {
|
||||
Role::User => {
|
||||
// Handle tool responses
|
||||
for content in message.content {
|
||||
// I believe with the protocol we aren't intended to pass back user messages, so we only deal with
|
||||
// the tool responses here
|
||||
if let MessageContent::ToolResponse(response) = content {
|
||||
// We should return a result for either an error or a success
|
||||
match response.tool_result {
|
||||
Ok(result) => {
|
||||
tx.send(ProtocolFormatter::format_tool_response(
|
||||
&response.id,
|
||||
&result,
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
Err(err) => {
|
||||
// Send an error message first
|
||||
tx.send(ProtocolFormatter::format_error(&err.to_string()))
|
||||
.await?;
|
||||
// Then send an empty tool response to maintain the protocol
|
||||
let result =
|
||||
vec![Content::text(format!("Error: {}", err)).with_priority(0.0)];
|
||||
tx.send(ProtocolFormatter::format_tool_response(
|
||||
&response.id,
|
||||
&result,
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Role::Assistant => {
|
||||
for content in message.content {
|
||||
match content {
|
||||
MessageContent::ToolRequest(request) => {
|
||||
match request.tool_call {
|
||||
Ok(tool_call) => {
|
||||
tx.send(ProtocolFormatter::format_tool_call(
|
||||
&request.id,
|
||||
&tool_call.name,
|
||||
&tool_call.arguments,
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
Err(err) => {
|
||||
// Send a placeholder tool call to maintain protocol
|
||||
tx.send(ProtocolFormatter::format_tool_call(
|
||||
&request.id,
|
||||
"invalid_tool",
|
||||
&json!({"error": err.to_string()}),
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
MessageContent::Text(text) => {
|
||||
for line in text.text.lines() {
|
||||
let modified_line = format!("{}\n", line);
|
||||
tx.send(ProtocolFormatter::format_text(&modified_line))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
MessageContent::ToolConfirmationRequest(_) => {
|
||||
// skip tool confirmation requests
|
||||
}
|
||||
MessageContent::Image(_) => {
|
||||
// skip images
|
||||
}
|
||||
MessageContent::ToolResponse(_) => {
|
||||
// skip tool responses
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
let json = serde_json::to_string(&event).unwrap_or_else(|e| {
|
||||
format!(
|
||||
r#"{{"type":"Error","error":"Failed to serialize event: {}"}}"#,
|
||||
e
|
||||
)
|
||||
});
|
||||
tx.send(format!("data: {}\n\n", json)).await
|
||||
}
|
||||
|
||||
async fn handler(
|
||||
@@ -278,19 +101,12 @@ async fn handler(
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// Check protocol header (optional in our case)
|
||||
if let Some(protocol) = headers.get("x-protocol") {
|
||||
if protocol.to_str().map(|p| p != "data").unwrap_or(true) {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
// Create channel for streaming
|
||||
let (tx, rx) = mpsc::channel(100);
|
||||
let stream = ReceiverStream::new(rx);
|
||||
|
||||
// Convert incoming messages
|
||||
let messages = convert_messages(request.messages);
|
||||
// Get messages directly from the request
|
||||
let messages = request.messages;
|
||||
|
||||
// Get a lock on the shared agent
|
||||
let agent = state.agent.clone();
|
||||
@@ -301,10 +117,20 @@ async fn handler(
|
||||
let agent = match agent.as_ref() {
|
||||
Some(agent) => agent,
|
||||
None => {
|
||||
let _ = tx
|
||||
.send(ProtocolFormatter::format_error("No agent configured"))
|
||||
.await;
|
||||
let _ = tx.send(ProtocolFormatter::format_finish("error")).await;
|
||||
let _ = stream_event(
|
||||
MessageEvent::Error {
|
||||
error: "No agent configured".to_string(),
|
||||
},
|
||||
&tx,
|
||||
)
|
||||
.await;
|
||||
let _ = stream_event(
|
||||
MessageEvent::Finish {
|
||||
reason: "error".to_string(),
|
||||
},
|
||||
&tx,
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -313,10 +139,20 @@ async fn handler(
|
||||
Ok(stream) => stream,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to start reply stream: {:?}", e);
|
||||
let _ = tx
|
||||
.send(ProtocolFormatter::format_error(&e.to_string()))
|
||||
.await;
|
||||
let _ = tx.send(ProtocolFormatter::format_finish("error")).await;
|
||||
let _ = stream_event(
|
||||
MessageEvent::Error {
|
||||
error: e.to_string(),
|
||||
},
|
||||
&tx,
|
||||
)
|
||||
.await;
|
||||
let _ = stream_event(
|
||||
MessageEvent::Finish {
|
||||
reason: "error".to_string(),
|
||||
},
|
||||
&tx,
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -326,25 +162,32 @@ async fn handler(
|
||||
response = timeout(Duration::from_millis(500), stream.next()) => {
|
||||
match response {
|
||||
Ok(Some(Ok(message))) => {
|
||||
if let Err(e) = stream_message(message, &tx).await {
|
||||
if let Err(e) = stream_event(MessageEvent::Message { message }, &tx).await {
|
||||
tracing::error!("Error sending message through channel: {}", e);
|
||||
let _ = tx.send(ProtocolFormatter::format_error(&e.to_string())).await;
|
||||
let _ = stream_event(
|
||||
MessageEvent::Error {
|
||||
error: e.to_string(),
|
||||
},
|
||||
&tx,
|
||||
).await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(Some(Err(e))) => {
|
||||
tracing::error!("Error processing message: {}", e);
|
||||
let _ = tx.send(ProtocolFormatter::format_error(&e.to_string())).await;
|
||||
let _ = stream_event(
|
||||
MessageEvent::Error {
|
||||
error: e.to_string(),
|
||||
},
|
||||
&tx,
|
||||
).await;
|
||||
break;
|
||||
}
|
||||
Ok(None) => {
|
||||
break;
|
||||
}
|
||||
Err(_) => { // Heartbeat, used to detect disconnected clients and then end running tools.
|
||||
Err(_) => { // Heartbeat, used to detect disconnected clients
|
||||
if tx.is_closed() {
|
||||
// Kill any running processes when the client disconnects
|
||||
// TODO is this used? I suspect post MCP this is on the server instead
|
||||
// goose::process_store::kill_processes();
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
@@ -354,24 +197,30 @@ async fn handler(
|
||||
}
|
||||
}
|
||||
|
||||
// Send finish message
|
||||
let _ = tx.send(ProtocolFormatter::format_finish("stop")).await;
|
||||
// Send finish event
|
||||
let _ = stream_event(
|
||||
MessageEvent::Finish {
|
||||
reason: "stop".to_string(),
|
||||
},
|
||||
&tx,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
Ok(SseResponse::new(stream))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, serde::Serialize)]
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct AskRequest {
|
||||
prompt: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
#[derive(Debug, Serialize)]
|
||||
struct AskResponse {
|
||||
response: String,
|
||||
}
|
||||
|
||||
// simple ask an AI for a response, non streaming
|
||||
// Simple ask an AI for a response, non streaming
|
||||
async fn ask_handler(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
@@ -478,85 +327,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_messages_user_only() {
|
||||
let incoming = vec![IncomingMessage {
|
||||
role: "user".to_string(),
|
||||
content: "Hello".to_string(),
|
||||
tool_invocations: vec![],
|
||||
}];
|
||||
|
||||
let messages = convert_messages(incoming);
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert_eq!(messages[0].role, Role::User);
|
||||
assert!(
|
||||
matches!(&messages[0].content[0], MessageContent::Text(text) if text.text == "Hello")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_messages_with_tool_invocation() {
|
||||
let tool_result = vec![Content::text("tool response").with_priority(0.0)];
|
||||
let incoming = vec![IncomingMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: "".to_string(),
|
||||
tool_invocations: vec![ToolInvocation {
|
||||
state: "result".to_string(),
|
||||
tool_call_id: "123".to_string(),
|
||||
tool_name: "test_tool".to_string(),
|
||||
args: json!({"key": "value"}),
|
||||
result: Some(tool_result.clone()),
|
||||
}],
|
||||
}];
|
||||
|
||||
let messages = convert_messages(incoming);
|
||||
assert_eq!(messages.len(), 2); // Tool request and response
|
||||
|
||||
// Check tool request
|
||||
assert_eq!(messages[0].role, Role::Assistant);
|
||||
assert!(
|
||||
matches!(&messages[0].content[0], MessageContent::ToolRequest(req) if req.id == "123")
|
||||
);
|
||||
|
||||
// Check tool response
|
||||
assert_eq!(messages[1].role, Role::User);
|
||||
assert!(
|
||||
matches!(&messages[1].content[0], MessageContent::ToolResponse(resp) if resp.id == "123")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_protocol_formatter() {
|
||||
// Test text formatting
|
||||
let text = "Hello world";
|
||||
let formatted = ProtocolFormatter::format_text(text);
|
||||
assert_eq!(formatted, "0:\"Hello world\"\n");
|
||||
|
||||
// Test tool call formatting
|
||||
let formatted =
|
||||
ProtocolFormatter::format_tool_call("123", "test_tool", &json!({"key": "value"}));
|
||||
assert!(formatted.starts_with("9:"));
|
||||
assert!(formatted.contains("\"toolCallId\":\"123\""));
|
||||
assert!(formatted.contains("\"toolName\":\"test_tool\""));
|
||||
|
||||
// Test tool response formatting
|
||||
let result = vec![Content::text("response").with_priority(0.0)];
|
||||
let formatted = ProtocolFormatter::format_tool_response("123", &result);
|
||||
assert!(formatted.starts_with("a:"));
|
||||
assert!(formatted.contains("\"toolCallId\":\"123\""));
|
||||
|
||||
// Test error formatting
|
||||
let formatted = ProtocolFormatter::format_error("Test error");
|
||||
println!("Formatted error: {}", formatted);
|
||||
assert!(formatted.starts_with("3:"));
|
||||
assert!(formatted.contains("Test error"));
|
||||
|
||||
// Test finish formatting
|
||||
let formatted = ProtocolFormatter::format_finish("stop");
|
||||
assert!(formatted.starts_with("d:"));
|
||||
assert!(formatted.contains("\"finishReason\":\"stop\""));
|
||||
}
|
||||
|
||||
mod integration_tests {
|
||||
use super::*;
|
||||
use axum::{body::Body, http::Request};
|
||||
@@ -575,7 +345,7 @@ mod tests {
|
||||
});
|
||||
let agent = AgentFactory::create("reference", mock_provider).unwrap();
|
||||
let state = AppState {
|
||||
config: Arc::new(Mutex::new(HashMap::new())), // Add this line
|
||||
config: Arc::new(Mutex::new(HashMap::new())),
|
||||
agent: Arc::new(Mutex::new(Some(agent))),
|
||||
secret_key: "test-secret".to_string(),
|
||||
};
|
||||
|
||||
@@ -14,19 +14,26 @@ use mcp_core::role::Role;
|
||||
use mcp_core::tool::ToolCall;
|
||||
use serde_json::Value;
|
||||
|
||||
mod tool_result_serde;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolRequest {
|
||||
pub id: String,
|
||||
#[serde(with = "tool_result_serde")]
|
||||
pub tool_call: ToolResult<ToolCall>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolResponse {
|
||||
pub id: String,
|
||||
#[serde(with = "tool_result_serde")]
|
||||
pub tool_result: ToolResult<Vec<Content>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolConfirmationRequest {
|
||||
pub id: String,
|
||||
pub tool_name: String,
|
||||
@@ -36,6 +43,7 @@ pub struct ToolConfirmationRequest {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
/// Content passed inside a message, which can be both simple content and tool content
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum MessageContent {
|
||||
Text(TextContent),
|
||||
Image(ImageContent),
|
||||
@@ -150,6 +158,7 @@ impl From<Content> for MessageContent {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
/// A message to or from an LLM
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Message {
|
||||
pub role: Role,
|
||||
pub created: i64,
|
||||
@@ -292,3 +301,123 @@ impl Message {
|
||||
.all(|c| matches!(c, MessageContent::Text(_)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mcp_core::handler::ToolError;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
#[test]
|
||||
fn test_message_serialization() {
|
||||
let message = Message::assistant()
|
||||
.with_text("Hello, I'll help you with that.")
|
||||
.with_tool_request(
|
||||
"tool123",
|
||||
Ok(ToolCall::new("test_tool", json!({"param": "value"}))),
|
||||
);
|
||||
|
||||
let json_str = serde_json::to_string_pretty(&message).unwrap();
|
||||
println!("Serialized message: {}", json_str);
|
||||
|
||||
// Parse back to Value to check structure
|
||||
let value: Value = serde_json::from_str(&json_str).unwrap();
|
||||
|
||||
// Check top-level fields
|
||||
assert_eq!(value["role"], "assistant");
|
||||
assert!(value["created"].is_i64());
|
||||
assert!(value["content"].is_array());
|
||||
|
||||
// Check content items
|
||||
let content = &value["content"];
|
||||
|
||||
// First item should be text
|
||||
assert_eq!(content[0]["type"], "text");
|
||||
assert_eq!(content[0]["text"], "Hello, I'll help you with that.");
|
||||
|
||||
// Second item should be toolRequest
|
||||
assert_eq!(content[1]["type"], "toolRequest");
|
||||
assert_eq!(content[1]["id"], "tool123");
|
||||
|
||||
// Check tool_call serialization
|
||||
assert_eq!(content[1]["toolCall"]["status"], "success");
|
||||
assert_eq!(content[1]["toolCall"]["value"]["name"], "test_tool");
|
||||
assert_eq!(
|
||||
content[1]["toolCall"]["value"]["arguments"]["param"],
|
||||
"value"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_serialization() {
|
||||
let message = Message::assistant().with_tool_request(
|
||||
"tool123",
|
||||
Err(ToolError::ExecutionError(
|
||||
"Something went wrong".to_string(),
|
||||
)),
|
||||
);
|
||||
|
||||
let json_str = serde_json::to_string_pretty(&message).unwrap();
|
||||
println!("Serialized error: {}", json_str);
|
||||
|
||||
// Parse back to Value to check structure
|
||||
let value: Value = serde_json::from_str(&json_str).unwrap();
|
||||
|
||||
// Check tool_call serialization with error
|
||||
let tool_call = &value["content"][0]["toolCall"];
|
||||
assert_eq!(tool_call["status"], "error");
|
||||
assert_eq!(tool_call["error"], "Execution failed: Something went wrong");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialization() {
|
||||
// Create a JSON string with our new format
|
||||
let json_str = r#"{
|
||||
"role": "assistant",
|
||||
"created": 1740171566,
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "I'll help you with that."
|
||||
},
|
||||
{
|
||||
"type": "toolRequest",
|
||||
"id": "tool123",
|
||||
"toolCall": {
|
||||
"status": "success",
|
||||
"value": {
|
||||
"name": "test_tool",
|
||||
"arguments": {"param": "value"}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
|
||||
let message: Message = serde_json::from_str(json_str).unwrap();
|
||||
|
||||
assert_eq!(message.role, Role::Assistant);
|
||||
assert_eq!(message.created, 1740171566);
|
||||
assert_eq!(message.content.len(), 2);
|
||||
|
||||
// Check first content item
|
||||
if let MessageContent::Text(text) = &message.content[0] {
|
||||
assert_eq!(text.text, "I'll help you with that.");
|
||||
} else {
|
||||
panic!("Expected Text content");
|
||||
}
|
||||
|
||||
// Check second content item
|
||||
if let MessageContent::ToolRequest(req) = &message.content[1] {
|
||||
assert_eq!(req.id, "tool123");
|
||||
if let Ok(tool_call) = &req.tool_call {
|
||||
assert_eq!(tool_call.name, "test_tool");
|
||||
assert_eq!(tool_call.arguments, json!({"param": "value"}));
|
||||
} else {
|
||||
panic!("Expected successful tool call");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected ToolRequest content");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
use mcp_core::handler::{ToolError, ToolResult};
|
||||
use serde::ser::SerializeStruct;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
pub fn serialize<T, S>(value: &ToolResult<T>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
T: Serialize,
|
||||
S: Serializer,
|
||||
{
|
||||
match value {
|
||||
Ok(val) => {
|
||||
let mut state = serializer.serialize_struct("ToolResult", 2)?;
|
||||
state.serialize_field("status", "success")?;
|
||||
state.serialize_field("value", val)?;
|
||||
state.end()
|
||||
}
|
||||
Err(err) => {
|
||||
let mut state = serializer.serialize_struct("ToolResult", 2)?;
|
||||
state.serialize_field("status", "error")?;
|
||||
state.serialize_field("error", &err.to_string())?;
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For deserialization, let's use a simpler approach that works with the format we're serializing to
|
||||
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<ToolResult<T>, D::Error>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
// Define a helper enum to handle the two possible formats
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ResultFormat<T> {
|
||||
Success { status: String, value: T },
|
||||
Error { status: String, error: String },
|
||||
}
|
||||
|
||||
let format = ResultFormat::deserialize(deserializer)?;
|
||||
|
||||
match format {
|
||||
ResultFormat::Success { status, value } => {
|
||||
if status == "success" {
|
||||
Ok(Ok(value))
|
||||
} else {
|
||||
Err(serde::de::Error::custom(format!(
|
||||
"Expected status 'success', got '{}'",
|
||||
status
|
||||
)))
|
||||
}
|
||||
}
|
||||
ResultFormat::Error { status, error } => {
|
||||
if status == "error" {
|
||||
Ok(Err(ToolError::ExecutionError(error)))
|
||||
} else {
|
||||
Err(serde::de::Error::custom(format!(
|
||||
"Expected status 'error', got '{}'",
|
||||
status
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user