Buckets:
| #!/usr/bin/env python3 | |
| # /// script | |
| # requires-python = ">=3.11" | |
| # dependencies = [ | |
| # "smolagents[openai]", | |
| # "openai", | |
| # "datasets", | |
| # "huggingface_hub", | |
| # "iconclass", | |
| # "sentence-transformers", | |
| # "numpy", | |
| # "pillow", | |
| # ] | |
| # /// | |
| """Interactive Iconclass tool-agent (NEXT_SESSION §2) — the real multi-step agent. | |
| GATE_PRECISION.md showed the anchored-fusion gate is stuck at H-F1 48.5 because a | |
| *single-shot* discriminator can't separate the right leaf from its siblings. The | |
| agent DE-RISK was GREEN: holistic reasoning recovered 27/51 (53%) of the GT codes | |
| the gate missed. This script builds the REAL agent that adds the two untested wins: | |
| (a) LIVE FT-retriever search — the agent issues its own search_iconclass queries | |
| (the de-risk only reasoned over the cached K=24 pool; ~48% of GT isn't in it). | |
| (b) a VERIFICATION step — the agent must split its proposals into ACCEPTED | |
| and REJECTED (with a reason each), which both fixes the precision tax AND | |
| yields model-grade hard negatives (the rejected proposals). | |
| final_codes = anchor (the precise FT-4B) ∪ agent-accepted extras (anchored fusion). | |
| Trace capture is first-class: one JSONL row per image (incl the rejected proposals) | |
| → hf://buckets/davanstrien/iconclass-agent-traces, for eventual SFT distillation. | |
| Eval on the clean 788 ruler over the SAME ~40-image de-risk split (25 HARD where the | |
| gate missed a recoverable in-pool GT + 15 RANDOM control); reports anchor / gate / | |
| agent H-F1 and "GT the gate missed that the agent recovered", like agent_derisk.py. | |
| USAGE | |
| # probe one HARD image verbosely (pick agent type: toolcalling vs code) | |
| uv run agent_iconclass.py --endpoint-url https://XXXX.endpoints.huggingface.cloud --probe | |
| # full 40-image validation | |
| uv run agent_iconclass.py --endpoint-url https://XXXX.endpoints.huggingface.cloud \ | |
| --n-hard 25 --n-random 15 --out agent_traces.jsonl --results agent_iconclass_results.json | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import sys | |
| import threading | |
| import time | |
| import uuid | |
| from concurrent.futures import ThreadPoolExecutor, as_completed | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| import agent_tools # noqa: E402 (our improved tools) | |
| from describe_retrieve import ( # noqa: E402 | |
| DATASET, | |
| SPLIT, | |
| VLM_EXTRA_BODY, | |
| _calculate_hierarchical_f1, | |
| _code_description, | |
| _extract_json, | |
| first_image, | |
| ) | |
| DEFAULT_MODEL_ID = "Qwen/Qwen3.6-35B-A3B-FP8" | |
| # Generic-pose pollution down-weight (verbatim from agent_derisk.py / fuse_rank.py) | |
| POSE_PREFIXES = ( | |
| "31A231", | |
| "33A81", | |
| "31A2313", | |
| "31A235", | |
| "31A2351", | |
| "31A2623", | |
| "31A2711", | |
| "31A27113", | |
| "31B621", | |
| "31A233", | |
| "43C214", | |
| ) | |
| POSE_PENALTY = 0.3 | |
| def base(c): | |
| return c.split("(")[0].strip() if isinstance(c, str) else "" | |
| def is_pose(c): | |
| b = base(c) | |
| return any(b == p or b.startswith(p) for p in POSE_PREFIXES) | |
| def adj(c, s): | |
| return s * POSE_PENALTY if is_pose(c) else s | |
| # ===================================================================== data | |
| def load_inputs(pool_path, ft_path, grade_path): | |
| """Mirror agent_derisk.py: FT pool + 4B anchor + cached 35B judge grades.""" | |
| d = json.load(open(pool_path)) | |
| ft = json.load(open(ft_path)) | |
| ftc = { | |
| int(r["index"]): list(r.get("predicted_codes", []) or []) for r in ft["results"] | |
| } | |
| gc = json.load(open(grade_path)) | |
| recs = {int(r["index"]): r for r in d["records"] if not r.get("error")} | |
| return recs, ftc, gc | |
| def missed(recs, idx, ftc): | |
| """Retrieved candidates NOT already in the anchor, with pose-adjusted sim.""" | |
| r = recs[idx] | |
| ab = set(ftc.get(idx, [])) | |
| return [ | |
| (c, adj(c, r["retrieved_scores"].get(c, 0.5))) | |
| for c in r["retrieved_codes"] | |
| if c not in ab | |
| ] | |
| def gate_pred(recs, idx, ftc, gc, n=3): | |
| """The deployed gate: anchor + top-N missed codes with judge grade >= 4.""" | |
| ms = [(c, s) for c, s in missed(recs, idx, ftc) if gc.get(f"{idx}|{c}", 0) >= 4] | |
| ms.sort(key=lambda x: (gc.get(f"{idx}|{x[0]}", 0) / 5) * x[1], reverse=True) | |
| return ftc.get(idx, []) + [c for c, _ in ms[:n]] | |
| def recoverable_missed_gt(recs, idx, ftc): | |
| """GT codes present (by base) in the missed-candidate pool — the reachable ones.""" | |
| gb = {base(g) for g in recs[idx]["gt_codes"]} | |
| return [c for c, _ in missed(recs, idx, ftc) if base(c) in gb] | |
| def stride(lst, k): | |
| if len(lst) <= k: | |
| return lst | |
| step = len(lst) / k | |
| return [lst[int(i * step)] for i in range(k)] | |
| def select_split(recs, ftc, gc, n_hard, n_random): | |
| """HARD = gate missed a recoverable GT; RANDOM = control. Deterministic.""" | |
| hard, rand_pool = [], [] | |
| for idx in sorted(recs): | |
| gpred = set(gate_pred(recs, idx, ftc, gc)) | |
| rec_gt = recoverable_missed_gt(recs, idx, ftc) | |
| if rec_gt and any(c not in gpred for c in rec_gt): | |
| hard.append(idx) | |
| rand_pool.append(idx) | |
| hard_sel = stride(hard, n_hard) | |
| rand_sel = [i for i in stride(rand_pool, n_random * 2) if i not in set(hard_sel)][ | |
| :n_random | |
| ] | |
| return hard, sorted(set(hard_sel)), sorted(set(rand_sel) - set(hard_sel)) | |
| # ===================================================================== image | |
| def prep_image(example, max_size=768): | |
| from PIL import Image | |
| img = first_image(example) | |
| if img is None: | |
| return None | |
| if max(img.size) > max_size: | |
| r = max_size / max(img.size) | |
| img = img.resize((int(img.size[0] * r), int(img.size[1] * r)), Image.LANCZOS) | |
| return img.convert("RGB") if img.mode != "RGB" else img | |
| # ===================================================================== prompt | |
| TASK_TEMPLATE = """You are an expert Iconclass cataloguer examining ONE artwork (the image shown to you). | |
| A specialist model has ALREADY assigned these codes — treat them as correct and depicted (do NOT repeat them): | |
| {anchor} | |
| Some additional candidate codes were retrieved by similarity. You may accept, reject, or ignore them, and you should ALSO search for more codes yourself: | |
| {pool} | |
| YOUR JOB: find the ADDITIONAL Iconclass codes that are GENUINELY depicted in THIS artwork, and REJECT plausible-but-absent ones. | |
| METHOD: | |
| 1. Look at the image. Identify the actual scene/story, the figures, objects, attributes, gestures. | |
| 2. Use search_iconclass(query) to find codes for what you see — especially SPECIFIC leaves (the exact saint, the exact pose, the exact object), not just general categories. | |
| 3. Use lookup_iconclass_code(code) to read a candidate's definition and its SIBLINGS, and compare against the image to pick the RIGHT leaf (e.g. head-left vs head-right). | |
| 4. Be STRICT. Only commit codes truly depicted in THIS image. Reject the wrong saint / wrong story, generic poses that are not central, and anything you cannot verify against the image. | |
| Keep tool use focused (a handful of searches/lookups). When done, call final_answer with a JSON object EXACTLY of this shape: | |
| {{"accepted": ["code", ...], "rejected": [{{"code": "code", "reason": "one short reason it is NOT depicted"}}, ...]}} | |
| - accepted: the ADDITIONAL depicted codes (exclude the already-assigned ones). [] if none. | |
| - rejected: candidates you considered but rejected, each with a one-line reason.""" | |
| def build_task(anchor_codes, pool): | |
| anchor_txt = ( | |
| "\n".join(f" - {c} = {_code_description(c) or c}" for c in anchor_codes) | |
| or " (none)" | |
| ) | |
| if pool: | |
| pool_txt = "\n".join( | |
| f" {i + 1}. {p['code']} = {p['definition']} (sim {p['sim']}, judge {p['judge_grade']})" | |
| for i, p in enumerate(pool) | |
| ) | |
| else: | |
| pool_txt = " (none retrieved — search for codes yourself)" | |
| return TASK_TEMPLATE.format(anchor=anchor_txt, pool=pool_txt) | |
| # ===================================================================== agent | |
| def make_model(args): | |
| from smolagents import OpenAIModel | |
| return OpenAIModel( | |
| model_id=args.model_id, | |
| api_base=args.endpoint_url.rstrip("/") + "/v1", | |
| api_key=args.api_key, | |
| temperature=0.0, | |
| max_tokens=args.max_tokens, | |
| extra_body=VLM_EXTRA_BODY, | |
| ) | |
| def make_agent(model, agent_type, max_steps, verbosity=0): | |
| from smolagents import CodeAgent, ToolCallingAgent, tool | |
| tools = [ | |
| tool(agent_tools.search_iconclass), | |
| tool(agent_tools.lookup_iconclass_code), | |
| tool(agent_tools.get_parent_code), | |
| tool(agent_tools.define_codes), | |
| tool(agent_tools.validate_iconclass_code), | |
| ] | |
| cls = CodeAgent if agent_type == "code" else ToolCallingAgent | |
| kwargs = dict( | |
| tools=tools, model=model, max_steps=max_steps, verbosity_level=verbosity | |
| ) | |
| if agent_type == "code": | |
| kwargs["additional_authorized_imports"] = ["json"] | |
| return cls(**kwargs) | |
| def _coerce_obj(obj): | |
| """Coerce a final-answer payload to a dict/list. Handles JSON strings AND | |
| Python-repr strings (single quotes) the model sometimes emits.""" | |
| if isinstance(obj, (dict, list)): | |
| return obj | |
| if not isinstance(obj, str): | |
| return {} | |
| j = _extract_json(obj) | |
| if isinstance(j, (dict, list)): | |
| return j | |
| import ast | |
| import re as _re | |
| m = _re.search(r"[{\[].*[}\]]", obj, _re.S) | |
| if m: | |
| for parser in (json.loads, ast.literal_eval): | |
| try: | |
| return parser(m.group(0)) | |
| except Exception: | |
| continue | |
| return {} | |
| def parse_final(result): | |
| """Robustly pull (accepted, rejected[{code,reason}]) from the final answer.""" | |
| obj = _coerce_obj(result) | |
| # the model occasionally nests the dict as a string inside {"answer": "..."} | |
| if isinstance(obj, dict) and "accepted" not in obj and "rejected" not in obj: | |
| for v in obj.values(): | |
| cv = _coerce_obj(v) | |
| if isinstance(cv, dict) and ("accepted" in cv or "rejected" in cv): | |
| obj = cv | |
| break | |
| accepted, rejected = [], [] | |
| if isinstance(obj, dict): | |
| accepted = [ | |
| c for c in (obj.get("accepted") or []) if isinstance(c, str) and c.strip() | |
| ] | |
| for r in obj.get("rejected") or []: | |
| if isinstance(r, dict) and r.get("code"): | |
| rejected.append( | |
| {"code": r["code"], "reason": str(r.get("reason", ""))[:200]} | |
| ) | |
| elif isinstance(r, str) and r.strip(): | |
| rejected.append({"code": r, "reason": ""}) | |
| elif isinstance(obj, list): | |
| accepted = [c for c in obj if isinstance(c, str) and c.strip()] | |
| return accepted, rejected | |
| def extract_tool_calls(agent): | |
| """Compact trajectory from agent memory — tool calls + observations, NO images.""" | |
| out = [] | |
| try: | |
| from smolagents import ActionStep | |
| except Exception: | |
| ActionStep = None | |
| for step in getattr(agent.memory, "steps", []): | |
| if ActionStep is not None and not isinstance(step, ActionStep): | |
| continue | |
| tcs = getattr(step, "tool_calls", None) or [] | |
| calls = [] | |
| for tc in tcs: | |
| calls.append( | |
| { | |
| "tool": getattr(tc, "name", None), | |
| "args": getattr(tc, "arguments", None), | |
| } | |
| ) | |
| obs = getattr(step, "observations", None) | |
| mo = getattr(step, "model_output", None) | |
| out.append( | |
| { | |
| "step": getattr(step, "step_number", None), | |
| "model_output": (mo[:600] if isinstance(mo, str) else None), | |
| "tool_calls": calls, | |
| "observations": (obs[:800] if isinstance(obs, str) else None), | |
| "error": str(getattr(step, "error", None)) | |
| if getattr(step, "error", None) | |
| else None, | |
| } | |
| ) | |
| return out | |
| def run_one(args, model, idx, example, anchor, pool, gt): | |
| """Run the agent on one image; return the full trace row.""" | |
| img = prep_image(example) | |
| row = { | |
| "index": idx, | |
| "run_id": args.run_id, | |
| "ts": args.ts, | |
| "model": args.model_id, | |
| "gt_codes": gt, | |
| "anchor_codes": anchor, | |
| "candidate_pool": pool, | |
| "tool_calls": [], | |
| "proposed": [], | |
| "accepted": [], | |
| "rejected": [], | |
| "final_codes": list(anchor), | |
| "raw_response": "", | |
| "eval": {}, | |
| } | |
| if img is None: | |
| row["error"] = "no image" | |
| return row | |
| agent = make_agent(model, args.agent_type, args.max_steps, args.verbosity) | |
| task = build_task(anchor, pool) | |
| try: | |
| result = agent.run(task, images=[img]) | |
| except Exception as e: | |
| row["error"] = f"agent: {str(e)[:200]}" | |
| row["tool_calls"] = extract_tool_calls(agent) | |
| return row | |
| accepted, rejected = parse_final(result) | |
| # keep only valid, non-anchor codes for the committed extras | |
| anchor_set = set(anchor) | |
| valid_accepted = [ | |
| c | |
| for c in accepted | |
| if c not in anchor_set and agent_tools.validate_iconclass_code(c) | |
| ] | |
| proposed = list(dict.fromkeys(accepted + [r["code"] for r in rejected])) | |
| # Defensive cap: a degenerate enumeration (runaway code-family walk — e.g. | |
| # 100+ codes from one fake subtree, seen when the server's generation_config | |
| # overrode temp=0 -> 1.0) is not real labeling. 15 is well above any sane | |
| # iconclass count (anchor<=6 + a few extras); clear + flag for diagnosis. | |
| DEGEN_CAP = 15 | |
| degenerate = len(valid_accepted) > DEGEN_CAP | |
| if degenerate: | |
| print( | |
| f" [idx={idx}] DEGENERATE: {len(valid_accepted)} accepted (> {DEGEN_CAP}) " | |
| f"— clearing (likely temp-override code-family enumeration).", | |
| flush=True, | |
| ) | |
| valid_accepted = [] | |
| final_codes = list(anchor) + [c for c in valid_accepted if c not in anchor_set] | |
| row["tool_calls"] = extract_tool_calls(agent) | |
| row["proposed"] = proposed | |
| row["accepted"] = valid_accepted | |
| row["rejected"] = rejected | |
| row["final_codes"] = final_codes | |
| row["degenerate"] = degenerate | |
| row["raw_response"] = (str(result) if not isinstance(result, str) else result)[ | |
| :4000 | |
| ] | |
| return row | |
| # ===================================================================== eval | |
| def evaluate_row(row, recs, ftc, gc): | |
| idx = row["index"] | |
| gt = row["gt_codes"] | |
| anchor = row["anchor_codes"] | |
| agent_pred = row["final_codes"] | |
| gpred = gate_pred(recs, idx, ftc, gc) | |
| rec_gt = recoverable_missed_gt(recs, idx, ftc) | |
| gate_set = set(gpred) | |
| gt_missed_by_gate = [c for c in rec_gt if c not in gate_set] | |
| agent_set = set(agent_pred) | |
| gt_recovered_by_agent = [c for c in gt_missed_by_gate if c in agent_set] | |
| row["eval"] = { | |
| "anchor_hf1": _calculate_hierarchical_f1(anchor, gt), | |
| "gate_hf1": _calculate_hierarchical_f1(gpred, gt), | |
| "agent_hf1": _calculate_hierarchical_f1(agent_pred, gt), | |
| "oracle_hf1": _calculate_hierarchical_f1(anchor + rec_gt, gt), | |
| "n_gt_missed_by_gate": len(gt_missed_by_gate), | |
| "n_gt_recovered_by_agent": len(gt_recovered_by_agent), | |
| "recovered_missed_gt": gt_recovered_by_agent, | |
| "n_agent_extras": len(row["accepted"]), | |
| "n_rejected": len(row["rejected"]), | |
| } | |
| return row | |
| def summarize(rows, hard_set): | |
| def avg(rs, k): | |
| return sum(r["eval"][k] for r in rs) / len(rs) if rs else 0.0 | |
| for label, subset in ( | |
| ("HARD", [r for r in rows if r["index"] in hard_set]), | |
| ("RANDOM", [r for r in rows if r["index"] not in hard_set]), | |
| ("ALL", rows), | |
| ): | |
| subset = [r for r in subset if r.get("eval")] | |
| if not subset: | |
| continue | |
| print(f"\n=== {label} (n={len(subset)}) ===") | |
| print(f" anchor-only H-F1 : {avg(subset, 'anchor_hf1'):.1%}") | |
| print( | |
| f" GATE H-F1 : {avg(subset, 'gate_hf1'):.1%} (deployed; 48.5 on full 788)" | |
| ) | |
| print( | |
| f" AGENT H-F1 : {avg(subset, 'agent_hf1'):.1%} <-- agent vs gate" | |
| ) | |
| print( | |
| f" oracle H-F1 : {avg(subset, 'oracle_hf1'):.1%} (anchor + all recoverable GT)" | |
| ) | |
| tot_missed = sum(r["eval"]["n_gt_missed_by_gate"] for r in subset) | |
| tot_rec = sum(r["eval"]["n_gt_recovered_by_agent"] for r in subset) | |
| print( | |
| f" GT the gate MISSED that the AGENT RECOVERED: {tot_rec}/{tot_missed} (de-risk: 27/51)" | |
| ) | |
| print( | |
| f" avg extras committed: {avg(subset, 'n_agent_extras'):.1f} avg rejected: {avg(subset, 'n_rejected'):.1f}" | |
| ) | |
| # ===================================================================== lazy stream | |
| class _LazyImages: | |
| """Stream an image dataset once, exposing examples by DENSE index. | |
| Matches infer_anchor.py's dense indexing (only image-bearing rows consume a | |
| dense index). Decodes images on demand under a lock so the agent starts on | |
| image 0 immediately and memory stays bounded by the in-flight window (~the | |
| executor's concurrency), NOT N — fixing the upfront-materialization cliff | |
| that wedged large ``--limit`` runs (decoding thousands of full-res PILs | |
| before any agent work). Relies on the ThreadPoolExecutor dispatching tasks | |
| in submission (index) order so the lookahead stays ~concurrency. | |
| """ | |
| def __init__(self, dataset_name, split_name): | |
| from datasets import load_dataset | |
| self._it = iter(load_dataset(dataset_name, split=split_name, streaming=True)) | |
| self._lock = threading.Lock() | |
| self._buf = {} # dense_idx -> example (pending consumers) | |
| self._next_dense = 0 | |
| self._exhausted = False | |
| def get(self, dense_idx): | |
| """Return the example for dense_idx (popping it), or None if past end.""" | |
| with self._lock: | |
| if dense_idx in self._buf: | |
| return self._buf.pop(dense_idx) | |
| while not self._exhausted: | |
| try: | |
| ex = next(self._it) | |
| except StopIteration: | |
| self._exhausted = True | |
| break | |
| if first_image(ex) is None: | |
| continue # no image: doesn't consume a dense index (matches infer_anchor) | |
| if self._next_dense == dense_idx: | |
| self._next_dense += 1 | |
| return ex # hot path: requested next-in-line, don't buffer | |
| self._buf[self._next_dense] = ex | |
| self._next_dense += 1 | |
| return self._buf.pop(dense_idx, None) | |
| # ===================================================================== main | |
| def main(): | |
| sys.stdout.reconfigure(line_buffering=True) | |
| ap = argparse.ArgumentParser(description=__doc__) | |
| ap.add_argument( | |
| "--endpoint-url", required=True, help="base URL of the deployed endpoint" | |
| ) | |
| ap.add_argument("--model-id", default=DEFAULT_MODEL_ID) | |
| ap.add_argument("--api-key", default=None, help="defaults to the cached HF token") | |
| ap.add_argument( | |
| "--agent-type", default="toolcalling", choices=["toolcalling", "code"] | |
| ) | |
| ap.add_argument("--pool", default="dr_test788_bgebase.json") | |
| ap.add_argument("--ft", default="sftbrill_test788.json") | |
| ap.add_argument("--grade-cache", default="grade_cache.json") | |
| ap.add_argument("--n-hard", type=int, default=25) | |
| ap.add_argument("--n-random", type=int, default=15) | |
| ap.add_argument( | |
| "--pool-hint", | |
| type=int, | |
| default=12, | |
| help="# missed candidates shown to the agent", | |
| ) | |
| ap.add_argument("--max-steps", type=int, default=10) | |
| ap.add_argument("--max-tokens", type=int, default=3000) | |
| ap.add_argument("--concurrency", type=int, default=4) | |
| ap.add_argument("--verbosity", type=int, default=0) | |
| ap.add_argument( | |
| "--probe", action="store_true", help="run ONE hard image verbosely, then exit" | |
| ) | |
| ap.add_argument("--out", default="agent_traces.jsonl") | |
| ap.add_argument("--results", default="agent_iconclass_results.json") | |
| ap.add_argument( | |
| "--resume", | |
| action="store_true", | |
| help="If --out exists, skip indices already present and append (don't wipe). " | |
| "Crash insurance for long unlabeled runs; ignored if the file is absent.", | |
| ) | |
| # ---- Exp B: unlabeled agent-data-build mode (no GT, no cached pool) ---- | |
| ap.add_argument( | |
| "--unlabeled", | |
| action="store_true", | |
| help="Unlabeled mode: 9B anchor from --ft, LIVE search_iconclass (no cached " | |
| "pool), no GT eval. For Exp B ORPO pair generation on e.g. european_art.", | |
| ) | |
| ap.add_argument( | |
| "--dataset", | |
| default=None, | |
| help="Override the image dataset (unlabeled mode). Default: biglam/european_art.", | |
| ) | |
| ap.add_argument( | |
| "--split", default=None, help="Override the split (unlabeled mode)." | |
| ) | |
| ap.add_argument( | |
| "--limit", | |
| type=int, | |
| default=20, | |
| help="Max images to run in unlabeled mode (sel = first --limit anchor rows).", | |
| ) | |
| args = ap.parse_args() | |
| if not args.api_key: | |
| from huggingface_hub import get_token | |
| args.api_key = get_token() | |
| args.run_id = uuid.uuid4().hex[:12] | |
| args.ts = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) | |
| if args.probe: | |
| args.verbosity = max(args.verbosity, 2) | |
| # ---- mode branch: test-wired (GT + cached pool + 4B preds) vs unlabeled ---- | |
| if args.unlabeled: | |
| # Exp B data-build: 9B anchor from --ft, LIVE search (no cached pool), no GT. | |
| ftc = { | |
| int(r["index"]): list(r.get("predicted_codes", []) or []) | |
| for r in json.load(open(args.ft))["results"] | |
| } | |
| gc = {} | |
| recs = None # unlabeled: no GT records | |
| sel = sorted(ftc.keys())[: args.limit] | |
| hard_set = set() | |
| dataset_name = args.dataset or "biglam/european_art" | |
| split_name = args.split or "train" | |
| print( | |
| f"UNLABELED mode: anchor={args.ft} ({len(ftc)} rows) | " | |
| f"{dataset_name}[{split_name}] | running {len(sel)} | " | |
| f"run_id={args.run_id} agent_type={args.agent_type} endpoint={args.endpoint_url}" | |
| ) | |
| agent_tools._get_index() | |
| # Lazy streaming: decode images on demand by dense index (matching | |
| # infer_anchor.py — only image-bearing rows consume a dense index), so the | |
| # agent starts on image 0 immediately and memory is bounded by the in-flight | |
| # window, not N. Fixes the upfront-materialization cliff at large --limit. | |
| ds = _LazyImages(dataset_name, split_name) | |
| print(f"lazy-streaming {dataset_name}[{split_name}] (decode-on-demand)") | |
| def pool_for(idx): | |
| return [] # live search_iconclass only (no cached retrieval pool) | |
| else: | |
| recs, ftc, gc = load_inputs(args.pool, args.ft, args.grade_cache) | |
| hard_all, hard_sel, rand_sel = select_split( | |
| recs, ftc, gc, args.n_hard, args.n_random | |
| ) | |
| if args.probe: | |
| sel = hard_sel[:1] | |
| else: | |
| sel = sorted(set(hard_sel) | set(rand_sel)) | |
| hard_set = set(hard_sel) | |
| print( | |
| f"hard available={len(hard_all)} selected: hard={len(hard_sel)} random={len(rand_sel)} total={len(sel)}" | |
| ) | |
| print( | |
| f"run_id={args.run_id} agent_type={args.agent_type} endpoint={args.endpoint_url}" | |
| ) | |
| # pre-warm the FT index once (avoid a thread race on first search) | |
| agent_tools._get_index() | |
| # attach images + per-image pool hint | |
| from datasets import load_dataset | |
| print(f"loading {DATASET}[{SPLIT}] …") | |
| ds = load_dataset(DATASET, split=SPLIT) | |
| def pool_for(idx): | |
| ms = sorted(missed(recs, idx, ftc), key=lambda x: x[1], reverse=True)[ | |
| : args.pool_hint | |
| ] | |
| return [ | |
| { | |
| "code": c, | |
| "definition": _code_description(c) or c, | |
| "sim": round(s, 3), | |
| "judge_grade": gc.get(f"{idx}|{c}", 0), | |
| } | |
| for c, s in ms | |
| ] | |
| model = make_model(args) | |
| lock = threading.Lock() | |
| rows = [] | |
| done = set() | |
| if args.resume and os.path.exists(args.out) and os.path.getsize(args.out) > 0: | |
| with open(args.out) as _f: | |
| for _l in _f: | |
| try: | |
| done.add(int(json.loads(_l)["index"])) | |
| except Exception: | |
| continue | |
| before = len(sel) | |
| sel = [i for i in sel if i not in done] | |
| print(f"RESUME: {len(done)} indices already in {args.out}; {before} -> {len(sel)} to run") | |
| else: | |
| open(args.out, "w").close() # fresh local JSONL for this run | |
| def task_fn(idx): | |
| ex = ds.get(idx) if isinstance(ds, _LazyImages) else ds[idx] | |
| if ex is None: | |
| row = { | |
| "index": idx, | |
| "run_id": args.run_id, | |
| "ts": args.ts, | |
| "model": args.model_id, | |
| "error": "image not in streamed subset", | |
| "anchor_codes": ftc.get(idx, []), | |
| "accepted": [], | |
| "rejected": [], | |
| "final_codes": list(ftc.get(idx, [])), | |
| } | |
| with lock: | |
| with open(args.out, "a") as f: | |
| f.write(json.dumps(row, default=str) + "\n") | |
| return row | |
| anchor = ftc.get(idx, []) | |
| gt = recs[idx]["gt_codes"] if recs is not None else [] | |
| pool = pool_for(idx) | |
| row = run_one(args, model, idx, ex, anchor, pool, gt) | |
| if recs is not None: | |
| evaluate_row(row, recs, ftc, gc) | |
| with lock: | |
| with open(args.out, "a") as f: | |
| f.write(json.dumps(row, default=str) + "\n") | |
| return row | |
| print(f"running agent on {len(sel)} images (concurrency={args.concurrency})…") | |
| t0 = time.time() | |
| if args.concurrency <= 1 or args.probe: | |
| for i, idx in enumerate(sel): | |
| rows.append(task_fn(idx)) | |
| print(f" [{i + 1}/{len(sel)}] idx={idx} done") | |
| else: | |
| with ThreadPoolExecutor(max_workers=args.concurrency) as ex_pool: | |
| futs = {ex_pool.submit(task_fn, idx): idx for idx in sel} | |
| for i, fut in enumerate(as_completed(futs)): | |
| rows.append(fut.result()) | |
| print(f" [{i + 1}/{len(sel)}] idx={futs[fut]} done") | |
| print(f"agent runs done in {time.time() - t0:.0f}s") | |
| n_err = sum(1 for r in rows if r.get("error")) | |
| if n_err: | |
| print(f" WARNING: {n_err}/{len(rows)} rows had errors") | |
| if args.probe: | |
| r = rows[0] | |
| print("\n--- PROBE TRACE ---") | |
| print( | |
| json.dumps( | |
| {k: r[k] for k in ("accepted", "rejected", "final_codes")}, indent=2 | |
| ) | |
| ) | |
| print("tool_calls:") | |
| for tc in r["tool_calls"]: | |
| print(f" step {tc['step']}: {[c['tool'] for c in tc['tool_calls']]}") | |
| print(f"eval: {r.get('eval')}") | |
| print(f"\nLocal trace → {args.out}") | |
| return | |
| if args.unlabeled: | |
| # Exp B pair-format summary (no GT metrics). | |
| n_ok = [r for r in rows if not r.get("error")] | |
| tot_acc = sum(len(r.get("accepted", [])) for r in n_ok) | |
| tot_rej = sum(len(r.get("rejected", [])) for r in n_ok) | |
| tot_anc = sum(len(r.get("anchor_codes", [])) for r in n_ok) | |
| print("\n=== UNLABELED run summary ===") | |
| print(f" rows: {len(n_ok)}/{len(rows)} ok ({n_err} errors)") | |
| print( | |
| f" avg anchor codes/img : {tot_anc / max(len(n_ok), 1):.1f} " | |
| f"avg accepted/img: {tot_acc / max(len(n_ok), 1):.1f} " | |
| f"avg rejected/img: {tot_rej / max(len(n_ok), 1):.1f}" | |
| ) | |
| print( | |
| " ORPO pairs (per image): chosen = anchor ∪ accepted, rejected = anchor" | |
| ) | |
| else: | |
| summarize(rows, hard_set) | |
| json.dump( | |
| { | |
| "run_id": args.run_id, | |
| "ts": args.ts, | |
| "model": args.model_id, | |
| "agent_type": args.agent_type, | |
| "unlabeled": bool(args.unlabeled), | |
| "n": len(rows), | |
| "n_err": n_err, | |
| "rows": rows, | |
| }, | |
| open(args.results, "w"), | |
| indent=2, | |
| default=str, | |
| ) | |
| print(f"\nSaved → {args.results} traces → {args.out}") | |
| if __name__ == "__main__": | |
| main() | |
Xet Storage Details
- Size:
- 28.6 kB
- Xet hash:
- f02aec40f8db57d22f0f6aaa3fd60695242c163a0cf2bf174c7cf1848803c0d5
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.