Spaces:
Sleeping
Sleeping
Commit ·
11f8f10
1
Parent(s): 5b64237
updated inference
Browse files- .gitignore +2 -1
- Dockerfile +2 -19
- README.md +0 -62
- inference.py +68 -322
- openenv.yaml +35 -96
.gitignore
CHANGED
|
@@ -7,4 +7,5 @@ venv/
|
|
| 7 |
*.egg-info/
|
| 8 |
dist/
|
| 9 |
build/
|
| 10 |
-
.pytest_cache/
|
|
|
|
|
|
| 7 |
*.egg-info/
|
| 8 |
dist/
|
| 9 |
build/
|
| 10 |
+
.pytest_cache/
|
| 11 |
+
myenv/
|
Dockerfile
CHANGED
|
@@ -4,16 +4,6 @@
|
|
| 4 |
# ---------------------------------------------------------------
|
| 5 |
FROM python:3.11-slim
|
| 6 |
|
| 7 |
-
# Install system utilities for DevOps tasks
|
| 8 |
-
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 9 |
-
nginx \
|
| 10 |
-
docker.io \
|
| 11 |
-
systemctl \
|
| 12 |
-
curl \
|
| 13 |
-
git \
|
| 14 |
-
vim \
|
| 15 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 16 |
-
|
| 17 |
# Create non-root user for Hugging Face Spaces
|
| 18 |
RUN useradd -m -u 1000 appuser
|
| 19 |
|
|
@@ -26,17 +16,10 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|
| 26 |
# Copy application code
|
| 27 |
COPY --chown=appuser:appuser . .
|
| 28 |
|
| 29 |
-
# HF Spaces compatibility
|
| 30 |
-
RUN chmod +x /app/app.py 2>/dev/null || true
|
| 31 |
-
|
| 32 |
USER appuser
|
| 33 |
|
| 34 |
# Expose the port HF Spaces expects
|
| 35 |
EXPOSE 7860
|
| 36 |
|
| 37 |
-
#
|
| 38 |
-
|
| 39 |
-
CMD python -c "import requests; requests.get('http://localhost:7860/health')" || exit 1
|
| 40 |
-
|
| 41 |
-
# Start the FastAPI server
|
| 42 |
-
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1", "--reload"]
|
|
|
|
| 4 |
# ---------------------------------------------------------------
|
| 5 |
FROM python:3.11-slim
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
# Create non-root user for Hugging Face Spaces
|
| 8 |
RUN useradd -m -u 1000 appuser
|
| 9 |
|
|
|
|
| 16 |
# Copy application code
|
| 17 |
COPY --chown=appuser:appuser . .
|
| 18 |
|
|
|
|
|
|
|
|
|
|
| 19 |
USER appuser
|
| 20 |
|
| 21 |
# Expose the port HF Spaces expects
|
| 22 |
EXPOSE 7860
|
| 23 |
|
| 24 |
+
# Start the FastAPI server (No --reload in production)
|
| 25 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -97,65 +97,3 @@ $step = @{
|
|
| 97 |
} | ConvertTo-Json -Depth 5
|
| 98 |
|
| 99 |
Invoke-WebRequest -Uri "http://127.0.0.1:7860/step" -Method POST -ContentType "application/json" -Body $step
|
| 100 |
-
|
| 101 |
-
## Test With LLM (OpenAI Key)
|
| 102 |
-
|
| 103 |
-
1) Keep API server running.
|
| 104 |
-
2) Set key and run inference:
|
| 105 |
-
|
| 106 |
-
PowerShell:
|
| 107 |
-
|
| 108 |
-
$env:OPENAI_API_KEY = "your-openai-key"
|
| 109 |
-
python inference.py --task task1 --model gpt-4o-mini
|
| 110 |
-
|
| 111 |
-
You should see step logs, rewards, and a grader score.
|
| 112 |
-
|
| 113 |
-
## Test With Gemini API Key
|
| 114 |
-
|
| 115 |
-
inference.py now supports OpenAI-compatible base URLs.
|
| 116 |
-
|
| 117 |
-
Use Gemini via OpenAI-compatible endpoint:
|
| 118 |
-
|
| 119 |
-
PowerShell:
|
| 120 |
-
|
| 121 |
-
$env:GEMINI_API_KEY = "your-gemini-key"
|
| 122 |
-
$env:OPENAI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
|
| 123 |
-
python inference.py --task task1 --model gemini-2.5-flash
|
| 124 |
-
|
| 125 |
-
Notes:
|
| 126 |
-
- You can also use OPENAI_API_KEY instead of GEMINI_API_KEY.
|
| 127 |
-
- If your model name is unavailable, switch to a Gemini model enabled on your key.
|
| 128 |
-
- Keep the environment server running at http://127.0.0.1:7860 (or pass --api-url).
|
| 129 |
-
|
| 130 |
-
## Docker
|
| 131 |
-
|
| 132 |
-
Build:
|
| 133 |
-
|
| 134 |
-
docker build -t devopsenv .
|
| 135 |
-
|
| 136 |
-
Run:
|
| 137 |
-
|
| 138 |
-
docker run -p 7860:7860 devopsenv
|
| 139 |
-
|
| 140 |
-
Then open:
|
| 141 |
-
- http://127.0.0.1:7860/health
|
| 142 |
-
- http://127.0.0.1:7860/docs
|
| 143 |
-
|
| 144 |
-
## Project Files
|
| 145 |
-
|
| 146 |
-
- app.py: FastAPI API
|
| 147 |
-
- environment.py: episode logic and simulator
|
| 148 |
-
- graders.py: deterministic scoring
|
| 149 |
-
- data.py: task metadata
|
| 150 |
-
- models.py: Pydantic schemas
|
| 151 |
-
- inference.py: LLM baseline runner
|
| 152 |
-
- test_integration.py: local end-to-end check
|
| 153 |
-
|
| 154 |
-
## Troubleshooting
|
| 155 |
-
|
| 156 |
-
- Port already in use:
|
| 157 |
-
- change server port or stop old process.
|
| 158 |
-
- 400/404 from API:
|
| 159 |
-
- check episode_id and task_id values.
|
| 160 |
-
- LLM errors:
|
| 161 |
-
- verify API key, model name, and OPENAI_BASE_URL for Gemini.
|
|
|
|
| 97 |
} | ConvertTo-Json -Depth 5
|
| 98 |
|
| 99 |
Invoke-WebRequest -Uri "http://127.0.0.1:7860/step" -Method POST -ContentType "application/json" -Body $step
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
inference.py
CHANGED
|
@@ -1,352 +1,98 @@
|
|
| 1 |
"""
|
| 2 |
Baseline LLM inference agent for DevOpsEnv.
|
| 3 |
-
|
| 4 |
-
This script reads an OpenEnv environment's state() and uses an LLM to generate
|
| 5 |
-
actions that solve the DevOps tasks.
|
| 6 |
-
|
| 7 |
-
Usage:
|
| 8 |
-
python inference.py --task task1 --model gpt-4 --hf-token <token>
|
| 9 |
"""
|
| 10 |
import os
|
| 11 |
-
import sys
|
| 12 |
import json
|
| 13 |
import argparse
|
| 14 |
-
import time
|
| 15 |
-
from pathlib import Path
|
| 16 |
-
from typing import Optional
|
| 17 |
-
|
| 18 |
import requests
|
| 19 |
-
from google import genai
|
| 20 |
from openai import OpenAI
|
| 21 |
|
| 22 |
-
#
|
| 23 |
-
def _load_dotenv_from_workspace() -> None:
|
| 24 |
-
"""Load KEY=VALUE pairs from .env into os.environ without overriding existing vars."""
|
| 25 |
-
dotenv_path = Path(__file__).resolve().parent / ".env"
|
| 26 |
-
if not dotenv_path.exists():
|
| 27 |
-
return
|
| 28 |
-
|
| 29 |
-
for raw_line in dotenv_path.read_text(encoding="utf-8").splitlines():
|
| 30 |
-
line = raw_line.strip()
|
| 31 |
-
if not line or line.startswith("#"):
|
| 32 |
-
continue
|
| 33 |
-
if line.startswith("export "):
|
| 34 |
-
line = line[7:].strip()
|
| 35 |
-
if "=" not in line:
|
| 36 |
-
continue
|
| 37 |
-
|
| 38 |
-
key, value = line.split("=", 1)
|
| 39 |
-
key = key.strip()
|
| 40 |
-
value = value.strip()
|
| 41 |
-
if not key:
|
| 42 |
-
continue
|
| 43 |
-
|
| 44 |
-
# Remove surrounding quotes if present.
|
| 45 |
-
if (value.startswith('"') and value.endswith('"')) or (
|
| 46 |
-
value.startswith("'") and value.endswith("'")
|
| 47 |
-
):
|
| 48 |
-
value = value[1:-1]
|
| 49 |
-
|
| 50 |
-
os.environ.setdefault(key, value)
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
_load_dotenv_from_workspace()
|
| 54 |
-
|
| 55 |
-
# Read config from environment/.env
|
| 56 |
API_BASE_URL = os.environ.get("API_BASE_URL", "http://localhost:7860")
|
| 57 |
-
MODEL_NAME = os.environ.get("MODEL_NAME", "gpt-4")
|
| 58 |
-
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 59 |
-
OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "")
|
| 60 |
-
GEMINI_DEFAULT_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3-flash-preview")
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
def _get_openai_client() -> OpenAI:
|
| 64 |
-
"""Create an OpenAI-compatible client for OpenAI-style chat completions."""
|
| 65 |
-
api_key = os.environ.get("OPENAI_API_KEY", "sk-test")
|
| 66 |
-
client_kwargs = {"api_key": api_key}
|
| 67 |
-
if OPENAI_BASE_URL:
|
| 68 |
-
client_kwargs["base_url"] = OPENAI_BASE_URL
|
| 69 |
-
return OpenAI(**client_kwargs)
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
def _get_gemini_client() -> genai.Client:
|
| 73 |
-
"""Create a Gemini client using the official google-genai SDK."""
|
| 74 |
-
api_key = os.environ.get("GEMINI_API_KEY") or os.environ.get("OPENAI_API_KEY", "")
|
| 75 |
-
if not api_key:
|
| 76 |
-
raise ValueError("GEMINI_API_KEY is required for Gemini models")
|
| 77 |
-
return genai.Client(api_key=api_key)
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def _is_gemini_model(model: str) -> bool:
|
| 81 |
-
"""Detect whether the requested model should use the Gemini SDK path."""
|
| 82 |
-
m = (model or "").lower()
|
| 83 |
-
return "gemini" in m
|
| 84 |
|
|
|
|
|
|
|
| 85 |
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
if not m or m.lower() == "gemini":
|
| 90 |
-
return GEMINI_DEFAULT_MODEL
|
| 91 |
-
return m
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
def _generate_action_text(
|
| 95 |
-
model: str,
|
| 96 |
-
system_prompt: str,
|
| 97 |
-
user_prompt: str,
|
| 98 |
-
openai_client: Optional[OpenAI],
|
| 99 |
-
gemini_client: Optional[genai.Client],
|
| 100 |
-
) -> str:
|
| 101 |
-
"""Generate model output text using Gemini SDK or OpenAI-compatible chat."""
|
| 102 |
-
if _is_gemini_model(model):
|
| 103 |
-
if gemini_client is None:
|
| 104 |
-
raise ValueError("Gemini client was not initialized")
|
| 105 |
-
gemini_model = _resolve_gemini_model(model)
|
| 106 |
-
combined_prompt = (
|
| 107 |
-
f"System instructions:\n{system_prompt}\n\n"
|
| 108 |
-
f"User request:\n{user_prompt}"
|
| 109 |
-
)
|
| 110 |
-
response = gemini_client.models.generate_content(
|
| 111 |
-
model=gemini_model,
|
| 112 |
-
contents=combined_prompt,
|
| 113 |
-
)
|
| 114 |
-
return response.text or ""
|
| 115 |
-
|
| 116 |
-
if openai_client is None:
|
| 117 |
-
raise ValueError("OpenAI client was not initialized")
|
| 118 |
-
|
| 119 |
-
response = openai_client.chat.completions.create(
|
| 120 |
-
model=model,
|
| 121 |
-
messages=[
|
| 122 |
-
{"role": "system", "content": system_prompt},
|
| 123 |
-
{"role": "user", "content": user_prompt},
|
| 124 |
-
],
|
| 125 |
-
temperature=0.3,
|
| 126 |
-
max_tokens=1000,
|
| 127 |
-
)
|
| 128 |
-
return response.choices[0].message.content or ""
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
def send_request(method: str, endpoint: str, **kwargs):
|
| 132 |
-
"""Send HTTP request to the environment server."""
|
| 133 |
-
url = f"{API_BASE_URL}{endpoint}"
|
| 134 |
-
response = requests.request(method, url, timeout=10, **kwargs)
|
| 135 |
-
response.raise_for_status()
|
| 136 |
-
return response.json()
|
| 137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
-
def run_agent(task_id: str
|
| 140 |
-
"""Run the agent on a specific task."""
|
| 141 |
-
model = model or MODEL_NAME
|
| 142 |
-
if _is_gemini_model(model):
|
| 143 |
-
model = _resolve_gemini_model(model)
|
| 144 |
-
openai_client: Optional[OpenAI] = None
|
| 145 |
-
gemini_client: Optional[genai.Client] = None
|
| 146 |
-
if _is_gemini_model(model):
|
| 147 |
-
gemini_client = _get_gemini_client()
|
| 148 |
-
else:
|
| 149 |
-
openai_client = _get_openai_client()
|
| 150 |
-
|
| 151 |
-
# Initialize episode
|
| 152 |
print(f"\n{'='*60}")
|
| 153 |
-
print(f"
|
| 154 |
-
print(f"
|
| 155 |
print(f"{'='*60}\n")
|
| 156 |
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
print(f"Max Steps: {max_steps}")
|
| 163 |
-
print(f"\nTask: {obs['task_description']}\n")
|
| 164 |
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
|
|
|
| 168 |
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
# Prepare prompt for LLM
|
| 176 |
-
system_prompt = """You are an expert Linux DevOps engineer/SRE.
|
| 177 |
-
Your job is to diagnose and fix broken systems using bash commands and file edits.
|
| 178 |
-
You are interacting with a simulated Linux environment.
|
| 179 |
-
|
| 180 |
-
Available actions:
|
| 181 |
-
1. bash_cmd: Execute a bash command
|
| 182 |
-
2. file_edit: Edit a file
|
| 183 |
-
3. submit: Submit when the task is complete
|
| 184 |
-
|
| 185 |
-
Respond in JSON format with this structure:
|
| 186 |
-
{
|
| 187 |
-
"action_type": "bash_cmd" | "file_edit" | "submit",
|
| 188 |
-
"command": "command to execute" (if bash_cmd),
|
| 189 |
-
"file_path": "/path/to/file" (if file_edit),
|
| 190 |
-
"file_content": "new file content" (if file_edit),
|
| 191 |
-
"summary": "why you're taking this action"
|
| 192 |
-
}
|
| 193 |
-
|
| 194 |
-
Be strategic:
|
| 195 |
-
- Start by diagnosing the system
|
| 196 |
-
- Use ps, systemctl, curl, etc. to understand issues
|
| 197 |
-
- Fix the root cause
|
| 198 |
-
- Submit when done
|
| 199 |
-
"""
|
| 200 |
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
{json.dumps(obs['system_state'], indent=2)}
|
| 209 |
-
|
| 210 |
-
Previous actions: {len(state['history'])} taken so far
|
| 211 |
-
|
| 212 |
-
History of commands:
|
| 213 |
-
{json.dumps(state['history'][-3:], indent=2) if state['history'] else 'None yet'}
|
| 214 |
-
|
| 215 |
-
What should I do next? Think step-by-step about what the issue is and how to fix it.
|
| 216 |
-
"""
|
| 217 |
|
| 218 |
try:
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
|
|
|
| 226 |
)
|
| 227 |
-
try:
|
| 228 |
-
# Try to extract JSON from response
|
| 229 |
-
if "```json" in response_text:
|
| 230 |
-
json_str = response_text.split("```json")[1].split("```")[0]
|
| 231 |
-
elif "```" in response_text:
|
| 232 |
-
json_str = response_text.split("```")[1].split("```")[0]
|
| 233 |
-
else:
|
| 234 |
-
json_str = response_text
|
| 235 |
-
|
| 236 |
-
action_data = json.loads(json_str)
|
| 237 |
-
except (json.JSONDecodeError, IndexError):
|
| 238 |
-
print(f"Failed to parse LLM response: {response_text[:100]}")
|
| 239 |
-
# Fallback to simple diagnosis
|
| 240 |
-
action_data = {"action_type": "bash_cmd", "command": "ps aux"}
|
| 241 |
-
|
| 242 |
-
except Exception as e:
|
| 243 |
-
print(f"LLM error: {e}. Falling back to heuristic...")
|
| 244 |
-
# Fallback heuristic actions
|
| 245 |
-
if step_count == 1:
|
| 246 |
-
action_data = {"action_type": "bash_cmd", "command": "systemctl status nginx"}
|
| 247 |
-
else:
|
| 248 |
-
action_data = {"action_type": "submit", "summary": "Diagnostics complete"}
|
| 249 |
-
|
| 250 |
-
# Step in environment
|
| 251 |
-
try:
|
| 252 |
-
result = send_request("POST", "/step", json={
|
| 253 |
-
"episode_id": episode_id,
|
| 254 |
-
"action": action_data
|
| 255 |
-
})
|
| 256 |
-
|
| 257 |
-
obs = result["observation"]
|
| 258 |
-
reward = result["reward"]
|
| 259 |
-
done = result["done"]
|
| 260 |
-
|
| 261 |
-
step_count = obs["step_number"]
|
| 262 |
-
total_reward = reward["total_reward"]
|
| 263 |
-
|
| 264 |
-
actions_taken.append(action_data)
|
| 265 |
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
print(f"Command: {action_data['command']}")
|
| 270 |
-
elif action_data.get("file_path"):
|
| 271 |
-
print(f"File: {action_data['file_path']}")
|
| 272 |
|
| 273 |
-
print(f"Reward: {reward['step_reward']:+.3f} (total: {total_reward:.3f})")
|
| 274 |
-
print(f"Info: {reward['explanation'][:100]}")
|
| 275 |
-
|
| 276 |
-
if done:
|
| 277 |
-
print(f"\n{'='*60}")
|
| 278 |
-
print("EPISODE COMPLETE!")
|
| 279 |
-
print(f"Final Reward: {total_reward:.3f}")
|
| 280 |
-
print(f"Steps taken: {step_count}")
|
| 281 |
-
print(f"{'='*60}\n")
|
| 282 |
-
break
|
| 283 |
-
|
| 284 |
except Exception as e:
|
| 285 |
-
print(f"
|
| 286 |
-
|
| 287 |
|
| 288 |
-
#
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
"episode_id": episode_id,
|
| 304 |
-
"final_reward": total_reward,
|
| 305 |
-
"step_count": step_count,
|
| 306 |
-
"actions": actions_taken,
|
| 307 |
-
}
|
| 308 |
-
|
| 309 |
|
| 310 |
-
|
| 311 |
-
parser = argparse.ArgumentParser(
|
| 312 |
-
parser.add_argument("--task", default="task1", help="Task ID
|
| 313 |
-
parser.add_argument(
|
| 314 |
-
"--model",
|
| 315 |
-
default=None,
|
| 316 |
-
help=(
|
| 317 |
-
"Model name (default: env var MODEL_NAME). "
|
| 318 |
-
"For Gemini, pass a real model ID like gemini-3-flash-preview "
|
| 319 |
-
"or use --model gemini to auto-resolve to GEMINI_MODEL."
|
| 320 |
-
),
|
| 321 |
-
)
|
| 322 |
-
parser.add_argument("--api-url", default=None, help="API URL (default: env var API_BASE_URL)")
|
| 323 |
-
parser.add_argument("--hf-token", default=None, help="HF token (default: env var HF_TOKEN)")
|
| 324 |
-
parser.add_argument(
|
| 325 |
-
"--openai-base-url",
|
| 326 |
-
default=None,
|
| 327 |
-
help="OpenAI-compatible base URL for non-OpenAI providers (for example Gemini OpenAI API)",
|
| 328 |
-
)
|
| 329 |
-
|
| 330 |
args = parser.parse_args()
|
| 331 |
|
| 332 |
-
|
| 333 |
-
global API_BASE_URL, MODEL_NAME, HF_TOKEN, OPENAI_BASE_URL
|
| 334 |
-
if args.api_url:
|
| 335 |
-
API_BASE_URL = args.api_url
|
| 336 |
-
if args.model:
|
| 337 |
-
MODEL_NAME = args.model
|
| 338 |
-
if args.hf_token:
|
| 339 |
-
HF_TOKEN = args.hf_token
|
| 340 |
-
if args.openai_base_url:
|
| 341 |
-
OPENAI_BASE_URL = args.openai_base_url
|
| 342 |
-
|
| 343 |
-
try:
|
| 344 |
-
result = run_agent(args.task, model=MODEL_NAME)
|
| 345 |
-
print(json.dumps(result, indent=2))
|
| 346 |
-
except Exception as e:
|
| 347 |
-
print(f"Fatal error: {e}", file=sys.stderr)
|
| 348 |
-
sys.exit(1)
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
if __name__ == "__main__":
|
| 352 |
-
main()
|
|
|
|
| 1 |
"""
|
| 2 |
Baseline LLM inference agent for DevOpsEnv.
|
| 3 |
+
Strictly uses the OpenAI Client, but can route to Gemini via base_url.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
import os
|
|
|
|
| 6 |
import json
|
| 7 |
import argparse
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
import requests
|
|
|
|
| 9 |
from openai import OpenAI
|
| 10 |
|
| 11 |
+
# Required Hackathon Variables
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
API_BASE_URL = os.environ.get("API_BASE_URL", "http://localhost:7860")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
+
# Default to Gemini 1.5 Flash (Free tier)
|
| 15 |
+
MODEL_NAME = os.environ.get("MODEL_NAME", "gemini-1.5-flash")
|
| 16 |
|
| 17 |
+
# This will be your Gemini API Key in HF Spaces secrets,
|
| 18 |
+
# but the hackathon validator will inject their own key here.
|
| 19 |
+
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
+
# Google's official OpenAI-compatible endpoint
|
| 22 |
+
OPENAI_BASE_URL = os.environ.get(
|
| 23 |
+
"OPENAI_BASE_URL",
|
| 24 |
+
"https://generativelanguage.googleapis.com/v1beta/openai/"
|
| 25 |
+
)
|
| 26 |
|
| 27 |
+
def run_agent(task_id: str):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
print(f"\n{'='*60}")
|
| 29 |
+
print(f"Initializing OpenAI client with model: {MODEL_NAME}")
|
| 30 |
+
print(f"Routing to: {OPENAI_BASE_URL}")
|
| 31 |
print(f"{'='*60}\n")
|
| 32 |
|
| 33 |
+
# The hackathon requires using the OpenAI client
|
| 34 |
+
client = OpenAI(
|
| 35 |
+
api_key=HF_TOKEN,
|
| 36 |
+
base_url=OPENAI_BASE_URL
|
| 37 |
+
)
|
|
|
|
|
|
|
| 38 |
|
| 39 |
+
# 1. Reset Environment
|
| 40 |
+
print(f"Starting task: {task_id}")
|
| 41 |
+
res = requests.post(f"{API_BASE_URL}/reset", json={"task_id": task_id}).json()
|
| 42 |
+
episode_id = res["episode_id"]
|
| 43 |
|
| 44 |
+
# Max steps to prevent infinite loops (costs money/time)
|
| 45 |
+
for step in range(20):
|
| 46 |
+
# 2. Get State
|
| 47 |
+
state_data = requests.get(f"{API_BASE_URL}/state", params={"episode_id": episode_id}).json()
|
| 48 |
+
sys_state = state_data.get("system_state", {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
+
# 3. Call LLM
|
| 51 |
+
prompt = (
|
| 52 |
+
f"Current System State: {json.dumps(sys_state)}\n"
|
| 53 |
+
"You are a DevOps assistant. What action will you take? "
|
| 54 |
+
"Reply strictly in JSON with 'action_type' ('bash_cmd', 'file_edit', or 'submit'), "
|
| 55 |
+
"and the required fields ('command' for bash, 'filepath' & 'file_content' for edits)."
|
| 56 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
try:
|
| 59 |
+
response = client.chat.completions.create(
|
| 60 |
+
model=MODEL_NAME,
|
| 61 |
+
messages=[
|
| 62 |
+
{"role": "system", "content": "You output strictly valid JSON. Diagnose the issue and fix it. Use action_type='submit' when done."},
|
| 63 |
+
{"role": "user", "content": prompt}
|
| 64 |
+
],
|
| 65 |
+
# Gemini supports JSON mode via the OpenAI proxy
|
| 66 |
+
response_format={"type": "json_object"}
|
| 67 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
+
action_str = response.choices[0].message.content
|
| 70 |
+
action = json.loads(action_str)
|
| 71 |
+
print(f"Step {step} Action: {action}")
|
|
|
|
|
|
|
|
|
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
except Exception as e:
|
| 74 |
+
print(f"LLM Error or JSON parse failed: {e}. Submitting to end early.")
|
| 75 |
+
action = {"action_type": "submit", "summary": "Failed to parse LLM response"}
|
| 76 |
|
| 77 |
+
# 4. Step Environment
|
| 78 |
+
step_res = requests.post(
|
| 79 |
+
f"{API_BASE_URL}/step",
|
| 80 |
+
json={"episode_id": episode_id, "action": action}
|
| 81 |
+
).json()
|
| 82 |
+
|
| 83 |
+
if step_res.get("done") or action.get("action_type") == "submit":
|
| 84 |
+
print("\nEpisode finished!")
|
| 85 |
+
break
|
| 86 |
+
|
| 87 |
+
# 5. Grade
|
| 88 |
+
grade = requests.post(f"{API_BASE_URL}/grader", json={"episode_id": episode_id}).json()
|
| 89 |
+
print(f"\nGrader Results:")
|
| 90 |
+
print(f"Final Score: {grade['score']} / 1.0")
|
| 91 |
+
print(f"Feedback: {grade['feedback']}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
+
if __name__ == "__main__":
|
| 94 |
+
parser = argparse.ArgumentParser()
|
| 95 |
+
parser.add_argument("--task", default="task1", help="Task ID to run")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
args = parser.parse_args()
|
| 97 |
|
| 98 |
+
run_agent(args.task)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
openenv.yaml
CHANGED
|
@@ -1,22 +1,20 @@
|
|
| 1 |
-
name:
|
| 2 |
version: "1.0.0"
|
| 3 |
description: >
|
| 4 |
-
An OpenEnv-compliant
|
| 5 |
-
Agents
|
| 6 |
-
|
| 7 |
|
| 8 |
-
domain:
|
| 9 |
tags:
|
| 10 |
- openenv
|
| 11 |
-
-
|
| 12 |
-
-
|
| 13 |
-
-
|
| 14 |
-
-
|
| 15 |
-
- generation
|
| 16 |
-
- real-world
|
| 17 |
|
| 18 |
license: MIT
|
| 19 |
-
author:
|
| 20 |
|
| 21 |
# -------------------------------------------------
|
| 22 |
# Environment interface
|
|
@@ -26,8 +24,7 @@ interface:
|
|
| 26 |
method: POST
|
| 27 |
path: /reset
|
| 28 |
request:
|
| 29 |
-
task_id: string
|
| 30 |
-
ticket_index: integer # optional, 0-4
|
| 31 |
response: Observation
|
| 32 |
|
| 33 |
step:
|
|
@@ -36,7 +33,7 @@ interface:
|
|
| 36 |
request:
|
| 37 |
episode_id: string
|
| 38 |
action: Action
|
| 39 |
-
response: StepResult
|
| 40 |
|
| 41 |
state:
|
| 42 |
method: GET
|
|
@@ -57,52 +54,31 @@ interface:
|
|
| 57 |
episode_id: string
|
| 58 |
response: GraderResponse
|
| 59 |
|
| 60 |
-
baseline:
|
| 61 |
-
method: POST
|
| 62 |
-
path: /baseline
|
| 63 |
-
request:
|
| 64 |
-
model: string # optional, default heuristic
|
| 65 |
-
ticket_index: integer # optional, default 0
|
| 66 |
-
response: BaselineResult
|
| 67 |
-
|
| 68 |
health:
|
| 69 |
method: GET
|
| 70 |
path: /health
|
| 71 |
|
| 72 |
# -------------------------------------------------
|
| 73 |
-
# Typed models
|
| 74 |
# -------------------------------------------------
|
| 75 |
models:
|
| 76 |
Observation:
|
| 77 |
task_id: string
|
| 78 |
task_description: string
|
| 79 |
episode_id: string
|
| 80 |
-
|
| 81 |
thread_history: list[dict]
|
| 82 |
available_actions: list[string]
|
| 83 |
step_number: integer
|
| 84 |
max_steps: integer
|
| 85 |
hint: string | null
|
| 86 |
|
| 87 |
-
TicketInfo:
|
| 88 |
-
ticket_id: string
|
| 89 |
-
subject: string
|
| 90 |
-
body: string
|
| 91 |
-
customer_tier: string # free | pro | enterprise
|
| 92 |
-
account_age_days: integer
|
| 93 |
-
previous_tickets: integer
|
| 94 |
-
attachments: list[string]
|
| 95 |
-
|
| 96 |
Action:
|
| 97 |
-
action_type: string
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
response_text: string | null
|
| 103 |
-
resolution_steps: list[string] | null
|
| 104 |
-
escalation_team: string | null
|
| 105 |
-
escalation_reason: string | null
|
| 106 |
|
| 107 |
Reward:
|
| 108 |
step_reward: float
|
|
@@ -128,81 +104,44 @@ models:
|
|
| 128 |
GraderResponse:
|
| 129 |
episode_id: string
|
| 130 |
task_id: string
|
| 131 |
-
score: float
|
| 132 |
breakdown: dict[string, float]
|
| 133 |
feedback: string
|
| 134 |
|
| 135 |
# -------------------------------------------------
|
| 136 |
-
# Tasks
|
| 137 |
# -------------------------------------------------
|
| 138 |
tasks:
|
| 139 |
task1:
|
| 140 |
-
name: "
|
| 141 |
difficulty: easy
|
| 142 |
-
max_steps:
|
| 143 |
description: >
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
scoring:
|
| 148 |
-
category_correct: 0.50
|
| 149 |
-
priority_correct: 0.40
|
| 150 |
-
efficiency: 0.10
|
| 151 |
-
tickets: 5
|
| 152 |
-
|
| 153 |
task2:
|
| 154 |
-
name: "
|
| 155 |
difficulty: medium
|
| 156 |
-
max_steps:
|
| 157 |
description: >
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
scoring:
|
| 161 |
-
entity_coverage: 0.60
|
| 162 |
-
action_coverage: 0.30
|
| 163 |
-
no_hallucination: 0.10
|
| 164 |
-
tickets: 5
|
| 165 |
|
| 166 |
task3:
|
| 167 |
-
name: "
|
| 168 |
difficulty: hard
|
| 169 |
-
max_steps:
|
| 170 |
description: >
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
step completeness, tone (apology, urgency, timeline), and response length.
|
| 174 |
-
scoring:
|
| 175 |
-
keyword_coverage: 0.30
|
| 176 |
-
step_coverage: 0.30
|
| 177 |
-
tone_compliance: 0.25
|
| 178 |
-
length_adequate: 0.10
|
| 179 |
-
no_empty_steps: 0.05
|
| 180 |
-
tickets: 5
|
| 181 |
-
|
| 182 |
-
# -------------------------------------------------
|
| 183 |
-
# Reward design
|
| 184 |
-
# -------------------------------------------------
|
| 185 |
-
reward:
|
| 186 |
-
type: dense
|
| 187 |
-
step_cost: -0.02 # small cost per step (encourages efficiency)
|
| 188 |
-
submit_bonus: 0.05 # bonus for explicit submit action
|
| 189 |
-
max_step_penalty: -0.10 # penalty for exhausting max_steps
|
| 190 |
-
grader_bonus: up_to_1.0 # grader score (0–1) added as terminal bonus
|
| 191 |
-
|
| 192 |
-
# -------------------------------------------------
|
| 193 |
-
# Reproducibility
|
| 194 |
-
# -------------------------------------------------
|
| 195 |
-
reproducibility:
|
| 196 |
-
dataset: static # all 15 tickets are fixed, no randomisation
|
| 197 |
-
graders: deterministic # rule-based, no LLM judge
|
| 198 |
-
baseline_mode: heuristic # no API key required for reference scores
|
| 199 |
|
| 200 |
# -------------------------------------------------
|
| 201 |
# Deployment
|
| 202 |
# -------------------------------------------------
|
| 203 |
deployment:
|
| 204 |
framework: FastAPI
|
| 205 |
-
python: ">=3.
|
| 206 |
port: 7860
|
| 207 |
dockerfile: Dockerfile
|
| 208 |
huggingface_space: true
|
|
|
|
| 1 |
+
name: DevOpsEnv
|
| 2 |
version: "1.0.0"
|
| 3 |
description: >
|
| 4 |
+
An OpenEnv-compliant Linux DevOps and SRE troubleshooting environment.
|
| 5 |
+
Agents act as a junior SRE to diagnose and fix broken simulated server states
|
| 6 |
+
using bash commands and file edits.
|
| 7 |
|
| 8 |
+
domain: software_engineering
|
| 9 |
tags:
|
| 10 |
- openenv
|
| 11 |
+
- devops
|
| 12 |
+
- sre
|
| 13 |
+
- troubleshooting
|
| 14 |
+
- linux
|
|
|
|
|
|
|
| 15 |
|
| 16 |
license: MIT
|
| 17 |
+
author: SolvorLabs
|
| 18 |
|
| 19 |
# -------------------------------------------------
|
| 20 |
# Environment interface
|
|
|
|
| 24 |
method: POST
|
| 25 |
path: /reset
|
| 26 |
request:
|
| 27 |
+
task_id: string # task1 | task2 | task3
|
|
|
|
| 28 |
response: Observation
|
| 29 |
|
| 30 |
step:
|
|
|
|
| 33 |
request:
|
| 34 |
episode_id: string
|
| 35 |
action: Action
|
| 36 |
+
response: StepResult
|
| 37 |
|
| 38 |
state:
|
| 39 |
method: GET
|
|
|
|
| 54 |
episode_id: string
|
| 55 |
response: GraderResponse
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
health:
|
| 58 |
method: GET
|
| 59 |
path: /health
|
| 60 |
|
| 61 |
# -------------------------------------------------
|
| 62 |
+
# Typed models (Matches your models.py)
|
| 63 |
# -------------------------------------------------
|
| 64 |
models:
|
| 65 |
Observation:
|
| 66 |
task_id: string
|
| 67 |
task_description: string
|
| 68 |
episode_id: string
|
| 69 |
+
system_state: dict
|
| 70 |
thread_history: list[dict]
|
| 71 |
available_actions: list[string]
|
| 72 |
step_number: integer
|
| 73 |
max_steps: integer
|
| 74 |
hint: string | null
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
Action:
|
| 77 |
+
action_type: string # bash_cmd | file_edit | submit
|
| 78 |
+
command: string | null
|
| 79 |
+
filepath: string | null
|
| 80 |
+
file_content: string | null
|
| 81 |
+
summary: string | null
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
Reward:
|
| 84 |
step_reward: float
|
|
|
|
| 104 |
GraderResponse:
|
| 105 |
episode_id: string
|
| 106 |
task_id: string
|
| 107 |
+
score: float # 0.0 – 1.0
|
| 108 |
breakdown: dict[string, float]
|
| 109 |
feedback: string
|
| 110 |
|
| 111 |
# -------------------------------------------------
|
| 112 |
+
# Tasks (Matches your data.py)
|
| 113 |
# -------------------------------------------------
|
| 114 |
tasks:
|
| 115 |
task1:
|
| 116 |
+
name: "Nginx Service Recovery"
|
| 117 |
difficulty: easy
|
| 118 |
+
max_steps: 10
|
| 119 |
description: >
|
| 120 |
+
The Nginx web server has crashed. The agent must restart the service
|
| 121 |
+
and ensure port 80 is returning an HTTP 200 OK status.
|
| 122 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
task2:
|
| 124 |
+
name: "Docker Compose Fix"
|
| 125 |
difficulty: medium
|
| 126 |
+
max_steps: 15
|
| 127 |
description: >
|
| 128 |
+
A Docker container is misconfigured with the wrong port mapping.
|
| 129 |
+
The agent must edit docker-compose.yml to fix the port and spin the container up.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
task3:
|
| 132 |
+
name: "API Memory Leak"
|
| 133 |
difficulty: hard
|
| 134 |
+
max_steps: 20
|
| 135 |
description: >
|
| 136 |
+
A Python mock API is experiencing a memory leak. The agent must diagnose
|
| 137 |
+
the running processes, kill the faulty process, patch the python file, and restart.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
# -------------------------------------------------
|
| 140 |
# Deployment
|
| 141 |
# -------------------------------------------------
|
| 142 |
deployment:
|
| 143 |
framework: FastAPI
|
| 144 |
+
python: ">=3.11"
|
| 145 |
port: 7860
|
| 146 |
dockerfile: Dockerfile
|
| 147 |
huggingface_space: true
|