diff --git a/src/llm/conversation.rs b/src/llm/conversation.rs index 69e1531..e5b746a 100644 --- a/src/llm/conversation.rs +++ b/src/llm/conversation.rs @@ -239,5 +239,6 @@ pub const DEFAULT_SYSTEM_PROMPT: &str = "You are a concise and helpful assistant Keep replies practical and natural. \ Unless the user asks for detail, keep most replies under 120 Chinese characters. \ \ - 如果需要了解用户的长期记忆或历史对话摘要,使用 read_memories / read_summaries 工具。\ - 如果用户提到重要的个人信息或约定,使用 write_memory 工具记录下来。"; + 可用的核心工具:query_capabilities(查看所有可用工具)、call_capability(按名调用工具)。\ + 可使用 read_memories / write_memory 管理用户长期记忆,read_summaries 查看历史摘要。\ + 调用工具前先用 query_capabilities 确认工具有哪些。"; diff --git a/src/main.rs b/src/main.rs index 37a986e..0539f3b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -238,28 +238,43 @@ async fn cmd_listen( ..Default::default() }; - // 构建 LLM tools 列表(skills + 上下文工具) + // 仅暴露两个元工具:查询能力 + 调用能力 + // 具体工具描述通过 query_capabilities 返回 let mut tools_list: Vec = Vec::new(); - if let Some(ref registry) = skill_registry { - for spec in registry.list_specs() { - tools_list.push(serde_json::json!({ - "type": "function", - "function": { - "name": spec.name, - "description": spec.description, - "parameters": spec.parameters, - } - })); + // query_capabilities:列出所有可用工具及其说明 + tools_list.push(serde_json::json!({ + "type": "function", + "function": { + "name": "query_capabilities", + "description": "列出所有可用工具的详细说明、参数格式和调用示例。在需要了解有哪些工具可用时首先调用此工具。", + "parameters": { "type": "object", "properties": {} } } - } + })); - // 上下文工具(长期记忆 + 摘要) + // call_capability:按名称调用任意工具 + tools_list.push(serde_json::json!({ + "type": "function", + "function": { + "name": "call_capability", + "description": "按工具名称调用一个已注册的工具。name 为工具名(来自 query_capabilities),prompt 为自然语言需求", + "parameters": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "工具名称" }, + "prompt": { "type": "string", "description": "自然语言需求" } + }, + "required": ["name"] + } + } + })); + + // 上下文工具(仍然直接暴露给 LLM) tools_list.push(serde_json::json!({ "type": "function", "function": { "name": "read_memories", - "description": "读取用户的长期记忆(跨会话持久保存的个人信息、偏好、约定等)", + "description": "读取用户的长期记忆(偏好、个人信息、约定)", "parameters": { "type": "object", "properties": {} } } })); @@ -267,7 +282,7 @@ async fn cmd_listen( "type": "function", "function": { "name": "write_memory", - "description": "写入一条用户的长期记忆(当用户提到重要偏好、个人信息、约定时调用)", + "description": "记录用户的重要信息或偏好", "parameters": { "type": "object", "properties": { @@ -281,28 +296,11 @@ async fn cmd_listen( "type": "function", "function": { "name": "read_summaries", - "description": "读取历史会话摘要(包括因上下文压缩或空闲超时产生的摘要)", + "description": "读取历史会话摘要", "parameters": { "type": "object", "properties": {} } } })); - // call_capability 桥接:iPet 兼容,按 skill 名调用 - tools_list.push(serde_json::json!({ - "type": "function", - "function": { - "name": "call_capability", - "description": "按名称调用一个 skill(等同于直接使用该 skill 的工具名)。name 参数为 skill 的名称,prompt 为自然语言需求", - "parameters": { - "type": "object", - "properties": { - "name": { "type": "string", "description": "要调用的 skill 名称" }, - "prompt": { "type": "string", "description": "给该 skill 的自然语言需求" } - }, - "required": ["name"] - } - } - })); - cfg.tools = Some(tools_list); // 创建 Conversation @@ -372,6 +370,17 @@ async fn cmd_listen( return Ok(memory_store.write_for(&user_id, content).await); } "read_summaries" => return Ok(context::tools::read_summaries(&session).await), + "query_capabilities" => { + if let Some(r) = reg.as_ref() { + return Ok(build_capability_list(r)); + } + return Ok("暂无工具".to_string()); + } + "get_current_datetime" => { + let now = chrono::Local::now(); + return Ok(serde_json::json!({"datetime": now.format("%Y-%m-%d %H:%M:%S").to_string()}).to_string()); + } + "manage_memos" => return handle_memos(&args).await, "call_capability" => { let cap_params: serde_json::Value = serde_json::from_str(&args).unwrap_or_default(); let cap_name = cap_params.get("name").and_then(|v| v.as_str()).map(|s| s.to_string()); @@ -800,6 +809,55 @@ fn global_skills_dir() -> std::path::PathBuf { .join("skills") } +fn build_capability_list(reg: &SkillRegistry) -> String { + let mut lines = vec!["📋 可用工具:".to_string()]; + for spec in reg.list_specs() { + let risk = if spec.risk_level == tools::skill::RiskLevel::High { " ⚠️需确认" } else { "" }; + lines.push(format!( + "\n🔹 {} {} — {}", spec.name, risk, spec.description + )); + } + lines.join("\n") +} + +async fn handle_memos(args_json: &str) -> Result { + use std::io::{Read, Write}; + let params: serde_json::Value = serde_json::from_str(args_json).unwrap_or_default(); + let action = params.get("action").and_then(|v| v.as_str()).unwrap_or("list"); + let path = ".data/memos.json"; + std::fs::create_dir_all(".data").ok(); + + let mut data: Vec = if std::path::Path::new(path).exists() { + serde_json::from_str(&std::fs::read_to_string(path).unwrap_or_default()).unwrap_or_default() + } else { + vec![] + }; + + match action { + "add" => { + let content = params.get("content").and_then(|v| v.as_str()).unwrap_or(""); + if content.is_empty() { return Ok(r#"{"error":"请提供 content 参数"}"#.to_string()); } + let next_id = data.iter().filter_map(|m| m.get("id").and_then(|v| v.as_i64())).max().unwrap_or(0) + 1; + data.push(serde_json::json!({"id": next_id, "content": content, "created_at": chrono::Local::now().format("%Y-%m-%d %H:%M").to_string()})); + std::fs::write(path, serde_json::to_string_pretty(&data).unwrap()).ok(); + Ok(format!("已添加备忘录 #{}", next_id)) + } + "list" => { + if data.is_empty() { return Ok("暂无备忘录".to_string()); } + Ok(data.iter().map(|m| format!("#{} [{}] {}", m["id"], m.get("created_at").and_then(|v| v.as_str()).unwrap_or("?"), m["content"].as_str().unwrap_or("?"))).collect::>().join("\n")) + } + "delete" => { + let id = params.get("id").and_then(|v| v.as_i64()).unwrap_or(0); + let before = data.len(); + data.retain(|m| m.get("id").and_then(|v| v.as_i64()) != Some(id)); + if data.len() == before { return Ok(format!("未找到备忘录 #{}", id)); } + std::fs::write(path, serde_json::to_string_pretty(&data).unwrap()).ok(); + Ok(format!("已删除备忘录 #{}", id)) + } + _ => Ok("未知操作,支持: add, list, delete".to_string()) + } +} + async fn cmd_skills(action: SkillsAction) { use dialoguer::{MultiSelect, theme::ColorfulTheme}; use console::Term;