| """Gradio application for the public CFO-HRM accounting learning Space.""" |
|
|
| from __future__ import annotations |
|
|
| import html |
| import json |
| from pathlib import Path |
| from typing import Any |
|
|
| import gradio as gr |
|
|
| from gradio_inference import ReviewResult, review_case |
|
|
| SPACE_DIR = Path(__file__).resolve().parent |
| GUIDE_TEXT = (SPACE_DIR / "EIL5.md").read_text(encoding="utf-8") |
| CASES_PAYLOAD = json.loads((SPACE_DIR / "demo_cases.json").read_text(encoding="utf-8")) |
| CASES = {str(item["id"]): item for item in CASES_PAYLOAD["cases"]} |
| CASE_CHOICES = [(str(item["label"]), str(item["id"])) for item in CASES_PAYLOAD["cases"]] |
|
|
| PROPOSAL_HEADERS = [ |
| "Bank row", |
| "Amount", |
| "Proposed GL", |
| "Confidence", |
| "Review reason", |
| "Review route", |
| "Bundled reference GL", |
| ] |
| RECORD_HEADERS = [ |
| "ID", |
| "Date", |
| "Amount", |
| "Currency", |
| "Entity", |
| "Reference", |
| "Narrative", |
| ] |
|
|
|
|
| def describe_case(case_id: str) -> str: |
| """Return accountant-facing context for one immutable fictional case.""" |
|
|
| item = CASES.get(case_id) |
| if item is None: |
| return "Choose one of the bundled fictional reconciliation cases." |
| label = html.escape(str(item["label"])) |
| description = html.escape(str(item["description"])) |
| period = html.escape(str(item["period"])) |
| return ( |
| f"### {label}\n\n{description}\n\n" |
| f"**Accounting period:** {period} \n" |
| "**Data classification:** Fixed, fictional training records" |
| ) |
|
|
|
|
| def _money(amount: float, currency: str) -> str: |
| return f"{currency} {amount:,.2f}" |
|
|
|
|
| def _summary_html(result: ReviewResult) -> str: |
| summary = result["summary"] |
| route = html.escape(summary["review_route"]) |
| case_label = html.escape(result["case"]["label"]) |
| alert_class = "" if summary["requires_human_review"] else " is-clear" |
| control_copy = ( |
| "At least one row needs an accountant's attention." |
| if summary["requires_human_review"] |
| else "No row crossed the selected escalation threshold; normal close controls still apply." |
| ) |
| reference = summary["exact_reference"] |
| reference_copy = "Yes" if reference is True else "No" if reference is False else "Not available" |
| return f""" |
| <div class="review-alert{alert_class}"> |
| <strong>{route}</strong> |
| <p>{html.escape(control_copy)} This demonstration never authorizes posting.</p> |
| </div> |
| <div class="metric-row" aria-label="Reconciliation review summary"> |
| <div class="metric-card"> |
| <strong class="metric-value">{summary["matched"]}</strong> |
| <span class="metric-label">Proposed matches</span> |
| </div> |
| <div class="metric-card"> |
| <strong class="metric-value">{summary["unmatched"]}</strong> |
| <span class="metric-label">Unmatched rows</span> |
| </div> |
| <div class="metric-card"> |
| <strong class="metric-value">{summary["flagged"]}</strong> |
| <span class="metric-label">Rows routed to review</span> |
| </div> |
| <div class="metric-card"> |
| <strong class="metric-value">{summary["mean_confidence"] * 100:.1f}%</strong> |
| <span class="metric-label">Mean model confidence</span> |
| </div> |
| </div> |
| <p class="quiet-note"> |
| <strong>{case_label}</strong> · Agreement with the bundled reference answer: |
| {reference_copy} · Posting authorized: <strong>No</strong> |
| </p> |
| """ |
|
|
|
|
| def _proposal_rows(result: ReviewResult) -> list[list[str]]: |
| rows: list[list[str]] = [] |
| for proposal in result["proposals"]: |
| reference = proposal["reference_gl_id"] or "Unmatched" |
| rows.append( |
| [ |
| proposal["bank_id"], |
| _money(proposal["amount"], proposal["currency"]), |
| proposal["gl_id"] or "Unmatched", |
| f"{proposal['confidence'] * 100:.1f}%", |
| proposal["exception"].title(), |
| proposal["route"].replace(" / ", " — ").title(), |
| reference, |
| ] |
| ) |
| return rows |
|
|
|
|
| def _table_html(title: str, headers: list[str], rows: list[list[str]]) -> str: |
| safe_title = html.escape(title) |
| heading_cells = "".join(f'<th scope="col">{html.escape(header)}</th>' for header in headers) |
| if rows: |
| body_rows = "".join( |
| "<tr>" + "".join(f"<td>{html.escape(str(cell))}</td>" for cell in row) + "</tr>" |
| for row in rows |
| ) |
| else: |
| body_rows = ( |
| f'<tr><td colspan="{len(headers)}">' |
| "Run a fictional case to populate this table.</td></tr>" |
| ) |
| return f""" |
| <section aria-label="{safe_title}"> |
| <h3>{safe_title}</h3> |
| <div class="table-scroll"> |
| <table> |
| <thead><tr>{heading_cells}</tr></thead> |
| <tbody>{body_rows}</tbody> |
| </table> |
| </div> |
| </section> |
| """ |
|
|
|
|
| def _record_rows(records: list[dict[str, Any]]) -> list[list[str]]: |
| return [ |
| [ |
| str(record["id"]), |
| str(record["date"]), |
| _money(float(record["amount"]), str(record["currency"])), |
| str(record["currency"]), |
| str(record["entity"]), |
| str(record["reference"]) or "—", |
| str(record["narrative"]), |
| ] |
| for record in records |
| ] |
|
|
|
|
| def run_reconciliation( |
| case_id: str, |
| threshold_percent: float, |
| ) -> tuple[ |
| str, |
| str, |
| str, |
| str, |
| dict[str, Any], |
| ]: |
| """Review a fixed fictional bank-reconciliation case with the released model. |
| |
| The endpoint performs CPU-only ONNX inference, proposes a globally one-to-one |
| bank-to-ledger assignment, and applies the selected confidence threshold. It |
| returns a review summary, proposal table, source records, and an audit object. |
| It has no upload, ERP, approval, payment, or journal-posting capability. |
| """ |
|
|
| try: |
| result = review_case(case_id, threshold_percent) |
| except Exception as error: |
| raise gr.Error( |
| "The fictional case could not be reviewed. Please try again or inspect the Space logs." |
| ) from error |
| return ( |
| _summary_html(result), |
| _table_html("Model proposals", PROPOSAL_HEADERS, _proposal_rows(result)), |
| _table_html("Bank statement", RECORD_HEADERS, _record_rows(result["bank_rows"])), |
| _table_html("General ledger", RECORD_HEADERS, _record_rows(result["gl_rows"])), |
| dict(result), |
| ) |
|
|
|
|
| THEME = gr.themes.Soft( |
| primary_hue=gr.themes.colors.slate, |
| secondary_hue=gr.themes.colors.orange, |
| neutral_hue=gr.themes.colors.gray, |
| radius_size=gr.themes.sizes.radius_sm, |
| text_size=gr.themes.sizes.text_md, |
| ) |
|
|
| with gr.Blocks( |
| title="CFO-HRM Accounting Lab", |
| fill_width=True, |
| ) as demo: |
| with gr.Column(elem_id="cfo-app"): |
| gr.HTML( |
| """ |
| <div class="header-row"> |
| <div> |
| <h1>CFO-HRM Accounting Lab</h1> |
| <p>A learning environment for bank-reconciliation reasoning, |
| designed for accounting teams.</p> |
| </div> |
| <span class="model-badge">Educational model demo</span> |
| </div> |
| """, |
| elem_id="app-header", |
| ) |
| gr.Markdown( |
| ( |
| "**Model status:** The released ONNX checkpoint runs on this Space's CPU. " |
| "All records are fictional. The output is a review proposal—not an approval " |
| "or journal entry." |
| ), |
| elem_id="model-status", |
| ) |
|
|
| with gr.Tabs(selected="guide"): |
| with gr.Tab("Training guide", id="guide"): |
| gr.Markdown( |
| GUIDE_TEXT, |
| elem_id="guide-content", |
| elem_classes=["guide-markdown"], |
| header_links=False, |
| ) |
|
|
| with gr.Tab("Reconciliation review", id="review"): |
| gr.Markdown( |
| """ |
| ## Review a fictional month-end case |
| |
| Choose a case and the confidence level below which a row should |
| be escalated. The model proposes relationships; an accountant |
| remains responsible for evidence, approval, and posting. |
| """, |
| elem_classes=["section-intro"], |
| ) |
| with gr.Column(elem_id="demo-panel"): |
| with gr.Row(): |
| with gr.Column(elem_id="case-inputs", scale=2): |
| case_selector = gr.Dropdown( |
| choices=CASE_CHOICES, |
| value="clean-close", |
| label="Fictional reconciliation case", |
| info="Each case contains a bank statement, ledger, and reference answer.", |
| ) |
| threshold = gr.Slider( |
| minimum=50, |
| maximum=99, |
| value=90, |
| step=1, |
| label="Escalate below this confidence", |
| info="Higher values send more rows to human review.", |
| ) |
| run_button = gr.Button( |
| "Run reconciliation review", |
| variant="primary", |
| elem_id="run-review", |
| ) |
| case_context = gr.Markdown( |
| describe_case("clean-close"), |
| elem_classes=["app-card"], |
| ) |
|
|
| gr.Examples( |
| examples=[ |
| ["clean-close", 90], |
| ["bank-fee", 90], |
| ["reference-light", 90], |
| ], |
| example_labels=[ |
| "Clean close", |
| "Bank fee", |
| "Missing references", |
| ], |
| inputs=[case_selector, threshold], |
| outputs=[], |
| cache_examples=False, |
| label="Try a prepared case", |
| elem_id="case-examples", |
| api_visibility="private", |
| ) |
|
|
| review_summary = gr.HTML( |
| """ |
| <div class="review-alert is-clear"> |
| <strong>Ready for review</strong> |
| <p>Select a case, choose a threshold, and run the model.</p> |
| </div> |
| """, |
| elem_id="review-output", |
| ) |
| proposal_table = gr.HTML( |
| _table_html("Model proposals", PROPOSAL_HEADERS, []), |
| elem_id="results-table", |
| elem_classes=["accounting-table"], |
| ) |
|
|
| with gr.Accordion("Source records", open=False): |
| gr.Markdown( |
| "These are the fixed fictional records supplied to the model.", |
| elem_classes=["quiet-note"], |
| ) |
| bank_table = gr.HTML( |
| _table_html("Bank statement", RECORD_HEADERS, []), |
| elem_classes=["accounting-table"], |
| ) |
| gl_table = gr.HTML( |
| _table_html("General ledger", RECORD_HEADERS, []), |
| elem_classes=["accounting-table"], |
| ) |
|
|
| with gr.Accordion("Audit object", open=False): |
| audit_json = gr.JSON( |
| value={}, |
| label="Read-only model result", |
| open=False, |
| ) |
|
|
| with gr.Tab("Controls & limitations", id="controls"): |
| gr.Markdown( |
| """ |
| ## What this demonstration can and cannot do |
| |
| **It can** |
| |
| - run the released CFO-HRM checkpoint against three fixed, |
| fictional reconciliation cases; |
| - propose one-to-one bank-to-ledger relationships; |
| - surface unmatched, exceptional, or low-confidence rows; and |
| - compare its proposals with a bundled reference answer. |
| |
| **It cannot** |
| |
| - receive company files or credentials; |
| - connect to a bank, ERP, subledger, or general ledger; |
| - approve a reconciliation, prepare or post a journal entry, |
| move money, or make an autonomous financial decision; or |
| - replace evidence, segregation of duties, management review, |
| or the accountant's professional judgment. |
| |
| The threshold is a teaching control, not a calibrated policy |
| recommendation. Before any real deployment, use representative |
| company data, validate performance by entity and exception type, |
| document ownership, and place every downstream action behind |
| explicit human approval. |
| """, |
| elem_id="controls-note", |
| elem_classes=["app-card"], |
| ) |
|
|
| gr.Markdown( |
| """ |
| [Model and evaluation](https://huggingface.co/aznatkoiny/cfo-hrm) |
| · [Source and controls](https://github.com/Aznatkoiny/cfo-hrm) |
| · Educational use only |
| """, |
| elem_id="app-footer", |
| ) |
|
|
| case_selector.change( |
| fn=describe_case, |
| inputs=case_selector, |
| outputs=case_context, |
| show_progress="hidden", |
| api_visibility="private", |
| ) |
| run_button.click( |
| fn=run_reconciliation, |
| inputs=[case_selector, threshold], |
| outputs=[ |
| review_summary, |
| proposal_table, |
| bank_table, |
| gl_table, |
| audit_json, |
| ], |
| api_name="review_reconciliation", |
| api_description=( |
| "Run the released CFO-HRM model against one fixed fictional bank-" |
| "reconciliation case. The result is read-only and never authorizes posting." |
| ), |
| concurrency_limit=1, |
| time_limit=30, |
| ) |
|
|
| demo.queue(max_size=20, default_concurrency_limit=1) |
|
|
|
|
| if __name__ == "__main__": |
| demo.launch( |
| theme=THEME, |
| css_paths=str(SPACE_DIR / "gradio.css"), |
| mcp_server=True, |
| footer_links=["api"], |
| ) |
|
|