"""MiniWeb Task Annotation Interface. Two modes: 1. Review Sites — browse each site, leave free-form feedback 2. Annotate Tasks — system samples N sites × M macros, annotator designs task The annotation blueprint is registered in the main MiniWeb app. Access at http://localhost:8080/annotate/ """ from helpers.paths import REPO_ROOT import json import os import random import uuid import pickle from datetime import datetime from pathlib import Path from flask import Blueprint, jsonify, redirect, render_template, request, session, url_for from miniweb.annotation.macro_locations import MACRO_LOCATIONS from miniweb.annotation import macros as _registry from miniweb.annotation import final_set as _final_set from miniweb.annotation.storage import ANNOTATIONS_DIR from miniweb.app import demo as _demo PROJECT_ROOT = REPO_ROOT SITES_DIR = PROJECT_ROOT / "src" / "miniweb" / "sites" # Annotator credentials: override via MINIWEB_ANNOTATORS env var # Format: "user1:pass1,user2:pass2,..." _DEFAULT_ANNOTATORS = {"annotator1": "miniweb", "annotator2": "miniweb", "annotator3": "miniweb", "annotator4": "miniweb"} def _load_annotator_credentials(): env = os.environ.get("MINIWEB_ANNOTATORS", "") if env: creds = {} for pair in env.split(","): if ":" in pair: user, pw = pair.split(":", 1) creds[user.strip()] = pw.strip() return creds if creds else _DEFAULT_ANNOTATORS return _DEFAULT_ANNOTATORS ANNOTATOR_CREDENTIALS = _load_annotator_credentials() annotation_bp = Blueprint( "annotation", __name__, template_folder="templates", static_folder="static", url_prefix="/annotate", ) # --------------------------------------------------------------------------- # Macro descriptions — verb + modality + explanation + example # --------------------------------------------------------------------------- # Derived from the canonical registry (data/macros.yaml) via src/miniweb/annotation/macros.py. # Retired-name entries are gone — resolve any name via _canon()/_registry.describe(). _MACRO_DESCRIPTIONS = _registry.descriptions() # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _save_task(task): """Save task + trajectory to the shared annotations directory.""" from miniweb.annotation.storage import save_task as fs_save, generate_task_id task_id = task.get("task_id") or generate_task_id() task["task_id"] = task_id # annotator becomes a directory name and a URL path segment — strip # slashes/backslashes (a stray trailing backslash once forked the dataset in two) annotator = task.get("annotator", "anonymous").strip().strip("\\/") or "anonymous" task["annotator"] = annotator trajectory = task.pop("trajectory", []) # Primary storage: file system fs_save(annotator, task_id, task, trajectory) return task_id def _load_sites(): from miniweb.annotation.storage import list_tasks sites = [] # Count annotated tasks per site from file storage task_counts = {} for t in list_tasks(): for s in t.get("sites", []): sid = s["id"] if isinstance(s, dict) else s task_counts[sid] = task_counts.get(sid, 0) + 1 for site_json in sorted(SITES_DIR.glob("*/site.json")): if site_json.parent.name.startswith("_"): continue routes = site_json.parent / "routes.py" if not routes.exists() or routes.stat().st_size < 500: continue meta = json.loads(site_json.read_text()) meta["url"] = f"/sites/{meta['id']}/" meta["annotated_count"] = task_counts.get(meta["id"], 0) meta["review_count"] = 0 # Human-readable site type (specific kind of site, from its id), for # "Name (Type)" labels — e.g. "Tax Filing, DMV & Permits". Plus the broad # category for reference. from miniweb.app import SITE_CATEGORIES, site_type meta["category"] = SITE_CATEGORIES.get(meta["id"], "Other") meta["type"] = site_type(meta["id"]) sites.append(meta) return sites def _load_macros(): """Collect all unique (canonical) macros across all sites from MACRO_LOCATIONS.""" macros = set() for site_macros in MACRO_LOCATIONS.values(): macros.update(_canon(m) for m in site_macros.keys()) return sorted(macros) def _load_site_macros(site_id): """Load supported macros for a site. MACRO_LOCATIONS is the audited source of truth — it lists only macros whose UI elements have been verified to exist on each site. Falls back to README, then tasks.json for sites not yet audited. """ # MACRO_LOCATIONS is the audited source of truth if site_id in MACRO_LOCATIONS: macros = [_canon(m) for m in MACRO_LOCATIONS[site_id].keys()] if macros: return sorted(set(macros)) # Fallback to README spec for sites not yet in MACRO_LOCATIONS import re as _re readme = SITES_DIR / site_id / "doc" / "README.md" if readme.exists(): try: text = readme.read_text() # Try comma-separated format match = _re.search( r"## Target Macros[^\n]*\n\s*\n?([\w_,\s]+?)(?:\n\n|\n##|\Z)", text, _re.DOTALL, ) if match: macros = [m.strip() for m in match.group(1).split(",") if m.strip()] if macros: return sorted(set(macros)) # Try numbered list format: "1. **macro_name** -- ..." list_macros = _re.findall( r"\d+\.\s+\*\*(\w+)\*\*", text[text.find("## Target Macros"):] if "## Target Macros" in text else "" ) if list_macros: return sorted(set(list_macros)) except OSError: pass # Fallback to tasks.json tasks_file = SITES_DIR / site_id / "tasks.json" macros = set() if tasks_file.exists(): try: for t in json.loads(tasks_file.read_text()): for m in t.get("macros", []): macros.add(m) except (json.JSONDecodeError, OSError): pass return sorted(macros) _NA_FILE = ANNOTATIONS_DIR / "na_reports.json" # Skip-modal reports (reason + details) — same download-and-collect workflow # as na_reports.json _SKIP_REPORTS_FILE = ANNOTATIONS_DIR / "skip_reports.json" def _load_na_reports(): """Load NA reports from disk.""" if _NA_FILE.exists(): try: return json.loads(_NA_FILE.read_text()) except (json.JSONDecodeError, OSError): pass return [] def _save_na_report(sites, macro, annotator): """Append a single NA report.""" reports = _load_na_reports() reports.append({ "sites": sites, "macro": macro, "annotator": annotator, "timestamp": datetime.now().isoformat(), }) _NA_FILE.write_text(json.dumps(reports, indent=2)) def _get_na_macros(site_ids): """Get macros marked N/A for these sites (threshold: 2+ reports).""" from collections import Counter reports = _load_na_reports() site_set = set(site_ids) counts = Counter() for r in reports: if site_set & set(r.get("sites", [])): counts[r["macro"]] += 1 return {m for m, c in counts.items() if c >= 2} def _get_macro_coverage(): """Count how many annotated tasks cover each macro (from file storage).""" from miniweb.annotation.storage import list_tasks coverage = {} for t in list_tasks(): for m in t.get("macros", []): m = _canon(m) coverage[m] = coverage.get(m, 0) + 1 return coverage def _task_site_ids(task): """Normalize a stored task's sites to a list of site-id strings.""" ids = [] for s in task.get("sites", []): ids.append(s.get("id") if isinstance(s, dict) else s) return ids # Coverage floor: each macro should be demonstrated on K distinct sites via # single-site tasks (or on all its sites, when fewer than K support it). # K is adjustable from the coverage page; persisted in floor_config.json. _FLOOR_CONFIG_FILE = Path(__file__).parent / "floor_config.json" _DEFAULT_FLOOR_K = 3 def _get_floor_k(): try: k = int(json.loads(_FLOOR_CONFIG_FILE.read_text()).get("k", _DEFAULT_FLOOR_K)) return max(1, min(10, k)) except (OSError, ValueError, json.JSONDecodeError): return _DEFAULT_FLOOR_K # Retired macro names (merged 2026-07-08) -> canonical. Applied when READING # task files so historical annotations keep counting; task files themselves # are never rewritten. (filter_by_radio is site-scoped and NOT aliased — # it remains a valid macro on other sites.) _MACRO_ALIASES = _registry.alias_map() # Canonical-name resolver (alias chain with cycle guard) — from the registry. _canon = _registry.canon _CANONICAL_LOCATIONS = None def _canonical_macro_locations(): """MACRO_LOCATIONS re-keyed by canonical macro name, location lists aggregated across any alias keys that merge into the same macro. The UI samples canonical names, so lookups must be keyed the same way.""" global _CANONICAL_LOCATIONS if _CANONICAL_LOCATIONS is None: out = {} for site, macros in MACRO_LOCATIONS.items(): site_out = {} for m, locs in macros.items(): locs = [locs] if isinstance(locs, str) else list(locs or []) site_out.setdefault(_canon(m), []).extend(locs) out[site] = site_out _CANONICAL_LOCATIONS = out return _CANONICAL_LOCATIONS def _site_macro_pool(site_id): """Macros eligible for floor coverage on a site (excl. NA + navigation).""" pool = set(_load_site_macros(site_id)) - _get_na_macros([site_id]) pool.discard("navigate_by_route") return pool def _get_macro_site_coverage(): """macro -> set of site ids where a SINGLE-SITE task covers it.""" from miniweb.annotation.storage import list_tasks cov = {} for t in list_tasks(): ids = _task_site_ids(t) if len(ids) != 1: continue for m in t.get("macros", []): cov.setdefault(_canon(m), set()).add(ids[0]) return cov def _macro_k_targets(): """macro -> min(configured K, number of sites supporting it).""" k = _get_floor_k() supporting = {} for sid in MACRO_LOCATIONS: for m in _site_macro_pool(sid): supporting[m] = supporting.get(m, 0) + 1 return {m: min(k, n) for m, n in supporting.items()} def _get_cell_counts(): """Count existing tasks per (num_sites, num_macros) cell (from file storage).""" from miniweb.annotation.storage import list_tasks counts = {} for t in list_tasks(): n_sites = len(t.get("sites", [])) n_macros = len({_canon(m) for m in t.get("macros", [])}) key = (n_sites, n_macros) counts[key] = counts.get(key, 0) + 1 return counts def _load_graph_edges(): """Load cross-site edges from proposed_edges.csv, grouped by source.""" import csv from collections import defaultdict outgoing = defaultdict(list) incoming = defaultdict(list) edges_file = Path(__file__).parent / "proposed_edges.csv" if edges_file.exists(): with open(edges_file) as f: for row in csv.DictReader(f): if row.get("status") != "approved": continue src = row.get("source", "") tgt = row.get("target", "") evt = row.get("event_type", "") if src and tgt: outgoing[src].append((tgt, evt)) incoming[tgt].append((src, evt)) return outgoing, incoming # Edge types that imply specific macros the task should exercise # Edge-type -> hint macros to boost when that cross-site edge is present. # Canonical names only: the consumer matches these against the canonical macro # pool, so a retired name here would silently never fire. _EDGE_MACRO_HINTS = { "purchase": ["checkout_by_form", "add_by_button"], "payment": ["pay_by_form", "pay_by_dropdown"], "booking": ["book_by_form", "book_by_date_range"], "signup": ["submit_form", "authenticate_by_form"], "credential": ["authenticate_by_form"], "notification": ["extract_by_route"], "file_created": ["submit_form", "upload_by_upload"], "message": ["message_from_free_text"], "translate": ["translate_by_query", "compute_by_dropdown"], "share": ["share_by_dropdown", "share_by_toggle"], "crosspost": ["submit_form"], "shorten": ["submit_form"], "location": ["navigate_by_route", "search_by_query"], "directions": ["route_by_query"], "deadline": ["extract_by_date_range"], "query": ["search_by_query"], } def _generate_prompt(sites, coverage, force_single=False, n_sites_override=None): """Site-first prompt sampler. Strategy: 1. Pick the site with the most uncovered macros 2. Pick chain length using ratio 3:2:3:2 for lengths 1:2:3:4+ (weighted toward lengths the site still needs) 3. Pick uncovered macros for that site 4. For cross-site: expand along graph edges """ rng = random.Random() site_pool = list(sites) if not site_pool: return None site_map = {s["id"]: s for s in site_pool} # Load graph outgoing, incoming = _load_graph_edges() # --- Per-site coverage analysis --- from miniweb.annotation.storage import list_tasks all_tasks = list_tasks() # Count which macros are covered per site (by any task) site_macro_covered = {} # site_id -> set of covered macros site_chain_counts = {} # site_id -> {chain_len: count} for t in all_tasks: t_sites = [s["id"] if isinstance(s, dict) else s for s in t.get("sites", [])] t_macros = t.get("macros", []) chain_len = len(t_macros) for sid in t_sites: site_macro_covered.setdefault(sid, set()).update(t_macros) site_chain_counts.setdefault(sid, {}) site_chain_counts[sid][chain_len] = site_chain_counts[sid].get(chain_len, 0) + 1 # For each site, find uncovered macros site_uncovered = {} for s in site_pool: sid = s["id"] all_macros = set(_load_site_macros(sid)) covered = site_macro_covered.get(sid, set()) uncovered = all_macros - covered site_uncovered[sid] = uncovered # --- 1. Pick site: prefer sites whose uncovered macros are globally RARE (not # just numerous) — a site with 2 uncovered rare macros is more valuable for # global balance than one with 8 uncovered ubiquitous ones. --- site_weights = [] for s in site_pool: sid = s["id"] uncov = site_uncovered.get(sid, set()) if uncov: site_weights.append(sum(1.0 / (coverage.get(m, 0) + 1) for m in uncov)) else: site_weights.append(0.1) # small weight for fully covered sites total = sum(site_weights) norm_weights = [w / total for w in site_weights] seed = rng.choices(site_pool, weights=norm_weights, k=1)[0] sampled_sites = [seed] sampled_ids = {seed["id"]} edge_types_used = [] # --- 2. Pick chain length using ratio 3:2:3:2 for lengths 1:2:3:4 --- # Weight toward lengths the site still needs chain_counts = site_chain_counts.get(seed["id"], {}) # Target ratio for lengths 1..6. Still favors shorter chains, but includes the # longer 5- and 6-macro chains (weighted by scarcity: each length's target is # divided by its current count below, so under-filled lengths get priority). target_ratio = {1: 3, 2: 2, 3: 3, 4: 2, 5: 1, 6: 1} chain_weights = [] chain_options = [] for length, target_w in target_ratio.items(): if force_single and length > 3: continue current = chain_counts.get(length, 0) # Weight: target ratio / (current count + 1) — under-filled lengths get priority w = target_w / (current + 1) chain_weights.append(w) chain_options.append(length) if not chain_options: chain_options = [1] chain_weights = [1.0] total = sum(chain_weights) chain_weights = [w / total for w in chain_weights] n_macros = rng.choices(chain_options, weights=chain_weights, k=1)[0] # For single-site mode, cap at 3 if force_single: n_sites = 1 n_macros = min(n_macros, 3) elif n_sites_override: # Annotator explicitly chose the task's site count (1, 2, or 3). n_sites = max(1, min(3, int(n_sites_override))) if n_macros < n_sites: n_macros = n_sites else: # Longer chains often span sites, so make them progressively more likely to # be multi-site (single-site stays the majority). The longest chains may # reach 3 sites; shorter ones cap at 2. n_sites = 1 if n_macros >= 3: p_cross = min(0.6, 0.2 + 0.08 * (n_macros - 3)) # 3→.20 … 6→.44 if rng.random() < p_cross: n_sites = 2 if n_macros >= 5 and rng.random() < 0.35: n_sites = 3 if not force_single and n_sites >= 2: # Cross-site task — expand along graph edges from the seed to n_sites sites. candidates = [] # Direct outgoing for target, etype in outgoing.get(seed["id"], []): if target in site_map and target not in sampled_ids: # Weight: prefer under-annotated targets + non-hub sites t = site_map[target] hub_penalty = 0.3 if target in ("password-managers", "email") else 1.0 w = hub_penalty / (t.get("annotated_count", 0) + 1) candidates.append((t, w, etype, "outgoing")) # Direct incoming for source, etype in incoming.get(seed["id"], []): if source in site_map and source not in sampled_ids: s = site_map[source] hub_penalty = 0.3 if source in ("password-managers", "email") else 1.0 w = hub_penalty / (s.get("annotated_count", 0) + 1) candidates.append((s, w, etype, "incoming")) # Hub-mediated (seed and other site share a common target/source) if len(candidates) < 3: seed_targets = {t for t, _ in outgoing.get(seed["id"], [])} seed_sources = {s for s, _ in incoming.get(seed["id"], [])} hubs = seed_targets | seed_sources for hub in hubs: # Other sites that also connect to this hub for other, etype in outgoing.get(hub, []): if other in site_map and other not in sampled_ids and other != seed["id"]: o = site_map[other] w = 0.5 / (o.get("annotated_count", 0) + 1) candidates.append((o, w, f"via_{hub}", "hub")) for other, etype in incoming.get(hub, []): if other in site_map and other not in sampled_ids and other != seed["id"]: o = site_map[other] w = 0.5 / (o.get("annotated_count", 0) + 1) candidates.append((o, w, f"via_{hub}", "hub")) # Deduplicate candidates (keep highest weight per site) best = {} for site_obj, w, etype, direction in candidates: sid = site_obj["id"] if sid not in best or w > best[sid][1]: best[sid] = (site_obj, w, etype, direction) candidates = list(best.values()) # Pick additional sites from candidates for _ in range(n_sites - 1): if not candidates: # Fallback: random under-covered sites, filling all the way to the # requested count (previously stopped after one, so a forced # 3-site prompt could come back with only 2 sites). remaining = [s for s in site_pool if s["id"] not in sampled_ids] while remaining and len(sampled_sites) < n_sites: rw = [1.0 / (s.get("annotated_count", 0) + 1) for s in remaining] t = sum(rw) pick = rng.choices(remaining, weights=[w / t for w in rw], k=1)[0] sampled_sites.append(pick) sampled_ids.add(pick["id"]) remaining = [s for s in remaining if s["id"] != pick["id"]] break cs, ws, etypes, dirs = zip(*candidates) total = sum(ws) pick_idx = rng.choices(range(len(cs)), weights=[w/total for w in ws], k=1)[0] pick = cs[pick_idx] sampled_sites.append(pick) sampled_ids.add(pick["id"]) edge_types_used.append(etypes[pick_idx]) candidates = [(s, w, e, d) for s, w, e, d in candidates if s["id"] != pick["id"]] # 2b. Auto-include companion sites required for 2FA flows (skip in single-site mode): # - password-managers requires instant-messaging (SMS verification codes) # - any transaction site (banking edges) requires email (2FA confirmation) required_companions = {} if not force_single and "password-managers" in sampled_ids: required_companions["instant-messaging"] = "2fa_code" if not force_single: # Check if any sampled site sends payments/purchases to banking for sid in list(sampled_ids): for target, etype in outgoing.get(sid, []): if target == "banking" and etype in ("payment", "purchase", "trade"): required_companions["email"] = "2fa_verification" break # Also if banking itself is sampled (transactions need email 2FA) if "banking" in sampled_ids: required_companions["email"] = "2fa_verification" for companion_id, reason in required_companions.items(): if len(sampled_sites) >= (n_sites_override or 3): break # honor the annotator-chosen site count (default cap: 3) if companion_id not in sampled_ids and companion_id in site_map: sampled_sites.append(site_map[companion_id]) sampled_ids.add(companion_id) edge_types_used.append(reason) # Update n_sites to reflect added companions n_sites = len(sampled_sites) # Ensure n_macros >= n_sites if n_macros < n_sites: n_macros = n_sites # 3. Pick macros — combine site macros + edge-implied macros macro_pool = set() for s in sampled_sites: macro_pool.update(_load_site_macros(s["id"])) # Remove macros previously marked as N/A for these sites na_macros = _get_na_macros([s["id"] for s in sampled_sites]) macro_pool -= na_macros # Add edge-implied macros — only if they exist in the sampled sites' macro pool edge_macros = [] for etype in edge_types_used: etype_clean = etype.split("via_")[0] if etype.startswith("via_") else etype hints = _EDGE_MACRO_HINTS.get(etype_clean, []) for h in hints: if h in macro_pool: edge_macros.append(h) if not macro_pool: macro_pool = set(_load_macros()) macro_pool = sorted(macro_pool) if not macro_pool: return None # Weight: prefer macros that are uncovered on this site AND globally rare. The # uncovered boost is scaled by GLOBAL scarcity (1/(cov+1)), so a ubiquitous macro # that's merely uncovered on this one site (e.g. search at cov 51) doesn't out- # weigh a globally-rare macro (e.g. edit_by_cell at cov 0) — otherwise the common, # every-site macros dominate the corpus. site_uncov = site_uncovered.get(seed["id"], set()) edge_macro_set = set(edge_macros) macro_weights = [] for m in macro_pool: scarcity = 1.0 / (coverage.get(m, 0) + 1) w = (10.0 * scarcity) if m in site_uncov else scarcity if m in edge_macro_set: w *= 3.0 # boost edge-implied macros macro_weights.append(w) total = sum(macro_weights) macro_weights = [w / total for w in macro_weights] _QA_VERBS = {"extract", "compute", "count", "compare", "verify", "calculate", "rank", "list"} sampled_macros = [] has_qa = False remaining_macros = list(zip(macro_pool, macro_weights)) for _ in range(min(n_macros, len(remaining_macros))): if not remaining_macros: break # If we already have a QA macro, exclude other QA macros from candidates if has_qa: remaining_macros = [(m, w) for m, w in remaining_macros if m.split("_")[0] not in _QA_VERBS] if not remaining_macros: break ms, ws = zip(*remaining_macros) total = sum(ws) ws = [w / total for w in ws] pick = rng.choices(ms, weights=ws, k=1)[0] sampled_macros.append(pick) if pick.split("_")[0] in _QA_VERBS: has_qa = True remaining_macros = [(m, w) for m, w in remaining_macros if m != pick] # 4. Ensure every sampled site has at least 1 sampled macro it can perform sampled_macro_set = set(sampled_macros) for s in sampled_sites: site_macros = set(_load_site_macros(s["id"])) - na_macros if not (site_macros & sampled_macro_set): # This site has no overlap — add one of its macros site_only = sorted(site_macros - sampled_macro_set) if site_only: # Prefer under-covered macros from this site pick = min(site_only, key=lambda m: coverage.get(m, 0)) sampled_macros.append(pick) sampled_macro_set.add(pick) return { "sites": [{"id": s["id"], "name": s.get("name", s["id"]), "category": s.get("category", "Other"), "type": s.get("type", "")} for s in sampled_sites], "macros": sampled_macros, "num_sites": len(sampled_sites), "num_macros": len(sampled_macros), "edges": edge_types_used, } def _generate_site_prompt(site, rng=None): """Sampler for an annotator-chosen site. The annotator picks the site. This function picks: 1. Chain length using ratio 3:2:3:2 for lengths 1:2:3:4 (weighted toward lengths the site still needs) 2. Macros — strongly prefer uncovered macros on this site, all macros weighted equally (no difficulty bias) """ rng = rng or random.Random() site_id = site["id"] all_macros = sorted(set(_load_site_macros(site_id))) if not all_macros: return None # Get per-site coverage from miniweb.annotation.storage import list_tasks site_tasks = [t for t in list_tasks() if site_id in [s["id"] if isinstance(s, dict) else s for s in t.get("sites", [])]] # Which macros are already covered on this site? covered = set() chain_counts = {} # chain_len -> count for t in site_tasks: covered.update(t.get("macros", [])) cl = len(t.get("macros", [])) chain_counts[cl] = chain_counts.get(cl, 0) + 1 uncovered = [m for m in all_macros if m not in covered] # 1. Pick chain length: ratio for lengths 1..6 (favors shorter, includes 5/6) target_ratio = {1: 3, 2: 2, 3: 3, 4: 2, 5: 1, 6: 1} chain_options = [] chain_weights = [] for length, target_w in target_ratio.items(): current = chain_counts.get(length, 0) w = target_w / (current + 1) chain_options.append(length) chain_weights.append(w) total = sum(chain_weights) chain_weights = [w / total for w in chain_weights] n_macros = rng.choices(chain_options, weights=chain_weights, k=1)[0] # 2. Pick macros — strongly prefer uncovered, equal weight otherwise _QA_VERBS = {"extract", "compute", "count", "compare", "verify", "calculate", "rank", "list"} sampled = [] remaining = list(all_macros) has_qa = False for _ in range(min(n_macros, len(remaining))): if has_qa: remaining = [m for m in remaining if m.split("_")[0] not in _QA_VERBS] if not remaining: break ws = [] for m in remaining: if m in uncovered: ws.append(10.0) # uncovered — strong priority else: site_count = sum(1 for t in site_tasks if m in t.get("macros", [])) ws.append(1.0 / (site_count + 1)) total = sum(ws) ws = [w / total for w in ws] pick = rng.choices(remaining, weights=ws, k=1)[0] sampled.append(pick) if pick.split("_")[0] in _QA_VERBS: has_qa = True remaining = [m for m in remaining if m != pick] # Compute how many tasks still needed per chain length (lengths 1..6) total_macros = len(all_macros) import math batches = max(1, math.ceil(total_macros / 24)) targets = {1: 3 * batches, 2: 2 * batches, 3: 3 * batches, 4: 2 * batches, 5: 1 * batches, 6: 1 * batches} chain_remaining = {k: max(0, targets[k] - chain_counts.get(k, 0)) for k in targets} return { "sites": [{"id": site_id, "name": site.get("name", site_id), "category": site.get("category", "Other"), "type": site.get("type", "")}], "macros": sampled, "num_sites": 1, "num_macros": len(sampled), "edges": [], "chosen_site": True, "total_macros": total_macros, "uncovered_macros": len(uncovered), "chain_counts": chain_counts, "chain_remaining": chain_remaining, } # --------------------------------------------------------------------------- # Auth # --------------------------------------------------------------------------- _AUTH_EXEMPT = {"/annotate/login", "/annotate/api/auto_login", "/annotate/api/auto_logout"} # The public demo (MINIWEB_DEMO=1, app/demo.py): no login, the released tasks prefilled, nothing saved. These POSTs # only touch the visitor's own session (site logins, 2FA) or read (macro search, verifier runs); every other write # is refused. The pages and APIs of the audits, the site reviews, the accounts and the macro graph are hidden, and # so are the reviewers' tools (the Verifier Builder, Task Review, and the playback facts, which give answers away). _DEMO_WRITES = {"/annotate/api/auto_login", "/annotate/api/auto_logout", "/annotate/api/toggle_2fa", "/annotate/api/macro_search", "/annotate/api/run_task_verifier", "/annotate/api/run_verifiers"} _DEMO_HIDDEN = ("/annotate/accounts", "/annotate/grader-audit", "/annotate/task-audit", "/annotate/review", "/annotate/graph", "/annotate/api/grader_audit/", "/annotate/api/task_audit/", "/annotate/api/review", "/annotate/api/graph/", "/annotate/verify", "/annotate/task-review", "/annotate/api/task_review/", "/annotate/playback", "/annotate/api/playback_facts") TRAJECTORIES_URL = "https://minhphd.github.io/miniweb/trajectories.html" # every agent episode of the paper _DEMO_BANNER = ( '