mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-27 21:05:34 +00:00
feat: add SkillManageTool for agent-authored procedural memory
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
153435edb6
commit
c1e01b1e8b
@@ -1085,6 +1085,15 @@ class CompressionConfig:
|
||||
strategy: str = "session_consolidation"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class SkillsConfig:
|
||||
"""Configuration for agent-authored procedural skills."""
|
||||
|
||||
skills_dir: str = "~/.openjarvis/skills/"
|
||||
nudge_interval: int = 15
|
||||
auto_discover: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class JarvisConfig:
|
||||
"""Top-level configuration for OpenJarvis."""
|
||||
@@ -1112,6 +1121,7 @@ class JarvisConfig:
|
||||
memory_files: MemoryFilesConfig = field(default_factory=MemoryFilesConfig)
|
||||
system_prompt: SystemPromptConfig = field(default_factory=SystemPromptConfig)
|
||||
compression: CompressionConfig = field(default_factory=CompressionConfig)
|
||||
skills: SkillsConfig = field(default_factory=SkillsConfig)
|
||||
|
||||
@property
|
||||
def memory(self) -> StorageConfig:
|
||||
|
||||
@@ -104,4 +104,18 @@ def load_skill(
|
||||
return manifest
|
||||
|
||||
|
||||
__all__ = ["load_skill"]
|
||||
def discover_skills(directory: str | Path) -> list[SkillManifest]:
|
||||
"""Scan directory for TOML skill files and load them."""
|
||||
directory = Path(directory).expanduser()
|
||||
if not directory.exists():
|
||||
return []
|
||||
manifests = []
|
||||
for toml_file in sorted(directory.glob("*.toml")):
|
||||
try:
|
||||
manifests.append(load_skill(toml_file))
|
||||
except Exception:
|
||||
continue
|
||||
return manifests
|
||||
|
||||
|
||||
__all__ = ["load_skill", "discover_skills"]
|
||||
|
||||
@@ -86,4 +86,9 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import openjarvis.tools.skill_manage # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
__all__ = ["BaseTool", "ToolExecutor", "ToolSpec"]
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""SkillManageTool — create, list, load, or delete agent-authored skills."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, List
|
||||
|
||||
from openjarvis.core.registry import ToolRegistry
|
||||
from openjarvis.core.types import ToolResult
|
||||
from openjarvis.tools._stubs import BaseTool, ToolSpec
|
||||
|
||||
|
||||
@ToolRegistry.register("skill_manage")
|
||||
class SkillManageTool(BaseTool):
|
||||
"""Manage agent-authored procedural skills."""
|
||||
|
||||
def __init__(self, skills_dir: Path | str = "~/.openjarvis/skills/") -> None:
|
||||
self._skills_dir = Path(skills_dir).expanduser()
|
||||
|
||||
@property
|
||||
def spec(self) -> ToolSpec:
|
||||
return ToolSpec(
|
||||
name="skill_manage",
|
||||
description="Create, list, load, or delete agent-authored skills.",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["create", "list", "load", "delete"],
|
||||
"description": "Action to perform.",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Skill name (for create/load/delete).",
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Skill description (for create).",
|
||||
},
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"description": (
|
||||
"List of step dicts with tool_name and optional"
|
||||
" arguments_template (for create)."
|
||||
),
|
||||
},
|
||||
},
|
||||
"required": ["action"],
|
||||
},
|
||||
category="skill",
|
||||
)
|
||||
|
||||
def execute(self, **params: Any) -> ToolResult:
|
||||
action = params.get("action", "list")
|
||||
name = params.get("name", "")
|
||||
if action == "create":
|
||||
return self._create(
|
||||
name, params.get("description", ""), params.get("steps", [])
|
||||
)
|
||||
elif action == "list":
|
||||
return self._list()
|
||||
elif action == "load":
|
||||
return self._load(name)
|
||||
elif action == "delete":
|
||||
return self._delete(name)
|
||||
return ToolResult(
|
||||
tool_name=self.spec.name,
|
||||
success=False,
|
||||
content=f"Unknown action: {action}",
|
||||
)
|
||||
|
||||
def _create(
|
||||
self, name: str, description: str, steps: List[dict]
|
||||
) -> ToolResult:
|
||||
if not name:
|
||||
return ToolResult(
|
||||
tool_name=self.spec.name,
|
||||
success=False,
|
||||
content="Skill name is required.",
|
||||
)
|
||||
self._skills_dir.mkdir(parents=True, exist_ok=True)
|
||||
path = self._skills_dir / f"{name}.toml"
|
||||
lines = [
|
||||
"[skill]",
|
||||
f'name = "{name}"',
|
||||
f'description = "{description}"',
|
||||
"",
|
||||
]
|
||||
for step in steps:
|
||||
lines.append("[[skill.steps]]")
|
||||
lines.append(f'tool_name = "{step.get("tool_name", "")}"')
|
||||
if "arguments_template" in step:
|
||||
lines.append(
|
||||
f"arguments_template = '{step['arguments_template']}'"
|
||||
)
|
||||
if "output_key" in step:
|
||||
lines.append(f'output_key = "{step["output_key"]}"')
|
||||
lines.append("")
|
||||
path.write_text("\n".join(lines))
|
||||
return ToolResult(
|
||||
tool_name=self.spec.name,
|
||||
success=True,
|
||||
content=f"Created skill: {name}",
|
||||
)
|
||||
|
||||
def _list(self) -> ToolResult:
|
||||
if not self._skills_dir.exists():
|
||||
return ToolResult(
|
||||
tool_name=self.spec.name,
|
||||
success=True,
|
||||
content="No skills directory found.",
|
||||
)
|
||||
skills = []
|
||||
for f in sorted(self._skills_dir.glob("*.toml")):
|
||||
skills.append(f.stem)
|
||||
if not skills:
|
||||
return ToolResult(
|
||||
tool_name=self.spec.name,
|
||||
success=True,
|
||||
content="No skills found.",
|
||||
)
|
||||
return ToolResult(
|
||||
tool_name=self.spec.name,
|
||||
success=True,
|
||||
content="Available skills:\n"
|
||||
+ "\n".join(f"- {s}" for s in skills),
|
||||
)
|
||||
|
||||
def _load(self, name: str) -> ToolResult:
|
||||
path = self._skills_dir / f"{name}.toml"
|
||||
if not path.exists():
|
||||
return ToolResult(
|
||||
tool_name=self.spec.name,
|
||||
success=False,
|
||||
content=f"Skill not found: {name}",
|
||||
)
|
||||
return ToolResult(
|
||||
tool_name=self.spec.name,
|
||||
success=True,
|
||||
content=path.read_text(),
|
||||
)
|
||||
|
||||
def _delete(self, name: str) -> ToolResult:
|
||||
path = self._skills_dir / f"{name}.toml"
|
||||
if not path.exists():
|
||||
return ToolResult(
|
||||
tool_name=self.spec.name,
|
||||
success=False,
|
||||
content=f"Skill not found: {name}",
|
||||
)
|
||||
path.unlink()
|
||||
return ToolResult(
|
||||
tool_name=self.spec.name,
|
||||
success=True,
|
||||
content=f"Deleted skill: {name}",
|
||||
)
|
||||
@@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def skills_dir(tmp_path: Path) -> Path:
|
||||
d = tmp_path / "skills"
|
||||
d.mkdir()
|
||||
return d
|
||||
|
||||
|
||||
def test_skill_create(skills_dir: Path):
|
||||
from openjarvis.tools.skill_manage import SkillManageTool
|
||||
|
||||
tool = SkillManageTool(skills_dir=skills_dir)
|
||||
result = tool.execute(
|
||||
action="create",
|
||||
name="api_health",
|
||||
description="Check API health",
|
||||
steps=[
|
||||
{
|
||||
"tool_name": "http_request",
|
||||
"arguments_template": '{"url": "{endpoint}/health"}',
|
||||
}
|
||||
],
|
||||
)
|
||||
assert result.success
|
||||
assert (skills_dir / "api_health.toml").exists()
|
||||
|
||||
|
||||
def test_skill_list(skills_dir: Path):
|
||||
from openjarvis.tools.skill_manage import SkillManageTool
|
||||
|
||||
tool = SkillManageTool(skills_dir=skills_dir)
|
||||
tool.execute(
|
||||
action="create",
|
||||
name="skill_a",
|
||||
description="Skill A",
|
||||
steps=[{"tool_name": "calculator"}],
|
||||
)
|
||||
tool.execute(
|
||||
action="create",
|
||||
name="skill_b",
|
||||
description="Skill B",
|
||||
steps=[{"tool_name": "calculator"}],
|
||||
)
|
||||
result = tool.execute(action="list")
|
||||
assert "skill_a" in result.content
|
||||
assert "skill_b" in result.content
|
||||
|
||||
|
||||
def test_skill_delete(skills_dir: Path):
|
||||
from openjarvis.tools.skill_manage import SkillManageTool
|
||||
|
||||
tool = SkillManageTool(skills_dir=skills_dir)
|
||||
tool.execute(
|
||||
action="create",
|
||||
name="temp_skill",
|
||||
description="Temp",
|
||||
steps=[{"tool_name": "calculator"}],
|
||||
)
|
||||
assert (skills_dir / "temp_skill.toml").exists()
|
||||
result = tool.execute(action="delete", name="temp_skill")
|
||||
assert result.success
|
||||
assert not (skills_dir / "temp_skill.toml").exists()
|
||||
|
||||
|
||||
def test_skill_load(skills_dir: Path):
|
||||
from openjarvis.tools.skill_manage import SkillManageTool
|
||||
|
||||
tool = SkillManageTool(skills_dir=skills_dir)
|
||||
tool.execute(
|
||||
action="create",
|
||||
name="my_skill",
|
||||
description="My skill desc",
|
||||
steps=[
|
||||
{
|
||||
"tool_name": "web_search",
|
||||
"arguments_template": '{"q": "test"}',
|
||||
}
|
||||
],
|
||||
)
|
||||
result = tool.execute(action="load", name="my_skill")
|
||||
assert "web_search" in result.content
|
||||
assert "My skill desc" in result.content
|
||||
Reference in New Issue
Block a user