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:
@@ -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.")
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user