From 495c8cdc7e8065e61940acc40fae33374671ace6 Mon Sep 17 00:00:00 2001 From: ooodc <549496103@qq.com> Date: Sat, 2 May 2026 09:38:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=8A=80=E8=83=BD?= =?UTF-8?q?=E5=8C=B9=E9=85=8D=E6=91=98=E8=A6=81=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8A=80=E8=83=BD=E6=8F=90=E7=A4=BA=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=EF=BC=8C=E6=98=8E=E7=A1=AE=E6=8A=80=E8=83=BD=E4=B8=8E?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E7=9A=84=E5=8C=BA=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot --- src/agent/agent_loop.rs | 74 ++++++++++++++++++- src/gateway/default_agent_prompt.md | 3 +- .../memory_maintenance_system_prompt.md | 31 +++++++- src/skills/mod.rs | 6 +- 4 files changed, 109 insertions(+), 5 deletions(-) diff --git a/src/agent/agent_loop.rs b/src/agent/agent_loop.rs index 9eb45d0..dff814e 100644 --- a/src/agent/agent_loop.rs +++ b/src/agent/agent_loop.rs @@ -529,6 +529,10 @@ pub trait EmittedMessageHandler: Send + Sync + 'static { pub trait SkillProvider: Send + Sync + 'static { fn system_index_prompt(&self) -> Option; + + fn matching_skill_summary(&self, _name: &str) -> Option { + None + } } #[derive(Default)] @@ -1024,9 +1028,17 @@ impl AgentLoop { Some(t) => t, None => { tracing::warn!(tool = %tool_call.name, "Tool not found"); + let skill_hint = self.skills.matching_skill_summary(&tool_call.name); + let error = match skill_hint { + Some(summary) => format!( + "Tool '{}' not found. A skill with the same name exists: {}. Skills are not tools. Call skill_activate with {{\"name\": \"{}\"}} first.", + tool_call.name, summary, tool_call.name + ), + None => format!("Tool '{}' not found", tool_call.name), + }; return ToolExecutionOutcome::failure( - format!("Error: Tool '{}' not found", tool_call.name), - Some(format!("Tool '{}' not found", tool_call.name)), + format!("Error: {}", error), + Some(error), ); } }; @@ -1073,6 +1085,7 @@ impl AgentLoop { #[cfg(test)] mod tests { use super::*; + use crate::config::LLMProviderConfig; use crate::observability::{MultiObserver, Observer}; use tempfile::tempdir; @@ -1098,6 +1111,63 @@ mod tests { } } + struct TestSkillProvider; + + impl SkillProvider for TestSkillProvider { + fn system_index_prompt(&self) -> Option { + None + } + + fn matching_skill_summary(&self, name: &str) -> Option { + (name == "baidu-search").then(|| "用于百度搜索和天气查询的技能".to_string()) + } + } + + fn test_runtime_config() -> LLMProviderConfig { + LLMProviderConfig { + provider_type: "openai".to_string(), + name: "test".to_string(), + base_url: "http://localhost".to_string(), + api_key: "test-key".to_string(), + extra_headers: std::collections::HashMap::new(), + llm_timeout_secs: 120, + model_id: "test-model".to_string(), + temperature: Some(0.0), + max_tokens: Some(32), + context_window_tokens: None, + model_extra: std::collections::HashMap::new(), + max_tool_iterations: 1, + tool_result_max_chars: 20_000, + context_tool_result_trim_chars: 20_000, + } + } + + #[tokio::test] + async fn test_missing_tool_with_same_name_skill_returns_activation_hint() { + let loop_instance = AgentLoop::with_tools_and_skill_provider( + test_runtime_config(), + Arc::new(ToolRegistry::new()), + Arc::new(TestSkillProvider), + ) + .unwrap(); + + let outcome = loop_instance + .execute_tool_internal(&ToolCall { + id: "call-1".to_string(), + name: "baidu-search".to_string(), + arguments: serde_json::json!({ + "queries": "佛山今天几点下雨" + }), + }) + .await; + + assert_eq!(outcome.state, ToolExecutionState::Completed); + assert!(!outcome.success); + assert!(outcome.output.contains("技能")); + assert!(outcome.output.contains("skill_activate")); + assert!(outcome.output.contains("baidu-search")); + } + #[tokio::test] async fn test_observer_receives_tool_events() { // Verify MultiObserver works diff --git a/src/gateway/default_agent_prompt.md b/src/gateway/default_agent_prompt.md index 5c5205f..e526422 100644 --- a/src/gateway/default_agent_prompt.md +++ b/src/gateway/default_agent_prompt.md @@ -46,4 +46,5 @@ ## 补充要求 - 回答应以帮助用户完成当前目标为中心。 -- 在信息不足时先补关键前提,在信息充分时直接执行。 \ No newline at end of file +- 在信息不足时先补关键前提,在信息充分时直接执行。 +- Skill 不是工具名。看到可用 Skill 时,不能直接调用 Skill 名称;必须先调用 skill_activate,并传入对应的 name。 \ No newline at end of file diff --git a/src/gateway/memory_maintenance_system_prompt.md b/src/gateway/memory_maintenance_system_prompt.md index 889e6ae..f81ab95 100644 --- a/src/gateway/memory_maintenance_system_prompt.md +++ b/src/gateway/memory_maintenance_system_prompt.md @@ -1 +1,30 @@ -你是 PicoBot 的后台记忆整理器。你必须根据输入的候选记忆做语义整理,并严格返回 JSON,不要输出 Markdown 代码块,不要输出额外解释。输出 JSON 字段必须包含:user_facts, preferences, behavior_patterns, merges, conflicts, low_value_ids, managed_markdown。user_facts、preferences、behavior_patterns 是字符串数组。merges 是对象数组,每个对象必须包含 source_ids、namespace、memory_key、content。conflicts 是对象数组,每个对象必须包含 source_ids、note。low_value_ids 是需要删除的候选记忆 id 数组。只能引用输入里出现过的候选 id。managed_markdown 必须是 Markdown 文本,且只保留稳定模式,不写一次性事件。 \ No newline at end of file +你是 PicoBot 的后台记忆整理器。 + +你的任务是: + +- 根据输入的候选记忆做语义整理。 +- 严格返回 JSON。 +- 不要输出 Markdown 代码块。 +- 不要输出额外解释。 + +输出 JSON 必须包含以下字段: + +- user_facts +- preferences +- behavior_patterns +- merges +- conflicts +- low_value_ids +- managed_markdown + +字段要求如下: + +- user_facts、preferences、behavior_patterns:字符串数组。 +- merges:对象数组。每个对象必须包含 source_ids、namespace、memory_key、content。 +- conflicts:对象数组。每个对象必须包含 source_ids、note。 +- low_value_ids:需要删除的候选记忆 id 数组。 +- managed_markdown:必须是 Markdown 文本,且只保留稳定模式,不写一次性事件。 + +额外约束: + +- 只能引用输入里出现过的候选 id。 \ No newline at end of file diff --git a/src/skills/mod.rs b/src/skills/mod.rs index 9fa3873..0b5390d 100644 --- a/src/skills/mod.rs +++ b/src/skills/mod.rs @@ -226,6 +226,10 @@ impl crate::agent::SkillProvider for SkillRuntime { fn system_index_prompt(&self) -> Option { SkillRuntime::system_index_prompt(self) } + + fn matching_skill_summary(&self, name: &str) -> Option { + self.get_skill(name).map(|skill| skill.description) + } } impl SkillSource { @@ -319,7 +323,7 @@ impl SkillCatalog { } let mut prompt = String::from( - "You have access to skills discovered from local skill directories. Use a skill only when the user's request clearly matches the skill description.\nIf a skill is needed, call tool skill_activate with {\"name\": \"\"}.\nAvailable skills:\n", + "You have access to skills discovered from local skill directories. Use a skill only when the user's request clearly matches the skill description.\nSkills are not tools. Never call a skill name directly as a tool, even if the skill name looks tool-like.\nIf a skill is needed, you must call tool skill_activate with {\"name\": \"\"} before using the skill instructions.\nDo not call directly. Always call skill_activate first.\nAvailable skills:\n", ); for skill in self.skills.iter().take(self.max_listed_skills) {