finance: reconcile-receipts v2 — 7-verb toolbox + skill (closes unimatrix27/ideas#31) - #7
finance: reconcile-receipts v2 — 7-verb toolbox + skill (closes unimatrix27/ideas#31)#7unimatrix27 wants to merge 4 commits into
Conversation
…trix27/ideas#31) Adds a parallel, simplified replacement for the v1 reconcile pipeline (PRs #1–#6). v2 operates against the 3-state TX model from NousResearch#31: {ignored, belege_sent, open} — no buckets, no proposals, no candidate/match tables. The LLM reads mail bodies + PDF text directly and decides; there are no vendor-specific parsers. The two skills coexist: * v1 lives under skills/finance/reconcile-receipts/ + finance/tools/ (PRs #5, #6) and uses bank.receipt_candidates / receipt_matches / receipt_status_v. * v2 lives under skills/finance/reconcile-receipts-v2/ + finance/reconcile_v2/ and only touches bank.transactions, bank.belege_sent, bank.agent_anomalies, bank.agent_reconcile_runs. No existing tables are dropped, altered, or migrated. Cleanup of the obsolete v1 tables is a separate follow-up PR, per NousResearch#31. Contents: * finance/reconcile_v2/ — verbs, adapter (in-memory + Postgres), graph inbox client + mail sender, notifier, ignore_rules parser, CLI. Exactly 7 verbs: list_open_txs, get_tx_context, search_inbox, send_beleg, mark_ignored, flag_anomaly, finalize_run * skills/finance/reconcile-receipts-v2/SKILL.md — written around the 3-state model; explicitly forbids inventing new tools. * skills/finance/reconcile-receipts-v2/ignore_rules.md — empty template with a self-documenting header (iban / counterparty / verwendungszweck patterns). LLM may APPEND; never rewrite/delete. * pyproject.toml — register the new finance.* package and the finance-reconcile-v2 console_script. Tests: 28 offline tests pass against InMemoryAdapter + FakeInboxClient + FakeMailSender + RecordingNotifier. Coverage includes: * list_open_txs filters ignored + sent + applies ignore_rules * search_inbox refuses zero-filter calls (no full-mailbox scans) * send_beleg happy path, idempotency on (tx_id, attachment), step-a failure leaves no row, rejects ignored tx * mark_ignored blocks both true→false and redundant true→true * flag_anomaly / finalize_run write exactly one row each * ignore_rules parser (ASCII + Unicode arrow, OR-alternatives, etc.) Live smoke: `finance-reconcile-v2 list_open_txs --month 2026-04` ran read-only against the Supabase pooler (port 6543) and returned the expected open-TX set for April 2026 — confirming the SQL shape and the `ignored=false AND NOT EXISTS belege_sent` filter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-import |
4 |
unsupported-operator |
1 |
invalid-argument-type |
1 |
First entries
finance/reconcile_v2/cli.py:86: [unresolved-import] unresolved-import: Cannot resolve imported module `psycopg2`
finance/reconcile_v2/adapter.py:396: [unresolved-import] unresolved-import: Cannot resolve imported module `psycopg2.extras`
finance/reconcile_v2/tests/test_verbs.py:22: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
finance/reconcile_v2/tests/test_verbs.py:632: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["April 2026"]` and `str | None`
finance/reconcile_v2/graph.py:918: [unresolved-import] unresolved-import: Cannot resolve imported module `pymupdf`
finance/reconcile_v2/notifier.py:25: [invalid-argument-type] invalid-argument-type: Argument to function `print` is incorrect: Expected `SupportsWrite[str] | None`, found `object`
✅ Fixed issues: none
Unchanged: 4210 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
…h_inbox errors graph.py now imports MicrosoftGraphTokenProvider from tools/microsoft_graph_auth and prefers it when MSGRAPH_* creds are present. When only the operator's LINEO_MS_* + on-disk refresh-token bundle exist (the current Lineo finance host), it falls back to a delegated provider that reads the *existing* bundle at ~/.hermes/lineo-ms-tokens/sebastian.json — no new credential file, no new auth bootstrap. Removed the dead TokenProvider that pointed at ~/.hermes/secrets/ms_graph_tokens.json. search_inbox now swallows Graph/network/auth failures the same way get_tx_context already did (log a warning, return []) so a transient outage can't blow up an LLM tool turn with an uncaught traceback. ValueError still bubbles — that signals a caller bug, not a 5xx. Added 3 tests for token-provider selection and 2 for search_inbox error policy; 33 tests pass.
|
Fix in 9443b60: |
The same-tx idempotency probe missed the case where the same PDF was
already forwarded for a different bank_tx_id (or any prior tx_id at
all). The agent would happily re-send it.
send_beleg now probes bank.belege_sent for an existing row matching
the candidate PDF on ANY of, in priority order:
1. outlook_message_id (same forwarded mail — strongest)
2. internet_message_id (RFC-5322 Message-Id)
3. attachment_filename element AND equal bank_tx_amount
(cheap stand-in for hashing the PDF)
On a cross-tx hit, send_beleg refuses and returns
{sent: false, status: "already_sent", existing_belege_sent_id,
existing_bank_tx_id, sent_at, matched_on, existing_row}; sender is
never invoked. The same-(tx_id, attachment) idempotency hit
continues to short-circuit with {sent: true, idempotent: true}.
Implementation:
* adapter.Adapter: new find_belege_sent_match read method.
* InMemoryAdapter + PostgresAdapter both implement it; the PG side
uses ROUND(numeric, 2) for amount equality and ANY(array) for the
filename probe.
* verbs.send_beleg: new probe + _match_key helper that names which
key caused the refusal so the LLM can decide downstream.
* skills/.../reconcile-receipts-v2/SKILL.md: one-line note on the
send_beleg row so the LLM knows the verb can refuse and how to
handle it (link via existing_belege_sent_id or flag_anomaly).
Tests (3 new in test_verbs.py, all offline against InMemoryAdapter):
* clean send still passes (existing test_send_beleg_happy_path).
* test_send_beleg_refuses_already_sent_outlook_message_id —
outlook_message_id collision for a different tx_id refuses, sender
not called, no new row.
* test_send_beleg_refuses_already_sent_filename_plus_amount — same
attachment_filename + same bank_tx_amount for a different tx
refuses (outlook/internet ids are deliberately distinct).
30 tests pass (28 prior + 2 new + 1 existing covering clean send).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Dedup added in 50c163a: |
When `send_beleg` dedups against an existing `bank.belege_sent` row
whose `bank_tx_id` is NULL (typical of legacy `outlook_auto_rule`
forwards), link it to the candidate tx instead of refusing. The PDF
is already in DATEV; a second mail would be wasted. Returns
`{status: "linked_existing", belege_sent_id, matched_on}`. Only refuse
(unchanged behaviour) when the existing row is already linked to a
DIFFERENT tx — that's a real cross-tx conflict.
Also drops the `flag_anomaly` verb (Sebastian's call — anomalies are
not the agent's concern). The CLI verb, the verb implementation, the
SKILL.md table entry, and the three offline tests for it are gone.
The `bank.agent_anomalies` table is left in place as legacy data;
`mark_ignored` still writes its audit row there (schema cleanup is
a separate concern).
Net surface: the toolbox is six verbs, not seven.
|
b394788: |
Summary
Parallel, simplified replacement for the v1 reconcile pipeline, written
around the 3-state TX model from unimatrix27/ideas#31. v2 keeps only
four tables (
transactions,belege_sent,agent_anomalies,agent_reconcile_runs) and exposes exactly seven verbs.The full design rationale is in issue NousResearch#31; this PR is the
implementation.
skills/finance/reconcile-receipts-v2/withSKILL.md(3-state model, explicit "do not invent new tools" rule) and
ignore_rules.md(empty template, format documented in header).finance/reconcile_v2/: 7 verbs —list_open_txs,get_tx_context,search_inbox,send_beleg,mark_ignored,flag_anomaly,finalize_run. The LLM reads mail bodies / PDF textdirectly — no vendor-specific parsing.
disjoint table sets. Cleanup of the obsolete v1 tables is a separate
follow-up PR.
What's in / what's deliberately out
In:
list_open_txsfilterignored=false AND NOT EXISTS belege_sent,with a second pass through
ignore_rules.md(counterparty / IBAN /Verwendungszweck patterns)
search_inboxrefuses zero-filter calls — must supply vendor /amount / date_window / message_id
send_belegidempotent on (tx_id, attachment filename)mark_ignoredblockstrue → falseand redundanttrue → trueignore_rules.mdis human-readable and human-editable; the LLM mayappend (e.g. "ignoriere IBAN X komplett") but not rewrite
Out (in this PR):
follow-up cleanup PR.
Test plan
pytest finance/reconcile_v2/tests -q)finance-reconcile-v2 list_open_txs --month 2026-04 --limit 3against Supabase pooler (port 6543) returnedthe expected April-2026 open set.
finance-reconcile-v2under cron alongside or instead of the v1 entry)
🤖 Generated with Claude Code