""" DebugOps inference script = Runs the LLM agent (or heuristic fallback) against all three tasks and emits structured stdout logs in the exact required format: [START] task= env=debugops model= [STEP] step= action= reward= done= error= [END] success= steps= score= rewards= Environment variables API_BASE_URL LLM endpoint (default: HuggingFace router) MODEL_NAME Model ID (default: Qwen/Qwen2.5-72B-Instruct) HF_TOKEN API key (also checked as OPENAI_API_KEY or API_KEY) """ from __future__ import annotations import os import sys from typing import Any, Dict, List try: from openai import OpenAI _OPENAI_AVAILABLE = True except ImportError: _OPENAI_AVAILABLE = False from grader.grader import evaluate_episode # Configuration API_KEY = os.getenv("HF_TOKEN") or os.getenv("OPENAI_API_KEY") or os.getenv("API_KEY") API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1") MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct") VALID_ACTIONS = ["restart_api", "restart_db", "restart_cache", "scale_up", "noop"] # OpenAI client (spec requires OpenAI client for all LLM calls) _client: Any = None if _OPENAI_AVAILABLE and API_KEY: _client = OpenAI(api_key=API_KEY, base_url=API_BASE_URL) # Heuristic fallback agent (used when no LLM key or on API error) def _fallback_agent(state: Dict[str, Any], action_history: List[str]) -> str: """ Rule-based agent driven by log keywords and fix_progress from state. Uses fix_progress from the observation to know exactly which step to issue next. """ logs = " ".join(state.get("logs", [])).lower() metrics = state.get("metrics", {}) progress = state.get("fix_progress", 0) # Known fix sequences keyed by log signal (ordered by specificity) _log_to_seq = [ ("timeout", ["scale_up", "restart_api"]), ("upstream", ["scale_up", "restart_api"]), ("heap", ["restart_api", "restart_db"]), ("oom", ["restart_api", "restart_db"]), ("memory", ["restart_api", "restart_db"]), ("db pool", ["restart_db", "scale_up"]), ("connections", ["restart_db", "scale_up"]), ("cache miss", ["restart_cache", "scale_up"]), ("cache", ["restart_cache", "scale_up"]), ("db", ["restart_db", "scale_up"]), ] # Identify sequence from logs seq = None for keyword, candidate_seq in _log_to_seq: if keyword in logs: seq = candidate_seq break if seq and progress < len(seq): # Issue the next step in the correct sequence return seq[progress] # Metric-based fallback if metrics.get("cpu", 0) > 80 and "scale_up" not in action_history: return "scale_up" if metrics.get("error_rate", 0) > 0.5 and "restart_api" not in action_history: return "restart_api" # Try any action not yet used twice for a in ["restart_api", "restart_db", "restart_cache", "scale_up"]: if action_history.count(a) < 2: return a return "noop" # LLM call (OpenAI client as required) def _call_llm( state: Dict[str, Any], action_history: List[str], reward_history: List[float], ) -> str: if _client is None: return _fallback_agent(state, action_history) # Build a human-readable step history with outcome signals step_lines = [] for i, (a, r) in enumerate(zip(action_history, reward_history)): outcome = "✓ progress made" if r > 20 else ("✗ wrong / no effect" if r < -5 else "~ neutral") step_lines.append(f" step {i}: {a:18s} reward={r:>8.1f} [{outcome}]") history_block = "\n".join(step_lines) if step_lines else " (none yet)" services_degraded = [s for s, h in state["services"].items() if h == "degraded"] metrics = state["metrics"] prompt = f"""You are an expert SRE triaging a production incident. Your goal is to resolve it in as few steps as possible. SYSTEM STATE (step {state['time_step']}) Degraded services : {services_degraded if services_degraded else 'none'} Metrics : latency={metrics.get('latency', 0):.0f}ms error_rate={metrics.get('error_rate', 0):.2%} cpu={metrics.get('cpu', 0):.0f}% Metric trend : {state.get('metric_trend', 'unknown')} Fix progress : {state.get('fix_progress', 0)} step(s) completed correctly so far SYSTEM LOGS {chr(10).join(' ' + l for l in state['logs'])} ACTION HISTORY & OUTCOMES {history_block} INSTRUCTIONS Root causes have multi-step fix sequences that MUST be performed in order. A positive reward means the last action was a correct step — continue the sequence. A negative reward means the last action was wrong — try something different. Do NOT repeat an action that already got a negative reward. If fix_progress increased after your last action, continue to the NEXT step in the sequence. Choose ONE action from: restart_api, restart_db, restart_cache, scale_up, noop Respond with ONLY the action name.""" try: response = _client.chat.completions.create( model=MODEL_NAME, messages=[{"role": "user", "content": prompt}], temperature=0, max_tokens=20, timeout=15, ) action = response.choices[0].message.content.strip().lower() action = action.split()[0] if action else "noop" # strip any punctuation action = "".join(c for c in action if c.isalnum() or c == "_") if action not in VALID_ACTIONS: return _fallback_agent(state, action_history) return action except Exception: return _fallback_agent(state, action_history) def run_episode(task_name: str = "simple") -> None: if task_name == "simple": from tasks.task_simple import create_env max_steps = 15 elif task_name == "multi_service": from tasks.task_multi_service import create_env max_steps = 12 elif task_name == "critical": from tasks.task_critical import create_env max_steps = 10 else: raise ValueError(f"Unknown task: {task_name!r}") env = create_env() state = env.reset() print( f"[START] task={task_name} env=debugops model={MODEL_NAME}", flush=True, ) action_history: List[str] = [] reward_history: List[float] = [] episode_logs: List[Dict[str, Any]] = [] done = False step = 0 while not done and step < max_steps: action = _call_llm(state, action_history, reward_history) try: next_state, reward, done, info = env.step(action) error = "null" except Exception as exc: next_state, reward, done, info = state, 0.0, True, {} error = type(exc).__name__ action_history.append(action) reward_history.append(reward) episode_logs.append({"reward": reward, "info": info}) print( f"[STEP] step={step} action={action} reward={round(reward, 3)} " f"done={str(done).lower()} error={error}", flush=True, ) # Early exit on explicit success flag if info.get("success", False): done = True state = next_state step += 1 result = evaluate_episode(episode_logs, max_steps=max_steps) score = result["score"] # already in [0, 1] success = result["resolved"] print( f"[END] success={str(success).lower()} steps={step} " f"score={round(score, 3)} " f"rewards={','.join(str(round(e['reward'], 3)) for e in episode_logs)}", flush=True, ) if __name__ == "__main__": for task in ["simple", "multi_service", "critical"]: run_episode(task)