Skip to content
Tharun2075Public

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

GL Agent ("GL Copilot") — Enterprise General Ledger AI

Location: Google Drive 2075 → Macbackup/Downloads/Projects/gl_demo Created: 2026-07-31 · Status: MVP built & tested — Phases 1, 2, 4, 5 implemented on local SQLite + DeepSeek (mock data). See Development History and Test Results.

📄 Companion docs: PROJECT_BRIEF.md (vision/personas/roadmap) · PLAN.md (Phase 1 design, implemented) · CLAUDE.md (architecture, commands, security for agents)


1. The Conception — why this project exists

Every accounting team lives in a fog of unstructured decisions. A purchase approval says "Feb consulting — Acme India" — which of 200+ accounts does it land on? Month-end means thousands of small classification calls made by humans, inconsistently, under deadline. The pain isn't data entry — it's judgment at scale, currently done by scarce senior accountants.

The idea: an AI agent that reads the messy, free-text reality of accounting (memos, descriptions, source documents) and turns it into correct, audit-ready double-entry journal entries, while answering GL questions, flagging anomalies, and driving the month-end close.

Deliberately NOT AP/AR: Accounts Payable/Receivable agents are document-workflow automation (commoditized). GL is where the judgment and controls live — classification, reconciliation, accruals, allocations, FX, intercompany, close, audit support.

Why GL: journal entries have a checkable right answer: a mapping either lands on the right account or it doesn't, and an entry either balances or it doesn't. So the demo keeps a labelled eval set (data/eval/mapping_golden.jsonl) and reports its mapping accuracy against it (see section 7).

The core act: free-text memo → correct account code + balanced journal entry, validated by a rules engine so the LLM can never post unchecked.

The guardrail (product principle, enforced end-to-end):

LLM proposes → rules engine validates → human approves → ledger posts

No code path lets a model write to the books unchecked.


2. What the project is supposed to do (the 8 capabilities)

# Capability Status
1 GL mapping — memo → account code + confidence ✅ built
2 JE proposal — free-text → balanced, validated JE ✅ built
3 Validation engine — rules gate every proposal (balance, account exists, ISO date, min lines, single-sided) ✅ built (closed-period + duplicate checks pending)
4 GL Q&A / analytics (ask the ledger in plain English) ⏳ not built
5 Reconciliation assistant ⏳ not built
6 Anomaly detection 🟡 data flagged (438 anomalies found in the anonymized GL); no product surface yet
7 Close management ⏳ not built
8 Policy RAG ⏳ not built

3. How it gets data — data intake (inputs)

Real GL data arrives from many systems in many formats. The connector framework normalizes every source into one canonical schema (which mirrors the Australian SME dataset):

  • chart_of_accounts — code, name, type, sub_type, postable
  • journal_entries — id, entry_number, date, description, source, period, status
  • journal_entry_lines — entry_id, account, debit, credit, description
  • plus periods, trial_balance views; and append-only audit tables ingest_log, validation_failures, je_proposals, je_proposal_events

The pluggable seam: Connector ABC → TabularConnector → CSVConnector/JSONConnector. Each connector declares mapping = {canonical_field: source_column}. A future Tally/bank/REST source is a new connector + a mapping, never an edit to the pipeline. A new client of a known format is just a config mapping — no code.

Other inputs the system needs:

  • Source files (CSV/JSON/beancount) on local disk
  • A chart of accounts (the CoA defines the labels)
  • The DeepSeek API key (.env) for LLM classification/proposal
  • Human approval for anything that posts to the books

4. What it spits out (outputs)

  • A canonical SQLite store (gl.db): CoA + journal entries + lines, idempotently loaded
  • Validation reports — every violation persisted to validation_failures (immutable triage list)
  • Mapping results — memo → account code + confidence + method (from map_memo)
  • Proposed journal entries — balanced, rules-validated JEs, recorded to je_proposals (never auto-posted)
  • Posted journal entries — only via explicit approval (gl.approve), with full traceability (source_id = proposal id) and an audit event in je_proposal_events
  • Eval reports — reports/mapping_eval_{full,offline}.txt, reports/je_eval_{full,offline}.txt

5. Where it will be deployed

Now (MVP, running locally): SQLite 3.43 + bare Python stdlib + the DeepSeek API. Zero pip dependencies.

Production north-star (AWS): the design is port-ready from day one.

MVP (now) AWS production
SQLite 3.43 RDS/Aurora PostgreSQL + pgvector (schema is Postgres-portable)
Local file reads S3 (checksum idempotency = S3 ETag; never depend on paths/mtimes)
DeepSeek API AWS Bedrock (the gl_core/llm.py provider seam is the swap point)
CLI scripts Lambda / Step Functions (each connector = a stateless unit)
.env / credentials/ AWS Secrets Manager / SSM Parameter Store
— Snowflake = GL data store of record; on-prem SLM for data-residency-sensitive enterprises

Enterprise hardening (documented in CLAUDE.md → Security): KMS encryption at rest, TLS in transit, IAM least-privilege, VPC with private subnets, S3 Object Lock for tamper-proof retention, CloudTrail, regional pinning for customer data.


6. Where each dataset came from (data provenance)

All datasets are free / mock / public. No real client financials — DeepSeek is China-hosted, so only mock data ever goes to it.

Dataset Source Files What it is Used for
Australian SME Kaggle (Mindweave sample; full set is paid on Gumroad) kaggle/australian-sme-business-dataset/ (26 tables) Day-by-day retail SME bookkeeping simulation — 56-account CoA, 200 JEs / 500 lines in the sample (full: 7,407 / 18,623) Primary training/eval — defines the canonical schema
Anonymized GL Kaggle (CC0) kaggle/anonymized-general-ledger-dataset/GL_Dataset.csv (21,551 rows) Real-world account-level ledger with anomalies (Benford) Real-data stress test; anomaly detection
US Standard GL Kaggle (MIT) kaggle/us-standard-general-ledger/ledger.csv (241 Q&A) + definitions PDF USSGL account Q&A pairs RAG/eval for US federal accounting
GL financial Kaggle kaggle/generalledger/Data file for students.xlsx Classic student GL Reference (deferred — needs openpyxl)
Receiver General Kaggle kaggle/receiver-general-accounting-transactions/tcrg-rgat.csv (9,282 rows) Canadian federal GL journal vouchers, EN/FR headers Real government GL, international flavor
Beancount GitHub (MIT) beancount/*.beancount (4 ledgers, 8,312 lines) Realistic plain-text double-entry ledgers Memo/description texture
SEC EDGAR sec.gov (public domain) sec/company_tickers.json (10,412 cos) + companyfacts_*.json Public-company XBRL financials Analytics QA, not JE-level

The key missing piece (being built): a large labeled (free-text memo → account) eval set. Currently 740 rows in data/eval/mapping_golden.jsonl, grown from 52 via the augment_golden.py generator.


7. Development history — every phase, what we built, and the test results

Phase 0 — Data collection (done before this build)

Downloaded and verified the datasets above into data/, wrote PROJECT_BRIEF.md and PLAN.md. No code.

Phase 1 — Ingestion pipeline (the canonical store)

Built: gl_core/models.py (dataclasses + coercers to_amount/to_bool/to_int), schema.py (DDL + 3 views), connectors.py (Connector/Tabular/CSV/JSON with the mapping seam), validation.py (rules R1 balance · R2 account_exists · R3 date_iso · R4 min_lines · R5 single_sided), pipeline.py (run_ingest: load → validate → one atomic transaction, INSERT OR IGNORE = hard idempotency). CLIs scripts/ingest_sme.py + scripts/verify.py. .gitignore created, git init.

Tested:

  • Ingest of the SME sample: 56 CoA / 200 JEs / 500 lines, 756 records, all inserted
  • Idempotency: a re-run inserts 0 new rows (checksum-keyed, stable UUID PKs)
  • 11 of the 200 JEs are orphaned (0 lines) — expected; flagged by R4, still loaded
  • verify.py — ALL 9 CHECKS PASSED: PRAGMA integrity_check=ok, counts 56/200/500, 0 unbalanced JEs, 0 orphan lines, Σdebit = Σcredit = 711,379.29 (diff 0.00), trial-balance view (top-10 by |net| sensible: opening equity −200k, sales −173k, COGS +37.5k, salaries +124.7k), sample queries, audit tables present
  • Coercers proven on the messy anonymized-GL shapes (comma groups 2,380,000, (1,200.00) negatives, cr/dr, $, — → all handled)

Phase 2 — Mapping core (memo → account)

Built: gl_core/mapper.py (normalize, lexical_score, classify_with_llm, map_memo — LLM proposes, rules gate: account exists + postable), gl_core/providers/deepseek.py (first real provider on the llm.py seam, pure urllib), scripts/map_memo.py, scripts/eval_mapping.py, scripts/build_mapping_golden.py.

Tested (52-row eval set):

  • Offline lexical baseline: 51.9%
  • Full stack (DeepSeek + lexical + rules): 92.3% top-1
  • After accounting-principles prompt guidance + a corrected label: 98.1% (asset/equity/liability/revenue 100%, expense 96.8%)
  • The remaining miss was a derived-label artifact ("Supplier invoice received" → data says COGS, model says AP — a defensible answer)

Phase 3 (partial) — Eval harness hardening

Built per-mode reports (mapping_eval_{full,offline}.txt), added --workers parallelism, grew the eval set (see Eval set growth below).

Phase 4 — JE proposal + validation engine

Built: gl_core/proposer.py (propose_je: LLM proposes lines → deterministic balance plug → R1–R5 validate), scripts/propose_je.py (records to je_proposals, never posts), scripts/build_je_golden.py + scripts/eval_je.py.

The balance guarantee: a pure arithmetic correction computes Σdebit − Σcredit and plugs the residual into the correct side (or appends a bank leg). 100% balance is guaranteed by construction — never subject to model drift. (An early plug_to-hint design let the model override the arithmetic and dropped balance to 92%; the hint was removed, restoring the guarantee.)

Tested (25-row JE eval):

  • Balance rate 25/25 = 100% (the Phase 4 DoD)
  • Amounts 26/26 = 100% on hand-authored exact-amount JEs (payroll, GST sales, loan repayment, depreciation…)
  • Structural match 80.3% overall; hand-authored free-text JEs 100% on both structure and amounts (templated derived memos without amounts score lower — a labeling-signal limitation, not a defect)

Phase 5 — MCP server (gl.* tools + approve → post)

Built: gl_core/mcp_server.py (minimal pure-stdlib stdio MCP server, newline-delimited JSON-RPC 2.0), gl_core/posting.py (maker-checker: post_proposal re-validates R1–R5 from the stored payload, blocks double-posts and rejected proposals, one transaction, audit-logged), je_proposal_events audit table, scripts/mcp_server.py launcher.

Tested:

  • Unit + subprocess round-trip: initialize → tools/list (4 tools) → gl.propose_je → gl.validate → gl.query → gl.approve → posted JE lands with source_id = proposal id and an approved event; unknown tool → JSON-RPC error
  • Live smoke (real LLM): "Paid vendor 50,000 for annual software license" → proposal 7 → approved → posted JE Dr 6-3200 50,000 / Cr 1-1000 50,000, Σdebit = Σcredit = 50,000, diff 0.00, clean protocol stream

Hardening pass (before Phase 5 sign-off)

Fixed: stale docs (CLAUDE.md/README/PLAN said "no code exists"), zero tests → 67-test suite, raw-traceback empty-db UX → friendly "run ingest_sme.py first", eval-report overwrites → per-mode files, no LLM retry → 3× retry with backoff, deterministic CoA ordering, je_proposals verify check.

Eval set growth

Built: scripts/augment_golden.py — grows mapping_golden.jsonl with label-safe free-text memos (slot-filled templates from the real SME products/suppliers/months + LLM paraphrases of the templated seeds).

An eval-hygiene bug caught: the first LLM paraphrase pass leaked the answer into the memo ("Monthly depreciation expense on store shelving - account 6-2800"). Fixed with a code-leak filter (drops any memo containing its own account code) + an anti-leak prompt instruction. Final set: 740 rows, 0 code-leaks (was 52).

Re-tested on the 740-row set:

  • Offline lexical baseline: 48.1% (harder set — expected drop from 51.9%)
  • Full stack: 97.4% top-1 (revenue/asset/equity 100%, expense 98.7%, liability 76.4%) — the mapper generalizes to 14× more varied free text without collapsing
  • The liability dip is mostly a template-design miss ("GST on {product} sale" → model reads "sale" as revenue; GST-on-sale belongs to GST Payable — a real, minor product gap)

Real-data stress test — the anonymized GL connector

Built: gl_core/anonymized.py (connector set for the 21,551-row Kaggle anonymized GL: dd-mm-yy date normalization, comma-grouped amounts, space-y headers, duplicate-line dedup) + scripts/ingest_anonymized.py.

A real bug found by "see how it goes": the first design made one JE per row → 21,551 false single-sided "JEs". The data is actually double-entry when grouped by transaction (Date + Transaction ID). After the fix:

  • 5,765 real transactions (from 21,551 rows → 18,703 distinct lines), 102 accounts
  • R1 flags 438 transactions as unbalanced (~7.6%) — the genuine anomalies this dataset exists for (e.g. OT00359: debits 73,402.00 vs credits 73,401.00 — a $1 discrepancy)
  • The canonical gl.db was left untouched

8. How it's tested — every check, in detail

Unit tests — python3 -m unittest discover -s tests → 67/67 PASS (stdlib, no pip):

  • test_coercers — every parsing quirk (comma groups, parens, cr/dr, $, —, None/empty)
  • test_validation — R1–R5 on crafted balanced/unbalanced/unknown-account/bad-date/1-line/double-sided cases
  • test_connectors — CSV + JSON (array + dict-of-columns), the mapping rename seam, empty files, checksums
  • test_pipeline — ingest counts, idempotency (2nd run = 0 inserted), violation persistence, atomic rollback on error
  • test_mapper — normalize, lexical ranking, offline fallback, unknown-code rejection, provider-failure degradation
  • test_proposer — the invariant: adversarial LLM output (single-line, all-debit, wrong-side imbalance) still yields balanced=True after the plug; audit recording is append-only
  • test_parsing — fenced/stray/missing-field JSON from the LLM
  • test_posting — post lands balanced with traceability; double-post blocked; invalid proposal refused; rejected-proposal post blocked; proposal row immutable
  • test_mcp_server — full subprocess stdio round-trip (prove "any MCP client can drive it")
  • test_anonymized — transaction grouping, messy coercions, duplicate-line dedup
  • test_augment_golden — leak filter + slot-fill volume

scripts/verify.py — 9 DB assertions (integrity, counts, every JE balances, no orphan lines, global balance, orphans flagged, trial balance, sample queries, audit tables).

Eval harnesses — measure and gate the DoD (summary in §7; current numbers in reports/).


9. How it will be maintained

All commands run from the repo root; pure stdlib, no install. Full list in CLAUDE.md → Commands. Key ones:

python3 scripts/ingest_sme.py              # load the SME sample into gl.db
python3 scripts/verify.py                  # assert the store is sound (exit 0/1)
python3 scripts/map_memo.py "Feb consulting — Acme"   # memo → account
python3 scripts/propose_je.py "Paid vendor 50k for software license"  # propose + record (never posts)
python3 scripts/eval_mapping.py [--offline] [--workers N]   # mapping accuracy
python3 scripts/eval_je.py                 # JE balance rate (DoD 100%)
python3 scripts/build_mapping_golden.py && python3 scripts/augment_golden.py  # regenerate/grow the eval set
python3 scripts/mcp_server.py --db gl.db   # the MCP server (stdio JSON-RPC)
python3 -m unittest discover -s tests      # the full test suite

Maintenance routine: after any change, run the test suite + the two evals; regenerate goldens only when the data changes. Register the MCP server in a client via:

{"mcpServers": {"gl": {"command": "python3", "args": ["scripts/mcp_server.py", "--db", "gl.db"]}}}

10. Environment & LLM setup

  • Python 3.14.6 (Homebrew) — deliberately zero-pip (csv, json, sqlite3, dataclasses, urllib, unittest)
  • SQLite 3.43 — the dev store (Postgres-portable by design)

LLM — DeepSeek (OpenAI-compatible chat API). Set DEEPSEEK_API_KEY in your environment or a local .env file (gitignored — never commit it). The provider is a swappable seam (gl_core/llm.py), so the endpoint can change without touching any callers.

Compute (future) — no GPU compute has been used yet. Reaching the SLM fine-tuning phase (Phase 6) will require renting or purchasing GPU compute (e.g. a cloud GPU provider).


11. Security

  • The guardrail — LLM proposes → rules validate → human approves → ledger posts; gl.approve is the only book-writing tool and it re-validates + audits.
  • Append-only audit trail — ingest_log, validation_failures, je_proposals, je_proposal_events are never UPDATE/DELETEd; violations are evidence, not errors.
  • Mock data only to DeepSeek (China-hosted). Real client financials → Bedrock/on-prem only (the north-star).
  • AWS hardening (production) — KMS at rest, TLS in transit, IAM least-privilege, private-subnet VPC, S3 versioning + Object Lock, CloudTrail, regional pinning.
  • Secrets never enter the repo — .env*, credentials/, and all *.db are gitignored (see .gitignore). Never commit API keys or credentials; if one leaks, rotate it.

12. Roadmap — what's next

  1. Capability #4 — GL Q&A / analytics (NL→SQL over the store) and capability #6 — anomaly detection as a product surface (the 438 anomalies are already findable)
  2. Phase 3 formal CI — gate accuracy across prompt changes
  3. More connectors / data — Receiver-General, beancount parser, fresh Kaggle GL datasets; grow the eval set toward 2,000+
  4. Phase 6 — SLM tier — fine-tune a small classifier on the GL corpus (requires renting/purchasing GPU compute)
  5. Phase 7 — Enterprise — Bedrock + Snowflake + on-prem deployment

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages