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 159 additions and 107 deletions
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
Loading…
Reference in a new issue