From 9bd97216b5a5f1b45d2393b00e8f3f757ec12ae1 Mon Sep 17 00:00:00 2001 From: "Peter B. Johnson" Date: Wed, 23 Sep 2026 03:56:51 +0100 Subject: [PATCH] implement: Run the web page through the two-route conversion (t41) --- README.md | 23 +- in2lambda_agent/routes.py | 44 +++- in2lambda_agent/ui/page.html | 156 +++++------- in2lambda_agent/ui/server.py | 232 ++++++------------ tests/test_routes.py | 19 ++ tests/test_ui.py | 456 ++++++++++++++++------------------- 6 files changed, 413 insertions(+), 517 deletions(-) diff --git a/README.md b/README.md index 43f0f3a..bd5381b 100644 --- a/README.md +++ b/README.md @@ -294,16 +294,19 @@ This serves one page on `http://127.0.0.1:8765/` and opens it; `--no-open` print address and opens nothing. The page lists `--corpus` — `./ExampleContents` by default, or the current directory where there is no such folder — one directory at a time: click a folder to list it, and a document to pick it. A source elsewhere goes into the -box by hand. Set the options the `run` command takes, and press Go. Each stage line -arrives on the page as the stage finishes, with the tokens -and seconds of each model call. A run in review mode stops with its questions, each -beside its rendered PDF, and approve, reject and edit answer them without leaving the -page; the stages of a rejection's fixing rounds arrive the same way. When the run -ends, the page links to the zip, the rendered PDFs, the draft, the spec and the run -record. - -It is a harness for trying the agent by hand. It listens on this machine only, has no -authentication, and runs one run at a time. +box by hand. + +The page runs the `convert` command: the two routes, the reconciliation and the build. +Name the solutions document, or leave that box empty for the document beside the source; +name a Lua filter for route B, or tick Write filter for one model call that writes +`filter.lua` into the out directory; then press Go. Each stage line — `ocr`, `route A`, +`route B`, `fields`, `build` — arrives on the page as the stage finishes. When the run +ends, the page shows each flagged field with the reason it is flagged and each route's +reading of it, the counts of the reconciliation, the tokens, and links to the zip and to +the filter where the run wrote one. + +The page is a harness for trying the agent by hand. It listens on this machine only, has +no authentication, and runs one conversion at a time. ### Checking the OCR against the page diff --git a/in2lambda_agent/routes.py b/in2lambda_agent/routes.py index 730ccee..a968ff7 100644 --- a/in2lambda_agent/routes.py +++ b/in2lambda_agent/routes.py @@ -26,7 +26,7 @@ import subprocess from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Optional +from typing import Any, Callable, Optional from in2lambda.api.part import Part from in2lambda.api.question import Question @@ -440,6 +440,22 @@ def markdown_of(document: Path, cache_dir: Path, settings: Settings) -> tuple[st return out.stdout.decode("utf-8"), document.parent +def _read_as(document: Path, cache_dir: Path) -> str: + """How one document's markdown is got, for the ocr stage line. + + Asked before the conversion, because a PDF the cache held no entry for is cached by + the time the line is written. + """ + suffix = Path(document).suffix.lower() + if suffix in (".md", ".markdown"): + return "read" + if suffix != ".pdf": + return "pandoc" + from in2lambda_agent.ocr import cached + + return "cached" if cached(document, cache_dir) is not None else "mathpix" + + def convert( document: Path, solutions: Optional[Path] = None, @@ -450,6 +466,7 @@ def convert( settings: Optional[Settings] = None, lua: Optional[Path] = None, name: str = "set", + on_stage: Optional[Callable[[str, str], None]] = None, ) -> Converted: """Route A, route B where a filter is given, reconcile, verify, write. @@ -457,16 +474,31 @@ def convert( merged before the comparison. Where a filter run fails, the route A reply is the result and `route_b_error` holds pandoc's message, so that one sheet of a folder does not stop the other eight. + + `on_stage`, where it is given, is called with a name and a message as each step + finishes - `ocr`, `route A`, `route B`, `fields`, `build` - so that a caller watching + a run shows each line as the step ends rather than the report at the end of it. """ settings = settings or load_settings() backend = backend or choose_backend(settings) + + def said(stage: str, message: str) -> None: + if on_stage is not None: + on_stage(stage, message) + + read = [f"{Path(d).name}: {_read_as(d, cache_dir)}" for d in (document, solutions) if d is not None] markdown, images = markdown_of(document, cache_dir, settings) solutions_md = markdown_of(solutions, cache_dir, settings)[0] if solutions else None + said("ocr", "; ".join(read)) source = markdown + ("\n" + solutions_md if solutions_md else "") reply, usage = direct(markdown, solutions_md, backend) + tokens = usage.usage.input_tokens + usage.usage.output_tokens + said("route A", f"{tokens} tokens") counts, error = (0, 0, 0, 0), None flags = [Flag(k, fields(reply)[k], "", "not a quote of the source") for k in not_verbatim(reply, source)] - if lua is not None: + if lua is None: + said("route B", "did not run: no filter") + else: try: other = run_filter(lua, document) if solutions is not None: @@ -474,6 +506,7 @@ def convert( except (subprocess.CalledProcessError, json.JSONDecodeError) as problem: stderr = getattr(problem, "stderr", None) error = (stderr.decode("utf-8", "replace") if stderr else str(problem)).strip() + said("route B", f"failed: {error}") else: reconciled = reconcile(reply, other, source, backend) reply, flags = reconciled.fields, reconciled.flags @@ -481,13 +514,16 @@ def convert( reconciled.agreed + reconciled.defaulted + reconciled.adjudicated, reconciled.agreed, reconciled.defaulted, reconciled.adjudicated, ) + said("route B", "ran") for k in stray_minus(reply): if not any(f.field == k for f in flags): flags.append(Flag(k, fields(reply)[k], "", STRAY_MINUS)) + said("fields", _counted([*counts, len(flags)])) built = to_set(reply, name=name, directory=images) + zip_path = build(built, out_dir) + said("build", str(zip_path)) return Converted( - set=built, zip_path=build(built, out_dir), flags=flags, reply=reply, - tokens=usage.usage.input_tokens + usage.usage.output_tokens, + set=built, zip_path=zip_path, flags=flags, reply=reply, tokens=tokens, fields=counts[0], agreed=counts[1], defaulted=counts[2], adjudicated=counts[3], route_b_error=error, ) diff --git a/in2lambda_agent/ui/page.html b/in2lambda_agent/ui/page.html index df77781..0aa4f87 100644 --- a/in2lambda_agent/ui/page.html +++ b/in2lambda_agent/ui/page.html @@ -1,11 +1,11 @@ @@ -30,7 +30,6 @@ padding: 0.8rem; white-space: pre-wrap; } - iframe { border: 1px solid #ccc; height: 22rem; width: 100%; } #entries { border: 1px solid #ddd; list-style: none; @@ -42,7 +41,8 @@ } #entries a { color: #06c; cursor: pointer; } #where { margin: 0.4rem 0 0.2rem 9rem; } - .question { border-top: 1px solid #ddd; margin-top: 1rem; padding-top: 1rem; } + .flag { border-top: 1px solid #ddd; margin-top: 1rem; padding-top: 1rem; } + .flag h3 { font-size: 1rem; margin: 0; } .error { color: #b00; } .note { color: #666; } @@ -58,32 +58,22 @@

in2lambda agent

-