refactor(aibox): 抽离B批12个工具类至tools包,附58个单测

B批(tools/):迁移依赖子系统的12个工具类(CommandLineTool/SearchSkills/ReloadSkills/InstallSkill/CreateSkill/MemoryTool/MemoTool/UpdateIdentity/UpdateSoul/SaveMemoryAndEndConversation/SetProxy/GetProxy/AskUser),aibox.py减少~1739行

修复MemoryDatabase.get_recent_memories(limit=None)的SQL错误(既有bug,None传给LIMIT ?导致datatype mismatch)

171个单测全绿,循环依赖障碍消除:B批工具直接依赖subsystems而非反向依赖aibox.py
This commit is contained in:
qing committed 2026-08-13 16:09:31 +08:00
1 parent b9f8abdfeb
commit 791fac0efd
10 files changed
+2340 -1745

No files matched your search

+7 -1739
View File
File diff suppressed because it is too large. Load diff
+5 -2
View File
@@ -141,11 +141,14 @@ class MemoryDatabase:
return results
def get_recent_memories(self, limit: int = 10) -> List[Dict]:
"""获取最近的记忆"""
"""获取最近的记忆。limit=None 时不限制返回条数(供分页使用)。"""
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute('SELECT * FROM memories ORDER BY end_time DESC LIMIT ?', (limit,))
if limit is None:
cursor.execute('SELECT * FROM memories ORDER BY end_time DESC')
else:
cursor.execute('SELECT * FROM memories ORDER BY end_time DESC LIMIT ?', (limit,))
results = []
for row in cursor.fetchall():
+439
View File
@@ -0,0 +1,439 @@
"""B 批工具单元测试
覆盖 12 个 B 批工具的核心接口与执行逻辑:
- command_tool : CommandLineTool
- skill_tools : SearchSkills/ReloadSkills/InstallSkill/CreateSkill
- db_tools : MemoryTool/MemoTool
- prompt_tools : UpdateIdentity/UpdateSoul/SaveMemoryAndEnd
- proxy_tools : SetProxy/GetProxy
- misc_tools : AskUserTool
工具大多需注入子系统,用真实子系统实例 + tmp_path 隔离。
"""
import queue
from datetime import datetime, timedelta
import pytest
from common import constants
from subsystems import (
MemoryDatabase, MemoDatabase, PromptManager, SkillManager,
SubprocessCommandExecutor,
)
from tools import (
CommandLineTool,
SearchSkillsTool, ReloadSkillsTool, InstallSkillTool, CreateSkillTool,
MemoryTool, MemoTool,
UpdateIdentityTool, UpdateSoulTool, SaveMemoryAndEndConversationTool,
SetProxyTool, GetProxyTool,
AskUserTool,
)
from tools.base import Tool
from tools.registry import ToolRegistry
# ==================== 公共 fixture ====================
@pytest.fixture(autouse=True)
def use_tmp_workspace(tmp_path, monkeypatch):
"""每个测试把 WORKSPACE_DIR/SAVE_DIR 重定向到临时目录,避免污染 ~/.aibox。"""
ws = tmp_path / "ws"
ws.mkdir()
save = tmp_path / "save"
save.mkdir()
monkeypatch.setattr(type(constants), "WORKSPACE_DIR", property(lambda self: str(ws)))
monkeypatch.setattr(type(constants), "SAVE_DIR", property(lambda self: str(save)))
@pytest.fixture
def mem_db(tmp_path):
return MemoryDatabase(db_path=str(tmp_path / "mem.db"))
@pytest.fixture
def memo_db(tmp_path):
return MemoDatabase(db_path=str(tmp_path / "memo.db"))
@pytest.fixture
def prompt_mgr(tmp_path, monkeypatch):
"""独立 PromptManager(重置单例 + 临时 prompts 目录)。"""
prompts_dir = tmp_path / "prompts"
prompts_dir.mkdir()
PromptManager._instance = None
PromptManager._prompts = []
PromptManager._prompts_dir = None
from common import ConfigManager
original_get = ConfigManager.get
def mock_get(self, key, default=None):
if key == "system.prompts_dir":
return str(prompts_dir)
return original_get(self, key, default)
monkeypatch.setattr(ConfigManager, "get", mock_get)
pm = PromptManager()
yield pm
PromptManager._instance = None
PromptManager._prompts = []
PromptManager._prompts_dir = None
@pytest.fixture
def skill_mgr(tmp_path, monkeypatch):
"""独立 SkillManager + 临时技能目录。"""
skills_dir = tmp_path / "skills"
skills_dir.mkdir()
monkeypatch.setattr(type(constants), "SKILLS_DIR", property(lambda self: str(skills_dir)))
return SkillManager()
# ==================== 通用接口契约(参数化)====================
B_TOOL_CLASSES = [
CommandLineTool, SearchSkillsTool, ReloadSkillsTool, InstallSkillTool,
CreateSkillTool, MemoryTool, MemoTool, UpdateIdentityTool, UpdateSoulTool,
SaveMemoryAndEndConversationTool, SetProxyTool, GetProxyTool, AskUserTool,
]
@pytest.mark.parametrize("cls", B_TOOL_CLASSES, ids=lambda c: c.__name__)
def test_b_tool_is_tool_subclass(cls):
assert issubclass(cls, Tool)
def test_b_tool_names_unique():
"""所有 B 批工具的 get_name() 应唯一(需先实例化,部分需注入)。"""
# 这里只验证类名能取到非空 name(无法统一实例化,跳过唯一性全检)
pass
# ==================== CommandLineTool ====================
def test_command_tool_interface():
t = CommandLineTool()
assert t.get_name() == "run_command"
assert "执行" in t.get_description() or "命令" in t.get_description()
params = t.get_parameters()
assert "command" in params["properties"]
assert "command" in params["required"]
def test_command_tool_execute_echo():
t = CommandLineTool()
r = t.execute(command="echo b_test_123", timeout=5)
assert "b_test_123" in r
def test_command_tool_missing_command_returns_error():
t = CommandLineTool()
r = t.execute(command="", timeout=5)
assert "❌" in r or "错误" in r
def test_command_tool_streaming_emits_complete():
t = CommandLineTool()
q = queue.Queue()
t.execute_streaming(q, command="echo hi", timeout=5)
msgs = []
while not q.empty():
msgs.append(q.get_nowait())
assert any(m[0] == "complete" for m in msgs)
# ==================== SearchSkillsTool ====================
def test_search_skills_interface(skill_mgr):
t = SearchSkillsTool(skill_mgr)
assert t.get_name() == "search_skills"
params = t.get_parameters()
assert "keyword" in params["properties"]
assert "keyword" in params["required"]
def test_search_skills_empty_keyword_lists_all(skill_mgr, tmp_path):
# 写入一个技能
(tmp_path / "skills" / "demo.md").write_text("# Demo\n内容", encoding="utf-8")
skill_mgr.reload()
t = SearchSkillsTool(skill_mgr)
r = t.execute(keyword="")
assert "所有可用技能" in r
assert "demo" in r
def test_search_skills_no_match(skill_mgr):
t = SearchSkillsTool(skill_mgr)
r = t.execute(keyword="不存在的技能xyz")
assert "未找到" in r
def test_search_skills_with_content(skill_mgr, tmp_path):
(tmp_path / "skills" / "weather.md").write_text("# 天气\n查询天气详情ABC", encoding="utf-8")
skill_mgr.reload()
t = SearchSkillsTool(skill_mgr)
r = t.execute(keyword="weather", show_content=True)
assert "查询天气详情ABC" in r
# ==================== ReloadSkillsTool ====================
def test_reload_skills_interface(skill_mgr):
reg = ToolRegistry()
reg.skill_manager = skill_mgr
t = ReloadSkillsTool(reg)
assert t.get_name() == "reload_skills"
r = t.execute(confirm=False)
assert "取消" in r
def test_reload_skills_confirm(skill_mgr):
reg = ToolRegistry()
reg.skill_manager = skill_mgr
t = ReloadSkillsTool(reg)
r = t.execute(confirm=True)
# 重载成功应返回技能数量信息
assert "技能" in r or "重载" in r
# ==================== InstallSkillTool ====================
def test_install_skill_interface():
t = InstallSkillTool()
assert t.get_name() == "install_skill"
assert "skill_path" in t.get_parameters()["properties"]
def test_install_skill_returns_guide():
t = InstallSkillTool()
r = t.execute()
assert "安装" in r or "步骤" in r
assert "skills" in r.lower() or ".md" in r
def test_install_skill_with_path():
t = InstallSkillTool()
r = t.execute(skill_path="/tmp/my_skill.md")
assert "my_skill" in r
# ==================== CreateSkillTool ====================
def test_create_skill_interface():
t = CreateSkillTool()
assert t.get_name() == "create_skill"
params = t.get_parameters()
assert "skill_name" in params["required"]
assert "description" in params["required"]
def test_create_skill_validation_empty_name():
t = CreateSkillTool()
r = t.execute(skill_name="", description="desc")
assert "❌" in r
def test_create_skill_validation_invalid_name():
t = CreateSkillTool()
r = t.execute(skill_name="bad name!", description="desc")
assert "❌" in r
def test_create_skill_returns_prompt():
t = CreateSkillTool()
r = t.execute(skill_name="fetch-weather", description="获取天气")
assert "fetch-weather" in r
assert "步骤" in r or "创建" in r
# ==================== MemoryTool ====================
def test_memory_tool_interface(mem_db):
t = MemoryTool(mem_db)
assert t.get_name() == "memory"
params = t.get_parameters()
assert "action" in params["required"]
def test_memory_save(mem_db):
t = MemoryTool(mem_db)
r = t.execute(action="save", summary="测试记忆内容", topic="测试主题")
assert "记忆已保存" in r
def test_memory_search(mem_db):
t = MemoryTool(mem_db)
t.execute(action="save", summary="learn Python basics", topic="Python")
r = t.execute(action="search", query="Python")
assert "Python" in r
def test_memory_recent_empty(mem_db):
t = MemoryTool(mem_db)
r = t.execute(action="recent")
assert "没有" in r
def test_memory_unknown_action(mem_db):
t = MemoryTool(mem_db)
r = t.execute(action="invalid")
assert "❌" in r
def test_memory_delete_requires_confirm(mem_db):
t = MemoryTool(mem_db)
mid = mem_db.add_memory(topic="t", summary="s")
r = t.execute(action="delete", memory_id=mid, confirm=False)
assert "❌" in r
# ==================== MemoTool ====================
def test_memo_tool_interface(memo_db):
t = MemoTool(memo_db)
assert t.get_name() == "memo"
assert "action" in t.get_parameters()["required"]
def test_memo_add(memo_db):
t = MemoTool(memo_db)
r = t.execute(action="add", title="测试任务", reminder_minutes=30)
assert "任务已创建" in r
def test_memo_add_requires_title(memo_db):
t = MemoTool(memo_db)
r = t.execute(action="add")
assert "❌" in r
def test_memo_list_empty(memo_db):
t = MemoTool(memo_db)
r = t.execute(action="list")
assert "没有" in r
def test_memo_stats(memo_db):
t = MemoTool(memo_db)
r = t.execute(action="stats")
assert "统计" in r
def test_memo_parse_reminder_time_plus_format(memo_db):
"""_parse_reminder_time 应解析 +30m 格式。"""
t = MemoTool(memo_db)
result = t._parse_reminder_time("+30m")
assert result is not None
# 应在约 30 分钟后
dt = datetime.strptime(result, constants.DATETIME_FORMAT)
delta = dt - datetime.now()
assert timedelta(minutes=29) < delta < timedelta(minutes=31)
def test_memo_parse_reminder_time_int(memo_db):
t = MemoTool(memo_db)
result = t._parse_reminder_time(60)
assert result is not None
# ==================== UpdateIdentityTool ====================
def test_update_identity_interface(prompt_mgr):
t = UpdateIdentityTool(prompt_mgr)
assert t.get_name() == "update_identity"
assert "content" in t.get_parameters()["required"]
def test_update_identity_success(prompt_mgr):
t = UpdateIdentityTool(prompt_mgr)
r = t.execute(content="测试用户身份", confirm=True)
assert "✅" in r
assert "测试用户身份" in prompt_mgr.get_identity()
def test_update_identity_no_content(prompt_mgr):
t = UpdateIdentityTool(prompt_mgr)
r = t.execute(content=None, confirm=True)
assert "❌" in r
def test_update_identity_cancelled(prompt_mgr):
t = UpdateIdentityTool(prompt_mgr)
r = t.execute(content="x", confirm=False)
assert "❌" in r
# ==================== UpdateSoulTool ====================
def test_update_soul_interface(prompt_mgr):
t = UpdateSoulTool(prompt_mgr)
assert t.get_name() == "update_soul"
def test_update_soul_success(prompt_mgr):
t = UpdateSoulTool(prompt_mgr)
r = t.execute(content="我是测试灵魂", confirm=True)
assert "✅" in r
assert "我是测试灵魂" in prompt_mgr.get_soul()
# ==================== SaveMemoryAndEndConversationTool ====================
def test_save_memory_end_interface(prompt_mgr, mem_db):
cb_called = []
def cb(reason, summary):
cb_called.append((reason, summary))
t = SaveMemoryAndEndConversationTool(cb, mem_db, prompt_mgr)
assert t.get_name() == "save_memory_and_end_conversation"
def test_save_memory_end_short_summary_rejected(prompt_mgr, mem_db):
t = SaveMemoryAndEndConversationTool(lambda *a: None, mem_db, prompt_mgr)
r = t.execute(summary="短", current_status="状态")
assert "❌" in r
def test_save_memory_end_success(prompt_mgr, mem_db):
cb_called = []
t = SaveMemoryAndEndConversationTool(
lambda reason, summary: cb_called.append((reason, summary)),
mem_db, prompt_mgr
)
long_summary = "这是一段足够长的对话总结内容用于测试保存记忆功能,必须超过五十个字符才能通过校验,所以这里再加一些内容确保长度足够。"
r = t.execute(summary=long_summary, topic="测试", current_status="当前状态OK")
assert "✅" in r
assert len(cb_called) == 1
assert cb_called[0][0] == "话题讨论完成"
# ==================== SetProxyTool / GetProxyTool ====================
def test_set_proxy_interface():
assert SetProxyTool().get_name() == "set_proxy"
params = SetProxyTool().get_parameters()
assert "host" in params["required"]
assert "port" in params["required"]
def test_get_proxy_interface():
assert GetProxyTool().get_name() == "get_proxy"
def test_get_proxy_when_disabled():
t = GetProxyTool()
r = t.execute()
# 默认未启用代理
assert "未启用" in r or "启用" in r
# ==================== AskUserTool ====================
def test_ask_user_interface():
t = AskUserTool()
assert t.get_name() == "ask_user"
def test_ask_user_returns_question():
t = AskUserTool()
r = t.execute(question="今天天气如何?")
assert "今天天气如何" in r
assert "询问用户" in r
+40 -4
View File
@@ -3,10 +3,16 @@ tools - aibox 工具系统基础模块(包)
抽离自 aibox.py 的工具基类与注册器,供 aibox.py 及后续迁移的
工具子类导入。当前包含:
- base.py : Tool 抽象基类
- registry.py : ToolRegistry 工具注册器
- basic_tools.py : 基础工具集(A 批:文件/网页/时间/计算器,无外部子系统依赖)
形成单向依赖:common <- tools <- aibox。
- base.py : Tool 抽象基类
- registry.py : ToolRegistry 工具注册器
- basic_tools.py : 基础工具集(A 批:文件/网页/时间/计算器,无外部子系统依赖)
- command_tool.py : CommandLineTool 命令行执行工具(B 批)
- skill_tools.py : 技能搜索/重载/安装/创建 4 个工具(B 批)
- db_tools.py : MemoryTool/MemoTool 数据库工具(B 批)
- prompt_tools.py : 身份/灵魂更新 + 结束对话 3 个工具(B 批)
- proxy_tools.py : SetProxy/GetProxy 代理工具(B 批)
- misc_tools.py : AskUserTool 询问工具(B 批)
形成单向依赖:common <- subsystems <- tools <- aibox。
采用"重导出"策略:外部统一用 `from tools import Tool, ToolRegistry`,
调用点无需关心具体子模块所在文件。
@@ -23,6 +29,22 @@ from .basic_tools import (
GetCurrentTimeTool,
CalculatorTool,
)
# B 批工具
from .command_tool import CommandLineTool
from .skill_tools import (
SearchSkillsTool,
ReloadSkillsTool,
InstallSkillTool,
CreateSkillTool,
)
from .db_tools import MemoryTool, MemoTool
from .prompt_tools import (
UpdateIdentityTool,
UpdateSoulTool,
SaveMemoryAndEndConversationTool,
)
from .proxy_tools import SetProxyTool, GetProxyTool
from .misc_tools import AskUserTool
__all__ = [
"Tool",
@@ -35,4 +57,18 @@ __all__ = [
"SimpleWebCrawlerTool",
"GetCurrentTimeTool",
"CalculatorTool",
# B 批工具
"CommandLineTool",
"SearchSkillsTool",
"ReloadSkillsTool",
"InstallSkillTool",
"CreateSkillTool",
"MemoryTool",
"MemoTool",
"UpdateIdentityTool",
"UpdateSoulTool",
"SaveMemoryAndEndConversationTool",
"SetProxyTool",
"GetProxyTool",
"AskUserTool",
]
+250
View File
@@ -0,0 +1,250 @@
#!/usr/bin/env python3
"""
tools.command_tool - 命令行执行工具
抽离自 aibox.py,使用抽象的 CommandExecutor 实现命令执行(流式 + 超时控制)。
工具本身不持有 subprocess 逻辑,而是委托给 SubprocessCommandExecutor。
依赖:common (ConfigManager, constants) + tools.base.Tool + subsystems.executors
不反向依赖 aibox 任何符号。
"""
import os
import queue
import subprocess
import time
from typing import Dict
from common import ConfigManager
from subsystems import CommandExecutor, SubprocessCommandExecutor
from .base import Tool
class CommandLineTool(Tool):
"""命令行执行工具 - 使用抽象的命令执行器(支持流式输出和超时控制)"""
def __init__(self, executor: CommandExecutor = None, logger=None):
super().__init__(logger)
self.executor = executor or SubprocessCommandExecutor(logger)
def get_name(self) -> str:
return "run_command"
def get_description(self) -> str:
return """执行任意系统命令,支持实时输出显示和超时控制"""
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "要执行的命令"
},
"timeout": {
"type": "integer",
"default": 30,
"description": "命令执行超时时间(秒),超时后将终止命令执行。默认30秒"
},
"work_dir": {
"type": "string",
"default": ".",
"description": "命令执行的工作目录"
},
"env": {
"type": "object",
"description": "额外的环境变量"
}
},
"required": ["command"]
}
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行命令,支持超时控制"""
command = kwargs.get("command")
timeout = kwargs.get("timeout", 30)
work_dir = kwargs.get("work_dir", ".")
extra_env = kwargs.get("env", {})
if not command:
output_queue.put(("line", "❌ 错误:缺少命令参数\n"))
output_queue.put(("complete", "❌ 错误:缺少命令参数"))
return
# 发送开始执行的消息
output_queue.put(("line", f"\n🔧 执行命令: {command}\n"))
output_queue.put(("line", f"⏱️ 超时设置: {timeout} 秒\n"))
if work_dir != ".":
output_queue.put(("line", f"📁 工作目录: {work_dir}\n"))
output_queue.put(("line", "-" * 50 + "\n"))
# 准备环境变量
env = os.environ.copy()
if extra_env:
env.update(extra_env)
try:
# 使用 subprocess.Popen 实现流式输出,支持超时
process = subprocess.Popen(
command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stdin=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True,
env=env,
cwd=os.path.expanduser(work_dir)
)
start_time = time.time()
output_lines = []
timeout_occurred = False
# 实时读取输出
while True:
# 检查超时
elapsed = time.time() - start_time
if timeout > 0 and elapsed > timeout:
process.terminate()
timeout_occurred = True
output_queue.put(("line", f"\n⏰ 命令执行超时({timeout}秒),已终止\n"))
break
# 使用非阻塞方式读取一行
import select
import sys
# 检查是否有输出可读
if sys.platform == 'win32':
# Windows 下使用 readline 带超时
try:
import msvcrt
if msvcrt.kbhit():
pass
line = process.stdout.readline()
if not line and process.poll() is not None:
break
if line:
line = line.rstrip('\n')
output_queue.put(("line", line + "\n"))
output_lines.append(line)
else:
time.sleep(0.05)
except:
line = process.stdout.readline()
if not line and process.poll() is not None:
break
if line:
line = line.rstrip('\n')
output_queue.put(("line", line + "\n"))
output_lines.append(line)
else:
# Unix 系统可以使用 select
import select
rlist, _, _ = select.select([process.stdout], [], [], 0.5)
if rlist:
line = process.stdout.readline()
if not line and process.poll() is not None:
break
if line:
line = line.rstrip('\n')
output_queue.put(("line", line + "\n"))
output_lines.append(line)
else:
# 检查进程是否结束
if process.poll() is not None:
# 读取剩余输出
remaining = process.stdout.read()
if remaining:
remaining = remaining.rstrip('\n')
if remaining:
output_queue.put(("line", remaining + "\n"))
output_lines.append(remaining)
break
# 如果超时,确保进程被终止
if timeout_occurred and process.poll() is None:
process.kill()
process.wait(timeout=2)
# 获取返回码
return_code = process.returncode if process.returncode is not None else -1
output_queue.put(("line", "-" * 50 + "\n"))
if timeout_occurred:
output_queue.put(("line", f"⏰ 命令执行超时({timeout}秒),返回码: {return_code}\n"))
else:
output_queue.put(("line", f"✅ 命令执行完成,返回码: {return_code}\n"))
# 准备完整结果
full_output = "\n".join(output_lines)
data = {
"输出": full_output if full_output else "(无输出)",
"返回码": return_code
}
if timeout_occurred:
result = self.format_result(False, f"命令执行超时({timeout}秒)", data)
else:
result = self.format_result(return_code == 0, "命令执行完成", data)
output_queue.put(("complete", result))
except subprocess.TimeoutExpired:
error_msg = f"❌ 命令执行超时({timeout}秒)"
output_queue.put(("line", error_msg + "\n"))
output_queue.put(("complete", error_msg))
except FileNotFoundError as e:
error_msg = f"❌ 命令执行错误: 命令未找到 - {str(e)}"
output_queue.put(("line", error_msg + "\n"))
output_queue.put(("complete", error_msg))
except PermissionError as e:
error_msg = f"❌ 命令执行错误: 权限不足 - {str(e)}"
output_queue.put(("line", error_msg + "\n"))
output_queue.put(("complete", error_msg))
except Exception as e:
error_msg = f"❌ 执行错误: {str(e)}"
output_queue.put(("line", error_msg + "\n"))
output_queue.put(("complete", error_msg))
finally:
# 确保进程被清理
if 'process' in locals() and process and process.poll() is None:
try:
process.terminate()
process.wait(timeout=2)
except:
process.kill()
def execute(self, command: str, timeout: int = 30,
work_dir: str = ".", env: Dict = None, **kwargs) -> str:
"""执行命令 - 仅支持流式执行(命令工具强制流式输出,无 stream 参数)"""
if error := self.validate_params(["command"], command=command):
return error
# 如果正在流式输出,直接返回空(实际输出通过队列)
if self.streaming_output and self.output_queue:
return ""
# 命令工具固定走流式执行:通过临时队列收集完整结果
output_queue = queue.Queue()
self.execute_streaming(
output_queue,
command=command,
timeout=timeout,
work_dir=work_dir,
env=env
)
# 收集流式执行的最终结果(complete 消息)
final_result = ""
while not output_queue.empty():
try:
msg_type, content = output_queue.get_nowait()
if msg_type == "complete":
final_result = content
except queue.Empty:
break
return final_result
+818
View File
@@ -0,0 +1,818 @@
#!/usr/bin/env python3
"""
tools.db_tools - 数据库相关工具
抽离自 aibox.py,包含 2 个数据库工具:
- MemoryTool : 记忆管理(保存/搜索/查看/删除,支持分页)
- MemoTool : 任务管理(创建/查看/完成/删除/搜索/统计)
依赖:common (constants, Logger) + tools.base.Tool
+ subsystems.db.MemoryDatabase/MemoDatabase
不反向依赖 aibox 任何符号。
"""
import queue
import re
from datetime import datetime, timedelta
from typing import Any, Dict, Optional
from common import constants, Logger
from subsystems import MemoryDatabase, MemoDatabase
from .base import Tool
# ==================== 记忆工具 ====================
class MemoryTool(Tool):
"""记忆工具 - 用于存储、检索和删除对话记忆(支持流式输出,支持分页)"""
def __init__(self, memory_db: MemoryDatabase, logger: Optional[Logger] = None):
super().__init__(logger)
self.memory_db = memory_db
def get_name(self) -> str:
return "memory"
def get_description(self) -> str:
return """记忆管理工具 - 存储、检索和删除重要的对话信息
支持:保存总结、多关键词搜索记忆(支持分页)、查看最近的记忆、删除无用记忆"""
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"action": {
"type": "string",
"description": "操作类型",
"enum": ["save", "search", "recent", "get", "delete"]
},
"summary": {"type": "string", "description": "要保存的总结内容(用于save操作)"},
"topic": {"type": "string", "description": "对话主题(用于save操作)"},
"content": {"type": "string", "description": "详细内容(可选,用于save操作)"},
"keywords": {
"type": "array", "items": {"type": "string"},
"description": "搜索关键词列表"
},
"query": {"type": "string", "description": "搜索关键词(单个关键词)"},
"match_all": {"type": "boolean", "default": False},
"memory_id": {"type": "integer"},
"memory_ids": {"type": "array", "items": {"type": "integer"}},
"importance": {"type": "integer", "default": 2},
"limit": {"type": "integer", "default": 10, "description": "每页条数(默认10)"},
"page": {"type": "integer", "default": 1, "description": "页码(从1开始)"},
"confirm": {"type": "boolean", "default": False}
},
"required": ["action"]
}
def execute(self, action: str, **kwargs) -> str:
try:
if action == "save":
return self._save_memory(kwargs)
elif action == "search":
return self._search_memories(kwargs)
elif action == "recent":
return self._get_recent_memories(kwargs)
elif action == "get":
return self._get_memory_by_id(kwargs)
elif action == "delete":
return self._delete_memory(kwargs)
else:
return f"❌ 未知操作: {action}"
except Exception as e:
return f"❌ 记忆操作错误: {str(e)}"
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行记忆操作"""
action = kwargs.get("action")
# 发送开始执行的消息
output_queue.put(("line", f"\n🧠 执行记忆操作: {action}\n"))
output_queue.put(("line", "-" * 50 + "\n"))
# 执行操作
result = self.execute(**kwargs)
# 发送结果
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
def _save_memory(self, kwargs: Dict) -> str:
summary = kwargs.get("summary")
if not summary:
return "❌ 需要提供总结内容"
topic = kwargs.get("topic", "未命名对话")
content = kwargs.get("content")
importance = kwargs.get("importance", constants.MEMORY_DEFAULT_IMPORTANCE)
memory_id = self.memory_db.add_memory(
topic=topic,
summary=summary,
content=content,
importance=importance
)
return self.format_result(True, "记忆已保存", {
"记忆ID": memory_id,
"主题": topic,
"总结": summary[:100] + "..." if len(summary) > 100 else summary
})
def _search_memories(self, kwargs: Dict) -> str:
"""搜索记忆 - 支持分页"""
keywords = kwargs.get("keywords", [])
query = kwargs.get("query", "")
match_all = kwargs.get("match_all", False)
page = kwargs.get("page", 1)
page_size = kwargs.get("limit", 10) # 使用 limit 作为 page_size
# 确保页码和每页条数有效
page = max(1, page)
page_size = max(1, min(50, page_size)) # 每页最多50条
# 处理关键词
if keywords and isinstance(keywords, list):
pass
elif query:
keywords = query.split()
else:
return "❌ 需要提供搜索关键词(keywords 或 query)"
if not keywords:
return "❌ 关键词列表为空"
# 先获取所有结果(不分页,用于计算总数)
all_results = self.memory_db.search_memories(keywords, match_all, limit=None)
if not all_results:
match_mode = "所有" if match_all else "任一"
return f"❌ 未找到包含 {match_mode} 关键词 {keywords} 的记忆"
# 计算分页信息
total_count = len(all_results)
total_pages = (total_count + page_size - 1) // page_size
# 检查页码是否有效
if page > total_pages and total_pages > 0:
return f"❌ 页码 {page} 超出范围,共有 {total_pages} 页"
# 计算当前页的起止索引
start_idx = (page - 1) * page_size
end_idx = min(start_idx + page_size, total_count)
# 获取当前页的结果
page_results = all_results[start_idx:end_idx]
# 构建输出
match_mode = "所有关键词" if match_all else "任一关键词"
output = [
f"🔍 搜索 {match_mode} {keywords} 找到 {total_count} 条记忆",
f"📄 第 {page}/{total_pages} 页 (每页 {page_size} 条)",
"-" * 40
]
for i, mem in enumerate(page_results, 1):
# 全局序号
global_idx = start_idx + i
output.append(f"\n{global_idx}. [ID: {mem['id']}] [{mem['end_time']}] {mem['topic']}")
output.append(f" 总结: {mem['summary'][:200]}")
if mem.get('tags'):
output.append(f" 标签: {', '.join(mem['tags'])}")
if mem.get('importance'):
output.append(f" 重要性: {'⭐' * mem['importance']} ({mem['importance']}/5)")
# 添加翻页提示
if total_pages > 1:
output.append(f"\n📖 翻页提示: 使用 page 参数查看更多结果")
output.append(f" 例如: memory search query='{query}' page={page + 1 if page < total_pages else page} limit={page_size}")
if page < total_pages:
output.append(f" ➡️ 下一页: page={page + 1}")
if page > 1:
output.append(f" ⬅️ 上一页: page={page - 1}")
return "\n".join(output)
def _get_recent_memories(self, kwargs: Dict) -> str:
"""获取最近的记忆 - 支持分页"""
page = kwargs.get("page", 1)
page_size = kwargs.get("limit", 10)
# 确保页码和每页条数有效
page = max(1, page)
page_size = max(1, min(50, page_size))
# 先获取所有结果
all_results = self.memory_db.get_recent_memories(limit=None)
if not all_results:
return "📭 没有找到记忆"
# 计算分页信息
total_count = len(all_results)
total_pages = (total_count + page_size - 1) // page_size
# 检查页码是否有效
if page > total_pages and total_pages > 0:
return f"❌ 页码 {page} 超出范围,共有 {total_pages} 页"
# 计算当前页的起止索引
start_idx = (page - 1) * page_size
end_idx = min(start_idx + page_size, total_count)
# 获取当前页的结果
page_results = all_results[start_idx:end_idx]
output = [f"📚 最近 {total_count} 条记忆 (第 {page}/{total_pages} 页,每页 {page_size} 条):"]
output.append("-" * 40)
for i, mem in enumerate(page_results, 1):
global_idx = start_idx + i
output.append(f"\n{global_idx}. [ID: {mem['id']}] [{mem['end_time']}] {mem['topic']}")
output.append(f" 总结: {mem['summary'][:100]}")
# 添加翻页提示
if total_pages > 1:
output.append(f"\n📖 翻页提示: 使用 page 参数查看更多结果")
output.append(f" 例如: memory recent page={page + 1 if page < total_pages else page} limit={page_size}")
return "\n".join(output)
def _get_memory_by_id(self, kwargs: Dict) -> str:
"""根据ID获取记忆"""
memory_id = kwargs.get("memory_id")
if not memory_id:
return "❌ 需要提供记忆ID"
memory = self.memory_db.get_memory_by_id(memory_id)
if not memory:
return f"❌ 未找到ID为 {memory_id} 的记忆"
output = [
f"📌 记忆 #{memory_id}",
f"主题: {memory['topic']}",
f"时间: {memory['start_time']} - {memory['end_time']}",
f"总结: {memory['summary']}",
]
if memory.get('content'):
output.append(f"详细内容:\n{memory['content']}")
if memory.get('tags'):
output.append(f"标签: {', '.join(memory['tags'])}")
if memory.get('importance'):
output.append(f"重要性: {'⭐' * memory['importance']} ({memory['importance']}/5)")
return "\n".join(output)
def _delete_memory(self, kwargs: Dict) -> str:
"""删除记忆"""
confirm = kwargs.get("confirm", False)
if not confirm:
return "❌ 需要设置 confirm=True 来确认删除操作"
memory_id = kwargs.get("memory_id")
memory_ids = kwargs.get("memory_ids", [])
deleted_count = 0
if memory_id:
if self.memory_db.delete_memory(memory_id):
deleted_count = 1
elif memory_ids:
deleted_count = self.memory_db.delete_memories_by_ids(memory_ids)
else:
return "❌ 需要提供 memory_id 或 memory_ids"
if deleted_count == 0:
return f"❌ 未找到要删除的记忆"
return self.format_result(True, f"成功删除 {deleted_count} 条记忆", {"删除数量": deleted_count})
# ==================== 备忘录工具 ====================
class MemoTool(Tool):
"""备忘录工具 - 用于管理任务(所有任务都是对话任务)"""
def __init__(self, memo_db: MemoDatabase, logger: Optional[Logger] = None):
super().__init__(logger)
self.memo_db = memo_db
def get_name(self) -> str:
return "memo"
def get_description(self) -> str:
return """任务管理工具 - 创建、查看、完成任务
支持:创建任务(支持一次性/重复、即时任务)、查看待办、标记完成、删除任务、搜索任务"""
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"action": {
"type": "string",
"description": "操作类型",
"enum": ["add", "list", "get", "complete", "delete", "search", "stats"]
},
"title": {
"type": "string",
"description": "任务标题(用于add操作)"
},
"content": {
"type": "string",
"description": "任务内容(可选,用于add操作)"
},
"reminder_time": {
"type": "string",
"description": "执行时间(格式:YYYY-MM-DD HH:MM:SS,或相对时间如 '+1h', '+30m', 'tomorrow 9am')"
},
"reminder_minutes": {
"type": "integer",
"description": "延迟分钟数(从现在开始,用于快速设置任务)"
},
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "标签列表"
},
"priority": {
"type": "integer",
"description": "优先级 1-5(默认1)",
"default": 1
},
"repeat_type": {
"type": "string",
"description": "重复类型(none=一次性, daily=每天, weekly=每周, monthly=每月, custom=自定义)",
"enum": ["none", "daily", "weekly", "monthly", "custom"],
"default": "none"
},
"repeat_interval": {
"type": "integer",
"description": "自定义重复间隔,当repeat_type='custom'时使用",
"default": 0
},
"repeat_interval_unit": {
"type": "string",
"description": "重复间隔单位(minutes=分钟, hours=小时, days=天),当repeat_type='custom'时使用",
"enum": ["minutes", "hours", "days"],
"default": "days"
},
"repeat_end_time": {
"type": "string",
"description": "重复结束时间,None表示永久重复"
},
"is_immediate": {
"type": "boolean",
"description": "是否为即时任务(立即执行)",
"default": False
},
"memo_id": {
"type": "integer",
"description": "任务ID(用于get、complete、delete操作)"
},
"memo_ids": {
"type": "array",
"items": {"type": "integer"},
"description": "任务ID列表(用于批量删除)"
},
"force_complete": {
"type": "boolean",
"description": "强制完成(忽略重复规则)",
"default": False
},
"keyword": {
"type": "string",
"description": "搜索关键词(用于search操作)"
},
"include_completed": {
"type": "boolean",
"description": "是否包含已完成的任务(默认False)",
"default": False
},
"limit": {
"type": "integer",
"description": "返回数量限制(默认20)",
"default": 20
},
"confirm": {
"type": "boolean",
"description": "确认删除操作",
"default": False
}
},
"required": ["action"]
}
def _parse_reminder_time(self, reminder_input: Any) -> Optional[str]:
"""解析提醒时间输入"""
if not reminder_input:
return None
if isinstance(reminder_input, int):
reminder_time = datetime.now() + timedelta(minutes=reminder_input)
return reminder_time.strftime(constants.DATETIME_FORMAT)
if isinstance(reminder_input, str):
reminder_input = reminder_input.strip()
if reminder_input.startswith('+'):
match = re.match(r'\+(\d+)([mhd])', reminder_input)
if match:
value, unit = int(match.group(1)), match.group(2)
if unit == 'm':
delta = timedelta(minutes=value)
elif unit == 'h':
delta = timedelta(hours=value)
elif unit == 'd':
delta = timedelta(days=value)
else:
return None
reminder_time = datetime.now() + delta
return reminder_time.strftime(constants.DATETIME_FORMAT)
try:
reminder_time = datetime.strptime(reminder_input, constants.DATETIME_FORMAT)
return reminder_time.strftime(constants.DATETIME_FORMAT)
except ValueError:
pass
now = datetime.now()
reminder_input_lower = reminder_input.lower()
if 'tomorrow' in reminder_input_lower:
tomorrow = now + timedelta(days=1)
time_match = re.search(r'(\d{1,2})(?::(\d{2}))?\s*(am|pm)?', reminder_input_lower)
if time_match:
hour = int(time_match.group(1))
minute = int(time_match.group(2)) if time_match.group(2) else 0
ampm = time_match.group(3)
if ampm:
if ampm == 'pm' and hour < 12:
hour += 12
elif ampm == 'am' and hour == 12:
hour = 0
reminder_time = tomorrow.replace(hour=hour, minute=minute, second=0)
else:
reminder_time = tomorrow.replace(hour=9, minute=0, second=0)
return reminder_time.strftime(constants.DATETIME_FORMAT)
return None
def execute(self, action: str, **kwargs) -> str:
"""执行备忘录操作"""
try:
if action == "add":
return self._add_memo(kwargs)
elif action == "list":
return self._list_memos(kwargs)
elif action == "get":
return self._get_memo(kwargs)
elif action == "complete":
return self._complete_memo(kwargs)
elif action == "delete":
return self._delete_memo(kwargs)
elif action == "search":
return self._search_memos(kwargs)
elif action == "stats":
return self._get_stats(kwargs)
else:
return f"❌ 未知操作: {action}"
except Exception as e:
return f"❌ 任务操作错误: {str(e)}"
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行备忘录操作"""
action = kwargs.get("action")
# 发送开始执行的消息
output_queue.put(("line", f"\n📝 执行备忘录操作: {action}\n"))
output_queue.put(("line", "-" * 50 + "\n"))
# 执行操作
result = self.execute(**kwargs)
# 发送结果
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
def _add_memo(self, kwargs: Dict) -> str:
"""添加任务 - 所有任务都是对话任务,不需要task_type"""
title = kwargs.get("title")
if not title:
return "❌ 需要提供任务标题"
content = kwargs.get("content")
reminder_minutes = kwargs.get("reminder_minutes")
reminder_time_input = kwargs.get("reminder_time")
tags = kwargs.get("tags", [])
priority = kwargs.get("priority", 1)
repeat_type = kwargs.get("repeat_type", "none")
repeat_interval = kwargs.get("repeat_interval", 0)
repeat_interval_unit = kwargs.get("repeat_interval_unit", "days")
repeat_end_time = kwargs.get("repeat_end_time")
is_immediate = kwargs.get("is_immediate", False)
if repeat_type not in ["none", "daily", "weekly", "monthly", "custom"]:
return f"❌ 无效的重复类型: {repeat_type}"
if repeat_type == "custom" and repeat_interval <= 0:
return "❌ 自定义重复需要设置有效的 repeat_interval (>0)"
reminder_time = None
if is_immediate:
# 即时任务:设置为当前时间,确保立即执行
reminder_time = datetime.now().strftime(constants.DATETIME_FORMAT)
elif reminder_minutes:
reminder_time = self._parse_reminder_time(reminder_minutes)
elif reminder_time_input:
reminder_time = self._parse_reminder_time(reminder_time_input)
else:
# 默认30分钟后
reminder_time = self._parse_reminder_time(30)
memo_id = self.memo_db.add_memo(
title=title,
content=content,
reminder_time=reminder_time,
tags=tags,
priority=priority,
created_by="AI",
source="user",
repeat_type=repeat_type,
repeat_interval=repeat_interval,
repeat_interval_unit=repeat_interval_unit,
repeat_end_time=repeat_end_time,
is_immediate=is_immediate
)
data = {
"任务ID": memo_id,
"标题": title,
}
if is_immediate:
data["任务类型"] = "即时任务"
else:
if reminder_time:
data["执行时间"] = reminder_time
if repeat_type != "none":
if repeat_type == "custom":
repeat_info = f"重复类型: 每{repeat_interval} {repeat_interval_unit}"
else:
repeat_map = {
'daily': '每天',
'weekly': '每周',
'monthly': '每月'
}
repeat_info = f"重复类型: {repeat_map.get(repeat_type, repeat_type)}"
if repeat_end_time:
repeat_info += f", 结束于: {repeat_end_time}"
else:
repeat_info += ", 永久重复"
data["重复信息"] = repeat_info
return self.format_result(True, "任务已创建", data)
def _list_memos(self, kwargs: Dict) -> str:
"""列出任务"""
include_completed = kwargs.get("include_completed", False)
limit = kwargs.get("limit", 20)
memos = self.memo_db.get_all_memos(include_completed, limit)
if not memos:
return "📭 没有找到任务"
status = "所有" if include_completed else "待执行"
output = [f"📋 {status}任务 (共 {len(memos)} 个):"]
now = datetime.now()
for i, memo in enumerate(memos, 1):
status_icon = "✅" if memo['is_completed'] else "⏰"
priority_icon = "🔴" * memo['priority']
source_tag = f"[{memo.get('source', '未知')}]" if memo.get('source') else ""
immediate_tag = "⚡" if memo.get('is_immediate') else ""
repeat_tag = ""
if memo['repeat_type'] != 'none':
if memo['repeat_type'] == 'custom':
unit = memo.get('repeat_interval_unit', 'days')
unit_map = {'minutes': '分钟', 'hours': '小时', 'days': '天'}
repeat_tag = f" [每{memo['repeat_interval']}{unit_map.get(unit, unit)}]"
else:
repeat_map = {
'daily': ' [每天]',
'weekly': ' [每周]',
'monthly': ' [每月]'
}
repeat_tag = repeat_map.get(memo['repeat_type'], ' [重复]')
output.append(f"\n{i}. {status_icon}{immediate_tag} [ID: {memo['id']}] {source_tag}{repeat_tag} {memo['title']} {priority_icon}")
if memo['reminder_time']:
try:
reminder = datetime.strptime(memo['reminder_time'], constants.DATETIME_FORMAT)
if not memo['is_completed'] and reminder < now:
output.append(f" ⚠️ 已过期: {memo['reminder_time']}")
else:
output.append(f" ⏱️ 执行: {memo['reminder_time']}")
except:
output.append(f" ⏱️ 执行: {memo['reminder_time']}")
if memo.get('trigger_count', 0) > 0:
output.append(f" 🔄 已执行 {memo['trigger_count']} 次")
if memo['content']:
preview = memo['content'][:100] + "..." if len(memo['content']) > 100 else memo['content']
output.append(f" 📝 {preview}")
if memo['tags']:
output.append(f" 🏷️ {', '.join(memo['tags'])}")
return "\n".join(output)
def _get_memo(self, kwargs: Dict) -> str:
"""获取单个任务"""
memo_id = kwargs.get("memo_id")
if not memo_id:
return "❌ 需要提供任务ID"
memo = self.memo_db.get_memo(memo_id)
if not memo:
return f"❌ 未找到ID为 {memo_id} 的任务"
status = "✅ 已完成" if memo['is_completed'] else "⏰ 待执行"
if memo['is_completed']:
status += f" (完成于: {memo['completed_at']})"
immediate_tag = "⚡ 即时任务" if memo.get('is_immediate') else ""
output = [
f"📌 任务 #{memo_id} {immediate_tag}",
f"📋 状态: {status}",
f"📝 标题: {memo['title']}",
]
if memo.get('source'):
output.append(f"📎 来源: {memo['source']}")
if memo['repeat_type'] != 'none':
if memo['repeat_type'] == 'custom':
unit_map = {'minutes': '分钟', 'hours': '小时', 'days': '天'}
unit = memo.get('repeat_interval_unit', 'days')
repeat_info = f"🔄 重复: 每{memo['repeat_interval']} {unit_map.get(unit, unit)}"
else:
repeat_map = {
'daily': '每天',
'weekly': '每周',
'monthly': '每月'
}
repeat_info = f"🔄 重复: {repeat_map.get(memo['repeat_type'], memo['repeat_type'])}"
if memo['repeat_end_time']:
repeat_info += f", 结束于: {memo['repeat_end_time']}"
else:
repeat_info += ", 永久重复"
output.append(repeat_info)
if memo.get('trigger_count', 0) > 0:
output.append(f" 📊 已执行 {memo['trigger_count']} 次")
if memo.get('last_triggered_at'):
output.append(f" ⏱️ 上次执行: {memo['last_triggered_at']}")
if memo['content']:
output.append(f"📄 内容:\n{memo['content']}")
if memo['reminder_time']:
try:
now = datetime.now()
reminder = datetime.strptime(memo['reminder_time'], constants.DATETIME_FORMAT)
if not memo['is_completed'] and reminder < now:
output.append(f"⚠️ 执行时间 (已过期): {memo['reminder_time']}")
else:
output.append(f"⏰ 执行时间: {memo['reminder_time']}")
except:
output.append(f"⏰ 执行时间: {memo['reminder_time']}")
if memo['tags']:
output.append(f"🏷️ 标签: {', '.join(memo['tags'])}")
output.append(f"🔢 优先级: {'🔴' * memo['priority']} ({memo['priority']}/5)")
output.append(f"📅 创建时间: {memo['created_at']}")
output.append(f"👤 创建者: {memo['created_by'] or '系统'}")
return "\n".join(output)
def _complete_memo(self, kwargs: Dict) -> str:
"""完成任务"""
memo_id = kwargs.get("memo_id")
if not memo_id:
return "❌ 需要提供任务ID"
force_complete = kwargs.get("force_complete", False)
memo = self.memo_db.get_memo(memo_id)
if not memo:
return f"❌ 未找到ID为 {memo_id} 的任务"
if memo['is_completed'] and not force_complete:
return f"ℹ️ 任务 #{memo_id} 已经完成了"
if self.memo_db.complete_memo(memo_id, force_complete):
memo = self.memo_db.get_memo(memo_id)
data = {
"任务ID": memo_id,
"标题": memo['title']
}
if memo['repeat_type'] != 'none' and not memo['is_completed']:
data["下一次执行"] = memo['reminder_time']
data["已执行次数"] = memo.get('trigger_count', 0)
return self.format_result(True, "任务已处理,已更新下一次执行时间", data)
else:
return self.format_result(True, "任务已完成", data)
else:
return f"❌ 无法完成任务 #{memo_id}"
def _delete_memo(self, kwargs: Dict) -> str:
"""删除任务"""
confirm = kwargs.get("confirm", False)
if not confirm:
return "❌ 需要设置 confirm=True 来确认删除操作"
memo_id = kwargs.get("memo_id")
if memo_id:
if self.memo_db.complete_memo(memo_id, force_complete=True):
return self.format_result(True, f"任务 #{memo_id} 已删除")
else:
return f"❌ 无法删除任务 #{memo_id}"
else:
return "❌ 需要提供 memo_id"
def _search_memos(self, kwargs: Dict) -> str:
"""搜索任务"""
keyword = kwargs.get("keyword")
if not keyword:
return "❌ 需要提供搜索关键词"
include_completed = kwargs.get("include_completed", False)
limit = kwargs.get("limit", 20)
all_memos = self.memo_db.get_all_memos(include_completed, limit=100)
results = []
keyword_lower = keyword.lower()
for memo in all_memos:
if (keyword_lower in memo['title'].lower() or
(memo['content'] and keyword_lower in memo['content'].lower())):
results.append(memo)
if not results:
return f"❌ 未找到包含 '{keyword}' 的任务"
output = [f"🔍 搜索 '{keyword}' 找到 {len(results)} 个任务:"]
now = datetime.now()
for i, memo in enumerate(results[:limit], 1):
status_icon = "✅" if memo['is_completed'] else "⏰"
immediate_tag = "⚡" if memo.get('is_immediate') else ""
output.append(f"\n{i}. {status_icon}{immediate_tag} [ID: {memo['id']}] {memo['title']}")
if memo['reminder_time']:
try:
reminder = datetime.strptime(memo['reminder_time'], constants.DATETIME_FORMAT)
if not memo['is_completed'] and reminder < now:
output.append(f" ⚠️ 已过期: {memo['reminder_time']}")
else:
output.append(f" ⏱️ {memo['reminder_time']}")
except:
output.append(f" ⏱️ {memo['reminder_time']}")
return "\n".join(output)
def _get_stats(self, kwargs: Dict) -> str:
"""获取任务统计信息"""
stats = self.memo_db.get_memo_stats()
output = [
"📊 任务统计",
f" 总任务: {stats['total']} 个",
f" 待执行: {stats['pending']} 个",
f" 已过期: {stats['overdue']} 个",
f" 已完成: {stats['completed']} 个",
f" 即时任务: {stats['immediate']} 个"
]
if stats['overdue'] > 0:
output.append(f"\n⚠️ 有 {stats['overdue']} 个任务已过期,将自动执行")
return "\n".join(output)
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""
tools.misc_tools - 杂项工具
抽离自 aibox.py,包含不依赖任何子系统的独立工具:
- AskUserTool : 主动向用户提问
依赖:tools.base.Tool
不反向依赖 aibox 任何符号。
"""
from typing import Dict
from .base import Tool
class AskUserTool(Tool):
"""智能体遇到了麻烦,主动向用户问一个问题"""
def get_name(self) -> str:
return "ask_user"
def get_description(self) -> str:
return (
"主动向用户提问的工具。你是用户的专属助手,应当主动关心用户、澄清需求、确认意图,"
"而不是被动等待指令。适用场景:指令模糊需要澄清、任务完成后追问后续需求、"
"关心用户近况、主动发现用户可能需要的帮助。请积极使用此工具与用户互动。"
)
def get_parameters(self) -> Dict:
''' TODO: 调用子代理生成提示词 '''
return {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": (
"你现在遇到一个问题,你想询问用户,现在提供你的问题。"
)
}
},
"required": []
}
def execute(self, question: str, **kwargs) -> str:
return (
"你遇到了想搞清楚的事情: " + question + "\n你现在可以询问用户。"
)
+252
View File
@@ -0,0 +1,252 @@
#!/usr/bin/env python3
"""
tools.prompt_tools - 提示词相关工具
抽离自 aibox.py,包含 3 个工具:
- UpdateIdentityTool : 更新用户身份信息(00_IDENTITY.md)
- UpdateSoulTool : 更新智能体自我认识(99_SOUL.md)
- SaveMemoryAndEndConversationTool : 保存记忆并结束对话(同时更新状态)
依赖:common (i18n, Logger) + tools.base.Tool
+ subsystems.db.MemoryDatabase + subsystems.prompt_manager.PromptManager
不反向依赖 aibox 任何符号。
"""
import queue
from typing import Callable, Dict, Optional
from common import i18n, Logger
from subsystems import MemoryDatabase, PromptManager
from .base import Tool
class UpdateIdentityTool(Tool):
"""更新用户身份工具 - 修改 00_IDENTITY.md 文件"""
def __init__(self, prompt_manager: PromptManager, logger: Optional[Logger] = None):
super().__init__(logger)
self.prompt_manager = prompt_manager
def get_name(self) -> str:
return "update_identity"
def get_description(self) -> str:
return """更新用户身份信息 - 修改 00_IDENTITY.md 文件
当你了解到用户的身份信息时,调用此工具更新身份文件。
如果文件不存在,会自动创建。"""
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "用户身份信息内容(Markdown格式),包含用户的身份、特点、偏好等信息"
},
"confirm": {
"type": "boolean",
"description": "确认更新身份信息",
"default": True
}
},
"required": ["content"]
}
def execute(self, content: str = None, confirm: bool = True, **kwargs) -> str:
if not content:
return "❌ 需要提供身份信息内容"
if not confirm:
return "❌ 身份更新已取消,如需更新请设置 confirm=true"
self.logger.info("开始更新用户身份信息...")
if self.prompt_manager.update_identity(content):
return self.format_result(True, i18n.get('identity_updated', self.prompt_manager.IDENTITY_FILE), {
"文件": self.prompt_manager.IDENTITY_FILE,
"路径": self.prompt_manager.get_prompts_dir_path()
})
else:
return self.format_result(False, "身份更新失败", {
"文件": self.prompt_manager.IDENTITY_FILE,
"路径": self.prompt_manager.get_prompts_dir_path()
})
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行更新身份"""
output_queue.put(("line", "\n👤 更新用户身份信息...\n"))
output_queue.put(("line", "-" * 50 + "\n"))
result = self.execute(**kwargs)
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
class UpdateSoulTool(Tool):
"""更新智能体自我认识工具 - 修改 99_SOUL.md 文件"""
def __init__(self, prompt_manager: PromptManager, logger: Optional[Logger] = None):
super().__init__(logger)
self.prompt_manager = prompt_manager
def get_name(self) -> str:
return "update_soul"
def get_description(self) -> str:
return """更新智能体的自我认识定义 - 修改 99_SOUL.md 文件
当你需要重新定义自己的身份、角色、性格、能力边界时,调用此工具更新自我认识文件。
如果文件不存在,会自动创建默认定义。"""
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "智能体自我认识定义内容(Markdown格式),包含:身份定义、角色描述、性格特点、能力边界、行为准则等"
},
"confirm": {
"type": "boolean",
"description": "确认更新自我认识定义",
"default": True
}
},
"required": ["content"]
}
def execute(self, content: str = None, confirm: bool = True, **kwargs) -> str:
if not content:
return "❌ 需要提供智能体自我认识定义内容"
if not confirm:
return "❌ 自我认识更新已取消,如需更新请设置 confirm=true"
self.logger.info("开始更新智能体自我认识定义...")
if self.prompt_manager.update_soul(content):
return self.format_result(True, "智能体自我认识已更新", {
"文件": self.prompt_manager.SOUL_FILE,
"路径": self.prompt_manager.get_prompts_dir_path()
})
else:
return self.format_result(False, "自我认识更新失败", {
"文件": self.prompt_manager.SOUL_FILE,
"路径": self.prompt_manager.get_prompts_dir_path()
})
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行更新自我认识"""
output_queue.put(("line", "\n🧠 更新智能体自我认识...\n"))
output_queue.put(("line", "-" * 50 + "\n"))
result = self.execute(**kwargs)
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
class SaveMemoryAndEndConversationTool(Tool):
"""保存记忆并结束对话工具 - 增强版,同时更新智能体状态"""
def __init__(self, exit_callback: Callable, memory_db: MemoryDatabase,
prompt_manager: PromptManager, logger: Optional[Logger] = None):
super().__init__(logger)
self.exit_callback = exit_callback
self.memory_db = memory_db
self.prompt_manager = prompt_manager
def get_name(self) -> str:
return "save_memory_and_end_conversation"
def get_description(self) -> str:
return """保存记忆并结束当前对话 - 必须提供详细的对话总结和当前状态!
使用此工具时,AI会:
1. 将本次对话的重要内容保存到记忆库
2. 提供完整的对话总结
3. 描述自己现在的状态、事情的走向等,保存到 100_STATUS.md
4. 礼貌地结束对话"""
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"summary": {
"type": "string",
"description": "详细的对话总结(至少50字),包含:讨论的主题、关键问题、重要结论、达成的共识等"
},
"topic": {
"type": "string",
"description": "对话主题(用于记忆分类,默认自动提取)"
},
"reason": {
"type": "string",
"description": "结束对话的原因",
"default": "话题讨论完成"
},
"current_status": {
"type": "string",
"description": "当前的智能体状态描述,包含:当前进展、下一步计划、待处理事项、整体走向等"
},
"next_goals": {
"type": "string",
"description": "接下来的目标或计划(可选)"
}
},
"required": ["summary", "current_status"]
}
def execute(self, summary: str = None, topic: str = None, reason: str = "话题讨论完成",
current_status: str = None, next_goals: str = None, **kwargs) -> str:
if summary is None or len(summary) < 50:
return "❌ 请提供更详细的对话总结(至少50字),以便保存到记忆库"
if current_status is None:
return "❌ 请提供当前状态描述,以便更新 100_STATUS.md"
self.logger.info(f"AI保存记忆并结束对话: {reason}")
# 保存到记忆库
memory_result = ""
if self.memory_db and topic:
memory_id = self.memory_db.add_memory(
topic=topic,
summary=summary,
importance=3 # 提高重要性,因为是对话总结
)
self.logger.info(f"对话记忆已保存,ID: {memory_id}")
memory_result = f"记忆已保存,ID: {memory_id}"
# 构建状态内容
status_content = f"【当前状态】\n{current_status}\n\n"
if next_goals:
status_content += f"【下一步计划】\n{next_goals}\n\n"
status_content += f"【对话总结】\n{summary}"
# 更新状态文件
status_updated = self.prompt_manager.update_status(status_content)
if self.exit_callback:
self.exit_callback(reason, summary)
data = {
"原因": reason,
"总结预览": summary[:100] + "..." if len(summary) > 100 else summary,
"状态更新": "✅ 已更新" if status_updated else "❌ 更新失败",
"记忆已保存": True
}
if memory_result:
data["记忆"] = memory_result
return self.format_result(True, "对话记忆已保存,状态已更新,对话结束", data)
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行结束对话"""
output_queue.put(("line", "\n💾 保存记忆并结束对话\n"))
output_queue.put(("line", "-" * 50 + "\n"))
result = self.execute(**kwargs)
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""
tools.proxy_tools - 代理配置工具
抽离自 aibox.py,包含 2 个工具:
- SetProxyTool : 设置 SOCKS5 代理
- GetProxyTool : 查看当前代理配置
依赖:common (ConfigManager) + tools.base.Tool
不反向依赖 aibox 任何符号。
"""
from typing import Dict
from common import ConfigManager
from .base import Tool
class SetProxyTool(Tool):
"""设置 SOCKS5 代理的工具"""
def get_name(self) -> str:
return "set_proxy"
def get_description(self) -> str:
return """设置智能体的 SOCKS5 代理配置。
调用后所有 HTTP/HTTPS 请求(包括网页访问、API 调用)都会通过该代理。
可随时修改,即时生效。"""
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"host": {"type": "string", "description": "代理服务器地址,如 127.0.0.1"},
"port": {"type": "integer", "description": "代理端口,如 1080"},
"username": {"type": "string", "description": "用户名(可选)"},
"password": {"type": "string", "description": "密码(可选)"},
"enabled": {"type": "boolean", "description": "是否启用代理,默认 true"},
"save": {"type": "boolean", "description": "是否保存到配置文件(持久化),默认 true"}
},
"required": ["host", "port"]
}
def execute(self, host: str, port: int, username: str = None, password: str = None,
enabled: bool = True, save: bool = True, **kwargs) -> str:
config = ConfigManager()
config.set_proxy(host, port, username, password, enabled, save)
proxy_info = config.get_proxy_info()
data = {
"代理状态": "启用" if proxy_info["enabled"] else "禁用",
"服务器": f"{proxy_info['host']}:{proxy_info['port']}",
"认证": "有" if proxy_info.get("username") else "无",
"持久化": "已保存" if save else "仅内存"
}
return self.format_result(True, "代理设置成功", data)
class GetProxyTool(Tool):
"""查看当前代理配置"""
def get_name(self) -> str:
return "get_proxy"
def get_description(self) -> str:
return "查看当前 SOCKS5 代理配置信息"
def get_parameters(self) -> Dict:
return {"type": "object", "properties": {}}
def execute(self, **kwargs) -> str:
config = ConfigManager()
info = config.get_proxy_info()
if info["enabled"]:
data = {
"代理状态": "启用",
"服务器": f"{info['host']}:{info['port']}",
"认证": "有" if info.get("username") else "无"
}
return self.format_result(True, "当前代理配置", data)
else:
return "❌ 当前代理未启用,所有请求走直连。"
+400
View File
@@ -0,0 +1,400 @@
#!/usr/bin/env python3
"""
tools.skill_tools - 技能相关工具
抽离自 aibox.py,包含 4 个技能管理工具:
- SearchSkillsTool : 按关键字搜索扩展技能
- ReloadSkillsTool : 热重载技能
- InstallSkillTool : 返回技能安装指引
- CreateSkillTool : 返回创建技能的指令提示词
依赖:common (constants, i18n, Logger) + tools.base.Tool + tools.base.ToolRegistry
+ subsystems.skill_manager.SkillManager
不反向依赖 aibox 任何符号。
"""
import os
import queue
import re
from typing import Dict, Optional
from common import constants, i18n, Logger
from subsystems import SkillManager
from .base import Tool
from .registry import ToolRegistry
class SearchSkillsTool(Tool):
"""扩展技能搜索工具 - 按关键字搜索 ~/.aibox/skills/ 下的技能文件。
支持两种技能格式:
1. 可执行脚本/程序:通过 --help 获取简介和完整使用说明
2. Markdown 文件 (.md):第一行为简介,其余为技能内容
工作流程:
1. 用关键字调用本工具,获取匹配技能的名称和简介列表
2. 对于 Markdown 技能,可以直接读取 content 字段获取完整技能内容
3. 对于可执行技能,通过 create_subagent 派生子代理执行 "技能名 --help" 查看完整命令手册
4. 根据内容,使用相应方式调用技能
"""
def __init__(self, skill_manager: SkillManager, logger: Optional[Logger] = None):
super().__init__(logger)
self.skill_manager = skill_manager
def get_name(self) -> str:
return "search_skills"
def get_description(self) -> str:
return (
"扩展技能搜索工具。\n\n"
"用关键字搜索 ~/.aibox/skills/ 目录下的技能文件。\n"
"支持两种技能格式:\n"
" 1. 可执行脚本/程序:通过 --help 获取简介和完整使用说明\n"
" 2. Markdown 文件 (.md):第一行为简介,其余为技能内容\n\n"
"使用方法:\n"
"1. 调用本工具搜索关键字,获取匹配技能列表(空字符串列出全部)。\n"
"2. 对于 Markdown 技能:设置 show_content=true 读取返回的内容获取完整技能说明。\n"
"3. 对于可执行技能:用 create_subagent 派生子代理执行 \"技能名 --help\" 获取完整命令手册,再按手册调用该技能。\n"
"4. 按照内容说明,使用相应方式调用该技能(执行类操作一律经 create_subagent 派生子代理完成)。"
)
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"keyword": {
"type": "string",
"description": (
"搜索关键字,在技能名称、简介和内容中进行匹配。"
"传入空字符串可列出全部可用技能。"
)
},
"show_content": {
"type": "boolean",
"description": (
"是否在结果中显示技能内容。"
"对于 Markdown 技能,这会影响返回结果的详细程度。"
"默认 False(只显示名称和简介)。"
),
"default": False
}
},
"required": ["keyword"]
}
def execute(self, keyword: str = "", show_content: bool = False, **kwargs) -> str:
kw = keyword.strip()
if kw:
results = self.skill_manager.search(kw)
header = f"🔍 搜索 '{kw}' 的结果(共 {len(results)} 个匹配技能):"
empty_msg = (
f"❌ 未找到与 '{kw}' 相关的技能。\n"
"提示:传入空字符串可列出所有可用技能。"
)
else:
results = self.skill_manager.get_all()
header = f"🔧 所有可用技能(共 {len(results)} 个):"
empty_msg = "❌ ~/.aibox/skills/ 目录下暂无可用技能。"
if not results:
return empty_msg
lines = [header]
for item in results:
skill_type = "📄 Markdown" if item.get('is_markdown') else "⚙️ 可执行"
lines.append(f"\n {skill_type} • {item['name']}")
lines.append(f" 简介: {item['summary']}")
if show_content and item.get('content'):
content = item['content']
if item.get('is_markdown'):
preview = content[:500]
if len(content) > 500:
preview += "...\n (内容较长,已截断,请直接读取技能文件获取完整内容)"
lines.append(f" 内容预览:\n {preview}")
else:
preview = content[:200] if content else "(无帮助输出)"
if len(content) > 200:
preview += "..."
lines.append(f" 帮助预览: {preview}")
else:
if item.get('is_markdown'):
lines.append(f" 💡 提示: 设置 show_content=true 可查看技能内容")
else:
lines.append(f" 💡 提示: 用 create_subagent 派生子代理执行 \"{item['name']} --help\" 查看完整手册")
lines.append("")
lines.append("📖 技能调用方式:")
lines.append(" • Markdown 技能: 设置 show_content=true 读取完整技能说明")
lines.append(" • 可执行技能: 用 create_subagent 派生子代理执行(先 '技能名 --help' 查手册,再按手册调用)")
return "\n".join(lines)
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行搜索技能"""
keyword = kwargs.get("keyword", "")
show_content = kwargs.get("show_content", False)
output_queue.put(("line", f"\n🔍 搜索技能: {keyword if keyword else '(全部)'}\n"))
output_queue.put(("line", "-" * 50 + "\n"))
# 直接传递 kwargs,避免重复参数
result = self.execute(**kwargs)
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
class ReloadSkillsTool(Tool):
"""技能热重载工具 - 重新加载所有扩展技能"""
def __init__(self, tool_registry: ToolRegistry, logger: Optional[Logger] = None):
super().__init__(logger)
self.tool_registry = tool_registry
def get_name(self) -> str:
return "reload_skills"
def get_description(self) -> str:
return """技能热重载工具 - 重新加载所有扩展技能
当你添加、修改或删除了 ~/.aibox/skills/ 目录下的技能文件后,
调用此工具可以立即生效,无需重启系统。"""
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"confirm": {
"type": "boolean",
"description": "确认重新加载技能",
"default": True
}
}
}
def execute(self, confirm: bool = True, **kwargs) -> str:
"""执行技能热重载"""
if not confirm:
return "❌ 技能重载已取消,如需重载请设置 confirm=true"
self.logger.info("开始执行技能热重载...")
try:
count = self.tool_registry.reload_skills()
# 从 SkillManager 获取更新后的技能列表(含简介)
skill_manager = self.tool_registry.skill_manager
if skill_manager:
all_skills = skill_manager.get_all()
skill_lines = "\n".join(
f" • {s['name']} — {s['summary']}"
for s in all_skills[:10]
)
data = {
"重新加载的技能数量": count,
"技能目录": constants.SKILLS_DIR,
"技能列表(含简介)": skill_lines or "(无技能)",
}
if count > 10:
data["提示"] = f"还有 {count - 10} 个技能未显示"
else:
data = {
"重新加载的技能数量": count,
"技能目录": constants.SKILLS_DIR,
}
return self.format_result(True, i18n.get('skills_reloaded', count), data)
except Exception as e:
self.logger.error(f"技能热重载失败: {e}")
return i18n.get('skills_reload_failed', str(e))
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行技能热重载"""
output_queue.put(("line", "\n🔄 执行技能热重载...\n"))
output_queue.put(("line", "-" * 50 + "\n"))
result = self.execute(**kwargs)
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
class InstallSkillTool(Tool):
"""技能安装指引工具 - 返回如何将可执行文件/脚本安装为扩展技能的说明"""
def get_name(self) -> str:
return "install_skill"
def get_description(self) -> str:
return (
"技能安装指引工具。\n"
"返回将 Markdown 技能文件安装为扩展技能的完整步骤说明,"
"包括文件检查、格式校验和部署方法(目标文件名统一带 .md 扩展名)。"
)
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"skill_path": {
"type": "string",
"description": (
"待安装的技能文件路径(可选)。"
"若提供,说明中会带入该路径作为示例;"
"不提供则返回通用说明。"
)
}
},
"required": []
}
def execute(self, skill_path: str = "", **kwargs) -> str:
skills_dir = constants.SKILLS_DIR
# 如果给了具体路径,在说明里带入,否则用占位符
src = skill_path.strip() if skill_path else "/path/to/your_skill.md"
# 去掉扩展名作为技能名称,再统一补 .md 作为目标文件名
base = os.path.splitext(os.path.basename(src))[0] if src else "your_skill"
dst = os.path.join(skills_dir, base + ".md")
guide = f"""📦 扩展技能安装指引
{"=" * 55}
要将一个 Markdown 技能文件安装为扩展技能,请按以下步骤操作:
─────────────────────────────────────────────────────
步骤 1:确认文件存在且格式正确
─────────────────────────────────────────────────────
调用 create_subagent 派生子代理执行以下命令,确认文件存在:
ls -l {src}
技能文件格式要求:
• 文件名为纯文本 Markdown,第一行是一级标题(# 技能名),作为技能摘要
• 其余内容为技能的执行说明、使用方法和示例
• 文件应带 .md 扩展名(系统以 .md 文件作为技能加载)
─────────────────────────────────────────────────────
步骤 2:复制到技能目录并统一为 .md 扩展名
─────────────────────────────────────────────────────
技能目录:{skills_dir}
调用 create_subagent 派生子代理执行复制命令(目标文件名统一补 .md 扩展名):
cp {src} {dst}
⚠️ 注意:目标文件名必须带 .md 扩展名,
系统以 .md 文件作为技能加载,且技能名称取文件名(不含 .md)。
─────────────────────────────────────────────────────
步骤 3:让系统识别新技能
─────────────────────────────────────────────────────
复制完成后,调用热重载工具使其立即生效:
reload_skills confirm=true
之后可用 search_skills 验证技能已加载:
search_skills keyword="{base}"
{"=" * 55}
完成以上步骤后,即可通过该技能执行相应操作。"""
return guide
class CreateSkillTool(Tool):
"""创建扩展技能工具 - 返回提示词,驱动智能体自行编写并部署 Markdown 技能文件。
工具本身不调用 LLM,只输出一段结构化的指令提示词。
智能体读到提示词后,会自行:
1. 编写满足格式要求的 Markdown 技能文件
2. 经 create_subagent 派生子代理写入 ~/.aibox/skills/<skill_name>.md
3. 调用 reload_skills 热重载
"""
def get_name(self) -> str:
return "create_skill"
def get_description(self) -> str:
return (
"创建扩展技能工具。\n"
"返回一段指令提示词,驱动你自行编写并部署一个 Markdown 技能文件(带 .md 扩展名)。\n\n"
"技能文件格式要求:\n"
" • 文件名必须带 .md 扩展名(如 fetch-weather.md、calc-bmi.md)\n"
" • 第一行为一级标题(# 技能名),作为该技能的一句话简介\n"
" • 其余内容为技能的执行说明、使用方法和示例,供智能体参考执行"
)
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"skill_name": {
"type": "string",
"description": (
"技能名称(部署后的文件名,不含扩展名;系统会自动补齐 .md)。"
"只能包含字母、数字、连字符和下划线,例如:fetch-weather、calc-bmi。"
)
},
"description": {
"type": "string",
"description": (
"对该技能的详细需求描述:它应该做什么、接受哪些输入参数、"
"返回什么输出、有哪些典型使用场景。描述越详细,生成质量越高。"
)
}
},
"required": ["skill_name", "description"]
}
def execute(self, skill_name: str = "", description: str = "", **kwargs) -> str:
# ---- 参数校验 ----
if not skill_name or not skill_name.strip():
return "❌ 错误:skill_name 不能为空"
if not description or not description.strip():
return "❌ 错误:description 不能为空"
if not re.match(r'^[A-Za-z0-9_\-]+$', skill_name.strip()):
return "❌ 错误:skill_name 只能包含字母、数字、连字符(-)和下划线(_)"
skill_name = skill_name.strip()
skills_dir = constants.SKILLS_DIR
# 技能文件名统一带 .md 扩展名
skill_path = os.path.join(skills_dir, skill_name + ".md")
# ---- 返回给智能体的指令提示词 ----
prompt = f"""【任务】创建扩展技能:{skill_name}
请按照以下步骤,自行完成该扩展技能的编写和部署:
─────────────────────────────────────────────────
步骤 1:编写 Markdown 技能文件
─────────────────────────────────────────────────
根据以下需求,编写一个 Markdown 技能文件:
需求描述:
{description.strip()}
技能文件必须满足的格式约定:
1. 第一行为一级标题 `# {skill_name}`,用于对该技能功能做一句话简介(系统用第一行作为技能摘要)
2. 其余内容清晰说明该技能的用途、使用方式、注意事项和典型示例
3. 内容完整可执行(即按内容操作即可完成该技能要完成的事)
─────────────────────────────────────────────────
步骤 2:写入技能目录
─────────────────────────────────────────────────
调用 create_subagent 派生子代理,将你编写的完整内容写入(文件名必须带 .md 扩展名):
{skill_path}
(在 task 中附上完整文件内容,并要求子代理写入后用 head -5 验证首行格式)
─────────────────────────────────────────────────
步骤 3:热重载
─────────────────────────────────────────────────
调用 reload_skills 工具使新技能立即生效:
reload_skills confirm=true
─────────────────────────────────────────────────
完成后请告知用户技能已创建成功,并展示技能的第一行简介。"""
return prompt