feat: initial shira-hermes service

Shira AI backend using OpenAI-compatible agent with tool-calling loop,
replacing the smart-assistant route in ai-gateway.

Components:
- FastAPI adapter (same HTTP contract as ai-gateway)
- Agent runner with agentic tool-calling loop via ai-gateway /v1/chat/completions
- EspoCRM REST API client
- 20 MCP tools (CRM, documents, legal assistance)
- Prompt builder ported from ai-gateway JS to Python
- SOUL.md personality definition
- Dockerfile for Coolify deployment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 17:29:35 +00:00
commit 6f7b94d6f8
20 changed files with 1359 additions and 0 deletions
View File
+37
View File
@@ -0,0 +1,37 @@
"""Shared helper functions for MCP tools."""
from __future__ import annotations
import re
def normalize_date(date_str: str | None, default_time: str = "08:00:00") -> str | None:
"""Normalize various date formats to YYYY-MM-DD HH:mm:ss."""
if not date_str:
return None
# DD/MM/YYYY or DD-MM-YYYY or DD.MM.YYYY
m = re.match(r"^(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{4})(?:\s+(.+))?$", date_str)
if m:
base = f"{m.group(3)}-{m.group(2).zfill(2)}-{m.group(1).zfill(2)}"
return f"{base} {m.group(4) or default_time}"
# YYYY-MM-DD
if re.match(r"^\d{4}-\d{2}-\d{2}$", date_str):
return f"{date_str} {default_time}"
# YYYY-MM-DD HH:mm
if re.match(r"^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}$", date_str):
return f"{date_str}:00"
return date_str
def ok(message: str) -> str:
"""Return a success message."""
return message
def fail(message: str) -> str:
"""Return an error message (prefixed for visibility)."""
return f"{message}"
+354
View File
@@ -0,0 +1,354 @@
"""Core CRM tools: tasks, notes, meetings, calls, status, hearings, queries, deletions, rules."""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from mcp_server.espocrm_client import EspoCrmClient
from mcp_server.tools._helpers import normalize_date, ok, fail
VALID_STATUSES = {
"New": "חדש",
"Assigned": "בטיפול",
"Pending": "ממתין",
"PendingHearing": "ממתין לדיון",
"PendingDecision": "ממתין להחלטה",
"PendingResponse": "ממתין לתשובה",
"Negotiation": "משא ומתן",
"Suspended": "מושהה",
"ClosedSettlement": "סגור - הסכם",
"ClosedJudgment": "סגור - פס״ד",
"Closed": "סגור",
"Rejected": "נדחה",
}
STATUS_KEYS = list(VALID_STATUSES.keys())
def register_crm_tools(
tools: dict,
crm: "EspoCrmClient",
case_id: str | None,
user_id: str | None,
context: dict,
):
"""Register all core CRM tools into the tools dict."""
async def create_task(name: str, dateEnd: str = "", priority: str = "Normal", description: str = "") -> str:
try:
body = {
"name": name,
"status": "Not Started",
"priority": priority,
"dateEnd": normalize_date(dateEnd) if dateEnd else None,
"description": description or None,
"assignedUserId": user_id,
}
if case_id:
body["parentType"] = "Case"
body["parentId"] = case_id
result = await crm.post("Task", body)
return ok(f'משימה "{name}" נוצרה בהצלחה (ID: {result["id"]})')
except Exception as e:
return fail(f"שגיאה ביצירת משימה: {e}")
async def add_note(post: str) -> str:
try:
body = {"type": "Post", "post": post}
if case_id:
body["parentType"] = "Case"
body["parentId"] = case_id
result = await crm.post("Note", body)
return ok(f'הערה נוספה בהצלחה (ID: {result["id"]})')
except Exception as e:
return fail(f"שגיאה בהוספת הערה: {e}")
async def change_status(status: str) -> str:
if not case_id:
return fail("צריך להיות בתוך תיק כדי לשנות סטטוס")
try:
await crm.put(f"Case/{case_id}", {"status": status})
label = VALID_STATUSES.get(status, status)
return ok(f'סטטוס שונה ל-"{label}"')
except Exception as e:
return fail(f"שגיאה בשינוי סטטוס: {e}")
async def create_meeting(name: str, dateStart: str, dateEnd: str = "", description: str = "") -> str:
try:
start = normalize_date(dateStart)
body = {
"name": name,
"status": "Planned",
"dateStart": start,
"dateEnd": normalize_date(dateEnd) if dateEnd else start,
"description": description or None,
"assignedUserId": user_id,
}
if case_id:
body["parentType"] = "Case"
body["parentId"] = case_id
result = await crm.post("Meeting", body)
return ok(f'פגישה "{name}" נקבעה בהצלחה (ID: {result["id"]})')
except Exception as e:
return fail(f"שגיאה ביצירת פגישה: {e}")
async def create_call(
name: str,
dateStart: str = "",
dateEnd: str = "",
description: str = "",
direction: str = "Outbound",
status: str = "Held",
) -> str:
try:
start = normalize_date(dateStart) if dateStart else None
body = {
"name": name,
"status": status,
"direction": direction,
"dateStart": start,
"dateEnd": normalize_date(dateEnd) if dateEnd else start,
"description": description or None,
"assignedUserId": user_id,
}
if case_id:
body["parentType"] = "Case"
body["parentId"] = case_id
result = await crm.post("Call", body)
return ok(f'שיחה "{name}" תועדה בהצלחה (ID: {result["id"]})')
except Exception as e:
return fail(f"שגיאה בתיעוד שיחה: {e}")
async def schedule_hearing(dateTime: str, court: str = "", description: str = "") -> str:
if not case_id:
return fail("צריך להיות בתוך תיק כדי לתזמן דיון")
try:
dt = normalize_date(dateTime)
await crm.put(f"Case/{case_id}", {"cNextHearing": dt})
meeting_name = description or "דיון בבית המשפט"
if court:
meeting_name += f" - {court}"
body = {
"name": meeting_name,
"status": "Planned",
"dateStart": dt,
"dateEnd": dt,
"parentType": "Case",
"parentId": case_id,
"assignedUserId": user_id,
}
result = await crm.post("Meeting", body)
return ok(f"דיון נקבע ל-{dt} (ID: {result['id']})")
except Exception as e:
return fail(f"שגיאה בתזמון דיון: {e}")
async def query_info(query_type: str) -> str:
import json
case_info = context.get("case", {})
if query_type == "status":
return ok(json.dumps({
"status": VALID_STATUSES.get(case_info.get("status"), case_info.get("status")),
"court": case_info.get("cCourt"),
"judge": case_info.get("cJudge"),
"nextHearing": case_info.get("cNextHearing"),
}))
if query_type == "open_tasks":
return ok(json.dumps({"tasks": context.get("openTasks", [])}))
if query_type == "contacts":
return ok(json.dumps({"contacts": context.get("contacts", [])}))
if query_type in ("summary", "alerts", "workload", "hearings"):
return ok(json.dumps({"summary": context.get("summary", {})}))
return ok(json.dumps({"case": case_info}))
async def delete_entity(entity_type: str, entityId: str) -> str:
type_labels = {"Task": "משימה", "Meeting": "פגישה", "Call": "שיחה", "Note": "הערה"}
label = type_labels.get(entity_type, entity_type)
try:
if entity_type != "Note":
entity = await crm.get(f"{entity_type}/{entityId}")
name = entity.get("name", entityId)
else:
name = entityId
await crm.delete(f"{entity_type}/{entityId}")
return ok(f'{label} "{name}" נמחקה בהצלחה')
except Exception as e:
return fail(f"שגיאה במחיקת {label}: {e}")
async def save_memory(category: str, content: str, importance: str = "normal") -> str:
if not case_id:
return fail("צריך להיות בתוך תיק כדי לשמור בזיכרון")
try:
await crm.post("Note", {
"type": "Post",
"post": f"🧠 {category}: {content}",
"parentType": "Case",
"parentId": case_id,
})
return ok(f'נשמר בזיכרון: "{content[:60]}"')
except Exception as e:
return fail(f"שגיאה בשמירת זיכרון: {e}")
async def save_rule(name: str, rule: str, scope: str = "global") -> str:
try:
result = await crm.post("AssistantRule", {
"name": name,
"rule": rule,
"scope": scope,
"isActive": True,
})
return ok(f'כלל חדש נשמר: "{name}" (ID: {result["id"]})')
except Exception as e:
return fail(f"שגיאה בשמירת כלל: {e}")
# Register all tools
tools["create_task"] = {
"description": "Create a task in the CRM. Links to case if case context is available.",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Task name"},
"dateEnd": {"type": "string", "description": "Due date — any format (YYYY-MM-DD, DD/MM/YYYY)"},
"priority": {"type": "string", "enum": ["Urgent", "High", "Normal", "Low"], "description": "Priority level"},
"description": {"type": "string", "description": "Task description"},
},
"required": ["name"],
},
"handler": create_task,
}
tools["add_note"] = {
"description": "Add a note/comment to the case stream or as standalone.",
"parameters": {
"type": "object",
"properties": {
"post": {"type": "string", "description": "Note content"},
},
"required": ["post"],
},
"handler": add_note,
}
tools["change_status"] = {
"description": "Change case status. Only works when viewing a specific case.",
"parameters": {
"type": "object",
"properties": {
"status": {"type": "string", "enum": STATUS_KEYS, "description": "New status (English key)"},
},
"required": ["status"],
},
"handler": change_status,
}
tools["create_meeting"] = {
"description": "Schedule a meeting. Links to case if available.",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Meeting name"},
"dateStart": {"type": "string", "description": "Start date — any format (YYYY-MM-DD HH:mm, DD/MM/YYYY)"},
"dateEnd": {"type": "string", "description": "End date"},
"description": {"type": "string", "description": "Meeting description"},
},
"required": ["name", "dateStart"],
},
"handler": create_meeting,
}
tools["create_call"] = {
"description": "Record a phone call. Use when user reports they spoke/talked/called someone (שוחחתי, דיברתי, התקשרתי). Do NOT use create_meeting for phone calls.",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": 'Call subject (e.g. "שיחה עם לקוחה אדל")'},
"dateStart": {"type": "string", "description": "Call date — any format"},
"dateEnd": {"type": "string", "description": "End time"},
"description": {"type": "string", "description": "Call summary / notes"},
"direction": {"type": "string", "enum": ["Outbound", "Inbound"], "description": "Call direction"},
"status": {"type": "string", "enum": ["Planned", "Held", "Not Held"], "description": "Call status"},
},
"required": ["name"],
},
"handler": create_call,
}
tools["schedule_hearing"] = {
"description": "Schedule a court hearing. Updates case next hearing date and creates a meeting.",
"parameters": {
"type": "object",
"properties": {
"dateTime": {"type": "string", "description": "Hearing date — any format (YYYY-MM-DD HH:mm, DD/MM/YYYY)"},
"court": {"type": "string", "description": "Court name"},
"description": {"type": "string", "description": "Hearing description"},
},
"required": ["dateTime"],
},
"handler": schedule_hearing,
}
tools["query_info"] = {
"description": "Query case or office information from context.",
"parameters": {
"type": "object",
"properties": {
"query_type": {
"type": "string",
"enum": ["status", "next_hearing", "open_tasks", "contacts", "general", "summary", "alerts", "workload", "hearings"],
},
},
"required": ["query_type"],
},
"handler": query_info,
}
# Delete tools
for entity_type, tool_name, desc in [
("Task", "delete_task", "Delete a task by ID."),
("Meeting", "delete_meeting", "Delete a meeting by ID."),
("Call", "delete_call", "Delete a call record by ID."),
("Note", "delete_note", "Delete a note/comment by ID."),
]:
et = entity_type
async def _delete(entityId: str, _et=et) -> str:
return await delete_entity(_et, entityId)
tools[tool_name] = {
"description": desc,
"parameters": {
"type": "object",
"properties": {"entityId": {"type": "string", "description": f"The {entity_type.lower()} ID to delete"}},
"required": ["entityId"],
},
"handler": _delete,
}
tools["save_memory"] = {
"description": "Save an important fact, decision, or strategy to the case memory. Use PROACTIVELY when user shares key info.",
"parameters": {
"type": "object",
"properties": {
"category": {"type": "string", "enum": ["key_facts", "strategy", "decisions", "contacts_notes", "timeline", "documents_notes", "billing_notes"]},
"content": {"type": "string", "description": "The information to remember"},
"importance": {"type": "string", "enum": ["low", "normal", "high", "critical"]},
},
"required": ["category", "content"],
},
"handler": save_memory,
}
tools["save_rule"] = {
"description": 'Save a behavioral rule that Shira should follow in all future conversations. Use when user says "from now on always...", "whenever I say X do Y", etc.',
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Short rule name"},
"rule": {"type": "string", "description": "The full rule text"},
"scope": {"type": "string", "enum": ["global", "case", "office"], "description": "Rule scope"},
},
"required": ["name", "rule"],
},
"handler": save_rule,
}
+155
View File
@@ -0,0 +1,155 @@
"""Document tools: list, read, read_multiple, batch_rename, generate_from_template."""
from __future__ import annotations
import json
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from mcp_server.espocrm_client import EspoCrmClient
from mcp_server.tools._helpers import ok, fail
def register_document_tools(
tools: dict,
crm: "EspoCrmClient",
case_id: str | None,
context: dict,
):
"""Register document-related tools."""
async def list_documents() -> str:
docs = context.get("documents", {})
if not docs.get("totalFiles"):
return ok("לא נמצאו מסמכים")
return ok(json.dumps(docs, ensure_ascii=False))
async def read_document(filePath: str) -> str:
try:
result = await crm.post("SmartAssistant/action/readDocument", {"filePath": filePath})
if not result.get("success") or not result.get("text"):
return fail(f'לא ניתן לחלץ טקסט מ-"{result.get("fileName", filePath)}"')
return ok(f'=== {result["fileName"]} ({result.get("charCount", "?")} תווים) ===\n\n{result["text"]}')
except Exception as e:
return fail(f"שגיאה בקריאת מסמך: {e}")
async def read_multiple_documents(filePaths: list[str], maxCharsPerFile: int = 50000) -> str:
try:
result = await crm.post("SmartAssistant/action/readMultipleDocuments", {
"filePaths": filePaths,
"maxCharsPerFile": maxCharsPerFile,
})
lines = []
for doc in result.get("documents", []):
lines.append(f'=== {doc["fileName"]} ({doc.get("charCount", "?")} תווים) ===')
if doc.get("error"):
lines.append(f'שגיאה: {doc["error"]}')
elif not doc.get("text"):
lines.append("לא ניתן לחלץ טקסט")
else:
lines.append(doc["text"])
lines.append("")
return ok("\n".join(lines))
except Exception as e:
return fail(f"שגיאה בקריאת מסמכים: {e}")
async def batch_rename_documents(renames: list[dict]) -> str:
try:
result = await crm.post("SmartAssistant/action/batchRename", {"renames": renames})
lines = [f'שינוי שמות: {result.get("successCount", 0)}/{result.get("totalCount", 0)} הצליחו']
for r in result.get("results", []):
if r.get("success"):
lines.append(f'"{r.get("oldName", "?")}""{r.get("newName", "?")}"')
else:
lines.append(f'"{r.get("oldName", "?")}": {r.get("error", "unknown")}')
return ok("\n".join(lines))
except Exception as e:
return fail(f"שגיאה בשינוי שמות: {e}")
async def generate_from_template(
templateId: str,
documentSubject: str = "",
customPlaceholders: dict | None = None,
) -> str:
if not case_id:
return fail("צריך להיות בתוך תיק כדי לייצר מסמך מתבנית")
try:
result = await crm.post("SmartAssistant/action/generateFromTemplate", {
"templateId": templateId,
"caseId": case_id,
"documentSubject": documentSubject or None,
"customPlaceholders": customPlaceholders or {},
})
return ok(result.get("message", "✅ מסמך נוצר בהצלחה"))
except Exception as e:
return fail(f"שגיאה ביצירת מסמך מתבנית: {e}")
# Register
tools["list_documents"] = {
"description": "List documents in the case folder. Shows folder structure with file names and paths.",
"parameters": {"type": "object", "properties": {}},
"handler": list_documents,
}
tools["read_document"] = {
"description": "Read and extract text content from a document (PDF, DOCX, TXT). Requires the file path from list_documents.",
"parameters": {
"type": "object",
"properties": {
"filePath": {"type": "string", "description": "Full path of the document (from list_documents results)"},
},
"required": ["filePath"],
},
"handler": read_document,
}
tools["read_multiple_documents"] = {
"description": "Read and extract text from multiple documents at once.",
"parameters": {
"type": "object",
"properties": {
"filePaths": {"type": "array", "items": {"type": "string"}, "description": "Array of file paths to read"},
"maxCharsPerFile": {"type": "integer", "description": "Max chars per file (default 50000)"},
},
"required": ["filePaths"],
},
"handler": read_multiple_documents,
}
tools["batch_rename_documents"] = {
"description": "Rename multiple documents at once.",
"parameters": {
"type": "object",
"properties": {
"renames": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sourcePath": {"type": "string", "description": "Current file path"},
"newName": {"type": "string", "description": "New file name (without path)"},
},
"required": ["sourcePath", "newName"],
},
"description": "Array of rename operations",
},
},
"required": ["renames"],
},
"handler": batch_rename_documents,
}
tools["generate_from_template"] = {
"description": "Generate a document from any template. Templates are listed in context under availableTemplates.",
"parameters": {
"type": "object",
"properties": {
"templateId": {"type": "string", "description": "DocumentTemplate ID (from availableTemplates in context)"},
"documentSubject": {"type": "string", "description": "Custom document name"},
"customPlaceholders": {"type": "object", "description": "Extra placeholder values"},
},
"required": ["templateId"],
},
"handler": generate_from_template,
}
+149
View File
@@ -0,0 +1,149 @@
"""Legal Assistance tools: Direct Access initial report generation."""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from mcp_server.espocrm_client import EspoCrmClient
from mcp_server.tools._helpers import normalize_date, ok, fail
def register_legal_tools(
tools: dict,
crm: "EspoCrmClient",
case_id: str | None,
context: dict,
):
"""Register legal assistance tools."""
async def generate_initial_report(**kwargs) -> str:
if not case_id:
return fail("צריך להיות בתוך תיק כדי ליצור דוח גישה ישירה")
try:
# Normalize date fields
for f in ("appointmentDate", "filingDeadline", "firstContactDate", "meetingDate"):
if kwargs.get(f):
normalized = normalize_date(kwargs[f])
kwargs[f] = normalized.split(" ")[0] if normalized else kwargs[f]
# Extract legalAnalysis sub-object from flat args
legal_analysis = {}
legal_keys = [
"q1DocsReviewed", "q1DocsDetail", "q2Reasoned", "q2ReasonDetail",
"q3PanelMatch", "q4PanelComposition", "q5ComplaintsAddressed", "q6ComplaintsDetail",
"q7ClinicalExam", "q8ClinicalGap", "q9MoharaApplied", "q9MoharaDetail",
"q10RehabGap", "q10RehabDetail", "q11ScoreGap", "q11ScoreDetail",
"q12UniqueAspects", "q12UniqueDetail",
]
for k in legal_keys:
if k in kwargs:
legal_analysis[k] = kwargs.pop(k)
if legal_analysis:
kwargs["legalAnalysis"] = legal_analysis
result = await crm.post("SmartAssistant/action/executeTool", {
"tool": "generate_direct_access_report",
"params": kwargs,
"caseId": case_id,
})
# Update case fields if not already set
case_info = context.get("case", {})
updates = {}
if kwargs.get("appointmentDate") and not case_info.get("cAppointmentDate"):
updates["cAppointmentDate"] = kwargs["appointmentDate"]
if kwargs.get("aidType") and not case_info.get("cAidType"):
updates["cAidType"] = kwargs["aidType"]
if kwargs.get("urgencyLevel") and not case_info.get("cUrgencyLevel"):
updates["cUrgencyLevel"] = kwargs["urgencyLevel"]
if kwargs.get("filingDeadline") and not case_info.get("cFilingDeadline"):
updates["cFilingDeadline"] = kwargs["filingDeadline"]
if updates:
await crm.put(f"Case/{case_id}", updates)
return ok(result.get("message", "✅ דוח גישה ישירה נוצר בהצלחה!"))
except Exception as e:
return fail(f"שגיאה ביצירת דוח גישה ישירה: {e}")
courts = [
"בית הדין האזורי לעבודה ירושלים",
"בית הדין האזורי לעבודה תל אביב",
"בית הדין האזורי לעבודה חיפה",
"בית הדין האזורי לעבודה באר שבע",
"בית הדין האזורי לעבודה נצרת",
"בית הדין הארצי לעבודה",
]
subjects = [
"נכות כללית", "נכות מעבודה", "נכות איבה", "ניידות",
"אי כושר", "שירותים מיוחדים", "סיעוד",
"גמלת הבטחת הכנסה", "דמי אבטלה", "אחר",
]
aid_types = [
"הגשת ערעור", "יעוץ והדרכה", "סיוע נוסף ביטוח לאומי",
"סיוע נוסף תחום אחר", "אי מתן סיוע",
]
recommendations = [
"המשך ייצוג", "ייעוץ והדרכה חלף ייצוג", "ויתור הלקוח",
"החלפת ייצוג", "לא נוצר קשר", "לא התקיימה פגישה",
"עדיין לא ניתן להחליט", "נדרשים מסמכים נוספים", "המלצה לסירוב",
]
tools["generate_initial_report"] = {
"description": "Generate Direct Access initial report (דוח דיווח ראשוני — גישה ישירה). Call ONLY after collecting ALL required data through conversation.",
"parameters": {
"type": "object",
"properties": {
"aidType": {"type": "string", "enum": aid_types, "description": "מהות הסיוע"},
"urgencyLevel": {"type": "string", "enum": ["רגיל", "דחוף", "בהול"]},
"filingDeadline": {"type": "string", "description": "מועד אחרון להגשה (YYYY-MM-DD)"},
"appointmentDate": {"type": "string", "description": "מועד קבלת המינוי (YYYY-MM-DD)"},
"firstContactDate": {"type": "string"},
"contactDelayReason": {"type": "string"},
"meetingDate": {"type": "string"},
"meetingDelayReason": {"type": "string"},
"meetingAttendees": {"type": "string"},
"contactStatus": {"type": "string", "enum": ["תקין", "לא תקין"]},
"contactStatusDetail": {"type": "string"},
"proceedingNumber": {"type": "string"},
"courtName": {"type": "string", "enum": courts},
"proceedingSubject": {"type": "string", "enum": subjects},
"claimSummary": {"type": "string"},
"defenseClaims": {"type": "string"},
# Legal analysis (flat)
"q1DocsReviewed": {"type": "boolean"},
"q1DocsDetail": {"type": "string"},
"q2Reasoned": {"type": "boolean"},
"q2ReasonDetail": {"type": "string"},
"q3PanelMatch": {"type": "boolean"},
"q4PanelComposition": {"type": "string"},
"q5ComplaintsAddressed": {"type": "boolean"},
"q6ComplaintsDetail": {"type": "string"},
"q7ClinicalExam": {"type": "boolean"},
"q8ClinicalGap": {"type": "string"},
"q9MoharaApplied": {"type": "boolean"},
"q9MoharaDetail": {"type": "string"},
"q10RehabGap": {"type": "boolean"},
"q10RehabDetail": {"type": "string"},
"q11ScoreGap": {"type": "boolean"},
"q11ScoreDetail": {"type": "string"},
"q12UniqueAspects": {"type": "boolean"},
"q12UniqueDetail": {"type": "string"},
# Recommendation
"recommendationType": {"type": "string", "enum": recommendations},
"recommendationDetail": {"type": "string"},
# Additional rights
"additionalAidNeeded": {"type": "string", "enum": ["לא נחוץ", "נחוץ בתחום ביטוח לאומי", "נחוץ בתחום אחר"]},
"additionalAidNIDetail": {"type": "string"},
"additionalAidOtherDetail": {"type": "string"},
"additionalAidUrgency": {"type": "string", "enum": ["רגיל", "דחוף", "בהול"]},
"additionalAidDeadlines": {"type": "string"},
"willingToRepresent": {"type": "boolean"},
"notes": {"type": "string"},
},
"required": ["aidType", "proceedingSubject", "recommendationType"],
},
"handler": generate_initial_report,
}