feat: 添加参数提取工具函数,支持处理字符串化 JSON 数组,优化技能管理和会话发送功能
This commit is contained in:
parent
da9cec6d35
commit
efc8af12eb
@ -763,6 +763,8 @@ impl AgentLoop {
|
||||
iteration,
|
||||
response_len = response.content.len(),
|
||||
tool_calls_len = response.tool_calls.len(),
|
||||
content = %response.content,
|
||||
model = %response.model,
|
||||
"LLM response received"
|
||||
);
|
||||
|
||||
@ -782,9 +784,17 @@ impl AgentLoop {
|
||||
}
|
||||
|
||||
// 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!(
|
||||
iteration,
|
||||
count = response.tool_calls.len(),
|
||||
content = %response.content,
|
||||
tool_calls = %tool_calls_json,
|
||||
model = %response.model,
|
||||
"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))
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Returns the provided error message if args is null.
|
||||
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>> {
|
||||
let attachment_paths = value
|
||||
.as_array()
|
||||
.ok_or_else(|| anyhow!("attachments must be an array of local file paths"))?;
|
||||
// 支持两种格式:实际数组 或 字符串化的 JSON 数组
|
||||
let paths = if let Some(arr) = value.as_array() {
|
||||
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());
|
||||
for path_value in attachment_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"));
|
||||
if paths.is_empty() {
|
||||
return Err(anyhow!("attachments must be an array of local file paths"));
|
||||
}
|
||||
|
||||
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))?;
|
||||
if !metadata.is_file() {
|
||||
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));
|
||||
}
|
||||
|
||||
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);
|
||||
item.mime_type = mime_guess::from_path(raw_path)
|
||||
item.mime_type = mime_guess::from_path(&raw_path)
|
||||
.first_raw()
|
||||
.map(ToOwned::to_owned);
|
||||
attachments.push(item);
|
||||
@ -319,4 +337,20 @@ mod tests {
|
||||
assert_eq!(attachments.len(), 1);
|
||||
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::tools::traits::{Tool, ToolResult};
|
||||
use crate::tools::{extract_bool, extract_string_array};
|
||||
|
||||
pub struct SkillManageTool {
|
||||
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()) {
|
||||
Some(value) => match SkillScope::parse(value) {
|
||||
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> {
|
||||
let names = args
|
||||
.get("names")
|
||||
.ok_or_else(|| "disable requires names".to_string())?
|
||||
.as_array()
|
||||
.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)
|
||||
// 支持两种格式:实际数组 或 字符串化的 JSON 数组
|
||||
extract_string_array(args, "names")
|
||||
.filter(|arr| !arr.is_empty())
|
||||
.ok_or_else(|| "disable requires names (array of strings)".to_string())
|
||||
}
|
||||
|
||||
fn skill_change_payload(change: crate::skills::SkillAvailabilityChange) -> serde_json::Value {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user