aznatkoiny's picture
Migrate Accounting Lab to restrained Gradio interface
0772aab verified
Raw
History Blame Contribute Delete
14.8 kB
"""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"],
)