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:
@@ -11,7 +11,11 @@ AI_GATEWAY_API_KEY=
|
||||
# Claude model (passed through to ai-gateway)
|
||||
CLAUDE_MODEL=sonnet
|
||||
MAX_OUTPUT_TOKENS=4096
|
||||
MAX_AGENT_ITERATIONS=10
|
||||
|
||||
# EspoCRM — for MCP tools
|
||||
ESPOCRM_URL=https://espocrm.dev.marcus-law.co.il
|
||||
ESPOCRM_API_KEY=
|
||||
|
||||
# Data directory — persistent volume for skills, memory, profiles
|
||||
SHIRA_DATA_DIR=/opt/data
|
||||
|
||||
+7
-3
@@ -11,12 +11,16 @@ COPY mcp_server/ mcp_server/
|
||||
COPY config/ config/
|
||||
|
||||
# Install dependencies
|
||||
RUN pip install --no-cache-dir fastapi "uvicorn[standard]" httpx pydantic openai
|
||||
RUN pip install --no-cache-dir fastapi "uvicorn[standard]" httpx pydantic openai pyyaml
|
||||
|
||||
# Create data directory for skills/memory
|
||||
RUN mkdir -p /opt/data/skills /opt/data/memory
|
||||
# Create persistent data directories
|
||||
RUN mkdir -p /opt/data/skills /opt/data/profiles /opt/data/cases
|
||||
|
||||
# Copy pre-seeded skills (will be overridden by persistent volume if mounted)
|
||||
COPY config/skills/ /opt/data/skills/
|
||||
|
||||
ENV PORT=3000
|
||||
ENV SHIRA_DATA_DIR=/opt/data
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "3000", "--log-level", "info"]
|
||||
|
||||
+18
-1
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
@@ -16,10 +17,12 @@ logging.basicConfig(
|
||||
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
||||
)
|
||||
|
||||
logger = logging.getLogger("shira.app")
|
||||
|
||||
app = FastAPI(
|
||||
title="shira-hermes",
|
||||
description="Shira AI Assistant — Hermes Agent backend for EspoCRM SmartAssistant",
|
||||
version="0.1.0",
|
||||
version="0.2.0",
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
@@ -31,3 +34,17 @@ app.add_middleware(
|
||||
|
||||
app.include_router(health_router)
|
||||
app.include_router(smart_assistant_router)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def ensure_data_dirs():
|
||||
"""Create persistent data directories if they don't exist."""
|
||||
data_dir = os.environ.get("SHIRA_DATA_DIR", "/opt/data")
|
||||
dirs = [
|
||||
f"{data_dir}/skills",
|
||||
f"{data_dir}/profiles",
|
||||
f"{data_dir}/cases",
|
||||
]
|
||||
for d in dirs:
|
||||
Path(d).mkdir(parents=True, exist_ok=True)
|
||||
logger.info("Data directories ready at %s", data_dir)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -12,6 +12,9 @@ from mcp_server.espocrm_client import EspoCrmClient
|
||||
from mcp_server.tools.crm_tools import register_crm_tools
|
||||
from mcp_server.tools.document_tools import register_document_tools
|
||||
from mcp_server.tools.legal_tools import register_legal_tools
|
||||
from api.services.skills import register_skill_tools
|
||||
from api.services.memory import register_memory_tools
|
||||
from api.services.delegate import register_delegate_tool
|
||||
|
||||
logger = logging.getLogger("shira.agent")
|
||||
|
||||
@@ -50,8 +53,17 @@ class AgentRunner:
|
||||
context: dict,
|
||||
espocrm_url: str,
|
||||
espocrm_api_key: str,
|
||||
depth: int = 0,
|
||||
blocked_tools: set[str] | None = None,
|
||||
allowed_toolsets: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Run a conversation with tool-calling loop. Returns the final text response."""
|
||||
"""Run a conversation with tool-calling loop. Returns the final text response.
|
||||
|
||||
Args:
|
||||
depth: Current delegation depth. 0 = main conversation, 1 = child agent.
|
||||
blocked_tools: Tool names to exclude (used by delegation to prevent recursion).
|
||||
allowed_toolsets: If set, only register tools from these categories (crm, documents, legal).
|
||||
"""
|
||||
|
||||
# Build tools from context
|
||||
crm = EspoCrmClient(espocrm_url, espocrm_api_key)
|
||||
@@ -59,9 +71,26 @@ class AgentRunner:
|
||||
user_id = (context.get("currentUser") or {}).get("id")
|
||||
|
||||
tools_registry: dict[str, dict] = {}
|
||||
register_crm_tools(tools_registry, crm, case_id, user_id, context)
|
||||
register_document_tools(tools_registry, crm, case_id, context)
|
||||
register_legal_tools(tools_registry, crm, case_id, context)
|
||||
|
||||
# Core CRM/document/legal tools
|
||||
should_register_all = allowed_toolsets is None
|
||||
if should_register_all or "crm" in (allowed_toolsets or []):
|
||||
register_crm_tools(tools_registry, crm, case_id, user_id, context)
|
||||
if should_register_all or "documents" in (allowed_toolsets or []):
|
||||
register_document_tools(tools_registry, crm, case_id, context)
|
||||
if should_register_all or "legal" in (allowed_toolsets or []):
|
||||
register_legal_tools(tools_registry, crm, case_id, context)
|
||||
|
||||
# Phase 3 tools (only for main conversation, not child agents)
|
||||
if depth == 0:
|
||||
register_skill_tools(tools_registry)
|
||||
register_memory_tools(tools_registry, case_id, user_id)
|
||||
register_delegate_tool(tools_registry, context, espocrm_url, espocrm_api_key)
|
||||
|
||||
# Remove blocked tools (for delegation safety)
|
||||
if blocked_tools:
|
||||
for name in blocked_tools:
|
||||
tools_registry.pop(name, None)
|
||||
|
||||
# Build OpenAI tools format
|
||||
openai_tools = []
|
||||
@@ -78,10 +107,11 @@ class AgentRunner:
|
||||
# Build messages with system prompt
|
||||
openai_messages = [{"role": "system", "content": system_prompt}]
|
||||
openai_messages.extend(messages)
|
||||
self._last_messages = openai_messages
|
||||
|
||||
# Agentic loop
|
||||
for iteration in range(self.max_iterations):
|
||||
logger.info("[agent] iteration %d, messages=%d", iteration + 1, len(openai_messages))
|
||||
logger.info("[agent] depth=%d iteration %d, messages=%d", depth, iteration + 1, len(openai_messages))
|
||||
|
||||
try:
|
||||
response = await self.client.chat.completions.create(
|
||||
@@ -133,3 +163,7 @@ class AgentRunner:
|
||||
# Exhausted iterations
|
||||
logger.warning("[agent] max iterations (%d) reached", self.max_iterations)
|
||||
return "הגעתי למספר המקסימלי של פעולות. נסי לשאול שוב בצורה ממוקדת יותר."
|
||||
|
||||
def get_messages(self) -> list[dict]:
|
||||
"""Return the conversation messages from the last run (for post-conversation learning)."""
|
||||
return getattr(self, "_last_messages", [])
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""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,
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
"""Self-learning module — post-conversation skill extraction via meta-prompt."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
from api.services.skills import list_skills, view_skill, save_skill
|
||||
|
||||
logger = logging.getLogger("shira.learner")
|
||||
|
||||
# Minimum thresholds to trigger learning
|
||||
MIN_TOOL_CALLS = 3
|
||||
MIN_TURNS = 4
|
||||
|
||||
EXTRACTION_PROMPT = """\
|
||||
אתה מנתח שיחות ומזהה תבניות עבודה (workflows) שניתן להפוך ל-skills לשימוש חוזר.
|
||||
|
||||
נתח את השיחה הבאה בין עורך דין לעוזרת AI משפטית. חפש:
|
||||
- רצף פעולות שחוזר על עצמו או שנראה כתבנית כללית
|
||||
- workflow מורכב שעורך דין אחר יכול להשתמש בו
|
||||
- תהליך שכדאי לתעד כדי שהעוזרת תבצע אותו טוב יותר בפעם הבאה
|
||||
|
||||
Skills קיימים (אל תיצור כפילויות):
|
||||
{existing_skills}
|
||||
|
||||
אם מצאת workflow חדש שראוי להיות skill, החזר JSON:
|
||||
{{"create": true, "name": "skill-name-in-english", "description": "תיאור בעברית", "content": "תוכן ה-SKILL.md המלא כולל frontmatter"}}
|
||||
|
||||
אם מצאת שיפור ל-skill קיים, החזר:
|
||||
{{"update": true, "name": "existing-skill-name", "description": "תיאור מעודכן", "content": "תוכן SKILL.md מעודכן"}}
|
||||
|
||||
אם אין מה ללמוד, החזר:
|
||||
{{"create": false}}
|
||||
|
||||
חשוב: החזר רק JSON תקין, בלי טקסט נוסף.
|
||||
"""
|
||||
|
||||
|
||||
def _should_learn(messages: list[dict]) -> bool:
|
||||
"""Check if conversation meets learning thresholds."""
|
||||
tool_calls = sum(1 for m in messages if m.get("role") == "tool")
|
||||
turns = sum(1 for m in messages if m.get("role") in ("user", "assistant"))
|
||||
return tool_calls >= MIN_TOOL_CALLS and turns >= MIN_TURNS
|
||||
|
||||
|
||||
def _summarize_conversation(messages: list[dict]) -> str:
|
||||
"""Create a condensed version of the conversation for the extraction prompt."""
|
||||
parts = []
|
||||
for m in messages:
|
||||
role = m.get("role", "")
|
||||
content = m.get("content", "")
|
||||
if role == "system":
|
||||
continue
|
||||
if role == "tool":
|
||||
tool_id = m.get("tool_call_id", "")
|
||||
parts.append(f"[Tool Result {tool_id}]: {content[:200]}")
|
||||
elif role == "assistant":
|
||||
# Include tool calls info if present
|
||||
tool_calls = m.get("tool_calls", [])
|
||||
if tool_calls:
|
||||
for tc in tool_calls:
|
||||
fn = tc.get("function", {})
|
||||
parts.append(f"[Assistant calls {fn.get('name', '?')}({fn.get('arguments', '')[:100]})]")
|
||||
elif content:
|
||||
parts.append(f"[Assistant]: {content[:300]}")
|
||||
elif role == "user":
|
||||
parts.append(f"[User]: {content[:200]}")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
async def extract_skills(messages: list[dict]) -> None:
|
||||
"""Analyze a completed conversation and extract skills if applicable.
|
||||
|
||||
This runs as a background task — errors are logged but never raised.
|
||||
"""
|
||||
if not _should_learn(messages):
|
||||
logger.debug("Conversation too short for learning (skipped)")
|
||||
return
|
||||
|
||||
try:
|
||||
existing = list_skills()
|
||||
existing_summary = "\n".join(
|
||||
f'- {s["name"]}: {s["description"]}' for s in existing
|
||||
) or "אין skills קיימים."
|
||||
|
||||
conversation_text = _summarize_conversation(messages)
|
||||
prompt = EXTRACTION_PROMPT.format(existing_skills=existing_summary)
|
||||
|
||||
client = AsyncOpenAI(
|
||||
base_url=os.environ.get("AI_GATEWAY_URL", "http://localhost:3000").rstrip("/") + "/v1",
|
||||
api_key=os.environ.get("AI_GATEWAY_API_KEY", ""),
|
||||
)
|
||||
|
||||
response = await client.chat.completions.create(
|
||||
model=os.environ.get("CLAUDE_MODEL", "sonnet"),
|
||||
messages=[
|
||||
{"role": "system", "content": prompt},
|
||||
{"role": "user", "content": f"השיחה לניתוח:\n\n{conversation_text}"},
|
||||
],
|
||||
max_tokens=2048,
|
||||
)
|
||||
|
||||
result_text = (response.choices[0].message.content or "").strip()
|
||||
|
||||
# Parse JSON — handle possible markdown wrapping
|
||||
if result_text.startswith("```"):
|
||||
result_text = result_text.split("\n", 1)[-1].rsplit("```", 1)[0].strip()
|
||||
|
||||
result = json.loads(result_text)
|
||||
|
||||
if result.get("create"):
|
||||
name = result["name"]
|
||||
existing_content = view_skill(name)
|
||||
if existing_content:
|
||||
logger.info("Skill '%s' already exists, skipping create", name)
|
||||
return
|
||||
content = result.get("content", "")
|
||||
if not content:
|
||||
# Build content from description
|
||||
content = (
|
||||
f"---\nname: {name}\n"
|
||||
f"description: {result.get('description', '')}\n"
|
||||
f"version: 1.0.0\n---\n\n"
|
||||
f"{result.get('description', '')}\n"
|
||||
)
|
||||
save_skill(name, content)
|
||||
logger.info("Self-learning: created skill '%s'", name)
|
||||
|
||||
elif result.get("update"):
|
||||
name = result["name"]
|
||||
content = result.get("content", "")
|
||||
if content:
|
||||
save_skill(name, content)
|
||||
logger.info("Self-learning: updated skill '%s'", name)
|
||||
|
||||
else:
|
||||
logger.debug("Self-learning: no new skill identified")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning("Self-learning: failed to parse extraction result: %s", e)
|
||||
except Exception as e:
|
||||
logger.error("Self-learning error: %s", e)
|
||||
@@ -0,0 +1,227 @@
|
||||
"""Per-lawyer and per-case memory store — bounded markdown files with atomic writes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger("shira.memory")
|
||||
|
||||
DATA_DIR = os.environ.get("SHIRA_DATA_DIR", "/opt/data")
|
||||
PROFILES_DIR = f"{DATA_DIR}/profiles"
|
||||
CASES_DIR = f"{DATA_DIR}/cases"
|
||||
|
||||
PROFILE_MAX_CHARS = 1500
|
||||
CASE_MEMORY_MAX_CHARS = 2500
|
||||
ENTRY_DELIMITER = "\n§\n"
|
||||
|
||||
# Per-path locks for thread-safe writes
|
||||
_locks: dict[str, asyncio.Lock] = {}
|
||||
|
||||
|
||||
def _get_lock(path: str) -> asyncio.Lock:
|
||||
if path not in _locks:
|
||||
_locks[path] = asyncio.Lock()
|
||||
return _locks[path]
|
||||
|
||||
|
||||
def _read_file(path: str) -> str:
|
||||
p = Path(path)
|
||||
if not p.exists():
|
||||
return ""
|
||||
return p.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _write_file_atomic(path: str, content: str) -> None:
|
||||
p = Path(path)
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, tmp = tempfile.mkstemp(dir=str(p.parent), suffix=".tmp")
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
os.rename(tmp, str(p))
|
||||
except Exception:
|
||||
try:
|
||||
os.unlink(tmp)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
|
||||
|
||||
def _entries(content: str) -> list[str]:
|
||||
if not content.strip():
|
||||
return []
|
||||
return [e.strip() for e in content.split(ENTRY_DELIMITER) if e.strip()]
|
||||
|
||||
|
||||
def _join_entries(entries: list[str]) -> str:
|
||||
return ENTRY_DELIMITER.join(entries)
|
||||
|
||||
|
||||
def _truncate(content: str, max_chars: int) -> str:
|
||||
"""Remove oldest entries (from the top) until content fits."""
|
||||
if len(content) <= max_chars:
|
||||
return content
|
||||
entries = _entries(content)
|
||||
while entries and len(_join_entries(entries)) > max_chars:
|
||||
entries.pop(0)
|
||||
return _join_entries(entries)
|
||||
|
||||
|
||||
# ---------- Public API ----------
|
||||
|
||||
def load_user_profile(user_id: str) -> str:
|
||||
"""Load the per-lawyer profile. Returns empty string if none exists."""
|
||||
return _read_file(f"{PROFILES_DIR}/{user_id}.md")
|
||||
|
||||
|
||||
def load_case_memory(case_id: str) -> str:
|
||||
"""Load the per-case memory. Returns empty string if none exists."""
|
||||
return _read_file(f"{CASES_DIR}/{case_id}/memory.md")
|
||||
|
||||
|
||||
async def update_user_profile(user_id: str, action: str, content: str, match: str = "") -> str:
|
||||
"""Add, replace, or remove an entry in the lawyer's profile."""
|
||||
path = f"{PROFILES_DIR}/{user_id}.md"
|
||||
async with _get_lock(path):
|
||||
current = _read_file(path)
|
||||
entries = _entries(current)
|
||||
|
||||
if action == "add":
|
||||
entries.append(content)
|
||||
result = _truncate(_join_entries(entries), PROFILE_MAX_CHARS)
|
||||
_write_file_atomic(path, result)
|
||||
return f'✅ נוסף לפרופיל: "{content[:60]}"'
|
||||
|
||||
elif action == "replace":
|
||||
if not match:
|
||||
return "❌ צריך לספק match כדי למצוא את הרשומה להחלפה."
|
||||
replaced = False
|
||||
for i, e in enumerate(entries):
|
||||
if match in e:
|
||||
entries[i] = content
|
||||
replaced = True
|
||||
break
|
||||
if not replaced:
|
||||
return f'❌ לא נמצאה רשומה עם "{match}"'
|
||||
result = _truncate(_join_entries(entries), PROFILE_MAX_CHARS)
|
||||
_write_file_atomic(path, result)
|
||||
return f'✅ רשומה עודכנה בפרופיל.'
|
||||
|
||||
elif action == "remove":
|
||||
if not match:
|
||||
return "❌ צריך לספק match כדי למצוא את הרשומה למחיקה."
|
||||
original_len = len(entries)
|
||||
entries = [e for e in entries if match not in e]
|
||||
if len(entries) == original_len:
|
||||
return f'❌ לא נמצאה רשומה עם "{match}"'
|
||||
_write_file_atomic(path, _join_entries(entries))
|
||||
return "✅ רשומה הוסרה מהפרופיל."
|
||||
|
||||
elif action == "read":
|
||||
return current if current else "הפרופיל ריק."
|
||||
|
||||
return f"❌ פעולה לא מוכרת: {action}"
|
||||
|
||||
|
||||
async def update_case_memory(case_id: str, action: str, content: str, match: str = "") -> str:
|
||||
"""Add, replace, or remove an entry in the case memory."""
|
||||
path = f"{CASES_DIR}/{case_id}/memory.md"
|
||||
async with _get_lock(path):
|
||||
current = _read_file(path)
|
||||
entries = _entries(current)
|
||||
|
||||
if action == "add":
|
||||
entries.append(content)
|
||||
result = _truncate(_join_entries(entries), CASE_MEMORY_MAX_CHARS)
|
||||
_write_file_atomic(path, result)
|
||||
return f'✅ נשמר בזיכרון התיק: "{content[:60]}"'
|
||||
|
||||
elif action == "replace":
|
||||
if not match:
|
||||
return "❌ צריך לספק match כדי למצוא את הרשומה להחלפה."
|
||||
replaced = False
|
||||
for i, e in enumerate(entries):
|
||||
if match in e:
|
||||
entries[i] = content
|
||||
replaced = True
|
||||
break
|
||||
if not replaced:
|
||||
return f'❌ לא נמצאה רשומה עם "{match}"'
|
||||
result = _truncate(_join_entries(entries), CASE_MEMORY_MAX_CHARS)
|
||||
_write_file_atomic(path, result)
|
||||
return "✅ רשומה עודכנה בזיכרון התיק."
|
||||
|
||||
elif action == "remove":
|
||||
if not match:
|
||||
return "❌ צריך לספק match כדי למצוא את הרשומה למחיקה."
|
||||
original_len = len(entries)
|
||||
entries = [e for e in entries if match not in e]
|
||||
if len(entries) == original_len:
|
||||
return f'❌ לא נמצאה רשומה עם "{match}"'
|
||||
_write_file_atomic(path, _join_entries(entries))
|
||||
return "✅ רשומה הוסרה מזיכרון התיק."
|
||||
|
||||
elif action == "read":
|
||||
return current if current else "זיכרון התיק ריק."
|
||||
|
||||
return f"❌ פעולה לא מוכרת: {action}"
|
||||
|
||||
|
||||
def register_memory_tools(tools: dict, case_id: str | None, user_id: str | None) -> None:
|
||||
"""Register memory tools into the tools dict."""
|
||||
|
||||
async def _update_user_profile(action: str, content: str = "", match: str = "") -> str:
|
||||
if not user_id:
|
||||
return "❌ לא ניתן לזהות את המשתמש."
|
||||
return await update_user_profile(user_id, action, content, match)
|
||||
|
||||
async def _update_case_memory(action: str, content: str = "", match: str = "") -> str:
|
||||
if not case_id:
|
||||
return "❌ צריך להיות בתוך תיק כדי לעדכן את הזיכרון."
|
||||
return await update_case_memory(case_id, action, content, match)
|
||||
|
||||
tools["update_user_profile"] = {
|
||||
"description": (
|
||||
"Manage the lawyer's personal profile — preferences, communication style, common case types. "
|
||||
"This persists across conversations. Use proactively when you learn about the user's preferences."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["add", "replace", "remove", "read"],
|
||||
"description": "add=append entry, replace=find by match and replace, remove=find by match and delete, read=show current profile",
|
||||
},
|
||||
"content": {"type": "string", "description": "The content to add or replace with"},
|
||||
"match": {"type": "string", "description": "Substring to match when replacing or removing"},
|
||||
},
|
||||
"required": ["action"],
|
||||
},
|
||||
"handler": _update_user_profile,
|
||||
}
|
||||
|
||||
tools["update_case_memory"] = {
|
||||
"description": (
|
||||
"Manage the case's persistent memory — key facts, strategy, decisions, timeline. "
|
||||
"This persists across conversations about this case. Use alongside save_memory (which writes to CRM notes)."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["add", "replace", "remove", "read"],
|
||||
"description": "add=append entry, replace=find by match and replace, remove=find by match and delete, read=show current memory",
|
||||
},
|
||||
"content": {"type": "string", "description": "The content to add or replace with"},
|
||||
"match": {"type": "string", "description": "Substring to match when replacing or removing"},
|
||||
},
|
||||
"required": ["action"],
|
||||
},
|
||||
"handler": _update_case_memory,
|
||||
}
|
||||
@@ -57,7 +57,7 @@ def _now() -> str:
|
||||
return datetime.now(ISR_TZ).strftime("%d/%m/%Y %H:%M")
|
||||
|
||||
|
||||
def build_case_prompt(context: dict) -> str:
|
||||
def build_case_prompt(context: dict, user_profile: str = "", case_memory_notes: str = "", available_skills: list[dict] | None = None) -> str:
|
||||
case_info = context.get("case", {})
|
||||
contacts = context.get("contacts", [])
|
||||
open_tasks = context.get("openTasks", [])
|
||||
@@ -155,10 +155,25 @@ def build_case_prompt(context: dict) -> str:
|
||||
parts.append(f'\n\nCurrent user: {current_user.get("name", "")}')
|
||||
parts.append(f"\n\n{LEGAL_ASSISTANCE_PROMPT}")
|
||||
|
||||
# Skills section
|
||||
if available_skills:
|
||||
parts.append("\n\n=== SKILLS (learned workflows) ===\n")
|
||||
parts.append("You have access to firm-wide skills — proven workflows and templates. Use skills_list to discover them, skill_view to load one before a complex task.\n")
|
||||
parts.append("Available: " + ", ".join(s["name"] for s in available_skills))
|
||||
|
||||
# Cross-conversation memory sections
|
||||
if case_memory_notes:
|
||||
parts.append("\n\n=== YOUR NOTES (from previous conversations about this case) ===\n")
|
||||
parts.append(case_memory_notes)
|
||||
|
||||
if user_profile:
|
||||
parts.append("\n\n=== ABOUT THIS USER (learned preferences) ===\n")
|
||||
parts.append(user_profile)
|
||||
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
def build_office_prompt(context: dict) -> str:
|
||||
def build_office_prompt(context: dict, user_profile: str = "", available_skills: list[dict] | None = None) -> str:
|
||||
summary = context.get("summary", {})
|
||||
alerts = context.get("alerts", [])
|
||||
cases_by_status = context.get("casesByStatus", {})
|
||||
@@ -223,4 +238,16 @@ def build_office_prompt(context: dict) -> str:
|
||||
parts.append("\n".join(f'• [{r.get("scope", "")}] {r.get("name", "")}: {r.get("rule", "")}' for r in assistant_rules))
|
||||
|
||||
parts.append(f'\n\nCurrent user: {current_user.get("name", "")}')
|
||||
|
||||
# Skills section
|
||||
if available_skills:
|
||||
parts.append("\n\n=== SKILLS (learned workflows) ===\n")
|
||||
parts.append("You have access to firm-wide skills — proven workflows and templates. Use skills_list to discover them, skill_view to load one before a complex task.\n")
|
||||
parts.append("Available: " + ", ".join(s["name"] for s in available_skills))
|
||||
|
||||
# User profile
|
||||
if user_profile:
|
||||
parts.append("\n\n=== ABOUT THIS USER (learned preferences) ===\n")
|
||||
parts.append(user_profile)
|
||||
|
||||
return "".join(parts)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: deadline-tracker
|
||||
description: סריקת מועדים קריטיים והתראות פרואקטיביות
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# מעקב מועדים
|
||||
|
||||
## מתי להשתמש
|
||||
- כשעורך דין שואל "מה יש לי השבוע?" / "מה דחוף?"
|
||||
- כשמזהים משימות באיחור בקונטקסט
|
||||
- כשיש דיון ב-7 ימים הקרובים
|
||||
- במצב office — כשמבקשים סקירת מועדים
|
||||
|
||||
## תהליך
|
||||
|
||||
1. **סרוק את הקונטקסט** לכל דבר עם תאריך:
|
||||
- `openTasks` — בדוק `dateEnd` מול התאריך הנוכחי
|
||||
- `upcomingMeetings` — בדוק `dateStart`
|
||||
- `cNextHearing` — מועד הדיון הבא
|
||||
- `cFilingDeadline` — מועד הגשה (גישה ישירה)
|
||||
|
||||
2. **סווג לפי דחיפות:**
|
||||
- 🔴 **עבר מועד** — משימות שה-dateEnd עבר
|
||||
- 🟠 **היום / מחר** — פעולות ל-24 שעות הקרובות
|
||||
- 🟡 **השבוע** — ב-7 ימים הקרובים
|
||||
- 🟢 **בהמשך** — מעבר ל-7 ימים
|
||||
|
||||
3. **הצג בפורמט:**
|
||||
|
||||
```
|
||||
📅 מעקב מועדים — [תאריך היום]
|
||||
|
||||
🔴 עבר מועד:
|
||||
- [משימה] — היה צריך ב-[תאריך] (לפני [X] ימים)
|
||||
|
||||
🟠 היום / מחר:
|
||||
- [פגישה/דיון] — [תאריך ושעה]
|
||||
- [משימה] — מועד: [תאריך]
|
||||
|
||||
🟡 השבוע:
|
||||
- [דיון] — [תאריך] ב-[בית משפט]
|
||||
- [משימה] — מועד: [תאריך]
|
||||
|
||||
🟢 בהמשך:
|
||||
- [פגישה] — [תאריך]
|
||||
```
|
||||
|
||||
4. **הצע פעולות:**
|
||||
- למשימות באיחור: "האם לעדכן את המועד?"
|
||||
- לדיון קרוב: "האם להתחיל הכנה?" (skill: hearing-preparation)
|
||||
- למועד הגשה קרוב: "האם יש מה להכין?"
|
||||
|
||||
## כללים
|
||||
- תמיד ציין כמה ימים נשארו / עברו
|
||||
- סדר לפי דחיפות (עבר מועד → היום → השבוע)
|
||||
- אם אין כלום דחוף — ציין "אין מועדים דחופים"
|
||||
- שלב עם office mode — הראה מועדים לכל התיקים
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: direct-access-report-flow
|
||||
description: תהליך שלב-אחר-שלב לאיסוף מידע ויצירת דוח גישה ישירה
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# דוח גישה ישירה — תהליך איסוף מידע
|
||||
|
||||
## מתי להשתמש
|
||||
כשעורך דין מבקש ליצור דוח גישה ישירה / דוח דיווח ראשוני, או כשהתיק מסוג DirectAccess.
|
||||
|
||||
## 8 שלבי התהליך
|
||||
|
||||
### שלב 1: מילוי אוטומטי
|
||||
שלוף מהקונטקסט של התיק:
|
||||
- `cAppointmentDate` → תאריך מינוי
|
||||
- `cLegalAidType` → סוג סיוע
|
||||
- `cUrgencyLevel` → רמת דחיפות
|
||||
- `cFilingDeadline` → מועד הגשה
|
||||
|
||||
### שלב 2: פרטי מינוי
|
||||
שאל על:
|
||||
- תאריך מינוי (אם לא קיים)
|
||||
- סוג סיוע משפטי (ייצוג/ייעוץ/הגשת כתב טענות)
|
||||
- רמת דחיפות
|
||||
- מועד אחרון להגשה
|
||||
|
||||
### שלב 3: קשר עם לקוח
|
||||
שאל (קבץ 2-3 שאלות):
|
||||
- האם נוצר קשר עם הלקוח?
|
||||
- תאריך יצירת קשר ראשון
|
||||
- אם היה עיכוב — מה הסיבה?
|
||||
- תאריך פגישה
|
||||
- אם היה עיכוב בפגישה — מה הסיבה?
|
||||
|
||||
### שלב 4: פרטי הליך
|
||||
שאל:
|
||||
- מספר הליך
|
||||
- בית משפט (בחירה מ-6 אפשרויות)
|
||||
- נושא ההליך
|
||||
- תמצית התביעה
|
||||
- טענות ההגנה
|
||||
|
||||
### שלב 5: ניתוח משפטי (12 שאלות)
|
||||
שאל בקבוצות של 2-3:
|
||||
- q1: עיון במסמכים — האם עיינת? מה מצאת?
|
||||
- q2: נימוקים — האם עיינת בנימוקי הערעור?
|
||||
- q3: הרכב — האם יש התאמה/אי-התאמה בהרכב?
|
||||
- q4: תלונות — האם הוגשו תלונות?
|
||||
- q5: שימוע — האם היה שימוע כדין?
|
||||
- q6: בדיקות — האם בוצעו בדיקות קליניות?
|
||||
- q7: שיקום — האם בוצע שיקום?
|
||||
- q8: ציון — מה הציון שנקבע?
|
||||
- q9: קביעה — האם הקביעה סבירה?
|
||||
- q10: ייחודי — האם יש היבט ייחודי?
|
||||
- q11: תקדימים — פסיקה רלוונטית
|
||||
- q12: סיכום — הערכה כללית
|
||||
|
||||
### שלב 6: המלצה
|
||||
שאל:
|
||||
- סוג המלצה (9 אפשרויות)
|
||||
- פירוט ההמלצה
|
||||
|
||||
### שלב 7: מיצוי זכויות
|
||||
שאל:
|
||||
- האם נדרש סיוע נוסף?
|
||||
- פירוט
|
||||
- דחיפות
|
||||
|
||||
### שלב 8: יצירת הדוח
|
||||
כשכל המידע נאסף — הפעל generate_initial_report עם כל הפרמטרים.
|
||||
|
||||
## כללים חשובים
|
||||
- **לא** להפעיל את generate_initial_report עד שכל המידע נאסף
|
||||
- שאל בסגנון שיחה טבעי, לא כטופס
|
||||
- קבץ 2-3 שאלות קשורות ביחד
|
||||
- אם המשתמש לא יודע תשובה — דלג ועבור הלאה
|
||||
- שמור התקדמות בזיכרון עם save_memory
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: hearing-preparation
|
||||
description: הכנה לדיון בבית משפט — צ'קליסט ותזכורות
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# הכנה לדיון בבית משפט
|
||||
|
||||
## מתי להשתמש
|
||||
כשעורך דין מבקש להתכונן לדיון, שואל "מה צריך להכין", או כשיש דיון בשבוע הקרוב.
|
||||
|
||||
## תהליך
|
||||
|
||||
1. **בדיקת פרטי הדיון** — תאריך, בית משפט, שופט, נושא
|
||||
2. **סריקת מסמכים** — השתמש ב-list_documents ובדוק אילו מסמכים רלוונטיים
|
||||
3. **בדיקת משימות פתוחות** — האם יש משימות הכנה שטרם הושלמו
|
||||
4. **עובדות מפתח** — שלוף מזיכרון התיק (strategy, key_facts, decisions)
|
||||
5. **יצירת צ'קליסט** — הצע רשימת הכנה
|
||||
|
||||
## צ'קליסט הכנה לדיון
|
||||
|
||||
```
|
||||
✅ הכנה לדיון — [תאריך] — [בית משפט]
|
||||
|
||||
📑 מסמכים:
|
||||
☐ כתבי טענות מעודכנים
|
||||
☐ פרוטוקולים מדיונים קודמים
|
||||
☐ ראיות / נספחים
|
||||
☐ חוות דעת מומחה (אם רלוונטי)
|
||||
|
||||
📋 הכנה מקצועית:
|
||||
☐ סיכום טענות עיקריות
|
||||
☐ רשימת תקדימים רלוונטיים
|
||||
☐ רשימת עדים (אם רלוונטי)
|
||||
☐ חישובי סכומים (אם רלוונטי)
|
||||
|
||||
📞 תיאום:
|
||||
☐ אישור מועד עם לקוח
|
||||
☐ תיאום עם עדים
|
||||
☐ עדכון צד שכנגד (אם נדרש)
|
||||
|
||||
📦 ליום הדיון:
|
||||
☐ עותקים לשופט ולצדדים
|
||||
☐ תעודת זהות הלקוח
|
||||
☐ ייפוי כוח (אם נדרש)
|
||||
```
|
||||
|
||||
## כללים
|
||||
- אם חסרים מסמכים קריטיים — הדגש ב-⚠️
|
||||
- הצע ליצור משימות לכל פריט חסר
|
||||
- אם הדיון בעוד פחות מ-3 ימים — סמן דחיפות
|
||||
- שמור עובדות מפתח בזיכרון עם save_memory
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: legal-case-summary
|
||||
description: יצירת סיכום תיק משפטי מקיף עם כל הפרטים הרלוונטיים
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# סיכום תיק משפטי
|
||||
|
||||
## מתי להשתמש
|
||||
כשעורך דין מבקש סיכום תיק, סקירה כללית, או "מה המצב בתיק".
|
||||
|
||||
## תהליך
|
||||
|
||||
1. **איסוף מידע** — השתמש ב-query_info עם query_type "general" כדי לקבל את כל פרטי התיק
|
||||
2. **סריקת משימות** — בדוק openTasks ו-overdue
|
||||
3. **סריקת פגישות** — בדוק upcomingMeetings ו-recentMeetings
|
||||
4. **מסמכים** — אם יש מסמכים רלוונטיים, ציין אותם
|
||||
5. **זיכרון** — כלול עובדות מפתח מ-caseMemory
|
||||
|
||||
## מבנה הסיכום
|
||||
|
||||
```
|
||||
📋 סיכום תיק: [שם התיק] (#[מספר])
|
||||
|
||||
📊 סטטוס: [סטטוס בעברית]
|
||||
⚖️ בית משפט: [שם] | שופט: [שם]
|
||||
📅 דיון הבא: [תאריך]
|
||||
|
||||
👥 אנשי קשר:
|
||||
- [שם] ([תפקיד]) — [טלפון]
|
||||
|
||||
📌 עובדות מפתח:
|
||||
- [מזיכרון התיק]
|
||||
|
||||
📋 משימות פתוחות ([מספר]):
|
||||
- [רשימה]
|
||||
|
||||
📅 פגישות קרובות:
|
||||
- [רשימה]
|
||||
|
||||
📝 הערות אחרונות:
|
||||
- [רשימה]
|
||||
|
||||
⚠️ דגשים:
|
||||
- [משימות באיחור, מועדים קרובים, פעולות נדרשות]
|
||||
```
|
||||
|
||||
## כללים
|
||||
- תמיד בעברית
|
||||
- תאריכים בפורמט DD/MM/YYYY
|
||||
- סמן משימות באיחור ב-⚠️
|
||||
- אם יש מועד דיון ב-7 ימים הקרובים — הדגש
|
||||
- הצע פעולות המשך (למשל "האם ליצור משימה?")
|
||||
@@ -10,6 +10,7 @@ dependencies = [
|
||||
"pydantic>=2.10.0",
|
||||
"mcp>=1.0.0",
|
||||
"openai>=1.50.0",
|
||||
"pyyaml>=6.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
Reference in New Issue
Block a user