#!/usr/bin/env python # -*- coding: utf-8 -*- """ Inference entry point for Track A. This script owns the executable prediction flow: 1. Load test scenarios. 2. Hydrate placeholder telemetry from the competition server when needed. 3. Load the trained model bundle produced by train.py. 4. Ask the Qwen/OpenRouter assistant to call the trained-model tool. 5. Checkpoint and finally write results/result.csv, debug JSON, and traces JSON. Reusable feature extraction, prediction helpers, server utilities, and file writers live in src/model_core.py. Training and cross-validation live in train.py. python main.py \ --test_path "data/Phase_2/test.json" \ --model_bundle "results/model_v4_bundle.pkl" \ --out "results/result.csv" \ --debug_out "results/debug_phase2_v4.json" \ --traces_out "results/traces.json" \ --checkpoint_every 1 \ --max_steps 4 \ --max_tool_calls 8 \ --concurrency 1 \ --question_timeout 180 """ from __future__ import annotations import argparse import os import re import time from concurrent.futures import ThreadPoolExecutor, as_completed from typing import List, Optional import httpx from openai import OpenAI from tqdm import tqdm from src.model_core import ( build_question_text, get_env_value, get_options, hydrate_scenario_from_server, load_env_file, load_json, load_model_bundle, result_csv_path, run_agent_with_trained_model, scenario_needs_server_data, write_debug, write_json, write_submission, ) DEFAULT_TRACK_B_TEST = os.path.join("..", "Track B", "data", "Phase_2", "test.json") def normalize_track_a_answer(answer: object) -> str: text = str(answer or "").strip() if not text: return "" labels = re.findall(r"C\d+", text) if not labels: return text labels = sorted(dict.fromkeys(labels), key=lambda label: int(label[1:])) return "|".join(labels) def normalize_submission_rows(rows: list[dict[str, str]]) -> list[dict[str, str]]: out = [] for row in rows: out.append( { "ID": row.get("ID", ""), "Track A": normalize_track_a_answer(row.get("Track A", "")), "Track B": str(row.get("Track B", "") or ""), } ) return out def write_result(path: str, rows: list[dict[str, str]]) -> None: write_submission(path, normalize_submission_rows(rows)) def append_track_b_blank_rows( rows: list[dict[str, str]], track_b_test_path: str ) -> int: if not track_b_test_path or not os.path.exists(track_b_test_path): return 0 track_b = load_json(track_b_test_path) existing = {str(row.get("ID", "")) for row in rows} added = 0 for i, s in enumerate(track_b, start=1): sid = str(s.get("scenario_id") or s.get("ID") or "").strip() if not sid: task_id = s.get("task", {}).get("id", i) raise ValueError(f"Missing scenario_id for Track B task id {task_id}") if sid in existing: raise ValueError(f"Track B ID already exists in output rows: {sid}") rows.append({"ID": sid, "Track A": "", "Track B": ""}) existing.add(sid) added += 1 return added def main() -> None: load_env_file() parser = argparse.ArgumentParser() parser.add_argument("--test_path", default="data/Phase_1/test.json") parser.add_argument("--model_bundle", default="results/model_v4_bundle.pkl") parser.add_argument("--track_b_test", default=DEFAULT_TRACK_B_TEST) parser.add_argument("--out", default="results/result.csv") parser.add_argument("--debug_out", default="results/debug_v4.json") parser.add_argument("--traces_out", default="results/traces.json") parser.add_argument("--max_samples", type=int, default=None) parser.add_argument("--use_server_data", action="store_true") parser.add_argument("--no_auto_server_data", action="store_true") parser.add_argument("--server_url", default="https://124.71.227.61/no") parser.add_argument("--env_path", default=None) parser.add_argument("--auth_token_env", default="AUTH_TOKEN") parser.add_argument("--timeout", type=float, default=30.0) parser.add_argument("--verify_ssl", action="store_true") parser.add_argument("--try_scenario_endpoint", action="store_true") parser.add_argument("--checkpoint_every", type=int, default=1) parser.add_argument("--no_progress", action="store_true") parser.add_argument("--no_agent", action="store_true") parser.add_argument("--max_steps", type=int, default=4) parser.add_argument("--max_tool_calls", type=int, default=8) parser.add_argument("--concurrency", type=int, default=1) parser.add_argument( "--model_url", default=os.getenv("OPENROUTER_URL") or os.getenv("OPENAI_BASE_URL") or "https://openrouter.ai/api/v1", ) parser.add_argument( "--model_name", default=os.getenv("OPENROUTER_MODEL") or os.getenv("OPENAI_MODEL") or "qwen/qwen3.5-35b-a3b", ) parser.add_argument( "--api_key_env", default="OPENROUTER_API_KEY,AGENT_API_KEY,OPENAI_API_KEY", ) parser.add_argument("--agent_timeout", type=float, default=60.0) parser.add_argument("--llm_timeout", type=float, default=None) parser.add_argument("--question_timeout", type=float, default=180.0) parser.add_argument("--max_output_tokens", type=int, default=900) parser.add_argument("--history_chars", type=int, default=24000) parser.add_argument("--observation_chars", type=int, default=20000) parser.add_argument("--temperature", type=float, default=0.0) args = parser.parse_args() args.out = result_csv_path(args.out) llm_timeout = args.llm_timeout if args.llm_timeout is not None else args.agent_timeout test = load_json(args.test_path) if args.max_samples is not None: test = test[: max(0, args.max_samples)] print(f"Loaded test={len(test)}") loaded_env = load_env_file(args.env_path) auto_server_data = not args.no_auto_server_data and any( scenario_needs_server_data(s) for s in test ) use_server_data = args.use_server_data or auto_server_data if use_server_data: print( f"Server data mode enabled: {args.server_url}" + (f" (env: {loaded_env})" if loaded_env else "") ) if not os.path.exists(args.model_bundle): raise FileNotFoundError( f"Model bundle not found: {args.model_bundle}. Run train.py first." ) print(f"Loading trained model bundle: {args.model_bundle}") tmodel, smodel, model_metadata = load_model_bundle(args.model_bundle) if model_metadata: print(f"Model metadata: {model_metadata}") use_agent = not args.no_agent llm_client: Optional[OpenAI] = None if use_agent: api_key = get_env_value(args.api_key_env) if api_key: llm_client = OpenAI( base_url=args.model_url, api_key=api_key, http_client=httpx.Client(verify=args.verify_ssl), timeout=llm_timeout, ) print(f"Agent mode enabled: {args.model_name}") else: print( f"Warning: {args.api_key_env} is not set; using direct trained-model fallback." ) row_entries, debug_entries, trace_entries = [], [], [] print("Predicting Track A test...") headers = {"Content-Type": "application/json"} token = os.environ.get(args.auth_token_env, "").strip() if token: headers["Authorization"] = f"Bearer {token}" headers["X-API-Token"] = token if use_server_data and not token: print(f"Warning: {args.auth_token_env} is not set; server may reject requests.") def ordered_rows() -> list[dict[str, str]]: return [row for _, row in sorted(row_entries, key=lambda x: x[0])] def ordered_debug() -> list[dict]: return [row for _, row in sorted(debug_entries, key=lambda x: x[0])] def ordered_traces() -> list[dict]: return [row for _, row in sorted(trace_entries, key=lambda x: x[0])] def checkpoint() -> None: write_result(args.out, ordered_rows()) write_debug(args.debug_out, ordered_debug()) write_json(args.traces_out, ordered_traces()) def solve_one(i: int, s: dict) -> tuple[int, dict, dict, dict, str]: sid = s.get("scenario_id") or s.get("ID") or f"test_{i}" start_time = time.perf_counter() tool_calls: List[str] = [] pred_s = s used_server_data = False try: if use_server_data and ( args.use_server_data or scenario_needs_server_data(s) ): with httpx.Client( headers=headers, timeout=args.timeout, verify=args.verify_ssl, follow_redirects=True, ) as client_ctx: pred_s, tool_calls = hydrate_scenario_from_server( s, client_ctx, args.server_url, try_scenario_endpoint=args.try_scenario_endpoint, ) used_server_data = bool(tool_calls) labels, dbg, completion, agent_tool_calls, agent_used = ( run_agent_with_trained_model( llm_client, args.model_name, tmodel, smodel, pred_s, timeout=llm_timeout, max_steps=args.max_steps, max_tool_calls=args.max_tool_calls, temperature=args.temperature, max_output_tokens=args.max_output_tokens, history_chars=args.history_chars, observation_chars=args.observation_chars, question_timeout=args.question_timeout, ) ) tool_calls.extend(agent_tool_calls) pred = "|".join(labels) elapsed = round(time.perf_counter() - start_time, 3) row = {"ID": sid, "Track A": pred, "Track B": ""} debug_row = { "scenario_id": sid, "prediction": pred, "debug": dbg, "options": get_options(pred_s), "used_server_data": used_server_data, "still_needs_server_data": scenario_needs_server_data(pred_s), "agent_used": agent_used, } trace_row = { "scenario_id": sid, "question": build_question_text(pred_s), "completion": completion, "prediction": labels, "ground_truth": pred_s.get("answer", "To be determined"), "score": 0.0, "execution_time_seconds": elapsed, "tool_calls": "\n".join(tool_calls), "boxed": f"\\boxed{{{pred}}}", } prob = dbg.get("template_prob", 0.0) try: prob_text = f"{float(prob):.3f}" except Exception: prob_text = "nan" msg = ( f"[A {i}/{len(test)}] {sid} -> {pred} " f"({dbg.get('template')} {prob_text})" ) return i, row, debug_row, trace_row, msg except Exception as exc: elapsed = round(time.perf_counter() - start_time, 3) row = {"ID": sid, "Track A": "", "Track B": ""} debug_row = { "scenario_id": sid, "prediction": "", "debug": { "runner_exception": f"{type(exc).__name__}: {exc}", "agent_used": False, }, "options": get_options(pred_s), "used_server_data": used_server_data, "still_needs_server_data": scenario_needs_server_data(pred_s), "agent_used": False, } trace_row = { "scenario_id": sid, "question": build_question_text(pred_s), "completion": "", "prediction": [], "ground_truth": pred_s.get("answer", "To be determined"), "score": 0.0, "execution_time_seconds": elapsed, "tool_calls": "\n".join(tool_calls), "boxed": "\\boxed{}", "messages": [ { "action": "runner_exception", "observation": f"{type(exc).__name__}: {exc}", } ], } return i, row, debug_row, trace_row, f"[A {i}/{len(test)}] {sid} -> ERROR: {exc}" max_workers = max(1, int(args.concurrency)) if max_workers == 1: iterator = enumerate(test, start=1) if not args.no_progress: iterator = tqdm(iterator, total=len(test), desc="Predicting Track A") for i, s in iterator: result_i, row, debug_row, trace_row, msg = solve_one(i, s) row_entries.append((result_i, row)) debug_entries.append((result_i, debug_row)) trace_entries.append((result_i, trace_row)) if args.checkpoint_every and result_i % args.checkpoint_every == 0: checkpoint() if result_i <= 5 or result_i % 50 == 0: if args.no_progress: print(msg) else: tqdm.write(msg) else: with ThreadPoolExecutor(max_workers=max_workers) as executor: future_to_index = { executor.submit(solve_one, i, s): i for i, s in enumerate(test, start=1) } iterator = as_completed(future_to_index) if not args.no_progress: iterator = tqdm(iterator, total=len(test), desc="Predicting Track A") completed = 0 for future in iterator: completed += 1 result_i, row, debug_row, trace_row, msg = future.result() row_entries.append((result_i, row)) debug_entries.append((result_i, debug_row)) trace_entries.append((result_i, trace_row)) if args.checkpoint_every and completed % args.checkpoint_every == 0: checkpoint() if args.no_progress: print(f"[{completed}/{len(test)}] {msg}") else: tqdm.write(f"[{completed}/{len(test)}] {msg}") rows = ordered_rows() added_track_b = append_track_b_blank_rows(rows, args.track_b_test) if added_track_b: print(f"Added Track B blank ID rows: {added_track_b}") write_result(args.out, rows) write_debug(args.debug_out, ordered_debug()) write_json(args.traces_out, ordered_traces()) print(f"Saved result: {args.out}") print(f"Saved debug: {args.debug_out}") print(f"Saved traces: {args.traces_out}") if __name__ == "__main__": main()