mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 12:28:48 +02:00
Agent: add get_daily_spending for day-scoped questions
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:
@@ -30,6 +30,10 @@ Context you can rely on:
|
|||||||
|
|
||||||
How to answer:
|
How to answer:
|
||||||
- ALWAYS call a tool to get data. Do not invent balances, dates or merchants.
|
- 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
|
- Call multiple tools in parallel when the question spans domains
|
||||||
(e.g. net worth + recent transactions).
|
(e.g. net worth + recent transactions).
|
||||||
- Format currency with the appropriate symbol: ₡ for CRC (no decimals), $ for
|
- Format currency with the appropriate symbol: ₡ for CRC (no decimals), $ for
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from app.models.models import (
|
|||||||
from app.services.budget_projection import (
|
from app.services.budget_projection import (
|
||||||
MAX_YEAR,
|
MAX_YEAR,
|
||||||
MIN_YEAR,
|
MIN_YEAR,
|
||||||
|
NOT_INSTALLMENT_ANCHOR,
|
||||||
compute_monthly_projection,
|
compute_monthly_projection,
|
||||||
compute_yearly_projection_with_cumulative,
|
compute_yearly_projection_with_cumulative,
|
||||||
get_cycle_range,
|
get_cycle_range,
|
||||||
@@ -496,6 +497,39 @@ def list_categories() -> list[dict]:
|
|||||||
|
|
||||||
|
|
||||||
# Registered with the agent in agent.py
|
# 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:
|
def get_current_date() -> dict:
|
||||||
"""Today's date in the user's timezone (Costa Rica, UTC-6) and the
|
"""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
|
active credit-card billing cycle. ALWAYS call this before resolving any
|
||||||
@@ -523,6 +557,7 @@ def get_current_date() -> dict:
|
|||||||
|
|
||||||
TOOLS = [
|
TOOLS = [
|
||||||
get_current_date,
|
get_current_date,
|
||||||
|
get_daily_spending,
|
||||||
get_accounts,
|
get_accounts,
|
||||||
get_net_worth,
|
get_net_worth,
|
||||||
get_recent_transactions,
|
get_recent_transactions,
|
||||||
|
|||||||
@@ -183,3 +183,34 @@ def test_get_current_date_is_costa_rica_and_cycle_consistent():
|
|||||||
start, end = out["cycle_range"]
|
start, end = out["cycle_range"]
|
||||||
assert start <= out["date"] < end
|
assert start <= out["date"] < end
|
||||||
assert start.endswith("-18") and end.endswith("-18")
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user