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
parent 787cc015b7
commit 6de444c62f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 159 additions and 107 deletions

View file

@ -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
```

View file

@ -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: <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
session.add_system_prompt(system_prompt);
// start chat
session.chat().await?;
session.chat(support_tool).await?;
}
}

View file

@ -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::<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");
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(())

View file

@ -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)
}
}

View file

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

View file

@ -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]]

View file

@ -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<Vec<ToolCall>>,
}
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)]

View file

@ -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<String>;
async fn call(&self, args: Value) -> Result<CallToolResult>;
}
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<String> {
async fn call(&self, args: Value) -> Result<CallToolResult> {
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)]