From a0c2211399e5cff26e896989d159fc01eb7bd965 Mon Sep 17 00:00:00 2001 From: Chaim Date: Tue, 14 Apr 2026 04:09:43 +0000 Subject: [PATCH] fix: convert UTC dates to Israel timezone in display and input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EspoCRM stores dates in UTC. Shira was displaying them raw (e.g., 08:00 UTC instead of 11:00 Israel time) and storing user-provided local times as UTC. - Add _to_local() in prompt_builder to convert UTC→Asia/Jerusalem for all displayed dates (hearings, meetings, calls, tasks, notes) - Update normalize_date() in _helpers to convert Israel local→UTC before sending to EspoCRM Co-Authored-By: Claude Opus 4.6 (1M context) --- api/services/prompt_builder.py | 29 +++++++++++++++++++------- mcp_server/tools/_helpers.py | 38 ++++++++++++++++++++++++---------- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/api/services/prompt_builder.py b/api/services/prompt_builder.py index bf6fa76..0d05e1f 100644 --- a/api/services/prompt_builder.py +++ b/api/services/prompt_builder.py @@ -22,6 +22,21 @@ VALID_STATUSES = { ISR_TZ = ZoneInfo("Asia/Jerusalem") + +def _to_local(dt_str: str | None, fallback: str = "") -> str: + """Convert a UTC datetime string from EspoCRM to Israel local time (DD/MM/YYYY HH:MM).""" + if not dt_str: + return fallback + try: + s = dt_str.strip().replace("Z", "+00:00") + dt = datetime.fromisoformat(s) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=ZoneInfo("UTC")) + return dt.astimezone(ISR_TZ).strftime("%d/%m/%Y %H:%M") + except (ValueError, TypeError): + return dt_str + + TOOL_RULES = ( "TOOL RULES (CRITICAL — you MUST follow these):\n" "- When user asks for an action (create task, meeting, note, etc.) — USE the tool. Do not describe what you would do — DO IT.\n" @@ -129,19 +144,19 @@ def build_case_prompt(context: dict, user_profile: str = "", case_memory_notes: f'Type: {case_info.get("type", "")}\n', f'Court: {case_info.get("cCourt", "")} | Case#: {case_info.get("cCourtCaseNumber", "")}\n', f'Judge: {case_info.get("cJudge", "")}\n', - f'Next Hearing: {case_info.get("cNextHearing", "Not set")}\n', + f'Next Hearing: {_to_local(case_info.get("cNextHearing"), "Not set")}\n', f'Assigned: {case_info.get("assignedUserName", "")}\n', f'Account: {case_info.get("accountName", "")}\n\n', "=== CONTACTS ===\n", "\n".join(f'- {c.get("name", "")} ({c.get("role", "N/A")}) {c.get("phoneNumber", "")}' for c in contacts) or "None", "\n\n=== TASKS ===\n", - "\n".join(f'- {t.get("name", "")} (due: {t.get("dateEnd", "N/A")})' for t in open_tasks) or "None", + "\n".join(f'- {t.get("name", "")} (due: {_to_local(t.get("dateEnd"), "N/A")})' for t in open_tasks) or "None", "\n\n=== MEETINGS ===\n", - "\n".join(f'- {m.get("name", "")} ({m.get("dateStart", "")})' for m in upcoming_meetings) or "None", + "\n".join(f'- {m.get("name", "")} ({_to_local(m.get("dateStart"))})' for m in upcoming_meetings) or "None", "\n\n=== RECENT CALLS ===\n", - "\n".join(f'- {c.get("name", "")} ({c.get("dateStart", "")}) [{c.get("direction", "")}, {c.get("status", "")}]' for c in recent_calls) or "None", + "\n".join(f'- {c.get("name", "")} ({_to_local(c.get("dateStart"))}) [{c.get("direction", "")}, {c.get("status", "")}]' for c in recent_calls) or "None", "\n\n=== RECENT NOTES ===\n", - "\n".join(f'- [{n.get("createdAt", "")}] {(n.get("post", "") or "")[:100]}' for n in recent_notes) or "None", + "\n".join(f'- [{_to_local(n.get("createdAt"))}] {(n.get("post", "") or "")[:100]}' for n in recent_notes) or "None", "\n\n=== TEMPLATES ===\n", "\n".join(f'- {t.get("name", "")}' for t in templates) or "None", docs_section, @@ -216,7 +231,7 @@ def build_office_prompt(context: dict, user_profile: str = "", available_skills: if hearings: parts.append("\n".join( - f'{h.get("caseName", "")} - {h.get("hearingDate", "")}' + f'{h.get("caseName", "")} - {_to_local(h.get("hearingDate"))}' + (f' ({h["court"]})' if h.get("court") else "") for h in hearings )) @@ -229,7 +244,7 @@ def build_office_prompt(context: dict, user_profile: str = "", available_skills: parts.append(f'Status: {dc.get("status", "")}\n') parts.append(f'Court: {dc.get("court", "")}\n') parts.append(f'Judge: {dc.get("judge", "")}\n') - parts.append(f'Next Hearing: {dc.get("nextHearing", "Not set")}\n') + parts.append(f'Next Hearing: {_to_local(dc.get("nextHearing"), "Not set")}\n') parts.append(f'Contacts: {", ".join(c.get("name", "") for c in drill_down.get("contacts", []))}\n') parts.append(f'Open Tasks: {"; ".join(t.get("name", "") for t in drill_down.get("openTasks", []))}\n') parts.append("NOTE: This case was identified from the user message. Use its context for any actions.") diff --git a/mcp_server/tools/_helpers.py b/mcp_server/tools/_helpers.py index e0d96df..f695c8e 100644 --- a/mcp_server/tools/_helpers.py +++ b/mcp_server/tools/_helpers.py @@ -3,10 +3,19 @@ from __future__ import annotations import re +from datetime import datetime +from zoneinfo import ZoneInfo + +ISR_TZ = ZoneInfo("Asia/Jerusalem") +UTC_TZ = ZoneInfo("UTC") 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.""" + """Normalize various date formats to YYYY-MM-DD HH:mm:ss in UTC. + + User input is assumed to be in Israel local time (Asia/Jerusalem). + Output is UTC for EspoCRM storage. + """ if not date_str: return None @@ -14,17 +23,24 @@ def normalize_date(date_str: str | None, default_time: str = "08:00:00") -> str 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}" + local_str = f"{base} {m.group(4) or default_time}" + elif re.match(r"^\d{4}-\d{2}-\d{2}$", date_str): + local_str = f"{date_str} {default_time}" + elif re.match(r"^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}$", date_str): + local_str = f"{date_str}:00" + elif re.match(r"^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}$", date_str): + local_str = date_str + else: + return date_str - # 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 + # Convert Israel local time → UTC + try: + dt = datetime.strptime(local_str, "%Y-%m-%d %H:%M:%S") + local_dt = dt.replace(tzinfo=ISR_TZ) + utc_dt = local_dt.astimezone(UTC_TZ) + return utc_dt.strftime("%Y-%m-%d %H:%M:%S") + except ValueError: + return local_str def ok(message: str) -> str: