oclaw/runtime/tools/public/memory_wiki_tools.py
oliver acfb2cf73b fix(chat): compress-history on PostgreSQL dict rows
- Normalize select id,content rows from dict_row vs sqlite Row in history_tool_result_compact.

- Expand memory_wiki_search ToolSpec to match wiki_search parameters; add schema regression test.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 19:22:45 +08:00

199 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import importlib.util
import sys
from types import SimpleNamespace
from typing import Any, Callable
from svc.config.paths import PROJECT_ROOT
from runtime.tools.base import ToolSpec
def _plugin_cfg() -> dict[str, Any]:
cfg_path = (PROJECT_ROOT / "oclaw.json").resolve()
if not cfg_path.exists():
return {}
try:
import json
obj = json.loads(cfg_path.read_text(encoding="utf-8"))
except Exception:
return {}
plugins = obj.get("plugins") if isinstance(obj, dict) else {}
entries = plugins.get("entries") if isinstance(plugins, dict) else {}
entry = entries.get("memory-wiki") if isinstance(entries, dict) else {}
return entry if isinstance(entry, dict) else {}
def _wiki_handlers() -> dict[str, Callable[[dict[str, Any]], dict[str, Any]]]:
api_path = (PROJECT_ROOT / "runtime" / "extensions" / "memory-wiki" / "api.py").resolve()
spec = importlib.util.spec_from_file_location("memory_wiki_api", str(api_path))
if spec is None or spec.loader is None:
return {}
mod = importlib.util.module_from_spec(spec)
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):
return {}
specs = fn(SimpleNamespace(plugin_config=_plugin_cfg()))
out: dict[str, Callable[[dict[str, Any]], dict[str, Any]]] = {}
for spec_item in specs:
if not isinstance(spec_item, dict):
continue
name = str(spec_item.get("name") or "").strip()
handler = spec_item.get("handler")
if name and callable(handler):
out[name] = handler
return out
def _delegate(tool_name: str, args: dict[str, Any]) -> dict[str, Any]:
handlers = _wiki_handlers()
h = handlers.get(tool_name)
if not callable(h):
return {"ok": False, "error": f"wiki handler unavailable: {tool_name}"}
try:
return h(dict(args or {}))
except Exception as exc:
return {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
def _status_tool(public_name: str, desc: str) -> ToolSpec:
return ToolSpec(
name=public_name,
description=desc,
parameters={"type": "object", "properties": {}, "required": [], "additionalProperties": False},
handler=lambda args: _delegate("wiki_status", args),
tags=frozenset({"wiki", "curator"}),
risk_level="low",
read_only=True,
)
def _get_tool(public_name: str, desc: str) -> ToolSpec:
return ToolSpec(
name=public_name,
description=desc,
parameters={
"type": "object",
"properties": {
"path": {"type": "string"},
"start_line": {"type": "integer"},
"end_line": {"type": "integer"},
},
"required": ["path"],
"additionalProperties": False,
},
handler=lambda args: _delegate("wiki_get", args),
tags=frozenset({"wiki", "curator"}),
risk_level="low",
read_only=True,
)
def _search_tool(public_name: str, desc: str) -> ToolSpec:
return ToolSpec(
name=public_name,
description=desc,
parameters={
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query string."},
"limit": {"type": "integer", "description": "Max hits per page (server caps to wiki max)."},
"offset": {"type": "integer", "description": "Hit offset for pagination (0-based)."},
"context_lines": {
"type": "integer",
"description": "Lines of context around each match (server caps at 5).",
},
"is_regex": {"type": "boolean"},
"case_sensitive": {"type": "boolean"},
"path_prefix": {
"type": "string",
"description": "Only search under this relative path prefix (e.g. users/).",
},
"expand_query": {
"type": "boolean",
"description": "If true, try synonym-expanded variants (ignored when is_regex is true).",
},
"max_rounds": {
"type": "integer",
"description": "Max expansion rounds when expand_query is true (1–5).",
},
},
"required": ["query"],
"additionalProperties": False,
},
handler=lambda args: _delegate("wiki_search", args),
tags=frozenset({"wiki", "curator"}),
risk_level="low",
read_only=True,
)
def _lint_tool(public_name: str, desc: str) -> ToolSpec:
return ToolSpec(
name=public_name,
description=desc,
parameters={
"type": "object",
"properties": {"path": {"type": "string"}},
"required": [],
"additionalProperties": False,
},
handler=lambda args: _delegate("wiki_lint", args),
tags=frozenset({"wiki", "curator"}),
risk_level="low",
read_only=True,
)
def _apply_tool(public_name: str, desc: str) -> ToolSpec:
return ToolSpec(
name=public_name,
description=desc,
parameters={
"type": "object",
"properties": {
"action": {"type": "string", "enum": ["write", "append", "delete"]},
"path": {"type": "string"},
"content": {"type": "string"},
},
"required": ["action", "path"],
"additionalProperties": False,
},
handler=lambda args: _delegate("wiki_apply", args),
tags=frozenset({"wiki", "curator", "write"}),
risk_level="low",
read_only=False,
)
def memory_wiki_status_tool() -> ToolSpec:
return _status_tool("memory_wiki_status", "Read wiki runtime status for memory.")
def memory_wiki_get_tool() -> ToolSpec:
return _get_tool("memory_wiki_get", "Read a markdown file from memory wiki.")
def memory_wiki_search_tool() -> ToolSpec:
return _search_tool("memory_wiki_search", "Search memory wiki markdown.")
def memory_wiki_lint_tool() -> ToolSpec:
return _lint_tool("memory_wiki_lint", "Lint memory wiki markdown structure.")
def memory_wiki_apply_tool() -> ToolSpec:
return _apply_tool("memory_wiki_apply", "Apply write/append/delete changes to memory wiki markdown.")
__all__ = [
"memory_wiki_status_tool",
"memory_wiki_get_tool",
"memory_wiki_search_tool",
"memory_wiki_lint_tool",
"memory_wiki_apply_tool",
]