feat: 添加参数提取工具函数,支持处理字符串化 JSON 数组,优化技能管理和会话发送功能
This commit is contained in:
parent
da9cec6d35
commit
efc8af12eb
@ -763,6 +763,8 @@ impl AgentLoop {
|
|||||||
iteration,
|
iteration,
|
||||||
response_len = response.content.len(),
|
response_len = response.content.len(),
|
||||||
tool_calls_len = response.tool_calls.len(),
|
tool_calls_len = response.tool_calls.len(),
|
||||||
|
content = %response.content,
|
||||||
|
model = %response.model,
|
||||||
"LLM response received"
|
"LLM response received"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -782,9 +784,17 @@ impl AgentLoop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute tool calls
|
// Execute tool calls
|
||||||
|
let tool_calls_json = serde_json::to_string_pretty(&response.tool_calls)
|
||||||
|
.unwrap_or_else(|_| response.tool_calls.iter()
|
||||||
|
.map(|tc| format!("{}({})", tc.name, tc.arguments))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", "));
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
iteration,
|
iteration,
|
||||||
count = response.tool_calls.len(),
|
count = response.tool_calls.len(),
|
||||||
|
content = %response.content,
|
||||||
|
tool_calls = %tool_calls_json,
|
||||||
|
model = %response.model,
|
||||||
"Tool calls detected, executing tools"
|
"Tool calls detected, executing tools"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -143,6 +143,44 @@ pub fn require_bool(args: &serde_json::Value, key: &str) -> Result<bool, String>
|
|||||||
.ok_or_else(|| format!("Missing required parameter: {}", key))
|
.ok_or_else(|| format!("Missing required parameter: {}", key))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract a string array parameter, handling both actual arrays and stringified JSON arrays.
|
||||||
|
pub fn extract_string_array(args: &serde_json::Value, key: &str) -> Option<Vec<String>> {
|
||||||
|
args.get(key).and_then(|v| {
|
||||||
|
if let Some(arr) = v.as_array() {
|
||||||
|
Some(
|
||||||
|
arr.iter()
|
||||||
|
.filter_map(|item| item.as_str())
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
} else if let Some(s) = v.as_str() {
|
||||||
|
// Try to parse as JSON array string
|
||||||
|
serde_json::from_str::<Vec<serde_json::Value>>(s)
|
||||||
|
.ok()
|
||||||
|
.map(|arr| {
|
||||||
|
arr.iter()
|
||||||
|
.filter_map(|item| item.as_str())
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.filter(|result: &Vec<String>| !result.is_empty())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract a required string array parameter.
|
||||||
|
pub fn require_string_array(args: &serde_json::Value, key: &str) -> Result<Vec<String>, String> {
|
||||||
|
extract_string_array(args, key)
|
||||||
|
.filter(|arr| !arr.is_empty())
|
||||||
|
.ok_or_else(|| format!("Missing required parameter: {}", key))
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if args is null and return an error result if so.
|
/// Check if args is null and return an error result if so.
|
||||||
/// Returns the provided error message if args is null.
|
/// Returns the provided error message if args is null.
|
||||||
pub fn check_null_args(args: &serde_json::Value, tool_name: &str) -> Option<ToolResult> {
|
pub fn check_null_args(args: &serde_json::Value, tool_name: &str) -> Option<ToolResult> {
|
||||||
|
|||||||
@ -164,21 +164,39 @@ fn validate_context(context: &ToolContext) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_attachments(value: &serde_json::Value) -> anyhow::Result<Vec<MediaItem>> {
|
fn parse_attachments(value: &serde_json::Value) -> anyhow::Result<Vec<MediaItem>> {
|
||||||
let attachment_paths = value
|
// 支持两种格式:实际数组 或 字符串化的 JSON 数组
|
||||||
.as_array()
|
let paths = if let Some(arr) = value.as_array() {
|
||||||
.ok_or_else(|| anyhow!("attachments must be an array of local file paths"))?;
|
arr
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.as_str())
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
} else if let Some(s) = value.as_str() {
|
||||||
|
// 尝试解析字符串化的 JSON 数组
|
||||||
|
serde_json::from_str::<Vec<serde_json::Value>>(s)
|
||||||
|
.ok()
|
||||||
|
.map(|arr| {
|
||||||
|
arr.iter()
|
||||||
|
.filter_map(|v| v.as_str())
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
let mut attachments = Vec::with_capacity(attachment_paths.len());
|
if paths.is_empty() {
|
||||||
for path_value in attachment_paths {
|
return Err(anyhow!("attachments must be an array of local file paths"));
|
||||||
let raw_path = path_value
|
}
|
||||||
.as_str()
|
|
||||||
.ok_or_else(|| anyhow!("attachments entries must be strings"))?
|
|
||||||
.trim();
|
|
||||||
if raw_path.is_empty() {
|
|
||||||
return Err(anyhow!("attachment paths must not be empty"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let metadata = std::fs::metadata(raw_path)
|
let mut attachments = Vec::with_capacity(paths.len());
|
||||||
|
for raw_path in paths {
|
||||||
|
let metadata = std::fs::metadata(&raw_path)
|
||||||
.map_err(|err| anyhow!("failed to access attachment '{}': {}", raw_path, err))?;
|
.map_err(|err| anyhow!("failed to access attachment '{}': {}", raw_path, err))?;
|
||||||
if !metadata.is_file() {
|
if !metadata.is_file() {
|
||||||
return Err(anyhow!("attachment path is not a file: {}", raw_path));
|
return Err(anyhow!("attachment path is not a file: {}", raw_path));
|
||||||
@ -187,9 +205,9 @@ fn parse_attachments(value: &serde_json::Value) -> anyhow::Result<Vec<MediaItem>
|
|||||||
return Err(anyhow!("attachment file is empty: {}", raw_path));
|
return Err(anyhow!("attachment file is empty: {}", raw_path));
|
||||||
}
|
}
|
||||||
|
|
||||||
let media_type = infer_media_type(raw_path);
|
let media_type = infer_media_type(&raw_path);
|
||||||
let mut item = MediaItem::new(raw_path.to_string(), media_type);
|
let mut item = MediaItem::new(raw_path.to_string(), media_type);
|
||||||
item.mime_type = mime_guess::from_path(raw_path)
|
item.mime_type = mime_guess::from_path(&raw_path)
|
||||||
.first_raw()
|
.first_raw()
|
||||||
.map(ToOwned::to_owned);
|
.map(ToOwned::to_owned);
|
||||||
attachments.push(item);
|
attachments.push(item);
|
||||||
@ -319,4 +337,20 @@ mod tests {
|
|||||||
assert_eq!(attachments.len(), 1);
|
assert_eq!(attachments.len(), 1);
|
||||||
assert_eq!(attachments[0].media_type, "image");
|
assert_eq!(attachments[0].media_type, "image");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_attachments_handles_stringified_json_array() {
|
||||||
|
let file = NamedTempFile::new().unwrap();
|
||||||
|
std::fs::write(file.path(), b"demo").unwrap();
|
||||||
|
let txt_path = file.path().with_extension("txt");
|
||||||
|
std::fs::rename(file.path(), &txt_path).unwrap();
|
||||||
|
|
||||||
|
// Test with stringified JSON array (like LLM might send)
|
||||||
|
let path_str = txt_path.to_string_lossy().to_string().replace("\\", "\\\\");
|
||||||
|
let json_string = format!("[\"{}\"]", path_str);
|
||||||
|
let attachments = parse_attachments(&json!(json_string)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(attachments.len(), 1);
|
||||||
|
assert_eq!(attachments[0].media_type, "file");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -4,6 +4,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use crate::skills::{SkillRuntime, SkillScope};
|
use crate::skills::{SkillRuntime, SkillScope};
|
||||||
use crate::tools::traits::{Tool, ToolResult};
|
use crate::tools::traits::{Tool, ToolResult};
|
||||||
|
use crate::tools::{extract_bool, extract_string_array};
|
||||||
|
|
||||||
pub struct SkillManageTool {
|
pub struct SkillManageTool {
|
||||||
skills: Arc<SkillRuntime>,
|
skills: Arc<SkillRuntime>,
|
||||||
@ -86,7 +87,7 @@ impl Tool for SkillManageTool {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let reload = args.get("reload").and_then(|v| v.as_bool()).unwrap_or(true);
|
let reload = extract_bool(&args, "reload").unwrap_or(true);
|
||||||
let scope = match args.get("scope").and_then(|v| v.as_str()) {
|
let scope = match args.get("scope").and_then(|v| v.as_str()) {
|
||||||
Some(value) => match SkillScope::parse(value) {
|
Some(value) => match SkillScope::parse(value) {
|
||||||
Some(scope) => scope,
|
Some(scope) => scope,
|
||||||
@ -280,28 +281,10 @@ fn error_result(message: &str) -> ToolResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_disable_names(args: &serde_json::Value) -> Result<Vec<String>, String> {
|
fn parse_disable_names(args: &serde_json::Value) -> Result<Vec<String>, String> {
|
||||||
let names = args
|
// 支持两种格式:实际数组 或 字符串化的 JSON 数组
|
||||||
.get("names")
|
extract_string_array(args, "names")
|
||||||
.ok_or_else(|| "disable requires names".to_string())?
|
.filter(|arr| !arr.is_empty())
|
||||||
.as_array()
|
.ok_or_else(|| "disable requires names (array of strings)".to_string())
|
||||||
.ok_or_else(|| "names must be an array of strings".to_string())?;
|
|
||||||
|
|
||||||
let mut parsed = Vec::new();
|
|
||||||
for item in names {
|
|
||||||
let name = item
|
|
||||||
.as_str()
|
|
||||||
.ok_or_else(|| "names must be an array of strings".to_string())?
|
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
if name.is_empty() {
|
|
||||||
return Err("names must not contain empty values".to_string());
|
|
||||||
}
|
|
||||||
parsed.push(name);
|
|
||||||
}
|
|
||||||
if parsed.is_empty() {
|
|
||||||
return Err("names must not be empty".to_string());
|
|
||||||
}
|
|
||||||
Ok(parsed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn skill_change_payload(change: crate::skills::SkillAvailabilityChange) -> serde_json::Value {
|
fn skill_change_payload(change: crate::skills::SkillAvailabilityChange) -> serde_json::Value {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user