feat: add skills, memory, delegation and self-learning (Phase 3)
Add three Hermes-inspired capabilities to Shira: - Skills system: firm-wide workflow templates (list/view/manage tools), 4 pre-seeded skills (case summary, hearing prep, direct access report, deadline tracker), auto-learning via post-conversation meta-prompt - Cross-conversation memory: per-lawyer profiles and per-case memory stored as bounded markdown files, injected into system prompts - Sub-agent delegation: spawn up to 3 child AgentRunner instances for parallel document analysis and complex multi-step tasks New files: skills.py, memory.py, delegate.py, learner.py, 4 SKILL.md Updated: agent_runner (6 new tools, depth/blocked_tools support), prompt_builder (skills/memory injection), route (memory loading, background learning), main.py (data dirs), Dockerfile (pyyaml, skills) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""Skills engine — list, view, manage, and register skill tools."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
logger = logging.getLogger("shira.skills")
|
||||
|
||||
SKILLS_DIR = os.environ.get("SHIRA_DATA_DIR", "/opt/data") + "/skills"
|
||||
|
||||
|
||||
def _parse_frontmatter(text: str) -> tuple[dict, str]:
|
||||
"""Parse YAML frontmatter from a SKILL.md file. Returns (meta, body)."""
|
||||
if not text.startswith("---"):
|
||||
return {}, text
|
||||
end = text.find("---", 3)
|
||||
if end == -1:
|
||||
return {}, text
|
||||
try:
|
||||
meta = yaml.safe_load(text[3:end]) or {}
|
||||
except yaml.YAMLError:
|
||||
meta = {}
|
||||
body = text[end + 3:].lstrip("\n")
|
||||
return meta, body
|
||||
|
||||
|
||||
def list_skills() -> list[dict]:
|
||||
"""Scan the skills directory and return metadata for each skill."""
|
||||
skills_path = Path(SKILLS_DIR)
|
||||
if not skills_path.exists():
|
||||
return []
|
||||
|
||||
result = []
|
||||
for entry in sorted(skills_path.iterdir()):
|
||||
skill_file = entry / "SKILL.md" if entry.is_dir() else None
|
||||
if not skill_file or not skill_file.exists():
|
||||
continue
|
||||
try:
|
||||
meta, _ = _parse_frontmatter(skill_file.read_text(encoding="utf-8"))
|
||||
result.append({
|
||||
"name": meta.get("name", entry.name),
|
||||
"description": meta.get("description", ""),
|
||||
"version": meta.get("version", "1.0.0"),
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning("Failed to parse skill %s: %s", entry.name, e)
|
||||
return result
|
||||
|
||||
|
||||
def view_skill(skill_name: str) -> str | None:
|
||||
"""Load the full SKILL.md content for a given skill name."""
|
||||
skill_file = Path(SKILLS_DIR) / skill_name / "SKILL.md"
|
||||
if not skill_file.exists():
|
||||
return None
|
||||
return skill_file.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def save_skill(skill_name: str, content: str) -> None:
|
||||
"""Create or overwrite a skill file."""
|
||||
skill_dir = Path(SKILLS_DIR) / skill_name
|
||||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
(skill_dir / "SKILL.md").write_text(content, encoding="utf-8")
|
||||
logger.info("Saved skill: %s", skill_name)
|
||||
|
||||
|
||||
def register_skill_tools(tools: dict) -> None:
|
||||
"""Register skill-related tools into the tools dict."""
|
||||
|
||||
async def skills_list() -> str:
|
||||
import json
|
||||
skills = list_skills()
|
||||
if not skills:
|
||||
return "אין skills זמינים כרגע."
|
||||
lines = ["Skills זמינים:"]
|
||||
for s in skills:
|
||||
lines.append(f'- **{s["name"]}**: {s["description"]}')
|
||||
return "\n".join(lines)
|
||||
|
||||
async def skill_view(skill_name: str) -> str:
|
||||
content = view_skill(skill_name)
|
||||
if content is None:
|
||||
return f'❌ Skill "{skill_name}" לא נמצא.'
|
||||
return content
|
||||
|
||||
async def skill_manage(action: str, skill_name: str, content: str = "") -> str:
|
||||
if action == "create":
|
||||
if not content:
|
||||
return "❌ צריך לספק תוכן ל-skill חדש."
|
||||
existing = view_skill(skill_name)
|
||||
if existing:
|
||||
return f'❌ Skill "{skill_name}" כבר קיים. השתמש ב-action "update" כדי לעדכן.'
|
||||
save_skill(skill_name, content)
|
||||
return f'✅ Skill "{skill_name}" נוצר בהצלחה.'
|
||||
elif action == "update":
|
||||
existing = view_skill(skill_name)
|
||||
if not existing:
|
||||
return f'❌ Skill "{skill_name}" לא נמצא. השתמש ב-action "create" כדי ליצור.'
|
||||
if not content:
|
||||
return "❌ צריך לספק תוכן מעודכן."
|
||||
save_skill(skill_name, content)
|
||||
return f'✅ Skill "{skill_name}" עודכן בהצלחה.'
|
||||
else:
|
||||
return f'❌ פעולה לא מוכרת: {action}. השתמש ב-"create" או "update".'
|
||||
|
||||
tools["skills_list"] = {
|
||||
"description": "List all available skills (learned workflows and templates). Returns skill names and descriptions.",
|
||||
"parameters": {"type": "object", "properties": {}, "required": []},
|
||||
"handler": skills_list,
|
||||
}
|
||||
|
||||
tools["skill_view"] = {
|
||||
"description": "View the full content of a specific skill. Use after skills_list to load a relevant workflow before a complex task.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"skill_name": {"type": "string", "description": "The skill name (from skills_list)"},
|
||||
},
|
||||
"required": ["skill_name"],
|
||||
},
|
||||
"handler": skill_view,
|
||||
}
|
||||
|
||||
tools["skill_manage"] = {
|
||||
"description": "Create or update a skill. Use when you discover a reusable workflow pattern during a conversation.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {"type": "string", "enum": ["create", "update"], "description": "Create new or update existing"},
|
||||
"skill_name": {"type": "string", "description": "Skill name (lowercase-dashes)"},
|
||||
"content": {"type": "string", "description": "Full skill content in markdown with YAML frontmatter"},
|
||||
},
|
||||
"required": ["action", "skill_name", "content"],
|
||||
},
|
||||
"handler": skill_manage,
|
||||
}
|
||||
Reference in New Issue
Block a user