Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """Hackathon baseline inference script for TalkingHeadBench. | |
| This script: | |
| - Loads benchmark cases from tests/test_set/. | |
| - Connects to a running TalkingHeadBench OpenEnv server. | |
| - Runs one episode for each task tier: image, clips, weights. | |
| - Uses the OpenAI Python client for all LLM calls. | |
| - Prints per-tier scores and a weighted final score. | |
| Required environment variables: | |
| - API_BASE_URL | |
| - MODEL_NAME | |
| - HF_TOKEN | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import re | |
| import sys | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from typing import Any | |
| from openai import OpenAI | |
| from pydantic import ValidationError | |
| from client import TalkingHeadBenchEnv | |
| from models import ImageDiagnosticsAction, ParamAnomalyAction, PhonemeRiskAction | |
| PROJECT_ROOT = Path(__file__).resolve().parent | |
| TEST_SET_DIR = PROJECT_ROOT / "tests" / "test_set" | |
| TEST_SET_FILES: dict[str, Path] = { | |
| "image": TEST_SET_DIR / "subenv1_cases.json", | |
| "clips": TEST_SET_DIR / "subenv2_cases.json", | |
| "weights": TEST_SET_DIR / "subenv3_cases.json", | |
| } | |
| SYSTEM_PROMPT = ( | |
| "You are a senior diagnostic engineer for TalkingHeadBench. " | |
| "Return only strict JSON matching the expected action schema. " | |
| "Never prescribe absolute numeric parameter values; only directional advice." | |
| ) | |
| ACTION_SCHEMA_SUMMARY = """Step 0 (ImageDiagnosticsAction): | |
| - regime_classification: frontal_simple|non_frontal|complex_background|occluded|low_quality | |
| - identified_risk_factors: list[str] | |
| - prompt_issues: list[str] | |
| - recommended_prompt_modifications: list[str] | |
| - image_usability_score: float in [0,1] | |
| - reasoning: str | |
| Step 1 (ParamAnomalyAction): | |
| - config_risk_level: safe|marginal|risky|dangerous | |
| - anomalies: list[{parameter, issue, severity, linked_failure_mode}] | |
| - predicted_failure_modes: list[str] | |
| - directional_fixes: list[{target, direction, rationale, priority}] | |
| - summary: str | |
| Step 2 (PhonemeRiskAction): | |
| - phoneme_risk_ranking: list[{phoneme, risk_score, risk_type, confidence, evidence}] | |
| - predicted_behavior_triggers: list[{trigger_phoneme, triggered_behavior, association_strength, is_intended, concern_level}] | |
| - risky_phoneme_clusters: list[{phonemes, cluster_risk_type, combined_risk_score, interaction_description}] | |
| - model_behavioral_safety: safe|minor_concerns|moderate_risk|high_risk|unsafe | |
| - mitigation_recommendations: list[{target, action, rationale, priority}] | |
| - summary: str | |
| """ | |
| SCHEMA_MODELS = { | |
| "ImageDiagnosticsAction": ImageDiagnosticsAction, | |
| "ParamAnomalyAction": ParamAnomalyAction, | |
| "PhonemeRiskAction": PhonemeRiskAction, | |
| } | |
| STEP_SCHEMAS = [ | |
| "ImageDiagnosticsAction", | |
| "ParamAnomalyAction", | |
| "PhonemeRiskAction", | |
| ] | |
| class TierRunResult: | |
| tier: str | |
| case_id: str | None | |
| reward: float | |
| scores: dict[str, Any] | |
| steps: int | |
| used_custom_bundle: bool | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description="Run TalkingHeadBench baseline inference.") | |
| parser.add_argument( | |
| "--env-url", | |
| default=os.getenv("THB_ENV_URL", "http://localhost:8000"), | |
| help="TalkingHeadBench server base URL (default: THB_ENV_URL or http://localhost:8000)", | |
| ) | |
| return parser.parse_args() | |
| def require_env(name: str) -> str: | |
| value = os.getenv(name, "").strip() | |
| if not value: | |
| raise SystemExit(f"Missing required environment variable: {name}") | |
| return value | |
| def load_test_set_cases(path: Path) -> list[dict[str, Any]]: | |
| if not path.exists(): | |
| raise SystemExit(f"Missing test-set file: {path}") | |
| try: | |
| payload = json.loads(path.read_text(encoding="utf-8")) | |
| except json.JSONDecodeError as exc: | |
| raise SystemExit(f"Invalid JSON in {path}: {exc}") from exc | |
| cases = payload.get("cases") if isinstance(payload, dict) else None | |
| if not isinstance(cases, list): | |
| raise SystemExit(f"Invalid test-set format in {path}: expected top-level 'cases' list") | |
| if not cases: | |
| raise SystemExit(f"Test-set file has no cases: {path}") | |
| return cases | |
| def _coerce_to_dict(value: Any) -> dict[str, Any]: | |
| if value is None: | |
| return {} | |
| if isinstance(value, dict): | |
| return value | |
| if hasattr(value, "model_dump"): | |
| dumped = value.model_dump() | |
| return dumped if isinstance(dumped, dict) else {"value": dumped} | |
| if hasattr(value, "dict"): | |
| dumped = value.dict() | |
| return dumped if isinstance(dumped, dict) else {"value": dumped} | |
| if hasattr(value, "__dict__"): | |
| return {k: v for k, v in vars(value).items() if not k.startswith("_")} | |
| return {"value": str(value)} | |
| def unpack_step_result(result: Any) -> tuple[dict[str, Any], bool, float | None, dict[str, Any] | None]: | |
| raw_observation = getattr(result, "observation", result) | |
| obs = _coerce_to_dict(raw_observation) | |
| done = bool(getattr(result, "done", obs.get("done", False))) | |
| reward = getattr(result, "reward", obs.get("reward")) | |
| scores = getattr(result, "scores", obs.get("scores")) | |
| if not isinstance(scores, dict): | |
| scores = _coerce_to_dict(scores) if scores is not None else None | |
| if obs.get("scores") is None and scores: | |
| obs["scores"] = scores | |
| return obs, done, reward, scores | |
| def expected_schema_name(step_index: int, observation: dict[str, Any]) -> str: | |
| schema = observation.get("expected_action_schema") | |
| if isinstance(schema, str) and schema: | |
| return schema | |
| if 0 <= step_index < len(STEP_SCHEMAS): | |
| return STEP_SCHEMAS[step_index] | |
| return "unknown" | |
| def minimal_action(step_index: int) -> dict[str, Any]: | |
| if step_index == 0: | |
| return { | |
| "regime_classification": "frontal_simple", | |
| "identified_risk_factors": [], | |
| "prompt_issues": [], | |
| "recommended_prompt_modifications": [], | |
| "image_usability_score": 0.5, | |
| "reasoning": "Fallback action due to parsing failure.", | |
| } | |
| if step_index == 1: | |
| return { | |
| "config_risk_level": "marginal", | |
| "anomalies": [], | |
| "predicted_failure_modes": [], | |
| "directional_fixes": [], | |
| "summary": "Fallback action due to parsing failure.", | |
| } | |
| if step_index == 2: | |
| return { | |
| "phoneme_risk_ranking": [], | |
| "predicted_behavior_triggers": [], | |
| "risky_phoneme_clusters": [], | |
| "model_behavioral_safety": "minor_concerns", | |
| "mitigation_recommendations": [], | |
| "summary": "Fallback action due to parsing failure.", | |
| } | |
| return {} | |
| def _extract_json_candidate(text: str) -> dict[str, Any]: | |
| stripped = text.strip() | |
| if stripped.startswith("```"): | |
| stripped = re.sub(r"^```(?:json)?\s*", "", stripped) | |
| stripped = re.sub(r"\s*```$", "", stripped) | |
| try: | |
| payload = json.loads(stripped) | |
| if isinstance(payload, dict): | |
| return payload | |
| except json.JSONDecodeError: | |
| pass | |
| match = re.search(r"\{.*\}", stripped, flags=re.DOTALL) | |
| if not match: | |
| raise ValueError("No JSON object found in model response") | |
| payload = json.loads(match.group(0)) | |
| if not isinstance(payload, dict): | |
| raise ValueError("Parsed JSON is not an object") | |
| return payload | |
| def validate_action_payload(schema_name: str, payload: dict[str, Any]) -> dict[str, Any]: | |
| model_cls = SCHEMA_MODELS.get(schema_name) | |
| if model_cls is None: | |
| return payload | |
| return model_cls.model_validate(payload).model_dump() | |
| def call_chat_completion( | |
| *, | |
| client: OpenAI, | |
| model_name: str, | |
| messages: list[dict[str, str]], | |
| ) -> str: | |
| response = client.chat.completions.create( | |
| model=model_name, | |
| messages=messages, | |
| temperature=0.2, | |
| max_tokens=1200, | |
| ) | |
| choices = response.choices or [] | |
| if not choices: | |
| raise ValueError("Model response did not include choices") | |
| content = choices[0].message.content | |
| if not isinstance(content, str) or not content.strip(): | |
| raise ValueError("Model response did not include text content") | |
| return content.strip() | |
| def build_user_prompt( | |
| *, | |
| tier: str, | |
| case_id: str | None, | |
| step_index: int, | |
| schema_name: str, | |
| observation: dict[str, Any], | |
| ) -> str: | |
| instruction = observation.get("instruction", "") | |
| node = observation.get("node", "unknown") | |
| signals = observation.get("signals", {}) | |
| return ( | |
| f"Task tier: {tier}\n" | |
| f"Case id: {case_id or 'unknown'}\n" | |
| f"Environment step index: {step_index}\n" | |
| f"Current node: {node}\n" | |
| f"Expected action schema: {schema_name}\n\n" | |
| f"Action schema summary:\n{ACTION_SCHEMA_SUMMARY}\n" | |
| f"Instruction:\n{instruction}\n\n" | |
| f"Signals (JSON):\n{json.dumps(signals, indent=2, sort_keys=True)}\n\n" | |
| f"Return only one JSON object that satisfies {schema_name}." | |
| ) | |
| def generate_action( | |
| *, | |
| client: OpenAI, | |
| model_name: str, | |
| tier: str, | |
| case_id: str | None, | |
| step_index: int, | |
| schema_name: str, | |
| observation: dict[str, Any], | |
| ) -> dict[str, Any]: | |
| user_prompt = build_user_prompt( | |
| tier=tier, | |
| case_id=case_id, | |
| step_index=step_index, | |
| schema_name=schema_name, | |
| observation=observation, | |
| ) | |
| messages = [ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": user_prompt}, | |
| ] | |
| last_error = "unknown" | |
| last_raw = "" | |
| for attempt in range(2): | |
| raw = call_chat_completion(client=client, model_name=model_name, messages=messages) | |
| last_raw = raw | |
| try: | |
| parsed = _extract_json_candidate(raw) | |
| return validate_action_payload(schema_name, parsed) | |
| except (ValueError, json.JSONDecodeError, ValidationError) as exc: | |
| last_error = str(exc) | |
| if attempt == 0: | |
| messages.extend( | |
| [ | |
| {"role": "assistant", "content": raw}, | |
| { | |
| "role": "user", | |
| "content": ( | |
| "Your previous output was invalid. " | |
| f"Error: {last_error}. " | |
| f"Return only corrected JSON for {schema_name}. " | |
| "Do not include markdown fences or extra text." | |
| ), | |
| }, | |
| ] | |
| ) | |
| print( | |
| "Warning: LLM JSON parsing failed after retry. " | |
| f"Using minimal fallback action for step {step_index}. " | |
| f"Last error: {last_error}" | |
| ) | |
| if last_raw: | |
| print("Raw model output:") | |
| print(last_raw) | |
| return minimal_action(step_index) | |
| def _build_custom_bundle_for_tier(tier: str, case: dict[str, Any]) -> dict[str, Any]: | |
| observation = case.get("observation", {}) | |
| if not isinstance(observation, dict): | |
| raise ValueError(f"Case {case.get('id', 'unknown')} has non-dict observation") | |
| case_id = str(case.get("id") or "unknown") | |
| if tier == "image": | |
| if "image_obs" in observation and isinstance(observation.get("image_obs"), dict): | |
| image_obs = observation["image_obs"] | |
| proposed_config = observation.get("proposed_config", {}) | |
| else: | |
| image_obs = observation | |
| proposed_config = observation.get("proposed_config", {}) | |
| if not isinstance(proposed_config, dict): | |
| proposed_config = {} | |
| return { | |
| "case_id": case_id, | |
| "image_observation": image_obs, | |
| "param_config": proposed_config, | |
| } | |
| if tier == "clips": | |
| return { | |
| "case_id": case_id, | |
| "clip_signal_observations": [observation], | |
| } | |
| if tier == "weights": | |
| return { | |
| "case_id": case_id, | |
| "weight_observation": observation, | |
| } | |
| raise ValueError(f"Unsupported tier: {tier}") | |
| def run_tier_episode( | |
| *, | |
| env_url: str, | |
| tier: str, | |
| case: dict[str, Any], | |
| client: OpenAI, | |
| model_name: str, | |
| ) -> TierRunResult: | |
| bundle = _build_custom_bundle_for_tier(tier, case) | |
| planned_case_id = str(case.get("id") or "unknown") | |
| with TalkingHeadBenchEnv(base_url=env_url).sync() as env: | |
| result = env.reset(mode=tier, custom_bundle=bundle) | |
| step_index = 0 | |
| while True: | |
| observation, done, reward, scores = unpack_step_result(result) | |
| case_id = observation.get("case_id") or planned_case_id | |
| schema_name = expected_schema_name(step_index, observation) | |
| if done: | |
| final_reward = float(reward if reward is not None else 0.0) | |
| return TierRunResult( | |
| tier=tier, | |
| case_id=str(case_id) if case_id is not None else None, | |
| reward=final_reward, | |
| scores=scores or {}, | |
| steps=step_index, | |
| used_custom_bundle=True, | |
| ) | |
| action = generate_action( | |
| client=client, | |
| model_name=model_name, | |
| tier=tier, | |
| case_id=str(case_id) if case_id is not None else None, | |
| step_index=step_index, | |
| schema_name=schema_name, | |
| observation=observation, | |
| ) | |
| result = env.step(action) | |
| step_index += 1 | |
| if step_index > 8: | |
| raise RuntimeError(f"Episode exceeded expected step count for tier={tier}") | |
| def extract_tier_score(result: TierRunResult) -> float: | |
| key_by_tier = { | |
| "image": "subenv1_score", | |
| "clips": "subenv2_score", | |
| "weights": "subenv3_score", | |
| } | |
| score_key = key_by_tier.get(result.tier) | |
| if score_key and isinstance(result.scores, dict) and score_key in result.scores: | |
| value = result.scores.get(score_key) | |
| if isinstance(value, (int, float)): | |
| return float(value) | |
| return float(result.reward) | |
| def main() -> None: | |
| args = parse_args() | |
| api_base_url = require_env("API_BASE_URL") | |
| model_name = require_env("MODEL_NAME") | |
| hf_token = require_env("HF_TOKEN") | |
| tier_cases: dict[str, list[dict[str, Any]]] = { | |
| tier: load_test_set_cases(path) for tier, path in TEST_SET_FILES.items() | |
| } | |
| client = OpenAI(api_key=hf_token, base_url=api_base_url) | |
| print("TalkingHeadBench baseline inference") | |
| print(f"Environment URL: {args.env_url}") | |
| print(f"LLM base URL: {api_base_url}") | |
| print(f"Model: {model_name}") | |
| run_order = ["image", "clips", "weights"] | |
| tier_results: dict[str, TierRunResult] = {} | |
| for tier in run_order: | |
| print(f"\nRunning tier: {tier}") | |
| case = tier_cases[tier][0] | |
| result = run_tier_episode( | |
| env_url=args.env_url, | |
| tier=tier, | |
| case=case, | |
| client=client, | |
| model_name=model_name, | |
| ) | |
| tier_results[tier] = result | |
| print( | |
| f"tier={tier} case_id={result.case_id} reward={result.reward:.4f} " | |
| f"steps={result.steps} custom_bundle={result.used_custom_bundle}" | |
| ) | |
| s1 = extract_tier_score(tier_results["image"]) | |
| s2 = extract_tier_score(tier_results["clips"]) | |
| s3 = extract_tier_score(tier_results["weights"]) | |
| weighted_final = 0.25 * s1 + 0.35 * s2 + 0.40 * s3 | |
| report = { | |
| "per_subenv_scores": { | |
| "subenv1_score": round(s1, 6), | |
| "subenv2_score": round(s2, 6), | |
| "subenv3_score": round(s3, 6), | |
| }, | |
| "weighted_final_score": round(weighted_final, 6), | |
| "tiers": { | |
| tier: { | |
| "case_id": tier_results[tier].case_id, | |
| "reward": round(tier_results[tier].reward, 6), | |
| "steps": tier_results[tier].steps, | |
| "scores": tier_results[tier].scores, | |
| "loaded_cases": len(tier_cases[tier]), | |
| } | |
| for tier in run_order | |
| }, | |
| } | |
| print("\nStructured score report") | |
| print(json.dumps(report, indent=2, sort_keys=True)) | |
| if __name__ == "__main__": | |
| try: | |
| main() | |
| except KeyboardInterrupt: | |
| sys.exit(130) | |