diff --git a/app/agents/task_agent.py b/app/agents/task_agent.py index 8688765..8ce4dbe 100644 --- a/app/agents/task_agent.py +++ b/app/agents/task_agent.py @@ -141,11 +141,21 @@ async def delete_task(task_id: str) -> str: @tool -async def list_tasks_due_today() -> str: - """List all tasks whose due date falls on today's date.""" - now = datetime.now(tz=timezone.utc) - start_ms = int(datetime(now.year, now.month, now.day, tzinfo=timezone.utc).timestamp() * 1000) - end_ms = start_ms + 86_400_000 - 1 # last ms of today +async def list_tasks_due_today(user_timezone: str = "UTC") -> str: + """List all tasks whose due date falls on today's date. + + user_timezone: IANA timezone name (e.g. 'Europe/Rome', 'America/New_York'). + Always pass the user's timezone so 'today' is computed in their local time. + """ + try: + from zoneinfo import ZoneInfo + tz = ZoneInfo(user_timezone or "UTC") + except Exception: + tz = timezone.utc + now_local = datetime.now(tz=tz) + start_dt = datetime(now_local.year, now_local.month, now_local.day, tzinfo=tz) + start_ms = int(start_dt.timestamp() * 1000) + end_ms = start_ms + 86_400_000 - 1 result = await execute_on_client( action="select", table="tasks", diff --git a/app/agents/timeline_agent.py b/app/agents/timeline_agent.py index c6c4e7e..2939972 100644 --- a/app/agents/timeline_agent.py +++ b/app/agents/timeline_agent.py @@ -94,10 +94,20 @@ async def delete_timeline(timeline_id: str) -> str: @tool -async def list_timelines_today() -> str: - """List all timeline events (milestones) whose date falls on today (UTC).""" - now = datetime.now(tz=timezone.utc) - start_ms = int(datetime(now.year, now.month, now.day, tzinfo=timezone.utc).timestamp() * 1000) +async def list_timelines_today(user_timezone: str = "UTC") -> str: + """List all timeline events (milestones) whose date falls on today. + + user_timezone: IANA timezone name (e.g. 'Europe/Rome', 'America/New_York'). + Always pass the user's timezone so 'today' is computed in their local time. + """ + try: + from zoneinfo import ZoneInfo + tz = ZoneInfo(user_timezone or "UTC") + except Exception: + tz = timezone.utc + now_local = datetime.now(tz=tz) + start_dt = datetime(now_local.year, now_local.month, now_local.day, tzinfo=tz) + start_ms = int(start_dt.timestamp() * 1000) end_ms = start_ms + 86_400_000 - 1 result = await execute_on_client( action="select", diff --git a/app/api/routes/device_ws.py b/app/api/routes/device_ws.py index 1c8abb5..47f8511 100644 --- a/app/api/routes/device_ws.py +++ b/app/api/routes/device_ws.py @@ -226,6 +226,7 @@ async def _handle_home_request( context: dict = { "conversation_history": frame.get("conversation_history", []), "_debug": {"request_id": request_id, "session_id": session_id, "user_id": user_id}, + "format_prefs": frame.get("format_prefs"), **memory_context, } @@ -295,6 +296,7 @@ async def _handle_floating_request( context: dict = { "scope": scope, "_debug": {"request_id": request_id, "session_id": session_id, "user_id": user_id}, + "format_prefs": frame.get("format_prefs"), **memory_context, } @@ -380,6 +382,7 @@ async def _handle_brief_request( context: dict = { "_debug": {"request_id": request_id, "session_id": session_id, "user_id": user_id}, + "format_prefs": frame.get("format_prefs"), **memory_context, } diff --git a/app/core/deep_agent.py b/app/core/deep_agent.py index 5f528f1..a885ea1 100644 --- a/app/core/deep_agent.py +++ b/app/core/deep_agent.py @@ -55,6 +55,42 @@ def _language_instruction(context: dict[str, Any]) -> str: f"All your output text must be written in {lang}." ) +def _datetime_context_injection(context: dict[str, Any]) -> str: + """Build a system-prompt paragraph with current timestamp, user timezone, and format prefs.""" + fp = context.get("format_prefs") + if not fp or not isinstance(fp, dict): + return "" + try: + from zoneinfo import ZoneInfo + from datetime import datetime as _dt, timezone as _utc + tz_name: str = str(fp.get("timezone") or "UTC") + now_iso: str = str(fp.get("now_iso") or "") + date_fmt: str = str(fp.get("date_format") or "dd/MM/yyyy") + time_fmt: str = str(fp.get("time_format") or "24h") + + if now_iso: + now_utc = _dt.fromisoformat(now_iso.replace("Z", "+00:00")) + else: + now_utc = _dt.now(_utc.utc) + + tz = ZoneInfo(tz_name) + now_local = now_utc.astimezone(tz) + today_local = now_local.strftime("%Y-%m-%d") + weekday_local = now_local.strftime("%A") + + return ( + f"\n\nCurrent instant: {now_utc.isoformat()}. " + f"User local date: {today_local} ({weekday_local}). " + f"Timezone: {tz_name}. " + f"Display preference: dateFormat={date_fmt}, timeFormat={time_fmt}. " + f"When calling tools with date fields, always pass integer Unix milliseconds (ms since epoch, UTC). " + f"When calling list_tasks_due_today or list_timelines_today, always pass user_timezone=\"{tz_name}\". " + f"When presenting dates to the user in chat, format using the display preference above." + ) + except Exception: + return "" + + def _proactive_hints_injection(context: dict[str, Any]) -> str: """Return a system-prompt paragraph listing proactive behavioral hints. @@ -938,6 +974,7 @@ async def run_home(user_id: str, message: str, context: dict[str, Any]) -> str: ) system_prompt += _relational_memory_injection(context) system_prompt += _proactive_hints_injection(context) + system_prompt += _datetime_context_injection(context) system_prompt += _language_instruction(context) response = await _run_single_agent( user_id=user_id, @@ -958,6 +995,7 @@ async def run_floating(user_id: str, message: str, context: dict[str, Any]) -> t ) system_prompt += _relational_memory_injection(context) system_prompt += _proactive_hints_injection(context) + system_prompt += _datetime_context_injection(context) system_prompt += _language_instruction(context) response = await _run_single_agent( user_id=user_id, @@ -984,6 +1022,7 @@ async def run_home_stream( ) system_prompt += _relational_memory_injection(context) system_prompt += _proactive_hints_injection(context) + system_prompt += _datetime_context_injection(context) system_prompt += _language_instruction(context) text_chunks: list[str] = [] async for event in _run_single_agent_stream( @@ -1019,6 +1058,7 @@ async def run_floating_stream( ) system_prompt += _relational_memory_injection(context) system_prompt += _proactive_hints_injection(context) + system_prompt += _datetime_context_injection(context) system_prompt += _language_instruction(context) sanitizer = _FloatingStreamSanitizer() emitted_sanitized = False diff --git a/app/schemas.py b/app/schemas.py index 5661c04..4c33386 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -142,6 +142,16 @@ class WsDeviceHello(BaseModel): # ── WebSocket v3 Frame Models ───────────────────────────────────────── +class FormatPrefsModel(BaseModel): + """User display preferences sent by Electron on each request.""" + + timezone: str = "UTC" + date_format: str = "dd/MM/yyyy" + time_format: str = "24h" + locale: str = "en-US" + now_iso: str = "" + + class WsFloatingScope(BaseModel): """Scope for a floating request — narrows the agent to a specific entity.""" @@ -155,6 +165,7 @@ class WsHomeRequest(BaseModel): type: Literal[WsFrameType.home_request] = WsFrameType.home_request message: str conversation_history: list[dict[str, Any]] = Field(default_factory=list) + format_prefs: FormatPrefsModel | None = None class WsFloatingRequest(BaseModel): @@ -163,6 +174,7 @@ class WsFloatingRequest(BaseModel): type: Literal[WsFrameType.floating_request] = WsFrameType.floating_request message: str scope: WsFloatingScope + format_prefs: FormatPrefsModel | None = None class WsBriefRequest(BaseModel): @@ -173,6 +185,7 @@ class WsBriefRequest(BaseModel): session_id: str | None = None mode: Literal["home", "project"] project_id: str | None = None + format_prefs: FormatPrefsModel | None = None class WsStreamStart(BaseModel):