mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 10:28:47 +02:00
Agent: fix date handling — Costa Rica timezone and future cuotas
Two bugs from prod (server clock is UTC, user is UTC-6): - get_recent_transactions returned future-dated Tasa Cero cuotas; it now excludes anything dated after the user's today, same rule as /transactions/recent. - "¿Cuánto he gastado hoy?" answered for the wrong day: the prompt baked date.today() in at boot — frozen from startup AND in server-UTC (8 pm in Costa Rica is already tomorrow in UTC). The prompt no longer carries a date; a new get_current_date tool returns today in CR time plus the active billing cycle, and the prompt directs the model to call it for any relative date reference. today_cr() lives in timeutil (CR has no DST, fixed UTC-6). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
from agent_framework import Agent
|
||||
from agent_framework.openai import OpenAIChatCompletionClient
|
||||
|
||||
@@ -22,8 +20,10 @@ Context you can rely on:
|
||||
following month. When the user says "this month" or "last month" without
|
||||
qualifiers, assume they mean the calendar month unless they mention
|
||||
"cycle", "corte", or their credit card.
|
||||
- Today's date is {today}. Use it when the user says "this month", "last
|
||||
month", "last year", etc.
|
||||
- You do NOT have a reliable clock, and the server runs in UTC while the
|
||||
user lives in Costa Rica (UTC-6). Whenever the question involves a
|
||||
relative date — "hoy", "ayer", "este mes", "este ciclo", "el año
|
||||
pasado" — call get_current_date FIRST and derive ranges from its answer.
|
||||
- Amounts are stored as raw numbers in their native currency (see `currency`
|
||||
field on transactions/accounts). Tools that return `total_crc` are already
|
||||
converted; tools that return per-transaction amounts are NOT.
|
||||
@@ -69,7 +69,10 @@ def build_agent() -> Agent:
|
||||
)
|
||||
return Agent(
|
||||
name="wealthysmart",
|
||||
instructions=SYSTEM_PROMPT.replace("{today}", date.today().isoformat()),
|
||||
# No date baked in: build_agent() runs once at startup, so anything
|
||||
# substituted here freezes at boot (and in server-UTC). The model
|
||||
# gets "today" from the get_current_date tool instead.
|
||||
instructions=SYSTEM_PROMPT,
|
||||
client=client,
|
||||
tools=TOOLS,
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ write tool requires an explicit user-confirmation step in the UI.
|
||||
from __future__ import annotations
|
||||
|
||||
import contextvars
|
||||
from datetime import datetime
|
||||
from datetime import datetime, time, timedelta
|
||||
from typing import Annotated, Optional
|
||||
|
||||
from pydantic import Field
|
||||
@@ -40,6 +40,7 @@ from app.services.budget_projection import (
|
||||
get_cycle_range,
|
||||
)
|
||||
from app.services import exchange_rate as fx
|
||||
from app.timeutil import today_cr
|
||||
from app.services.exchange_rate import (
|
||||
get_converted_amount_expr,
|
||||
get_current_rate,
|
||||
@@ -140,7 +141,11 @@ def get_recent_transactions(
|
||||
q = select(Transaction).where(
|
||||
col(Transaction.transaction_type).notin_(
|
||||
[TransactionType.SALARY, TransactionType.DEPOSITO]
|
||||
)
|
||||
),
|
||||
# Tasa Cero generates future-dated cuotas; "recent" means already
|
||||
# billed (same rule as /transactions/recent). Bound is end of the
|
||||
# user's today, Costa Rica time.
|
||||
Transaction.date < datetime.combine(today_cr() + timedelta(days=1), time.min),
|
||||
)
|
||||
if source:
|
||||
q = q.where(Transaction.source == TransactionSource(source))
|
||||
@@ -491,7 +496,33 @@ def list_categories() -> list[dict]:
|
||||
|
||||
|
||||
# Registered with the agent in agent.py
|
||||
def get_current_date() -> dict:
|
||||
"""Today's date in the user's timezone (Costa Rica, UTC-6) and the
|
||||
active credit-card billing cycle. ALWAYS call this before resolving any
|
||||
relative date reference — 'hoy', 'ayer', 'este mes', 'este ciclo',
|
||||
'el ciclo pasado' — the server clock and your own assumptions about
|
||||
today are unreliable."""
|
||||
today = today_cr()
|
||||
# get_cycle_range(y, m) is the cycle STARTING on the 18th of m; before
|
||||
# the 18th we are still in the cycle that started last month.
|
||||
if today.day >= 18:
|
||||
cycle_year, cycle_month = today.year, today.month
|
||||
elif today.month == 1:
|
||||
cycle_year, cycle_month = today.year - 1, 12
|
||||
else:
|
||||
cycle_year, cycle_month = today.year, today.month - 1
|
||||
start, end = get_cycle_range(cycle_year, cycle_month)
|
||||
return {
|
||||
"date": today.isoformat(),
|
||||
"weekday": today.strftime("%A"),
|
||||
"cycle_year": cycle_year,
|
||||
"cycle_month": cycle_month,
|
||||
"cycle_range": [start.date().isoformat(), end.date().isoformat()],
|
||||
}
|
||||
|
||||
|
||||
TOOLS = [
|
||||
get_current_date,
|
||||
get_accounts,
|
||||
get_net_worth,
|
||||
get_recent_transactions,
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
from datetime import datetime, timezone
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
|
||||
# Costa Rica has no DST; a fixed offset is exact year-round.
|
||||
CR_TZ = timezone(timedelta(hours=-6))
|
||||
|
||||
|
||||
def today_cr() -> date:
|
||||
"""Current date in Costa Rica (UTC-6).
|
||||
|
||||
The server clock is UTC — at 6 pm in Costa Rica it is already "tomorrow"
|
||||
in UTC. Anything user-facing that means "today" (agent answers, date
|
||||
defaults) must come from here, never date.today().
|
||||
"""
|
||||
return datetime.now(CR_TZ).date()
|
||||
|
||||
|
||||
def utcnow() -> datetime:
|
||||
|
||||
Reference in New Issue
Block a user