fix: convert UTC dates to Israel timezone in display and input

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 04:09:43 +00:00
parent 40877d83d2
commit a0c2211399
2 changed files with 49 additions and 18 deletions
+22 -7
View File
@@ -22,6 +22,21 @@ VALID_STATUSES = {
ISR_TZ = ZoneInfo("Asia/Jerusalem") 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 = (
"TOOL RULES (CRITICAL — you MUST follow these):\n" "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" "- 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'Type: {case_info.get("type", "")}\n',
f'Court: {case_info.get("cCourt", "")} | Case#: {case_info.get("cCourtCaseNumber", "")}\n', f'Court: {case_info.get("cCourt", "")} | Case#: {case_info.get("cCourtCaseNumber", "")}\n',
f'Judge: {case_info.get("cJudge", "")}\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'Assigned: {case_info.get("assignedUserName", "")}\n',
f'Account: {case_info.get("accountName", "")}\n\n', f'Account: {case_info.get("accountName", "")}\n\n',
"=== CONTACTS ===\n", "=== CONTACTS ===\n",
"\n".join(f'- {c.get("name", "")} ({c.get("role", "N/A")}) {c.get("phoneNumber", "")}' for c in contacts) or "None", "\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\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\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\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\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\n=== TEMPLATES ===\n",
"\n".join(f'- {t.get("name", "")}' for t in templates) or "None", "\n".join(f'- {t.get("name", "")}' for t in templates) or "None",
docs_section, docs_section,
@@ -216,7 +231,7 @@ def build_office_prompt(context: dict, user_profile: str = "", available_skills:
if hearings: if hearings:
parts.append("\n".join( 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 "") + (f' ({h["court"]})' if h.get("court") else "")
for h in hearings 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'Status: {dc.get("status", "")}\n')
parts.append(f'Court: {dc.get("court", "")}\n') parts.append(f'Court: {dc.get("court", "")}\n')
parts.append(f'Judge: {dc.get("judge", "")}\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'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(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.") parts.append("NOTE: This case was identified from the user message. Use its context for any actions.")
+27 -11
View File
@@ -3,10 +3,19 @@
from __future__ import annotations from __future__ import annotations
import re 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: 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: if not date_str:
return None 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) m = re.match(r"^(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{4})(?:\s+(.+))?$", date_str)
if m: if m:
base = f"{m.group(3)}-{m.group(2).zfill(2)}-{m.group(1).zfill(2)}" 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 # Convert Israel local time → UTC
if re.match(r"^\d{4}-\d{2}-\d{2}$", date_str): try:
return f"{date_str} {default_time}" dt = datetime.strptime(local_str, "%Y-%m-%d %H:%M:%S")
local_dt = dt.replace(tzinfo=ISR_TZ)
# YYYY-MM-DD HH:mm utc_dt = local_dt.astimezone(UTC_TZ)
if re.match(r"^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}$", date_str): return utc_dt.strftime("%Y-%m-%d %H:%M:%S")
return f"{date_str}:00" except ValueError:
return local_str
return date_str
def ok(message: str) -> str: def ok(message: str) -> str: