Agent: add get_daily_spending for day-scoped questions
All checks were successful
Deploy to VPS / test (push) Successful in 1m32s
Deploy to VPS / deploy (push) Successful in 14s

Prod showed the failure mode after the date fix: get_current_date
returned the right day, but the model answered "¿cuánto he gastado
hoy?" from cycle-level aggregates (the only spend tools it had) and
concluded ₡0. Give it a day-grained tool — per-day CRC totals mirroring
/analytics/daily-spending (COMPRA only, no installment anchors, no
future cuotas) — and a prompt rule to match tool to time grain.
Verified: fresh thread now calls get_current_date →
get_daily_spending(2026-07-04, 2026-07-05) and answers correctly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-07-04 18:36:48 -06:00
parent 334d41bd9a
commit 965e30a2d2
3 changed files with 70 additions and 0 deletions

View File

@@ -30,6 +30,10 @@ Context you can rely on:
How to answer:
- ALWAYS call a tool to get data. Do not invent balances, dates or merchants.
- Match the tool to the question's time grain: day-scoped questions ("hoy",
"ayer", "el martes", a specific date) need get_daily_spending with that
day's bounds — cycle or category summaries have NO per-day data, so never
answer a day question from them.
- Call multiple tools in parallel when the question spans domains
(e.g. net worth + recent transactions).
- Format currency with the appropriate symbol: ₡ for CRC (no decimals), $ for

View File

@@ -35,6 +35,7 @@ from app.models.models import (
from app.services.budget_projection import (
MAX_YEAR,
MIN_YEAR,
NOT_INSTALLMENT_ANCHOR,
compute_monthly_projection,
compute_yearly_projection_with_cumulative,
get_cycle_range,
@@ -496,6 +497,39 @@ def list_categories() -> list[dict]:
# Registered with the agent in agent.py
def get_daily_spending(
start_date: Annotated[str, Field(description="ISO date lower bound, inclusive")],
end_date: Annotated[str, Field(description="ISO date upper bound, exclusive")],
) -> list[dict]:
"""Spending per day in CRC (converted), COMPRA only, excluding future
Tasa Cero cuotas and installment anchors. THE tool for day-scoped
questions: 'cuánto gasté hoy / ayer / el martes'. Days with no spending
are simply absent from the result."""
session = _s()
amount_crc = get_converted_amount_expr(session)
rows = session.exec(
select(
func.date(Transaction.date).label("day"),
func.coalesce(func.sum(amount_crc), 0),
func.count(),
)
.where(
Transaction.transaction_type == TransactionType.COMPRA,
NOT_INSTALLMENT_ANCHOR,
Transaction.date >= datetime.fromisoformat(start_date),
Transaction.date < datetime.fromisoformat(end_date),
# Future-dated Tasa Cero cuotas are not money already spent.
Transaction.date < datetime.combine(today_cr() + timedelta(days=1), time.min),
)
.group_by(func.date(Transaction.date))
.order_by(func.date(Transaction.date))
).all()
return [
{"date": str(day), "total_crc": float(total), "count": count}
for day, total, count in rows
]
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
@@ -523,6 +557,7 @@ def get_current_date() -> dict:
TOOLS = [
get_current_date,
get_daily_spending,
get_accounts,
get_net_worth,
get_recent_transactions,

View File

@@ -183,3 +183,34 @@ def test_get_current_date_is_costa_rica_and_cycle_consistent():
start, end = out["cycle_range"]
assert start <= out["date"] < end
assert start.endswith("-18") and end.endswith("-18")
def test_daily_spending_day_scoped(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("100"), merchant="TODAY-A", date=today.replace(hour=9))
)
bound_session.add(
Transaction(amount=Decimal("50"), merchant="TODAY-B", date=today.replace(hour=15))
)
bound_session.add(
Transaction(
amount=Decimal("999"),
merchant="FUTURE CUOTA",
date=today + timedelta(days=40),
)
)
bound_session.commit()
out = tools.get_daily_spending(
start_date=today.date().isoformat(),
end_date=(today.date() + timedelta(days=60)).isoformat(),
)
assert len(out) == 1
assert out[0]["date"] == today.date().isoformat()
assert out[0]["total_crc"] == 150.0
assert out[0]["count"] == 2