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