AI desktop path: plain-HTML Review and Cost tabs over the Qt bridge, gate in Python

ai_sections.html pastes into the no-AI dashboard.html; ai_bridge.py adds one
ai_request slot that hands every AI contract request (incl. GET /roi) to the
review backend's dispatch(). Desktop page check reruns the web review and cost
cases plus a tampered-page gate case. Eval fixture e-pyqt-ai added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
VasanthandClaude Opus 5.5 committed 2026-10-16 15:32:45 +05:30
1 parent 128f153f84
commit d89dce3c33
13 files changed
+203 -14

No files matched your search

+9 -1
View File
@@ -29,6 +29,12 @@ uv run --with pytest --with playwright pytest tests
by-hand assumptions), plus every no-AI case. Tests set run and review state on the stub
directly (the `stub` fixture). To check a generated dashboard folder instead, set
`PAGE_CHECK_AI_DIR=path/to/dashboard`. Needs npm on PATH, else it skips.
- `page_check_ai_desktop/`: **Seam 1, AI desktop page.** The no-AI `dashboard.html` with
`ai_sections.html` pasted in, loaded from `file://` with a fake Qt bridge whose
`ai_request` slot goes to `server_stub.dispatch()` in this process (what `ai_bridge.py`
does). Runs the AI web page's review and cost cases and the no-AI cases unchanged, plus
a tampered page calling the slot directly (still refused). To check a generated folder,
set `PAGE_CHECK_AI_DESKTOP_DIR=path/to/dashboard`; `PAGE_CHECK_WIRED=1` as above.
- `test_pricing.py`: the AI cost function (normal, long-context, thinking tokens,
unknown model).
- `test_reference_scan.py`: no patient data, internal IPs, credentials or keys in any
@@ -45,7 +51,9 @@ uv run --with pytest --with playwright pytest tests
fixture folder with different answers: `c-keep` and `c-restyle` both run on
`c-web-existing-frontend`, and also check what changed in the files it already had.
An `"ai": True` entry (`d-ai-extraction`) expects the AI skill's React starter and
runs `page_check_ai/` on the folder it built.
runs `page_check_ai/` on the folder it built. An `"ai_desktop": True` entry
(`e-pyqt-ai`) expects the desktop AI page and runs `page_check/` (with the bot replay)
and `page_check_ai_desktop/` on it.
For a bot fixture (`b-pyqt-bot`), the eval also dry-runs the generated
`bot_runner.py`, saves its events to `evals/results/<fixture>.run.jsonl`, and
replays them in the page check.
+21 -2
View File
@@ -80,6 +80,17 @@ FIXTURES = {
"page_has": ["--accent:#ff3049"],
"ai": True,
},
# (e) PyQt bot with an LLM extraction call: the AI skill's desktop path, no web server.
"e-pyqt-ai": {
"prompt": "Build me an RPA dashboard for this project. I can't answer questions while you "
"work, so here are my answers to anything the skill asks. Colours: Tebra Sky. "
"Optional sections: none.",
"frontend": r"^\s*(none|no\b)",
"asks": [r"colou?r", r"section"],
"never_asks": [r"\bkeep\b|restyle", r"desktop|web"],
"bot": True,
"ai_desktop": True,
},
}
# A line a restyle may change: a custom property, a comment, a blank, or a web-font link.
TOKEN_LINE = r"^\s*(--[\w-]+\s*:.*|/\*.*\*/|<link[^>]*fonts\.googleapis\.com.*|@import.*fonts\.googleapis\.com.*)?\s*$"
@@ -171,6 +182,11 @@ def main(fixture):
html += css.read_text(encoding="utf-8") if css.exists() else ""
checks["AI path: review starter and server stub built"] = all(
(pages[0].parent / f).exists() for f in ["server_stub.py", "bands.py", "src/components/Review.tsx"])
if cfg.get("ai_desktop"): # the no-AI page with the plain-HTML AI sections, hosted by ai_bridge.py
host = pages[0].with_name("ai_bridge.py")
checks["AI desktop path: sections, ai_bridge and review backend built"] = 'id="ai-review-tab"' in html and all(
pages[0].with_name(f).exists() for f in ["ai_bridge.py", "server_stub.py", "bands.py", "pricing.py"])
checks["no placeholders left in ai_bridge.py"] = host.exists() and "{{" not in host.read_text(encoding="utf-8")
shutil.copy(pages[0], results / f"{fixture}.dashboard.html")
checks["no placeholders left"] = "{{" not in html
if cfg.get("brand"):
@@ -186,8 +202,11 @@ def main(fixture):
env.update(bot_checks(project, html, checks, results / f"{fixture}.run.jsonl"))
if cfg.get("ai"):
env["PAGE_CHECK_AI_DIR"] = str(pages[0].parent)
check_dir = ROOT / "tests" / ("page_check_ai" if cfg.get("ai") else "page_check")
pc = subprocess.run([sys.executable, "-m", "pytest", str(check_dir), "-q"],
check_dirs = [ROOT / "tests" / ("page_check_ai" if cfg.get("ai") else "page_check")]
if cfg.get("ai_desktop"):
env["PAGE_CHECK_AI_DESKTOP_DIR"] = str(pages[0].parent)
check_dirs.append(ROOT / "tests" / "page_check_ai_desktop")
pc = subprocess.run([sys.executable, "-m", "pytest", *map(str, check_dirs), "-q"],
env=env, capture_output=True, text=True)
checks["page check passes"] = pc.returncode == 0
page_check = pc.stdout.strip().splitlines()[-1:] if pc.stdout else [pc.stderr[-500:]]
+11
View File
@@ -0,0 +1,11 @@
# Invoice intake bot
A desktop RPA bot. It reads each scanned invoice in `inbox/` with an LLM
(`extractor.py`) and queues the fields for a person to review.
The operator window will be a PyQt6 app.
Run the bot on its own:
```
python -m bot
```
+31
View File
@@ -0,0 +1,31 @@
"""Invoice intake bot: picks up scanned invoices and extracts their fields with an LLM.
Start it with: python -m bot"""
import time
from pathlib import Path
INBOX = Path(__file__).with_name("inbox")
def say(line):
print(line, flush=True)
time.sleep(0.2)
def main():
say("[step 1] Initializing")
docs = sorted(INBOX.glob("*.pdf"))
say(f"[inbox] {len(docs)} invoices waiting")
say("[step 2] Extracting fields")
for i, doc in enumerate(docs, 1):
try:
from extractor import extract_invoice # the LLM call
rows = extract_invoice(doc.read_bytes(), "application/pdf")
say(f"[extract] {doc.name}: {len(rows)} fields ({i}/{len(docs)})")
except Exception as e: # one bad document must not stop the batch
say(f"ERROR: extraction failed for {doc.name}: {type(e).__name__}")
say("[step 3] Queueing for review")
say(f"[all done] {len(docs)} invoices processed")
if __name__ == "__main__":
main()
+34
View File
@@ -0,0 +1,34 @@
"""The LLM extraction call: one scanned invoice in, its fields out, each with word scores."""
import json
from google import genai
from google.genai import types
MODEL = "gemini-2.5-flash"
FIELDS = { # field name -> mandatory
"Invoice number": True,
"Invoice date": True,
"Vendor": True,
"PO number": False,
"Due date": True,
"Total": True,
}
PROMPT = (
"Read this invoice. Return JSON: a list of {field, value, word_scores}, one per field in "
+ ", ".join(FIELDS)
+ ". word_scores is your 0-1 legibility score for each word of the value. Use an empty "
"value and no scores when a field isn't on the page."
)
def extract_invoice(document: bytes, mime_type: str) -> list[dict]:
client = genai.Client() # credentials come from the environment
response = client.models.generate_content(
model=MODEL,
contents=[types.Part.from_bytes(data=document, mime_type=mime_type), PROMPT],
config=types.GenerateContentConfig(response_mime_type="application/json"),
)
rows = json.loads(response.text)
for row in rows:
row["mandatory"] = FIELDS.get(row["field"], False)
return rows
+2
View File
@@ -0,0 +1,2 @@
%PDF-1.4
% sample invoice placeholder
+2
View File
@@ -0,0 +1,2 @@
%PDF-1.4
% sample invoice placeholder
+3
View File
@@ -0,0 +1,3 @@
PyQt6
PyQt6-WebEngine
google-genai
+1 -1
View File
@@ -63,7 +63,7 @@ def site(tmp_path_factory):
sections = (AI / "desktop" / "ai_sections.html").read_text(encoding="utf-8")
out = tmp_path_factory.mktemp("desktop")
(out / "dashboard.html").write_text(html.replace("</body>", sections + "\n</body>"), encoding="utf-8")
for f in ["server_stub.py", "bands.py"]:
for f in ["server_stub.py", "bands.py", "pricing.py"]:
(out / f).write_bytes((AI / "web" / f).read_bytes())
return out
@@ -16,6 +16,14 @@ from test_ai_review import ( # noqa: E402,F401
test_page_x_of_y_updates_while_running,
)
from test_ai_review import FIELDS, FLAGGED, open_review # noqa: E402
from test_ai_cost import ( # noqa: E402,F401
cost_page,
test_by_hand_panel_is_labelled_assumptions_and_recomputes,
test_coverage_pill_counts_priced_of_all_documents,
test_month_breakdown_groups_runs_by_month,
test_runs_show_tokens_and_cost_and_unpriced_is_not_zero,
test_spend_tiles_sum_the_priced_runs,
)
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "page_check"))
from test_contract import ( # noqa: E402,F401