Pranav Dhiran commited on
Commit
3371bd8
Β·
1 Parent(s): 1fd028d
Files changed (1) hide show
  1. inference.py +401 -0
inference.py ADDED
@@ -0,0 +1,401 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Inference Script β€” SRE Incident Response OpenEnv
4
+ =================================================
5
+ Runs a ReAct-style OpenAI agent against all three tasks and
6
+ reports reproducible baseline scores.
7
+
8
+ Usage:
9
+ export OPENAI_API_KEY="sk-..."
10
+ export OPENENV_BASE_URL="http://localhost:7860" # or your HF Space URL
11
+ python inference.py
12
+
13
+ # Run specific tasks:
14
+ python inference.py --tasks task1 task2
15
+
16
+ # Use a different model:
17
+ python inference.py --model gpt-4o
18
+
19
+ Requirements:
20
+ pip install openai httpx rich
21
+ """
22
+
23
+ import os
24
+ import sys
25
+ import json
26
+ import re
27
+ import argparse
28
+ import time
29
+ from typing import Optional
30
+
31
+ import httpx
32
+
33
+ try:
34
+ from rich.console import Console
35
+ from rich.table import Table
36
+ from rich.panel import Panel
37
+ from rich import print as rprint
38
+ RICH = True
39
+ except ImportError:
40
+ RICH = False
41
+ Console = None
42
+
43
+
44
+ # ─── Config ──────────────────────────────────────────────────────────────────
45
+
46
+ DEFAULT_MODEL = "gpt-4o-mini"
47
+ DEFAULT_BASE_URL = os.environ.get("OPENENV_BASE_URL", "http://localhost:7860")
48
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
49
+
50
+ SYSTEM_PROMPT = """You are an expert Site Reliability Engineer (SRE) responding to a production incident.
51
+ You will receive alerts, service statuses, and investigation results.
52
+ Your goal is to identify the root cause and resolve the incident efficiently.
53
+
54
+ At each step, respond with ONLY a valid JSON object in this exact format:
55
+ {"action_type": "<action>", "parameters": {<params>}}
56
+
57
+ Available actions:
58
+ - query_logs: {"service": "<name>"} β€” fetch recent logs for a service
59
+ - check_metrics: {"service": "<name>"} β€” get current metrics for a service
60
+ - check_config: {"service": "<name>"} β€” inspect live runtime configuration
61
+ - restart_service: {"service": "<name>"} β€” restart a service (use carefully)
62
+ - rollback_deployment: {"service": "<name>"} β€” roll back to previous version
63
+ - kill_query: {"source": "<service>"} β€” terminate long-running DB queries from a source
64
+ - scale_service: {"service": "<name>", "replicas": <int>} β€” change replica count
65
+ - examine_trace: {"trace_id": "<id>"} β€” examine distributed trace
66
+ - acknowledge_alert: {"alert_id": "<id>"} β€” acknowledge an alert
67
+ - resolve_incident: {} β€” mark incident as resolved (only when services are healthy)
68
+
69
+ SRE Investigation Strategy:
70
+ 1. Read ALL alerts and service statuses carefully
71
+ 2. Look at recent deployments β€” they are often correlated with incidents
72
+ 3. Use query_logs and check_metrics to gather evidence before acting
73
+ 4. Form a clear hypothesis about the root cause
74
+ 5. Apply the most targeted fix (prefer rollback over restart when deployment changed)
75
+ 6. Verify all affected services are healthy
76
+ 7. Call resolve_incident to complete the episode
77
+
78
+ Respond ONLY with JSON. No markdown. No explanation."""
79
+
80
+
81
+ # ─── Helpers ─────────────────────────────────────────────────────────────────
82
+
83
+ def log(msg: str, level: str = "INFO"):
84
+ prefix = {"INFO": "β„Ή", "OK": "βœ“", "WARN": "⚠", "ERR": "βœ—"}.get(level, "β€’")
85
+ print(f" {prefix} {msg}")
86
+
87
+
88
+ def format_observation(obs: dict) -> str:
89
+ """Format observation dict into a concise prompt string."""
90
+ lines = [f"=== INCIDENT β€” Step {obs.get('step', 0)} ===\n"]
91
+
92
+ lines.append("ACTIVE ALERTS:")
93
+ for alert in obs.get("alerts", []):
94
+ ack = " [ACK]" if alert.get("acknowledged") else ""
95
+ sev = alert.get("severity", "?").upper()
96
+ lines.append(f" [{sev}]{ack} {alert.get('service')}: {alert.get('message')}")
97
+
98
+ lines.append("\nSERVICE STATUS:")
99
+ for name, svc in obs.get("services", {}).items():
100
+ conn = ""
101
+ if svc.get("connections") is not None:
102
+ conn = f" | conns: {svc['connections']}/{svc.get('max_connections', '?')}"
103
+ lines.append(
104
+ f" {name}: {svc.get('status', '?').upper()} | "
105
+ f"cpu: {svc.get('cpu_percent', 0):.1f}% | "
106
+ f"mem: {svc.get('memory_percent', 0):.1f}% | "
107
+ f"errors: {svc.get('error_rate', 0):.1f}/s | "
108
+ f"v{svc.get('version', '?')}{conn}"
109
+ )
110
+
111
+ if obs.get("recent_deployments"):
112
+ lines.append("\nRECENT DEPLOYMENTS:")
113
+ for dep in obs["recent_deployments"]:
114
+ lines.append(
115
+ f" {dep.get('service')}: v{dep.get('previous', '?')} β†’ "
116
+ f"v{dep.get('version')} deployed at {dep.get('deployed_at')}"
117
+ )
118
+
119
+ if obs.get("message"):
120
+ lines.append(f"\nLAST ACTION RESULT:\n{obs['message']}")
121
+
122
+ if obs.get("runbook_hints"):
123
+ lines.append("\nRUNBOOK HINTS:")
124
+ for h in obs["runbook_hints"]:
125
+ lines.append(f" β€’ {h}")
126
+
127
+ return "\n".join(lines)
128
+
129
+
130
+ def call_llm(client: httpx.Client, model: str, messages: list) -> str:
131
+ """Call OpenAI chat completions API."""
132
+ response = client.post(
133
+ "https://api.openai.com/v1/chat/completions",
134
+ headers={
135
+ "Authorization": f"Bearer {OPENAI_API_KEY}",
136
+ "Content-Type": "application/json",
137
+ },
138
+ json={
139
+ "model": model,
140
+ "messages": messages,
141
+ "max_tokens": 200,
142
+ "temperature": 0.0,
143
+ },
144
+ timeout=30.0,
145
+ )
146
+ response.raise_for_status()
147
+ return response.json()["choices"][0]["message"]["content"].strip()
148
+
149
+
150
+ def parse_action(text: str) -> dict:
151
+ """Parse JSON action from LLM output, with fallback."""
152
+ text = text.strip()
153
+ # Remove markdown code blocks if present
154
+ text = re.sub(r"```(?:json)?\s*|\s*```", "", text).strip()
155
+ try:
156
+ return json.loads(text)
157
+ except json.JSONDecodeError:
158
+ match = re.search(r'\{[^{}]*\}', text, re.DOTALL)
159
+ if match:
160
+ try:
161
+ return json.loads(match.group())
162
+ except json.JSONDecodeError:
163
+ pass
164
+ # Fallback: safe no-op
165
+ return {"action_type": "acknowledge_alert", "parameters": {"alert_id": "ALT-001"}}
166
+
167
+
168
+ # ─── Core Runner ─────────────────────────────────────────────────────────────
169
+
170
+ def run_task(
171
+ env_client: httpx.Client,
172
+ llm_client: httpx.Client,
173
+ task_id: str,
174
+ model: str,
175
+ max_steps: int,
176
+ verbose: bool = True,
177
+ ) -> dict:
178
+ """Run one complete episode for a task. Returns result dict."""
179
+
180
+ if verbose:
181
+ print(f"\n{'─'*60}")
182
+ print(f" Task: {task_id.upper()}")
183
+ print(f"{'─'*60}")
184
+
185
+ episode_log = []
186
+ score = 0.0
187
+ steps_taken = 0
188
+ session_id = None
189
+
190
+ try:
191
+ # ── Reset ────────────────────────────────────────────────────
192
+ reset_resp = env_client.post("/reset", json={"task_id": task_id, "seed": 42})
193
+ reset_resp.raise_for_status()
194
+ obs = reset_resp.json()
195
+ session_id = obs["session_id"]
196
+
197
+ if verbose:
198
+ task_name = obs.get("message", "").split("Task:")[1].split("(")[0].strip() \
199
+ if "Task:" in obs.get("message", "") else task_id
200
+ log(f"Session: {session_id[:8]}...", "INFO")
201
+ log(obs.get("message", ""), "INFO")
202
+
203
+ conversation = []
204
+ done = False
205
+
206
+ # ── Episode Loop ─────────────────────────────────────────────
207
+ for step_num in range(max_steps):
208
+ obs_text = format_observation(obs)
209
+ conversation.append({"role": "user", "content": obs_text})
210
+
211
+ # Trim conversation to last 4 turns (keep it focused)
212
+ messages = [{"role": "system", "content": SYSTEM_PROMPT}]
213
+ messages += conversation[-8:]
214
+
215
+ # Get action from LLM
216
+ action_text = call_llm(llm_client, model, messages)
217
+ conversation.append({"role": "assistant", "content": action_text})
218
+
219
+ action_dict = parse_action(action_text)
220
+ action_type = action_dict.get("action_type", "unknown")
221
+ parameters = action_dict.get("parameters", {})
222
+
223
+ if verbose:
224
+ params_str = json.dumps(parameters) if parameters else "{}"
225
+ print(f" Step {step_num+1:2d}: {action_type}({params_str})", end="")
226
+
227
+ # Take step
228
+ step_resp = env_client.post("/step", json={
229
+ "session_id": session_id,
230
+ "action": {"action_type": action_type, "parameters": parameters},
231
+ })
232
+ step_resp.raise_for_status()
233
+ step_data = step_resp.json()
234
+
235
+ obs = step_data["observation"]
236
+ reward_val = step_data["reward"]["value"]
237
+ done = step_data["done"]
238
+ steps_taken = step_num + 1
239
+
240
+ if verbose:
241
+ reward_str = f"{reward_val:+.3f}"
242
+ current_score = step_data["info"].get("grader_score", 0.0)
243
+ print(f" β†’ reward: {reward_str} | score: {current_score:.3f}")
244
+
245
+ episode_log.append({
246
+ "step": step_num + 1,
247
+ "action_type": action_type,
248
+ "parameters": parameters,
249
+ "reward": reward_val,
250
+ "message_preview": obs.get("message", "")[:150],
251
+ })
252
+
253
+ if done:
254
+ break
255
+
256
+ # ── Get Final Grade ───────────────────────────────────────────
257
+ grader_resp = env_client.post("/grader", json={"session_id": session_id})
258
+ grader_resp.raise_for_status()
259
+ grader_data = grader_resp.json()
260
+ score = grader_data["score"]
261
+ breakdown = grader_data.get("breakdown", {})
262
+
263
+ if verbose:
264
+ print(f"\n {'─'*30}")
265
+ log(f"Final score: {score:.4f}", "OK" if score >= 0.6 else "WARN")
266
+ log(f"Steps taken: {steps_taken}", "INFO")
267
+ if breakdown:
268
+ log("Breakdown:", "INFO")
269
+ for k, v in breakdown.items():
270
+ print(f" {k}: {v:+.4f}")
271
+
272
+ except Exception as e:
273
+ if verbose:
274
+ log(f"Error: {e}", "ERR")
275
+ episode_log.append({"error": str(e)})
276
+
277
+ # Get task info for name/difficulty
278
+ tasks_resp = env_client.get("/tasks")
279
+ task_info = {}
280
+ if tasks_resp.status_code == 200:
281
+ for t in tasks_resp.json().get("tasks", []):
282
+ if t["task_id"] == task_id:
283
+ task_info = t
284
+ break
285
+
286
+ return {
287
+ "task_id": task_id,
288
+ "task_name": task_info.get("name", task_id),
289
+ "difficulty": task_info.get("difficulty", "?"),
290
+ "score": score,
291
+ "steps_taken": steps_taken,
292
+ "success": score >= task_info.get("passing_score", 0.6),
293
+ "episode_log": episode_log,
294
+ }
295
+
296
+
297
+ # ─── Main ─────────────────────────────────────────────────────────────────────
298
+
299
+ def main():
300
+ parser = argparse.ArgumentParser(
301
+ description="Run inference agent against SRE Incident Response environment"
302
+ )
303
+ parser.add_argument("--model", default=DEFAULT_MODEL, help="OpenAI model to use")
304
+ parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="Environment base URL")
305
+ parser.add_argument("--max-steps", type=int, default=12, help="Max steps per episode")
306
+ parser.add_argument("--tasks", nargs="+", default=["task1", "task2", "task3"],
307
+ help="Tasks to run (task1, task2, task3)")
308
+ parser.add_argument("--quiet", action="store_true", help="Suppress step-by-step output")
309
+ parser.add_argument("--output", help="Save results to JSON file")
310
+ args = parser.parse_args()
311
+
312
+ if not OPENAI_API_KEY:
313
+ print("ERROR: OPENAI_API_KEY environment variable not set.")
314
+ sys.exit(1)
315
+
316
+ print(f"\n{'═'*60}")
317
+ print(f" SRE Incident Response β€” Inference Evaluation")
318
+ print(f"{'═'*60}")
319
+ print(f" Model: {args.model}")
320
+ print(f" Env URL: {args.base_url}")
321
+ print(f" Tasks: {', '.join(args.tasks)}")
322
+ print(f" MaxSteps: {args.max_steps}")
323
+ print(f"{'═'*60}")
324
+
325
+ # Verify environment is reachable
326
+ with httpx.Client(base_url=args.base_url, timeout=30.0) as env_client:
327
+ try:
328
+ health = env_client.get("/health")
329
+ health.raise_for_status()
330
+ print(f"\n βœ“ Environment healthy: {health.json()}")
331
+ except Exception as e:
332
+ print(f"\n βœ— Environment not reachable at {args.base_url}: {e}")
333
+ sys.exit(1)
334
+
335
+ results = []
336
+ start = time.time()
337
+
338
+ with httpx.Client(timeout=60.0) as llm_client:
339
+ for task_id in args.tasks:
340
+ result = run_task(
341
+ env_client=env_client,
342
+ llm_client=llm_client,
343
+ task_id=task_id,
344
+ model=args.model,
345
+ max_steps=args.max_steps,
346
+ verbose=not args.quiet,
347
+ )
348
+ results.append(result)
349
+ time.sleep(0.5) # Rate limiting courtesy
350
+
351
+ # ── Summary ───────────────────────────────────────────────────────────────
352
+ elapsed = time.time() - start
353
+ mean_score = sum(r["score"] for r in results) / len(results) if results else 0.0
354
+ passed = sum(1 for r in results if r["success"])
355
+
356
+ print(f"\n{'═'*60}")
357
+ print(f" INFERENCE RESULTS SUMMARY")
358
+ print(f"{'═'*60}")
359
+ print(f" {'Task':<35} {'Diff':<8} {'Score':<8} {'Steps':<7} {'Status'}")
360
+ print(f" {'─'*55}")
361
+ for r in results:
362
+ status = "βœ“ PASS" if r["success"] else "βœ— FAIL"
363
+ print(
364
+ f" {r['task_name']:<35} {r['difficulty']:<8} "
365
+ f"{r['score']:.4f} {r['steps_taken']:<7} {status}"
366
+ )
367
+ print(f" {'─'*55}")
368
+ print(f" {'Mean Score':<35} {'':8} {mean_score:.4f}")
369
+ print(f" Tasks passed: {passed}/{len(results)}")
370
+ print(f" Elapsed: {elapsed:.1f}s")
371
+ print(f"{'═'*60}\n")
372
+
373
+ # ── Save results ──────────────────────────────────────────────────────────
374
+ output = {
375
+ "model": args.model,
376
+ "environment": "sre-incident-response",
377
+ "results": results,
378
+ "summary": {
379
+ "mean_score": round(mean_score, 4),
380
+ "tasks_passed": passed,
381
+ "total_tasks": len(results),
382
+ "elapsed_seconds": round(elapsed, 1),
383
+ },
384
+ }
385
+
386
+ if args.output:
387
+ with open(args.output, "w") as f:
388
+ json.dump(output, f, indent=2)
389
+ print(f" Results saved to {args.output}")
390
+ else:
391
+ # Always save a inference_results.json for reproducibility
392
+ with open("inference_results.json", "w") as f:
393
+ json.dump(output, f, indent=2)
394
+ print(f" Results saved to inference_results.json")
395
+
396
+ # Exit code: 0 if all tasks pass, 1 otherwise
397
+ sys.exit(0 if passed == len(results) else 1)
398
+
399
+
400
+ if __name__ == "__main__":
401
+ main()