mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-02-27 08:00:36 +08:00
Merge pull request #2651 from zhayujie/feat-cow-agent
fix: optimize suggestion words and retries
This commit is contained in:
@@ -456,7 +456,7 @@ class MemoryManager:
|
||||
- 当天笔记 → memory/{today_file}
|
||||
- 静默存储,仅在明确要求时确认
|
||||
|
||||
**使用原则**: 自然使用记忆,就像你本来就知道。不要主动提起或列举记忆,除非用户明确询问。"""
|
||||
**使用原则**: 自然使用记忆,就像你本来就知道。不需要生硬地提起或列举记忆,除非用户提到。"""
|
||||
else:
|
||||
guidance = f"""## Memory System
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
System Prompt Builder - 系统提示词构建器
|
||||
|
||||
参考 clawdbot 的 system-prompt.ts,实现中文版的模块化提示词构建
|
||||
实现模块化的系统提示词构建,支持工具、技能、记忆等多个子系统
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -90,22 +90,21 @@ def build_agent_system_prompt(
|
||||
**kwargs
|
||||
) -> str:
|
||||
"""
|
||||
构建Agent系统提示词(精简版,中文)
|
||||
构建Agent系统提示词
|
||||
|
||||
包含的sections:
|
||||
1. 基础身份
|
||||
2. 工具说明
|
||||
3. 技能系统
|
||||
4. 记忆系统
|
||||
5. 用户身份
|
||||
6. 文档路径
|
||||
7. 工作空间
|
||||
8. 项目上下文文件
|
||||
顺序说明(按重要性和逻辑关系排列):
|
||||
1. 工具系统 - 核心能力,最先介绍
|
||||
2. 技能系统 - 紧跟工具,因为技能需要用 read 工具读取
|
||||
3. 记忆系统 - 独立的记忆能力
|
||||
4. 工作空间 - 工作环境说明
|
||||
5. 用户身份 - 用户信息(可选)
|
||||
6. 项目上下文 - SOUL.md, USER.md, AGENTS.md(定义人格和身份)
|
||||
7. 运行时信息 - 元信息(时间、模型等)
|
||||
|
||||
Args:
|
||||
workspace_dir: 工作空间目录
|
||||
language: 语言 ("zh" 或 "en")
|
||||
base_persona: 基础人格描述
|
||||
base_persona: 基础人格描述(已废弃,由SOUL.md定义)
|
||||
user_identity: 用户身份信息
|
||||
tools: 工具列表
|
||||
context_files: 上下文文件列表
|
||||
@@ -120,33 +119,30 @@ def build_agent_system_prompt(
|
||||
"""
|
||||
sections = []
|
||||
|
||||
# 1. 基础身份
|
||||
sections.extend(_build_identity_section(base_persona, language))
|
||||
|
||||
# 2. 工具说明
|
||||
# 1. 工具系统(最重要,放在最前面)
|
||||
if tools:
|
||||
sections.extend(_build_tooling_section(tools, language))
|
||||
|
||||
# 3. 技能系统
|
||||
# 2. 技能系统(紧跟工具,因为需要用 read 工具)
|
||||
if skill_manager:
|
||||
sections.extend(_build_skills_section(skill_manager, tools, language))
|
||||
|
||||
# 4. 记忆系统
|
||||
# 3. 记忆系统(独立的记忆能力)
|
||||
if memory_manager:
|
||||
sections.extend(_build_memory_section(memory_manager, tools, language))
|
||||
|
||||
# 5. 用户身份
|
||||
# 4. 工作空间(工作环境说明)
|
||||
sections.extend(_build_workspace_section(workspace_dir, language, is_first_conversation))
|
||||
|
||||
# 5. 用户身份(如果有)
|
||||
if user_identity:
|
||||
sections.extend(_build_user_identity_section(user_identity, language))
|
||||
|
||||
# 6. 工作空间
|
||||
sections.extend(_build_workspace_section(workspace_dir, language, is_first_conversation))
|
||||
|
||||
# 7. 项目上下文文件(SOUL.md, USER.md等)
|
||||
# 6. 项目上下文文件(SOUL.md, USER.md, AGENTS.md - 定义人格)
|
||||
if context_files:
|
||||
sections.extend(_build_context_files_section(context_files, language))
|
||||
|
||||
# 8. 运行时信息(如果有)
|
||||
# 7. 运行时信息(元信息,放在最后)
|
||||
if runtime_info:
|
||||
sections.extend(_build_runtime_section(runtime_info, language))
|
||||
|
||||
@@ -270,9 +266,9 @@ def _build_skills_section(skill_manager: Any, tools: Optional[List[Any]], langua
|
||||
"",
|
||||
"在回复之前:扫描下方 <available_skills> 中的 <description> 条目。",
|
||||
"",
|
||||
f"- 如果恰好有一个技能明确适用:使用 `{read_tool_name}` 工具读取其 <location> 路径下的 SKILL.md 文件,然后遵循它。",
|
||||
"- 如果多个技能都适用:选择最具体的一个,然后读取并遵循。",
|
||||
"- 如果没有明确适用的:不要读取任何 SKILL.md。",
|
||||
f"- 如果恰好有一个技能明确适用:使用 `{read_tool_name}` 工具读取其 <location> 路径下的 SKILL.md 文件,然后遵循它",
|
||||
"- 如果多个技能都适用:选择最具体的一个,然后读取并遵循",
|
||||
"- 如果没有明确适用的:不要读取任何 SKILL.md",
|
||||
"",
|
||||
"**约束**: 永远不要一次性读取多个技能;只在选择后再读取。",
|
||||
"",
|
||||
@@ -455,7 +451,27 @@ def _build_runtime_section(runtime_info: Dict[str, Any], language: str) -> List[
|
||||
if not runtime_info:
|
||||
return []
|
||||
|
||||
# Only include if there's actual runtime info to display
|
||||
lines = [
|
||||
"## 运行时信息",
|
||||
"",
|
||||
]
|
||||
|
||||
# Add current time if available
|
||||
if runtime_info.get("current_time"):
|
||||
time_str = runtime_info["current_time"]
|
||||
weekday = runtime_info.get("weekday", "")
|
||||
timezone = runtime_info.get("timezone", "")
|
||||
|
||||
time_line = f"当前时间: {time_str}"
|
||||
if weekday:
|
||||
time_line += f" {weekday}"
|
||||
if timezone:
|
||||
time_line += f" ({timezone})"
|
||||
|
||||
lines.append(time_line)
|
||||
lines.append("")
|
||||
|
||||
# Add other runtime info
|
||||
runtime_parts = []
|
||||
if runtime_info.get("model"):
|
||||
runtime_parts.append(f"模型={runtime_info['model']}")
|
||||
@@ -465,14 +481,8 @@ def _build_runtime_section(runtime_info: Dict[str, Any], language: str) -> List[
|
||||
if runtime_info.get("channel") and runtime_info.get("channel") != "web":
|
||||
runtime_parts.append(f"渠道={runtime_info['channel']}")
|
||||
|
||||
if not runtime_parts:
|
||||
return []
|
||||
|
||||
lines = [
|
||||
"## 运行时信息",
|
||||
"",
|
||||
"运行时: " + " | ".join(runtime_parts),
|
||||
""
|
||||
]
|
||||
if runtime_parts:
|
||||
lines.append("运行时: " + " | ".join(runtime_parts))
|
||||
lines.append("")
|
||||
|
||||
return lines
|
||||
|
||||
@@ -236,24 +236,9 @@ def _get_agents_template() -> str:
|
||||
|
||||
这个文件夹是你的家。好好对待它。
|
||||
|
||||
## 系统自动加载
|
||||
|
||||
以下文件在每次会话启动时**已经自动加载**到系统提示词中,你无需再次读取:
|
||||
|
||||
- ✅ `SOUL.md` - 你的人格设定(已加载)
|
||||
- ✅ `USER.md` - 用户信息(已加载)
|
||||
- ✅ `AGENTS.md` - 本文件(已加载)
|
||||
|
||||
## 按需读取
|
||||
|
||||
以下文件**不会自动加载**,需要时使用相应工具读取:
|
||||
|
||||
- 📝 `memory/YYYY-MM-DD.md` - 每日记忆(用 memory_search 检索)
|
||||
- 🧠 `MEMORY.md` - 长期记忆(用 memory_search 检索)
|
||||
|
||||
## 记忆系统
|
||||
|
||||
你每次会话都是全新的。这些文件是你的连续性:
|
||||
你每次会话都是全新的,记忆文件让你保持连续性:
|
||||
|
||||
### 📝 每日记忆:`memory/YYYY-MM-DD.md`
|
||||
- 原始的对话日志
|
||||
@@ -296,13 +281,9 @@ def _get_agents_template() -> str:
|
||||
- 不要在未经询问的情况下运行破坏性命令
|
||||
- 当有疑问时,先问
|
||||
|
||||
## 工具使用
|
||||
## 工作空间演化
|
||||
|
||||
技能提供你的工具。当你需要一个时,查看它的 `SKILL.md`。
|
||||
|
||||
## 让它成为你的
|
||||
|
||||
这只是一个起点。随着你弄清楚什么有效,添加你自己的约定、风格和规则。
|
||||
这个工作空间会随着你的使用而不断成长。当你学到新东西、发现更好的方式,或者犯错后改正时,记录下来。
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -95,15 +95,16 @@ class Agent:
|
||||
"""
|
||||
Get the full system prompt including skills.
|
||||
|
||||
:param skill_filter: Optional list of skill names to include
|
||||
:return: Complete system prompt with skills appended
|
||||
"""
|
||||
base_prompt = self.system_prompt
|
||||
skills_prompt = self.get_skills_prompt(skill_filter=skill_filter)
|
||||
Note: Skills are now built into the system prompt by PromptBuilder,
|
||||
so we just return the base prompt directly. This method is kept for
|
||||
backward compatibility.
|
||||
|
||||
if skills_prompt:
|
||||
return base_prompt + "\n" + skills_prompt
|
||||
return base_prompt
|
||||
:param skill_filter: Optional list of skill names to include (deprecated)
|
||||
:return: Complete system prompt
|
||||
"""
|
||||
# Skills are now included in system_prompt by PromptBuilder
|
||||
# No need to append them here
|
||||
return self.system_prompt
|
||||
|
||||
def refresh_skills(self):
|
||||
"""Refresh the loaded skills."""
|
||||
|
||||
@@ -55,6 +55,9 @@ class AgentStreamExecutor:
|
||||
|
||||
# Message history - use provided messages or create new list
|
||||
self.messages = messages if messages is not None else []
|
||||
|
||||
# Tool failure tracking for retry protection
|
||||
self.tool_failure_history = [] # List of (tool_name, args_hash, success) tuples
|
||||
|
||||
def _emit_event(self, event_type: str, data: dict = None):
|
||||
"""Emit event"""
|
||||
@@ -68,6 +71,60 @@ class AgentStreamExecutor:
|
||||
except Exception as e:
|
||||
logger.error(f"Event callback error: {e}")
|
||||
|
||||
def _hash_args(self, args: dict) -> str:
|
||||
"""Generate a simple hash for tool arguments"""
|
||||
import hashlib
|
||||
# Sort keys for consistent hashing
|
||||
args_str = json.dumps(args, sort_keys=True, ensure_ascii=False)
|
||||
return hashlib.md5(args_str.encode()).hexdigest()[:8]
|
||||
|
||||
def _check_consecutive_failures(self, tool_name: str, args: dict) -> tuple[bool, str]:
|
||||
"""
|
||||
Check if tool has failed too many times consecutively
|
||||
|
||||
Returns:
|
||||
(should_stop, reason)
|
||||
"""
|
||||
args_hash = self._hash_args(args)
|
||||
|
||||
# Count consecutive failures for same tool + args
|
||||
same_args_failures = 0
|
||||
for name, ahash, success in reversed(self.tool_failure_history):
|
||||
if name == tool_name and ahash == args_hash:
|
||||
if not success:
|
||||
same_args_failures += 1
|
||||
else:
|
||||
break # Stop at first success
|
||||
else:
|
||||
break # Different tool or args, stop counting
|
||||
|
||||
if same_args_failures >= 3:
|
||||
return True, f"Tool '{tool_name}' with same arguments failed {same_args_failures} times consecutively. Stopping to prevent infinite loop."
|
||||
|
||||
# Count consecutive failures for same tool (any args)
|
||||
same_tool_failures = 0
|
||||
for name, ahash, success in reversed(self.tool_failure_history):
|
||||
if name == tool_name:
|
||||
if not success:
|
||||
same_tool_failures += 1
|
||||
else:
|
||||
break # Stop at first success
|
||||
else:
|
||||
break # Different tool, stop counting
|
||||
|
||||
if same_tool_failures >= 6:
|
||||
return True, f"Tool '{tool_name}' failed {same_tool_failures} times consecutively (with any arguments). Stopping to prevent infinite loop."
|
||||
|
||||
return False, ""
|
||||
|
||||
def _record_tool_result(self, tool_name: str, args: dict, success: bool):
|
||||
"""Record tool execution result for failure tracking"""
|
||||
args_hash = self._hash_args(args)
|
||||
self.tool_failure_history.append((tool_name, args_hash, success))
|
||||
# Keep only last 50 records to avoid memory bloat
|
||||
if len(self.tool_failure_history) > 50:
|
||||
self.tool_failure_history = self.tool_failure_history[-50:]
|
||||
|
||||
def run_stream(self, user_message: str) -> str:
|
||||
"""
|
||||
Execute streaming reasoning loop
|
||||
@@ -413,7 +470,16 @@ class AgentStreamExecutor:
|
||||
arguments = json.loads(tc["arguments"]) if tc["arguments"] else {}
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse tool arguments: {tc['arguments']}")
|
||||
arguments = {}
|
||||
logger.error(f"JSON decode error: {e}")
|
||||
# Return a clear error message to the LLM instead of empty dict
|
||||
# This helps the LLM understand what went wrong
|
||||
tool_calls.append({
|
||||
"id": tc["id"],
|
||||
"name": tc["name"],
|
||||
"arguments": {},
|
||||
"_parse_error": f"Invalid JSON in tool arguments: {tc['arguments'][:100]}... Error: {str(e)}"
|
||||
})
|
||||
continue
|
||||
|
||||
tool_calls.append({
|
||||
"id": tc["id"],
|
||||
@@ -481,6 +547,31 @@ class AgentStreamExecutor:
|
||||
tool_id = tool_call["id"]
|
||||
arguments = tool_call["arguments"]
|
||||
|
||||
# Check if there was a JSON parse error
|
||||
if "_parse_error" in tool_call:
|
||||
parse_error = tool_call["_parse_error"]
|
||||
logger.error(f"Skipping tool execution due to parse error: {parse_error}")
|
||||
result = {
|
||||
"status": "error",
|
||||
"result": f"Failed to parse tool arguments. {parse_error}. Please ensure your tool call uses valid JSON format with all required parameters.",
|
||||
"execution_time": 0
|
||||
}
|
||||
self._record_tool_result(tool_name, arguments, False)
|
||||
return result
|
||||
|
||||
# Check for consecutive failures (retry protection)
|
||||
should_stop, stop_reason = self._check_consecutive_failures(tool_name, arguments)
|
||||
if should_stop:
|
||||
logger.error(f"🛑 {stop_reason}")
|
||||
self._record_tool_result(tool_name, arguments, False)
|
||||
# 返回错误给 LLM,让它尝试其他方法
|
||||
result = {
|
||||
"status": "error",
|
||||
"result": f"{stop_reason}\n\nThis approach is not working. Please try a completely different method or ask the user for more information/clarification.",
|
||||
"execution_time": 0
|
||||
}
|
||||
return result
|
||||
|
||||
self._emit_event("tool_execution_start", {
|
||||
"tool_call_id": tool_id,
|
||||
"tool_name": tool_name,
|
||||
@@ -507,6 +598,10 @@ class AgentStreamExecutor:
|
||||
"execution_time": execution_time
|
||||
}
|
||||
|
||||
# Record tool result for failure tracking
|
||||
success = result.status == "success"
|
||||
self._record_tool_result(tool_name, arguments, success)
|
||||
|
||||
# Auto-refresh skills after skill creation
|
||||
if tool_name == "bash" and result.status == "success":
|
||||
command = arguments.get("command", "")
|
||||
@@ -530,6 +625,9 @@ class AgentStreamExecutor:
|
||||
"result": str(e),
|
||||
"execution_time": 0
|
||||
}
|
||||
# Record failure
|
||||
self._record_tool_result(tool_name, arguments, False)
|
||||
|
||||
self._emit_event("tool_execution_end", {
|
||||
"tool_call_id": tool_id,
|
||||
"tool_name": tool_name,
|
||||
|
||||
@@ -4,7 +4,6 @@ from agent.tools.tool_manager import ToolManager
|
||||
|
||||
# Import basic tools (no external dependencies)
|
||||
from agent.tools.calculator.calculator import Calculator
|
||||
from agent.tools.current_time.current_time import CurrentTime
|
||||
|
||||
# Import file operation tools
|
||||
from agent.tools.read.read import Read
|
||||
@@ -82,7 +81,6 @@ __all__ = [
|
||||
'BaseTool',
|
||||
'ToolManager',
|
||||
'Calculator',
|
||||
'CurrentTime',
|
||||
'Read',
|
||||
'Write',
|
||||
'Edit',
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import datetime
|
||||
import time
|
||||
|
||||
from agent.tools.base_tool import BaseTool, ToolResult
|
||||
|
||||
|
||||
class CurrentTime(BaseTool):
|
||||
name: str = "time"
|
||||
description: str = "A tool to get current date and time information."
|
||||
params: dict = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "Optional format for the time (e.g., 'iso', 'unix', 'human'). Default is 'human'."
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string",
|
||||
"description": "Optional timezone specification (e.g., 'UTC', 'local'). Default is 'local'."
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
config: dict = {}
|
||||
|
||||
def execute(self, args: dict) -> ToolResult:
|
||||
try:
|
||||
# Get the format and timezone parameters, with defaults
|
||||
time_format = args.get("format", "human").lower()
|
||||
timezone = args.get("timezone", "local").lower()
|
||||
|
||||
# Get current time
|
||||
current_time = datetime.datetime.now()
|
||||
|
||||
# Handle timezone if specified
|
||||
if timezone == "utc":
|
||||
current_time = datetime.datetime.utcnow()
|
||||
|
||||
# Format the time according to the specified format
|
||||
if time_format == "iso":
|
||||
# ISO 8601 format
|
||||
formatted_time = current_time.isoformat()
|
||||
elif time_format == "unix":
|
||||
# Unix timestamp (seconds since epoch)
|
||||
formatted_time = time.time()
|
||||
else:
|
||||
# Human-readable format
|
||||
formatted_time = current_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Prepare additional time components for the response
|
||||
year = current_time.year
|
||||
month = current_time.month
|
||||
day = current_time.day
|
||||
hour = current_time.hour
|
||||
minute = current_time.minute
|
||||
second = current_time.second
|
||||
weekday = current_time.strftime("%A") # Full weekday name
|
||||
|
||||
result = {
|
||||
"current_time": formatted_time,
|
||||
"components": {
|
||||
"year": year,
|
||||
"month": month,
|
||||
"day": day,
|
||||
"hour": hour,
|
||||
"minute": minute,
|
||||
"second": second,
|
||||
"weekday": weekday
|
||||
},
|
||||
"format": time_format,
|
||||
"timezone": timezone
|
||||
}
|
||||
return ToolResult.success(result=result)
|
||||
except Exception as e:
|
||||
return ToolResult.fail(result=str(e))
|
||||
@@ -473,6 +473,15 @@ class AgentBridge:
|
||||
# Load context files
|
||||
context_files = load_context_files(workspace_root)
|
||||
|
||||
# Initialize skill manager
|
||||
skill_manager = None
|
||||
try:
|
||||
from agent.skills import SkillManager
|
||||
skill_manager = SkillManager(workspace_dir=workspace_root)
|
||||
logger.info(f"[AgentBridge] Initialized SkillManager with {len(skill_manager.skills)} skills for session {session_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[AgentBridge] Failed to initialize SkillManager for session {session_id}: {e}")
|
||||
|
||||
# Check if this is the first conversation
|
||||
from agent.prompt.workspace import is_first_conversation, mark_conversation_started
|
||||
is_first = is_first_conversation(workspace_root)
|
||||
@@ -483,15 +492,49 @@ class AgentBridge:
|
||||
language="zh"
|
||||
)
|
||||
|
||||
# Get current time and timezone info
|
||||
import datetime
|
||||
import time
|
||||
|
||||
now = datetime.datetime.now()
|
||||
|
||||
# Get timezone info
|
||||
try:
|
||||
offset = -time.timezone if not time.daylight else -time.altzone
|
||||
hours = offset // 3600
|
||||
minutes = (offset % 3600) // 60
|
||||
if minutes:
|
||||
timezone_name = f"UTC{hours:+03d}:{minutes:02d}"
|
||||
else:
|
||||
timezone_name = f"UTC{hours:+03d}"
|
||||
except Exception:
|
||||
timezone_name = "UTC"
|
||||
|
||||
# Chinese weekday mapping
|
||||
weekday_map = {
|
||||
'Monday': '星期一',
|
||||
'Tuesday': '星期二',
|
||||
'Wednesday': '星期三',
|
||||
'Thursday': '星期四',
|
||||
'Friday': '星期五',
|
||||
'Saturday': '星期六',
|
||||
'Sunday': '星期日'
|
||||
}
|
||||
weekday_zh = weekday_map.get(now.strftime("%A"), now.strftime("%A"))
|
||||
|
||||
runtime_info = {
|
||||
"model": conf().get("model", "unknown"),
|
||||
"workspace": workspace_root,
|
||||
"channel": conf().get("channel_type", "unknown")
|
||||
"channel": conf().get("channel_type", "unknown"),
|
||||
"current_time": now.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"weekday": weekday_zh,
|
||||
"timezone": timezone_name
|
||||
}
|
||||
|
||||
system_prompt = prompt_builder.build(
|
||||
tools=tools,
|
||||
context_files=context_files,
|
||||
skill_manager=skill_manager,
|
||||
memory_manager=memory_manager,
|
||||
runtime_info=runtime_info,
|
||||
is_first_conversation=is_first
|
||||
@@ -507,6 +550,7 @@ class AgentBridge:
|
||||
max_steps=50,
|
||||
output_mode="logger",
|
||||
workspace_dir=workspace_root,
|
||||
skill_manager=skill_manager,
|
||||
enable_skills=True
|
||||
)
|
||||
|
||||
|
||||
@@ -285,7 +285,18 @@ class FeiShuChanel(ChatChannel):
|
||||
context["origin_ctype"] = ctype
|
||||
|
||||
cmsg = context["msg"]
|
||||
context["session_id"] = cmsg.from_user_id
|
||||
|
||||
# Set session_id based on chat type to ensure proper session isolation
|
||||
if cmsg.is_group:
|
||||
# Group chat: combine user_id and group_id to create unique session per user per group
|
||||
# This ensures:
|
||||
# - Same user in different groups have separate conversation histories
|
||||
# - Same user in private chat and group chat have separate histories
|
||||
context["session_id"] = f"{cmsg.from_user_id}:{cmsg.other_user_id}"
|
||||
else:
|
||||
# Private chat: use user_id only
|
||||
context["session_id"] = cmsg.from_user_id
|
||||
|
||||
context["receiver"] = cmsg.other_user_id
|
||||
|
||||
if ctype == ContextType.TEXT:
|
||||
|
||||
Reference in New Issue
Block a user