From 6de444c62fad7fe66d2696b07db8049d52b23e62 Mon Sep 17 00:00:00 2001 From: jokemanfire Date: Mon, 12 May 2025 13:52:57 +0800 Subject: [PATCH] feat(examples): Add func call support (#168) Some works about simple-chat. 1. fix some bugs. 2. add func call optional. Signed-off-by: jokemanfire --- examples/simple-chat-client/README.md | 4 +- .../simple-chat-client/src/bin/simple_chat.rs | 49 +++-- examples/simple-chat-client/src/chat.rs | 177 ++++++++++-------- examples/simple-chat-client/src/client.rs | 7 +- examples/simple-chat-client/src/config.rs | 1 + examples/simple-chat-client/src/config.toml | 1 + examples/simple-chat-client/src/model.rs | 16 +- examples/simple-chat-client/src/tool.rs | 11 +- 8 files changed, 159 insertions(+), 107 deletions(-) diff --git a/examples/simple-chat-client/README.md b/examples/simple-chat-client/README.md index eeca532..aecaf6c 100644 --- a/examples/simple-chat-client/README.md +++ b/examples/simple-chat-client/README.md @@ -9,7 +9,7 @@ After configuring the config file, you can run the example: ```bash ./simple_chat --help # show help info ./simple_chat config > config.toml # output default config to file -./simple_chat chat --config my_config.toml # start chat with specified config -./simple_chat chat --config my_config.toml --model gpt-4o-mini # start chat with specified model +./simple_chat --config my_config.toml chat # start chat with specified config +./simple_chat --config my_config.toml --model gpt-4o-mini chat # start chat with specified model ``` diff --git a/examples/simple-chat-client/src/bin/simple_chat.rs b/examples/simple-chat-client/src/bin/simple_chat.rs index d23a9ad..d094643 100644 --- a/examples/simple-chat-client/src/bin/simple_chat.rs +++ b/examples/simple-chat-client/src/bin/simple_chat.rs @@ -95,33 +95,42 @@ async fn main() -> Result<()> { .unwrap_or_else(|| "gpt-4o-mini".to_string()), ); - // build system prompt - let mut system_prompt = - "you are a assistant, you can help user to complete various tasks. you have the following tools to use:\n".to_string(); + let support_tool = config.support_tool.unwrap_or(true); + let mut system_prompt; + // if not support tool call, add tool call format guidance + if !support_tool { + // build system prompt + system_prompt = + "you are a assistant, you can help user to complete various tasks. you have the following tools to use:\n".to_string(); - // add tool info to system prompt - for tool in session.get_tools() { - system_prompt.push_str(&format!( - "\ntool name: {}\ndescription: {}\nparameters: {}\n", - tool.name(), - tool.description(), - serde_json::to_string_pretty(&tool.parameters()) - .expect("failed to serialize tool parameters") - )); + // add tool info to system prompt + for tool in session.get_tools() { + system_prompt.push_str(&format!( + "\ntool name: {}\ndescription: {}\nparameters: {}\n", + tool.name(), + tool.description(), + serde_json::to_string_pretty(&tool.parameters()) + .expect("failed to serialize tool parameters") + )); + } + + // add tool call format guidance + system_prompt.push_str( + "\nif you need to call tool, please use the following format:\n\ + Tool: \n\ + Inputs: \n", + ); + println!("system prompt: {}", system_prompt); + } else { + system_prompt = + "you are a assistant, you can help user to complete various tasks.".to_string(); } - // add tool call format guidance - system_prompt.push_str( - "\nif you need to call tool, please use the following format:\n\ - Tool: \n\ - Inputs: \n", - ); - // add system prompt session.add_system_prompt(system_prompt); // start chat - session.chat().await?; + session.chat(support_tool).await?; } } diff --git a/examples/simple-chat-client/src/chat.rs b/examples/simple-chat-client/src/chat.rs index bf2d1ff..221389e 100644 --- a/examples/simple-chat-client/src/chat.rs +++ b/examples/simple-chat-client/src/chat.rs @@ -4,10 +4,11 @@ use std::{ }; use anyhow::Result; +use serde_json; use crate::{ client::ChatClient, - model::{CompletionRequest, Message}, + model::{CompletionRequest, Message, ToolFunction}, tool::{Tool as ToolTrait, ToolSet}, }; @@ -36,7 +37,84 @@ impl ChatSession { self.tool_set.tools() } - pub async fn chat(&mut self) -> Result<()> { + pub async fn analyze_tool_call(&mut self, response: &Message) { + let mut tool_calls_func = Vec::new(); + if let Some(tool_calls) = response.tool_calls.as_ref() { + for tool_call in tool_calls { + if tool_call._type == "function" { + tool_calls_func.push(tool_call.function.clone()); + } + } + } else { + // check if message contains tool call + if response.content.contains("Tool:") { + let lines: Vec<&str> = response.content.split('\n').collect(); + // simple parse tool call + let mut tool_name = None; + let mut args_text = Vec::new(); + let mut parsing_args = false; + + for line in lines { + if line.starts_with("Tool:") { + tool_name = line.strip_prefix("Tool:").map(|s| s.trim().to_string()); + parsing_args = false; + } else if line.starts_with("Inputs:") { + parsing_args = true; + } else if parsing_args { + args_text.push(line.trim()); + } + } + if let Some(name) = tool_name { + tool_calls_func.push(ToolFunction { + name, + arguments: args_text.join("\n"), + }); + } + } + } + // call tool + for tool_call in tool_calls_func { + println!("tool call: {:?}", tool_call); + let tool = self.tool_set.get_tool(&tool_call.name); + if let Some(tool) = tool { + // call tool + let args = serde_json::from_str::(&tool_call.arguments) + .unwrap_or_default(); + match tool.call(args).await { + Ok(result) => { + if result.is_error.is_some_and(|b| b) { + self.messages + .push(Message::user("tool call failed, mcp call error")); + } else { + result.content.iter().for_each(|content| { + if let Some(content_text) = content.as_text() { + let json_result = serde_json::from_str::( + &content_text.text, + ) + .unwrap_or_default(); + let pretty_result = + serde_json::to_string_pretty(&json_result).unwrap(); + println!("call tool result: {}", pretty_result); + self.messages.push(Message::user(format!( + "call tool result: {}", + pretty_result + ))); + } + }); + } + } + Err(e) => { + println!("tool call failed: {}", e); + self.messages + .push(Message::user(format!("tool call failed: {}", e))); + } + } + } else { + println!("tool not found: {}", tool_call.name); + } + } + } + pub async fn chat(&mut self, support_tool: bool) -> Result<()> { println!("welcome to use simple chat client, use 'exit' to quit"); loop { @@ -56,20 +134,23 @@ impl ChatSession { } self.messages.push(Message::user(&input)); - - // prepare tool list - let tools = self.tool_set.tools(); - let tool_definitions = if !tools.is_empty() { - Some( - tools - .iter() - .map(|tool| crate::model::Tool { - name: tool.name(), - description: tool.description(), - parameters: tool.parameters(), - }) - .collect(), - ) + let tool_definitions = if support_tool { + // prepare tool list + let tools = self.tool_set.tools(); + if !tools.is_empty() { + Some( + tools + .iter() + .map(|tool| crate::model::Tool { + name: tool.name(), + description: tool.description(), + parameters: tool.parameters(), + }) + .collect(), + ) + } else { + None + } } else { None }; @@ -84,65 +165,11 @@ impl ChatSession { // send request let response = self.client.complete(request).await?; - - if let Some(choice) = response.choices.first() { - println!("AI: {}", choice.message.content); - self.messages.push(choice.message.clone()); - - // check if message contains tool call - if choice.message.content.contains("Tool:") { - let lines: Vec<&str> = choice.message.content.split('\n').collect(); - - // simple parse tool call - let mut tool_name = None; - let mut args_text = Vec::new(); - let mut parsing_args = false; - - for line in lines { - if line.starts_with("Tool:") { - tool_name = line.strip_prefix("Tool:").map(|s| s.trim().to_string()); - parsing_args = false; - } else if line.starts_with("Inputs:") { - parsing_args = true; - } else if parsing_args { - args_text.push(line.trim()); - } - } - - if let Some(name) = tool_name { - if let Some(tool) = self.tool_set.get_tool(&name) { - println!("calling tool: {}", name); - - // simple handle args - let args_str = args_text.join("\n"); - let args = match serde_json::from_str(&args_str) { - Ok(v) => v, - Err(_) => { - // try to handle args as string - serde_json::Value::String(args_str) - } - }; - - // call tool - match tool.call(args).await { - Ok(result) => { - println!("tool result: {}", result); - - // add tool result to dialog - self.messages.push(Message::user(result)); - } - Err(e) => { - println!("tool call failed: {}", e); - self.messages - .push(Message::user(format!("tool call failed: {}", e))); - } - } - } else { - println!("tool not found: {}", name); - } - } - } - } + // get choice + let choice = response.choices.first().unwrap(); + println!("AI > {}", choice.message.content); + // analyze tool call + self.analyze_tool_call(&choice.message).await; } Ok(()) diff --git a/examples/simple-chat-client/src/client.rs b/examples/simple-chat-client/src/client.rs index 7b66f16..815033a 100644 --- a/examples/simple-chat-client/src/client.rs +++ b/examples/simple-chat-client/src/client.rs @@ -58,8 +58,11 @@ impl ChatClient for OpenAIClient { println!("API error: {}", error_text); return Err(anyhow::anyhow!("API Error: {}", error_text)); } - - let completion: CompletionResponse = response.json().await?; + let text_data = response.text().await?; + println!("Received response: {}", text_data); + let completion: CompletionResponse = serde_json::from_str(&text_data) + .map_err(anyhow::Error::from) + .unwrap(); Ok(completion) } } diff --git a/examples/simple-chat-client/src/config.rs b/examples/simple-chat-client/src/config.rs index e84d5b0..fccca80 100644 --- a/examples/simple-chat-client/src/config.rs +++ b/examples/simple-chat-client/src/config.rs @@ -11,6 +11,7 @@ pub struct Config { pub mcp: Option, pub model_name: Option, pub proxy: Option, + pub support_tool: Option, } #[derive(Debug, Serialize, Deserialize)] diff --git a/examples/simple-chat-client/src/config.toml b/examples/simple-chat-client/src/config.toml index 12ee84c..3542231 100644 --- a/examples/simple-chat-client/src/config.toml +++ b/examples/simple-chat-client/src/config.toml @@ -2,6 +2,7 @@ openai_key = "key" chat_url = "url" model_name = "model_name" proxy = false +support_tool = true # if support tool call [mcp] [[mcp.server]] diff --git a/examples/simple-chat-client/src/model.rs b/examples/simple-chat-client/src/model.rs index 4e9aeb6..290b059 100644 --- a/examples/simple-chat-client/src/model.rs +++ b/examples/simple-chat-client/src/model.rs @@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize}; pub struct Message { pub role: String, pub content: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_calls: Option>, } impl Message { @@ -11,6 +13,7 @@ impl Message { Self { role: "system".to_string(), content: content.to_string(), + tool_calls: None, } } @@ -18,6 +21,7 @@ impl Message { Self { role: "user".to_string(), content: content.to_string(), + tool_calls: None, } } @@ -25,6 +29,7 @@ impl Message { Self { role: "assistant".to_string(), content: content.to_string(), + tool_calls: None, } } } @@ -62,10 +67,17 @@ pub struct Choice { pub finish_reason: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct ToolCall { + pub id: String, + #[serde(rename = "type")] + pub _type: String, + pub function: ToolFunction, +} +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ToolFunction { pub name: String, - pub arguments: serde_json::Value, + pub arguments: String, } #[derive(Debug, Serialize, Deserialize)] diff --git a/examples/simple-chat-client/src/tool.rs b/examples/simple-chat-client/src/tool.rs index 6f99936..8a12b8e 100644 --- a/examples/simple-chat-client/src/tool.rs +++ b/examples/simple-chat-client/src/tool.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::Arc}; use anyhow::Result; use async_trait::async_trait; use rmcp::{ - model::{CallToolRequestParam, Tool as McpTool}, + model::{CallToolRequestParam, CallToolResult, Tool as McpTool}, service::ServerSink, }; use serde_json::Value; @@ -18,7 +18,7 @@ pub trait Tool: Send + Sync { fn name(&self) -> String; fn description(&self) -> String; fn parameters(&self) -> Value; - async fn call(&self, args: Value) -> Result; + async fn call(&self, args: Value) -> Result; } pub struct McpToolAdapter { @@ -50,12 +50,12 @@ impl Tool for McpToolAdapter { serde_json::to_value(&self.tool.input_schema).unwrap_or(serde_json::json!({})) } - async fn call(&self, args: Value) -> Result { + async fn call(&self, args: Value) -> Result { let arguments = match args { Value::Object(map) => Some(map), _ => None, }; - + println!("arguments: {:?}", arguments); let call_result = self .server .call_tool(CallToolRequestParam { @@ -63,9 +63,8 @@ impl Tool for McpToolAdapter { arguments, }) .await?; - let result = serde_json::to_string(&call_result).unwrap(); - Ok(result) + Ok(call_result) } } #[derive(Default)]