//! ## JSON 工具定义格式 //! //! 本模块定义了一种"声明式工具"格式 —— 只需要写一个 JSON 文件就能注册一个新工具, //! 无需改 Rust 代码、重新编译。 //! //! ### 设计理念:像 curl + jq 一样描述工具 //! //! ```json //! { //! "name": "stocks", //! "description": "股票行情查询", //! "url": "https://api.example.com/stocks/{{code}}", //! "method": "GET", //! "headers": { "Authorization": "${env:API_KEY}" }, //! "extract": "data.items", //! "fields": ["name", "price"] //! } //! ``` //! //! ### 三要素 //! 1. **URL** — API 端点模板(支持 `{{var}}` 和 `${param:name:type}` 传参) //! 2. **取节点** — `extract` 字段指定 JSON 点路径(类似 jq) //! 3. **选字段** — `fields` 字段只保留需要的字段 //! //! ### 参数推导 //! 自动从 URL/Body/Headers 中推导参数定义(无需手动写 parameters 字段), //! 也支持显式 `parameters` 声明以获得更精确的类型描述。 use std::collections::HashMap; /// HTTP 方法 #[derive(Debug, Clone, PartialEq)] pub enum HttpMethod { Get, Post, Put, Delete, } #[allow(dead_code)] impl HttpMethod { pub fn as_str(&self) -> &'static str { match self { HttpMethod::Get => "GET", HttpMethod::Post => "POST", HttpMethod::Put => "PUT", HttpMethod::Delete => "DELETE", } } } impl Default for HttpMethod { fn default() -> Self { HttpMethod::Get } } impl<'de> serde::Deserialize<'de> for HttpMethod { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let s = String::deserialize(deserializer)?; match s.to_uppercase().as_str() { "GET" => Ok(HttpMethod::Get), "POST" => Ok(HttpMethod::Post), "PUT" => Ok(HttpMethod::Put), "DELETE" => Ok(HttpMethod::Delete), other => Err(serde::de::Error::custom(format!( "不支持的 HTTP 方法: {} (支持: GET/POST/PUT/DELETE)", other ))), } } } /// ## JSON 工具定义(ToolDef) /// /// 描述一个通过 HTTP API 调用的外部工具。 /// /// ### 字段说明 /// * `name` — 工具名称,用作 `call_capability` 的 name /// * `description` — 工具描述(给 LLM 看的) /// * `url` — URL 模板,支持 `{{var}}` 和 `${param:name:type}` 两种传参 /// * `method` — HTTP 方法(GET/POST/PUT/DELETE),默认 GET /// * `headers` — HTTP 请求头,值可包含 `${env:KEY}`(环境变量)、 /// `${cmd:command}`(命令输出)、`${param:name:type}`(参数值) /// * `body` — POST/PUT 的 JSON body 模板,同样支持变量替换 /// * `parameters` — 显式参数声明(可选,未声明时自动推导) /// * `extract` — JSON 提取路径,如 "data.items" /// * `fields` — 筛选要保留的字段 /// * `timeout` — 超时秒数,默认 30 #[derive(Debug, Clone, serde::Deserialize)] pub struct ToolDef { /// 工具名称,用作 call_capability 的 name pub name: String, /// 工具描述 pub description: String, /// URL 模板,如 "https://api.example.com/stock/{{code}}" /// 支持 {{var}} 和 ${param:name:type} 两种传参形式 pub url: String, /// HTTP 方法,默认 GET #[serde(default)] pub method: HttpMethod, /// HTTP 头。值可包含 ${env:KEY}、${cmd:command}、${param:name:type} #[serde(default, skip_serializing_if = "Option::is_none")] pub headers: Option>, /// POST/PUT 的 JSON body 模板。字符串值可包含 {{var}} 和 ${param:name:type} #[serde(default, skip_serializing_if = "Option::is_none")] pub body: Option, /// 显式参数声明(可选) /// 格式:{ "name": { "type": "string", "description": "..." } } /// 未声明的参数自动从 ${param:name:type} 和 {{var}} 推导 #[serde(default, skip_serializing_if = "Option::is_none")] pub parameters: Option, /// JSON 提取路径(点路径),如 "data.items" #[serde(default, skip_serializing_if = "Option::is_none")] pub extract: Option, /// 筛选字段 #[serde(default, skip_serializing_if = "Option::is_none")] pub fields: Option>, /// 超时秒数,默认 30 #[serde(default = "default_timeout")] pub timeout: u64, } const fn default_timeout() -> u64 { 30 } impl ToolDef { /// 从文件加载 pub fn from_file(path: &std::path::Path) -> Result { let text = std::fs::read_to_string(path) .map_err(|e| format!("读取 {} 失败: {}", path.display(), e))?; serde_json::from_str(&text) .map_err(|e| format!("解析 {} 失败: {}", path.display(), e)) } /// 生成 SkillSpec /// /// 参数推导优先级: /// 1. 显式 parameters 字段(精确控制类型和描述) /// 2. 从 URL/Body/Headers 中扫描 `${param:name:type}` 推导 /// 3. 从 {{var}} 推导(全视为 string 类型,兼容旧格式) pub fn to_spec(&self) -> super::types::SkillSpec { let params = self.infer_parameters(); super::types::SkillSpec { name: self.name.clone(), description: self.description.clone(), risk_level: super::types::RiskLevel::Low, parameters: params, timeout_secs: self.timeout, } } /// 推导参数定义 fn infer_parameters(&self) -> serde_json::Value { // 1. 显式 parameters 优先 if let Some(ref p) = self.parameters { let p = p.clone(); // 如果已经是完整 schema(含 type/properties),直接返回 if p.get("type").is_some() { return p; } // 否则认为是属性定义 { name: { type, description }, ... } // 包装为完整 schema let required: Vec = p.as_object() .map(|o| o.keys().cloned().collect()) .unwrap_or_default(); return serde_json::json!({ "type": "object", "properties": p, "required": required }); } // 2. 从模板中扫描 ${param:name:type} 和 {{var}} let mut param_map: serde_json::Map = serde_json::Map::new(); Self::collect_params(&self.url, &mut param_map); if let Some(ref body) = self.body { Self::collect_params_from_json(body, &mut param_map); } if let Some(ref headers) = self.headers { for v in headers.values() { Self::collect_params(v, &mut param_map); } } let required: Vec = param_map.keys().cloned().collect(); serde_json::json!({ "type": "object", "properties": param_map, "required": required }) } /// 从字符串中收集参数(识别 ${param:name:type} 和 {{var}}) fn collect_params(s: &str, map: &mut serde_json::Map) { // 匹配 ${param:name:type} 和 ${param:name:type=default} let mut pos = 0; while let Some(start) = s[pos..].find("${param:") { let rel = pos + start + 8; // skip "${param:" let rest = &s[rel..]; if let Some(end) = rest.find('}') { let segment = &rest[..end]; let parts: Vec<&str> = segment.split(':').collect(); let name = parts[0].trim().to_string(); if name.is_empty() { pos = rel + end + 1; continue; } if !map.contains_key(&name) { let param_type = parts.get(1).copied().unwrap_or("string"); let description = parts.get(2).copied().unwrap_or(name.as_str()); let entry = serde_json::json!({ "type": param_type, "description": description }); map.insert(name.clone(), entry); } pos = rel + end + 1; continue; } pos = rel; } // 兼容旧格式 {{var}} Self::extract_vars(s, map); } /// 从 JSON 值中递归收集参数 fn collect_params_from_json( v: &serde_json::Value, map: &mut serde_json::Map, ) { match v { serde_json::Value::String(s) => Self::collect_params(s, map), serde_json::Value::Object(obj) => { for val in obj.values() { Self::collect_params_from_json(val, map); } } serde_json::Value::Array(arr) => { for item in arr { Self::collect_params_from_json(item, map); } } _ => {} } } /// 兼容旧格式 {{var}} — 全视为 string 类型 fn extract_vars(s: &str, map: &mut serde_json::Map) { let mut remaining = s; while let Some(start) = remaining.find("{{") { remaining = &remaining[start + 2..]; if let Some(end) = remaining.find("}}") { let name = remaining[..end].trim().to_string(); if !name.is_empty() && !name.starts_with('#') && !map.contains_key(&name) { map.insert( name.clone(), serde_json::json!({"type": "string", "description": name}), ); } remaining = &remaining[end + 2..]; } } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_minimal() { let json = r#"{ "name": "ping", "description": "Ping test", "url": "https://httpbin.org/get" }"#; let def: ToolDef = serde_json::from_str(json).unwrap(); assert_eq!(def.name, "ping"); assert_eq!(def.method, HttpMethod::Get); assert_eq!(def.timeout, 30); } #[test] fn test_parse_post() { let json = r#"{ "name": "translate", "description": "翻译", "url": "https://api.example.com/translate?text={{content}}", "method": "POST", "headers": { "Authorization": "${env:TR_KEY}" }, "body": { "q": "{{content}}", "target": "zh" }, "extract": "data.result", "fields": ["text"], "timeout": 60 }"#; let def: ToolDef = serde_json::from_str(json).unwrap(); assert_eq!(def.method, HttpMethod::Post); assert_eq!(def.timeout, 60); assert_eq!(def.extract.as_deref(), Some("data.result")); } #[test] fn test_infer_parameters_from_url() { let json = r#"{ "name": "test", "description": "test", "url": "https://api.example.com/{{city}}/weather?days={{days}}" }"#; let def: ToolDef = serde_json::from_str(json).unwrap(); let params = def.infer_parameters(); let props = params["properties"].as_object().unwrap(); assert!(props.contains_key("city")); assert!(props.contains_key("days")); let required = params["required"].as_array().unwrap(); assert!(required.iter().any(|v| v == "city")); assert!(required.iter().any(|v| v == "days")); } #[test] fn test_infer_parameters_from_body() { let json = r#"{ "name": "test", "description": "test", "url": "https://api.example.com/x", "body": { "q": "{{text}}", "target": "{{lang}}" } }"#; let def: ToolDef = serde_json::from_str(json).unwrap(); let params = def.infer_parameters(); let props = params["properties"].as_object().unwrap(); assert!(props.contains_key("text")); assert!(props.contains_key("lang")); } #[test] fn test_method_uppercase_flexibility() { let json = r#"{"name":"t","description":"t","url":"u","method":"post"}"#; let def: ToolDef = serde_json::from_str(json).unwrap(); assert_eq!(def.method, HttpMethod::Post); } #[test] fn test_infer_parameters_from_param_syntax() { let json = r#"{ "name": "weather", "description": "天气查询", "url": "https://api.example.com/weather/${param:city:string}", "headers": { "X-Key": "${param:api_key:string}" } }"#; let def: ToolDef = serde_json::from_str(json).unwrap(); let params = def.infer_parameters(); let props = params["properties"].as_object().unwrap(); assert!(props.contains_key("city"), "应有 city 参数"); assert_eq!(props["city"]["type"], "string", "city 类型应为 string"); assert!(props.contains_key("api_key"), "应有 api_key 参数"); assert_eq!(props["api_key"]["type"], "string", "api_key 类型应为 string"); let required = params["required"].as_array().unwrap(); assert!(required.iter().any(|v| v == "city")); } #[test] fn test_infer_parameters_explicit_priority() { let json = r#"{ "name": "my_tool", "description": "test", "url": "https://api.example.com/${param:city:string}", "parameters": { "city": { "type": "string", "description": "城市名称" }, "days": { "type": "integer", "description": "天数" } } }"#; let def: ToolDef = serde_json::from_str(json).unwrap(); let params = def.infer_parameters(); // 显式 parameters 覆盖了自动推导 let props = params["properties"].as_object().unwrap(); assert!(props.contains_key("days"), "显式声明的 days 应在"); assert_eq!(props["days"]["type"], "integer"); // 且自动 wrap 为完整 schema assert_eq!(params["type"], "object"); } }