Spaces:
Sleeping
Sleeping
Pranav Dhiran commited on
Commit Β·
e338f89
1
Parent(s): 7c1c452
phase 2
Browse files- Dockerfile +1 -0
- README.md +64 -1
- inference.py +110 -32
- requirements.txt +2 -0
Dockerfile
CHANGED
|
@@ -28,6 +28,7 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|
| 28 |
COPY app/ ./app/
|
| 29 |
COPY openenv.yaml .
|
| 30 |
COPY baseline.py .
|
|
|
|
| 31 |
|
| 32 |
# Ensure correct ownership
|
| 33 |
RUN chown -R appuser:appuser /app
|
|
|
|
| 28 |
COPY app/ ./app/
|
| 29 |
COPY openenv.yaml .
|
| 30 |
COPY baseline.py .
|
| 31 |
+
COPY inference.py .
|
| 32 |
|
| 33 |
# Ensure correct ownership
|
| 34 |
RUN chown -R appuser:appuser /app
|
README.md
CHANGED
|
@@ -12,4 +12,67 @@ tags:
|
|
| 12 |
|
| 13 |
# SRE Incident Response - OpenEnv Environment
|
| 14 |
|
| 15 |
-
This
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
# SRE Incident Response - OpenEnv Environment
|
| 14 |
|
| 15 |
+
This repository provides an **OpenEnv-compatible** environment where an agent acts as an on-call **Site Reliability Engineer (SRE)**. The environment exposes a simple HTTP API (FastAPI) that supports episodic rollouts via `/reset` and `/step`, plus grading via `/grader`.
|
| 16 |
+
|
| 17 |
+
## Whatβs in this repo
|
| 18 |
+
|
| 19 |
+
- **Environment server**: `app/main.py` (FastAPI, OpenEnv-style endpoints)
|
| 20 |
+
- **Task logic**: `app/tasks/` (three incident scenarios)
|
| 21 |
+
- **Inference runner**: `inference.py`
|
| 22 |
+
- Uses an OpenAI model if `OPENAI_API_KEY` is set
|
| 23 |
+
- Otherwise falls back to a deterministic, no-network policy
|
| 24 |
+
|
| 25 |
+
## Run locally (Docker)
|
| 26 |
+
|
| 27 |
+
Build and run:
|
| 28 |
+
|
| 29 |
+
```bash
|
| 30 |
+
docker build -t sre-incident-env .
|
| 31 |
+
docker run --rm -p 7860:7860 sre-incident-env
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
Then check:
|
| 35 |
+
|
| 36 |
+
```bash
|
| 37 |
+
curl http://localhost:7860/health
|
| 38 |
+
curl http://localhost:7860/tasks
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
## Run locally (Python)
|
| 42 |
+
|
| 43 |
+
Install:
|
| 44 |
+
|
| 45 |
+
```bash
|
| 46 |
+
pip install -r requirements.txt
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
Start the server:
|
| 50 |
+
|
| 51 |
+
```bash
|
| 52 |
+
python -m uvicorn app.main:app --host 0.0.0.0 --port 7860 --workers 1
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
## Inference / evaluation
|
| 56 |
+
|
| 57 |
+
Run inference against a running environment:
|
| 58 |
+
|
| 59 |
+
```bash
|
| 60 |
+
python inference.py --base-url http://localhost:7860
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
Notes:
|
| 64 |
+
|
| 65 |
+
- **Exit codes**: by default `inference.py` exits **0** if it completes (even if tasks fail), to avoid βrunner failedβ false negatives. Use `--strict-exit` if you want non-zero on failed tasks.
|
| 66 |
+
- **Auto-start**: if `--base-url` is `http://localhost:7860` and the server isnβt running, `inference.py` will try to start the local server automatically.
|
| 67 |
+
|
| 68 |
+
## Hugging Face Spaces
|
| 69 |
+
|
| 70 |
+
This repo is set up for **Docker Spaces** (see `Dockerfile`). The server binds to port **7860**, and `/health` is used as a health check.
|
| 71 |
+
|
| 72 |
+
## Meta x PyTorch Hackathon submission notes
|
| 73 |
+
|
| 74 |
+
If the hackathon evaluator runs `inference.py` directly, this repo is designed to:
|
| 75 |
+
|
| 76 |
+
- Install cleanly from `requirements.txt`
|
| 77 |
+
- Bring up the environment server (Docker or local)
|
| 78 |
+
- Run `inference.py` without crashing or returning a non-zero code just because a baseline policy didnβt βpassβ
|
inference.py
CHANGED
|
@@ -26,6 +26,8 @@ import json
|
|
| 26 |
import re
|
| 27 |
import argparse
|
| 28 |
import time
|
|
|
|
|
|
|
| 29 |
from typing import Optional
|
| 30 |
|
| 31 |
import httpx
|
|
@@ -186,6 +188,72 @@ def call_llm(client: httpx.Client, model: str, messages: list) -> str:
|
|
| 186 |
raise Exception(f"Unexpected response format from OpenAI API: {e}")
|
| 187 |
|
| 188 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
def parse_action(text: str) -> dict:
|
| 190 |
"""Parse JSON action from LLM output, with fallback."""
|
| 191 |
text = text.strip()
|
|
@@ -421,6 +489,11 @@ def main():
|
|
| 421 |
help="Tasks to run (task1, task2, task3)")
|
| 422 |
parser.add_argument("--quiet", action="store_true", help="Suppress step-by-step output")
|
| 423 |
parser.add_argument("--output", help="Save results to JSON file")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
args = parser.parse_args()
|
| 425 |
|
| 426 |
if not OPENAI_API_KEY:
|
|
@@ -436,41 +509,42 @@ def main():
|
|
| 436 |
safe_print(f" MaxSteps: {args.max_steps}")
|
| 437 |
safe_print(f"{HR_THICK*60}")
|
| 438 |
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
health = env_client.get("/health")
|
| 443 |
health.raise_for_status()
|
| 444 |
ok = "β" if UNICODE_OK else "+"
|
| 445 |
safe_print(f"\n {ok} Environment healthy: {health.json()}")
|
| 446 |
-
except httpx.RequestError as e:
|
| 447 |
-
x = "β" if UNICODE_OK else "x"
|
| 448 |
-
safe_print(f"\n {x} Environment not reachable at {args.base_url}: Network error - {e}")
|
| 449 |
-
sys.exit(1)
|
| 450 |
-
except httpx.HTTPStatusError as e:
|
| 451 |
-
x = "β" if UNICODE_OK else "x"
|
| 452 |
-
safe_print(f"\n {x} Environment health check failed (status {e.response.status_code}): {e.response.text}")
|
| 453 |
-
sys.exit(1)
|
| 454 |
-
except Exception as e:
|
| 455 |
-
x = "β" if UNICODE_OK else "x"
|
| 456 |
-
safe_print(f"\n {x} Environment not reachable at {args.base_url}: {e}")
|
| 457 |
-
sys.exit(1)
|
| 458 |
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
|
| 475 |
# ββ Summary βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 476 |
elapsed = time.time() - start
|
|
@@ -520,8 +594,12 @@ def main():
|
|
| 520 |
except Exception as e:
|
| 521 |
safe_print(f" WARN: Could not write results file: {e}")
|
| 522 |
|
| 523 |
-
# Exit code:
|
| 524 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 525 |
|
| 526 |
|
| 527 |
if __name__ == "__main__":
|
|
|
|
| 26 |
import re
|
| 27 |
import argparse
|
| 28 |
import time
|
| 29 |
+
import subprocess
|
| 30 |
+
import signal
|
| 31 |
from typing import Optional
|
| 32 |
|
| 33 |
import httpx
|
|
|
|
| 188 |
raise Exception(f"Unexpected response format from OpenAI API: {e}")
|
| 189 |
|
| 190 |
|
| 191 |
+
def _is_localhost_url(url: str) -> bool:
|
| 192 |
+
u = (url or "").strip().lower()
|
| 193 |
+
return u.startswith("http://localhost") or u.startswith("http://127.0.0.1")
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def _wait_for_health(base_url: str, timeout_s: float = 20.0) -> bool:
|
| 197 |
+
deadline = time.time() + timeout_s
|
| 198 |
+
last_err: Optional[Exception] = None
|
| 199 |
+
while time.time() < deadline:
|
| 200 |
+
try:
|
| 201 |
+
with httpx.Client(base_url=base_url, timeout=2.5) as c:
|
| 202 |
+
r = c.get("/health")
|
| 203 |
+
r.raise_for_status()
|
| 204 |
+
return True
|
| 205 |
+
except Exception as e:
|
| 206 |
+
last_err = e
|
| 207 |
+
time.sleep(0.4)
|
| 208 |
+
if last_err:
|
| 209 |
+
log(f"Health check still failing: {last_err}", "WARN")
|
| 210 |
+
return False
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def _start_local_server() -> subprocess.Popen:
|
| 214 |
+
"""
|
| 215 |
+
Start the environment server in a subprocess.
|
| 216 |
+
Intended for runners that execute inference without already running the env.
|
| 217 |
+
"""
|
| 218 |
+
cmd = [
|
| 219 |
+
sys.executable,
|
| 220 |
+
"-m",
|
| 221 |
+
"uvicorn",
|
| 222 |
+
"app.main:app",
|
| 223 |
+
"--host",
|
| 224 |
+
"127.0.0.1",
|
| 225 |
+
"--port",
|
| 226 |
+
"7860",
|
| 227 |
+
"--workers",
|
| 228 |
+
"1",
|
| 229 |
+
]
|
| 230 |
+
kwargs = {}
|
| 231 |
+
if os.name == "nt":
|
| 232 |
+
# Avoid CTRL-C propagation weirdness on Windows runners.
|
| 233 |
+
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined]
|
| 234 |
+
return subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, **kwargs)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def _stop_local_server(p: subprocess.Popen):
|
| 238 |
+
try:
|
| 239 |
+
if p.poll() is not None:
|
| 240 |
+
return
|
| 241 |
+
if os.name == "nt":
|
| 242 |
+
p.send_signal(signal.CTRL_BREAK_EVENT) # type: ignore[attr-defined]
|
| 243 |
+
try:
|
| 244 |
+
p.wait(timeout=5)
|
| 245 |
+
return
|
| 246 |
+
except Exception:
|
| 247 |
+
pass
|
| 248 |
+
p.terminate()
|
| 249 |
+
try:
|
| 250 |
+
p.wait(timeout=5)
|
| 251 |
+
except Exception:
|
| 252 |
+
p.kill()
|
| 253 |
+
except Exception:
|
| 254 |
+
pass
|
| 255 |
+
|
| 256 |
+
|
| 257 |
def parse_action(text: str) -> dict:
|
| 258 |
"""Parse JSON action from LLM output, with fallback."""
|
| 259 |
text = text.strip()
|
|
|
|
| 489 |
help="Tasks to run (task1, task2, task3)")
|
| 490 |
parser.add_argument("--quiet", action="store_true", help="Suppress step-by-step output")
|
| 491 |
parser.add_argument("--output", help="Save results to JSON file")
|
| 492 |
+
parser.add_argument(
|
| 493 |
+
"--strict-exit",
|
| 494 |
+
action="store_true",
|
| 495 |
+
help="Exit non-zero when not all tasks pass (default: exit 0 if script completes).",
|
| 496 |
+
)
|
| 497 |
args = parser.parse_args()
|
| 498 |
|
| 499 |
if not OPENAI_API_KEY:
|
|
|
|
| 509 |
safe_print(f" MaxSteps: {args.max_steps}")
|
| 510 |
safe_print(f"{HR_THICK*60}")
|
| 511 |
|
| 512 |
+
results = []
|
| 513 |
+
start = time.time()
|
| 514 |
+
|
| 515 |
+
server_proc: Optional[subprocess.Popen] = None
|
| 516 |
+
try:
|
| 517 |
+
# Verify environment is reachable; auto-start local server if needed.
|
| 518 |
+
if not _wait_for_health(args.base_url, timeout_s=3.0) and _is_localhost_url(args.base_url):
|
| 519 |
+
log("Environment not reachable; starting local server...", "WARN")
|
| 520 |
+
server_proc = _start_local_server()
|
| 521 |
+
|
| 522 |
+
if not _wait_for_health(args.base_url, timeout_s=20.0):
|
| 523 |
+
x = "β" if UNICODE_OK else "x"
|
| 524 |
+
safe_print(f"\n {x} Environment not reachable at {args.base_url}")
|
| 525 |
+
sys.exit(1)
|
| 526 |
+
|
| 527 |
+
with httpx.Client(base_url=args.base_url, timeout=30.0) as env_client:
|
| 528 |
health = env_client.get("/health")
|
| 529 |
health.raise_for_status()
|
| 530 |
ok = "β" if UNICODE_OK else "+"
|
| 531 |
safe_print(f"\n {ok} Environment healthy: {health.json()}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
|
| 533 |
+
with httpx.Client(timeout=60.0) as llm_client:
|
| 534 |
+
for task_id in args.tasks:
|
| 535 |
+
result = run_task(
|
| 536 |
+
env_client=env_client,
|
| 537 |
+
llm_client=llm_client,
|
| 538 |
+
task_id=task_id,
|
| 539 |
+
model=args.model,
|
| 540 |
+
max_steps=args.max_steps,
|
| 541 |
+
verbose=not args.quiet,
|
| 542 |
+
)
|
| 543 |
+
results.append(result)
|
| 544 |
+
time.sleep(0.5) # Rate limiting courtesy
|
| 545 |
+
finally:
|
| 546 |
+
if server_proc is not None:
|
| 547 |
+
_stop_local_server(server_proc)
|
| 548 |
|
| 549 |
# ββ Summary βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 550 |
elapsed = time.time() - start
|
|
|
|
| 594 |
except Exception as e:
|
| 595 |
safe_print(f" WARN: Could not write results file: {e}")
|
| 596 |
|
| 597 |
+
# Exit code:
|
| 598 |
+
# - default: 0 if script ran to completion (so runners don't treat "failed tasks" as a crash)
|
| 599 |
+
# - strict: 0 only if all tasks pass
|
| 600 |
+
if args.strict_exit:
|
| 601 |
+
sys.exit(0 if passed == len(results) else 1)
|
| 602 |
+
sys.exit(0)
|
| 603 |
|
| 604 |
|
| 605 |
if __name__ == "__main__":
|
requirements.txt
CHANGED
|
@@ -4,3 +4,5 @@ pydantic==2.10.3
|
|
| 4 |
httpx==0.28.1
|
| 5 |
python-multipart==0.0.19
|
| 6 |
pyyaml==6.0.2
|
|
|
|
|
|
|
|
|
| 4 |
httpx==0.28.1
|
| 5 |
python-multipart==0.0.19
|
| 6 |
pyyaml==6.0.2
|
| 7 |
+
openenv-core>=0.2.0
|
| 8 |
+
rich>=13.7.0
|