From 27b8698f9beb13b890bac3e73b07c8ac0c75df48 Mon Sep 17 00:00:00 2001 From: oliver Date: Wed, 29 Apr 2026 08:34:38 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20memory=20wiki=20=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E5=8A=A0=E8=BD=BD=E5=BC=82=E5=B8=B8=E5=B9=B6=E5=9C=A8?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E6=8F=90=E7=A4=BA=E8=AF=8D=E5=A3=B0=E6=98=8E?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=20wiki=20=E8=B7=AF=E5=BE=84=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为动态加载的 memory-wiki 模块补充 sys.modules 注册以避免 dataclass 导入期崩溃,同时在统一技能策略中明确默认根路径为 data/wiki,并新增工具加载回归测试。 Made-with: Cursor --- runtime/system_prompt.py | 1 + runtime/tools/experts/memory/wiki_tools.py | 4 +++ tests/test_memory_wiki_tools_loader.py | 37 ++++++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 tests/test_memory_wiki_tools_loader.py diff --git a/runtime/system_prompt.py b/runtime/system_prompt.py index f371bc0c..67de2ec5 100644 --- a/runtime/system_prompt.py +++ b/runtime/system_prompt.py @@ -42,6 +42,7 @@ def _unified_skill_policy_guidance() -> str: "- 不要为了“列出技能”而去读取 SKILL.md。只有在你确实需要某个技能的详细使用说明时,才读取对应 SKILL.md。\n" "- 当你需要技能细节时,请按目录中给出的 path 读取对应的 SKILL.md。\n" "- 当对话涉及长期记忆、用户身份/偏好、项目背景延续、复发问题沉淀时,优先启用 wiki-first-autonomy 技能,并优先使用 memory_wiki_search/memory_wiki_get 检索上下文,再执行与回复。\n" + "- memory wiki 默认根路径为 `data/wiki`(例如 `data/wiki/improvement/learnings.md`);未被用户明确覆盖时,统一按该路径读写与检索。\n" "- 当新增事实会影响后续决策时,完成当前任务后使用 memory_wiki_apply 写入结构化记忆,并用 memory_wiki_lint 做质量检查。\n" "- 技能包由说明文档和可选文件组成。运行时不会自动执行技能 `scripts/` 目录下的文件;\n" "- internal hooks 是独立系统,也不会自动执行这些脚本。\n" diff --git a/runtime/tools/experts/memory/wiki_tools.py b/runtime/tools/experts/memory/wiki_tools.py index 0f91aac7..a32603a8 100644 --- a/runtime/tools/experts/memory/wiki_tools.py +++ b/runtime/tools/experts/memory/wiki_tools.py @@ -1,6 +1,7 @@ from __future__ import annotations import importlib.util +import sys from types import SimpleNamespace from typing import Any, Callable @@ -30,6 +31,9 @@ def _wiki_handlers() -> dict[str, Callable[[dict[str, Any]], dict[str, Any]]]: if spec is None or spec.loader is None: return {} mod = importlib.util.module_from_spec(spec) + # Register module before exec so decorators (e.g., @dataclass) can resolve + # cls.__module__ via sys.modules during import-time processing. + sys.modules[spec.name] = mod spec.loader.exec_module(mod) # type: ignore[assignment] fn = getattr(mod, "build_wiki_tool_specs", None) if not callable(fn): diff --git a/tests/test_memory_wiki_tools_loader.py b/tests/test_memory_wiki_tools_loader.py new file mode 100644 index 00000000..4f624117 --- /dev/null +++ b/tests/test_memory_wiki_tools_loader.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from oclaw.runtime.tools.experts.memory.wiki_tools import ( + memory_wiki_apply_tool, + memory_wiki_get_tool, + memory_wiki_lint_tool, + memory_wiki_search_tool, + memory_wiki_status_tool, +) + + +def test_memory_wiki_tools_handlers_load_without_dataclass_module_error() -> None: + status = memory_wiki_status_tool().handler({}) + assert isinstance(status, dict) + assert "AttributeError" not in str(status.get("error", "")) + assert "__dict__" not in str(status.get("error", "")) + + search = memory_wiki_search_tool().handler({"query": "test"}) + assert isinstance(search, dict) + assert "AttributeError" not in str(search.get("error", "")) + assert "__dict__" not in str(search.get("error", "")) + + get_res = memory_wiki_get_tool().handler({"path": "improvement/learnings.md"}) + assert isinstance(get_res, dict) + assert "AttributeError" not in str(get_res.get("error", "")) + assert "__dict__" not in str(get_res.get("error", "")) + + lint = memory_wiki_lint_tool().handler({}) + assert isinstance(lint, dict) + assert "AttributeError" not in str(lint.get("error", "")) + assert "__dict__" not in str(lint.get("error", "")) + + apply_res = memory_wiki_apply_tool().handler({"action": "delete", "path": "improvement/_nonexistent_test_.md"}) + assert isinstance(apply_res, dict) + assert "AttributeError" not in str(apply_res.get("error", "")) + assert "__dict__" not in str(apply_res.get("error", "")) +