mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 11: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 __future__ import annotations
|
||||||
|
|
||||||
from datetime import date
|
|
||||||
|
|
||||||
from agent_framework import Agent
|
from agent_framework import Agent
|
||||||
from agent_framework.openai import OpenAIChatCompletionClient
|
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
|
following month. When the user says "this month" or "last month" without
|
||||||
qualifiers, assume they mean the calendar month unless they mention
|
qualifiers, assume they mean the calendar month unless they mention
|
||||||
"cycle", "corte", or their credit card.
|
"cycle", "corte", or their credit card.
|
||||||
- Today's date is {today}. Use it when the user says "this month", "last
|
- You do NOT have a reliable clock, and the server runs in UTC while the
|
||||||
month", "last year", etc.
|
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`
|
- Amounts are stored as raw numbers in their native currency (see `currency`
|
||||||
field on transactions/accounts). Tools that return `total_crc` are already
|
field on transactions/accounts). Tools that return `total_crc` are already
|
||||||
converted; tools that return per-transaction amounts are NOT.
|
converted; tools that return per-transaction amounts are NOT.
|
||||||
@@ -69,7 +69,10 @@ def build_agent() -> Agent:
|
|||||||
)
|
)
|
||||||
return Agent(
|
return Agent(
|
||||||
name="wealthysmart",
|
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,
|
client=client,
|
||||||
tools=TOOLS,
|
tools=TOOLS,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ write tool requires an explicit user-confirmation step in the UI.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextvars
|
import contextvars
|
||||||
from datetime import datetime
|
from datetime import datetime, time, timedelta
|
||||||
from typing import Annotated, Optional
|
from typing import Annotated, Optional
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
@@ -40,6 +40,7 @@ from app.services.budget_projection import (
|
|||||||
get_cycle_range,
|
get_cycle_range,
|
||||||
)
|
)
|
||||||
from app.services import exchange_rate as fx
|
from app.services import exchange_rate as fx
|
||||||
|
from app.timeutil import today_cr
|
||||||
from app.services.exchange_rate import (
|
from app.services.exchange_rate import (
|
||||||
get_converted_amount_expr,
|
get_converted_amount_expr,
|
||||||
get_current_rate,
|
get_current_rate,
|
||||||
@@ -140,7 +141,11 @@ def get_recent_transactions(
|
|||||||
q = select(Transaction).where(
|
q = select(Transaction).where(
|
||||||
col(Transaction.transaction_type).notin_(
|
col(Transaction.transaction_type).notin_(
|
||||||
[TransactionType.SALARY, TransactionType.DEPOSITO]
|
[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:
|
if source:
|
||||||
q = q.where(Transaction.source == TransactionSource(source))
|
q = q.where(Transaction.source == TransactionSource(source))
|
||||||
@@ -491,7 +496,33 @@ def list_categories() -> list[dict]:
|
|||||||
|
|
||||||
|
|
||||||
# Registered with the agent in agent.py
|
# 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 = [
|
TOOLS = [
|
||||||
|
get_current_date,
|
||||||
get_accounts,
|
get_accounts,
|
||||||
get_net_worth,
|
get_net_worth,
|
||||||
get_recent_transactions,
|
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:
|
def utcnow() -> datetime:
|
||||||
|
|||||||
@@ -149,3 +149,37 @@ def test_recent_transactions_json_safe(bound_session):
|
|||||||
out = tools.get_recent_transactions()
|
out = tools.get_recent_transactions()
|
||||||
json.dumps(out)
|
json.dumps(out)
|
||||||
assert out[0]["amount"] == 42.42
|
assert out[0]["amount"] == 42.42
|
||||||
|
|
||||||
|
|
||||||
|
def test_recent_transactions_exclude_future_cuotas(bound_session):
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from app.timeutil import today_cr
|
||||||
|
|
||||||
|
today = datetime.combine(today_cr(), datetime.min.time())
|
||||||
|
bound_session.add(
|
||||||
|
Transaction(amount=Decimal("10"), merchant="BILLED", date=today)
|
||||||
|
)
|
||||||
|
bound_session.add(
|
||||||
|
Transaction(
|
||||||
|
amount=Decimal("20"),
|
||||||
|
merchant="FUTURE CUOTA:03/06",
|
||||||
|
date=today + timedelta(days=45),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
bound_session.commit()
|
||||||
|
merchants = [t["merchant"] for t in tools.get_recent_transactions()]
|
||||||
|
assert "BILLED" in merchants
|
||||||
|
assert "FUTURE CUOTA:03/06" not in merchants
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_current_date_is_costa_rica_and_cycle_consistent():
|
||||||
|
from app.timeutil import today_cr
|
||||||
|
|
||||||
|
out = tools.get_current_date()
|
||||||
|
today = today_cr()
|
||||||
|
assert out["date"] == today.isoformat()
|
||||||
|
# The active cycle must contain today: [start, end)
|
||||||
|
start, end = out["cycle_range"]
|
||||||
|
assert start <= out["date"] < end
|
||||||
|
assert start.endswith("-18") and end.endswith("-18")
|
||||||
|
|||||||
Reference in New Issue
Block a user