Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| Inference Script β SRE Incident Response OpenEnv | |
| ================================================= | |
| Runs a ReAct-style OpenAI agent against all three tasks and | |
| reports reproducible baseline scores. | |
| Usage: | |
| export OPENAI_API_KEY="sk-..." | |
| export OPENENV_BASE_URL="http://localhost:7860" # or your HF Space URL | |
| python inference.py | |
| # Run specific tasks: | |
| python inference.py --tasks task1 task2 | |
| # Use a different model: | |
| python inference.py --model gpt-4o | |
| Requirements: | |
| pip install openai httpx rich | |
| """ | |
| import os | |
| import sys | |
| import json | |
| import re | |
| import argparse | |
| import time | |
| import subprocess | |
| import signal | |
| from typing import Optional | |
| import httpx | |
| try: | |
| from rich.console import Console | |
| from rich.table import Table | |
| from rich.panel import Panel | |
| from rich import print as rprint | |
| RICH = True | |
| except ImportError: | |
| RICH = False | |
| Console = None | |
| # βββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| DEFAULT_MODEL = os.environ.get("MODEL_NAME") or "gpt-4o-mini" | |
| DEFAULT_BASE_URL = os.environ.get("ENV_BASE_URL") or os.environ.get("OPENENV_BASE_URL") or "http://localhost:7860" | |
| OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") or os.environ.get("API_KEY") or "" | |
| DEFAULT_LLM_BASE_URL = ( | |
| os.environ.get("API_BASE_URL") | |
| or os.environ.get("LITELLM_BASE_URL") | |
| or os.environ.get("OPENAI_BASE_URL") | |
| or os.environ.get("OPENAI_API_BASE") | |
| or "https://api.openai.com/v1" | |
| ) | |
| SYSTEM_PROMPT = """You are an expert Site Reliability Engineer (SRE) responding to a production incident. | |
| You will receive alerts, service statuses, and investigation results. | |
| Your goal is to identify the root cause and resolve the incident efficiently. | |
| At each step, respond with ONLY a valid JSON object in this exact format: | |
| {"action_type": "<action>", "parameters": {<params>}} | |
| Available actions: | |
| - query_logs: {"service": "<name>"} β fetch recent logs for a service | |
| - check_metrics: {"service": "<name>"} β get current metrics for a service | |
| - check_config: {"service": "<name>"} β inspect live runtime configuration | |
| - restart_service: {"service": "<name>"} β restart a service (use carefully) | |
| - rollback_deployment: {"service": "<name>"} β roll back to previous version | |
| - kill_query: {"source": "<service>"} β terminate long-running DB queries from a source | |
| - scale_service: {"service": "<name>", "replicas": <int>} β change replica count | |
| - examine_trace: {"trace_id": "<id>"} β examine distributed trace | |
| - acknowledge_alert: {"alert_id": "<id>"} β acknowledge an alert | |
| - resolve_incident: {} β mark incident as resolved (only when services are healthy) | |
| SRE Investigation Strategy: | |
| 1. Read ALL alerts and service statuses carefully | |
| 2. Look at recent deployments β they are often correlated with incidents | |
| 3. Use query_logs and check_metrics to gather evidence before acting | |
| 4. Form a clear hypothesis about the root cause | |
| 5. Apply the most targeted fix (prefer rollback over restart when deployment changed) | |
| 6. Verify all affected services are healthy | |
| 7. Call resolve_incident to complete the episode | |
| Respond ONLY with JSON. No markdown. No explanation.""" | |
| # βββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _supports_unicode_stdout() -> bool: | |
| enc = getattr(sys.stdout, "encoding", None) or "" | |
| try: | |
| "ββ βΉβββββ’".encode(enc or "utf-8") | |
| return True | |
| except Exception: | |
| return False | |
| UNICODE_OK = _supports_unicode_stdout() | |
| HR_THICK = "β" if UNICODE_OK else "=" | |
| HR_THIN = "β" if UNICODE_OK else "-" | |
| ARROW = "β" if UNICODE_OK else "->" | |
| def safe_print(s: str = "", **kwargs): | |
| """ | |
| Print without crashing on Windows codepages that can't encode Unicode. | |
| """ | |
| try: | |
| print(s, **kwargs) | |
| except UnicodeEncodeError: | |
| enc = getattr(sys.stdout, "encoding", None) or "utf-8" | |
| s2 = s.encode(enc, errors="replace").decode(enc, errors="replace") | |
| print(s2, **kwargs) | |
| def emit_block(tag: str, payload: dict): | |
| """ | |
| Emit structured output blocks for automated validators. | |
| Format: [START]/[STEP]/[END] followed by one-line JSON. | |
| """ | |
| try: | |
| line = json.dumps(payload, ensure_ascii=True, separators=(",", ":")) | |
| except Exception: | |
| line = "{}" | |
| safe_print(f"[{tag}] {line}") | |
| def clamp_score_strict(score: float, eps: float = 0.01) -> float: | |
| """ | |
| Hackathon validator requirement: scores must be strictly within (0, 1). | |
| Clamp away from endpoints to avoid returning exactly 0.0 or 1.0. | |
| """ | |
| try: | |
| s = float(score) | |
| except Exception: | |
| s = 0.0 | |
| if s <= 0.0: | |
| return eps | |
| if s >= 1.0: | |
| return 1.0 - eps | |
| return s | |
| def log(msg: str, level: str = "INFO"): | |
| if UNICODE_OK: | |
| prefix = {"INFO": "βΉ", "OK": "β", "WARN": "β ", "ERR": "β"}.get(level, "β’") | |
| else: | |
| prefix = {"INFO": "i", "OK": "+", "WARN": "!", "ERR": "x"}.get(level, "-") | |
| safe_print(f" {prefix} {msg}") | |
| def format_observation(obs: dict) -> str: | |
| """Format observation dict into a concise prompt string.""" | |
| dash = "β" if UNICODE_OK else "-" | |
| lines = [f"=== INCIDENT {dash} Step {obs.get('step', 0)} ===\n"] | |
| lines.append("ACTIVE ALERTS:") | |
| for alert in obs.get("alerts", []): | |
| ack = " [ACK]" if alert.get("acknowledged") else "" | |
| sev = alert.get("severity", "?").upper() | |
| lines.append(f" [{sev}]{ack} {alert.get('service')}: {alert.get('message')}") | |
| lines.append("\nSERVICE STATUS:") | |
| for name, svc in obs.get("services", {}).items(): | |
| conn = "" | |
| if svc.get("connections") is not None: | |
| conn = f" | conns: {svc['connections']}/{svc.get('max_connections', '?')}" | |
| lines.append( | |
| f" {name}: {svc.get('status', '?').upper()} | " | |
| f"cpu: {svc.get('cpu_percent', 0):.1f}% | " | |
| f"mem: {svc.get('memory_percent', 0):.1f}% | " | |
| f"errors: {svc.get('error_rate', 0):.1f}/s | " | |
| f"v{svc.get('version', '?')}{conn}" | |
| ) | |
| if obs.get("recent_deployments"): | |
| lines.append("\nRECENT DEPLOYMENTS:") | |
| for dep in obs["recent_deployments"]: | |
| arrow = "β" if UNICODE_OK else "->" | |
| lines.append( | |
| f" {dep.get('service')}: v{dep.get('previous', '?')} {arrow} " | |
| f"v{dep.get('version')} deployed at {dep.get('deployed_at')}" | |
| ) | |
| if obs.get("message"): | |
| lines.append(f"\nLAST ACTION RESULT:\n{obs['message']}") | |
| if obs.get("runbook_hints"): | |
| lines.append("\nRUNBOOK HINTS:") | |
| for h in obs["runbook_hints"]: | |
| lines.append(f" β’ {h}") | |
| return "\n".join(lines) | |
| def call_llm(client: httpx.Client, model: str, messages: list, llm_base_url: str) -> str: | |
| """Call OpenAI chat completions API.""" | |
| try: | |
| base = (llm_base_url or "").rstrip("/") | |
| if base.endswith("/v1"): | |
| url = f"{base}/chat/completions" | |
| else: | |
| url = f"{base}/v1/chat/completions" | |
| response = client.post( | |
| url, | |
| headers={ | |
| "Authorization": f"Bearer {OPENAI_API_KEY}", | |
| "Content-Type": "application/json", | |
| }, | |
| json={ | |
| "model": model, | |
| "messages": messages, | |
| "max_tokens": 200, | |
| "temperature": 0.0, | |
| }, | |
| timeout=30.0, | |
| ) | |
| response.raise_for_status() | |
| return response.json()["choices"][0]["message"]["content"].strip() | |
| except httpx.RequestError as e: | |
| raise Exception(f"Network error calling OpenAI API: {e}") | |
| except httpx.HTTPStatusError as e: | |
| raise Exception(f"OpenAI API error (status {e.response.status_code}): {e.response.text}") | |
| except (KeyError, IndexError) as e: | |
| raise Exception(f"Unexpected response format from OpenAI API: {e}") | |
| def _is_localhost_url(url: str) -> bool: | |
| u = (url or "").strip().lower() | |
| return u.startswith("http://localhost") or u.startswith("http://127.0.0.1") | |
| def _wait_for_health(base_url: str, timeout_s: float = 20.0) -> bool: | |
| deadline = time.time() + timeout_s | |
| last_err: Optional[Exception] = None | |
| while time.time() < deadline: | |
| try: | |
| with httpx.Client(base_url=base_url, timeout=2.5) as c: | |
| r = c.get("/health") | |
| r.raise_for_status() | |
| return True | |
| except Exception as e: | |
| last_err = e | |
| time.sleep(0.4) | |
| if last_err: | |
| log(f"Health check still failing: {last_err}", "WARN") | |
| return False | |
| def _start_local_server() -> subprocess.Popen: | |
| """ | |
| Start the environment server in a subprocess. | |
| Intended for runners that execute inference without already running the env. | |
| """ | |
| cmd = [ | |
| sys.executable, | |
| "-m", | |
| "uvicorn", | |
| "app.main:app", | |
| "--host", | |
| "127.0.0.1", | |
| "--port", | |
| "7860", | |
| "--workers", | |
| "1", | |
| ] | |
| kwargs = {} | |
| if os.name == "nt": | |
| # Avoid CTRL-C propagation weirdness on Windows runners. | |
| kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined] | |
| return subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, **kwargs) | |
| def _stop_local_server(p: subprocess.Popen): | |
| try: | |
| if p.poll() is not None: | |
| return | |
| if os.name == "nt": | |
| p.send_signal(signal.CTRL_BREAK_EVENT) # type: ignore[attr-defined] | |
| try: | |
| p.wait(timeout=5) | |
| return | |
| except Exception: | |
| pass | |
| p.terminate() | |
| try: | |
| p.wait(timeout=5) | |
| except Exception: | |
| p.kill() | |
| except Exception: | |
| pass | |
| def parse_action(text: str) -> dict: | |
| """Parse JSON action from LLM output, with fallback.""" | |
| text = text.strip() | |
| # Remove markdown code blocks if present | |
| text = re.sub(r"```(?:json)?\s*|\s*```", "", text).strip() | |
| try: | |
| return json.loads(text) | |
| except json.JSONDecodeError: | |
| match = re.search(r'\{[^{}]*\}', text, re.DOTALL) | |
| if match: | |
| try: | |
| return json.loads(match.group()) | |
| except json.JSONDecodeError: | |
| pass | |
| # Fallback: safe no-op | |
| return {"action_type": "acknowledge_alert", "parameters": {"alert_id": "ALT-001"}} | |
| def _pick_target_service(obs: dict) -> Optional[str]: | |
| services = obs.get("services") or {} | |
| if not isinstance(services, dict) or not services: | |
| return None | |
| def score_service(item): | |
| _, svc = item | |
| try: | |
| err = float(svc.get("error_rate") or 0.0) | |
| except Exception: | |
| err = 0.0 | |
| status = str(svc.get("status") or "").lower() | |
| bad = 1.0 if status not in ("healthy", "ok", "passing") else 0.0 | |
| return (bad, err) | |
| return max(services.items(), key=score_service)[0] | |
| def fallback_policy(obs: dict, step_num: int) -> dict: | |
| """ | |
| Deterministic, no-network fallback agent. | |
| This is intentionally conservative: gather evidence, prefer rollback on recent deploys, | |
| and only resolve when things look healthy. | |
| """ | |
| alerts = obs.get("alerts") or [] | |
| if isinstance(alerts, list): | |
| for a in alerts: | |
| if isinstance(a, dict) and a.get("acknowledged") is False and a.get("alert_id"): | |
| return {"action_type": "acknowledge_alert", "parameters": {"alert_id": a["alert_id"]}} | |
| # If there's a recent deployment on a sick service, prefer rollback early. | |
| recent = obs.get("recent_deployments") or [] | |
| if isinstance(recent, list) and recent: | |
| target = _pick_target_service(obs) | |
| for dep in recent: | |
| if not isinstance(dep, dict): | |
| continue | |
| svc = dep.get("service") | |
| if svc and (target is None or svc == target): | |
| return {"action_type": "rollback_deployment", "parameters": {"service": svc}} | |
| target = _pick_target_service(obs) or "api" | |
| # Alternate between logs/metrics/config early to build context. | |
| if step_num % 3 == 0: | |
| return {"action_type": "query_logs", "parameters": {"service": target}} | |
| if step_num % 3 == 1: | |
| return {"action_type": "check_metrics", "parameters": {"service": target}} | |
| return {"action_type": "check_config", "parameters": {"service": target}} | |
| # βββ Core Runner βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def run_task( | |
| env_client: httpx.Client, | |
| llm_client: httpx.Client, | |
| task_id: str, | |
| model: str, | |
| llm_base_url: str, | |
| max_steps: int, | |
| verbose: bool = True, | |
| ) -> dict: | |
| """Run one complete episode for a task. Returns result dict.""" | |
| if verbose: | |
| safe_print(f"\n{HR_THIN*60}") | |
| safe_print(f" Task: {task_id.upper()}") | |
| safe_print(f"{HR_THIN*60}") | |
| episode_log = [] | |
| score = 0.0 | |
| steps_taken = 0 | |
| session_id = None | |
| try: | |
| # ββ Reset ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| reset_resp = env_client.post("/reset", json={"task_id": task_id, "seed": 42}) | |
| reset_resp.raise_for_status() | |
| obs = reset_resp.json() | |
| session_id = obs["session_id"] | |
| # Structured output: indicate a new episode started. | |
| emit_block("START", {"task_id": task_id, "session_id": session_id}) | |
| if verbose: | |
| task_name = obs.get("message", "").split("Task:")[1].split("(")[0].strip() \ | |
| if "Task:" in obs.get("message", "") else task_id | |
| log(f"Session: {session_id[:8]}...", "INFO") | |
| log(obs.get("message", ""), "INFO") | |
| conversation = [] | |
| done = False | |
| # ββ Episode Loop βββββββββββββββββββββββββββββββββββββββββββββ | |
| for step_num in range(max_steps): | |
| obs_text = format_observation(obs) | |
| conversation.append({"role": "user", "content": obs_text}) | |
| # Trim conversation to last 4 turns (keep it focused) | |
| messages = [{"role": "system", "content": SYSTEM_PROMPT}] | |
| messages += conversation[-8:] | |
| # Get action from LLM (or fallback policy if no API key) | |
| if OPENAI_API_KEY: | |
| action_text = call_llm(llm_client, model, messages, llm_base_url=llm_base_url) | |
| conversation.append({"role": "assistant", "content": action_text}) | |
| action_dict = parse_action(action_text) | |
| else: | |
| action_dict = fallback_policy(obs, step_num) | |
| conversation.append({"role": "assistant", "content": json.dumps(action_dict)}) | |
| action_type = action_dict.get("action_type", "unknown") | |
| parameters = action_dict.get("parameters", {}) | |
| if verbose: | |
| params_str = json.dumps(parameters) if parameters else "{}" | |
| safe_print(f" Step {step_num+1:2d}: {action_type}({params_str})", end="") | |
| # Take step | |
| step_resp = env_client.post("/step", json={ | |
| "session_id": session_id, | |
| "action": {"action_type": action_type, "parameters": parameters}, | |
| }) | |
| step_resp.raise_for_status() | |
| step_data = step_resp.json() | |
| obs = step_data["observation"] | |
| reward_val = step_data["reward"]["value"] | |
| done = step_data["done"] | |
| steps_taken = step_num + 1 | |
| # Structured output: one line per environment step. | |
| emit_block( | |
| "STEP", | |
| { | |
| "task_id": task_id, | |
| "session_id": session_id, | |
| "step": steps_taken, | |
| "action_type": action_type, | |
| "parameters": parameters, | |
| "reward": reward_val, | |
| "done": done, | |
| "grader_score": ( | |
| clamp_score_strict(step_data.get("info", {}).get("grader_score")) | |
| if step_data.get("info", {}).get("grader_score") is not None | |
| else None | |
| ), | |
| }, | |
| ) | |
| if verbose: | |
| reward_str = f"{reward_val:+.3f}" | |
| current_score = step_data["info"].get("grader_score", 0.0) | |
| safe_print(f" {ARROW} reward: {reward_str} | score: {current_score:.3f}") | |
| episode_log.append({ | |
| "step": step_num + 1, | |
| "action_type": action_type, | |
| "parameters": parameters, | |
| "reward": reward_val, | |
| "message_preview": obs.get("message", "")[:150], | |
| }) | |
| if done: | |
| break | |
| # ββ Get Final Grade βββββββββββββββββββββββββββββββββββββββββββ | |
| grader_resp = env_client.post("/grader", json={"session_id": session_id}) | |
| grader_resp.raise_for_status() | |
| grader_data = grader_resp.json() | |
| score = clamp_score_strict(grader_data["score"]) | |
| breakdown = grader_data.get("breakdown", {}) | |
| if verbose: | |
| safe_print(f"\n {HR_THIN*30}") | |
| log(f"Final score: {score:.4f}", "OK" if score >= 0.6 else "WARN") | |
| log(f"Steps taken: {steps_taken}", "INFO") | |
| if breakdown: | |
| log("Breakdown:", "INFO") | |
| for k, v in breakdown.items(): | |
| safe_print(f" {k}: {v:+.4f}") | |
| except httpx.RequestError as e: | |
| error_msg = f"Network error communicating with environment: {e}" | |
| if verbose: | |
| log(error_msg, "ERR") | |
| episode_log.append({"error": error_msg}) | |
| except httpx.HTTPStatusError as e: | |
| error_msg = f"Environment API error (status {e.response.status_code}): {e.response.text}" | |
| if verbose: | |
| log(error_msg, "ERR") | |
| episode_log.append({"error": error_msg}) | |
| except (KeyError, ValueError, TypeError) as e: | |
| error_msg = f"Unexpected response format from environment: {e}" | |
| if verbose: | |
| log(error_msg, "ERR") | |
| episode_log.append({"error": error_msg}) | |
| except Exception as e: | |
| if verbose: | |
| log(f"Error: {e}", "ERR") | |
| episode_log.append({"error": str(e)}) | |
| # Get task info for name/difficulty | |
| try: | |
| tasks_resp = env_client.get("/tasks") | |
| tasks_resp.raise_for_status() | |
| task_info = {} | |
| for t in tasks_resp.json().get("tasks", []): | |
| if t["task_id"] == task_id: | |
| task_info = t | |
| break | |
| except Exception as e: | |
| # If we can't get task info, use defaults | |
| task_info = {} | |
| if verbose: | |
| log(f"Warning: Could not retrieve task info: {e}", "WARN") | |
| return { | |
| "task_id": task_id, | |
| "task_name": task_info.get("name", task_id), | |
| "difficulty": task_info.get("difficulty", "?"), | |
| "score": clamp_score_strict(score), | |
| "steps_taken": steps_taken, | |
| "success": score >= task_info.get("passing_score", 0.6), | |
| "episode_log": episode_log, | |
| } | |
| # βββ Main βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Run inference agent against SRE Incident Response environment" | |
| ) | |
| parser.add_argument("--model", default=DEFAULT_MODEL, help="Model name to use (MODEL_NAME)") | |
| parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="Environment base URL (ENV_BASE_URL)") | |
| parser.add_argument("--max-steps", type=int, default=12, help="Max steps per episode") | |
| parser.add_argument("--tasks", nargs="+", default=["task1", "task2", "task3"], | |
| help="Tasks to run (task1, task2, task3)") | |
| parser.add_argument("--quiet", action="store_true", help="Suppress step-by-step output") | |
| parser.add_argument("--output", help="Save results to JSON file") | |
| parser.add_argument( | |
| "--llm-base-url", | |
| default=DEFAULT_LLM_BASE_URL, | |
| help="LLM proxy base URL (API_BASE_URL), e.g. https://<proxy>/v1", | |
| ) | |
| parser.add_argument( | |
| "--strict-exit", | |
| action="store_true", | |
| help="Exit non-zero when not all tasks pass (default: exit 0 if script completes).", | |
| ) | |
| args = parser.parse_args() | |
| if not OPENAI_API_KEY: | |
| safe_print("WARN: OPENAI_API_KEY not set; using deterministic fallback policy (no OpenAI calls).") | |
| dash = "β" if UNICODE_OK else "-" | |
| safe_print(f"\n{HR_THICK*60}") | |
| safe_print(f" SRE Incident Response {dash} Inference Evaluation") | |
| safe_print(f"{HR_THICK*60}") | |
| safe_print(f" Model: {args.model}") | |
| safe_print(f" Env URL: {args.base_url}") | |
| safe_print(f" Tasks: {', '.join(args.tasks)}") | |
| safe_print(f" MaxSteps: {args.max_steps}") | |
| safe_print(f"{HR_THICK*60}") | |
| # Structured output: run header (always emitted). | |
| emit_block( | |
| "START", | |
| { | |
| "model": args.model, | |
| "base_url": args.base_url, | |
| "llm_base_url": args.llm_base_url, | |
| "tasks": list(args.tasks), | |
| "max_steps": args.max_steps, | |
| "using_openai": bool(OPENAI_API_KEY), | |
| }, | |
| ) | |
| results = [] | |
| start = time.time() | |
| server_proc: Optional[subprocess.Popen] = None | |
| try: | |
| # Verify environment is reachable; auto-start local server if needed. | |
| if not _wait_for_health(args.base_url, timeout_s=3.0) and _is_localhost_url(args.base_url): | |
| log("Environment not reachable; starting local server...", "WARN") | |
| server_proc = _start_local_server() | |
| if not _wait_for_health(args.base_url, timeout_s=20.0): | |
| x = "β" if UNICODE_OK else "x" | |
| safe_print(f"\n {x} Environment not reachable at {args.base_url}") | |
| # Some validators expect per-task scores even on failure. Emit placeholder | |
| # task results with strictly (0,1) scores so the run is still parseable. | |
| placeholder_results = [ | |
| { | |
| "task_id": tid, | |
| "task_name": tid, | |
| "difficulty": "?", | |
| "score": clamp_score_strict(0.0), | |
| "steps_taken": 0, | |
| "success": False, | |
| "episode_log": [{"error": f"Environment not reachable at {args.base_url}"}], | |
| } | |
| for tid in list(args.tasks) | |
| ] | |
| emit_block( | |
| "END", | |
| { | |
| "model": args.model, | |
| "environment": "sre-incident-response", | |
| "results": placeholder_results, | |
| "summary": { | |
| "mean_score": clamp_score_strict(0.0), | |
| "tasks_passed": 0, | |
| "total_tasks": len(placeholder_results), | |
| }, | |
| "error": f"Environment not reachable at {args.base_url}", | |
| }, | |
| ) | |
| sys.exit(1) | |
| with httpx.Client(base_url=args.base_url, timeout=30.0) as env_client: | |
| health = env_client.get("/health") | |
| health.raise_for_status() | |
| ok = "β" if UNICODE_OK else "+" | |
| safe_print(f"\n {ok} Environment healthy: {health.json()}") | |
| with httpx.Client(timeout=60.0) as llm_client: | |
| for task_id in args.tasks: | |
| result = run_task( | |
| env_client=env_client, | |
| llm_client=llm_client, | |
| task_id=task_id, | |
| model=args.model, | |
| llm_base_url=args.llm_base_url, | |
| max_steps=args.max_steps, | |
| verbose=not args.quiet, | |
| ) | |
| results.append(result) | |
| time.sleep(0.5) # Rate limiting courtesy | |
| finally: | |
| if server_proc is not None: | |
| _stop_local_server(server_proc) | |
| # ββ Summary βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| elapsed = time.time() - start | |
| mean_score = sum(r["score"] for r in results) / len(results) if results else clamp_score_strict(0.0) | |
| mean_score = clamp_score_strict(mean_score) | |
| passed = sum(1 for r in results if r["success"]) | |
| safe_print(f"\n{HR_THICK*60}") | |
| safe_print(" INFERENCE RESULTS SUMMARY") | |
| safe_print(f"{HR_THICK*60}") | |
| safe_print(f" {'Task':<35} {'Diff':<8} {'Score':<8} {'Steps':<7} {'Status'}") | |
| safe_print(f" {HR_THIN*55}") | |
| for r in results: | |
| status = ("β PASS" if UNICODE_OK else "+ PASS") if r["success"] else ("β FAIL" if UNICODE_OK else "x FAIL") | |
| safe_print( | |
| f" {r['task_name']:<35} {r['difficulty']:<8} " | |
| f"{r['score']:.4f} {r['steps_taken']:<7} {status}" | |
| ) | |
| safe_print(f" {HR_THIN*55}") | |
| safe_print(f" {'Mean Score':<35} {'':8} {mean_score:.4f}") | |
| safe_print(f" Tasks passed: {passed}/{len(results)}") | |
| safe_print(f" Elapsed: {elapsed:.1f}s") | |
| safe_print(f"{HR_THICK*60}\n") | |
| # ββ Save results ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| output = { | |
| "model": args.model, | |
| "environment": "sre-incident-response", | |
| "results": results, | |
| "summary": { | |
| # Avoid rounding to 0.0/1.0; validator requires strict (0,1). | |
| "mean_score": clamp_score_strict(mean_score), | |
| "tasks_passed": passed, | |
| "total_tasks": len(results), | |
| "elapsed_seconds": round(elapsed, 1), | |
| }, | |
| } | |
| try: | |
| if args.output: | |
| with open(args.output, "w") as f: | |
| json.dump(output, f, indent=2) | |
| safe_print(f" Results saved to {args.output}") | |
| else: | |
| # Always save a inference_results.json for reproducibility | |
| with open("inference_results.json", "w") as f: | |
| json.dump(output, f, indent=2) | |
| safe_print(" Results saved to inference_results.json") | |
| except Exception as e: | |
| safe_print(f" WARN: Could not write results file: {e}") | |
| # Structured output: final summary block for validators. | |
| emit_block("END", output) | |
| # Exit code: | |
| # - default: 0 if script ran to completion (so runners don't treat "failed tasks" as a crash) | |
| # - strict: 0 only if all tasks pass | |
| if args.strict_exit: | |
| sys.exit(0 if passed == len(results) else 1) | |
| sys.exit(0) | |
| if __name__ == "__main__": | |
| try: | |
| main() | |
| except KeyboardInterrupt: | |
| safe_print("\nInterrupted.") | |
| raise | |
| except Exception as e: | |
| safe_print(f"FATAL: inference.py crashed: {e}") | |
| sys.exit(2) |