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:
parent
787cc015b7
commit
6de444c62f
8 changed files with 160 additions and 108 deletions
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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]]
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue