This repository has been archived on 2026-07-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
shira-hermes/api/services/skills.py
T
chaim 4aad81e11e 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>
2026-04-13 19:33:57 +00:00

140 lines
5.2 KiB
Python

"""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,
}