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:
2026-04-13 19:33:57 +00:00
parent 4c491ce45a
commit 4aad81e11e
15 changed files with 1022 additions and 13 deletions
+40 -2
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
import os
import logging
@@ -10,6 +11,9 @@ from pydantic import BaseModel
from api.services.prompt_builder import build_case_prompt, build_office_prompt
from api.services.agent_runner import AgentRunner
from api.services.skills import list_skills
from api.services.memory import load_user_profile, load_case_memory
from api.services.learner import extract_skills
logger = logging.getLogger("shira.api")
@@ -64,8 +68,29 @@ async def smart_assistant_chat(request: Request):
if not message and not conversation_history:
return {"text": "no message", "actions": [], "conversationId": conversation_id}
# Build system prompt
system_prompt = build_office_prompt(context) if mode == "office" else build_case_prompt(context)
# Extract IDs for memory loading
case_id = (context.get("case") or {}).get("id") or (context.get("drillDown", {}).get("case", {}).get("id"))
user_id = (context.get("currentUser") or {}).get("id")
# Load cross-conversation memory and skills
user_profile = load_user_profile(user_id) if user_id else ""
case_memory_notes = load_case_memory(case_id) if case_id else ""
available_skills = list_skills()
# Build system prompt with memory and skills injected
if mode == "office":
system_prompt = build_office_prompt(
context,
user_profile=user_profile,
available_skills=available_skills,
)
else:
system_prompt = build_case_prompt(
context,
user_profile=user_profile,
case_memory_notes=case_memory_notes,
available_skills=available_skills,
)
# Build messages from conversation history
messages = []
@@ -100,9 +125,22 @@ async def smart_assistant_chat(request: Request):
logger.info("[smart-assistant] response: %s...", text[:80])
# Trigger self-learning in the background (non-blocking)
conversation_messages = runner.get_messages()
if conversation_messages:
asyncio.create_task(_learn_in_background(conversation_messages))
return {
"text": text,
"actions": [],
"executedActions": [],
"conversationId": conversation_id,
}
async def _learn_in_background(messages: list[dict]) -> None:
"""Run skill extraction in background — errors are swallowed and logged."""
try:
await extract_skills(messages)
except Exception as e:
logger.error("[learner] background learning failed: %s", e)