diff --git a/backend/app/agent/agent.py b/backend/app/agent/agent.py index d0e27c6..fd13cff 100644 --- a/backend/app/agent/agent.py +++ b/backend/app/agent/agent.py @@ -35,6 +35,9 @@ How to answer: "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. +- For a question about a specific salary deposit or its amount (including + "cuánto recibí hoy"), call get_salary_deposits with the relevant date range; + get_salary_summary is aggregate-only and cannot answer it. - Answer like an analyst, not a calculator. An aggregate number on its own is not an answer — show what it is made of, right away, without waiting to be asked: diff --git a/backend/app/agent/tools.py b/backend/app/agent/tools.py index c617fc5..2bc8c31 100644 --- a/backend/app/agent/tools.py +++ b/backend/app/agent/tools.py @@ -48,6 +48,7 @@ from app.services.exchange_rate import ( ) _session_ctx: contextvars.ContextVar[Session] = contextvars.ContextVar("agent_session") +SALARY_TRANSACTION_TYPES = (TransactionType.SALARY, TransactionType.DEPOSITO) def set_session(session: Session) -> contextvars.Token: @@ -339,7 +340,7 @@ def get_salary_summary() -> dict: func.count(), func.coalesce(func.sum(amount_crc), 0), func.max(Transaction.date), - ).where(Transaction.transaction_type == TransactionType.SALARY) + ).where(col(Transaction.transaction_type).in_(SALARY_TRANSACTION_TYPES)) ).first() count = row[0] if row else 0 total = float(row[1]) if row else 0.0 @@ -347,6 +348,44 @@ def get_salary_summary() -> dict: return {"count": count, "total_crc": total, "latest_date": latest} +def get_salary_deposits( + limit: Annotated[int, Field(ge=1, le=100, description="How many deposits to return")] = 20, + start_date: Annotated[ + Optional[str], Field(description="ISO date lower bound, inclusive") + ] = None, + end_date: Annotated[ + Optional[str], Field(description="ISO date upper bound, exclusive") + ] = None, +) -> list[dict]: + """Individual salary deposits, newest first. Use this for questions about + a specific salary or a day/range (for example, "cuánto recibí hoy"). + Amounts remain in their recorded currency; use the currency field when + presenting them.""" + q = select(Transaction).where( + col(Transaction.transaction_type).in_(SALARY_TRANSACTION_TYPES) + ) + if start_date: + q = q.where(Transaction.date >= datetime.fromisoformat(start_date)) + if end_date: + q = q.where(Transaction.date < datetime.fromisoformat(end_date)) + q = q.order_by(col(Transaction.date).desc(), col(Transaction.id).desc()).limit(limit) + return [ + { + "id": t.id, + "date": t.date.isoformat(), + "amount": float(t.amount), + "currency": t.currency.value, + "merchant": t.merchant, + "source": t.source.value, + "bank": t.bank.value, + "transaction_type": t.transaction_type.value, + "reference": t.reference, + "notes": t.notes, + } + for t in _s().exec(q).all() + ] + + def get_municipal_receipts( limit: Annotated[int, Field(ge=1, le=50)] = 12, offset: Annotated[int, Field(ge=0, description="Skip the N most recent")] = 0, @@ -567,6 +606,7 @@ TOOLS = [ list_recurring_items, get_pension_snapshots, get_salary_summary, + get_salary_deposits, get_municipal_receipts, get_analytics_by_category, get_monthly_trend, diff --git a/backend/tests/test_agent_tools.py b/backend/tests/test_agent_tools.py index 62cf139..e087af1 100644 --- a/backend/tests/test_agent_tools.py +++ b/backend/tests/test_agent_tools.py @@ -17,6 +17,7 @@ from app.models.models import ( MunicipalReceipt, PensionSnapshot, Transaction, + TransactionType, WaterMeterReading, ) @@ -173,6 +174,52 @@ def test_recent_transactions_exclude_future_cuotas(bound_session): assert "FUTURE CUOTA:03/06" not in merchants +def test_salary_deposits_returns_individual_filtered_rows(bound_session): + bound_session.add_all( + [ + Transaction( + amount=Decimal("1500"), + merchant="SALARY JULY", + date=datetime(2026, 7, 14, 9), + transaction_type=TransactionType.SALARY, + reference="PAY-2026-07", + ), + Transaction( + amount=Decimal("1400"), + merchant="SALARY JUNE", + date=datetime(2026, 6, 14, 9), + transaction_type=TransactionType.SALARY, + ), + Transaction( + amount=Decimal("999"), + merchant="PURCHASE", + date=datetime(2026, 7, 14, 10), + ), + ] + ) + bound_session.commit() + + out = tools.get_salary_deposits( + start_date="2026-07-14", end_date="2026-07-15" + ) + + json.dumps(out) + assert out == [ + { + "id": out[0]["id"], + "date": "2026-07-14T09:00:00", + "amount": 1500.0, + "currency": "CRC", + "merchant": "SALARY JULY", + "source": "CREDIT_CARD", + "bank": "BAC", + "transaction_type": "SALARY", + "reference": "PAY-2026-07", + "notes": None, + } + ] + + def test_get_current_date_is_costa_rica_and_cycle_consistent(): from app.timeutil import today_cr