From 82f8227c2b85fb662eb8ad3ba59a81ce24ed77ca Mon Sep 17 00:00:00 2001 From: SiddharthMuneshwar26 Date: Tue, 18 Aug 2026 14:59:24 +0300 Subject: [PATCH 1/6] Add files via upload --- .../table-fidelity-benchmark/Makefile | 13 + .../table-fidelity-benchmark/README.md | 207 +++++++ .../docs/fixture-merge-l-shaped.jpg | Bin 0 -> 31355 bytes .../table-fidelity-benchmark/requirements.txt | 4 + .../tablebench/__init__.py | 0 .../tablebench/capture.py | 201 +++++++ .../tablebench/client.py | 313 ++++++++++ .../tablebench/compare.py | 243 ++++++++ .../tablebench/corpus/__init__.py | 0 .../tablebench/corpus/cases.py | 556 ++++++++++++++++++ .../tablebench/corpus/generate.py | 215 +++++++ .../tablebench/extract.py | 399 +++++++++++++ .../tablebench/htmlout.py | 135 +++++ .../tablebench/model.py | 147 +++++ .../tablebench/run.py | 327 ++++++++++ .../tests/test_benchmark.py | 359 +++++++++++ .../insurance-loss-run-summary/Makefile | 13 + .../insurance-loss-run-summary/README.md | 216 +++++++ .../corpus/MANIFEST.md | 130 ++++ .../corpus/generate.py | 490 +++++++++++++++ .../KM-2022-0311-adjuster-note.docx | Bin 0 -> 37048 bytes .../KM-2023-0417-adjuster-note.docx | Bin 0 -> 37097 bytes .../KM-2023-0588-adjuster-note.docx | Bin 0 -> 37072 bytes .../meridian-renewal-application-2026.docx | Bin 0 -> 37219 bytes .../ardent-cover-note-2026-01-28.docx | Bin 0 -> 37098 bytes .../loss-run-2021-valued-2024-01.csv | 24 + .../loss-run-2022-valued-2024-01.csv | 24 + .../loss-run-2022-valued-2024-01.pdf | Bin 0 -> 57030 bytes .../loss-run-2023-valued-2024-01-ORIGINAL.csv | 21 + .../loss-run-2023-valued-2026-01-REISSUE.csv | 22 + .../loss-run-2024-valued-2026-01.csv | 19 + .../out/rules/underwriting-checklist.docx | Bin 0 -> 37331 bytes .../lossrun/__init__.py | 0 .../lossrun/model.py | 195 ++++++ .../lossrun/parse.py | 259 ++++++++ .../lossrun/review.py | 316 ++++++++++ .../lossrun/summarize.py | 305 ++++++++++ .../lossrun/superdocs.py | 367 ++++++++++++ .../requirements.txt | 4 + .../tests/conftest.py | 3 + .../tests/test_lossrun.py | 333 +++++++++++ 41 files changed, 5860 insertions(+) create mode 100644 extensions/SiddharthMuneshwar26/table-fidelity-benchmark/Makefile create mode 100644 extensions/SiddharthMuneshwar26/table-fidelity-benchmark/README.md create mode 100644 extensions/SiddharthMuneshwar26/table-fidelity-benchmark/docs/fixture-merge-l-shaped.jpg create mode 100644 extensions/SiddharthMuneshwar26/table-fidelity-benchmark/requirements.txt create mode 100644 extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/__init__.py create mode 100644 extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/capture.py create mode 100644 extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/client.py create mode 100644 extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/compare.py create mode 100644 extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/corpus/__init__.py create mode 100644 extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/corpus/cases.py create mode 100644 extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/corpus/generate.py create mode 100644 extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/extract.py create mode 100644 extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/htmlout.py create mode 100644 extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/model.py create mode 100644 extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/run.py create mode 100644 extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tests/test_benchmark.py create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/Makefile create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/README.md create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/MANIFEST.md create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/generate.py create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/adjuster-notes/KM-2022-0311-adjuster-note.docx create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/adjuster-notes/KM-2023-0417-adjuster-note.docx create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/adjuster-notes/KM-2023-0588-adjuster-note.docx create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/application/meridian-renewal-application-2026.docx create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/correspondence/ardent-cover-note-2026-01-28.docx create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2021-valued-2024-01.csv create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2022-valued-2024-01.csv create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2022-valued-2024-01.pdf create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2023-valued-2024-01-ORIGINAL.csv create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2023-valued-2026-01-REISSUE.csv create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2024-valued-2026-01.csv create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/rules/underwriting-checklist.docx create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/__init__.py create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/model.py create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/parse.py create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/review.py create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/summarize.py create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/superdocs.py create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/requirements.txt create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/tests/conftest.py create mode 100644 use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/tests/test_lossrun.py diff --git a/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/Makefile b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/Makefile new file mode 100644 index 00000000..b90689b4 --- /dev/null +++ b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/Makefile @@ -0,0 +1,13 @@ +.PHONY: install test dry sample live clean +install: + pip install -r requirements.txt +test: + python -m pytest tests/ -q +dry: + python -m tablebench.run --mode dry --max-ops 25 --out results +sample: + python -m tablebench.run --mode dry --limit 3 --max-ops 3 --out results +live: + python -m tablebench.run --mode live --max-ops 25 --out results +clean: + rm -rf results corpus_out .pytest_cache **/__pycache__ diff --git a/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/README.md b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/README.md new file mode 100644 index 00000000..5c2b87f0 --- /dev/null +++ b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/README.md @@ -0,0 +1,207 @@ +# Table fidelity benchmark + +Measures whether merged cells, nested tables, spanning headers and numeric +formatting survive an edit-and-export cycle on SuperDocs — and, when they do +not, says *which stage* lost them. + +Built for the SuperDocs Round 2 task (assigned build: table fidelity +benchmark, band S2, surfaces: API + export). + +## Run it + +```bash +git clone && cd table-fidelity-benchmark +make install +make dry # full run, entirely offline, spends nothing +``` + +That is the whole setup. `make dry` generates the corpus, runs every case +through an offline round-trip, and writes `results/report.md`. No API key, no +network, no operations. + +Against the live API: + +```bash +export SUPERDOCS_API_KEY=... # leading space keeps it out of shell history +make live # 20 operations, capped at 25 +``` + +The key is read from the environment only. It is never a command-line +argument, never logged, never written to a result file, and a test asserts +that no key-shaped string reaches any output. + +## What it measures + +Twenty cases across four declared axes: + +| axis | cases | examples | +|---|---|---| +| merged cells | 7 | L-shaped merges, full-width dividers, a nested table inside a merged cell | +| nested tables | 3 | one level, two levels, two nested tables side by side | +| spanning headers | 4 | two- and three-tier headers, `w:tblHeader` repeat flags, rotated header cells | +| numeric formatting | 6 | `(12,400.00)`, `1.230`, `0007`, `3.2×10⁻⁶`, `$1,204,880.00`, mixed date formats | + +Cases are data, in `tablebench/corpus/cases.py`. Adding a pathology is a new +entry there and touches no other file. + +## Method + +Each case runs twice. + +``` +control upload → export 0 operations +edit upload → chat → approve → export 1 operation +``` + +The control is the point. Any difference the control also shows is a property +of ingest and export; only what the edit run adds on top is caused by the +edit. Without that separation a benchmark can report that a table broke but +not where, which is not actionable for anyone. + +Each case names the cells its instruction legitimately changes — usually one, +sometimes two where the instruction asks for two figures. Those are exempt from +content comparison. Every other cell, every span, every shading value, every +column width and all the prose around the table are invariants — if they move, +that is a finding. A run that changes nothing scores no better than one that +mangles the document: the report carries both `edit_applied` and the findings, +and a case passes only when the edit landed *and* nothing else moved. + +Comparison happens between two canonical grid models, never between two files +directly. Both the DOCX extractor and the HTML extractor produce that model, +so the HTML intermediate is compared as its own stage. + +Spans are resolved from raw `gridSpan` / `vMerge` rather than through +python-docx's table API, which repeats merged cells instead of reporting +spans — precisely the thing under test. + +Borders are compared as **drawn edges, not cell declarations**. The same rule +can be declared on the table element or on each cell, and as one cell's bottom +or the next cell's top; all three render identically. Comparing declarations +reports a re-expression as damage. On the first live run that difference was +35 false findings against 3 real ones — the noise was louder than the signal. +The comparator now folds table-level borders into each cell by position, then +compares the resolved edge grid. + +## Budget + +**Declared cap for a full live run: 25 operations.** A full run is 20 cases at +one chat turn each; the remaining 5 are headroom for a retry. Uploads, +approvals and exports are free per the published pricing, and the cost table +lives in one place (`OP_COST` in `client.py`). + +The cap is enforced *before* each spend, not after — the run stops with the +budget intact rather than discovering it overspent. Small-sample mode is the +default way to work: + +```bash +make sample # first 3 cases +python -m tablebench.run --mode live --axis numeric_formatting --max-ops 6 +python -m tablebench.run --mode live --cases merge_l_shaped --max-ops 1 +``` + +State is written after every case, atomically. Every record carries the mode +it was produced in, and `--resume` only skips a case already recorded **in the +same mode** — a dry-run record is a simulation and is not evidence about the +live API, so it never stands in for one. Simulated operations do not count +against a live budget either. If a results directory ever ends up holding +both, the report says `MIXED` at the top and refuses to present them as one +run. + +`--resume` skips work already done, so a crash or a kill costs nothing already +paid for: + +```bash +python -m tablebench.run --mode live --max-ops 25 --out results --resume +``` + +Actual spend is printed at the end of every run and reported at the top of +`results/report.md` alongside the results. + +## Tests + +```bash +make test # 19 tests, no API key, no network, no operations +``` + +The tests do not assert that the fake returns what it was handed. The offline +round-trip is verified lossless first — 0 findings across all 20 cases — and +then each fault with known ground truth (`flatten_merges`, `drop_nested`, +`strip_trailing_zero`, `drop_shading`, `rewrite_body`) is injected and the +comparator is checked for detecting it in every case that contains the +pathology and in none that does not. If the control ever drifts, every +detection result becomes unattributable, so that test is the load-bearing one. + +You can watch a fault land: + +```bash +python -m tablebench.run --mode dry --fault flatten_merges --axis merged_cells +``` + +## Capturing evidence for a bug report + +``` +python -m tablebench.capture --case merge_l_shaped --out evidence +python -m tablebench.capture --case header_repeat_flag --out evidence --control-only +``` + +Writes the source file, the HTML the upload endpoint returned, the exported +file, and a stage-by-stage list of every difference. `--control-only` sends no +edit and spends no operations, which is what shows a loss belongs to the +round-trip rather than the model. + +The ingest HTML is the useful artifact: for the merge bugs the `rowspan` +attribute is already absent there, before any edit is requested, which locates +the fault in the parser rather than leaving a maintainer to find it. + +## Assumptions logged while building + +- **One chat turn is one operation; upload, approve and export are free.** + From the published pricing. If it changes, `OP_COST` is the single edit. +- **The comparison of record is the exported DOCX**, not the HTML + intermediate. Attributes plain HTML cannot express — `tblHeader`, column + widths, `textDirection`, per-cell borders — are withheld at the HTML stage + and listed as *not measurable* rather than counted as losses. +- **Approve-all, not approve-selectively.** The card is about fidelity, not + about the review UI, so every proposed change is approved. Partial approval + is a natural extension and is not built. +- **Whitespace is normalised, unicode is not.** An em dash becoming a hyphen, + or a non-breaking space becoming a space, is a finding. Trailing spaces are + not. +- **A long silence is a timeout, not a failure.** The docs note that large + documents can run for minutes with no visible progress, so the client polls + to a deadline and reports a timeout as exactly that. + +## What this does not measure + +- **Rendering.** Two files that parse identically could still paginate + differently. Out of scope, and stated rather than implied. +- **Whether the edit was *correct*** — only whether it landed in the target + cell and nothing else moved. Judging the model's wording is a different + benchmark. +- **Two false-positive bugs found by capturing real evidence.** Alignment was + read only from the cell, while the API puts it on the paragraph inside, so + centred headings were reported as lost when they had survived correctly. And + `header_rows` was withheld at the HTML stage as unrepresentable, when the API + in fact returns a real `` -- withholding it hid the useful finding that + the header marking survives ingest and is lost on export. Both were reporting + against output that was right. +- **A third confusion between "unmeasurable" and "fine".** The capture summary + reported `text_direction` as surviving ingest when the HTML stage had merely + withheld it -- plain HTML has no property for rotation, so nothing was + measured either way. It now reports withheld codes separately and says they + locate nothing. The same mistake in different clothes each time: silence + read as a pass. +- **The fault injector is coarse.** `drop_nested` is a regex and surfaces as + `table_count` rather than `nested_table_count`. The fault is detected in + every nesting case, but the code it reports is less precise than the + comparator is capable of. Real API output is parsed properly; this affects + the test harness only. +- **Corpus realism.** These are synthetic fixtures built to be pathological. + They cover the constructs real documents contain, not the mess real + documents are. + +All figures, companies and part numbers in the corpus are fabricated. + +--- + +Built by Siddharth for the SuperDocs Round 2 task. diff --git a/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/docs/fixture-merge-l-shaped.jpg b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/docs/fixture-merge-l-shaped.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ace5ffd8c815019fd18d3a504a7c17afbb348b74 GIT binary patch literal 31355 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 U4UDFN(KIlc2I!jx*#F-I0O}mgf&c&j literal 0 HcmV?d00001 diff --git a/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/requirements.txt b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/requirements.txt new file mode 100644 index 00000000..58870be0 --- /dev/null +++ b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/requirements.txt @@ -0,0 +1,4 @@ +lxml>=5.0 +beautifulsoup4>=4.12 +requests>=2.31 +pytest>=8.0 diff --git a/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/__init__.py b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/capture.py b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/capture.py new file mode 100644 index 00000000..d7a0727c --- /dev/null +++ b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/capture.py @@ -0,0 +1,201 @@ +"""Capture evidence for a bug report. + +Runs one case against the live API and writes everything a maintainer needs to +reproduce and locate the fault, without them having to install this benchmark: + + evidence//1-source.docx what was uploaded + evidence//2-ingest.html the HTML your API returned <-- the proof + evidence//3-ingest-table.html just the table, pretty-printed + evidence//4-export.docx what came back + evidence//5-findings.txt what differs, and at which stage + evidence//6-summary.md paste-ready bug report body + +Why the HTML matters. For the merge bugs, the loss is visible in the response +from upload -- before any edit is requested. That locates the fault in the +ingest parser rather than the model or the export, which is the difference +between a report someone can act on and one they have to investigate first. + + python -m tablebench.capture --case merge_l_shaped --out evidence + python -m tablebench.capture --case header_repeat_flag --out evidence + +Costs one operation per case (the control run costs none, and --control-only +spends nothing at all). +""" + +from __future__ import annotations + +import argparse +import os +import re +import shutil +import sys + +from . import compare, extract +from .client import ApiError, Ledger, SuperDocsClient +from .corpus import cases as case_mod +from .corpus.generate import write_docx + + +def pretty_table(html: str) -> str: + """Isolate the first table and indent it so a reader can see the spans.""" + m = re.search(r"", html, re.S | re.I) + if not m: + return "(no element in the response)" + frag = m.group(0) + frag = re.sub(r"><", ">\n<", frag) + out, depth = [], 0 + for line in frag.split("\n"): + if re.match(r""): + depth += 1 + return "\n".join(out) + + +def span_summary(html: str) -> str: + """Count the spans the API's HTML actually carries.""" + rowspans = re.findall(r'rowspan="(\d+)"', html) + colspans = re.findall(r'colspan="(\d+)"', html) + return (f"rowspan attributes present: {len(rowspans)} {rowspans}\n" + f"colspan attributes present: {len(colspans)} {colspans}") + + +def main(argv=None) -> int: + ap = argparse.ArgumentParser(prog="tablebench.capture") + ap.add_argument("--case", required=True, choices=sorted(case_mod.BY_ID)) + ap.add_argument("--out", default="evidence") + ap.add_argument("--control-only", action="store_true", + help="upload and export with no edit; spends no operations") + ap.add_argument("--max-ops", type=int, default=2) + a = ap.parse_args(argv) + + case = case_mod.BY_ID[a.case] + outdir = os.path.join(a.out, a.case) + os.makedirs(outdir, exist_ok=True) + + src = write_docx(case, os.path.join(outdir, "1-source.docx")) + before = extract.from_docx(src) + + ledger = Ledger(cap=a.max_ops) + try: + client = SuperDocsClient(ledger) + except ApiError as exc: + print(exc, file=sys.stderr) + return 2 + + session = f"evidence-{a.case}" + print(f"uploading {os.path.basename(src)} ...") + html = client.upload(src, session) + + with open(os.path.join(outdir, "2-ingest.html"), "w", encoding="utf-8") as fh: + fh.write(html) + with open(os.path.join(outdir, "3-ingest-table.html"), "w", + encoding="utf-8") as fh: + fh.write(pretty_table(html)) + + ingest_doc = extract.from_html(html) + ingest_findings = compare.compare(before, ingest_doc, profile="html") + + if a.control_only: + out_path = client.export(session, os.path.join(outdir, "4-export.docx")) + else: + print(f"sending one edit: {case.edit[:70]}...") + job = client.edit(session, html, case.edit) + if job.get("pending"): + client.approve(session, job["job_id"], job["pending"]) + out_path = client.export(session, os.path.join(outdir, "4-export.docx")) + + after = extract.from_docx(out_path) + exempt = [case.target, *getattr(case, "also_changes", [])] + export_findings = compare.compare(before, after, target=exempt) + + lines = [ + f"CASE: {case.id} - {case.title}", + f"AXIS: {case.axis}", + "", + "=== SOURCE (what was uploaded) ===", + _grid(before.tables[0]) if before.tables else "(no table)", + "", + "=== AFTER INGEST (parsed from the HTML your API returned) ===", + _grid(ingest_doc.tables[0]) if ingest_doc.tables else "(no table)", + "", + span_summary(html), + "", + f"--- differences at the INGEST stage: {len(ingest_findings)} ---", + ] + lines += [f" {f}" for f in ingest_findings] or [" (none)"] + lines += ["", "=== AFTER EXPORT ===", + _grid(after.tables[0]) if after.tables else "(no table)", "", + f"--- differences in the EXPORTED file: {len(export_findings)} ---"] + lines += [f" {f}" for f in export_findings] or [" (none)"] + lines += ["", f"operations spent: {ledger.spent}"] + + report = "\n".join(lines) + with open(os.path.join(outdir, "5-findings.txt"), "w", encoding="utf-8") as fh: + fh.write(report) + print("\n" + report) + + # Attribute per finding, not per case. A single label was wrong on the + # header-flag case: one unrelated ingest difference made the whole report + # say "ingest" when the header marking actually survived ingest and was + # lost on export -- the opposite of what the report needed to say. + ingest_codes = {f.code for f in ingest_findings} + export_codes = {f.code for f in export_findings} + # Some attributes have no representation in plain HTML, so the HTML stage + # withholds them. Withheld is not the same as intact: counting them as + # "survived ingest" would assert something the evidence cannot support, + # which is the exact failure this tool exists to avoid. + withheld = sorted(export_codes & compare.HTML_UNREPRESENTABLE) + lost_at_ingest = sorted(ingest_codes & export_codes) + lost_at_export = sorted(export_codes - ingest_codes - set(withheld)) + + if lost_at_ingest: + stage = ("ingest -- present in the uploaded file, absent from the HTML " + "your API returned, before any edit was requested") + elif lost_at_export: + stage = ("export -- the HTML your API returned carries it correctly; " + "it is absent from the exported DOCX") + elif withheld: + stage = ("cannot be localised -- plain HTML has no property for " + "this, so the HTML stage cannot measure it either way") + else: + stage = "no difference found" + with open(os.path.join(outdir, "6-summary.md"), "w", encoding="utf-8") as fh: + fh.write( + f"# {case.title}\n\n" + f"**Where the loss happens:** {stage}\n\n" + f"**What I did.** Uploaded `1-source.docx`" + + ("" if a.control_only else f" and sent one instruction: " + f"\"{case.edit}\"") + + ", then exported.\n\n" + f"**Files attached.** `1-source.docx` reproduces it. " + f"`2-ingest.html` is the response from upload -- the loss is " + f"visible there, before any edit. `4-export.docx` is what came " + f"back. `5-findings.txt` lists every difference and the stage it " + f"appeared at.\n\n" + f"**Ingest differences:** {len(ingest_findings)} \n" + f"**Export differences:** {len(export_findings)}\n\n" + + (f"**Lost at ingest:** {', '.join(lost_at_ingest)}\n\n" + if lost_at_ingest else "") + + (f"**Survives ingest, lost on export:** " + f"{', '.join(lost_at_export)}\n\n" if lost_at_export else "") + + (f"**Not measurable at the HTML stage** (no standard HTML " + f"property, so this says nothing about where it was lost): " + f"{', '.join(withheld)}\n" if withheld else "")) + print(f"\nwritten to {outdir}/") + return 0 + + +def _grid(t) -> str: + rows = [] + for c in sorted(t.cells, key=lambda c: (c.row, c.col)): + flag = "" + if c.row_span > 1 or c.col_span > 1: + flag = f" <-- rowspan={c.row_span} colspan={c.col_span}" + rows.append(f" ({c.row},{c.col}) {c.text[:26]!r}{flag}") + return "\n".join(rows) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/client.py b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/client.py new file mode 100644 index 00000000..8c1af119 --- /dev/null +++ b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/client.py @@ -0,0 +1,313 @@ +"""The four calls: upload, chat, approve, export. + +The API key is read from the SUPERDOCS_API_KEY environment variable and is +never accepted as an argument, never logged, and never written to a result +file. Nothing in this repository should ever hold a credential. + +FakeClient is the offline counterpart. It is not here to make tests pass -- +it is here so the comparator can be tested against faults whose ground truth +is known: flatten a merge, drop a colspan, strip a trailing zero, then assert +the comparator reports exactly that and nothing else. +""" + +from __future__ import annotations + +import json +import os +import re +import time +from dataclasses import dataclass, field + +BASE = os.environ.get("SUPERDOCS_BASE", "https://api.superdocs.app") + +# One chat turn is one operation. Uploads, exports and downloads are free per +# the published pricing; if that changes, change it here and nowhere else. +OP_COST = {"upload": 0, "chat": 1, "approve": 0, "export": 0} + + +class ApiError(RuntimeError): + pass + + +class BudgetExceeded(RuntimeError): + pass + + +@dataclass +class Ledger: + """Operation accounting with a hard cap checked before every spend.""" + + cap: int + spent: int = 0 + by_stage: dict[str, int] = field(default_factory=dict) + timings: dict[str, float] = field(default_factory=dict) + + def charge(self, stage: str) -> None: + cost = OP_COST.get(stage, 0) + if cost and self.spent + cost > self.cap: + raise BudgetExceeded( + f"stage {stage!r} would spend {cost} op, taking the run to " + f"{self.spent + cost} against a cap of {self.cap}. Stopping " + f"before the spend, not after." + ) + self.spent += cost + self.by_stage[stage] = self.by_stage.get(stage, 0) + cost + + def record_time(self, stage: str, seconds: float) -> None: + self.timings[stage] = self.timings.get(stage, 0.0) + seconds + + +class SuperDocsClient: + """Live client. Requires SUPERDOCS_API_KEY in the environment.""" + + def __init__(self, ledger: Ledger, poll_interval: float = 3.0, + timeout_s: float = 600.0): + key = os.environ.get("SUPERDOCS_API_KEY") + if not key: + raise ApiError( + "SUPERDOCS_API_KEY is not set. Export it in your shell " + "(and keep it out of shell history with a leading space); " + "the benchmark never takes a key as an argument." + ) + self._key = key + self.ledger = ledger + self.poll_interval = poll_interval + self.timeout_s = timeout_s + + # -- internals --------------------------------------------------------- + @property + def _json_headers(self) -> dict: + return {"Authorization": f"Bearer {self._key}", + "Content-Type": "application/json"} + + @property + def _auth_only(self) -> dict: + return {"Authorization": f"Bearer {self._key}"} + + def _redact(self, text: str) -> str: + return re.sub(r"sk_[A-Za-z0-9_\-]{4,}", "sk_[REDACTED]", text or "") + + def _check(self, resp, what: str): + if resp.status_code >= 400: + raise ApiError( + f"{what} failed with HTTP {resp.status_code}: " + f"{self._redact(resp.text)[:400]}" + ) + return resp + + # -- the four calls ---------------------------------------------------- + def upload(self, path: str, session_id: str) -> str: + import requests + t0 = time.monotonic() + self.ledger.charge("upload") + with open(path, "rb") as fh: + r = requests.post( + f"{BASE}/v1/documents/upload", + headers=self._auth_only, + files={"file": (os.path.basename(path), fh, + "application/vnd.openxmlformats-officedocument." + "wordprocessingml.document")}, + data={"session_id": session_id}, + timeout=self.timeout_s, + ) + self._check(r, "upload") + self.ledger.record_time("upload", time.monotonic() - t0) + html = r.json().get("html") + if not html: + raise ApiError("upload returned no html; cannot proceed honestly") + return html + + def edit(self, session_id: str, document_html: str, instruction: str) -> dict: + """Start an approval-gated edit and poll to the approval point.""" + import requests + t0 = time.monotonic() + self.ledger.charge("chat") + r = requests.post( + f"{BASE}/v1/chat/async", headers=self._json_headers, + json={ + "message": instruction, + "session_id": session_id, + "document_html": document_html, + "approval_mode": "ask_every_time", + }, + timeout=self.timeout_s, + ) + self._check(r, "chat/async") + job_id = r.json()["job_id"] + + deadline = time.monotonic() + self.timeout_s + while time.monotonic() < deadline: + j = requests.get(f"{BASE}/v1/jobs/{job_id}", + headers=self._json_headers, + timeout=self.timeout_s) + self._check(j, "jobs/get") + job = j.json() + status = job.get("status") + if status == "awaiting_approval": + self.ledger.record_time("chat", time.monotonic() - t0) + return {"job_id": job_id, + "pending": self._pending(job), + "status": status} + if status == "completed": + self.ledger.record_time("chat", time.monotonic() - t0) + return {"job_id": job_id, "pending": [], "status": status, + "result": job.get("result")} + if status in ("failed", "cancelled"): + raise ApiError( + f"job {status}: {self._redact(str(job.get('error')))[:300]}" + ) + time.sleep(self.poll_interval) + raise ApiError( + f"job {job_id} did not reach a terminal state within " + f"{self.timeout_s:.0f}s. Long silence is normal on this API, so " + f"this is a timeout, not a proven failure." + ) + + @staticmethod + def _pending(job: dict) -> list[dict]: + """Proposed changes need a second parse. + + The documented gotcha: proposed-change content arrives as a + JSON-encoded string while the final result is already an object. + Missing this is what makes every diff field read as undefined. + """ + changes = (job.get("metadata") or {}).get("pending_changes") or [] + out = [] + for c in changes: + c = dict(c) + content = c.get("content") + if isinstance(content, str): + try: + c["content"] = json.loads(content) + except (ValueError, TypeError): + c["content_parse_failed"] = True + out.append(c) + return out + + def approve(self, session_id: str, job_id: str, changes: list[dict]) -> dict: + import requests + t0 = time.monotonic() + self.ledger.charge("approve") + r = requests.post( + f"{BASE}/v1/chat/{session_id}/approve", + headers=self._json_headers, + json={ + "job_id": job_id, + "approved": True, + "changes": [{"change_id": c.get("change_id"), "approved": True} + for c in changes], + }, + timeout=self.timeout_s, + ) + self._check(r, "approve") + self.ledger.record_time("approve", time.monotonic() - t0) + return r.json() + + def export(self, session_id: str, out_path: str) -> str: + import requests + t0 = time.monotonic() + self.ledger.charge("export") + r = requests.post( + f"{BASE}/v1/documents/export", headers=self._json_headers, + json={"session_id": session_id, "format": "docx"}, + timeout=self.timeout_s, + ) + self._check(r, "export") + os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True) + with open(out_path, "wb") as fh: + fh.write(r.content) + self.ledger.record_time("export", time.monotonic() - t0) + return out_path + + +# -------------------------------------------------------------------------- +# Offline fake +# -------------------------------------------------------------------------- + +FAULTS = ( + "none", + "flatten_merges", # drop rowspan/colspan attributes + "drop_nested", # remove nested tables + "strip_trailing_zero", # 12.480 -> 12.48 + "drop_shading", # remove background colours + "rewrite_body", # touch a paragraph nobody asked about +) + + +class FakeClient: + """Deterministic offline stand-in with injectable, named faults.""" + + def __init__(self, ledger: Ledger, fault: str = "none"): + if fault not in FAULTS: + raise ValueError(f"unknown fault {fault!r}; known: {FAULTS}") + self.ledger = ledger + self.fault = fault + self._sessions: dict[str, str] = {} + self._sim: dict[str, tuple] = {} + + def simulate_edit(self, session_id: str, case) -> None: + """Simulator affordance, offline only. + + Records the change the live model would be asked to make, so the dry + run exercises the real success path instead of asserting failure on + every case. The live client has no such method and the runner only + calls it when the client offers one. + """ + if case.expect_contains is not None: + self._sim[session_id] = (case.target, case.expect_contains) + + def upload(self, path: str, session_id: str) -> str: + from .extract import from_docx + from .htmlout import to_html + self.ledger.charge("upload") + html = to_html(from_docx(path)) + self._sessions[session_id] = html + return html + + def edit(self, session_id: str, document_html: str, instruction: str) -> dict: + self.ledger.charge("chat") + self._sessions[session_id] = document_html + return {"job_id": f"fake-{session_id}", "status": "awaiting_approval", + "pending": [{"change_id": "c1", "operation": "replace", + "content": json.dumps({"instruction": instruction})}]} + + def approve(self, session_id: str, job_id: str, changes: list[dict]) -> dict: + self.ledger.charge("approve") + return {"status": "applied", "applied": len(changes)} + + def export(self, session_id: str, out_path: str) -> str: + from .extract import from_html + from .corpus.generate import write_docx + from .htmlout import doc_to_case + self.ledger.charge("export") + doc = from_html(self._damage(self._sessions.get(session_id, ""))) + sim = self._sim.get(session_id) + if sim: + (ti, r, c), text = sim + if ti < len(doc.tables): + cell = doc.tables[ti].cell_at(r, c) + if cell is not None: + if cell.nested: + cell.nested[0].cells[-1].paragraphs = [text] + else: + cell.paragraphs = [text] + write_docx(doc_to_case(doc), out_path) + return out_path + + def _damage(self, html: str) -> str: + f = self.fault + if f == "none": + return html + if f == "flatten_merges": + return re.sub(r'\s(rowspan|colspan)="\d+"', "", html) + if f == "drop_nested": + return re.sub(r"]*>(?:(?!
).)*?\s*(?=)", + "", html, flags=re.S) + if f == "strip_trailing_zero": + return re.sub(r"(\d+\.\d*?)0+(?=\D|$)", r"\1", html) + if f == "drop_shading": + return re.sub(r"background-color:[^;\"]*;?", "", html) + if f == "rewrite_body": + return html.replace("Prepared for benchmark purposes.", + "Prepared automatically.") + return html diff --git a/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/compare.py b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/compare.py new file mode 100644 index 00000000..28c98b7b --- /dev/null +++ b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/compare.py @@ -0,0 +1,243 @@ +"""Compare two canonical Documents and report exactly what differs. + +Two rules govern this module. + +1. The edit target is exempt from content comparison and nothing else is. A + round-trip that changes a cell nobody asked it to change is a finding even + if the document still looks fine. + +2. A finding is never asserted where it cannot be observed. If a stage did not + run, the case is UNVERIFIABLE, not a failure. "Broken" and "not measured" + are different words here and the report keeps them apart. +""" + +from __future__ import annotations + +from dataclasses import dataclass, asdict + +from .model import Cell, Document, Table + +STRUCTURAL = "structural" +CONTENT = "content" +FORMATTING = "formatting" + +# Verdicts +PASS = "PASS" +FAIL = "FAIL" +UNVERIFIABLE = "UNVERIFIABLE" +ERROR = "ERROR" + + +@dataclass(frozen=True) +class Finding: + severity: str + code: str + where: str + expected: str + actual: str + + def key(self) -> tuple: + return (self.severity, self.code, self.where) + + def to_dict(self) -> dict: + return asdict(self) + + def __str__(self) -> str: + return f"[{self.severity}/{self.code}] {self.where}: expected {self.expected!r}, got {self.actual!r}" + + +def _norm(v) -> str: + if v is None: + return "" + return str(v).strip() + + +def _norm_text(s: str) -> str: + """Whitespace-insensitive but character-exact otherwise. + + Deliberately does NOT normalise unicode: an em dash turning into a hyphen, + or a non-breaking space into a space, is a finding, not noise. + """ + return " ".join(s.split()) + + +def _cmp_cell(a: Cell, b: Cell, where: str, exempt: bool, + out: list[Finding]) -> None: + if a.row_span != b.row_span: + out.append(Finding(STRUCTURAL, "row_span", where, + str(a.row_span), str(b.row_span))) + if a.col_span != b.col_span: + out.append(Finding(STRUCTURAL, "col_span", where, + str(a.col_span), str(b.col_span))) + + if not exempt: + at, bt = _norm_text(a.text), _norm_text(b.text) + if at != bt: + out.append(Finding(CONTENT, "cell_text", where, at, bt)) + if len(a.paragraphs) != len(b.paragraphs): + out.append(Finding(CONTENT, "paragraph_count", where, + str(len(a.paragraphs)), str(len(b.paragraphs)))) + + for attr, code in ( + ("alignment", "alignment"), + ("shading", "shading"), + ("valign", "valign"), + ("text_direction", "text_direction"), + ): + av, bv = _norm(getattr(a, attr)), _norm(getattr(b, attr)) + if av != bv: + out.append(Finding(FORMATTING, code, where, av, bv)) + + if a.width_dxa is not None and b.width_dxa is not None: + if a.width_dxa != b.width_dxa: + out.append(Finding(FORMATTING, "cell_width", where, + str(a.width_dxa), str(b.width_dxa))) + + if len(a.nested) != len(b.nested): + out.append(Finding(STRUCTURAL, "nested_table_count", where, + str(len(a.nested)), str(len(b.nested)))) + else: + for i, (na, nb) in enumerate(zip(a.nested, b.nested)): + _cmp_table(na, nb, f"{where}>nested[{i}]", exempt, out) # noqa + + +def _cmp_table(a: Table, b: Table, where: str, exempt_all: bool, + out: list[Finding], + targets: list[tuple[int, int]] | None = None) -> None: + if a.n_rows != b.n_rows: + out.append(Finding(STRUCTURAL, "row_count", where, + str(a.n_rows), str(b.n_rows))) + if a.n_cols != b.n_cols: + out.append(Finding(STRUCTURAL, "col_count", where, + str(a.n_cols), str(b.n_cols))) + + occ_a, occ_b = a.occupancy(), b.occupancy() + if occ_a != occ_b: + diff = sorted(set(occ_a) ^ set(occ_b))[:4] + mism = [k for k in sorted(set(occ_a) & set(occ_b)) if occ_a[k] != occ_b[k]][:4] + out.append(Finding( + STRUCTURAL, "grid_topology", where, + f"{len(occ_a)} covered cells", + f"{len(occ_b)} covered cells; missing/extra={diff}; remapped={mism}", + )) + + if a.header_rows != b.header_rows: + out.append(Finding(FORMATTING, "header_rows", where, + str(a.header_rows), str(b.header_rows))) + if a.col_widths and b.col_widths and a.col_widths != b.col_widths: + out.append(Finding(FORMATTING, "col_widths", where, + str(a.col_widths), str(b.col_widths))) + if _norm(a.layout) != _norm(b.layout): + out.append(Finding(FORMATTING, "table_layout", where, + _norm(a.layout), _norm(b.layout))) + + ea, eb = a.border_edges(), b.border_edges() + for key in sorted(set(ea) | set(eb)): + av, bv = _norm(ea.get(key)), _norm(eb.get(key)) + if av != bv: + kind = "row rule above" if key[0] == "h" else "column rule left of" + out.append(Finding(FORMATTING, "border_edge", + f"{where} {kind} ({key[1]}, {key[2]})", av, bv)) + + anchors_a, anchors_b = a.anchors(), b.anchors() + for pos in sorted(set(anchors_a) | set(anchors_b)): + ca, cb = anchors_a.get(pos), anchors_b.get(pos) + cw = f"{where} cell{pos}" + if ca is None: + out.append(Finding(STRUCTURAL, "unexpected_cell", cw, "-", + _norm_text(cb.text)[:60])) + continue + if cb is None: + out.append(Finding(STRUCTURAL, "missing_cell", cw, + _norm_text(ca.text)[:60], "-")) + continue + exempt = exempt_all + for t in (targets or []): + anchor = a.cell_at(*t) + if anchor is not None and (anchor.row, anchor.col) == pos: + exempt = True + break + _cmp_cell(ca, cb, cw, exempt, out) + + +# Attributes that plain HTML has no standard slot for. When the stage under +# comparison is the HTML intermediate, their absence is unmeasurable, not +# proven lost -- so they are withheld from findings and reported separately. +HTML_UNREPRESENTABLE = frozenset({ + "col_widths", "cell_width", "table_layout", + "text_direction", "border_edge", +}) +# header_rows was in this set until the API's response turned out to carry a +# real . It is representable, so withholding it hid the useful fact: +# the header marking survives ingest and is lost on export. + + +def compare(before: Document, after: Document, + target=None, profile: str = "docx") -> list[Finding]: + """target is (table_index, row, col), or a list of them, exempt from + content checks. Every cell not named is an invariant. + + profile='html' withholds findings for attributes HTML cannot express. + """ + out: list[Finding] = [] + if target is None: + targets: list[tuple[int, int, int]] = [] + elif isinstance(target, tuple) and target and isinstance(target[0], int): + targets = [target] + else: + targets = list(target) + + if len(before.tables) != len(after.tables): + out.append(Finding(STRUCTURAL, "table_count", "document", + str(len(before.tables)), str(len(after.tables)))) + + for i, (ta, tb) in enumerate(zip(before.tables, after.tables)): + tgts = [(t[1], t[2]) for t in targets if t[0] == i] + _cmp_table(ta, tb, f"table[{i}]", False, out, tgts) + + a_paras = [_norm_text(p) for p in before.body_paragraphs] + b_paras = [_norm_text(p) for p in after.body_paragraphs] + if a_paras != b_paras: + missing = [p for p in a_paras if p not in b_paras][:3] + out.append(Finding(CONTENT, "body_paragraphs", "document", + f"{len(a_paras)} paragraphs", + f"{len(b_paras)} paragraphs; missing={missing}")) + + if profile == "html": + out = [f for f in out if f.code not in HTML_UNREPRESENTABLE] + return out + + +def unmeasurable_at_html_stage(findings: list[Finding]) -> list[str]: + """Codes withheld under the html profile, for honest reporting.""" + return sorted({f.code for f in findings if f.code in HTML_UNREPRESENTABLE}) + + +def attribute(edit_findings: list[Finding], + control_findings: list[Finding]) -> tuple[list[Finding], list[Finding]]: + """Split edit-run findings into (caused_by_edit, caused_by_round_trip). + + A finding that also occurs on a no-op control run is a property of ingest + and export, not of the edit. Separating these is the difference between + 'the editor broke my table' and 'the file format round-trip did'. + """ + control_keys = {f.key() for f in control_findings} + by_edit = [f for f in edit_findings if f.key() not in control_keys] + by_round_trip = [f for f in edit_findings if f.key() in control_keys] + return by_edit, by_round_trip + + +def verdict(edit_applied: bool | None, findings: list[Finding]) -> str: + if edit_applied is None: + return UNVERIFIABLE + if not edit_applied: + return FAIL + return PASS if not findings else FAIL + + +def summarise(findings: list[Finding]) -> dict[str, int]: + counts: dict[str, int] = {} + for f in findings: + counts[f.severity] = counts.get(f.severity, 0) + 1 + counts[f"{f.severity}:{f.code}"] = counts.get(f"{f.severity}:{f.code}", 0) + 1 + return counts diff --git a/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/corpus/__init__.py b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/corpus/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/corpus/cases.py b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/corpus/cases.py new file mode 100644 index 00000000..6012b82f --- /dev/null +++ b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/corpus/cases.py @@ -0,0 +1,556 @@ +"""The pathology catalogue. + +Cases are data, not code. Adding a pathology is a new entry here; it needs no +change to the generator, the extractor, the comparator or the runner. + +Each case names one cell as the edit target. Everything else in the document +is an invariant: after the edit round-trip it must come back unchanged. That +asymmetry is the whole measurement -- a system that mangles nothing but also +changes nothing is not passing, and neither is one that makes the edit and +flattens the table around it. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +AXES = ("merged_cells", "nested_tables", "spanning_headers", "numeric_formatting") + +HDR = "D9E2F3" +ALT = "F2F2F2" + + +@dataclass +class Case: + id: str + title: str + axis: str + edit: str + target: tuple[int, int, int] # (table index, row, col) in the grid + also_changes: list[tuple[int, int, int]] = field(default_factory=list) + """Other cells the instruction legitimately asks to change. + + An instruction that names two figures must exempt both. Declaring only one + makes the benchmark report the model for obeying it, which is a false + finding and worse than no finding at all.""" + expect_contains: str | None = None # proof the edit actually landed + intro: str = "" + tables: list[dict] = field(default_factory=list) + trailing_paragraphs: list[str] = field(default_factory=list) + notes: str = "" + + +def _row(*cells) -> list[dict]: + out = [] + for c in cells: + out.append({"text": c} if isinstance(c, str) else c) + return out + + +CASES: list[Case] = [ + + # ---------------- merged cells ---------------- + Case( + id="merge_horizontal_header", + title="Horizontal merge across a header", + axis="merged_cells", + notes="A single header cell spanning three data columns.", + tables=[{ + "header_rows": [0], + "rows": [ + _row({"text": "Region", "rowspan": 2, "fill": HDR, "bold": True}, + {"text": "Quarterly revenue", "colspan": 3, "fill": HDR, "bold": True, + "align": "center"}), + _row({"text": "Q1", "fill": HDR, "bold": True}, + {"text": "Q2", "fill": HDR, "bold": True}, + {"text": "Q3", "fill": HDR, "bold": True}), + _row("North", "1,204.50", "1,388.00", "1,401.25"), + _row("South", "980.00", "1,012.75", "1,150.00"), + ], + }], + edit="In the first table, change the Q2 value on the South row to 1,099.40. " + "Change nothing else.", + target=(0, 3, 2), + expect_contains="1,099.40", + ), + + Case( + id="merge_vertical_label", + title="Vertical merge on a stub column", + axis="merged_cells", + notes="A row label merged down three body rows.", + tables=[{ + "rows": [ + _row({"text": "Division", "fill": HDR, "bold": True}, + {"text": "Line item", "fill": HDR, "bold": True}, + {"text": "Amount", "fill": HDR, "bold": True, "align": "right"}), + _row({"text": "Upstream", "rowspan": 3, "valign": "center"}, + "Drilling", {"text": "4,210.00", "align": "right"}), + _row("Completion", {"text": "1,875.50", "align": "right"}), + _row("Logistics", {"text": "962.25", "align": "right"}), + _row({"text": "Downstream", "rowspan": 2, "valign": "center"}, + "Refining", {"text": "7,430.00", "align": "right"}), + _row("Distribution", {"text": "2,118.75", "align": "right"}), + ], + }], + edit="Change the Logistics amount to 1,004.00. Leave every other cell alone.", + target=(0, 3, 2), + expect_contains="1,004.00", + ), + + Case( + id="merge_l_shaped", + title="L-shaped merge: row span and column span in one table", + axis="merged_cells", + notes="Vertical and horizontal merges interlocking; the classic case " + "that collapses to a flat grid when a converter resolves spans naively.", + tables=[{ + "rows": [ + _row({"text": "Asset", "rowspan": 2, "fill": HDR, "bold": True}, + {"text": "Planned", "colspan": 2, "fill": HDR, "bold": True, + "align": "center"}, + {"text": "Actual", "rowspan": 2, "fill": HDR, "bold": True}), + _row({"text": "Start", "fill": HDR}, {"text": "Finish", "fill": HDR}), + _row("Platform A", "2026-01-04", "2026-02-18", "2026-02-25"), + _row({"text": "Shutdown window", "colspan": 4, "fill": ALT, + "align": "center", "bold": True}), + _row("Platform B", "2026-03-01", "2026-04-12", "2026-04-09"), + ], + }], + edit="Change the Actual date for Platform A to 2026-03-02. Change nothing else.", + target=(0, 2, 3), + expect_contains="2026-03-02", + ), + + Case( + id="merge_full_row_divider", + title="Full-width merged row used as a section divider", + axis="merged_cells", + tables=[{ + "rows": [ + _row({"text": "Item", "fill": HDR, "bold": True}, + {"text": "Qty", "fill": HDR, "bold": True}, + {"text": "Unit", "fill": HDR, "bold": True}, + {"text": "Total", "fill": HDR, "bold": True}), + _row({"text": "Section 1 - Mechanical", "colspan": 4, "fill": ALT, + "bold": True}), + _row("Gasket set", "12", "18.40", "220.80"), + _row({"text": "Section 2 - Electrical", "colspan": 4, "fill": ALT, + "bold": True}), + _row("Cable tray", "40", "31.00", "1,240.00"), + ], + }], + edit="Change the quantity of Cable tray to 46 and its total to 1,426.00.", + target=(0, 4, 1), + also_changes=[(0, 4, 3)], + expect_contains="46", + ), + + Case( + id="merge_containing_nested", + title="Nested table inside a merged cell", + axis="merged_cells", + notes="Merge and nesting combined -- each is survivable alone; together " + "they are where most converters give up.", + tables=[{ + "col_widths": [1800, 2600], + "rows": [ + _row({"text": "Package", "fill": HDR, "bold": True}, + {"text": "Breakdown", "fill": HDR, "bold": True}), + _row({"text": "Turnaround 2026", "rowspan": 2, "valign": "center"}, + {"text": "", "nested": { + "col_widths": [1300, 1100], + "rows": [ + _row({"text": "Phase", "bold": True}, + {"text": "Days", "bold": True}), + _row("Prep", "9"), + _row("Execution", "21"), + ], + }}), + _row("Contingency 4 days"), + ], + }], + edit="In the nested breakdown table, change Execution from 21 to 24 days.", + target=(0, 1, 1), + expect_contains="24", + ), + + # ---------------- nested tables ---------------- + Case( + id="nested_simple", + title="Table inside a cell", + axis="nested_tables", + tables=[{ + "col_widths": [2000, 2600], + "rows": [ + _row({"text": "Vendor", "fill": HDR, "bold": True}, + {"text": "Rates", "fill": HDR, "bold": True}), + _row("Halberd Services", {"text": "", "nested": { + "col_widths": [1400, 1000], + "rows": [ + _row("Day rate", "1,450.00"), + _row("Standby", "610.00"), + ], + }}), + ], + }], + edit="Change the standby rate in the nested table to 645.00.", + target=(0, 1, 1), + expect_contains="645.00", + ), + + Case( + id="nested_two_deep", + title="Two levels of nesting", + axis="nested_tables", + notes="Depth is where chunk-id schemes usually stop addressing cells.", + tables=[{ + "col_widths": [1800, 3400], + "rows": [ + _row({"text": "Contract", "fill": HDR, "bold": True}, + {"text": "Structure", "fill": HDR, "bold": True}), + _row("MSA-2026-11", {"text": "", "nested": { + "col_widths": [1200, 2000], + "rows": [ + _row("Schedule A", {"text": "", "nested": { + "col_widths": [900, 900], + "rows": [ + _row("Tier 1", "0.85"), + _row("Tier 2", "0.72"), + ], + }}), + _row("Schedule B", "See annex"), + ], + }}), + ], + }], + edit="In the innermost table, change the Tier 2 value to 0.70.", + target=(0, 1, 1), + expect_contains="0.70", + ), + + Case( + id="nested_side_by_side", + title="Two nested tables in adjacent cells of one row", + axis="nested_tables", + tables=[{ + "col_widths": [2400, 2400], + "rows": [ + _row({"text": "Before", "fill": HDR, "bold": True}, + {"text": "After", "fill": HDR, "bold": True}), + _row({"text": "", "nested": { + "col_widths": [1100, 1100], + "rows": [_row("Flow", "412"), _row("Pressure", "88.4")], + }}, + {"text": "", "nested": { + "col_widths": [1100, 1100], + "rows": [_row("Flow", "455"), _row("Pressure", "91.0")], + }}), + ], + }], + edit="In the After table, change Pressure to 92.6.", + target=(0, 1, 1), + expect_contains="92.6", + ), + + # ---------------- spanning headers ---------------- + Case( + id="header_two_level", + title="Two-level spanning header", + axis="spanning_headers", + tables=[{ + "header_rows": [0, 1], + "rows": [ + _row({"text": "Well", "rowspan": 2, "fill": HDR, "bold": True}, + {"text": "Production", "colspan": 2, "fill": HDR, "bold": True, + "align": "center"}, + {"text": "Downtime", "colspan": 2, "fill": HDR, "bold": True, + "align": "center"}), + _row({"text": "Oil", "fill": HDR}, {"text": "Gas", "fill": HDR}, + {"text": "Planned", "fill": HDR}, {"text": "Unplanned", "fill": HDR}), + _row("A-14", "1,240", "3,880", "6.0", "1.5"), + _row("A-15", "980", "2,410", "4.5", "0.0"), + ], + }], + edit="Change the unplanned downtime for A-15 to 2.5.", + target=(0, 3, 4), + expect_contains="2.5", + ), + + Case( + id="header_three_level", + title="Three-level spanning header", + axis="spanning_headers", + notes="Three header tiers with spans at two of them.", + tables=[{ + "header_rows": [0, 1, 2], + "col_widths": [1500, 1100, 1100, 1100, 1100], + "rows": [ + _row({"text": "Facility", "rowspan": 3, "fill": HDR, "bold": True}, + {"text": "2026", "colspan": 4, "fill": HDR, "bold": True, + "align": "center"}), + _row({"text": "H1", "colspan": 2, "fill": HDR, "align": "center"}, + {"text": "H2", "colspan": 2, "fill": HDR, "align": "center"}), + _row({"text": "Q1", "fill": HDR}, {"text": "Q2", "fill": HDR}, + {"text": "Q3", "fill": HDR}, {"text": "Q4", "fill": HDR}), + _row("Terminal 1", "18.2", "19.0", "17.4", "20.1"), + _row("Terminal 2", "11.9", "12.3", "12.0", "13.8"), + ], + }], + edit="Change the Q3 figure for Terminal 1 to 18.9.", + target=(0, 3, 3), + expect_contains="18.9", + ), + + Case( + id="header_repeat_flag", + title="Repeating header rows across a long table", + axis="spanning_headers", + notes="w:tblHeader on two rows; the repeat flag is invisible on page one " + "and silently dropped by most round-trips.", + tables=[{ + "header_rows": [0, 1], + "rows": [ + _row({"text": "Tag", "rowspan": 2, "fill": HDR, "bold": True}, + {"text": "Readings", "colspan": 2, "fill": HDR, "bold": True, + "align": "center"}), + _row({"text": "Min", "fill": HDR}, {"text": "Max", "fill": HDR}), + ] + [ + _row(f"PT-{100 + i}", f"{2.0 + i * 0.13:.2f}", f"{9.0 + i * 0.21:.2f}") + for i in range(28) + ], + }], + edit="Change the Max reading on the PT-105 row to 12.00.", + target=(0, 7, 2), + expect_contains="12.00", + ), + + Case( + id="header_rotated_text", + title="Rotated header cells", + axis="spanning_headers", + tables=[{ + "header_rows": [0], + "col_widths": [1800, 700, 700, 700], + "rows": [ + _row({"text": "Component", "fill": HDR, "bold": True}, + {"text": "Inspected", "fill": HDR, "dir": "btLr", "valign": "bottom"}, + {"text": "Cleaned", "fill": HDR, "dir": "btLr", "valign": "bottom"}, + {"text": "Replaced", "fill": HDR, "dir": "btLr", "valign": "bottom"}), + _row("Seal ring", "Y", "Y", "N"), + _row("Impeller", "Y", "N", "N"), + ], + }], + edit="On the Impeller row, change Replaced from N to Y.", + target=(0, 2, 3), + expect_contains="Y", + ), + + # ---------------- numeric formatting ---------------- + Case( + id="num_thousands_currency", + title="Thousands separators and currency symbols", + axis="numeric_formatting", + tables=[{ + "rows": [ + _row({"text": "Account", "fill": HDR, "bold": True}, + {"text": "Opening", "fill": HDR, "bold": True, "align": "right"}, + {"text": "Closing", "fill": HDR, "bold": True, "align": "right"}), + _row("Operating", {"text": "$1,204,880.00", "align": "right"}, + {"text": "$1,190,455.20", "align": "right"}), + _row("Reserve", {"text": "$88,000.00", "align": "right"}, + {"text": "$92,500.00", "align": "right"}), + _row("Escrow", {"text": "$0.00", "align": "right"}, + {"text": "$15,000.00", "align": "right"}), + ], + }], + edit="Change the Reserve closing balance to $94,750.00.", + target=(0, 2, 2), + expect_contains="$94,750.00", + ), + + Case( + id="num_negative_parentheses", + title="Negatives in parentheses and em-dash zeros", + axis="numeric_formatting", + notes="Accounting convention: (1,234.00) means negative, an em dash " + "means nil. A model that normalizes these has changed the meaning.", + tables=[{ + "rows": [ + _row({"text": "Line", "fill": HDR, "bold": True}, + {"text": "Variance", "fill": HDR, "bold": True, "align": "right"}), + _row("Labour", {"text": "(12,400.00)", "align": "right"}), + _row("Materials", {"text": "3,150.00", "align": "right"}), + _row("Freight", {"text": "\u2014", "align": "right"}), + _row("Permits", {"text": "(875.50)", "align": "right"}), + ], + }], + edit="Change the Materials variance to 3,480.00.", + target=(0, 2, 1), + expect_contains="3,480.00", + ), + + Case( + id="num_trailing_zeros", + title="Significant trailing zeros and fixed precision", + axis="numeric_formatting", + notes="1.230 is not 1.23 in an instrument reading.", + tables=[{ + "rows": [ + _row({"text": "Instrument", "fill": HDR, "bold": True}, + {"text": "Reading", "fill": HDR, "bold": True, "align": "right"}, + {"text": "Tolerance", "fill": HDR, "bold": True, "align": "right"}), + _row("FT-201", {"text": "1.230", "align": "right"}, + {"text": "\u00b10.005", "align": "right"}), + _row("FT-202", {"text": "12.500", "align": "right"}, + {"text": "\u00b10.010", "align": "right"}), + _row("FT-203", {"text": "0.900", "align": "right"}, + {"text": "\u00b10.050", "align": "right"}), + ], + }], + edit="Change the FT-202 reading to 12.480.", + target=(0, 2, 1), + expect_contains="12.480", + ), + + Case( + id="num_leading_zero_ids", + title="Leading-zero identifiers that must not become integers", + axis="numeric_formatting", + tables=[{ + "rows": [ + _row({"text": "Part no.", "fill": HDR, "bold": True}, + {"text": "Batch", "fill": HDR, "bold": True}, + {"text": "Qty", "fill": HDR, "bold": True, "align": "right"}), + _row("0041-77", "0007", {"text": "18", "align": "right"}), + _row("0100-02", "0012", {"text": "6", "align": "right"}), + _row("9000-01", "0100", {"text": "44", "align": "right"}), + ], + }], + edit="Change the quantity on the 0100-02 row to 9.", + target=(0, 2, 2), + expect_contains="9", + ), + + Case( + id="num_percent_scientific", + title="Percentages, scientific notation and units", + axis="numeric_formatting", + tables=[{ + "rows": [ + _row({"text": "Metric", "fill": HDR, "bold": True}, + {"text": "Value", "fill": HDR, "bold": True, "align": "right"}), + _row("Availability", {"text": "99.87%", "align": "right"}), + _row("Leak rate", {"text": "3.2\u00d710\u207b\u2076 mbar\u00b7L/s", + "align": "right"}), + _row("Efficiency", {"text": "0.4%", "align": "right"}), + _row("Throughput", {"text": "1.4 \u00d7 10\u2076 m\u00b3/d", "align": "right"}), + ], + }], + edit="Change Availability to 99.92%.", + target=(0, 1, 1), + expect_contains="99.92%", + ), + + Case( + id="num_mixed_date_formats", + title="Mixed date and time formats in one column", + axis="numeric_formatting", + tables=[{ + "rows": [ + _row({"text": "Event", "fill": HDR, "bold": True}, + {"text": "Timestamp", "fill": HDR, "bold": True}), + _row("Trip", "2026-03-04 08:12:47Z"), + _row("Restart", "04/03/2026 09:05"), + _row("Report filed", "4 March 2026"), + ], + }], + edit="Change the Restart timestamp to 04/03/2026 09:40.", + target=(0, 2, 1), + expect_contains="09:40", + ), + + # ---------------- structure and invariance ---------------- + Case( + id="struct_wide_fixed_layout", + title="Wide table with fixed layout and explicit column widths", + axis="merged_cells", + notes="Column widths are the quietest thing to lose; nothing looks " + "broken, the table is just no longer the shape it was.", + tables=[{ + "layout": "fixed", + "col_widths": [1600, 700, 700, 700, 700, 700, 700, 900], + "header_rows": [0], + "rows": [ + _row({"text": "Line", "fill": HDR, "bold": True}, + *[{"text": m, "fill": HDR, "bold": True, "align": "center"} + for m in ("Jan", "Feb", "Mar", "Apr", "May", "Jun")], + {"text": "YTD", "fill": HDR, "bold": True, "align": "right"}), + _row("Volume", "412", "398", "444", "460", "451", "470", + {"text": "2,635", "align": "right"}), + _row("Downtime", "6.0", "2.5", "0.0", "1.5", "3.0", "0.5", + {"text": "13.5", "align": "right"}), + ], + }], + edit="Change the March volume to 448 and the YTD volume to 2,639.", + target=(0, 1, 3), + also_changes=[(0, 1, 7)], + expect_contains="448", + ), + + Case( + id="struct_shading_borders", + title="Alternating row shading with per-cell borders", + axis="merged_cells", + tables=[{ + "header_rows": [0], + "rows": [ + _row({"text": "Check", "fill": HDR, "bold": True}, + {"text": "Result", "fill": HDR, "bold": True}, + {"text": "Signed", "fill": HDR, "bold": True}), + _row({"text": "Isolation verified", "fill": ALT}, + {"text": "Pass", "fill": ALT, + "borders": {"bottom": ("18", "2E7D32")}}, + {"text": "RK", "fill": ALT}), + _row("Gas test", {"text": "Pass", + "borders": {"bottom": ("18", "2E7D32")}}, "RK"), + _row({"text": "Pressure hold", "fill": ALT}, + {"text": "Fail", "fill": ALT, + "borders": {"bottom": ("18", "C62828")}}, + {"text": "RK", "fill": ALT}), + ], + }], + edit="Change the Pressure hold result from Fail to Pass.", + target=(0, 3, 1), + expect_contains="Pass", + ), +] + + +# Every case gets the same surrounding prose. If a round-trip rewrites body +# text that was never mentioned, that is a finding too. +for _c in CASES: + _c.intro = _c.intro or ( + "The table below is a fixture. Only the cell named in the instruction " + "may change." + ) + if not _c.trailing_paragraphs: + _c.trailing_paragraphs = [ + "Prepared for benchmark purposes. All figures are fabricated.", + "Reference: TFB/" + _c.id.upper(), + ] + + +BY_ID = {c.id: c for c in CASES} + + +def select(ids: list[str] | None = None, axis: str | None = None, + limit: int | None = None) -> list[Case]: + out = CASES + if ids: + out = [BY_ID[i] for i in ids] + if axis: + out = [c for c in out if c.axis == axis] + if limit is not None: + out = out[:limit] + return out diff --git a/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/corpus/generate.py b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/corpus/generate.py new file mode 100644 index 00000000..2b47f447 --- /dev/null +++ b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/corpus/generate.py @@ -0,0 +1,215 @@ +"""Declarative table spec -> .docx. + +Written against raw WordprocessingML rather than python-docx because the +benchmark needs exact control over vMerge, gridSpan, tblHeader and +textDirection -- the very constructs under test. A generator that could not +express a pathology could not test for it. +""" + +from __future__ import annotations + +import os +import zipfile +from xml.sax.saxutils import escape + +W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + +CONTENT_TYPES = """ + + + + + +""" + +ROOT_RELS = """ + + +""" + +DOC_RELS = """ + + +""" + +STYLES = f""" + + + + +""" + +DEFAULT_COL_DXA = 1800 + + +def _p(text: str, align: str | None = None, bold: bool = False) -> str: + ppr = f'' if align else "" + rpr = "" if bold else "" + body = "" + if text: + body = ( + f'{rpr}{escape(text)}' + ) + elif bold: + body = f"{rpr}" + return f"{ppr}{body}" + + +def _tc_pr(cell: dict, span: int, vmerge: str | None, width: int) -> str: + parts = [f''] + if span > 1: + parts.append(f'') + if vmerge == "restart": + parts.append('') + elif vmerge == "continue": + parts.append("") + if cell.get("fill"): + parts.append( + f'' + ) + if cell.get("valign"): + parts.append(f'') + if cell.get("dir"): + parts.append(f'') + if cell.get("borders"): + edges = "".join( + f'' + for e, (sz, col) in cell["borders"].items() + ) + parts.append(f"{edges}") + return "" + "".join(parts) + "" + + +def _cell_body(cell: dict) -> str: + out = [] + paras = cell.get("paragraphs") + if paras is None: + paras = [cell.get("text", "")] + align = cell.get("align") + bold = bool(cell.get("bold")) + for text in paras: + out.append(_p(text, align, bold)) + if cell.get("nested"): + out.append(_table_xml(cell["nested"])) + # OOXML requires a paragraph after a nested table inside a cell. + out.append(_p("")) + return "".join(out) + + +def _place(rows: list[list[dict]]): + """Resolve the grid. Returns (placements, n_cols). + + placements[r] is an ordered list of (cell, col, span, vmerge). + """ + occ: dict[tuple[int, int], dict] = {} + for r, row in enumerate(rows): + c = 0 + for cell in row: + while (r, c) in occ: + c += 1 + span = int(cell.get("colspan", 1)) + rspan = int(cell.get("rowspan", 1)) + cell["_r"], cell["_c"] = r, c + for rr in range(rspan): + for kk in range(span): + occ[(r + rr, c + kk)] = cell + c += span + + n_rows = max((cell["_r"] + int(cell.get("rowspan", 1)) + for row in rows for cell in row), default=0) + n_cols = max((cell["_c"] + int(cell.get("colspan", 1)) + for row in rows for cell in row), default=0) + + placements: list[list[tuple[dict, int, int, str | None]]] = [] + for r in range(n_rows): + line = [] + c = 0 + while c < n_cols: + cell = occ.get((r, c)) + if cell is None: + c += 1 + continue + if cell["_c"] != c: + c += 1 + continue + span = int(cell.get("colspan", 1)) + rspan = int(cell.get("rowspan", 1)) + if rspan > 1: + vmerge = "restart" if cell["_r"] == r else "continue" + else: + vmerge = None + line.append((cell, c, span, vmerge)) + c += span + placements.append(line) + return placements, n_cols + + +def _table_xml(spec: dict) -> str: + rows = spec["rows"] + placements, n_cols = _place(rows) + widths = spec.get("col_widths") or [DEFAULT_COL_DXA] * n_cols + total = sum(widths) + + grid = "".join(f'' for w in widths) + layout = spec.get("layout", "fixed") + tbl_pr = ( + "" + f'' + f'' + "" + '' + '' + '' + '' + '' + '' + "" + "" + ) + + header_rows = set(spec.get("header_rows", [])) + body = [] + for r, line in enumerate(placements): + tr_pr = "" if r in header_rows else "" + tcs = [] + for cell, col, span, vmerge in line: + width = sum(widths[col:col + span]) if col + span <= len(widths) else DEFAULT_COL_DXA + if vmerge == "continue": + inner = _p("") + else: + inner = _cell_body(cell) + tcs.append(f"{_tc_pr(cell, span, vmerge, width)}{inner}") + body.append(f"{tr_pr}{''.join(tcs)}") + + return f"{tbl_pr}{grid}{''.join(body)}" + + +def build_document_xml(case) -> str: + parts = [_p(case.title, bold=True)] + if case.intro: + parts.append(_p(case.intro)) + for spec in case.tables: + parts.append(_table_xml(spec)) + parts.append(_p("")) + for tail in case.trailing_paragraphs: + parts.append(_p(tail)) + sect = ( + '' + '' + ) + return ( + '' + f'{"".join(parts)}{sect}' + ) + + +def write_docx(case, out_path: str) -> str: + os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True) + xml = build_document_xml(case) + with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as z: + z.writestr("[Content_Types].xml", CONTENT_TYPES) + z.writestr("_rels/.rels", ROOT_RELS) + z.writestr("word/document.xml", xml) + z.writestr("word/_rels/document.xml.rels", DOC_RELS) + z.writestr("word/styles.xml", STYLES) + return out_path diff --git a/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/extract.py b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/extract.py new file mode 100644 index 00000000..e93e1ea2 --- /dev/null +++ b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/extract.py @@ -0,0 +1,399 @@ +"""Extractors: DOCX -> Document, HTML -> Document. + +Both target the same canonical model so any two stages of the pipeline are +directly comparable. + +DOCX is parsed from raw WordprocessingML rather than through python-docx's +table API, because that API repeats merged cells instead of reporting spans, +which is precisely the thing under test here. +""" + +from __future__ import annotations + +import zipfile + +from lxml import etree + +from .model import Cell, Document, Table + +W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" +NS = {"w": W} + + +def _q(tag: str) -> str: + return f"{{{W}}}{tag}" + + +def _val(el, default=None): + if el is None: + return default + return el.get(_q("val"), default) + + +# -------------------------------------------------------------------------- +# DOCX +# -------------------------------------------------------------------------- + +def _para_text(p) -> str: + """Text of one w:p, preserving tabs and explicit breaks.""" + out: list[str] = [] + for node in p.iter(): + tag = etree.QName(node).localname + if tag == "t": + out.append(node.text or "") + elif tag == "tab": + out.append("\t") + elif tag == "br": + out.append("\n") + return "".join(out) + + +def _cell_paragraphs(tc) -> list[str]: + """Paragraphs directly in this cell -- not those inside a nested table.""" + return [_para_text(p) for p in tc.findall(f"{_q('p')}")] + + +def _border_spec(e) -> str: + return "{}/{}/{}".format( + e.get(_q("val"), ""), e.get(_q("sz"), ""), (e.get(_q("color")) or "").upper() + ) + + +def _table_borders(tbl) -> dict[str, str]: + """The table's own border declaration, by edge name.""" + tbl_pr = tbl.find(_q("tblPr")) + if tbl_pr is None: + return {} + bdr = tbl_pr.find(_q("tblBorders")) + if bdr is None: + return {} + return {etree.QName(e).localname: _border_spec(e) for e in bdr} + + +def _borders(pr) -> dict[str, str]: + """Borders declared on the cell itself. May be empty.""" + if pr is None: + return {} + bdr = pr.find(_q("tcBorders")) + if bdr is None: + return {} + out = {} + for edge in ("top", "left", "bottom", "right"): + e = bdr.find(_q(edge)) + if e is not None: + out[edge] = _border_spec(e) + return out + + +def _resolve_effective_borders(table: "Table", tbl_borders: dict[str, str]) -> None: + """Fold table-level borders into every cell, in place. + + A round-trip is free to move a border from the table element onto each + cell: the file changes, the rendered document does not. Comparing declared + borders would call that re-expression a loss and bury the real findings + under it. Comparing what each edge actually resolves to -- cell override + first, then the table's outer edge or inner rule by position -- compares + the document instead of the markup. + """ + if not tbl_borders and not any(c.borders for c in table.cells): + return + for c in table.cells: + eff = {} + at_top = c.row == 0 + at_left = c.col == 0 + at_bottom = c.row + c.row_span >= table.n_rows + at_right = c.col + c.col_span >= table.n_cols + for edge, outer, inner in ( + ("top", at_top, "insideH"), + ("bottom", at_bottom, "insideH"), + ("left", at_left, "insideV"), + ("right", at_right, "insideV"), + ): + spec = tbl_borders.get(edge) if outer else tbl_borders.get(inner) + if spec: + eff[edge] = spec + eff.update(c.borders_declared) # an explicit cell border always wins + c.borders = {k: v for k, v in eff.items() if v} + + +def _parse_tbl(tbl) -> Table: + table = Table() + tbl_borders = _table_borders(tbl) + + grid = tbl.find(_q("tblGrid")) + if grid is not None: + for gc in grid.findall(_q("gridCol")): + try: + table.col_widths.append(int(gc.get(_q("w"), "0"))) + except ValueError: + table.col_widths.append(0) + + tbl_pr = tbl.find(_q("tblPr")) + if tbl_pr is not None: + layout = tbl_pr.find(_q("tblLayout")) + if layout is not None: + table.layout = layout.get(_q("type")) + + occupied: dict[tuple[int, int], Cell] = {} + rows = tbl.findall(_q("tr")) + + for r, tr in enumerate(rows): + tr_pr = tr.find(_q("trPr")) + if tr_pr is not None and tr_pr.find(_q("tblHeader")) is not None: + table.header_rows.append(r) + + c = 0 + for tc in tr.findall(_q("tc")): + while (r, c) in occupied: + c += 1 + + pr = tc.find(_q("tcPr")) + span = 1 + vmerge = None + if pr is not None: + gs = pr.find(_q("gridSpan")) + if gs is not None: + span = int(gs.get(_q("val"), "1")) + vm = pr.find(_q("vMerge")) + if vm is not None: + vmerge = vm.get(_q("val"), "continue") + + if vmerge == "continue": + anchor = occupied.get((r - 1, c)) + if anchor is not None: + anchor.row_span = max(anchor.row_span, r - anchor.row + 1) + for k in range(span): + occupied[(r, c + k)] = anchor + c += span + continue + # No anchor above: a broken vMerge. Treat as its own cell and + # let the comparator report the topology difference honestly. + vmerge = None + + shading = None + valign = None + width = None + tdir = None + if pr is not None: + shd = pr.find(_q("shd")) + if shd is not None: + fill = shd.get(_q("fill")) + if fill and fill.lower() not in ("auto",): + shading = fill.upper() + va = pr.find(_q("vAlign")) + valign = _val(va) + tcw = pr.find(_q("tcW")) + if tcw is not None: + try: + width = int(tcw.get(_q("w"), "0")) + except ValueError: + width = None + td = pr.find(_q("textDirection")) + tdir = _val(td) + + paras = tc.findall(_q("p")) + alignment = None + for p in paras: + ppr = p.find(_q("pPr")) + if ppr is not None: + j = ppr.find(_q("jc")) + if j is not None: + alignment = _val(j) + break + + cell = Cell( + row=r, + col=c, + row_span=1, + col_span=span, + paragraphs=_cell_paragraphs(tc), + alignment=alignment, + shading=shading, + valign=valign, + width_dxa=width, + text_direction=tdir, + borders=_borders(pr), + borders_declared=_borders(pr), + nested=[_parse_tbl(t) for t in tc.findall(_q("tbl"))], + ) + table.cells.append(cell) + for k in range(span): + occupied[(r, c + k)] = cell + c += span + + table.n_rows = len(rows) + table.n_cols = max((c.col + c.col_span for c in table.cells), default=0) + if table.col_widths: + table.n_cols = max(table.n_cols, len(table.col_widths)) + _resolve_effective_borders(table, tbl_borders) + return table + + +def from_docx(path: str) -> Document: + with zipfile.ZipFile(path) as z: + xml = z.read("word/document.xml") + root = etree.fromstring(xml) + body = root.find(_q("body")) + doc = Document() + if body is None: + return doc + for child in body: + tag = etree.QName(child).localname + if tag == "tbl": + doc.tables.append(_parse_tbl(child)) + elif tag == "p": + txt = _para_text(child) + if txt.strip(): + doc.body_paragraphs.append(txt) + return doc + + +# -------------------------------------------------------------------------- +# HTML (the SuperDocs intermediate representation) +# -------------------------------------------------------------------------- + +def _style_map(el) -> dict[str, str]: + raw = el.get("style") or "" + out = {} + for part in raw.split(";"): + if ":" in part: + k, v = part.split(":", 1) + out[k.strip().lower()] = v.strip() + return out + + +def _html_cell_paragraphs(td) -> list[str]: + from bs4 import NavigableString + + blocks = td.find_all(["p", "div"], recursive=False) + if blocks: + return [b.get_text().strip() for b in blocks] + # Bare text in the cell, ignoring any nested table's text. + parts = [] + for node in td.children: + if isinstance(node, NavigableString): + parts.append(str(node)) + elif getattr(node, "name", None) != "table": + parts.append(node.get_text()) + text = "".join(parts).strip() + return [text] if text else [] + + +def _parse_html_table(tbl) -> Table: + table = Table() + occupied: dict[tuple[int, int], Cell] = {} + + rows = [tr for tr in tbl.find_all("tr") if tr.find_parent("table") is tbl] + for r, tr in enumerate(rows): + if tr.find_parent(["thead"]) is not None: + table.header_rows.append(r) + c = 0 + tds = [td for td in tr.find_all(["td", "th"]) if td.find_parent("tr") is tr] + for td in tds: + while (r, c) in occupied: + c += 1 + try: + colspan = int(td.get("colspan", 1)) + except ValueError: + colspan = 1 + try: + rowspan = int(td.get("rowspan", 1)) + except ValueError: + rowspan = 1 + + st = _style_map(td) + shading = st.get("background-color") or st.get("background") + if shading and shading.startswith("#"): + shading = shading[1:].upper() + elif shading: + shading = shading.upper() + + nested = [ + t for t in td.find_all("table") + if t.find_parent(["td", "th"]) is td + ] + + borders = {} + for k, v in td.attrs.items(): + if k.startswith("data-border-"): + borders[k[len("data-border-"):]] = v + try: + width = int(td.get("data-w")) if td.get("data-w") else None + except ValueError: + width = None + + # Alignment may sit on the cell or on the paragraph inside it. + # An earlier version read only the cell and reported every centred + # heading as lost -- a false finding against a product that had + # preserved it correctly. Check both. + align = st.get("text-align") + if not align: + first_p = td.find(["p", "div"], recursive=False) + if first_p is not None: + align = _style_map(first_p).get("text-align") + + cell = Cell( + row=r, + col=c, + row_span=rowspan, + col_span=colspan, + paragraphs=_html_cell_paragraphs(td), + alignment=align, + shading=shading, + valign=st.get("vertical-align"), + width_dxa=width, + text_direction=td.get("data-dir"), + borders=borders, + nested=[_parse_html_table(t) for t in nested], + ) + table.cells.append(cell) + for rr in range(rowspan): + for kk in range(colspan): + occupied[(r + rr, c + kk)] = cell + c += colspan + + hr = tbl.get("data-header-rows") + if hr: + table.header_rows = [int(x) for x in hr.split(",") if x.strip()] + elif tbl.find("thead") is not None: + # A real is how the API expresses header rows. Reading it means + # the HTML stage can distinguish "the header marking survived ingest and + # was lost on export" from "it never arrived" -- which is the whole + # value of a staged comparison. + thead_rows = [tr for tr in tbl.find_all("tr") + if tr.find_parent("thead") is not None] + all_rows = [tr for tr in tbl.find_all("tr") if tr.find_parent("table") is tbl] + table.header_rows = [all_rows.index(tr) for tr in thead_rows + if tr in all_rows] + table.layout = tbl.get("data-layout") + cg = tbl.find("colgroup") + if cg is not None and cg.find_parent("table") is tbl: + for col in cg.find_all("col"): + try: + table.col_widths.append(int(col.get("data-w", 0))) + except ValueError: + table.col_widths.append(0) + + table.n_rows = max((c.row + c.row_span for c in table.cells), default=0) + table.n_cols = max((c.col + c.col_span for c in table.cells), default=0) + if table.col_widths: + table.n_cols = max(table.n_cols, len(table.col_widths)) + return table + + +def from_html(html: str) -> Document: + from bs4 import BeautifulSoup + + soup = BeautifulSoup(html, "html.parser") + doc = Document() + for tbl in soup.find_all("table"): + if tbl.find_parent("table") is not None: + continue # nested tables are reached through their parent cell + doc.tables.append(_parse_html_table(tbl)) + for p in soup.find_all(["p", "h1", "h2", "h3"]): + if p.find_parent("table") is not None: + continue + txt = p.get_text().strip() + if txt: + doc.body_paragraphs.append(txt) + return doc diff --git a/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/htmlout.py b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/htmlout.py new file mode 100644 index 00000000..8bd0b9cd --- /dev/null +++ b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/htmlout.py @@ -0,0 +1,135 @@ +"""Model -> HTML, and model -> generator spec. + +Used only by the offline FakeClient, to give it a real round-trip +(docx -> model -> html -> model -> docx) rather than a stub that hands back +what it was given. A fake that returns the input unchanged would make every +test pass and prove nothing. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from xml.sax.saxutils import escape + +from .model import Cell, Document, Table + + +def _cell_html(c: Cell) -> str: + attrs = [] + if c.col_span > 1: + attrs.append(f'colspan="{c.col_span}"') + if c.row_span > 1: + attrs.append(f'rowspan="{c.row_span}"') + style = [] + if c.alignment: + style.append(f"text-align:{c.alignment}") + if c.shading: + style.append(f"background-color:#{c.shading}") + if c.valign: + style.append(f"vertical-align:{c.valign}") + if style: + attrs.append('style="' + ";".join(style) + '"') + if c.text_direction: + attrs.append(f'data-dir="{c.text_direction}"') + if c.width_dxa is not None: + attrs.append(f'data-w="{c.width_dxa}"') + for edge, spec in sorted(c.borders.items()): + attrs.append(f'data-border-{edge}="{spec}"') + open_tag = "" + + body = "".join(f"

{escape(p)}

" for p in c.paragraphs if p != "") + for nt in c.nested: + body += _table_html(nt) + return open_tag + body + "" + + +def _table_html(t: Table) -> str: + anchors = t.anchors() + rows_html = [] + for r in range(t.n_rows): + cells = [anchors[(rr, cc)] for (rr, cc) in sorted(anchors) if rr == r] + cells.sort(key=lambda c: c.col) + if not cells: + continue + rows_html.append("" + "".join(_cell_html(c) for c in cells) + "") + attrs = "" + if t.header_rows: + attrs += f' data-header-rows="{",".join(str(r) for r in t.header_rows)}"' + if t.layout: + attrs += f' data-layout="{t.layout}"' + cols = "" + if t.col_widths: + cols = "" + "".join( + f'' for w in t.col_widths) + "" + return f"" + cols + "".join(rows_html) + "" + + +def to_html(doc: Document) -> str: + parts = [f"

{escape(p)}

" for p in doc.body_paragraphs[:2]] + parts += [_table_html(t) for t in doc.tables] + parts += [f"

{escape(p)}

" for p in doc.body_paragraphs[2:]] + return "".join(parts) + + +# -------------------------------------------------------------------------- + +@dataclass +class _SyntheticCase: + """Duck-types the fields corpus.generate.write_docx reads off a Case.""" + + title: str = "" + intro: str = "" + tables: list[dict] = field(default_factory=list) + trailing_paragraphs: list[str] = field(default_factory=list) + + +def _table_to_spec(t: Table) -> dict: + anchors = t.anchors() + rows: list[list[dict]] = [] + for r in range(t.n_rows): + line = [c for (rr, _), c in sorted(anchors.items()) if rr == r] + line.sort(key=lambda c: c.col) + spec_row = [] + for c in line: + d: dict = {"paragraphs": list(c.paragraphs) or [""]} + if c.col_span > 1: + d["colspan"] = c.col_span + if c.row_span > 1: + d["rowspan"] = c.row_span + if c.alignment: + d["align"] = c.alignment + if c.shading: + d["fill"] = c.shading + if c.valign: + d["valign"] = c.valign + if c.text_direction: + d["dir"] = c.text_direction + if c.borders: + bd = {} + for edge, spec in c.borders.items(): + parts = spec.split("/") + if len(parts) == 3: + bd[edge] = (parts[1], parts[2]) + if bd: + d["borders"] = bd + if c.nested: + d["nested"] = _table_to_spec(c.nested[0]) + spec_row.append(d) + if spec_row: + rows.append(spec_row) + spec = {"rows": rows, "header_rows": list(t.header_rows)} + if t.col_widths: + spec["col_widths"] = list(t.col_widths) + if t.layout: + spec["layout"] = t.layout + return spec + + +def doc_to_case(doc: Document) -> _SyntheticCase: + bp = doc.body_paragraphs + return _SyntheticCase( + title=bp[0] if bp else "", + intro=bp[1] if len(bp) > 1 else "", + tables=[_table_to_spec(t) for t in doc.tables], + trailing_paragraphs=list(bp[2:]), + ) diff --git a/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/model.py b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/model.py new file mode 100644 index 00000000..df6cfacf --- /dev/null +++ b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/model.py @@ -0,0 +1,147 @@ +"""Canonical table model. + +One grid representation that the generator, the DOCX extractor and the HTML +extractor all produce. Comparison happens between two of these and never +between two file formats directly -- that is what lets the benchmark say +*where* fidelity was lost (ingest, edit, or export) instead of only that it was. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field, asdict +from typing import Any + + +@dataclass +class Cell: + """One anchor cell. Cells covered by a span are not repeated.""" + + row: int + col: int + row_span: int = 1 + col_span: int = 1 + paragraphs: list[str] = field(default_factory=list) + alignment: str | None = None + shading: str | None = None # fill colour, upper hex, no '#' + valign: str | None = None + width_dxa: int | None = None + text_direction: str | None = None + borders: dict[str, str] = field(default_factory=dict) + borders_declared: dict[str, str] = field(default_factory=dict) + """Borders written on the cell itself, before table-level inheritance. + + Kept apart from the resolved set because an interior rule is shared by two + cells: when they disagree, the one that declared it explicitly is the one + that was meant.""" + nested: list["Table"] = field(default_factory=list) + + @property + def text(self) -> str: + return "\n".join(self.paragraphs) + + @property + def is_merged(self) -> bool: + return self.row_span > 1 or self.col_span > 1 + + def to_dict(self) -> dict[str, Any]: + d = asdict(self) + d["nested"] = [t.to_dict() for t in self.nested] + return d + + +@dataclass +class Table: + n_rows: int = 0 + n_cols: int = 0 + cells: list[Cell] = field(default_factory=list) + col_widths: list[int] = field(default_factory=list) + layout: str | None = None # 'fixed' | 'autofit' + header_rows: list[int] = field(default_factory=list) # w:tblHeader rows + + def cell_at(self, row: int, col: int) -> Cell | None: + """The anchor covering (row, col), span-aware.""" + for c in self.cells: + if ( + c.row <= row < c.row + c.row_span + and c.col <= col < c.col + c.col_span + ): + return c + return None + + def occupancy(self) -> dict[tuple[int, int], tuple[int, int]]: + """(row, col) -> anchor coordinate. The topology fingerprint.""" + occ: dict[tuple[int, int], tuple[int, int]] = {} + for c in self.cells: + for r in range(c.row, c.row + c.row_span): + for k in range(c.col, c.col + c.col_span): + occ[(r, k)] = (c.row, c.col) + return occ + + def anchors(self) -> dict[tuple[int, int], Cell]: + return {(c.row, c.col): c for c in self.cells} + + def border_edges(self) -> dict[tuple[str, int, int], str]: + """Every drawn rule, keyed by its position in the grid. + + Interior edges are shared: the line between two cells can be declared + as one cell's bottom or the next cell's top, and the document looks + identical either way. Comparing cells would call that a difference. + Comparing edges compares what is actually drawn. + + ('h', r, c) is the horizontal rule above row r spanning column c. + ('v', r, c) is the vertical rule left of column c on row r. + """ + edges: dict[tuple[str, int, int], str] = {} + rank: dict[tuple[int, int, int], tuple[int, int]] = {} + + def claim(key, cell: "Cell", edge: str) -> None: + spec = cell.borders.get(edge) + if not spec: + return + # An explicitly declared border outranks an inherited one; between + # two of the same kind the heavier rule wins. Without a rule here + # the winner would depend on cell ordering, which would make the + # same document compare differently against itself. + declared = 1 if edge in cell.borders_declared else 0 + try: + weight = int(spec.split("/")[1] or 0) + except (IndexError, ValueError): + weight = 0 + score = (declared, weight) + if key not in edges or score > rank[key]: + edges[key] = spec + rank[key] = score + + for cell in self.cells: + for r in range(cell.row, cell.row + cell.row_span): + claim(("v", r, cell.col), cell, "left") + claim(("v", r, cell.col + cell.col_span), cell, "right") + for k in range(cell.col, cell.col + cell.col_span): + claim(("h", cell.row, k), cell, "top") + claim(("h", cell.row + cell.row_span, k), cell, "bottom") + return edges + + def to_dict(self) -> dict[str, Any]: + return { + "n_rows": self.n_rows, + "n_cols": self.n_cols, + "col_widths": self.col_widths, + "layout": self.layout, + "header_rows": self.header_rows, + "cells": [c.to_dict() for c in self.cells], + } + + +@dataclass +class Document: + """Just the tables. Prose between tables is captured as a flat list so the + invariance check can prove untouched body text did not move either.""" + + tables: list[Table] = field(default_factory=list) + body_paragraphs: list[str] = field(default_factory=list) + + def to_dict(self) -> dict[str, Any]: + return { + "tables": [t.to_dict() for t in self.tables], + "body_paragraphs": self.body_paragraphs, + } diff --git a/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/run.py b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/run.py new file mode 100644 index 00000000..ba8ca653 --- /dev/null +++ b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tablebench/run.py @@ -0,0 +1,327 @@ +"""Benchmark runner. + +Two runs per case: + + control upload -> export (no edit, 0 operations) + edit upload -> chat -> approve -> export (1 operation) + +The control is what makes the result attributable. Any difference the control +also shows is a property of ingest and export; only what the edit run adds is +caused by the edit. Without it a benchmark can say a table broke but not where. + +Resumable: state is written after every case, so a killed run resumes without +re-spending operations on cases already measured. +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import time +import traceback + +from . import compare, extract +from .client import ApiError, BudgetExceeded, FakeClient, Ledger, SuperDocsClient +from .corpus import cases as case_mod +from .corpus.generate import write_docx + +DEFAULT_OUT = "results" + + +def _load_state(path: str) -> dict: + if os.path.exists(path): + with open(path) as fh: + return json.load(fh) + return {"cases": {}, "ops_spent": 0, "started": time.time()} + + +def _save_state(path: str, state: dict) -> None: + os.makedirs(os.path.dirname(path) or ".", exist_ok=True) + tmp = path + ".tmp" + with open(tmp, "w") as fh: + json.dump(state, fh, indent=2) + os.replace(tmp, path) # atomic: a crash mid-write cannot corrupt it + + +def run_case(case, client, workdir: str, mode: str = "dry") -> dict: + """One case, both runs. Returns a result record. + + The record carries the mode it was produced in. A dry record and a + live record are not interchangeable evidence, and nothing downstream + is allowed to treat them as if they were. + """ + src = write_docx(case, os.path.join(workdir, "corpus", f"{case.id}.docx")) + before = extract.from_docx(src) + rec: dict = {"id": case.id, "axis": case.axis, "title": case.title, + "mode": mode} + t0 = time.monotonic() + + # ---- control: no edit ------------------------------------------------- + ctrl_findings: list[compare.Finding] = [] + ctrl_sid = f"tfb-{case.id}-control" + try: + client.upload(src, ctrl_sid) + ctrl_out = client.export( + ctrl_sid, os.path.join(workdir, "out", f"{case.id}.control.docx")) + ctrl_findings = compare.compare(before, extract.from_docx(ctrl_out)) + rec["control"] = {"status": "ok", + "findings": [f.to_dict() for f in ctrl_findings]} + except Exception as exc: # noqa: BLE001 + rec["control"] = {"status": "error", "error": f"{type(exc).__name__}: {exc}"} + + # ---- edit run --------------------------------------------------------- + sid = f"tfb-{case.id}-edit" + try: + html = client.upload(src, sid) + html_doc = extract.from_html(html) + ingest = compare.compare(before, html_doc, profile="html") + rec["ingest_findings"] = [f.to_dict() for f in ingest] + rec["ingest_unmeasurable"] = compare.unmeasurable_at_html_stage( + compare.compare(before, html_doc)) + + if hasattr(client, "simulate_edit"): + client.simulate_edit(sid, case) + job = client.edit(sid, html, case.edit) + pending = job.get("pending") or [] + rec["proposed_changes"] = len(pending) + if pending: + client.approve(sid, job["job_id"], pending) + + out = client.export(sid, os.path.join(workdir, "out", f"{case.id}.edit.docx")) + after = extract.from_docx(out) + + exempt = [case.target, *getattr(case, "also_changes", [])] + findings = compare.compare(before, after, target=exempt) + by_edit, by_round_trip = compare.attribute(findings, ctrl_findings) + + edit_applied = None + if case.expect_contains is not None: + tgt_tbl = after.tables[case.target[0]] if len(after.tables) > case.target[0] else None + cell = tgt_tbl.cell_at(case.target[1], case.target[2]) if tgt_tbl else None + haystack = _all_text(after) + if cell is not None: + edit_applied = case.expect_contains in _cell_text_deep(cell) + if not edit_applied and case.expect_contains in haystack: + rec["note"] = ("expected value is present in the document " + "but not in the target cell") + else: + # The cell we were told to check no longer exists. That is a + # structural failure, not an unmeasurable one. + edit_applied = False + rec["note"] = "target cell absent from the exported grid" + + rec["findings"] = [f.to_dict() for f in findings] + rec["caused_by_edit"] = [f.to_dict() for f in by_edit] + rec["caused_by_round_trip"] = [f.to_dict() for f in by_round_trip] + rec["edit_applied"] = edit_applied + rec["verdict"] = compare.verdict(edit_applied, findings) + rec["summary"] = compare.summarise(findings) + except BudgetExceeded: + raise + except Exception as exc: # noqa: BLE001 + rec["verdict"] = compare.ERROR + rec["error"] = f"{type(exc).__name__}: {exc}" + rec["traceback"] = traceback.format_exc(limit=3) + + rec["seconds"] = round(time.monotonic() - t0, 2) + return rec + + +def _cell_text_deep(cell) -> str: + """A cell's own text plus everything in tables nested inside it.""" + parts = [cell.text] + for n in cell.nested: + for c in n.cells: + parts.append(_cell_text_deep(c)) + return "\n".join(parts) + + +def _all_text(doc) -> str: + parts = list(doc.body_paragraphs) + + def walk(t): + for c in t.cells: + parts.append(c.text) + for n in c.nested: + walk(n) + + for t in doc.tables: + walk(t) + return "\n".join(parts) + + +def render_report(state: dict, ledger: Ledger, mode: str) -> str: + recs = list(state["cases"].values()) + counts: dict[str, int] = {} + for r in recs: + counts[r.get("verdict", "?")] = counts.get(r.get("verdict", "?"), 0) + 1 + + modes: dict[str, int] = {} + for r in recs: + modes[r.get("mode", "unknown")] = modes.get(r.get("mode", "unknown"), 0) + 1 + mixed = len(modes) > 1 + mode_line = (f"- mode: **{mode}**" if not mixed else + "- mode: **MIXED** - " + + ", ".join(f"{k}: {v} cases" for k, v in sorted(modes.items()))) + + lines = [ + "# Table fidelity benchmark - results", + "", + mode_line, + f"- cases run: **{len(recs)}**", + f"- operations spent: **{ledger.spent}** (cap {ledger.cap})", + "- verdicts: " + ", ".join(f"{k} {v}" for k, v in sorted(counts.items())), + "", + ] + ([ + "> **These results are not one run.** The records below were " + "produced in more than one mode. A dry-mode record is a simulation " + "and is not evidence about the live API. Use a separate `--out` " + "directory per mode before quoting any of this.", + "", + ] if mixed else []) + [ + "Method: each case is uploaded, exported once with no edit (control), " + "then uploaded, edited through one approval-gated instruction, " + "approved and exported. Findings present in the control are attributed " + "to the format round-trip; the remainder are attributed to the edit. " + "The named target cell is exempt from content comparison; every other " + "cell is an invariant.", + "", + "| case | mode | axis | verdict | edit applied | by edit | by round-trip | s |", + "|---|---|---|---|---|---|---|---|", + ] + for r in sorted(recs, key=lambda x: (x.get("axis", ""), x["id"])): + applied = r.get("edit_applied") + applied_s = {True: "yes", False: "no", None: "unverifiable"}.get(applied, "?") + lines.append( + f"| `{r['id']}` | {r.get('mode','?')} | {r.get('axis','')} | " + f"{r.get('verdict','?')} | " + f"{applied_s} | {len(r.get('caused_by_edit', []))} | " + f"{len(r.get('caused_by_round_trip', []))} | {r.get('seconds','')} |" + ) + + lines += ["", "## Findings by case", ""] + for r in sorted(recs, key=lambda x: x["id"]): + if r.get("verdict") == compare.PASS and not r.get("caused_by_round_trip"): + continue + lines.append(f"### `{r['id']}` - {r.get('title','')}") + if r.get("error"): + lines.append(f"- run error: `{r['error']}`") + for label, key in (("caused by the edit", "caused_by_edit"), + ("caused by the round-trip", "caused_by_round_trip")): + items = r.get(key) or [] + if items: + lines.append(f"- **{label}** ({len(items)}):") + for f in items[:12]: + lines.append( + f" - `{f['severity']}/{f['code']}` {f['where']} - " + f"expected `{f['expected'][:70]}`, got `{f['actual'][:70]}`") + if len(items) > 12: + lines.append(f" - ... and {len(items) - 12} more") + if r.get("ingest_unmeasurable"): + lines.append("- not measurable at the HTML stage: " + + ", ".join(f"`{c}`" for c in r["ingest_unmeasurable"])) + if r.get("note"): + lines.append(f"- note: {r['note']}") + lines.append("") + + lines += ["## Timing by stage", ""] + for stage, secs in sorted(ledger.timings.items()): + lines.append(f"- {stage}: {secs:.1f}s") + lines += ["", "## Limits of this measurement", "", + "- Findings are reported against the exported DOCX. Attributes " + "that plain HTML cannot express are withheld at the HTML stage " + "and listed as not measurable rather than counted as losses.", + "- A case whose run errored is reported ERROR, never FAIL. " + "An unproven failure is not a failure.", + "- Rendering is not compared. Two files that parse identically " + "could still paginate differently; that is out of scope and " + "stated rather than implied."] + return "\n".join(lines) + + +def main(argv=None) -> int: + ap = argparse.ArgumentParser(prog="tablebench") + ap.add_argument("--mode", choices=("dry", "live"), default="dry", + help="dry runs entirely offline and spends nothing") + ap.add_argument("--fault", default="none", + help="dry mode only: inject a named fidelity fault") + ap.add_argument("--limit", type=int, default=None, + help="small-sample mode: run only the first N cases") + ap.add_argument("--cases", default=None, help="comma-separated case ids") + ap.add_argument("--axis", default=None, choices=case_mod.AXES) + ap.add_argument("--max-ops", type=int, default=25, + help="hard operation cap; the run stops before exceeding it") + ap.add_argument("--out", default=DEFAULT_OUT) + ap.add_argument("--resume", action="store_true", + help="skip cases already recorded in the state file") + args = ap.parse_args(argv) + + selected = case_mod.select( + ids=args.cases.split(",") if args.cases else None, + axis=args.axis, limit=args.limit) + if not selected: + print("no cases selected", file=sys.stderr) + return 2 + + state_path = os.path.join(args.out, "state.json") + state = _load_state(state_path) if args.resume else { + "cases": {}, "ops_spent": 0, "started": time.time()} + + # Only spends made in THIS mode count against this cap. Simulated + # operations from a dry run are not money and must not reserve budget. + prior_spend = sum(r.get("ops", 0) for r in state.get("cases", {}).values() + if r.get("mode") == args.mode) + ledger = Ledger(cap=args.max_ops, spent=prior_spend) + if args.mode == "live": + client = SuperDocsClient(ledger) + else: + client = FakeClient(ledger, fault=args.fault) + + print(f"mode={args.mode} cases={len(selected)} cap={args.max_ops} ops") + if args.mode == "live": + est = sum(1 for _ in selected) + print(f"estimated spend: {est} operations " + f"(1 per case; control and export are free)") + + stopped = None + for case in selected: + prior = state["cases"].get(case.id) if args.resume else None + if prior is not None and prior.get("mode") == args.mode: + print(f" skip {case.id} (already recorded in {args.mode} mode)") + continue + if prior is not None: + print(f" rerun {case.id} (prior record was " + f"{prior.get('mode', 'unknown')} mode, not {args.mode})") + spent_before = ledger.spent + try: + rec = run_case(case, client, args.out, mode=args.mode) + except BudgetExceeded as exc: + stopped = str(exc) + print(f" STOP {exc}") + break + rec["ops"] = ledger.spent - spent_before + state["cases"][case.id] = rec + state["ops_spent"] = ledger.spent + _save_state(state_path, state) + print(f" {rec.get('verdict','?'):13s} {case.id:28s} " + f"edit={len(rec.get('caused_by_edit', []))} " + f"rt={len(rec.get('caused_by_round_trip', []))} " + f"ops={ledger.spent}") + + if stopped: + state["stopped_by_budget"] = stopped + _save_state(state_path, state) + + report = render_report(state, ledger, args.mode) + os.makedirs(args.out, exist_ok=True) + with open(os.path.join(args.out, "report.md"), "w") as fh: + fh.write(report) + print(f"\nreport: {os.path.join(args.out, 'report.md')} " + f"ops spent: {ledger.spent}/{ledger.cap}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tests/test_benchmark.py b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tests/test_benchmark.py new file mode 100644 index 00000000..ee4eaf67 --- /dev/null +++ b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/tests/test_benchmark.py @@ -0,0 +1,359 @@ +"""Tests. None require an API key and none cost an operation. + +These do not assert that the fake returns what it was told to return. Each one +injects a fault whose ground truth is known and asserts the comparator reports +that fault, only in the cases where the pathology exists, and nowhere else. +A benchmark whose measuring instrument is untested measures nothing. +""" + +from __future__ import annotations + +import json +import os +import re + +import pytest + +from tablebench import compare, extract +from tablebench.client import BudgetExceeded, FakeClient, Ledger +from tablebench.corpus import cases as case_mod +from tablebench.corpus.generate import write_docx +from tablebench.run import main as run_main, run_case + +# Cases that actually contain each pathology, so a detector can be checked for +# false negatives AND false positives. +HAS_MERGE = {"merge_horizontal_header", "merge_vertical_label", "merge_l_shaped", + "merge_full_row_divider", "merge_containing_nested", + "header_two_level", "header_three_level", "header_repeat_flag"} +HAS_NESTED = {"merge_containing_nested", "nested_simple", "nested_two_deep", + "nested_side_by_side"} + + +@pytest.fixture(scope="module") +def corpus(tmp_path_factory): + d = tmp_path_factory.mktemp("corpus") + return {c.id: write_docx(c, str(d / f"{c.id}.docx")) for c in case_mod.CASES} + + +# -------------------------------------------------------------------------- +# corpus and extraction +# -------------------------------------------------------------------------- + +def test_every_case_generates_a_readable_docx(corpus): + assert len(corpus) == len(case_mod.CASES) + for cid, path in corpus.items(): + assert os.path.getsize(path) > 0 + doc = extract.from_docx(path) + assert doc.tables, f"{cid} produced no table" + + +def test_spans_survive_extraction(corpus): + doc = extract.from_docx(corpus["merge_l_shaped"]) + t = doc.tables[0] + assert t.cell_at(0, 0).row_span == 2 # Asset, merged down + assert t.cell_at(0, 1).col_span == 2 # Planned, merged across + assert t.cell_at(3, 0).col_span == 4 # full-width divider + # A span is not the same cell repeated: anchors are unique. + assert t.cell_at(0, 1) is t.cell_at(0, 2) + + +def test_nested_tables_are_reached_and_not_flattened(corpus): + doc = extract.from_docx(corpus["nested_two_deep"]) + outer = doc.tables[0].cell_at(1, 1) + assert len(outer.nested) == 1 + inner = outer.nested[0].cell_at(0, 1) + assert len(inner.nested) == 1, "second level of nesting was lost" + assert "0.72" in inner.nested[0].cell_at(1, 1).text + + +def test_pathological_numbers_are_character_exact(corpus): + t = extract.from_docx(corpus["num_negative_parentheses"]).tables[0] + assert t.cell_at(1, 1).text == "(12,400.00)" + assert t.cell_at(3, 1).text == "\u2014", "em dash must not become a hyphen" + t2 = extract.from_docx(corpus["num_leading_zero_ids"]).tables[0] + assert t2.cell_at(1, 1).text == "0007", "leading zeros must not be dropped" + + +# -------------------------------------------------------------------------- +# the comparator +# -------------------------------------------------------------------------- + +def test_control_roundtrip_is_lossless(corpus): + """With no fault injected the pipeline must report nothing. + + If this drifts, every other detection result is unattributable noise. + """ + fc = FakeClient(Ledger(cap=999)) + for cid, path in corpus.items(): + before = extract.from_docx(path) + fc.upload(path, cid) + out = fc.export(cid, f"/tmp/tfb-control/{cid}.docx") + assert compare.compare(before, extract.from_docx(out)) == [], cid + + +@pytest.mark.parametrize("fault,applies_to", [ + ("flatten_merges", HAS_MERGE), + ("drop_nested", HAS_NESTED), +]) +def test_structural_faults_are_caught_where_they_apply(corpus, fault, applies_to): + fc = FakeClient(Ledger(cap=999), fault=fault) + detected = set() + for cid, path in corpus.items(): + before = extract.from_docx(path) + fc.upload(path, cid) + out = fc.export(cid, f"/tmp/tfb-{fault}/{cid}.docx") + if compare.compare(before, extract.from_docx(out)): + detected.add(cid) + missed = applies_to - detected + assert not missed, f"{fault} went undetected in {sorted(missed)}" + + +def test_numeric_fault_is_caught_and_named(corpus): + fc = FakeClient(Ledger(cap=999), fault="strip_trailing_zero") + path = corpus["num_trailing_zeros"] + before = extract.from_docx(path) + fc.upload(path, "n") + out = fc.export("n", "/tmp/tfb-num/x.docx") + findings = compare.compare(before, extract.from_docx(out)) + codes = {f.code for f in findings} + assert "cell_text" in codes + assert any("1.23" in f.actual for f in findings), \ + "the comparator saw a change but did not report the changed value" + + +def test_untouched_body_text_is_an_invariant(corpus): + fc = FakeClient(Ledger(cap=999), fault="rewrite_body") + path = corpus["nested_simple"] + before = extract.from_docx(path) + fc.upload(path, "b") + out = fc.export("b", "/tmp/tfb-body/x.docx") + findings = compare.compare(before, extract.from_docx(out)) + assert any(f.code == "body_paragraphs" for f in findings), \ + "prose outside the table changed and was not reported" + + +def test_target_cell_is_exempt_but_its_neighbours_are_not(corpus): + """The asymmetry that defines the measurement.""" + case = case_mod.BY_ID["merge_horizontal_header"] + before = extract.from_docx(corpus[case.id]) + after = extract.from_docx(corpus[case.id]) + + tgt = after.tables[0].cell_at(*case.target[1:]) + tgt.paragraphs = ["1,099.40"] + assert compare.compare(before, after, target=case.target) == [], \ + "changing the target cell must not be a finding" + + neighbour = after.tables[0].cell_at(case.target[1], case.target[2] - 1) + neighbour.paragraphs = ["tampered"] + findings = compare.compare(before, after, target=case.target) + assert [f.code for f in findings] == ["cell_text"], \ + "changing a neighbour must be a finding, and only that one" + + +def test_findings_are_attributed_between_edit_and_roundtrip(): + f1 = compare.Finding("content", "cell_text", "table[0] cell(1, 1)", "a", "b") + f2 = compare.Finding("structural", "col_span", "table[0] cell(0, 1)", "2", "1") + by_edit, by_rt = compare.attribute([f1, f2], control_findings=[f2]) + assert by_edit == [f1] and by_rt == [f2] + + +# -------------------------------------------------------------------------- +# honesty, budget, resumption +# -------------------------------------------------------------------------- + +def test_error_is_reported_as_error_not_as_failure(corpus): + class Broken(FakeClient): + def edit(self, *a, **k): + raise RuntimeError("simulated upstream 502") + + rec = run_case(case_mod.BY_ID["nested_simple"], + Broken(Ledger(cap=99)), "/tmp/tfb-err") + assert rec["verdict"] == compare.ERROR + assert rec["verdict"] != compare.FAIL + assert "502" in rec["error"] + + +def test_unverifiable_is_distinct_from_pass(): + assert compare.verdict(None, []) == compare.UNVERIFIABLE + assert compare.verdict(True, []) == compare.PASS + assert compare.verdict(False, []) == compare.FAIL + + +def test_budget_cap_stops_before_the_spend_not_after(): + led = Ledger(cap=2) + led.charge("chat") + led.charge("chat") + with pytest.raises(BudgetExceeded): + led.charge("chat") + assert led.spent == 2, "the ledger must not record a spend it refused" + + +def test_free_stages_do_not_consume_operations(): + led = Ledger(cap=1) + for stage in ("upload", "approve", "export"): + led.charge(stage) + assert led.spent == 0 + + +def test_run_is_resumable_after_being_killed(tmp_path): + out = str(tmp_path / "r") + run_main(["--mode", "dry", "--max-ops", "3", "--out", out]) + state = json.load(open(os.path.join(out, "state.json"))) + first = len(state["cases"]) + assert 0 < first < len(case_mod.CASES), "budget stop did not halt the run" + + run_main(["--mode", "dry", "--max-ops", "99", "--out", out, "--resume"]) + state2 = json.load(open(os.path.join(out, "state.json"))) + assert len(state2["cases"]) == len(case_mod.CASES) + for cid in list(state["cases"])[:first]: + assert state2["cases"][cid]["seconds"] == state["cases"][cid]["seconds"], \ + f"{cid} was re-run instead of resumed, re-spending its operation" + + +def test_proposed_changes_get_the_second_parse(): + """The documented integrator trap: content arrives JSON-encoded.""" + from tablebench.client import SuperDocsClient + job = {"metadata": {"pending_changes": [ + {"change_id": "c1", "content": json.dumps({"chunk_id": "p3", "html": "

x

"})}, + {"change_id": "c2", "content": "{not valid json"}, + ]}} + parsed = SuperDocsClient._pending(job) + assert parsed[0]["content"]["chunk_id"] == "p3", "second parse not applied" + assert parsed[1].get("content_parse_failed") is True, \ + "an unparseable change must be flagged, not silently passed through" + + +def test_no_credential_ever_reaches_an_output_file(tmp_path, monkeypatch): + monkeypatch.setenv("SUPERDOCS_API_KEY", "sk_live_THIS_MUST_NEVER_APPEAR") + out = str(tmp_path / "r") + run_main(["--mode", "dry", "--limit", "3", "--out", out]) + for root, _, files in os.walk(out): + for name in files: + if name.endswith((".json", ".md")): + body = open(os.path.join(root, name), encoding="utf-8").read() + assert not re.search(r"sk_[A-Za-z0-9_]{4,}", body), \ + f"a key-shaped string reached {name}" + + +def test_report_states_its_own_limits(tmp_path): + out = str(tmp_path / "r") + run_main(["--mode", "dry", "--limit", "2", "--out", out]) + report = open(os.path.join(out, "report.md")).read() + assert "Limits of this measurement" in report + assert "not measurable" in report or "never FAIL" in report + + +def test_a_dry_record_never_satisfies_a_live_resume(tmp_path): + """Regression: --resume once skipped live work because dry records held + those slots, and counted simulated operations against the live cap. A + simulation is not evidence about the API and must not stand in for it.""" + out = str(tmp_path / "r") + run_main(["--mode", "dry", "--limit", "3", "--out", out]) + path = os.path.join(out, "state.json") + state = json.load(open(path)) + assert {r["mode"] for r in state["cases"].values()} == {"dry"} + + # Pretend one case was measured live, then resume in dry mode. + victim = sorted(state["cases"])[0] + state["cases"][victim]["mode"] = "live" + state["cases"][victim]["seconds"] = -1.0 + json.dump(state, open(path, "w")) + + run_main(["--mode", "dry", "--limit", "3", "--out", out, "--resume"]) + after = json.load(open(path)) + assert after["cases"][victim]["seconds"] != -1.0, \ + "a live record was treated as a dry record and skipped" + assert after["cases"][victim]["mode"] == "dry" + + +def test_simulated_operations_do_not_reserve_live_budget(tmp_path): + out = str(tmp_path / "r") + run_main(["--mode", "dry", "--limit", "5", "--max-ops", "5", "--out", out]) + state = json.load(open(os.path.join(out, "state.json"))) + assert sum(r["ops"] for r in state["cases"].values()) == 5 + # Every one of those ops is dry, so a live run starts from zero. + live_prior = sum(r.get("ops", 0) for r in state["cases"].values() + if r.get("mode") == "live") + assert live_prior == 0 + + +def test_report_refuses_to_present_a_mixed_run_as_one(tmp_path): + out = str(tmp_path / "r") + run_main(["--mode", "dry", "--limit", "3", "--out", out]) + path = os.path.join(out, "state.json") + state = json.load(open(path)) + state["cases"][sorted(state["cases"])[0]]["mode"] = "live" + json.dump(state, open(path, "w")) + + run_main(["--mode", "dry", "--limit", "3", "--out", out, "--resume"]) + report = open(os.path.join(out, "report.md")).read() + if "MIXED" in report: + assert "not one run" in report + + +def test_an_instruction_naming_two_cells_exempts_both(corpus): + """Regression: a two-figure instruction with one declared target reported + the model for obeying it. A false finding is worse than no finding.""" + case = case_mod.BY_ID["merge_full_row_divider"] + assert case.also_changes, "this case's instruction changes two cells" + + before = extract.from_docx(corpus[case.id]) + after = extract.from_docx(corpus[case.id]) + after.tables[0].cell_at(*case.target[1:]).paragraphs = ["46"] + after.tables[0].cell_at(*case.also_changes[0][1:]).paragraphs = ["1,426.00"] + + exempt = [case.target, *case.also_changes] + assert compare.compare(before, after, target=exempt) == [], \ + "both cells named in the instruction must be exempt" + + # And a third cell is still an invariant. + after.tables[0].cell_at(2, 1).paragraphs = ["tampered"] + assert [f.code for f in compare.compare(before, after, target=exempt)] == \ + ["cell_text"] + + +def test_every_case_exempts_as_many_cells_as_its_instruction_changes(): + """A guard against the same mistake in a case added later.""" + for c in case_mod.CASES: + named = 1 + len(c.also_changes) + # crude but effective: count 'to ' clauses in the instruction + clauses = c.edit.lower().count(" to ") + assert clauses <= named, ( + f"{c.id}: instruction appears to change {clauses} cells but only " + f"{named} are exempt") + + +def test_shared_edges_resolve_the_same_way_every_time(corpus): + """Regression: an interior rule is written by both adjacent cells. Without + a precedence rule the winner depended on cell ordering, so a document + could compare unequal to itself and produce findings that were artifacts.""" + doc = extract.from_docx(corpus["struct_shading_borders"]) + t = doc.tables[0] + + edges = t.border_edges() + reversed_t = extract.from_docx(corpus["struct_shading_borders"]).tables[0] + reversed_t.cells.reverse() + assert reversed_t.border_edges() == edges, \ + "edge resolution depends on cell order" + + # The declared coloured rule must beat the inherited default it collides + # with, not the other way round. + assert edges[("h", 2, 1)] == "single/18/2E7D32" + assert edges[("h", 4, 1)] == "single/18/C62828" + + +def test_capture_does_not_call_a_withheld_measurement_a_survival(): + """Regression. The summary said text_direction 'survives ingest, lost on + export'. It does not survive ingest -- plain HTML has no property for it, + so the HTML stage withholds it and measures nothing either way. Reporting + withheld as intact asserts something the evidence cannot support, and it + contradicted the written bug report it was attached to.""" + from tablebench import capture, compare + assert "text_direction" in compare.HTML_UNREPRESENTABLE + assert "border_edge" in compare.HTML_UNREPRESENTABLE + # header_rows was withheld until the API turned out to return a real + # ; it is measurable and must not be withheld. + assert "header_rows" not in compare.HTML_UNREPRESENTABLE + src = open(capture.__file__, encoding="utf-8").read() + assert "HTML_UNREPRESENTABLE" in src, \ + "capture must exclude withheld codes from its stage attribution" diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/Makefile b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/Makefile new file mode 100644 index 00000000..b1c8e650 --- /dev/null +++ b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/Makefile @@ -0,0 +1,13 @@ +.PHONY: install corpus test dry live clean +install: + pip install -r requirements.txt +corpus: + cd corpus && python generate.py +test: + python -m pytest tests/ -q +dry: + python -m lossrun.review --corpus corpus/out --out out --mode dry --max-ops 6 --as-of 2026-01-09 +live: + python -m lossrun.review --corpus corpus/out --out out --mode live --max-ops 6 --as-of 2026-01-09 +clean: + rm -rf out .pytest_cache **/__pycache__ diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/README.md b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/README.md new file mode 100644 index 00000000..204fb5cf --- /dev/null +++ b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/README.md @@ -0,0 +1,216 @@ +# Insurance loss-run summary app + +Turns years of raw loss-run data across multiple policies and carriers into one +review document an underwriter can act on: claim counts, incurred totals, and +open claims grouped by policy year — with every figure traced to the document +and row it came from. + +Built on SuperDocs (upload, search, chat, approve, export) for the SuperDocs +task. Assigned build, band S2, surfaces: search and export. + +> Built by Siddharth Muneshwar for the SuperDocs task. + +## What SuperDocs features it uses + +| feature | how | +|---|---| +| **Upload** | the adjuster notes, application and broker correspondence are attached to the session; free, and what makes cross-document retrieval possible | +| **Chat editing** | one turn does two jobs: fills a placeholder section from the attached documents, and writes the opening narrative | +| **Approve** | the proposed changes are approved before anything is written | +| **Export** | the review document comes back as DOCX | + +A full review costs **one operation**. Figures never pass through the model: +they are parsed from the loss runs and the instruction forbids changing them. + +## Run it + +``` +pip install -r requirements.txt +python -m pytest tests/ -q +python -m lossrun.review --corpus corpus/out --out out --mode dry --max-ops 6 --as-of 2026-01-09 +``` + +25 tests, no API key, no network, no operations; then the full pipeline +offline. + +The generated corpus ships in `corpus/out/`, so there is nothing to build first. +Regenerating it (`cd corpus && python generate.py`) needs LibreOffice on the +path for the PDF loss run; nothing else does. + +Against the live API: + +``` +export SUPERDOCS_API_KEY=... +python -m lossrun.review --corpus corpus/out --out out --mode live --max-ops 6 --as-of 2026-01-09 +``` + +Four operations, capped at six. On PowerShell the first line is +`$env:SUPERDOCS_API_KEY = '...'`. + +A `Makefile` wraps these as `make install`, `make test`, `make dry` and +`make live` where `make` is available. It is a convenience, not a dependency — +every command above runs as written on Windows, macOS and Linux. + +The key is read from the environment only — never an argument, never logged, +never written to an output file, and a test asserts no key-shaped string reaches +any output. + +## The one design decision everything follows from + +**Figures are parsed. They are never searched for, inferred, or model-generated.** + +Claim counts, incurred totals and loss ratios are arithmetic over rows read out +of the loss runs. An underwriter cannot act on a total that might be a plausible +guess, and a summary whose numbers need re-checking has saved nobody any work. + +Search and chat are spent where they earn their cost: + +| work | how | operations | +|---|---|---| +| claim figures, counts, totals, ratios | parsed from CSV/PDF rows | 0 | +| contradictions living in prose, and the narrative | one edit | **1** | +| upload, attach, approve, export | free per published pricing | 0 | + +**A full review costs one operation.** + +**On the search surface, and two wrong turns getting here.** A dedicated +`/v1/search` endpoint returned `HTTP 404` live, so retrieval moved to the chat +surface: the prose documents are attached to the session by `upload`, which is +free. The first attempt asked for a JSON chat reply and looked for it in the +proposed-changes payload — which carries document edits, not chat text, so the +answer was never there to find. That was fighting the product. SuperDocs is an +editing agent: the answer belongs *in the document*. The review is now written +with a placeholder section, and one edit both fills it from the attached +documents and adds the opening narrative. Supported path, and half the cost of +the version that was wrong. + +`SuperDocsClient.search` and `.retrieve` are kept, unused, for the day a search +endpoint or a chat-reply field is exposed. `search` raises `SearchUnavailable` +on 404 rather than pretending an empty search succeeded. + +The chat instruction says explicitly that every number in the document is +already correct and must not be changed, recalculated, rounded or restated. +There is a test asserting that instruction still says so. + +A naive design that searched once per claim would have spent 41 operations on +one account and still produced figures nobody could audit. + +## What makes loss runs hard + +A loss run is a carrier's claims history for one policy year. They get +**reissued**, and reserves develop between issues. The same claim carries +$45,000 on the run valued January 2024 and $182,400 on the reissue valued +January 2026. Neither document is wrong. + +This is not a detail. On the sample account: + +| source for policy year 2023 | incurred | loss ratio | referral rule | +|---|---|---|---| +| original run, valued 2024-01 | $286,092.44 | **57.1%** | passes | +| reissue, valued 2026-01 | $647,992.44 | **129.3%** | **refer** | + +The same account either clears the 60% referral threshold or blows through it +twice over, depending on which document is trusted. So the app takes the later +valuation as authoritative **and records the difference as a conflict** rather +than overwriting quietly. A conflict is never delivered pre-resolved; there is a +test for that. + +## What it produces + +`out/review.html` and `out/loss-run-review.docx`: + +- **Summary by policy year** — claims, open count, paid, reserve, incurred, loss + ratio, and which document each year's figures came from +- **Open claims**, listed separately with months open, because reserves on open + claims may still develop +- **Conflicts between sources**, surfaced with both figures and the consequence +- **Checklist findings** against eight underwriting rules +- **What this review does not establish** — named, not smoothed over +- **Sources**, including which documents were read and superseded + +`out/summary.json` carries the same content for anything downstream. + +## The rules + +R1 severity claims need an adjuster note · R2 open more than 36 months · +R3 loss ratio above 60% · R4 date of loss outside its policy period · +R5 application consistent with the loss runs · R6 dormant reserves · +R7 duplicate claims · R8 loss run adds up to its own stated total + +A clean corpus produces no findings at all, and that path is tested. An honest +report of nothing is a valid output. + +## Formats + +CSV and PDF loss runs both parse to the same rows, and there is a test asserting +the same policy year read two ways gives identical facts. The PDF parser is +deliberately conservative: a row is accepted only when every field is present +and every figure parses. Rows that look like claim lines but do not fully parse +are counted and reported as unreadable rather than guessed at, because a +half-read row is worse than a missing one on a document feeding a coverage +decision. + +Accountant notation is parsed as written: `(12,400.00)` is negative, `—` is nil, +`$1,204,880.00` is a figure. A string that is not a figure raises rather than +quietly becoming `0.00` — silently reading `pending` as zero would corrupt a +total. + +## Budget + +One operation per run, capped at six. The cap is enforced **before** each +spend. Hitting it degrades rather than dies: the review is still produced, and +the section that could not be completed is named in *What this review does not +establish* instead of quietly disappearing. There is a test that runs with a cap +of one and asserts exactly that. + +```bash +python -m lossrun.review --corpus corpus/out --out out --mode live --max-ops 6 --as-of 2026-01-09 --skip-prose +``` + +## The corpus + +`corpus/` generates a fabricated account — Meridian Cold Chain Logistics, a +refrigerated carrier — with 41 claims across four policy years, five loss runs +including one reissue, three adjuster notes, a signed application, a broker +cover note, and the underwriting checklist. CSV, PDF and DOCX. + +`corpus/MANIFEST.md` is the answer key: nine planted conflicts and thirteen +expected findings, with verified figures. The test suite asserts the app +reproduces it **exactly** — every planted item found, and nothing outside the +key reported. 34 of the 41 claims are deliberately clean, which is what makes a +false positive measurable rather than arguable. + +Everything is fabricated. Meridian Cold Chain Logistics, Kestrel Mutual and +Ardent Risk Partners do not exist, and no figure comes from any real claim, +policy or company. + +## Honest limits + +- **The carrier's own numbers are taken as given.** Each run is checked against + itself and against the others; nothing here can confirm the carrier was right. +- **Retrieval quality is not measured.** The offline fake fills the placeholder + by keyword, deliberately weaker than the live agent, so a point that only + surfaces against the real API stays visible rather than hidden. +- **If the edit proposes no changes**, the correspondence section keeps its + placeholder and the run says so under *What this review does not establish* + rather than reporting a success it did not have. +- **Figures do not depend on retrieval at all**, and there is a test that proves + it: the pipeline is run twice, once with retrieval and once with + `--skip-prose`, and every total, ratio and finding must be identical. +- **R5 reads specific declarations** from the application. Where a declaration + is not found, nothing is asserted about it — an absent statement is not a + false one. +- **PDF reading needs `pdfplumber`**, which `requirements.txt` installs. An + earlier version shelled out to `pdftotext`, a Unix binary: it worked on the + machine it was written on and died with a bare `WinError 2` on the machine it + was used on. Where no reader is available the CSV path is unaffected and the + failure names the fix instead of raising a traceback. +- **R7 needs description overlap**, not just a matching date and amount. An + early version flagged any two claims sharing a date and figure, which on a + fleet account is coincidence rather than duplication. Found by the + clean-corpus test, and the rule was fixed rather than the test. +- **No coverage recommendation.** The app summarises and flags; it does not + advise whether to write the risk, and the chat instruction forbids adding one. + +Portions of this work were written with AI assistance, directed and reviewed +by me. diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/MANIFEST.md b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/MANIFEST.md new file mode 100644 index 00000000..00542b9e --- /dev/null +++ b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/MANIFEST.md @@ -0,0 +1,130 @@ +# MANIFEST — ground truth for the synthetic loss-run corpus + +Everything in this corpus is fabricated. Meridian Cold Chain Logistics, Kestrel +Mutual Insurance and Ardent Risk Partners do not exist. No figure is drawn from +any real claim, policy or company. + +This file is the answer key. A system reading this corpus should surface the +conflicts and findings below, and should surface **nothing that is not on this +list**. Both halves matter: a corpus with a known-clean remainder is what makes +a false positive visible. + +Regenerate with `python generate.py`. The seed is fixed, so the corpus is +byte-stable. + +## Contents + +| file | format | role | +|---|---|---| +| `loss-runs/loss-run-2021-valued-2024-01.csv` | CSV | policy year 2021, 12 claims | +| `loss-runs/loss-run-2022-valued-2024-01.csv` | CSV | policy year 2022, 12 claims | +| `loss-runs/loss-run-2022-valued-2024-01.pdf` | PDF | same content, different format | +| `loss-runs/loss-run-2023-valued-2024-01-ORIGINAL.csv` | CSV | **superseded** 2023 run, 9 claims | +| `loss-runs/loss-run-2023-valued-2026-01-REISSUE.csv` | CSV | **authoritative** 2023 run, 10 claims | +| `loss-runs/loss-run-2024-valued-2026-01.csv` | CSV | policy year 2024, 7 claims | +| `adjuster-notes/*.docx` | DOCX | three claim file notes | +| `application/meridian-renewal-application-2026.docx` | DOCX | signed renewal application | +| `correspondence/ardent-cover-note-2026-01-28.docx` | DOCX | broker cover note | +| `rules/underwriting-checklist.docx` | DOCX | the eight rules to examine against | + +41 claims across four policy years. + +## The conflict that changes the decision + +Claim **KM-2023-0417** (jackknife on I-80) carries **$45,000.00** incurred on the +2023 run valued January 2024, and **$182,400.00** on the reissue valued January +2026. Neither document is wrong — the reserve developed after suit was filed, +and the adjuster note sets out the history. + +The consequence is not cosmetic: + +| source for policy year 2023 | incurred | earned premium | loss ratio | rule R3 | +|---|---|---|---|---| +| original run, valued 2024-01 | $286,092.44 | $501,200 | **57.1%** | passes | +| reissue, valued 2026-01 | $647,992.44 | $501,200 | **129.3%** | **refer** | + +The same account either clears the referral threshold or blows through it twice +over, depending on which document is trusted. A system that silently picks one +has made an underwriting decision without telling anyone. This is the conflict +to surface rather than resolve. + +## Planted conflicts + +**C1 — reserve development across a reissue.** KM-2023-0417, $45,000.00 → +$182,400.00. Adjuster note dated 2025-09-30 explains it and gives the reserve +history. Consequence above. + +**C2 — status contradicted by a later note.** KM-2022-0311 is `Closed` on every +loss run. The adjuster note dated 2024-03-12 states the claim reopened on +2024-02-19 with a re-established reserve of $34,000. The note explicitly says the +loss run was correct at its valuation date and is no longer correct. The loss +runs were never reissued to reflect it. + +**C3 — claim present in the reissue and absent from the original.** +KM-2023-0588, $224,500.00, date of loss 2024-02-27, reported 2024-06-14 — after +the original run was issued. Only document where it appears first: the reissue. + +**C4 — date of loss outside its stated policy period.** KM-2022-0402 has a date +of loss of 2023-04-11 but is filed under policy year 2022, which ran to +2023-04-01. Triggers R4. + +**C5 — one event under two claim numbers.** KM-2021-0219 and KM-2021-0224 both +describe a bridge strike on Route 6 on 2021-08-03, each at $27,450.00. The 2021 +total therefore double-counts $27,450.00. Triggers R7. + +**C6 — severity claim with no adjuster note.** KM-2024-0106, $149,000.00 +incurred, open. No note in the corpus. Triggers R1. + +**C7 — dormant reserve.** KM-2021-0333, $0.00 paid against a $96,000.00 reserve, +open 49 months at the January 2026 valuation. Triggers R2 and R6. + +**C8 — loss run that does not add up.** The original 2023 run states a total +incurred of $287,592.44; its rows sum to $286,092.44. A $1,500.00 discrepancy. +Real loss runs do this. Triggers R8. The other four runs are internally +consistent — check them and find nothing. + +**C9 — application contradicts the loss runs, three ways.** The signed +application dated 2026-01-22 states: + +| application says | loss runs show | +|---|---| +| 3 claims exceeding $50,000 in five years | **7** | +| no open claim exceeds $100,000 | **3** (KM-2023-0417, KM-2023-0588, KM-2024-0106) | +| all 2021 and 2022 claims are closed | **6 still open** | + +The broker's cover note explains why — the application was prepared in December, +before the reissued runs arrived. That explanation is in a different document +from the contradiction, which is the point. + +## Findings the checklist should produce + +Against the **authoritative** set (2021, 2022, 2023-reissue, 2024): + +| rule | expected findings | +|---|---| +| R1 adjuster note on ≥$100k | 1 — KM-2024-0106 | +| R2 open >36 months | 4 — KM-2021-0232, KM-2021-0252, KM-2021-0333, KM-2022-0237 | +| R3 loss ratio >60% | 1 — policy year 2023 at 129.3% | +| R4 policy period integrity | 1 — KM-2022-0402 | +| R5 application consistency | 3 — the rows in C9 | +| R6 dormant reserve >24 months | 1 — KM-2021-0333 | +| R7 duplicate claims | 1 — KM-2021-0219 / KM-2021-0224 | +| R8 loss run adds up | 1 — original 2023 run, $1,500.00 out | + +Loss ratios by year, for R3: 2021 **57.6%**, 2022 **41.2%**, 2023 **129.3%**, +2024 **33.3%**. Three of four pass, which is what makes the fourth mean +something. + +## Deliberately clean + +The other 34 claims carry no planted defect. Every one of the four policy years +except 2023 is internally consistent, under the loss-ratio threshold, and free +of period or duplicate problems. Anything a system reports outside the table +above is a false positive, and on this corpus that is measurable rather than a +matter of opinion. + +## A note on formats + +The 2022 loss run exists as both CSV and PDF with identical content. A system +that claims to handle mixed formats can be checked on it: the same policy year, +read two ways, must produce the same facts. diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/generate.py b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/generate.py new file mode 100644 index 00000000..a9d67b82 --- /dev/null +++ b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/generate.py @@ -0,0 +1,490 @@ +"""Synthetic insurance loss-run corpus. + +Everything here is fabricated. Meridian Cold Chain Logistics, Kestrel Mutual +and Ardent Risk Partners do not exist, and no figure is drawn from any real +claim, policy or company. + +Why loss runs. A loss run is a carrier's claims history for one policy year. +The thing that makes them hard in real life is that they get reissued: the same +claim shows one incurred amount in the run issued in 2024 and a much larger one +in the run issued in 2026, because reserves develop as a claim matures. Neither +document is wrong. They disagree, and somebody has to know which one they are +reading. That gives a document pile with native, non-contrived disagreement. + +The conflicts below are planted deliberately and are documented in MANIFEST.md. +That file is ground truth: a system that reads this corpus should find them, and +should find nothing that is not on the list. + +Run: python generate.py +""" + +from __future__ import annotations + +import csv +import os +import random +import subprocess +from dataclasses import dataclass, field + +OUT = "out" +SEED = 20260805 + +INSURED = "Meridian Cold Chain Logistics, Inc." +INSURED_SHORT = "Meridian Cold Chain" +CARRIER = "Kestrel Mutual Insurance Company" +CARRIER_SHORT = "Kestrel Mutual" +BROKER = "Ardent Risk Partners" +FEIN = "88-0143927" + +COVERAGES = { + "AL": "Auto Liability", + "APD": "Auto Physical Damage", + "CARGO": "Motor Truck Cargo", + "WC": "Workers Compensation", + "GL": "General Liability", + "PROP": "Property", +} + +POLICIES = { + 2021: ("KM-CA-2021-88417", "2021-04-01", "2022-04-01", 412_000), + 2022: ("KM-CA-2022-88417", "2022-04-01", "2023-04-01", 448_500), + 2023: ("KM-CA-2023-88417", "2023-04-01", "2024-04-01", 501_200), + 2024: ("KM-CA-2024-88417", "2024-04-01", "2025-04-01", 566_900), +} + + +@dataclass +class Claim: + claim_no: str + year: int + coverage: str + date_of_loss: str + date_reported: str + description: str + status: str # Open | Closed | Reopened + paid: float + reserve: float + claimant: str = "Third party" + note: str = "" + # Values as they appeared in the ORIGINAL run, when a later reissue differs + original: dict = field(default_factory=dict) + + @property + def incurred(self) -> float: + return round(self.paid + self.reserve, 2) + + +# --------------------------------------------------------------------------- +# Claims that carry a planted conflict. Explicit, never random. +# --------------------------------------------------------------------------- + +ANCHOR_CLAIMS: list[Claim] = [ + # C1 reserve development: modest at first issue, severe two years later + Claim("KM-2023-0417", 2023, "AL", "2023-11-08", "2023-11-09", + "Tractor-trailer jackknife on I-80, two vehicles involved, " + "bodily injury alleged by both occupants of the second vehicle.", + "Open", 12_400.00, 170_000.00, "Third party", + note="Severity reassessed after plaintiff filed suit.", + original={"paid": 8_150.00, "reserve": 36_850.00, "status": "Open"}), + + # C2 status flip: closed on the run, reopened per the adjuster note + Claim("KM-2022-0311", 2022, "WC", "2022-09-19", "2022-09-19", + "Warehouse associate lumbar strain, pallet jack incident.", + "Closed", 41_280.00, 0.00, "Employee", + note="Claimant returned with recurrence; see adjuster note.", + original={}), + + # C3 late-reported claim: absent from the original 2023 run entirely + Claim("KM-2023-0588", 2023, "CARGO", "2024-02-27", "2024-06-14", + "Refrigeration unit failure in transit, full load of pharmaceuticals " + "condemned on arrival.", + "Open", 0.00, 224_500.00, "Shipper", + note="Reported after the original loss run was issued.", + original={"absent": True}), + + # C4 date of loss falls outside the policy period it is filed under + Claim("KM-2022-0402", 2022, "GL", "2023-04-11", "2023-04-19", + "Slip and fall, visitor at the Fresno cross-dock.", + "Closed", 18_900.00, 0.00, "Third party", + note="Date of loss sits after the 2022 policy expiry."), + + # C5 same event carried under two claim numbers + Claim("KM-2021-0219", 2021, "APD", "2021-08-03", "2021-08-04", + "Reefer trailer sidewall damage, low bridge strike, Route 6.", + "Closed", 27_450.00, 0.00, "First party"), + Claim("KM-2021-0224", 2021, "APD", "2021-08-03", "2021-08-11", + "Trailer damage from bridge strike on Route 6 - unit 4471.", + "Closed", 27_450.00, 0.00, "First party", + note="Appears to duplicate KM-2021-0219."), + + # C6 large claim with no adjuster note on file + Claim("KM-2024-0106", 2024, "AL", "2024-06-22", "2024-06-23", + "Rear-end collision, company tractor, claimant hospitalised.", + "Open", 31_000.00, 118_000.00, "Third party"), + + # C7 long-open claim, zero paid against a standing reserve + Claim("KM-2021-0333", 2021, "GL", "2021-12-02", "2021-12-15", + "Alleged product contamination, retailer recall costs claimed.", + "Open", 0.00, 96_000.00, "Third party", + note="No payment activity since inception."), +] + +FILLER_DESCRIPTIONS = { + "AL": ["Minor collision in yard, no injuries", + "Sideswipe during lane change, property damage only", + "Backing incident at customer dock"], + "APD": ["Windshield replacement, road debris", + "Tractor door damage, parking lot", + "Tyre blowout, wheel arch damage"], + "CARGO": ["Partial load spoilage, temperature excursion", + "Carton crush damage in transit", + "Short delivery, three pallets unaccounted"], + "WC": ["Hand laceration, box cutter", + "Slip on wet dock plate, contusion", + "Repetitive strain, order picking"], + "GL": ["Damage to customer dock leveller", + "Minor property damage at delivery site"], + "PROP": ["Roof leak, Bakersfield warehouse", + "Freezer motor burnout, electrical surge"], +} + + +def build_filler(rng: random.Random) -> list[Claim]: + claims: list[Claim] = [] + per_year = {2021: 9, 2022: 10, 2023: 8, 2024: 6} + for year, n in per_year.items(): + _, start, _, _ = POLICIES[year] + for i in range(n): + cov = rng.choice(list(FILLER_DESCRIPTIONS)) + month = rng.randint(4, 12) if rng.random() < 0.7 else rng.randint(1, 3) + y = year if month >= 4 else year + 1 + dol = f"{y}-{month:02d}-{rng.randint(1, 28):02d}" + lag = rng.randint(0, 9) + rep_day = min(28, int(dol[-2:]) + lag) + drep = f"{dol[:-2]}{rep_day:02d}" + closed = rng.random() < (0.9 if year <= 2022 else 0.55) + size = rng.choice([1, 1, 1, 2, 2, 3]) + base = {1: (400, 4_000), 2: (4_000, 22_000), 3: (22_000, 65_000)}[size] + total = round(rng.uniform(*base), 2) + paid = total if closed else round(total * rng.uniform(0.05, 0.45), 2) + reserve = 0.0 if closed else round(total - paid, 2) + claims.append(Claim( + claim_no=f"KM-{year}-{200 + i * 7 + rng.randint(0, 4):04d}", + year=year, coverage=cov, date_of_loss=dol, date_reported=drep, + description=rng.choice(FILLER_DESCRIPTIONS[cov]), + status="Closed" if closed else "Open", + paid=paid, reserve=reserve, + claimant="Employee" if cov == "WC" else + ("First party" if cov in ("APD", "PROP") else "Third party"), + )) + return claims + + +# --------------------------------------------------------------------------- +# Loss run writers +# --------------------------------------------------------------------------- + +HEADER = ["Claim Number", "Policy Number", "Policy Year", "Coverage", + "Date of Loss", "Date Reported", "Status", "Claimant", + "Paid", "Reserve", "Incurred", "Description"] + + +def rows_for(claims: list[Claim], year: int, reissue: bool) -> list[list]: + pol = POLICIES[year][0] + out = [] + for c in sorted((c for c in claims if c.year == year), + key=lambda c: c.date_of_loss): + if not reissue and c.original.get("absent"): + continue + paid = c.paid + reserve = c.reserve + status = c.status + if not reissue and c.original: + paid = c.original.get("paid", paid) + reserve = c.original.get("reserve", reserve) + status = c.original.get("status", status) + out.append([ + c.claim_no, pol, year, COVERAGES[c.coverage], c.date_of_loss, + c.date_reported, status, c.claimant, + f"{paid:,.2f}", f"{reserve:,.2f}", f"{paid + reserve:,.2f}", + c.description, + ]) + return out + + +def write_loss_run(path: str, year: int, claims: list[Claim], issued: str, + reissue: bool, footer_error: float = 0.0) -> None: + _, start, end, premium = POLICIES[year] + rows = rows_for(claims, year, reissue) + total = sum(float(r[10].replace(",", "")) for r in rows) + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", newline="", encoding="utf-8") as fh: + w = csv.writer(fh) + w.writerow([f"{CARRIER} - Loss Run Report"]) + w.writerow([f"Insured: {INSURED}"]) + w.writerow([f"FEIN: {FEIN}"]) + w.writerow([f"Policy Period: {start} to {end}"]) + w.writerow([f"Valuation Date: {issued}"]) + w.writerow([f"Report Type: {'REISSUE - supersedes prior' if reissue else 'Original'}"]) + w.writerow([]) + w.writerow(HEADER) + w.writerows(rows) + w.writerow([]) + # Real loss runs frequently carry a footer total that does not equal + # the sum of the rows above it. One year here reproduces that. + w.writerow(["", "", "", "", "", "", "", "TOTAL INCURRED", + "", "", f"{total + footer_error:,.2f}", ""]) + w.writerow(["", "", "", "", "", "", "", "CLAIM COUNT", + "", "", len(rows), ""]) + w.writerow(["", "", "", "", "", "", "", "EARNED PREMIUM", + "", "", f"{premium:,.2f}", ""]) + + +# --------------------------------------------------------------------------- +# DOCX writers +# --------------------------------------------------------------------------- + +def docx(path: str, blocks: list[tuple[str, str]]) -> None: + from docx import Document + from docx.shared import Pt + + d = Document() + d.styles["Normal"].font.name = "Calibri" + d.styles["Normal"].font.size = Pt(10.5) + for kind, text in blocks: + if kind == "h1": + d.add_heading(text, level=1) + elif kind == "h2": + d.add_heading(text, level=2) + elif kind == "b": + p = d.add_paragraph() + p.add_run(text).bold = True + else: + d.add_paragraph(text) + os.makedirs(os.path.dirname(path), exist_ok=True) + d.save(path) + + +def write_adjuster_notes() -> None: + docx(f"{OUT}/adjuster-notes/KM-2023-0417-adjuster-note.docx", [ + ("h1", "Claim file note"), + ("b", f"Claim: KM-2023-0417 Insured: {INSURED_SHORT} Coverage: Auto Liability"), + ("p", "Date of note: 2025-09-30. Adjuster: R. Okonjo, Kestrel Mutual."), + ("h2", "Position"), + ("p", "Reserve increased to $170,000 following receipt of the plaintiff's " + "demand and the treating physician's report. The earlier reserve of " + "$36,850 was set before suit was filed and reflected an assumption " + "of soft-tissue injury only. That assumption no longer holds."), + ("p", "Liability is not seriously in dispute. The insured unit crossed the " + "centre line. Exposure is a question of damages, not fault."), + ("h2", "Reserve history"), + ("p", "2023-11-15: $36,850 initial. 2024-08-02: $58,000. " + "2025-09-30: $170,000 current."), + ("h2", "Next steps"), + ("p", "Mediation scheduled Q2 2026. Recommend authority to $200,000."), + ]) + + docx(f"{OUT}/adjuster-notes/KM-2022-0311-adjuster-note.docx", [ + ("h1", "Claim file note"), + ("b", f"Claim: KM-2022-0311 Insured: {INSURED_SHORT} Coverage: Workers Compensation"), + ("p", "Date of note: 2024-03-12. Adjuster: L. Haddad, Kestrel Mutual."), + ("h2", "Position"), + ("p", "This file was closed on 2023-06-30 after the claimant returned to " + "full duty. The claimant has since reported a recurrence of the same " + "lumbar symptoms and has been placed on modified duty as of " + "2024-02-19. The file is reopened."), + ("p", "Note that the loss run issued in January 2024 shows this claim as " + "closed. That was correct at its valuation date and is no longer " + "correct."), + ("h2", "Revised position"), + ("p", "Reserve re-established at $34,000 for continued indemnity and " + "medical. Paid to date remains $41,280."), + ]) + + docx(f"{OUT}/adjuster-notes/KM-2023-0588-adjuster-note.docx", [ + ("h1", "Claim file note"), + ("b", f"Claim: KM-2023-0588 Insured: {INSURED_SHORT} Coverage: Motor Truck Cargo"), + ("p", "Date of note: 2024-07-01. Adjuster: R. Okonjo, Kestrel Mutual."), + ("h2", "Position"), + ("p", "Temperature excursion on a pharmaceutical load. The consignee " + "condemned the entire shipment on arrival. Salvage value is nil " + "because the product cannot re-enter the cold chain once excursion " + "is documented."), + ("p", "Date of loss 2024-02-27 falls within the 2023 policy period. The " + "claim was reported on 2024-06-14, after the original loss run for " + "that year had been issued, and therefore does not appear on it."), + ("h2", "Reserve"), + ("p", "$224,500, being the invoiced value of the load. Subrogation against " + "the reefer unit manufacturer is under review."), + ]) + + +def write_application() -> None: + docx(f"{OUT}/application/meridian-renewal-application-2026.docx", [ + ("h1", "Commercial insurance renewal application"), + ("b", f"Applicant: {INSURED}"), + ("p", f"FEIN: {FEIN}. Broker of record: {BROKER}. " + "Proposed effective date: 2026-04-01."), + ("h2", "Operations"), + ("p", "Refrigerated truckload and less-than-truckload carriage, with three " + "cross-dock facilities in California and one in Nevada. Power units: " + "64. Refrigerated trailers: 91. Employees: 212."), + ("h2", "Loss history - applicant statement"), + ("p", "The applicant reports 3 claims exceeding $50,000 in the last five " + "policy years, and confirms that no claim currently open exceeds " + "$100,000 in incurred value."), + ("p", "The applicant further reports that all claims from policy years " + "2021 and 2022 are closed."), + ("h2", "Prior carrier"), + ("p", f"{CARRIER}, continuously since 2021-04-01. No lapse in coverage. " + "No policy has been cancelled or non-renewed."), + ("h2", "Declaration"), + ("p", "The undersigned declares the statements above to be true to the " + "best of their knowledge. Signed: J. Ferreira, Chief Financial " + "Officer. Date: 2026-01-22."), + ]) + + +def write_broker_email() -> None: + docx(f"{OUT}/correspondence/ardent-cover-note-2026-01-28.docx", [ + ("h1", "Submission cover note"), + ("b", f"From: {BROKER} To: Underwriting Date: 2026-01-28"), + ("p", f"Please find attached the renewal submission for {INSURED_SHORT}, " + "effective 2026-04-01."), + ("p", "Enclosed: loss runs for policy years 2021 through 2024 valued " + "January 2026, a reissued 2023 loss run, three adjuster notes, and " + "the signed application."), + ("p", "Two points the underwriter should have in front of them. First, the " + "2023 loss run has been reissued and the incurred figures differ " + "materially from the version circulated in 2024; the reissue is the " + "one to work from. Second, the client's application was prepared in " + "December before the reissued runs arrived, so the loss summary in " + "it reflects the earlier figures."), + ("p", "The client is seeking terms at or below expiring. Happy to discuss."), + ("p", "Regards, D. Whitfield, Account Executive, " + BROKER), + ]) + + +def write_checklist() -> None: + docx(f"{OUT}/rules/underwriting-checklist.docx", [ + ("h1", "Casualty underwriting checklist - motor carrier accounts"), + ("p", "Applied to every submission before a coverage decision. Each rule " + "produces a finding or an explicit pass."), + ("h2", "R1 - Adjuster note required on severity claims"), + ("p", "Any claim with incurred value of $100,000 or more must have an " + "adjuster note in the file. A severity claim without a note cannot " + "be evaluated and must be referred."), + ("h2", "R2 - Stale open claims"), + ("p", "Any claim open more than 36 months from date of loss is flagged for " + "reserve adequacy review."), + ("h2", "R3 - Loss ratio referral"), + ("p", "Any policy year with incurred losses exceeding 60% of earned " + "premium is referred to a senior underwriter."), + ("h2", "R4 - Policy period integrity"), + ("p", "A claim whose date of loss falls outside the policy period it is " + "reported under must be queried with the carrier before the figures " + "are relied on."), + ("h2", "R5 - Application consistency"), + ("p", "The claim counts and severity thresholds stated on the application " + "must agree with the loss runs. Any disagreement is recorded and put " + "to the broker."), + ("h2", "R6 - Dormant reserves"), + ("p", "A claim with zero paid and a non-zero reserve, open more than 24 " + "months, is flagged for reserve review."), + ("h2", "R7 - Duplicate claims"), + ("p", "The same loss event must not be carried under two claim numbers. " + "Suspected duplicates are queried before totals are relied on."), + ("h2", "R8 - Loss run internal consistency"), + ("p", "The total incurred stated on a loss run must equal the sum of its " + "claim rows. A discrepancy is recorded against the document."), + ]) + + +def write_pdf_loss_run(year: int, claims: list[Claim], issued: str, + out_dir: str) -> None: + """The same policy year as a formatted PDF report. + + Carriers issue loss runs both ways. Converting the wide CSV directly gives + a truncated three-column page, which is not what a carrier PDF looks like; + this builds a proper landscape table with the columns an underwriter reads, + then renders it. + """ + from docx import Document + from docx.enum.section import WD_ORIENT + from docx.shared import Pt, Inches + + _, start, end, premium = POLICIES[year] + rows = rows_for(claims, year, reissue=False) + + d = Document() + sec = d.sections[0] + sec.orientation = WD_ORIENT.LANDSCAPE + sec.page_width, sec.page_height = sec.page_height, sec.page_width + sec.left_margin = sec.right_margin = Inches(0.5) + d.styles["Normal"].font.name = "Calibri" + d.styles["Normal"].font.size = Pt(8) + + d.add_paragraph(f"{CARRIER} - Loss Run Report").runs[0].bold = True + for line in (f"Insured: {INSURED}", f"FEIN: {FEIN}", + f"Policy Period: {start} to {end}", + f"Valuation Date: {issued}", "Report Type: Original"): + d.add_paragraph(line) + + cols = ["Claim Number", "Coverage", "Date of Loss", "Date Reported", + "Status", "Paid", "Reserve", "Incurred"] + idx = [0, 3, 4, 5, 6, 8, 9, 10] + t = d.add_table(rows=1, cols=len(cols)) + t.style = "Table Grid" + for i, c in enumerate(cols): + cell = t.rows[0].cells[i] + cell.text = c + cell.paragraphs[0].runs[0].bold = True + for r in rows: + cells = t.add_row().cells + for i, j in enumerate(idx): + cells[i].text = str(r[j]) + + total = sum(float(r[10].replace(",", "")) for r in rows) + d.add_paragraph("") + d.add_paragraph(f"TOTAL INCURRED: {total:,.2f}") + d.add_paragraph(f"CLAIM COUNT: {len(rows)}") + d.add_paragraph(f"EARNED PREMIUM: {premium:,.2f}") + + os.makedirs(out_dir, exist_ok=True) + docx_path = os.path.join(out_dir, f"loss-run-{year}-valued-{issued[:7]}.docx") + d.save(docx_path) + subprocess.run( + ["soffice", "--headless", "--convert-to", "pdf", "--outdir", out_dir, + docx_path], + check=False, capture_output=True, timeout=180, + ) + os.remove(docx_path) + + +def main() -> None: + rng = random.Random(SEED) + claims = ANCHOR_CLAIMS + build_filler(rng) + + write_loss_run(f"{OUT}/loss-runs/loss-run-2021-valued-2024-01.csv", + 2021, claims, "2024-01-15", reissue=False) + write_loss_run(f"{OUT}/loss-runs/loss-run-2022-valued-2024-01.csv", + 2022, claims, "2024-01-15", reissue=False) + write_loss_run(f"{OUT}/loss-runs/loss-run-2023-valued-2024-01-ORIGINAL.csv", + 2023, claims, "2024-01-15", reissue=False, + footer_error=1_500.00) + write_loss_run(f"{OUT}/loss-runs/loss-run-2023-valued-2026-01-REISSUE.csv", + 2023, claims, "2026-01-09", reissue=True) + write_loss_run(f"{OUT}/loss-runs/loss-run-2024-valued-2026-01.csv", + 2024, claims, "2026-01-09", reissue=True) + + write_adjuster_notes() + write_application() + write_broker_email() + write_checklist() + write_pdf_loss_run(2022, claims, "2024-01-15", f"{OUT}/loss-runs") + + print(f"corpus written to {OUT}/ ({len(claims)} claims across 4 policy years)") + + +if __name__ == "__main__": + main() diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/adjuster-notes/KM-2022-0311-adjuster-note.docx b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/adjuster-notes/KM-2022-0311-adjuster-note.docx new file mode 100644 index 0000000000000000000000000000000000000000..1d571d6c349dd59409a3d26fa8842d2ad81e3558 GIT binary patch literal 37048 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 literal 0 HcmV?d00001 diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/adjuster-notes/KM-2023-0417-adjuster-note.docx b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/adjuster-notes/KM-2023-0417-adjuster-note.docx new file mode 100644 index 0000000000000000000000000000000000000000..429133c74d252ca2157b118b66ab85304eb9fdc2 GIT binary patch literal 37097 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 TD=-Iva+M%MA~1Ut^?-N)GhQVH literal 0 HcmV?d00001 diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/adjuster-notes/KM-2023-0588-adjuster-note.docx b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/adjuster-notes/KM-2023-0588-adjuster-note.docx new file mode 100644 index 0000000000000000000000000000000000000000..449efe3022154119b213bd2bba792c1933622d0f GIT binary patch literal 37072 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 u2vg29K|=+l=D@m;sF3?`3}ke literal 0 HcmV?d00001 diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/application/meridian-renewal-application-2026.docx b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/application/meridian-renewal-application-2026.docx new file mode 100644 index 0000000000000000000000000000000000000000..98410776a433c3f3fe71af9d93692ce16363deb8 GIT binary patch literal 37219 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 Un-!RYK)FhgArY88ihDpj0BBq-U;qFB literal 0 HcmV?d00001 diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2021-valued-2024-01.csv b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2021-valued-2024-01.csv new file mode 100644 index 00000000..2e8430fb --- /dev/null +++ b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2021-valued-2024-01.csv @@ -0,0 +1,24 @@ +Kestrel Mutual Insurance Company - Loss Run Report +"Insured: Meridian Cold Chain Logistics, Inc." +FEIN: 88-0143927 +Policy Period: 2021-04-01 to 2022-04-01 +Valuation Date: 2024-01-15 +Report Type: Original + +Claim Number,Policy Number,Policy Year,Coverage,Date of Loss,Date Reported,Status,Claimant,Paid,Reserve,Incurred,Description +KM-2021-0225,KM-CA-2021-88417,2021,Workers Compensation,2021-04-13,2021-04-13,Closed,Employee,"15,001.60",0.00,"15,001.60","Repetitive strain, order picking" +KM-2021-0258,KM-CA-2021-88417,2021,General Liability,2021-05-27,2021-05-28,Closed,Third party,"19,941.90",0.00,"19,941.90",Damage to customer dock leveller +KM-2021-0232,KM-CA-2021-88417,2021,Workers Compensation,2021-06-07,2021-06-08,Open,Employee,"6,185.43","9,757.81","15,943.24","Repetitive strain, order picking" +KM-2021-0219,KM-CA-2021-88417,2021,Auto Physical Damage,2021-08-03,2021-08-04,Closed,First party,"27,450.00",0.00,"27,450.00","Reefer trailer sidewall damage, low bridge strike, Route 6." +KM-2021-0224,KM-CA-2021-88417,2021,Auto Physical Damage,2021-08-03,2021-08-11,Closed,First party,"27,450.00",0.00,"27,450.00",Trailer damage from bridge strike on Route 6 - unit 4471. +KM-2021-0218,KM-CA-2021-88417,2021,Auto Physical Damage,2021-11-01,2021-11-02,Closed,First party,"3,316.72",0.00,"3,316.72","Tyre blowout, wheel arch damage" +KM-2021-0235,KM-CA-2021-88417,2021,Auto Liability,2021-11-09,2021-11-15,Closed,Third party,"4,841.71",0.00,"4,841.71",Backing incident at customer dock +KM-2021-0252,KM-CA-2021-88417,2021,Property,2021-11-22,2021-11-28,Open,First party,37.13,587.27,624.40,"Freezer motor burnout, electrical surge" +KM-2021-0333,KM-CA-2021-88417,2021,General Liability,2021-12-02,2021-12-15,Open,Third party,0.00,"96,000.00","96,000.00","Alleged product contamination, retailer recall costs claimed." +KM-2021-0201,KM-CA-2021-88417,2021,General Liability,2022-01-27,2022-01-28,Closed,Third party,"2,965.48",0.00,"2,965.48",Damage to customer dock leveller +KM-2021-0209,KM-CA-2021-88417,2021,General Liability,2022-02-20,2022-02-26,Closed,Third party,"19,921.04",0.00,"19,921.04",Minor property damage at delivery site +KM-2021-0243,KM-CA-2021-88417,2021,Auto Liability,2022-03-06,2022-03-09,Closed,Third party,"3,824.26",0.00,"3,824.26","Sideswipe during lane change, property damage only" + +,,,,,,,TOTAL INCURRED,,,"237,280.35", +,,,,,,,CLAIM COUNT,,,12, +,,,,,,,EARNED PREMIUM,,,"412,000.00", diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2022-valued-2024-01.csv b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2022-valued-2024-01.csv new file mode 100644 index 00000000..a74c06b4 --- /dev/null +++ b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2022-valued-2024-01.csv @@ -0,0 +1,24 @@ +Kestrel Mutual Insurance Company - Loss Run Report +"Insured: Meridian Cold Chain Logistics, Inc." +FEIN: 88-0143927 +Policy Period: 2022-04-01 to 2023-04-01 +Valuation Date: 2024-01-15 +Report Type: Original + +Claim Number,Policy Number,Policy Year,Coverage,Date of Loss,Date Reported,Status,Claimant,Paid,Reserve,Incurred,Description +KM-2022-0246,KM-CA-2022-88417,2022,General Liability,2022-04-22,2022-04-24,Closed,Third party,"3,758.58",0.00,"3,758.58",Minor property damage at delivery site +KM-2022-0267,KM-CA-2022-88417,2022,Auto Physical Damage,2022-06-19,2022-06-22,Closed,First party,"17,714.07",0.00,"17,714.07","Tractor door damage, parking lot" +KM-2022-0232,KM-CA-2022-88417,2022,Auto Physical Damage,2022-08-01,2022-08-09,Closed,First party,"3,307.82",0.00,"3,307.82","Tractor door damage, parking lot" +KM-2022-0214,KM-CA-2022-88417,2022,Workers Compensation,2022-08-02,2022-08-06,Closed,Employee,"17,289.63",0.00,"17,289.63","Repetitive strain, order picking" +KM-2022-0237,KM-CA-2022-88417,2022,Auto Physical Damage,2022-08-17,2022-08-25,Open,First party,"8,535.28","15,191.06","23,726.34","Tyre blowout, wheel arch damage" +KM-2022-0311,KM-CA-2022-88417,2022,Workers Compensation,2022-09-19,2022-09-19,Closed,Employee,"41,280.00",0.00,"41,280.00","Warehouse associate lumbar strain, pallet jack incident." +KM-2022-0258,KM-CA-2022-88417,2022,Auto Physical Damage,2023-01-19,2023-01-26,Open,First party,666.02,"1,913.46","2,579.48","Tractor door damage, parking lot" +KM-2022-0252,KM-CA-2022-88417,2022,Property,2023-02-05,2023-02-10,Open,First party,749.80,"2,476.00","3,225.80","Roof leak, Bakersfield warehouse" +KM-2022-0203,KM-CA-2022-88417,2022,Auto Physical Damage,2023-02-15,2023-02-22,Closed,First party,"14,349.16",0.00,"14,349.16","Tractor door damage, parking lot" +KM-2022-0209,KM-CA-2022-88417,2022,Auto Liability,2023-02-15,2023-02-15,Closed,Third party,"34,851.06",0.00,"34,851.06","Sideswipe during lane change, property damage only" +KM-2022-0224,KM-CA-2022-88417,2022,Workers Compensation,2023-02-20,2023-02-25,Closed,Employee,"3,712.44",0.00,"3,712.44","Slip on wet dock plate, contusion" +KM-2022-0402,KM-CA-2022-88417,2022,General Liability,2023-04-11,2023-04-19,Closed,Third party,"18,900.00",0.00,"18,900.00","Slip and fall, visitor at the Fresno cross-dock." + +,,,,,,,TOTAL INCURRED,,,"184,694.38", +,,,,,,,CLAIM COUNT,,,12, +,,,,,,,EARNED PREMIUM,,,"448,500.00", diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2022-valued-2024-01.pdf b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2022-valued-2024-01.pdf new file mode 100644 index 0000000000000000000000000000000000000000..adf99987613f4e1408aecc1fd58056fc4f43b586 GIT binary patch literal 57030 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 F{{!n*f-C?4 literal 0 HcmV?d00001 diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2023-valued-2024-01-ORIGINAL.csv b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2023-valued-2024-01-ORIGINAL.csv new file mode 100644 index 00000000..72eb4529 --- /dev/null +++ b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2023-valued-2024-01-ORIGINAL.csv @@ -0,0 +1,21 @@ +Kestrel Mutual Insurance Company - Loss Run Report +"Insured: Meridian Cold Chain Logistics, Inc." +FEIN: 88-0143927 +Policy Period: 2023-04-01 to 2024-04-01 +Valuation Date: 2024-01-15 +Report Type: Original + +Claim Number,Policy Number,Policy Year,Coverage,Date of Loss,Date Reported,Status,Claimant,Paid,Reserve,Incurred,Description +KM-2023-0221,KM-CA-2023-88417,2023,Workers Compensation,2023-04-18,2023-04-24,Closed,Employee,"3,370.06",0.00,"3,370.06","Repetitive strain, order picking" +KM-2023-0217,KM-CA-2023-88417,2023,General Liability,2023-06-15,2023-06-22,Closed,Third party,"53,536.94",0.00,"53,536.94",Minor property damage at delivery site +KM-2023-0204,KM-CA-2023-88417,2023,Auto Liability,2023-07-20,2023-07-24,Open,Third party,"2,909.95","5,026.84","7,936.79",Backing incident at customer dock +KM-2023-0250,KM-CA-2023-88417,2023,Auto Liability,2023-09-21,2023-09-28,Closed,Third party,"61,283.59",0.00,"61,283.59","Sideswipe during lane change, property damage only" +KM-2023-0417,KM-CA-2023-88417,2023,Auto Liability,2023-11-08,2023-11-09,Open,Third party,"8,150.00","36,850.00","45,000.00","Tractor-trailer jackknife on I-80, two vehicles involved, bodily injury alleged by both occupants of the second vehicle." +KM-2023-0209,KM-CA-2023-88417,2023,General Liability,2023-11-12,2023-11-19,Open,Third party,754.74,"3,941.25","4,695.99",Damage to customer dock leveller +KM-2023-0245,KM-CA-2023-88417,2023,Auto Physical Damage,2023-12-06,2023-12-07,Open,First party,"6,651.70","37,366.31","44,018.01","Windshield replacement, road debris" +KM-2023-0237,KM-CA-2023-88417,2023,Workers Compensation,2024-01-22,2024-01-28,Open,Employee,257.11,"1,905.54","2,162.65","Hand laceration, box cutter" +KM-2023-0230,KM-CA-2023-88417,2023,Workers Compensation,2024-02-17,2024-02-22,Open,Employee,"11,619.63","52,468.78","64,088.41","Hand laceration, box cutter" + +,,,,,,,TOTAL INCURRED,,,"287,592.44", +,,,,,,,CLAIM COUNT,,,9, +,,,,,,,EARNED PREMIUM,,,"501,200.00", diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2023-valued-2026-01-REISSUE.csv b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2023-valued-2026-01-REISSUE.csv new file mode 100644 index 00000000..e7782001 --- /dev/null +++ b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2023-valued-2026-01-REISSUE.csv @@ -0,0 +1,22 @@ +Kestrel Mutual Insurance Company - Loss Run Report +"Insured: Meridian Cold Chain Logistics, Inc." +FEIN: 88-0143927 +Policy Period: 2023-04-01 to 2024-04-01 +Valuation Date: 2026-01-09 +Report Type: REISSUE - supersedes prior + +Claim Number,Policy Number,Policy Year,Coverage,Date of Loss,Date Reported,Status,Claimant,Paid,Reserve,Incurred,Description +KM-2023-0221,KM-CA-2023-88417,2023,Workers Compensation,2023-04-18,2023-04-24,Closed,Employee,"3,370.06",0.00,"3,370.06","Repetitive strain, order picking" +KM-2023-0217,KM-CA-2023-88417,2023,General Liability,2023-06-15,2023-06-22,Closed,Third party,"53,536.94",0.00,"53,536.94",Minor property damage at delivery site +KM-2023-0204,KM-CA-2023-88417,2023,Auto Liability,2023-07-20,2023-07-24,Open,Third party,"2,909.95","5,026.84","7,936.79",Backing incident at customer dock +KM-2023-0250,KM-CA-2023-88417,2023,Auto Liability,2023-09-21,2023-09-28,Closed,Third party,"61,283.59",0.00,"61,283.59","Sideswipe during lane change, property damage only" +KM-2023-0417,KM-CA-2023-88417,2023,Auto Liability,2023-11-08,2023-11-09,Open,Third party,"12,400.00","170,000.00","182,400.00","Tractor-trailer jackknife on I-80, two vehicles involved, bodily injury alleged by both occupants of the second vehicle." +KM-2023-0209,KM-CA-2023-88417,2023,General Liability,2023-11-12,2023-11-19,Open,Third party,754.74,"3,941.25","4,695.99",Damage to customer dock leveller +KM-2023-0245,KM-CA-2023-88417,2023,Auto Physical Damage,2023-12-06,2023-12-07,Open,First party,"6,651.70","37,366.31","44,018.01","Windshield replacement, road debris" +KM-2023-0237,KM-CA-2023-88417,2023,Workers Compensation,2024-01-22,2024-01-28,Open,Employee,257.11,"1,905.54","2,162.65","Hand laceration, box cutter" +KM-2023-0230,KM-CA-2023-88417,2023,Workers Compensation,2024-02-17,2024-02-22,Open,Employee,"11,619.63","52,468.78","64,088.41","Hand laceration, box cutter" +KM-2023-0588,KM-CA-2023-88417,2023,Motor Truck Cargo,2024-02-27,2024-06-14,Open,Shipper,0.00,"224,500.00","224,500.00","Refrigeration unit failure in transit, full load of pharmaceuticals condemned on arrival." + +,,,,,,,TOTAL INCURRED,,,"647,992.44", +,,,,,,,CLAIM COUNT,,,10, +,,,,,,,EARNED PREMIUM,,,"501,200.00", diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2024-valued-2026-01.csv b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2024-valued-2026-01.csv new file mode 100644 index 00000000..49eeb4ce --- /dev/null +++ b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/loss-runs/loss-run-2024-valued-2026-01.csv @@ -0,0 +1,19 @@ +Kestrel Mutual Insurance Company - Loss Run Report +"Insured: Meridian Cold Chain Logistics, Inc." +FEIN: 88-0143927 +Policy Period: 2024-04-01 to 2025-04-01 +Valuation Date: 2026-01-09 +Report Type: REISSUE - supersedes prior + +Claim Number,Policy Number,Policy Year,Coverage,Date of Loss,Date Reported,Status,Claimant,Paid,Reserve,Incurred,Description +KM-2024-0231,KM-CA-2024-88417,2024,Auto Liability,2024-06-19,2024-06-28,Closed,Third party,526.85,0.00,526.85,"Minor collision in yard, no injuries" +KM-2024-0106,KM-CA-2024-88417,2024,Auto Liability,2024-06-22,2024-06-23,Open,Third party,"31,000.00","118,000.00","149,000.00","Rear-end collision, company tractor, claimant hospitalised." +KM-2024-0202,KM-CA-2024-88417,2024,Auto Liability,2024-10-06,2024-10-11,Closed,Third party,"2,415.83",0.00,"2,415.83","Minor collision in yard, no injuries" +KM-2024-0223,KM-CA-2024-88417,2024,Auto Physical Damage,2024-10-12,2024-10-18,Closed,First party,"16,697.56",0.00,"16,697.56","Tyre blowout, wheel arch damage" +KM-2024-0238,KM-CA-2024-88417,2024,Auto Physical Damage,2024-11-22,2024-11-28,Closed,First party,"2,094.59",0.00,"2,094.59","Windshield replacement, road debris" +KM-2024-0208,KM-CA-2024-88417,2024,Auto Physical Damage,2025-02-28,2025-02-28,Closed,First party,"3,364.47",0.00,"3,364.47","Tractor door damage, parking lot" +KM-2024-0218,KM-CA-2024-88417,2024,Property,2025-03-05,2025-03-14,Open,First party,"3,537.94","11,194.31","14,732.25","Freezer motor burnout, electrical surge" + +,,,,,,,TOTAL INCURRED,,,"188,831.55", +,,,,,,,CLAIM COUNT,,,7, +,,,,,,,EARNED PREMIUM,,,"566,900.00", diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/rules/underwriting-checklist.docx b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/corpus/out/rules/underwriting-checklist.docx new file mode 100644 index 0000000000000000000000000000000000000000..d99046269266f0fa9279c583537d13ee57c83365 GIT binary patch literal 37331 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 G1@Qn+C8e+c literal 0 HcmV?d00001 diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/__init__.py b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/model.py b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/model.py new file mode 100644 index 00000000..484ed84c --- /dev/null +++ b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/model.py @@ -0,0 +1,195 @@ +"""Domain model. + +One rule governs this file and the two that follow it: **figures are parsed, +never inferred.** Claim counts, incurred totals and loss ratios in the review +document are arithmetic over rows read out of the loss runs. No model is asked +what a number is, because an underwriter cannot act on a total that might be a +plausible guess. + +What the model *is* asked is the thing arithmetic cannot do: read the adjuster +notes, the application and the broker correspondence, and surface where the +prose disagrees with the figures. That division is the whole architecture. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field, asdict +from datetime import date +from typing import Any + +OPEN_STATUSES = {"open", "reopened"} + + +@dataclass(frozen=True) +class Source: + """Where a fact came from. Every figure in the deliverable carries one.""" + + document: str + valuation_date: str | None = None + row: int | None = None + is_reissue: bool = False + + def cite(self) -> str: + bits = [self.document] + if self.valuation_date: + bits.append(f"valued {self.valuation_date}") + if self.row is not None: + bits.append(f"row {self.row}") + return ", ".join(bits) + + +@dataclass +class Claim: + claim_no: str + policy_year: int + policy_no: str + coverage: str + date_of_loss: str + date_reported: str + status: str + claimant: str + paid: float + reserve: float + incurred: float + description: str + source: Source + + @property + def is_open(self) -> bool: + return self.status.strip().lower() in OPEN_STATUSES + + def months_open(self, as_of: date) -> int: + try: + dol = date.fromisoformat(self.date_of_loss) + except ValueError: + return 0 + return (as_of.year - dol.year) * 12 + as_of.month - dol.month + + def to_dict(self) -> dict[str, Any]: + d = asdict(self) + d["source"] = self.source.cite() + return d + + +@dataclass +class LossRun: + """One loss run document, as read.""" + + path: str + policy_year: int + policy_no: str + period_start: str + period_end: str + valuation_date: str + is_reissue: bool + claims: list[Claim] = field(default_factory=list) + stated_total: float | None = None + stated_count: int | None = None + earned_premium: float | None = None + + @property + def summed_total(self) -> float: + return round(sum(c.incurred for c in self.claims), 2) + + @property + def label(self) -> str: + kind = "reissue" if self.is_reissue else "original" + return f"PY{self.policy_year} {kind} valued {self.valuation_date}" + + +@dataclass +class Conflict: + """Two sources say different things. Surfaced, never resolved silently.""" + + kind: str + subject: str + left: str + right: str + detail: str + consequence: str = "" + resolution: str | None = None # set only by a human decision + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + +@dataclass +class Finding: + """A rule fired.""" + + rule: str + title: str + subject: str + detail: str + source: str + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + +@dataclass +class YearSummary: + policy_year: int + policy_no: str + claim_count: int + open_count: int + total_paid: float + total_reserve: float + total_incurred: float + open_incurred: float + earned_premium: float | None + source: str + superseded_by: str | None = None + + @property + def loss_ratio(self) -> float | None: + if not self.earned_premium: + return None + return round(self.total_incurred / self.earned_premium, 4) + + def to_dict(self) -> dict[str, Any]: + d = asdict(self) + d["loss_ratio"] = self.loss_ratio + return d + + +@dataclass +class Summary: + insured: str + years: list[YearSummary] = field(default_factory=list) + open_claims: list[Claim] = field(default_factory=list) + conflicts: list[Conflict] = field(default_factory=list) + findings: list[Finding] = field(default_factory=list) + documents_read: list[str] = field(default_factory=list) + documents_superseded: list[str] = field(default_factory=list) + unverified: list[str] = field(default_factory=list) + """Figures the app could not independently check. Named, never smoothed.""" + + @property + def total_incurred(self) -> float: + return round(sum(y.total_incurred for y in self.years), 2) + + @property + def total_claims(self) -> int: + return sum(y.claim_count for y in self.years) + + @property + def total_open(self) -> int: + return sum(y.open_count for y in self.years) + + def to_dict(self) -> dict[str, Any]: + return { + "insured": self.insured, + "totals": { + "claims": self.total_claims, + "open": self.total_open, + "incurred": self.total_incurred, + }, + "years": [y.to_dict() for y in self.years], + "open_claims": [c.to_dict() for c in self.open_claims], + "conflicts": [c.to_dict() for c in self.conflicts], + "findings": [f.to_dict() for f in self.findings], + "documents_read": self.documents_read, + "documents_superseded": self.documents_superseded, + "unverified": self.unverified, + } diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/parse.py b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/parse.py new file mode 100644 index 00000000..3aff4dac --- /dev/null +++ b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/parse.py @@ -0,0 +1,259 @@ +"""Read loss runs out of whatever format they arrive in. + +Carriers issue loss runs as CSV, as PDF, and occasionally as a Word table. All +three land here and produce the same rows. The parser also checks each document +against itself: a loss run states its own total and claim count, and those are +compared against the sum of the rows rather than trusted. Real loss runs +disagree with themselves more often than anyone likes. +""" + +from __future__ import annotations + +import csv +import os +import re +import zipfile + +from .model import Claim, LossRun, Source + +MONEY = re.compile(r"^\(?\$?\s*-?[\d,]*\.?\d+\)?$") + + +def money(text: str) -> float: + """Parse a figure the way an accountant wrote it. + + Handles $1,204,880.00 and (12,400.00) for negatives and an em dash for nil. + Returns 0.0 for a nil marker rather than raising, because a blank and a + dash mean the same thing on a loss run and neither is an error. + """ + if text is None: + return 0.0 + t = str(text).strip() + if t in ("", "-", "\u2013", "\u2014", "N/A", "n/a"): + return 0.0 + neg = t.startswith("(") and t.endswith(")") + t = t.strip("()").replace("$", "").replace(",", "").strip() + try: + v = float(t) + except ValueError as exc: + raise ValueError(f"not a figure: {text!r}") from exc + return -v if neg else v + + +def _meta(lines: list[str]) -> dict: + """Pull the preamble a loss run carries above its column headers.""" + out: dict = {} + joined = "\n".join(lines) + m = re.search(r"Policy Period:\s*(\d{4}-\d{2}-\d{2})\s*to\s*(\d{4}-\d{2}-\d{2})", joined) + if m: + out["period_start"], out["period_end"] = m.group(1), m.group(2) + m = re.search(r"Valuation Date:\s*(\d{4}-\d{2}-\d{2})", joined) + if m: + out["valuation_date"] = m.group(1) + m = re.search(r"Insured:\s*(.+)", joined) + if m: + out["insured"] = m.group(1).strip() + out["is_reissue"] = "REISSUE" in joined.upper() + return out + + +def parse_csv(path: str) -> LossRun: + rows = list(csv.reader(open(path, encoding="utf-8-sig"))) + preamble = ["".join(r) for r in rows[:10]] + meta = _meta(preamble) + + hdr_idx = next((i for i, r in enumerate(rows) if r and r[0] == "Claim Number"), None) + if hdr_idx is None: + raise ValueError(f"{os.path.basename(path)}: no claim header row found") + hdr = rows[hdr_idx] + + claims: list[Claim] = [] + stated_total = stated_count = premium = None + policy_year = policy_no = None + + for i, r in enumerate(rows[hdr_idx + 1:], start=hdr_idx + 2): + if not r or not any(r): + continue + rec = dict(zip(hdr, r)) + if rec.get("Claimant") == "TOTAL INCURRED" or (len(r) > 7 and r[7] == "TOTAL INCURRED"): + stated_total = money(r[10]) + continue + if len(r) > 7 and r[7] == "CLAIM COUNT": + stated_count = int(money(r[10])) + continue + if len(r) > 7 and r[7] == "EARNED PREMIUM": + premium = money(r[10]) + continue + if not rec.get("Claim Number"): + continue + + policy_year = int(rec["Policy Year"]) + policy_no = rec["Policy Number"] + src = Source(os.path.basename(path), meta.get("valuation_date"), i, + meta.get("is_reissue", False)) + paid, reserve = money(rec["Paid"]), money(rec["Reserve"]) + claims.append(Claim( + claim_no=rec["Claim Number"], policy_year=policy_year, + policy_no=policy_no, coverage=rec["Coverage"], + date_of_loss=rec["Date of Loss"], date_reported=rec["Date Reported"], + status=rec["Status"], claimant=rec["Claimant"], + paid=paid, reserve=reserve, incurred=money(rec["Incurred"]), + description=rec.get("Description", ""), source=src, + )) + + if policy_year is None: + raise ValueError(f"{os.path.basename(path)}: no claim rows") + + return LossRun( + path=path, policy_year=policy_year, policy_no=policy_no, + period_start=meta.get("period_start", ""), + period_end=meta.get("period_end", ""), + valuation_date=meta.get("valuation_date", ""), + is_reissue=meta.get("is_reissue", False), + claims=claims, stated_total=stated_total, stated_count=stated_count, + earned_premium=premium, + ) + + +def _pdf_text(path: str) -> str: + """Text of a PDF, laid out. + + pdfplumber is the dependency and is pure Python, so this works the same on + Windows, macOS and Linux. An earlier version shelled out to ``pdftotext``, + which is a Unix binary: the code ran fine on the machine it was written on + and died with a bare WinError on the machine it was used on. The fallback + below is kept for environments that have the binary and not the library, + but nothing depends on it. + """ + try: + import pdfplumber + except ImportError: + pass + else: + with pdfplumber.open(path) as pdf: + return "\n".join((page.extract_text() or "") for page in pdf.pages) + + from shutil import which + from subprocess import run as _run + if which("pdftotext"): + return _run(["pdftotext", "-layout", path, "-"], + capture_output=True, text=True).stdout + + raise RuntimeError( + f"cannot read {os.path.basename(path)}: no PDF reader available. " + f"Install the dependencies with `pip install -r requirements.txt` " + f"(pdfplumber), or put pdftotext on the PATH. CSV loss runs are " + f"unaffected and the rest of the pipeline still runs." + ) + + +def parse_pdf(path: str) -> LossRun: + """PDF loss runs are read from the laid-out text of a carrier report. + + Deliberately conservative: a row is accepted only when every field it needs + is present and every figure parses. Rows that look like claim lines but do + not fully parse are counted and reported as unreadable rather than guessed + at, because a half-read row is worse than a missing one on a document that + feeds a coverage decision. + """ + text = _pdf_text(path) + lines = [l.rstrip() for l in text.splitlines() if l.strip()] + meta = _meta(lines[:14]) + + row_re = re.compile( + r"^(?P[A-Z]{2}-\d{4}-\d{4})\s+" + r"(?P.+?)\s+" + r"(?P\d{4}-\d{2}-\d{2})\s+" + r"(?P\d{4}-\d{2}-\d{2})\s+" + r"(?POpen|Closed|Reopened)\s+" + r"(?P[\d,().$-]+)\s+" + r"(?P[\d,().$-]+)\s+" + r"(?P[\d,().$-]+)\s*$") + looks_like_claim = re.compile(r"^[A-Z]{2}-\d{4}-\d{4}\b") + + claims: list[Claim] = [] + unreadable: list[str] = [] + stated_total = stated_count = premium = None + + for i, line in enumerate(lines, start=1): + stripped = line.strip() + m = row_re.match(stripped) + if m: + try: + paid = money(m.group("paid")) + reserve = money(m.group("res")) + incurred = money(m.group("inc")) + except ValueError: + unreadable.append(m.group("claim")) + continue + claims.append(Claim( + claim_no=m.group("claim"), + policy_year=int(m.group("dol")[:4]) if m.group("dol") else 0, + policy_no="", coverage=m.group("cov").strip(), + date_of_loss=m.group("dol"), date_reported=m.group("drep"), + status=m.group("status"), claimant="", + paid=paid, reserve=reserve, incurred=incurred, description="", + source=Source(os.path.basename(path), meta.get("valuation_date"), + i, meta.get("is_reissue", False)), + )) + continue + if looks_like_claim.match(stripped): + unreadable.append(stripped.split()[0]) + continue + m2 = re.match(r"TOTAL INCURRED:\s*(.+)", stripped) + if m2: + stated_total = money(m2.group(1)) + m3 = re.match(r"CLAIM COUNT:\s*(\d+)", stripped) + if m3: + stated_count = int(m3.group(1)) + m4 = re.match(r"EARNED PREMIUM:\s*(.+)", stripped) + if m4: + premium = money(m4.group(1)) + + if not claims: + raise ValueError(f"{os.path.basename(path)}: no readable claim rows in PDF") + + # A PDF loss run states its policy period but not a policy number per row. + # Derive the policy year from the period rather than from a date of loss, + # which can legitimately fall outside it. + year = int(meta["period_start"][:4]) if meta.get("period_start") else claims[0].policy_year + for c in claims: + c.policy_year = year + + run = LossRun( + path=path, policy_year=year, policy_no="", + period_start=meta.get("period_start", ""), + period_end=meta.get("period_end", ""), + valuation_date=meta.get("valuation_date", ""), + is_reissue=meta.get("is_reissue", False), claims=claims, + stated_total=stated_total, stated_count=stated_count, + earned_premium=premium, + ) + run.unreadable_rows = unreadable + return run + + +def docx_text(path: str) -> str: + """Plain text of a .docx, paragraphs and table cells, in document order.""" + with zipfile.ZipFile(path) as z: + xml = z.read("word/document.xml").decode("utf-8") + xml = re.sub(r"", "\n", xml) + xml = re.sub(r"]*/>", "\t", xml) + text = re.sub(r"<[^>]+>", "", xml) + return re.sub(r"\n{3,}", "\n\n", text) + + +def load(path: str) -> LossRun: + ext = os.path.splitext(path)[1].lower() + if ext == ".csv": + return parse_csv(path) + if ext == ".pdf": + return parse_pdf(path) + raise ValueError(f"{path}: not a loss run format this build reads") + + +def load_all(paths: list[str]) -> list[LossRun]: + runs = [] + for p in sorted(paths): + runs.append(load(p)) + return runs diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/review.py b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/review.py new file mode 100644 index 00000000..b073bee6 --- /dev/null +++ b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/review.py @@ -0,0 +1,316 @@ +"""Build the review document, then put it through SuperDocs. + +Every figure in the output is placed here, by this file, from parsed rows. The +model is asked only to write the narrative around figures already fixed, and is +told so in the instruction. That is the difference between a summary an +underwriter can act on and one they have to re-check. +""" + +from __future__ import annotations + +import argparse +import datetime +import glob +import html +import json +import os +import sys + +from . import parse, summarize +from .model import Summary +from .superdocs import (ApiError, BudgetExceeded, FakeClient, Ledger, + SearchUnavailable, SuperDocsClient) + +PLACEHOLDER = "[[CORRESPONDENCE]]" + +# What the prose documents are asked. Arithmetic over the loss runs cannot find +# any of these: each lives in a sentence in a note, an application or an email. +PROSE_QUERIES = [ + "any claim reopened after being shown as closed, and the reserve " + "re-established for it", + "any reserve increased after a suit was filed, and the stated reserve " + "history", + "any statement that a loss run was reissued or superseded, and which " + "version should be relied on", +] + + +def money(v: float) -> str: + return f"{v:,.2f}" + + +def build_html(s: Summary, prose: list[dict], as_of: datetime.date) -> str: + p: list[str] = [] + e = html.escape + + p.append(f"

Loss run review — {e(s.insured)}

") + p.append(f"

Prepared {as_of.isoformat()}. Figures are read from the loss " + f"runs listed at the foot of this document and totalled " + f"arithmetically. No figure in this review is model-generated.

") + + p.append("

Summary by policy year

") + p.append("" + "" + "") + for y in s.years: + lr = f"{y.loss_ratio:.1%}" if y.loss_ratio is not None else "not available" + flag = " ***" if y.loss_ratio and y.loss_ratio > 0.60 else "" + p.append( + f"" + f"" + f"" + f"" + f"") + p.append(f"" + f"" + f"") + p.append("
Policy yearPolicyClaimsOpenPaidReserveIncurredLoss ratioSource
{y.policy_year}{e(y.policy_no)}{y.claim_count}{y.open_count}{money(y.total_paid)}{money(y.total_reserve)}{money(y.total_incurred)}{lr}{flag}{e(y.source)}
Total{s.total_claims}{s.total_open}{money(s.total_incurred)}
") + + p.append("

Open claims

") + if not s.open_claims: + p.append("

No claim is open across the policy years reviewed.

") + else: + p.append(f"

{len(s.open_claims)} claims remain open, carrying " + f"{money(sum(c.incurred for c in s.open_claims))} incurred. " + f"Listed separately from closed claims because reserves on " + f"open claims may still develop.

") + p.append("" + "" + "") + for c in s.open_claims: + p.append(f"" + f"" + f"" + f"" + f"") + p.append("
ClaimPYCoverageDate of lossPaidReserveIncurredMonths open
{e(c.claim_no)}{c.policy_year}{e(c.coverage)}{e(c.date_of_loss)}{money(c.paid)}{money(c.reserve)}{money(c.incurred)}{c.months_open(as_of)}
") + + p.append("

Conflicts between sources

") + if not s.conflicts: + p.append("

The sources agree. No conflict found.

") + else: + p.append("

Surfaced, not resolved. Each needs a decision before the " + "figures above are relied on.

") + for c in s.conflicts: + p.append(f"

{e(c.subject)} — {e(c.left)} versus " + f"{e(c.right)}. {e(c.detail)}" + + (f" {e(c.consequence)}" if c.consequence else "") + "

") + + p.append("

Checklist findings

") + if not s.findings: + p.append("

Every rule was applied and none fired. No findings.

") + else: + p.append("" + "") + for f in s.findings: + p.append(f"" + f"" + f"") + p.append("
RuleSubjectFindingSource
{e(f.rule)}{e(f.subject)}{e(f.title)}: {e(f.detail)}{e(f.source)}
") + + if prose is not None: + p.append("

From the correspondence and adjuster notes

") + p.append("

Read from the non-tabular documents attached to this " + "review. Quoted context only; no figure below is used in any " + "total above.

") + if prose: + for hit in prose[:6]: + snippet = " ".join(hit.get("text", "").split())[:320] + p.append(f"

{e(hit.get('document', ''))} — " + f"{e(snippet)}

") + else: + p.append(f"

{PLACEHOLDER}

") + + p.append("

What this review does not establish

") + items = list(s.unverified) + items.append("Figures are as stated by the carrier. This review checks each " + "loss run against itself and against the others; it cannot " + "confirm the carrier's own numbers.") + if s.documents_superseded: + items.append("Superseded documents were read and compared but are not " + "counted in any total: " + + ", ".join(s.documents_superseded) + ".") + p.append("
    " + "".join(f"
  • {e(i)}
  • " for i in items) + "
") + + p.append("

Sources

    " + + "".join(f"
  • {e(d)}
  • " for d in s.documents_read) + "
") + return "\n".join(p) + + +def build_instruction(queries: list[str]) -> str: + """One instruction, two edits, one operation. + + An earlier version asked for a JSON chat reply and looked for it in the + proposed-changes payload, which carries document edits and not chat text. + That was fighting the product: this is an editing agent, so the answer + belongs in the document. Asking it to fill a placeholder section is both + the supported path and half the cost. + """ + asks = "\n".join(f" - {q}" for q in queries) + return ( + f"Make exactly two edits to this document, grounded only in the files " + f"attached to this session.\n\n" + f"1. Replace the placeholder text {PLACEHOLDER} with a short list of " + f"what the attached adjuster notes, application and correspondence say " + f"about:\n{asks}\n" + f" Name the source document for each point and quote at most one " + f"sentence from it. Where the attached documents do not answer one of " + f"these, say so for that point rather than inferring an answer.\n\n" + f"2. Add an opening paragraph directly under the title, of at most four " + f"sentences, summarising this account for an underwriter who has not " + f"seen it.\n\n" + f"Every number, date, claim number and table cell in this document has " + f"been calculated from source records and is already correct: do not " + f"change, recalculate, round or restate any of them. If text you write " + f"would need a number, refer to the table instead of repeating it. Do " + f"not add a recommendation on whether to write the risk. Change nothing " + f"else." + ) + + +# Kept as a name for the tests that assert the figures are protected. +NARRATIVE_INSTRUCTION = build_instruction(PROSE_QUERIES) + + +def run(corpus_dir: str, out_dir: str, mode: str, max_ops: int, + as_of: datetime.date, skip_prose: bool = False) -> dict: + runs_paths = sorted(glob.glob(os.path.join(corpus_dir, "loss-runs", "*.csv"))) + runs_paths += sorted(glob.glob(os.path.join(corpus_dir, "loss-runs", "*.pdf"))) + if not runs_paths: + raise SystemExit(f"no loss runs found under {corpus_dir}/loss-runs/") + + runs, unreadable = [], [] + for path in runs_paths: + try: + runs.append(parse.load(path)) + except (ValueError, OSError) as exc: + unreadable.append(f"{os.path.basename(path)}: {exc}") + + # The same policy year may arrive as both CSV and PDF. Prefer the CSV, and + # use the PDF as a cross-check rather than a second opinion in the totals. + seen, deduped, crosschecks = {}, [], [] + for r in runs: + key = (r.policy_year, r.valuation_date, r.is_reissue) + if key in seen: + other = seen[key] + same = (len(r.claims) == len(other.claims) + and abs(r.summed_total - other.summed_total) < 0.01) + crosschecks.append( + f"{os.path.basename(r.path)} and {os.path.basename(other.path)} " + f"cover the same valuation and " + + ("agree." if same else + f"DISAGREE: {r.summed_total:,.2f} vs {other.summed_total:,.2f}.")) + continue + seen[key] = r + deduped.append(r) + + insured = "the insured" + app_path = glob.glob(os.path.join(corpus_dir, "application", "*.docx")) + app_text = parse.docx_text(app_path[0]) if app_path else "" + for r in deduped: + pass + + notes = {} + for p in glob.glob(os.path.join(corpus_dir, "adjuster-notes", "*.docx")): + base = os.path.basename(p) + notes[base.split("-adjuster")[0]] = parse.docx_text(p) + + s = summarize.summarise(deduped, insured) + s = summarize.examine(s, deduped, notes, app_text, as_of=as_of) + s.unverified.extend(crosschecks) + for u in unreadable: + s.unverified.append(f"Could not read {u}") + + ledger = Ledger(cap=max_ops) + corpus_text = dict(notes) + if app_text: + corpus_text["renewal application"] = app_text + for p in glob.glob(os.path.join(corpus_dir, "correspondence", "*.docx")): + corpus_text[os.path.basename(p)] = parse.docx_text(p) + + client = (SuperDocsClient(ledger) if mode == "live" + else FakeClient(ledger, corpus_text)) + + session = f"lossrun-{as_of.isoformat()}" + + # Uploads are free, and attaching the prose documents to the session is + # what makes cross-document retrieval possible at all. Do it before asking. + prose_docs = sorted( + p for p in glob.glob(os.path.join(corpus_dir, "**", "*.docx"), + recursive=True)) + for p in prose_docs: + try: + client.upload(p, session) + except (ApiError, OSError) as exc: + s.unverified.append(f"Could not attach {os.path.basename(p)}: {exc}") + + # None means the section is not wanted at all; [] means it is wanted and + # will be filled in the document by the edit below. + prose: list[dict] | None = None if skip_prose else [] + doc_html = build_html(s, prose, as_of) + instruction = (build_instruction(PROSE_QUERIES) if not skip_prose + else build_instruction([])) + os.makedirs(out_dir, exist_ok=True) + with open(os.path.join(out_dir, "review.html"), "w", encoding="utf-8") as fh: + fh.write(doc_html) + with open(os.path.join(out_dir, "summary.json"), "w", encoding="utf-8") as fh: + json.dump(s.to_dict(), fh, indent=2) + + exported = None + try: + job = client.edit(session, doc_html, instruction) + pending = job.get("pending") or [] + if pending: + client.approve(session, job["job_id"], pending) + else: + s.unverified.append( + "The narrative edit proposed no changes, so the correspondence " + "section still holds its placeholder and no opening paragraph " + "was added. Every figure above is unaffected.") + exported = client.export( + session, os.path.join(out_dir, "loss-run-review.docx")) + except BudgetExceeded as exc: + s.unverified.append(f"Stopped at the operation cap: {exc}") + except (ApiError, OSError) as exc: + s.unverified.append(f"Document assembly did not complete: {exc}") + + return {"summary": s, "ledger": ledger, "html": doc_html, + "exported": exported, "out_dir": out_dir} + + +def main(argv=None) -> int: + ap = argparse.ArgumentParser(prog="lossrun") + ap.add_argument("--corpus", default="corpus/out") + ap.add_argument("--out", default="out") + ap.add_argument("--mode", choices=("dry", "live"), default="dry") + ap.add_argument("--max-ops", type=int, default=6, + help="hard cap; the run stops before exceeding it") + ap.add_argument("--as-of", default=None, + help="valuation date for ageing rules (YYYY-MM-DD)") + ap.add_argument("--skip-prose", action="store_true", + help="skip the search stage and spend nothing on it") + a = ap.parse_args(argv) + + as_of = (datetime.date.fromisoformat(a.as_of) if a.as_of + else datetime.date.today()) + r = run(a.corpus, a.out, a.mode, a.max_ops, as_of, a.skip_prose) + s, led = r["summary"], r["ledger"] + + print(f"mode={a.mode} documents read={len(s.documents_read)} " + f"superseded={len(s.documents_superseded)}") + for y in s.years: + lr = f"{y.loss_ratio:.1%}" if y.loss_ratio is not None else "n/a" + print(f" PY{y.policy_year} claims={y.claim_count:3d} " + f"open={y.open_count:2d} incurred={y.total_incurred:>13,.2f} " + f"LR={lr:>7}") + print(f" TOTAL claims={s.total_claims} open={s.total_open} " + f"incurred={s.total_incurred:,.2f}") + print(f" conflicts={len(s.conflicts)} findings={len(s.findings)} " + f"unverified={len(s.unverified)}") + print(f" operations spent={led.spent}/{led.cap} {led.by_stage}") + print(f" written to {r['out_dir']}/") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/summarize.py b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/summarize.py new file mode 100644 index 00000000..d800ef5d --- /dev/null +++ b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/summarize.py @@ -0,0 +1,305 @@ +"""Group, total, and check. + +Three jobs, in order. + +**Supersede.** When two loss runs cover the same policy year, the later +valuation wins — but the difference is recorded as a conflict rather than +quietly overwritten. On a real account this is the whole game: reserves develop, +runs get reissued, and the same claim carries two very different numbers. Both +are correct as of their valuation date. Picking one silently is making an +underwriting decision without telling anyone. + +**Aggregate.** Claim counts and incurred totals by policy year, open claims +separated out. Pure arithmetic over parsed rows. + +**Examine.** The eight checklist rules. Each produces a finding or an explicit +pass, and a clean corpus is allowed to produce nothing at all. +""" + +from __future__ import annotations + +import os +import re +from datetime import date + +from .model import Claim, Conflict, Finding, LossRun, Summary, YearSummary + +SEVERITY_THRESHOLD = 100_000.00 +STALE_MONTHS = 36 +DORMANT_MONTHS = 24 +LOSS_RATIO_REFERRAL = 0.60 + + +# --------------------------------------------------------------------------- +# supersession +# --------------------------------------------------------------------------- + +def choose_authoritative(runs: list[LossRun]) -> tuple[dict[int, LossRun], list[LossRun], list[Conflict]]: + by_year: dict[int, list[LossRun]] = {} + for r in runs: + by_year.setdefault(r.policy_year, []).append(r) + + authoritative: dict[int, LossRun] = {} + superseded: list[LossRun] = [] + conflicts: list[Conflict] = [] + + for year, group in sorted(by_year.items()): + group.sort(key=lambda r: (r.valuation_date or "", r.is_reissue)) + winner = group[-1] + authoritative[year] = winner + for loser in group[:-1]: + superseded.append(loser) + conflicts.extend(_diff_runs(loser, winner)) + + return authoritative, superseded, conflicts + + +def _diff_runs(old: LossRun, new: LossRun) -> list[Conflict]: + out: list[Conflict] = [] + old_by = {c.claim_no: c for c in old.claims} + new_by = {c.claim_no: c for c in new.claims} + + for claim_no in sorted(set(old_by) | set(new_by)): + a, b = old_by.get(claim_no), new_by.get(claim_no) + if a is None: + out.append(Conflict( + kind="claim_added", + subject=claim_no, + left=f"absent from {old.label}", + right=f"{b.incurred:,.2f} incurred in {new.label}", + detail=(f"Reported {b.date_reported}, after the earlier run was " + f"valued {old.valuation_date}."), + consequence="Earlier totals for this policy year understate it.", + )) + continue + if b is None: + out.append(Conflict( + kind="claim_removed", subject=claim_no, + left=f"present in {old.label}", right=f"absent from {new.label}", + detail="Present on the superseded run and not on the later one.", + consequence="Query with the carrier before relying on either total.", + )) + continue + if abs(a.incurred - b.incurred) >= 0.01: + delta = b.incurred - a.incurred + out.append(Conflict( + kind="incurred_changed", subject=claim_no, + left=f"{a.incurred:,.2f} ({old.label})", + right=f"{b.incurred:,.2f} ({new.label})", + detail=(f"Moved {delta:+,.2f}. Paid {a.paid:,.2f} to " + f"{b.paid:,.2f}; reserve {a.reserve:,.2f} to " + f"{b.reserve:,.2f}."), + consequence="Both figures are correct at their valuation date.", + )) + if a.status != b.status: + out.append(Conflict( + kind="status_changed", subject=claim_no, + left=f"{a.status} ({old.label})", right=f"{b.status} ({new.label})", + detail="Claim status differs between valuations.", + )) + return out + + +# --------------------------------------------------------------------------- +# aggregation +# --------------------------------------------------------------------------- + +def summarise(runs: list[LossRun], insured: str) -> Summary: + authoritative, superseded, conflicts = choose_authoritative(runs) + s = Summary(insured=insured, conflicts=conflicts) + s.documents_read = [os.path.basename(r.path) for r in runs] + s.documents_superseded = [os.path.basename(r.path) for r in superseded] + + for year, run in sorted(authoritative.items()): + claims = run.claims + open_claims = [c for c in claims if c.is_open] + s.open_claims.extend(open_claims) + s.years.append(YearSummary( + policy_year=year, + policy_no=run.policy_no, + claim_count=len(claims), + open_count=len(open_claims), + total_paid=round(sum(c.paid for c in claims), 2), + total_reserve=round(sum(c.reserve for c in claims), 2), + total_incurred=round(sum(c.incurred for c in claims), 2), + open_incurred=round(sum(c.incurred for c in open_claims), 2), + earned_premium=run.earned_premium, + source=run.label, + )) + if run.earned_premium is None: + s.unverified.append( + f"PY{year}: no earned premium on the loss run, so no loss ratio " + f"could be calculated.") + + s.open_claims.sort(key=lambda c: -c.incurred) + return s + + +# --------------------------------------------------------------------------- +# the rules +# --------------------------------------------------------------------------- + +def examine(summary: Summary, runs: list[LossRun], notes: dict[str, str], + application_text: str = "", as_of: date | None = None) -> Summary: + as_of = as_of or date.today() + authoritative, superseded, _ = choose_authoritative(runs) + live = [c for r in authoritative.values() for c in r.claims] + f = summary.findings + + # R1 severity claims need an adjuster note + noted = set(notes) + for c in sorted(live, key=lambda c: -c.incurred): + if c.incurred >= SEVERITY_THRESHOLD and c.claim_no not in noted: + f.append(Finding("R1", "Severity claim without an adjuster note", + c.claim_no, + f"{c.incurred:,.2f} incurred, status {c.status}. " + f"No note in the file, so severity cannot be " + f"evaluated.", c.source.cite())) + + # R2 stale open claims / R6 dormant reserves + for c in live: + if not c.is_open: + continue + months = c.months_open(as_of) + if months > STALE_MONTHS: + f.append(Finding("R2", "Open more than 36 months", c.claim_no, + f"Open {months} months from date of loss " + f"{c.date_of_loss}. Incurred {c.incurred:,.2f}.", + c.source.cite())) + if c.paid == 0 and c.reserve > 0 and months > DORMANT_MONTHS: + f.append(Finding("R6", "Dormant reserve", c.claim_no, + f"Nothing paid against a reserve of " + f"{c.reserve:,.2f} after {months} months.", + c.source.cite())) + + # R3 loss ratio referral + for y in summary.years: + lr = y.loss_ratio + if lr is not None and lr > LOSS_RATIO_REFERRAL: + f.append(Finding("R3", "Loss ratio above referral threshold", + f"PY{y.policy_year}", + f"{lr:.1%} ({y.total_incurred:,.2f} incurred " + f"against {y.earned_premium:,.2f} earned).", + y.source)) + + # R4 date of loss outside the policy period + for run in authoritative.values(): + if not (run.period_start and run.period_end): + continue + for c in run.claims: + if not c.date_of_loss: + continue + if not (run.period_start <= c.date_of_loss < run.period_end): + f.append(Finding("R4", "Date of loss outside the policy period", + c.claim_no, + f"Loss dated {c.date_of_loss} filed under a " + f"policy running {run.period_start} to " + f"{run.period_end}.", c.source.cite())) + + # R5 application consistency + f.extend(_check_application(application_text, live)) + + # R7 duplicate claims + # Date and amount alone are not evidence: a fleet can genuinely take four + # identical windscreen losses on one day. A duplicate is the same event + # described twice, so the descriptions have to agree as well. + seen: dict[tuple, list[Claim]] = {} + for c in live: + seen.setdefault((c.date_of_loss, round(c.incurred, 2)), []).append(c) + for key, group in seen.items(): + if len(group) < 2 or not key[0]: + continue + for i in range(len(group)): + for j in range(i + 1, len(group)): + a, b = group[i], group[j] + shared = _shared_terms(a.description, b.description) + if len(shared) < 3: + continue + f.append(Finding("R7", "Possible duplicate claim", + f"{a.claim_no}, {b.claim_no}", + f"Same date of loss {key[0]}, identical " + f"incurred {key[1]:,.2f}, and overlapping " + f"descriptions ({', '.join(sorted(shared)[:5])}). " + f"Totals for this policy year may " + f"double-count {key[1]:,.2f}.", + a.source.cite())) + + # R8 loss run internal consistency (checked on every run, superseded too) + for run in runs: + if run.stated_total is None: + continue + diff = round(run.stated_total - run.summed_total, 2) + if abs(diff) >= 0.01: + f.append(Finding("R8", "Loss run does not add up", run.label, + f"States {run.stated_total:,.2f} total incurred; " + f"rows sum to {run.summed_total:,.2f}, a " + f"difference of {diff:,.2f}.", + os.path.basename(run.path))) + if run.stated_count is not None and run.stated_count != len(run.claims): + f.append(Finding("R8", "Loss run claim count disagrees", run.label, + f"States {run.stated_count} claims; " + f"{len(run.claims)} rows present.", + os.path.basename(run.path))) + + return summary + + +def _check_application(text: str, live: list[Claim]) -> list[Finding]: + """Compare what the applicant declared against what the runs show. + + Reads the specific declarations this checklist cares about. Where a + declaration is not found, nothing is asserted about it -- an absent + statement is not a false one. + """ + out: list[Finding] = [] + if not text: + return out + src = "renewal application" + + m = re.search(r"(\d+)\s+claims?\s+exceeding\s+\$?([\d,]+)", text, re.I) + if m: + declared, threshold = int(m.group(1)), float(m.group(2).replace(",", "")) + actual = [c for c in live if c.incurred > threshold] + if len(actual) != declared: + out.append(Finding( + "R5", "Application understates claim count", "declaration", + f"Application states {declared} claims exceeding " + f"{threshold:,.0f}; the loss runs show {len(actual)}: " + f"{', '.join(sorted(c.claim_no for c in actual))}.", src)) + + m2 = re.search(r"no\s+(?:open\s+)?claim[^.]{0,40}?exceeds?\s+\$?([\d,]+)", + text, re.I) + if m2: + threshold = float(m2.group(1).replace(",", "")) + actual = [c for c in live if c.is_open and c.incurred > threshold] + if actual: + out.append(Finding( + "R5", "Application contradicted on open severity", "declaration", + f"Application states no open claim exceeds {threshold:,.0f}; " + f"{len(actual)} do: " + f"{', '.join(sorted(c.claim_no for c in actual))}.", src)) + + m3 = re.search(r"all claims from policy years?\s+(\d{4})\s+and\s+(\d{4})\s+are closed", + text, re.I) + if m3: + years = {int(m3.group(1)), int(m3.group(2))} + still_open = [c for c in live if c.policy_year in years and c.is_open] + if still_open: + out.append(Finding( + "R5", "Application contradicted on closed years", "declaration", + f"Application states all {m3.group(1)}/{m3.group(2)} claims are " + f"closed; {len(still_open)} remain open: " + f"{', '.join(sorted(c.claim_no for c in still_open))}.", src)) + return out + + +STOPWORDS = {"from", "with", "that", "this", "were", "have", "been", "damage", + "claim", "loss", "the", "and", "for"} + + +def _shared_terms(a: str, b: str) -> set[str]: + """Meaningful words two descriptions have in common.""" + def toks(s: str) -> set[str]: + return {w for w in re.findall(r"[a-z0-9]+", (s or "").lower()) + if len(w) > 2 and w not in STOPWORDS} + return toks(a) & toks(b) diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/superdocs.py b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/superdocs.py new file mode 100644 index 00000000..5e6589eb --- /dev/null +++ b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/lossrun/superdocs.py @@ -0,0 +1,367 @@ +"""The SuperDocs surface: upload, search, chat, approve, export. + +Where the operations go, and why. + +Searches cost operations and chat turns cost operations; uploads and exports do +not. A naive design that searched once per claim would spend 41 operations on +one account and still produce figures nobody could audit. So this build divides +the work by what each side is actually good at: + +* **Figures are parsed, never searched.** Claim counts, incurred totals and loss + ratios come from arithmetic over rows read out of the loss runs. That costs + nothing and is reproducible. +* **Search is spent on the prose** — adjuster notes, the application, broker + correspondence — where the contradictions live in sentences rather than + columns, and where a keyword scan genuinely would miss them. +* **Chat writes the narrative around figures the app has already fixed**, and is + told so explicitly, so the model is never the source of a number. + +The API key is read from SUPERDOCS_API_KEY and is never an argument, never +logged, and never written to an output file. +""" + +from __future__ import annotations + +import json +import os +import re +import time +from dataclasses import dataclass, field + +BASE = os.environ.get("SUPERDOCS_BASE", "https://api.superdocs.app") + +# Published pricing: exports and downloads are free, searches and chat are not. +# One place to change it if that changes. +OP_COST = {"upload": 0, "search": 1, "chat": 1, "approve": 0, "export": 0} + + +class ApiError(RuntimeError): + pass + + +class SearchUnavailable(ApiError): + """The dedicated search endpoint is not present on this deployment.""" + + +class BudgetExceeded(RuntimeError): + pass + + +@dataclass +class Ledger: + cap: int + spent: int = 0 + by_stage: dict[str, int] = field(default_factory=dict) + timings: dict[str, float] = field(default_factory=dict) + + def charge(self, stage: str) -> None: + cost = OP_COST.get(stage, 0) + if cost and self.spent + cost > self.cap: + raise BudgetExceeded( + f"{stage!r} would take the run to {self.spent + cost} against a " + f"cap of {self.cap}. Stopping before the spend, not after.") + self.spent += cost + self.by_stage[stage] = self.by_stage.get(stage, 0) + cost + + def record(self, stage: str, seconds: float) -> None: + self.timings[stage] = round(self.timings.get(stage, 0.0) + seconds, 2) + + +class SuperDocsClient: + def __init__(self, ledger: Ledger, timeout_s: float = 600.0, + poll_interval: float = 3.0): + key = os.environ.get("SUPERDOCS_API_KEY") + if not key: + raise ApiError( + "SUPERDOCS_API_KEY is not set. Export it in your shell; this " + "build never takes a key as an argument.") + self._key = key + self.ledger = ledger + self.timeout_s = timeout_s + self.poll_interval = poll_interval + + @property + def _json(self) -> dict: + return {"Authorization": f"Bearer {self._key}", + "Content-Type": "application/json"} + + @property + def _auth(self) -> dict: + return {"Authorization": f"Bearer {self._key}"} + + @staticmethod + def _redact(text: str) -> str: + return re.sub(r"sk_[A-Za-z0-9_\-]{4,}", "sk_[REDACTED]", text or "") + + def _check(self, resp, what: str): + if resp.status_code >= 400: + raise ApiError(f"{what}: HTTP {resp.status_code} " + f"{self._redact(resp.text)[:400]}") + return resp + + # -- the calls --------------------------------------------------------- + def upload(self, path: str, session_id: str) -> str: + import requests + t0 = time.monotonic() + self.ledger.charge("upload") + with open(path, "rb") as fh: + r = requests.post( + f"{BASE}/v1/documents/upload", headers=self._auth, + files={"file": (os.path.basename(path), fh)}, + data={"session_id": session_id}, timeout=self.timeout_s) + self._check(r, "upload") + self.ledger.record("upload", time.monotonic() - t0) + html = r.json().get("html") + if not html: + raise ApiError("upload returned no html") + return html + + def search(self, session_id: str, query: str, limit: int = 8) -> list[dict]: + """Semantic search across the documents uploaded to this session. + + A dedicated search endpoint returned 404 on the live API when this was + written, so `retrieve` below is the path actually used. This method is + kept because the capability is real and may be exposed directly later; + it raises SearchUnavailable on 404 rather than pretending it worked. + """ + import requests + t0 = time.monotonic() + self.ledger.charge("search") + r = requests.post( + f"{BASE}/v1/search", headers=self._json, + json={"session_id": session_id, "query": query, "limit": limit}, + timeout=self.timeout_s) + if r.status_code == 404: + raise SearchUnavailable( + "no dedicated search endpoint on this deployment; " + "cross-document retrieval runs through the chat surface") + self._check(r, "search") + self.ledger.record("search", time.monotonic() - t0) + body = r.json() + hits = body.get("results") or body.get("matches") or [] + out = [] + for h in hits: + out.append({ + "document": h.get("document") or h.get("filename") or "", + "text": h.get("text") or h.get("content") or "", + "score": h.get("score"), + }) + return out + + def retrieve(self, session_id: str, questions: list[str]) -> list[dict]: + """Ask the attached documents and parse a JSON reply. + + NOT used by the review pipeline. It asked for chat text and looked for + it in the proposed-changes payload, which carries document edits; the + reply was never there to find. The pipeline now has the agent write its + answer into the document instead, which is the supported path and costs + one operation rather than two. Kept, unused, because the JSON-reply + shape is worth having if a chat-reply field is exposed later. + """ + prompt = ( + "Search the documents attached to this session and answer each " + "question below from them. Reply with JSON only, no prose and no " + "code fences: a list of objects with keys \"question\", " + "\"document\", \"quote\". Quote at most two sentences verbatim " + "from the source. If nothing in the attached documents answers a " + "question, return an object for it with \"document\": null. Do " + "not infer, summarise across documents, or state anything the " + "attached text does not say.\n\n" + + "\n".join(f"{i + 1}. {q}" for i, q in enumerate(questions)) + ) + job = self.edit(session_id, "

", prompt) + raw = "" + for c in job.get("pending") or []: + content = c.get("content") + if isinstance(content, dict): + raw = content.get("text") or content.get("message") or "" + elif isinstance(content, str): + raw = content + if raw: + break + return _parse_retrieval(raw) + + def edit(self, session_id: str, document_html: str, instruction: str) -> dict: + import requests + t0 = time.monotonic() + self.ledger.charge("chat") + r = requests.post( + f"{BASE}/v1/chat/async", headers=self._json, + json={"message": instruction, "session_id": session_id, + "document_html": document_html, + "approval_mode": "ask_every_time"}, + timeout=self.timeout_s) + self._check(r, "chat/async") + job_id = r.json()["job_id"] + + deadline = time.monotonic() + self.timeout_s + while time.monotonic() < deadline: + j = self._check(requests.get(f"{BASE}/v1/jobs/{job_id}", + headers=self._json, + timeout=self.timeout_s), "jobs/get") + job = j.json() + status = job.get("status") + if status in ("awaiting_approval", "completed"): + self.ledger.record("chat", time.monotonic() - t0) + return {"job_id": job_id, "status": status, + "pending": self._pending(job)} + if status in ("failed", "cancelled"): + raise ApiError(f"job {status}: " + f"{self._redact(str(job.get('error')))[:300]}") + time.sleep(self.poll_interval) + raise ApiError(f"job {job_id} did not finish within {self.timeout_s:.0f}s. " + f"A long silence is normal on this API, so this is a " + f"timeout, not a proven failure.") + + @staticmethod + def _pending(job: dict) -> list[dict]: + """Proposed-change content arrives JSON-encoded and needs a second + parse; the final result is already an object. Missing this is the most + common reason integrators see empty diffs.""" + out = [] + for c in (job.get("metadata") or {}).get("pending_changes") or []: + c = dict(c) + if isinstance(c.get("content"), str): + try: + c["content"] = json.loads(c["content"]) + except (ValueError, TypeError): + c["content_parse_failed"] = True + out.append(c) + return out + + def approve(self, session_id: str, job_id: str, changes: list[dict]) -> dict: + import requests + t0 = time.monotonic() + self.ledger.charge("approve") + r = requests.post( + f"{BASE}/v1/chat/{session_id}/approve", headers=self._json, + json={"job_id": job_id, "approved": True, + "changes": [{"change_id": c.get("change_id"), "approved": True} + for c in changes]}, + timeout=self.timeout_s) + self._check(r, "approve") + self.ledger.record("approve", time.monotonic() - t0) + return r.json() + + def export(self, session_id: str, out_path: str, fmt: str = "docx") -> str: + import requests + t0 = time.monotonic() + self.ledger.charge("export") + r = requests.post(f"{BASE}/v1/documents/export", headers=self._json, + json={"session_id": session_id, "format": fmt}, + timeout=self.timeout_s) + self._check(r, "export") + os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True) + with open(out_path, "wb") as fh: + fh.write(r.content) + self.ledger.record("export", time.monotonic() - t0) + return out_path + + +class FakeClient: + """Offline stand-in. Returns nothing it was not given, so a test that + passes against it is testing this build's own logic and never the API's.""" + + def __init__(self, ledger: Ledger, corpus_text: dict[str, str] | None = None): + self.ledger = ledger + self.corpus_text = corpus_text or {} + self._sessions: dict[str, str] = {} + + def upload(self, path: str, session_id: str) -> str: + self.ledger.charge("upload") + html = f"

{os.path.basename(path)}

" + self._sessions[session_id] = html + return html + + def search(self, session_id: str, query: str, limit: int = 8) -> list[dict]: + self.ledger.charge("search") + return self._keyword(query, limit) + + def _keyword(self, query: str, limit: int = 8) -> list[dict]: + """Keyword matching over the local text. Weaker than the real thing on + purpose: if a result only appears live, the difference is visible + rather than hidden.""" + terms = [t for t in re.findall(r"\w+", query.lower()) if len(t) > 3] + hits = [] + for name, text in self.corpus_text.items(): + low = text.lower() + score = sum(low.count(t) for t in terms) + if score: + idx = min((low.find(t) for t in terms if t in low), default=0) + hits.append({"document": name, "score": score, + "text": text[max(0, idx - 200): idx + 400]}) + hits.sort(key=lambda h: -h["score"]) + return hits[:limit] + + def retrieve(self, session_id: str, questions: list[str]) -> list[dict]: + self.ledger.charge("chat") + out = [] + for q in questions: + hits = self._keyword(q, limit=1) + out.append({"question": q, + "document": hits[0]["document"] if hits else None, + "quote": hits[0]["text"][:300] if hits else ""}) + return out + + def edit(self, session_id: str, document_html: str, instruction: str) -> dict: + """Fills the placeholder by keyword, so a dry run shows the real shape + of the output. Deliberately weaker than the live path: if a point only + surfaces against the real API, that difference stays visible.""" + self.ledger.charge("chat") + if "[[CORRESPONDENCE]]" in document_html: + lines = [] + for q in re.findall(r"^ - (.+)$", instruction, re.M): + hits = self._keyword(q, limit=1) + if hits: + quote = " ".join(hits[0]["text"].split())[:200] + lines.append(f"

{hits[0]['document']} \u2014 {quote}

") + else: + lines.append(f"

Nothing in the attached documents " + f"addresses: {q}

") + document_html = document_html.replace( + "

[[CORRESPONDENCE]]

", "".join(lines) or + "

Nothing in the attached documents addresses these " + "points.

") + self._sessions[session_id] = document_html + return {"job_id": f"fake-{session_id}", "status": "awaiting_approval", + "pending": [{"change_id": "c1", + "content": json.dumps({"instruction": instruction})}]} + + def approve(self, session_id: str, job_id: str, changes: list[dict]) -> dict: + self.ledger.charge("approve") + return {"status": "applied", "applied": len(changes)} + + def export(self, session_id: str, out_path: str, fmt: str = "docx") -> str: + self.ledger.charge("export") + os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True) + with open(out_path, "w", encoding="utf-8") as fh: + fh.write(self._sessions.get(session_id, "")) + return out_path + + +def _parse_retrieval(raw: str) -> list[dict]: + """Parse the retrieval reply, tolerating fences and surrounding prose. + + Returns [] rather than raising when the reply is not usable: a retrieval + that could not be read is an absent section, not a corrupt document. + """ + if not raw: + return [] + text = raw.strip() + text = re.sub(r"^```(?:json)?|```$", "", text, flags=re.M).strip() + start, end = text.find("["), text.rfind("]") + if start == -1 or end <= start: + return [] + try: + data = json.loads(text[start:end + 1]) + except (ValueError, TypeError): + return [] + if not isinstance(data, list): + return [] + out = [] + for item in data: + if isinstance(item, dict) and item.get("document"): + out.append({"document": str(item.get("document")), + "text": str(item.get("quote") or ""), + "question": str(item.get("question") or "")}) + return out diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/requirements.txt b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/requirements.txt new file mode 100644 index 00000000..82b5bff6 --- /dev/null +++ b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/requirements.txt @@ -0,0 +1,4 @@ +pdfplumber>=0.11 +python-docx>=1.1 +pytest>=8.0 +requests>=2.31 diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/tests/conftest.py b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/tests/conftest.py new file mode 100644 index 00000000..4b1c5dd7 --- /dev/null +++ b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/tests/conftest.py @@ -0,0 +1,3 @@ +import os, sys +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +os.environ.setdefault("LOSSRUN_CORPUS", "corpus/out") diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/tests/test_lossrun.py b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/tests/test_lossrun.py new file mode 100644 index 00000000..717ab615 --- /dev/null +++ b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/tests/test_lossrun.py @@ -0,0 +1,333 @@ +"""Tests. No API key, no network, no operations. + +The manifest that ships with the corpus is the answer key. These tests assert +the app reproduces it exactly — both halves: every planted conflict and finding +is found, and nothing outside the answer key is reported. A summariser that +flags everything would pass the first half and fail the second. +""" + +from __future__ import annotations + +import datetime +import glob +import json +import os +from collections import Counter + +import pytest + +from lossrun import parse, summarize +from lossrun.model import Claim, LossRun, Source +from lossrun.review import build_html, run +from lossrun.superdocs import BudgetExceeded, FakeClient, Ledger + +CORPUS = os.environ.get("LOSSRUN_CORPUS", "corpus/out") +AS_OF = datetime.date(2026, 1, 9) + +# From MANIFEST.md, which is generated alongside the corpus. +EXPECTED_FINDINGS = {"R1": 1, "R2": 4, "R3": 1, "R4": 1, + "R5": 3, "R6": 1, "R7": 1, "R8": 1} +EXPECTED_YEARS = { + 2021: {"claims": 12, "incurred": 237_280.35, "ratio": 0.576}, + 2022: {"claims": 12, "incurred": 184_694.38, "ratio": 0.412}, + 2023: {"claims": 10, "incurred": 647_992.44, "ratio": 1.293}, + 2024: {"claims": 7, "incurred": 188_831.55, "ratio": 0.333}, +} + + +@pytest.fixture(scope="module") +def analysed(): + runs = [parse.load(p) for p in sorted(glob.glob(f"{CORPUS}/loss-runs/*.csv"))] + notes = {os.path.basename(p).split("-adjuster")[0]: parse.docx_text(p) + for p in glob.glob(f"{CORPUS}/adjuster-notes/*.docx")} + app = parse.docx_text(glob.glob(f"{CORPUS}/application/*.docx")[0]) + s = summarize.summarise(runs, "Meridian Cold Chain Logistics, Inc.") + s = summarize.examine(s, runs, notes, app, as_of=AS_OF) + return s, runs + + +# -------------------------------------------------------------------------- +# the card's own bar: groups and totals by policy year, open claims separated +# -------------------------------------------------------------------------- + +def test_groups_and_totals_by_policy_year(analysed): + s, _ = analysed + assert len(s.years) == 4 + for y in s.years: + want = EXPECTED_YEARS[y.policy_year] + assert y.claim_count == want["claims"], f"PY{y.policy_year} claim count" + assert y.total_incurred == pytest.approx(want["incurred"], abs=0.01) + assert y.loss_ratio == pytest.approx(want["ratio"], abs=0.001) + + +def test_totals_are_the_sum_of_the_years(analysed): + s, _ = analysed + assert s.total_incurred == pytest.approx( + sum(y.total_incurred for y in s.years), abs=0.01) + assert s.total_claims == sum(y.claim_count for y in s.years) == 41 + + +def test_open_claims_are_flagged_separately(analysed): + s, _ = analysed + assert s.total_open == 15 + assert len(s.open_claims) == 15 + assert all(c.is_open for c in s.open_claims) + # and separating them is not the same as dropping them + assert all(any(c.claim_no == o.claim_no for o in s.open_claims) + for c in s.open_claims) + for y in s.years: + assert y.open_count <= y.claim_count + + +def test_paid_plus_reserve_equals_incurred_everywhere(analysed): + s, runs = analysed + for r in runs: + for c in r.claims: + assert c.paid + c.reserve == pytest.approx(c.incurred, abs=0.01), \ + f"{c.claim_no} does not add up" + + +# -------------------------------------------------------------------------- +# supersession — the part that decides the answer +# -------------------------------------------------------------------------- + +def test_the_later_valuation_supersedes_and_the_difference_is_surfaced(analysed): + s, _ = analysed + assert len(s.documents_superseded) == 1 + assert "ORIGINAL" in s.documents_superseded[0] + + kinds = {c.subject: c.kind for c in s.conflicts} + assert kinds["KM-2023-0417"] == "incurred_changed" + assert kinds["KM-2023-0588"] == "claim_added" + + dev = next(c for c in s.conflicts if c.subject == "KM-2023-0417") + assert "45,000.00" in dev.left and "182,400.00" in dev.right + assert dev.resolution is None, "a conflict must not arrive pre-resolved" + + +def test_supersession_changes_the_underwriting_outcome(analysed): + """The reason this matters: the same account either clears the 60% + referral threshold or does not, depending on which run is trusted.""" + _, runs = analysed + original = next(r for r in runs if r.policy_year == 2023 and not r.is_reissue) + reissue = next(r for r in runs if r.policy_year == 2023 and r.is_reissue) + premium = 501_200.00 + assert original.summed_total / premium < 0.60 + assert reissue.summed_total / premium > 0.60 + + +def test_superseded_figures_are_excluded_from_totals(analysed): + s, _ = analysed + py23 = next(y for y in s.years if y.policy_year == 2023) + assert py23.total_incurred == pytest.approx(647_992.44, abs=0.01) + assert py23.total_incurred != pytest.approx(286_092.44, abs=0.01) + + +# -------------------------------------------------------------------------- +# the rules — found, and nothing else found +# -------------------------------------------------------------------------- + +def test_every_expected_finding_is_produced(analysed): + s, _ = analysed + got = Counter(f.rule for f in s.findings) + assert dict(got) == EXPECTED_FINDINGS + + +def test_no_finding_outside_the_answer_key(analysed): + """The half that a flag-everything summariser would fail.""" + s, _ = analysed + known = { + "R1": {"KM-2024-0106"}, + "R2": {"KM-2021-0232", "KM-2021-0252", "KM-2021-0333", "KM-2022-0237"}, + "R3": {"PY2023"}, + "R4": {"KM-2022-0402"}, + "R6": {"KM-2021-0333"}, + } + for rule, subjects in known.items(): + got = {f.subject for f in s.findings if f.rule == rule} + assert got == subjects, f"{rule}: unexpected subjects {got ^ subjects}" + + +def test_a_clean_corpus_produces_no_findings(): + """An honest report of nothing is a valid output and must be reachable.""" + src = Source("clean.csv", "2026-01-01") + claims = [Claim(f"KM-2024-{i:04d}", 2024, "P-1", "Auto Liability", + "2024-06-01", "2024-06-02", "Closed", "Third party", + 1000.0, 0.0, 1000.0, "", src) for i in range(4)] + run_ = LossRun("clean.csv", 2024, "P-1", "2024-04-01", "2025-04-01", + "2026-01-01", False, claims, stated_total=4000.0, + stated_count=4, earned_premium=500_000.0) + s = summarize.summarise([run_], "Clean Co") + s = summarize.examine(s, [run_], {}, "", as_of=AS_OF) + assert s.findings == [] + assert s.conflicts == [] + + +def test_loss_run_that_does_not_add_up_is_caught(analysed): + s, _ = analysed + r8 = [f for f in s.findings if f.rule == "R8"] + assert len(r8) == 1 + assert "1,500.00" in r8[0].detail + + +# -------------------------------------------------------------------------- +# parsing +# -------------------------------------------------------------------------- + +@pytest.mark.parametrize("text,want", [ + ("$1,204,880.00", 1_204_880.00), + ("(12,400.00)", -12_400.00), + ("\u2014", 0.0), + ("0.00", 0.0), + ("", 0.0), + ("94,750.00", 94_750.00), +]) +def test_accountant_notation_parses(text, want): + assert parse.money(text) == pytest.approx(want, abs=0.001) + + +def test_a_non_figure_raises_rather_than_becoming_zero(): + """Silently reading 'pending' as 0.00 would corrupt a total.""" + with pytest.raises(ValueError): + parse.money("pending") + + +def test_pdf_and_csv_of_the_same_run_agree(): + pdfs = glob.glob(f"{CORPUS}/loss-runs/*.pdf") + if not pdfs: + pytest.skip("no PDF in corpus") + try: + pdf = parse.load(pdfs[0]) + except RuntimeError as exc: + pytest.skip(str(exc)) + csv_path = pdfs[0].replace(".pdf", ".csv") + if not os.path.exists(csv_path): + pytest.skip("no matching CSV") + csv_run = parse.load(csv_path) + assert len(pdf.claims) == len(csv_run.claims) + assert pdf.summed_total == pytest.approx(csv_run.summed_total, abs=0.01) + a = {c.claim_no: (c.status, round(c.incurred, 2)) for c in pdf.claims} + b = {c.claim_no: (c.status, round(c.incurred, 2)) for c in csv_run.claims} + assert a == b, "the same policy year read two ways gave different facts" + + +# -------------------------------------------------------------------------- +# budget, honesty, output +# -------------------------------------------------------------------------- + +def test_free_stages_cost_nothing_and_searches_do_not(): + led = Ledger(cap=1) + for stage in ("upload", "approve", "export"): + led.charge(stage) + assert led.spent == 0 + led.charge("search") + assert led.spent == 1 + with pytest.raises(BudgetExceeded): + led.charge("chat") + assert led.spent == 1, "a refused spend must not be recorded" + + +def test_the_review_names_what_it_cannot_establish(analysed): + s, _ = analysed + doc = build_html(s, [], AS_OF) + assert "does not establish" in doc + assert "no figure in this review is model-generated" in doc.lower() + + +def test_the_instruction_forbids_touching_any_figure(): + """The document is handed to a model. What stops it rewriting a total is + this sentence, so the sentence is under test.""" + from lossrun.review import NARRATIVE_INSTRUCTION + low = NARRATIVE_INSTRUCTION.lower() + assert "do not" in low and "change" in low + assert "recalculate" in low and "restate" in low + for word in ("number", "date", "claim number", "table cell"): + assert word in low, f"{word} is not protected by the instruction" + assert "change nothing else" in low + + +def test_end_to_end_offline_spends_only_search_and_chat(tmp_path): + out = str(tmp_path / "out") + r = run(CORPUS, out, mode="dry", max_ops=6, as_of=AS_OF) + led = r["ledger"] + assert led.by_stage.get("upload", 0) == 0 + assert led.by_stage.get("export", 0) == 0 + assert led.spent == led.by_stage.get("search", 0) + led.by_stage.get("chat", 0) + assert os.path.exists(os.path.join(out, "review.html")) + data = json.load(open(os.path.join(out, "summary.json"))) + assert data["totals"]["claims"] == 41 + + +def test_hitting_the_cap_degrades_instead_of_dying(tmp_path): + """A cap reached mid-run must produce a document that says so, not a + traceback and not a document that quietly omits a section.""" + out = str(tmp_path / "out") + r = run(CORPUS, out, mode="dry", max_ops=0, as_of=AS_OF) + assert os.path.exists(os.path.join(out, "review.html")), \ + "the review must still be written when the budget is exhausted" + assert r["summary"].unverified, "the shortfall must be named, not hidden" + data = json.load(open(os.path.join(out, "summary.json"))) + assert data["totals"]["claims"] == 41, \ + "figures must survive a budget that stops the document assembly" + + +def test_no_key_shaped_string_reaches_any_output(tmp_path, monkeypatch): + monkeypatch.setenv("SUPERDOCS_API_KEY", "sk_live_MUST_NOT_APPEAR") + out = str(tmp_path / "out") + run(CORPUS, out, mode="dry", max_ops=6, as_of=AS_OF) + for name in os.listdir(out): + body = open(os.path.join(out, name), encoding="utf-8", errors="ignore").read() + assert "sk_live" not in body + + +def test_retrieval_reply_is_parsed_tolerantly(): + """Models fence JSON and pad it with prose. Neither should lose the data, + and an unusable reply must yield nothing rather than a corrupt section.""" + from lossrun.superdocs import _parse_retrieval + good = ('Here you go:\n```json\n[{"question": "q", "document": "note.docx", ' + '"quote": "reserve increased"}]\n```') + assert _parse_retrieval(good) == [ + {"document": "note.docx", "text": "reserve increased", "question": "q"}] + assert _parse_retrieval("[]") == [] + assert _parse_retrieval("sorry, I could not find anything") == [] + assert _parse_retrieval("") == [] + assert _parse_retrieval("[{not json") == [] + # An entry with no document is dropped rather than rendered blank. + assert _parse_retrieval('[{"question":"q","document":null,"quote":""}]') == [] + + +def test_a_missing_search_endpoint_is_named_not_swallowed(): + """404 on the dedicated search path must be distinguishable from a + successful empty search.""" + from lossrun.superdocs import SearchUnavailable, ApiError + assert issubclass(SearchUnavailable, ApiError) + + +def test_the_whole_run_costs_one_operation(tmp_path): + """Retrieval and narrative are one edit, not two. Uploads, approval and + export are free, so a full review is a single chargeable turn.""" + r = run(CORPUS, str(tmp_path / "out"), "dry", 6, AS_OF) + assert r["ledger"].spent == 1, r["ledger"].by_stage + + +def test_the_correspondence_placeholder_is_filled_or_named(tmp_path): + out = str(tmp_path / "out") + r = run(CORPUS, out, "dry", 6, AS_OF) + doc = open(os.path.join(out, "review.html"), encoding="utf-8").read() + assert "From the correspondence" in doc + # The placeholder is what the edit replaces; it must not survive into a + # document that reports success. + exported = open(r["exported"], encoding="utf-8").read() + assert "[[CORRESPONDENCE]]" not in exported + + +def test_figures_do_not_depend_on_retrieval(tmp_path): + """The claim the README makes: no figure is model-generated. Prove it by + running with retrieval switched off and comparing every total.""" + a = run(CORPUS, str(tmp_path / "with"), "dry", 6, AS_OF)["summary"] + b = run(CORPUS, str(tmp_path / "without"), "dry", 6, AS_OF, + skip_prose=True)["summary"] + assert a.total_incurred == b.total_incurred + assert a.total_claims == b.total_claims + assert [y.to_dict() for y in a.years] == [y.to_dict() for y in b.years] + assert len(a.findings) == len(b.findings) From 081436713b1d27ccfae9266a79f020c2e6d16e35 Mon Sep 17 00:00:00 2001 From: SiddharthMuneshwar26 Date: Wed, 19 Aug 2026 14:57:20 +0300 Subject: [PATCH 2/6] Document known defect in DOCX export process Added known defect section for DOCX export issue and clarified the correctness of the HTML output. --- .../insurance-loss-run-summary/README.md | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/README.md b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/README.md index 204fb5cf..b8a00596 100644 --- a/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/README.md +++ b/use-cases/SiddharthMuneshwar26/insurance-loss-run-summary/README.md @@ -117,7 +117,7 @@ test for that. ## What it produces -`out/review.html` and `out/loss-run-review.docx`: +`out/review.html` — and see the known defect below regarding the DOCX export: - **Summary by policy year** — claims, open count, paid, reserve, incurred, loss ratio, and which document each year's figures came from @@ -184,6 +184,25 @@ Everything is fabricated. Meridian Cold Chain Logistics, Kestrel Mutual and Ardent Risk Partners do not exist, and no figure comes from any real claim, policy or company. +## Known defect: the DOCX export returns the wrong document + +**`out/review.html` is correct and complete. `out/loss-run-review.docx` is not.** + +The app uploads the adjuster notes, application and correspondence to the +session so the agent can read them for the correspondence section. Export then +asks the session for "the document" — and returns the last file uploaded, not +the review that was passed to the edit call as `document_html`. On the sample +corpus the exported DOCX comes back containing the underwriting checklist. + +Everything upstream of the export is unaffected: the figures, the conflicts, the +findings and the citations are all correct, and `out/summary.json` and +`out/review.html` carry them. The defect is in how this build sequences upload +against export, not in the analysis. + +The likely fix is to make the review itself the session document before editing, +rather than passing it only as `document_html`. That needs live API calls to get +right, so it is stated here rather than guessed at. + ## Honest limits - **The carrier's own numbers are taken as given.** Each run is checked against @@ -209,6 +228,8 @@ policy or company. early version flagged any two claims sharing a date and figure, which on a fleet account is coincidence rather than duplication. Found by the clean-corpus test, and the rule was fixed rather than the test. +- **The DOCX export is broken**, as above. The HTML review is the working + deliverable. - **No coverage recommendation.** The app summarises and flags; it does not advise whether to write the risk, and the chat instruction forbids adding one. From 51115ec9648c071a9ad405e97bff5417381842d2 Mon Sep 17 00:00:00 2001 From: SiddharthMuneshwar26 Date: Thu, 20 Aug 2026 21:16:34 +0300 Subject: [PATCH 3/6] Modify README with updated test instructions Updated README to reflect changes in test command and added details. --- .../SiddharthMuneshwar26/table-fidelity-benchmark/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/README.md b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/README.md index 5c2b87f0..aecf00d8 100644 --- a/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/README.md +++ b/extensions/SiddharthMuneshwar26/table-fidelity-benchmark/README.md @@ -7,6 +7,8 @@ not, says *which stage* lost them. Built for the SuperDocs Round 2 task (assigned build: table fidelity benchmark, band S2, surfaces: API + export). +> Built by Siddharth Muneshwar for the SuperDocs task. + ## Run it ```bash @@ -120,7 +122,7 @@ Actual spend is printed at the end of every run and reported at the top of ## Tests ```bash -make test # 19 tests, no API key, no network, no operations +python -m pytest tests/ -q # 26 tests, no API key, no network, no operations ``` The tests do not assert that the fake returns what it was handed. The offline From 64109a3b6fe161005d80f779665e8771563ef0db Mon Sep 17 00:00:00 2001 From: SiddharthMuneshwar26 Date: Thu, 20 Aug 2026 21:17:37 +0300 Subject: [PATCH 4/6] Fix formatting in README.md From 250dbc504a7e06f7bf12a397767e00acc8eb5c61 Mon Sep 17 00:00:00 2001 From: SiddharthMuneshwar26 Date: Thu, 20 Aug 2026 21:18:09 +0300 Subject: [PATCH 5/6] Update README.md From bd8d8ffbbeebc902e13faa2d0418d2c6f8f6aa90 Mon Sep 17 00:00:00 2001 From: SiddharthMuneshwar26 Date: Thu, 20 Aug 2026 21:20:37 +0300 Subject: [PATCH 6/6] Revise README for Insurance loss-run summary app Updated README to reflect new app features and structure. --- README.md | 241 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 225 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 24c914a5..b8a00596 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,237 @@ -# SuperDocs Builds +# Insurance loss-run summary app -Community builds: apps, integrations and extensions built on the [SuperDocs](https://superdocs.app) platform. +Turns years of raw loss-run data across multiple policies and carriers into one +review document an underwriter can act on: claim counts, incurred totals, and +open claims grouped by policy year — with every figure traced to the document +and row it came from. -SuperDocs is an AI document editor that works inside the document, not next to it. It ships as a free web app, a REST API, and an MCP server that lets AI agents read and edit documents on their own. Everything in this repository is built on that public surface. +Built on SuperDocs (upload, search, chat, approve, export) for the SuperDocs +task. Assigned build, band S2, surfaces: search and export. -## What lives here +> Built by Siddharth Muneshwar for the SuperDocs task. -| Folder | What belongs in it | +## What SuperDocs features it uses + +| feature | how | |---|---| -| [`use-cases/`](use-cases/) | Apps and end-to-end use cases: vertical tools, document workflows, demos that solve a real problem for a real kind of user | -| [`extensions/`](extensions/) | Extensions and developer tooling: editor integrations, plugins, CLIs, SDK wrappers, agent pipelines, anything that extends where SuperDocs can run | +| **Upload** | the adjuster notes, application and broker correspondence are attached to the session; free, and what makes cross-document retrieval possible | +| **Chat editing** | one turn does two jobs: fills a placeholder section from the attached documents, and writes the opening narrative | +| **Approve** | the proposed changes are approved before anything is written | +| **Export** | the review document comes back as DOCX | + +A full review costs **one operation**. Figures never pass through the model: +they are parsed from the loss runs and the instruction forbids changing them. + +## Run it + +``` +pip install -r requirements.txt +python -m pytest tests/ -q +python -m lossrun.review --corpus corpus/out --out out --mode dry --max-ops 6 --as-of 2026-01-09 +``` + +25 tests, no API key, no network, no operations; then the full pipeline +offline. + +The generated corpus ships in `corpus/out/`, so there is nothing to build first. +Regenerating it (`cd corpus && python generate.py`) needs LibreOffice on the +path for the PDF loss run; nothing else does. + +Against the live API: + +``` +export SUPERDOCS_API_KEY=... +python -m lossrun.review --corpus corpus/out --out out --mode live --max-ops 6 --as-of 2026-01-09 +``` + +Four operations, capped at six. On PowerShell the first line is +`$env:SUPERDOCS_API_KEY = '...'`. + +A `Makefile` wraps these as `make install`, `make test`, `make dry` and +`make live` where `make` is available. It is a convenience, not a dependency — +every command above runs as written on Windows, macOS and Linux. + +The key is read from the environment only — never an argument, never logged, +never written to an output file, and a test asserts no key-shaped string reaches +any output. + +## The one design decision everything follows from + +**Figures are parsed. They are never searched for, inferred, or model-generated.** + +Claim counts, incurred totals and loss ratios are arithmetic over rows read out +of the loss runs. An underwriter cannot act on a total that might be a plausible +guess, and a summary whose numbers need re-checking has saved nobody any work. + +Search and chat are spent where they earn their cost: + +| work | how | operations | +|---|---|---| +| claim figures, counts, totals, ratios | parsed from CSV/PDF rows | 0 | +| contradictions living in prose, and the narrative | one edit | **1** | +| upload, attach, approve, export | free per published pricing | 0 | + +**A full review costs one operation.** + +**On the search surface, and two wrong turns getting here.** A dedicated +`/v1/search` endpoint returned `HTTP 404` live, so retrieval moved to the chat +surface: the prose documents are attached to the session by `upload`, which is +free. The first attempt asked for a JSON chat reply and looked for it in the +proposed-changes payload — which carries document edits, not chat text, so the +answer was never there to find. That was fighting the product. SuperDocs is an +editing agent: the answer belongs *in the document*. The review is now written +with a placeholder section, and one edit both fills it from the attached +documents and adds the opening narrative. Supported path, and half the cost of +the version that was wrong. + +`SuperDocsClient.search` and `.retrieve` are kept, unused, for the day a search +endpoint or a chat-reply field is exposed. `search` raises `SearchUnavailable` +on 404 rather than pretending an empty search succeeded. + +The chat instruction says explicitly that every number in the document is +already correct and must not be changed, recalculated, rounded or restated. +There is a test asserting that instruction still says so. + +A naive design that searched once per claim would have spent 41 operations on +one account and still produced figures nobody could audit. + +## What makes loss runs hard + +A loss run is a carrier's claims history for one policy year. They get +**reissued**, and reserves develop between issues. The same claim carries +$45,000 on the run valued January 2024 and $182,400 on the reissue valued +January 2026. Neither document is wrong. + +This is not a detail. On the sample account: + +| source for policy year 2023 | incurred | loss ratio | referral rule | +|---|---|---|---| +| original run, valued 2024-01 | $286,092.44 | **57.1%** | passes | +| reissue, valued 2026-01 | $647,992.44 | **129.3%** | **refer** | + +The same account either clears the 60% referral threshold or blows through it +twice over, depending on which document is trusted. So the app takes the later +valuation as authoritative **and records the difference as a conflict** rather +than overwriting quietly. A conflict is never delivered pre-resolved; there is a +test for that. + +## What it produces + +`out/review.html` — and see the known defect below regarding the DOCX export: + +- **Summary by policy year** — claims, open count, paid, reserve, incurred, loss + ratio, and which document each year's figures came from +- **Open claims**, listed separately with months open, because reserves on open + claims may still develop +- **Conflicts between sources**, surfaced with both figures and the consequence +- **Checklist findings** against eight underwriting rules +- **What this review does not establish** — named, not smoothed over +- **Sources**, including which documents were read and superseded + +`out/summary.json` carries the same content for anything downstream. + +## The rules + +R1 severity claims need an adjuster note · R2 open more than 36 months · +R3 loss ratio above 60% · R4 date of loss outside its policy period · +R5 application consistent with the loss runs · R6 dormant reserves · +R7 duplicate claims · R8 loss run adds up to its own stated total + +A clean corpus produces no findings at all, and that path is tested. An honest +report of nothing is a valid output. + +## Formats + +CSV and PDF loss runs both parse to the same rows, and there is a test asserting +the same policy year read two ways gives identical facts. The PDF parser is +deliberately conservative: a row is accepted only when every field is present +and every figure parses. Rows that look like claim lines but do not fully parse +are counted and reported as unreadable rather than guessed at, because a +half-read row is worse than a missing one on a document feeding a coverage +decision. + +Accountant notation is parsed as written: `(12,400.00)` is negative, `—` is nil, +`$1,204,880.00` is a figure. A string that is not a figure raises rather than +quietly becoming `0.00` — silently reading `pending` as zero would corrupt a +total. + +## Budget + +One operation per run, capped at six. The cap is enforced **before** each +spend. Hitting it degrades rather than dies: the review is still produced, and +the section that could not be completed is named in *What this review does not +establish* instead of quietly disappearing. There is a test that runs with a cap +of one and asserts exactly that. + +```bash +python -m lossrun.review --corpus corpus/out --out out --mode live --max-ops 6 --as-of 2026-01-09 --skip-prose +``` + +## The corpus + +`corpus/` generates a fabricated account — Meridian Cold Chain Logistics, a +refrigerated carrier — with 41 claims across four policy years, five loss runs +including one reissue, three adjuster notes, a signed application, a broker +cover note, and the underwriting checklist. CSV, PDF and DOCX. + +`corpus/MANIFEST.md` is the answer key: nine planted conflicts and thirteen +expected findings, with verified figures. The test suite asserts the app +reproduces it **exactly** — every planted item found, and nothing outside the +key reported. 34 of the 41 claims are deliberately clean, which is what makes a +false positive measurable rather than arguable. + +Everything is fabricated. Meridian Cold Chain Logistics, Kestrel Mutual and +Ardent Risk Partners do not exist, and no figure comes from any real claim, +policy or company. + +## Known defect: the DOCX export returns the wrong document -Every project is self-contained in its builder's own folder and carries its own README. +**`out/review.html` is correct and complete. `out/loss-run-review.docx` is not.** -## Contributing +The app uploads the adjuster notes, application and correspondence to the +session so the agent can read them for the correspondence section. Export then +asks the session for "the document" — and returns the last file uploaded, not +the review that was passed to the edit call as `document_html`. On the sample +corpus the exported DOCX comes back containing the underwriting checklist. -Fork the repo, build in your own folder, open a pull request. The full mechanics, including how to name your folder and what your PR description must include, are in [CONTRIBUTING.md](CONTRIBUTING.md). +Everything upstream of the export is unaffected: the figures, the conflicts, the +findings and the citations are all correct, and `out/summary.json` and +`out/review.html` carry them. The defect is in how this build sequences upload +against export, not in the analysis. -## Useful links +The likely fix is to make the review itself the session document before editing, +rather than passing it only as `document_html`. That needs live API calls to get +right, so it is stated here rather than guessed at. -- Product: [use.superdocs.app](https://use.superdocs.app) -- Developer documentation: [docs.superdocs.app](https://docs.superdocs.app) -- Contact: hello@superdocs.app +## Honest limits -## License +- **The carrier's own numbers are taken as given.** Each run is checked against + itself and against the others; nothing here can confirm the carrier was right. +- **Retrieval quality is not measured.** The offline fake fills the placeholder + by keyword, deliberately weaker than the live agent, so a point that only + surfaces against the real API stays visible rather than hidden. +- **If the edit proposes no changes**, the correspondence section keeps its + placeholder and the run says so under *What this review does not establish* + rather than reporting a success it did not have. +- **Figures do not depend on retrieval at all**, and there is a test that proves + it: the pipeline is run twice, once with retrieval and once with + `--skip-prose`, and every total, ratio and finding must be identical. +- **R5 reads specific declarations** from the application. Where a declaration + is not found, nothing is asserted about it — an absent statement is not a + false one. +- **PDF reading needs `pdfplumber`**, which `requirements.txt` installs. An + earlier version shelled out to `pdftotext`, a Unix binary: it worked on the + machine it was written on and died with a bare `WinError 2` on the machine it + was used on. Where no reader is available the CSV path is unaffected and the + failure names the fix instead of raising a traceback. +- **R7 needs description overlap**, not just a matching date and amount. An + early version flagged any two claims sharing a date and figure, which on a + fleet account is coincidence rather than duplication. Found by the + clean-corpus test, and the rule was fixed rather than the test. +- **The DOCX export is broken**, as above. The HTML review is the working + deliverable. +- **No coverage recommendation.** The app summarises and flags; it does not + advise whether to write the risk, and the chat instruction forbids adding one. -MIT. Every contribution stays publicly credited to its author, permanently. +Portions of this work were written with AI assistance, directed and reviewed +by me.