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:
1 parent
b9f8abdfeb
commit
791fac0efd
10 files changed
+2340
-1745
No files matched your search
+7
-1739
File diff suppressed because it is too large.
Load diff
@@ -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():
|
||||
|
||||
@@ -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
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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你现在可以询问用户。"
|
||||
)
|
||||
@@ -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))
|
||||
@@ -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 "❌ 当前代理未启用,所有请求走直连。"
|
||||
@@ -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
|
||||
Reference in new issue
Block a user