Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 16 additions & 20 deletions openkb/skill/tools.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
"""Path-scoped IO tools for the skill-create agent.

The skill-create agent runs with these capabilities:
* READ wiki structure — ``list_wiki_dir``
* READ wiki markdown — ``read_wiki_file_for_skill``
* READ wiki structure — ``list_wiki_dir`` (delegates to
``openkb.agent.tools.list_wiki_files``)
* READ wiki markdown — ``read_wiki_file_for_skill`` (delegates to
``openkb.agent.tools.read_wiki_file``)
* READ PageIndex source pages — ``get_skill_page_content`` (delegates to
``openkb.agent.tools.get_wiki_page_content``)
* READ wiki images — ``read_skill_image`` (delegates to
``openkb.agent.tools.read_wiki_image``)
* WRITE under skill root — ``write_skill_file``

The first four wrap the canonical wiki tools in ``openkb/agent/tools.py``
so the skill agent traverses the wiki the same way the query agent does —
no separate retrieval semantics, no second implementation to drift.
The first four are thin wrappers around the canonical wiki tools in
``openkb/agent/tools.py`` so the skill agent traverses the wiki the same
way the query agent does — no separate retrieval semantics, no second
implementation to drift.

These helpers enforce write boundaries at the Python level — every write
resolves its target path, then verifies it stays inside the skill root.
Expand All @@ -23,41 +26,34 @@

from openkb.agent.tools import (
get_wiki_page_content as _get_wiki_page_content,
list_wiki_files as _list_wiki_files,
read_wiki_file as _read_wiki_file,
read_wiki_image as _read_wiki_image,
)


def list_wiki_dir(directory: str, wiki_root: str) -> str:
"""List ``.md`` files in a wiki subdirectory.

Thin wrapper around :func:`openkb.agent.tools.list_wiki_files`.

Args:
directory: Path relative to *wiki_root* (e.g. ``"concepts"``).
wiki_root: Absolute path to ``<kb>/wiki``.
"""
root = Path(wiki_root).resolve()
target = (root / directory).resolve()
if not target.is_relative_to(root):
return "Access denied: path escapes wiki root."
if not target.exists() or not target.is_dir():
return "No files found."
names = sorted(p.name for p in target.iterdir() if p.suffix == ".md")
return "\n".join(names) if names else "No files found."
return _list_wiki_files(directory, wiki_root)


def read_wiki_file_for_skill(path: str, wiki_root: str) -> str:
"""Read a Markdown file from the wiki.

Thin wrapper around :func:`openkb.agent.tools.read_wiki_file`.

Args:
path: File path relative to *wiki_root* (e.g. ``"concepts/attention.md"``).
wiki_root: Absolute path to ``<kb>/wiki``.
"""
root = Path(wiki_root).resolve()
full = (root / path).resolve()
if not full.is_relative_to(root):
return "Access denied: path escapes wiki root."
if not full.exists():
return f"File not found: {path}"
return full.read_text(encoding="utf-8")
return _read_wiki_file(path, wiki_root)


def get_skill_page_content(doc_name: str, pages: str, wiki_root: str) -> str:
Expand Down