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 <hu.dingyang@zte.com.cn>
This commit is contained in:
jokemanfire 2025-05-12 13:52:57 +08:00 • committed by GitHub
commit 6de444c62f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 160 additions and 108 deletions

View file

@ -9,7 +9,7 @@ After configuring the config file, you can run the example:
```bash ```bash
./simple_chat --help # show help info ./simple_chat --help # show help info
./simple_chat config > config.toml # output default config to file ./simple_chat config > config.toml # output default config to file
./simple_chat chat --config my_config.toml # start chat with specified config ./simple_chat --config my_config.toml chat # 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 --model gpt-4o-mini chat # start chat with specified model
``` ```

View file

@ -95,33 +95,42 @@ async fn main() -> Result<()> {
.unwrap_or_else(|| "gpt-4o-mini".to_string()), .unwrap_or_else(|| "gpt-4o-mini".to_string()),
); );
// build system prompt let support_tool = config.support_tool.unwrap_or(true);
let mut 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(); // 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 // add tool info to system prompt
for tool in session.get_tools() { for tool in session.get_tools() {
system_prompt.push_str(&format!( system_prompt.push_str(&format!(
"\ntool name: {}\ndescription: {}\nparameters: {}\n", "\ntool name: {}\ndescription: {}\nparameters: {}\n",
tool.name(), tool.name(),
tool.description(), tool.description(),
serde_json::to_string_pretty(&tool.parameters()) serde_json::to_string_pretty(&tool.parameters())
.expect("failed to serialize 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: <tool name>\n\
Inputs: <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: <tool name>\n\
Inputs: <inputs>\n",
);
// add system prompt // add system prompt
session.add_system_prompt(system_prompt); session.add_system_prompt(system_prompt);
// start chat // start chat
session.chat().await?; session.chat(support_tool).await?;
} }
} }

View file

@ -4,10 +4,11 @@ use std::{
}; };
use anyhow::Result; use anyhow::Result;
use serde_json;
use crate::{ use crate::{
client::ChatClient, client::ChatClient,
model::{CompletionRequest, Message}, model::{CompletionRequest, Message, ToolFunction},
tool::{Tool as ToolTrait, ToolSet}, tool::{Tool as ToolTrait, ToolSet},
}; };
@ -36,7 +37,84 @@ impl ChatSession {
self.tool_set.tools() 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::<serde_json::Value>(&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::<serde_json::Value>(
&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"); println!("welcome to use simple chat client, use 'exit' to quit");
loop { loop {
@ -56,20 +134,23 @@ impl ChatSession {
} }
self.messages.push(Message::user(&input)); self.messages.push(Message::user(&input));
let tool_definitions = if support_tool {
// prepare tool list // prepare tool list
let tools = self.tool_set.tools(); let tools = self.tool_set.tools();
let tool_definitions = if !tools.is_empty() { if !tools.is_empty() {
Some( Some(
tools tools
.iter() .iter()
.map(|tool| crate::model::Tool { .map(|tool| crate::model::Tool {
name: tool.name(), name: tool.name(),
description: tool.description(), description: tool.description(),
parameters: tool.parameters(), parameters: tool.parameters(),
}) })
.collect(), .collect(),
) )
} else {
None
}
} else { } else {
None None
}; };
@ -84,65 +165,11 @@ impl ChatSession {
// send request // send request
let response = self.client.complete(request).await?; let response = self.client.complete(request).await?;
// get choice
if let Some(choice) = response.choices.first() { let choice = response.choices.first().unwrap();
println!("AI: {}", choice.message.content); println!("AI > {}", choice.message.content);
self.messages.push(choice.message.clone()); // analyze tool call
self.analyze_tool_call(&choice.message).await;
// 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);
}
}
}
}
} }
Ok(()) Ok(())

View file

@ -58,8 +58,11 @@ impl ChatClient for OpenAIClient {
println!("API error: {}", error_text); println!("API error: {}", error_text);
return Err(anyhow::anyhow!("API Error: {}", error_text)); return Err(anyhow::anyhow!("API Error: {}", error_text));
} }
let text_data = response.text().await?;
let completion: CompletionResponse = response.json().await?; println!("Received response: {}", text_data);
let completion: CompletionResponse = serde_json::from_str(&text_data)
.map_err(anyhow::Error::from)
.unwrap();
Ok(completion) Ok(completion)
} }
} }

View file

@ -11,6 +11,7 @@ pub struct Config {
pub mcp: Option<McpConfig>, pub mcp: Option<McpConfig>,
pub model_name: Option<String>, pub model_name: Option<String>,
pub proxy: Option<bool>, pub proxy: Option<bool>,
pub support_tool: Option<bool>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]

View file

@ -2,6 +2,7 @@ openai_key = "key"
chat_url = "url" chat_url = "url"
model_name = "model_name" model_name = "model_name"
proxy = false proxy = false
support_tool = true # if support tool call
[mcp] [mcp]
[[mcp.server]] [[mcp.server]]

View file

@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
pub struct Message { pub struct Message {
pub role: String, pub role: String,
pub content: String, pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
} }
impl Message { impl Message {
@ -11,6 +13,7 @@ impl Message {
Self { Self {
role: "system".to_string(), role: "system".to_string(),
content: content.to_string(), content: content.to_string(),
tool_calls: None,
} }
} }
@ -18,6 +21,7 @@ impl Message {
Self { Self {
role: "user".to_string(), role: "user".to_string(),
content: content.to_string(), content: content.to_string(),
tool_calls: None,
} }
} }
@ -25,6 +29,7 @@ impl Message {
Self { Self {
role: "assistant".to_string(), role: "assistant".to_string(),
content: content.to_string(), content: content.to_string(),
tool_calls: None,
} }
} }
} }
@ -62,10 +67,17 @@ pub struct Choice {
pub finish_reason: String, pub finish_reason: String,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ToolCall { 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 name: String,
pub arguments: serde_json::Value, pub arguments: String,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]

View file

@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::Arc};
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use rmcp::{ use rmcp::{
model::{CallToolRequestParam, Tool as McpTool}, model::{CallToolRequestParam, CallToolResult, Tool as McpTool},
service::ServerSink, service::ServerSink,
}; };
use serde_json::Value; use serde_json::Value;
@ -18,7 +18,7 @@ pub trait Tool: Send + Sync {
fn name(&self) -> String; fn name(&self) -> String;
fn description(&self) -> String; fn description(&self) -> String;
fn parameters(&self) -> Value; fn parameters(&self) -> Value;
async fn call(&self, args: Value) -> Result<String>; async fn call(&self, args: Value) -> Result<CallToolResult>;
} }
pub struct McpToolAdapter { pub struct McpToolAdapter {
@ -50,12 +50,12 @@ impl Tool for McpToolAdapter {
serde_json::to_value(&self.tool.input_schema).unwrap_or(serde_json::json!({})) serde_json::to_value(&self.tool.input_schema).unwrap_or(serde_json::json!({}))
} }
async fn call(&self, args: Value) -> Result<String> { async fn call(&self, args: Value) -> Result<CallToolResult> {
let arguments = match args { let arguments = match args {
Value::Object(map) => Some(map), Value::Object(map) => Some(map),
_ => None, _ => None,
}; };
println!("arguments: {:?}", arguments);
let call_result = self let call_result = self
.server .server
.call_tool(CallToolRequestParam { .call_tool(CallToolRequestParam {
@ -63,9 +63,8 @@ impl Tool for McpToolAdapter {
arguments, arguments,
}) })
.await?; .await?;
let result = serde_json::to_string(&call_result).unwrap();
Ok(result) Ok(call_result)
} }
} }
#[derive(Default)] #[derive(Default)]