From 6c218331b1f1208ea8be6bf226936d3b556ade3e Mon Sep 17 00:00:00 2001 From: zhayujie Date: Sun, 8 Feb 2026 18:59:59 +0800 Subject: [PATCH] fix: improve skill system prompts and simplify tool descriptions - Simplify skill-creator installation flow - Refine skill selection prompt for better matching - Add parameter alias and env variable hints for tools - Skip linkai-agent when unconfigured - Create skills/ dir in workspace on init --- README.md | 4 +- agent/prompt/builder.py | 149 +++++++++++--------------- agent/prompt/workspace.py | 4 + agent/protocol/agent_stream.py | 1 - agent/skills/formatter.py | 5 +- agent/skills/loader.py | 12 +-- agent/tools/bash/bash.py | 17 ++- agent/tools/env_config/env_config.py | 3 +- agent/tools/read/read.py | 6 +- config-template.json | 2 +- skills/bocha-search/SKILL.md | 91 ---------------- skills/bocha-search/scripts/search.sh | 75 ------------- skills/skill-creator/SKILL.md | 25 +++-- 13 files changed, 102 insertions(+), 292 deletions(-) delete mode 100644 skills/bocha-search/SKILL.md delete mode 100755 skills/bocha-search/scripts/search.sh diff --git a/README.md b/README.md index 9e49af3..71b4c1c 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ bash <(curl -sS https://cdn.link-ai.tech/code/cow/run.sh) 项目支持国内外主流厂商的模型接口,可选模型及配置说明参考:[模型说明](#模型说明)。 -> 注:Agent模式下推荐使用以下模型,可根据效果及成本综合选择:MiniMAx(MiniMax-M2.1)、GLM(glm-4.7)、Qwen(qwen3-max)、Claude(claude-opus-4-6、claude-sonnet-4-5、claude-sonnet-4-0)、Gemini(gemini-3-flash-preview、gemini-3-pro-preview) +> 注:Agent模式下推荐使用以下模型,可根据效果及成本综合选择:GLM(glm-4.7)、MiniMAx(MiniMax-M2.1)、Qwen(qwen3-max)、Claude(claude-opus-4-6、claude-sonnet-4-5、claude-sonnet-4-0)、Gemini(gemini-3-flash-preview、gemini-3-pro-preview) 同时支持使用 **LinkAI平台** 接口,可灵活切换 OpenAI、Claude、Gemini、DeepSeek、Qwen、Kimi 等多种常用模型,并支持知识库、工作流、插件等Agent能力,参考 [接口文档](https://docs.link-ai.tech/platform/api)。 @@ -173,7 +173,7 @@ pip3 install -r requirements-optional.txt
2. 其他配置 -+ `model`: 模型名称,Agent模式下推荐使用 `MiniMax-M2.1`、`glm-4.7`、`qwen3-max`、`claude-opus-4-6`、`claude-sonnet-4-5`、`claude-sonnet-4-0`、`gemini-3-flash-preview`、`gemini-3-pro-preview`,全部模型名称参考[common/const.py](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/common/const.py)文件 ++ `model`: 模型名称,Agent模式下推荐使用 `glm-4.7`、`MiniMax-M2.1`、`qwen3-max`、`claude-opus-4-6`、`claude-sonnet-4-5`、`claude-sonnet-4-0`、`gemini-3-flash-preview`、`gemini-3-pro-preview`,全部模型名称参考[common/const.py](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/common/const.py)文件 + `character_desc`:普通对话模式下的机器人系统提示词。在Agent模式下该配置不生效,由工作空间中的文件内容构成。 + `subscribe_msg`:订阅消息,公众号和企业微信channel中请填写,当被订阅时会自动回复, 可使用特殊占位符。目前支持的占位符有{trigger_prefix},在程序中它会自动替换成bot的触发词。
diff --git a/agent/prompt/builder.py b/agent/prompt/builder.py index 91ef1c0..5e492ef 100644 --- a/agent/prompt/builder.py +++ b/agent/prompt/builder.py @@ -157,96 +157,66 @@ def _build_identity_section(base_persona: Optional[str], language: str) -> List[ def _build_tooling_section(tools: List[Any], language: str) -> List[str]: - """构建工具说明section""" + """Build tooling section with concise tool list and call style guide.""" + # One-line summaries for known tools (details are in the tool schema) + core_summaries = { + "read": "读取文件内容", + "write": "创建或覆盖文件", + "edit": "精确编辑文件", + "ls": "列出目录内容", + "grep": "搜索文件内容", + "find": "按模式查找文件", + "bash": "执行shell命令", + "terminal": "管理后台进程", + "web_search": "网络搜索", + "web_fetch": "获取URL内容", + "browser": "控制浏览器", + "memory_search": "搜索记忆", + "memory_get": "读取记忆内容", + "env_config": "管理API密钥和技能配置", + "scheduler": "管理定时任务和提醒", + "send": "发送文件给用户", + } + + # Preferred display order + tool_order = [ + "read", "write", "edit", "ls", "grep", "find", + "bash", "terminal", + "web_search", "web_fetch", "browser", + "memory_search", "memory_get", + "env_config", "scheduler", "send", + ] + + # Build name -> summary mapping for available tools + available = {} + for tool in tools: + name = tool.name if hasattr(tool, 'name') else str(tool) + available[name] = core_summaries.get(name, "") + + # Generate tool lines: ordered tools first, then extras + tool_lines = [] + for name in tool_order: + if name in available: + summary = available.pop(name) + tool_lines.append(f"- {name}: {summary}" if summary else f"- {name}") + for name in sorted(available): + summary = available[name] + tool_lines.append(f"- {name}: {summary}" if summary else f"- {name}") + lines = [ "## 工具系统", "", - "你可以使用以下工具来完成任务。工具名称是大小写敏感的,请严格按照列表中的名称调用。", + "可用工具(名称大小写敏感,严格按列表调用):", + "\n".join(tool_lines), "", - "### 可用工具", + "工具调用风格:", + "", + "- 在多步骤任务、敏感操作或用户要求时简要解释决策过程", + "- 持续推进直到任务完成,完成后向用户报告结果。", + "- 回复中涉及密钥、令牌等敏感信息必须脱敏。", "", ] - - # 工具分类和排序 - tool_categories = { - "文件操作": ["read", "write", "edit", "ls", "grep", "find"], - "命令执行": ["bash", "terminal"], - "网络搜索": ["web_search", "web_fetch", "browser"], - "记忆系统": ["memory_search", "memory_get"], - "其他": [] - } - - # 构建工具映射 - tool_map = {} - tool_descriptions = { - "read": "读取文件内容", - "write": "创建新文件或完全覆盖现有文件(会删除原内容!追加内容请用 edit)。注意:单次 write 内容不要超过 10KB,超大文件请分步创建", - "edit": "精确编辑文件(追加、修改、删除部分内容)", - "ls": "列出目录内容", - "grep": "在文件中搜索内容", - "find": "按照模式查找文件", - "bash": "执行shell命令", - "terminal": "管理后台进程", - "web_search": "网络搜索(使用搜索引擎)", - "web_fetch": "获取URL内容", - "browser": "控制浏览器", - "memory_search": "搜索记忆文件", - "memory_get": "获取记忆文件内容", - "calculator": "计算器", - "current_time": "获取当前时间", - } - - for tool in tools: - tool_name = tool.name if hasattr(tool, 'name') else str(tool) - tool_desc = tool.description if hasattr(tool, 'description') else tool_descriptions.get(tool_name, "") - tool_map[tool_name] = tool_desc - - # 按分类添加工具 - for category, tool_names in tool_categories.items(): - category_tools = [(name, tool_map.get(name, "")) for name in tool_names if name in tool_map] - if category_tools: - lines.append(f"**{category}**:") - for name, desc in category_tools: - if desc: - lines.append(f"- `{name}`: {desc}") - else: - lines.append(f"- `{name}`") - del tool_map[name] # 移除已添加的工具 - lines.append("") - - # 添加其他未分类的工具 - if tool_map: - lines.append("**其他工具**:") - for name, desc in sorted(tool_map.items()): - if desc: - lines.append(f"- `{name}`: {desc}") - else: - lines.append(f"- `{name}`") - lines.append("") - - # 工具使用指南 - lines.extend([ - "### 工具调用风格", - "", - "默认规则: 对于常规、低风险的工具调用,直接调用即可,无需叙述。", - "", - "需要叙述的情况:", - "- 多步骤、复杂的任务", - "- 敏感操作(如删除文件)", - "- 用户明确要求解释过程", - "", - "叙述要求: 保持简洁、信息密度高,避免重复显而易见的步骤。", - "", - "完成标准:", - "- 确保用户的需求得到实际解决,而不仅仅是制定计划。", - "- 当任务需要多次工具调用时,持续推进直到完成, 解决完后向用户报告结果或回复用户的问题", - "- 每次工具调用后,评估是否已获得足够信息来推进或完成任务", - "- 避免重复调用相同的工具和相同参数获取相同的信息,除非用户明确要求", - "", - "**安全提醒**: 回复中涉及密钥、令牌、密码等敏感信息时,必须脱敏处理,禁止直接显示完整内容。", - "", - ]) - + return lines @@ -265,16 +235,17 @@ def _build_skills_section(skill_manager: Any, tools: Optional[List[Any]], langua break lines = [ - "## 技能系统", + "## 技能系统(mandatory)", "", "在回复之前:扫描下方 中的 条目。", "", - f"- 如果恰好有一个技能明确适用:使用 `{read_tool_name}` 工具读取其 路径下的 SKILL.md 文件,然后遵循它", - "- 如果多个技能都适用:选择最具体的一个,然后读取并遵循", - "- 如果没有明确适用的:不要读取任何 SKILL.md", + f"- 如果恰好有一个技能(Skill)明确适用:使用 `{read_tool_name}` 读取其 处的 SKILL.md,然后严格遵循它", + "- 如果多个技能都适用则选择最匹配的一个,如果没有明确适用的则不要读取任何 SKILL.md", + "- 读取 SKILL.md 后直接按其指令执行,无需多余的预检查", "", - "**约束**: 永远不要一次性读取多个技能;只在选择后再读取。", + "**注意**: 永远不要一次性读取多个技能,只在选择后再读取。技能和工具不同,必须先读取其SKILL.md并按照文件内容运行。", "", + "以下是可用技能:" ] # 添加技能列表(通过skill_manager获取) diff --git a/agent/prompt/workspace.py b/agent/prompt/workspace.py index 714632c..0c2d139 100644 --- a/agent/prompt/workspace.py +++ b/agent/prompt/workspace.py @@ -57,6 +57,10 @@ def ensure_workspace(workspace_dir: str, create_templates: bool = True) -> Works # 创建memory子目录 os.makedirs(memory_dir, exist_ok=True) + + # 创建skills子目录 (for workspace-level skills installed by agent) + skills_dir = os.path.join(workspace_dir, "skills") + os.makedirs(skills_dir, exist_ok=True) # 如果需要,创建模板文件 if create_templates: diff --git a/agent/protocol/agent_stream.py b/agent/protocol/agent_stream.py index 4c37892..8e6c08d 100644 --- a/agent/protocol/agent_stream.py +++ b/agent/protocol/agent_stream.py @@ -708,7 +708,6 @@ class AgentStreamExecutor: if not tool_id: import uuid tool_id = f"call_{uuid.uuid4().hex[:24]}" - logger.debug(f"⚠️ Tool call missing ID for '{tc.get('name')}', generated fallback: {tool_id}") try: # Safely get arguments, handle None case diff --git a/agent/skills/formatter.py b/agent/skills/formatter.py index 7868e09..7426146 100644 --- a/agent/skills/formatter.py +++ b/agent/skills/formatter.py @@ -23,18 +23,15 @@ def format_skills_for_prompt(skills: List[Skill]) -> str: return "" lines = [ - "\n\nThe following skills provide specialized instructions for specific tasks.", - "Use the read tool to load a skill's file when the task matches its description.", "", "", ] - + for skill in visible_skills: lines.append(" ") lines.append(f" {_escape_xml(skill.name)}") lines.append(f" {_escape_xml(skill.description)}") lines.append(f" {_escape_xml(skill.file_path)}") - lines.append(f" {_escape_xml(skill.base_dir)}") lines.append(" ") lines.append("") diff --git a/agent/skills/loader.py b/agent/skills/loader.py index db4fd03..a7fa5f9 100644 --- a/agent/skills/loader.py +++ b/agent/skills/loader.py @@ -188,16 +188,14 @@ class SkillLoader: import json config_path = os.path.join(skill_dir, "config.json") - template_path = os.path.join(skill_dir, "config.json.template") - # Try to load config.json or fallback to template - config_file = config_path if os.path.exists(config_path) else template_path - - if not os.path.exists(config_file): - return default_description + # Without config.json, skip this skill entirely (return empty to trigger exclusion) + if not os.path.exists(config_path): + logger.debug(f"[SkillLoader] linkai-agent skipped: no config.json found") + return "" try: - with open(config_file, 'r', encoding='utf-8') as f: + with open(config_path, 'r', encoding='utf-8') as f: config = json.load(f) apps = config.get("apps", []) diff --git a/agent/tools/bash/bash.py b/agent/tools/bash/bash.py index 8952d11..7b7ec2d 100644 --- a/agent/tools/bash/bash.py +++ b/agent/tools/bash/bash.py @@ -20,10 +20,11 @@ class Bash(BaseTool): name: str = "bash" description: str = f"""Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last {DEFAULT_MAX_LINES} lines or {DEFAULT_MAX_BYTES // 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. -IMPORTANT SAFETY GUIDELINES: -- You can freely create, modify, and delete files within the current workspace -- For operations outside the workspace or potentially destructive commands (rm -rf, system commands, etc.), always explain what you're about to do and ask for user confirmation first -- When in doubt, describe the command's purpose and ask for permission before executing""" +ENVIRONMENT: All API keys from env_config are auto-injected. Use $VAR_NAME directly. + +SAFETY: +- Freely create/modify/delete files within the workspace +- For destructive and out-of-workspace commands, explain and confirm first""" params: dict = { "type": "object", @@ -92,13 +93,7 @@ IMPORTANT SAFETY GUIDELINES: logger.debug("[Bash] python-dotenv not installed, skipping .env loading") except Exception as e: logger.debug(f"[Bash] Failed to load .env: {e}") - - # Debug logging - logger.debug(f"[Bash] CWD: {self.cwd}") - logger.debug(f"[Bash] Command: {command[:500]}") - logger.debug(f"[Bash] OPENAI_API_KEY in env: {'OPENAI_API_KEY' in env}") - logger.debug(f"[Bash] SHELL: {env.get('SHELL', 'not set')}") - logger.debug(f"[Bash] Python executable: {sys.executable}") + # getuid() only exists on Unix-like systems if hasattr(os, 'getuid'): logger.debug(f"[Bash] Process UID: {os.getuid()}") diff --git a/agent/tools/env_config/env_config.py b/agent/tools/env_config/env_config.py index 9916920..b656ee2 100644 --- a/agent/tools/env_config/env_config.py +++ b/agent/tools/env_config/env_config.py @@ -202,7 +202,8 @@ class EnvConfig(BaseTool): "key": key, "value": self._mask_value(value), "description": description, - "exists": True + "exists": True, + "note": f"Value is masked for security. In bash, use ${key} directly — it is auto-injected." }) else: return ToolResult.success({ diff --git a/agent/tools/read/read.py b/agent/tools/read/read.py index fe4b329..cd1baa2 100644 --- a/agent/tools/read/read.py +++ b/agent/tools/read/read.py @@ -67,10 +67,12 @@ class Read(BaseTool): :param args: Contains file path and optional offset/limit parameters :return: File content or error message """ - path = args.get("path", "").strip() + # Support 'location' as alias for 'path' (LLM may use it from skill listing) + path = args.get("path", "") or args.get("location", "") + path = path.strip() if isinstance(path, str) else "" offset = args.get("offset") limit = args.get("limit") - + if not path: return ToolResult.fail("Error: path parameter is required") diff --git a/config-template.json b/config-template.json index 7bd8665..d09d32a 100644 --- a/config-template.json +++ b/config-template.json @@ -1,6 +1,6 @@ { "channel_type": "web", - "model": "MiniMax-M2.1", + "model": "glm-4.7", "claude_api_key": "", "claude_api_base": "https://api.anthropic.com/v1", "open_ai_api_key": "", diff --git a/skills/bocha-search/SKILL.md b/skills/bocha-search/SKILL.md deleted file mode 100644 index ca52d6c..0000000 --- a/skills/bocha-search/SKILL.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -name: bocha-search -description: High-quality web search with AI-optimized results. Use when user needs to search the internet for current information, news, or research topics. -homepage: https://open.bocha.cn/ -metadata: - emoji: 🔍 - requires: - bins: ["curl"] - env: ["BOCHA_API_KEY"] - primaryEnv: "BOCHA_API_KEY" ---- - -# Bocha Search - -High-quality web search powered by Bocha AI, optimized for AI consumption. Returns web pages, images, and detailed metadata. - -## Setup - -This skill requires a Bocha API key. If not configured: - -1. Visit https://open.bocha.cn to get an API key -2. Set the key using: `env_config(action="set", key="BOCHA_API_KEY", value="your-key")` -3. Or manually add to `~/cow/.env`: `BOCHA_API_KEY=your-key` - -## Usage - -**Important**: Scripts are located relative to this skill's base directory. - -When you see this skill in ``, note the `` path. - -```bash -# General pattern: -bash "/scripts/search.sh" "" [count] [freshness] [summary] - -# Parameters: -# - query: Search query (required) -# - count: Number of results (1-50, default: 10) -# - freshness: Time range filter (default: noLimit) -# Options: noLimit, oneDay, oneWeek, oneMonth, oneYear, YYYY-MM-DD..YYYY-MM-DD -# - summary: Include text summary (true/false, default: false) -``` - -## Examples - -### Basic search -```bash -bash "/scripts/search.sh" "latest AI news" -``` - -### Search with more results -```bash -bash "/scripts/search.sh" "Python tutorials" 20 -``` - -### Search recent content with summary -```bash -bash "/scripts/search.sh" "阿里巴巴ESG报告" 10 oneWeek true -``` - -### Search specific date range -```bash -bash "/scripts/search.sh" "tech news" 15 "2025-01-01..2025-02-01" -``` - -## Response Format - -The API returns structured data compatible with Bing Search API: - -**Web Pages** (in `data.webPages.value`): -- `name`: Page title -- `url`: Page URL -- `snippet`: Short description -- `summary`: Full text summary (if requested) -- `siteName`: Website name -- `siteIcon`: Website icon URL -- `datePublished`: Publication date (UTC+8) -- `language`: Page language - -**Images** (in `data.images.value`): -- `contentUrl`: Image URL -- `hostPageUrl`: Source page URL -- `width`, `height`: Image dimensions -- `thumbnailUrl`: Thumbnail URL - -## Notes - -- **Optimized for AI**: Results include summaries and structured metadata -- **Time range**: Use `noLimit` for best results (algorithm auto-optimizes time range) -- **Timeout**: 30 seconds -- **Rate limits**: Check your API plan at https://open.bocha.cn -- **Response format**: Compatible with Bing Search API for easy integration diff --git a/skills/bocha-search/scripts/search.sh b/skills/bocha-search/scripts/search.sh deleted file mode 100755 index e803a19..0000000 --- a/skills/bocha-search/scripts/search.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash -# Bocha Web Search API wrapper -# API Docs: https://open.bocha.cn/ - -set -euo pipefail - -query="${1:-}" -count="${2:-10}" -freshness="${3:-noLimit}" -summary="${4:-false}" - -if [ -z "$query" ]; then - echo '{"error": "Query is required", "usage": "bash search.sh [count] [freshness] [summary]"}' - exit 1 -fi - -if [ -z "${BOCHA_API_KEY:-}" ]; then - echo '{"error": "BOCHA_API_KEY environment variable is not set", "help": "Visit https://open.bocha.cn to get an API key"}' - exit 1 -fi - -# Validate count (1-50) -if ! [[ "$count" =~ ^[0-9]+$ ]] || [ "$count" -lt 1 ] || [ "$count" -gt 50 ]; then - count=10 -fi - -# Build JSON request body -request_body=$(cat <&1) - -curl_exit_code=$? - -if [ $curl_exit_code -ne 0 ]; then - echo "{\"error\": \"Failed to call Bocha API\", \"details\": \"$response\"}" - exit 1 -fi - -# Simple JSON validation - check if response starts with { or [ -if [[ ! "$response" =~ ^[[:space:]]*[\{\[] ]]; then - echo "{\"error\": \"Invalid JSON response from API\", \"response\": \"$response\"}" - exit 1 -fi - -# Extract API code using grep and sed (basic JSON parsing) -api_code=$(echo "$response" | grep -o '"code"[[:space:]]*:[[:space:]]*[0-9]*' | grep -o '[0-9]*' | head -1) - -# If code extraction failed or code is not 200, check for error -if [ -n "$api_code" ] && [ "$api_code" != "200" ]; then - # Try to extract error message - api_msg=$(echo "$response" | grep -o '"msg"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/"msg"[[:space:]]*:[[:space:]]*"\(.*\)"/\1/' | head -1) - if [ -z "$api_msg" ]; then - api_msg="Unknown error" - fi - echo "{\"error\": \"API returned error\", \"code\": $api_code, \"message\": \"$api_msg\"}" - exit 1 -fi - -# Return the full response -echo "$response" diff --git a/skills/skill-creator/SKILL.md b/skills/skill-creator/SKILL.md index 9092cb9..6b6d5d1 100644 --- a/skills/skill-creator/SKILL.md +++ b/skills/skill-creator/SKILL.md @@ -1,6 +1,6 @@ --- name: skill-creator -description: Create or update skills. Use when designing, structuring, or packaging skills with scripts, references, and assets. COW simplified version - skills are used locally in workspace. +description: Create, install, or update skills in the workspace. Use when (1) installing a skill from a URL or remote source, (2) creating a new skill from scratch, (3) updating or restructuring existing skills. Always use this skill for any skill installation or creation task. license: Complete terms in LICENSE.txt --- @@ -93,9 +93,16 @@ Do NOT create auxiliary documentation files: **Critical Rule**: Only create files that the agent will actually execute (scripts) or that are too large for SKILL.md (references). Documentation, examples, and guides ALL belong in SKILL.md. -## Skill Creation Process +## Installing a Skill from URL -**COW Simplified Version** - Skills are used locally, no packaging/sharing needed. +1. Fetch the URL content (curl or web-fetch skill) +2. Extract `name` from YAML frontmatter +3. Create directory `/skills//` and save content as `SKILL.md` +4. Check the saved SKILL.md for an installation/setup section — if it defines additional steps (e.g., downloading scripts, installing dependencies), execute them; otherwise installation is complete + +The `` is the working directory from the "工作空间" section. + +## Skill Creation Process (from scratch) 1. **Understand** - Clarify use cases with concrete examples 2. **Plan** - Identify needed scripts, references, assets @@ -181,11 +188,13 @@ scripts/init_skill.py --path [--resources script Examples: ```bash -scripts/init_skill.py my-skill --path ~/cow/skills -scripts/init_skill.py my-skill --path ~/cow/skills --resources scripts,references -scripts/init_skill.py my-skill --path ~/cow/skills --resources scripts --examples +scripts/init_skill.py my-skill --path /skills +scripts/init_skill.py my-skill --path /skills --resources scripts,references +scripts/init_skill.py my-skill --path /skills --resources scripts --examples ``` +Where `` is your workspace directory shown in the "工作空间" section of the system prompt. + The script: - Creates the skill directory at the specified path @@ -195,7 +204,7 @@ The script: After initialization, customize the SKILL.md and add resources as needed. If you used `--examples`, replace or delete placeholder files. -**Important**: Always create skills in workspace directory (`~/cow/skills`), NOT in project directory. +**Important**: Always create skills in workspace skills directory (`/skills`), NOT in project directory. Check the "工作空间" section for the actual workspace path. ### Step 4: Edit the Skill @@ -335,7 +344,7 @@ scripts/quick_validate.py Example: ```bash -scripts/quick_validate.py ~/cow/skills/weather-api +scripts/quick_validate.py /skills/weather-api ``` Validation checks: