This repository has been archived on 2026-07-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
shira-hermes/mcp_server/tools/crm_tools.py
T
chaim a990f527a2 fix: save_rule falls back to user profile when AssistantRule 404
The AssistantRule entity may not exist on some CRM instances. When save_rule
gets a 404, it now falls back to saving the rule in the local user profile
(update_user_profile). Also improved update_user_profile description so
Shira is more likely to use it for user preferences.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 20:10:10 +00:00

361 lines
15 KiB
Python

"""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 CRM first, fall back to local user profile if entity doesn't exist
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:
if "404" in str(e) and user_id:
# AssistantRule entity not available — save to user profile instead
from api.services.memory import update_user_profile
entry = f"כלל [{scope}] {name}: {rule}"
return await update_user_profile(user_id, "add", entry)
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,
}