4aad81e11e
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>
131 lines
4.3 KiB
Python
131 lines
4.3 KiB
Python
"""Delegation engine — spawn child AgentRunner instances for complex parallel tasks."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import os
|
|
|
|
logger = logging.getLogger("shira.delegate")
|
|
|
|
MAX_CONCURRENT_CHILDREN = 3
|
|
BLOCKED_TOOLS = {"delegate_task", "update_user_profile", "update_case_memory", "skill_manage"}
|
|
|
|
|
|
async def run_child_task(
|
|
goal: str,
|
|
context: dict,
|
|
espocrm_url: str,
|
|
espocrm_api_key: str,
|
|
allowed_toolsets: list[str] | None = None,
|
|
) -> str:
|
|
"""Run a single child agent conversation with a focused goal."""
|
|
from api.services.agent_runner import AgentRunner
|
|
|
|
runner = AgentRunner(
|
|
ai_gateway_url=os.environ.get("AI_GATEWAY_URL", "http://localhost:3000"),
|
|
ai_gateway_api_key=os.environ.get("AI_GATEWAY_API_KEY", ""),
|
|
model=os.environ.get("CLAUDE_MODEL", "sonnet"),
|
|
max_tokens=int(os.environ.get("MAX_OUTPUT_TOKENS", "4096")),
|
|
max_iterations=8,
|
|
)
|
|
|
|
system_prompt = (
|
|
"You are a focused sub-agent working on a specific delegated task.\n"
|
|
"Complete the task thoroughly and return a clear, concise result.\n"
|
|
"Always respond in Hebrew.\n\n"
|
|
f"YOUR TASK:\n{goal}\n"
|
|
)
|
|
|
|
messages = [{"role": "user", "content": goal}]
|
|
|
|
try:
|
|
result = await runner.run(
|
|
system_prompt=system_prompt,
|
|
messages=messages,
|
|
context=context,
|
|
espocrm_url=espocrm_url,
|
|
espocrm_api_key=espocrm_api_key,
|
|
depth=1,
|
|
blocked_tools=BLOCKED_TOOLS,
|
|
allowed_toolsets=allowed_toolsets,
|
|
)
|
|
return result
|
|
except Exception as e:
|
|
logger.error("Child task failed: %s — %s", goal[:50], e)
|
|
return f"❌ משימה נכשלה: {e}"
|
|
|
|
|
|
async def delegate_tasks(
|
|
tasks: list[dict],
|
|
context: dict,
|
|
espocrm_url: str,
|
|
espocrm_api_key: str,
|
|
) -> str:
|
|
"""Run multiple child tasks concurrently. Returns combined results."""
|
|
if len(tasks) > MAX_CONCURRENT_CHILDREN:
|
|
tasks = tasks[:MAX_CONCURRENT_CHILDREN]
|
|
logger.warning("Truncated to %d concurrent children", MAX_CONCURRENT_CHILDREN)
|
|
|
|
coros = [
|
|
run_child_task(
|
|
goal=t["goal"],
|
|
context=context,
|
|
espocrm_url=espocrm_url,
|
|
espocrm_api_key=espocrm_api_key,
|
|
allowed_toolsets=t.get("toolsets"),
|
|
)
|
|
for t in tasks
|
|
]
|
|
|
|
results = await asyncio.gather(*coros, return_exceptions=True)
|
|
|
|
parts = []
|
|
for i, (task, result) in enumerate(zip(tasks, results), 1):
|
|
if isinstance(result, Exception):
|
|
parts.append(f"### משימה {i}: {task['goal']}\n❌ שגיאה: {result}")
|
|
else:
|
|
parts.append(f"### משימה {i}: {task['goal']}\n{result}")
|
|
|
|
return "\n\n".join(parts)
|
|
|
|
|
|
def register_delegate_tool(
|
|
tools: dict,
|
|
context: dict,
|
|
espocrm_url: str,
|
|
espocrm_api_key: str,
|
|
) -> None:
|
|
"""Register the delegate_task tool."""
|
|
|
|
async def delegate_task(goal: str, context_extra: str = "", toolsets: str = "") -> str:
|
|
"""Delegate a single complex task to a focused sub-agent."""
|
|
allowed = toolsets.split(",") if toolsets else None
|
|
result = await run_child_task(
|
|
goal=f"{goal}\n\n{context_extra}" if context_extra else goal,
|
|
context=context,
|
|
espocrm_url=espocrm_url,
|
|
espocrm_api_key=espocrm_api_key,
|
|
allowed_toolsets=allowed,
|
|
)
|
|
return result
|
|
|
|
tools["delegate_task"] = {
|
|
"description": (
|
|
"Delegate a complex task to a focused sub-agent that runs independently. "
|
|
"Use for: parallel document analysis, multi-step research, report preparation. "
|
|
"The sub-agent has access to CRM and document tools but cannot delegate further or modify memory/skills."
|
|
),
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"goal": {"type": "string", "description": "Clear description of the task for the sub-agent"},
|
|
"context_extra": {"type": "string", "description": "Additional context to help the sub-agent"},
|
|
"toolsets": {"type": "string", "description": "Comma-separated tool categories to allow (crm,documents,legal). Empty = all."},
|
|
},
|
|
"required": ["goal"],
|
|
},
|
|
"handler": delegate_task,
|
|
}
|