exploring-solver commited on
Commit
11f8f10
·
1 Parent(s): 5b64237

updated inference

Browse files
Files changed (5) hide show
  1. .gitignore +2 -1
  2. Dockerfile +2 -19
  3. README.md +0 -62
  4. inference.py +68 -322
  5. 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
- # Health check
38
- HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
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
- # Load .env values from current folder (if present) before reading config.
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
- def _resolve_gemini_model(model: str) -> str:
87
- """Map shorthand Gemini model names to concrete model IDs."""
88
- m = (model or "").strip()
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, max_steps: int = 20, model: Optional[str] = None) -> dict:
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"Starting task: {task_id}")
154
- print(f"Model: {model}")
155
  print(f"{'='*60}\n")
156
 
157
- obs = send_request("POST", "/reset", json={"task_id": task_id})
158
- episode_id = obs["episode_id"]
159
- max_steps = obs["max_steps"]
160
-
161
- print(f"Episode ID: {episode_id}")
162
- print(f"Max Steps: {max_steps}")
163
- print(f"\nTask: {obs['task_description']}\n")
164
 
165
- step_count = 0
166
- total_reward = 0.0
167
- actions_taken = []
 
168
 
169
- while step_count < max_steps:
170
- step_count += 1
171
-
172
- # Get current state
173
- state = send_request("GET", f"/state?episode_id={episode_id}")
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
- user_prompt = f"""
202
- Current system state:
203
- - Task: {obs['task_description']}
204
- - Step: {state['step_number']}/{state['max_steps']}
205
- - Reward so far: {state['total_reward']:.3f}
206
-
207
- System status:
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
- # Call LLM (Gemini SDK or OpenAI-compatible chat)
220
- response_text = _generate_action_text(
221
- model=model,
222
- system_prompt=system_prompt,
223
- user_prompt=user_prompt,
224
- openai_client=openai_client,
225
- gemini_client=gemini_client,
 
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
- print(f"\nStep {step_count}/{max_steps}")
267
- print(f"Action: {action_data['action_type']}")
268
- if action_data.get("command"):
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"Step error: {e}")
286
- break
287
 
288
- # Small delay to avoid rate limiting
289
- time.sleep(0.5)
290
-
291
- # Grade the episode
292
- try:
293
- grade_result = send_request("POST", "/grader", json={"episode_id": episode_id})
294
- print(f"\nGrader Results:")
295
- print(f"Score: {grade_result['score']:.3f}/1.0")
296
- print(f"Breakdown: {json.dumps(grade_result['breakdown'], indent=2)}")
297
- print(f"Feedback: {grade_result['feedback']}")
298
- except Exception as e:
299
- print(f"Grading error: {e}")
300
-
301
- return {
302
- "task_id": task_id,
303
- "episode_id": episode_id,
304
- "final_reward": total_reward,
305
- "step_count": step_count,
306
- "actions": actions_taken,
307
- }
308
-
309
 
310
- def main():
311
- parser = argparse.ArgumentParser(description="Run DevOpsEnv baseline agent")
312
- parser.add_argument("--task", default="task1", help="Task ID (task1, task2, or task3)")
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
- # Override env variables if provided
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: SupportEnv
2
  version: "1.0.0"
3
  description: >
4
- An OpenEnv-compliant customer support ticket triage environment for SaaS platforms.
5
- Agents learn to classify tickets, extract structured information, and generate
6
- professional resolutions skills directly transferable to real-world support automation.
7
 
8
- domain: customer_support
9
  tags:
10
  - openenv
11
- - customer-support
12
- - nlp
13
- - information-extraction
14
- - classification
15
- - generation
16
- - real-world
17
 
18
  license: MIT
19
- author: SupportEnv Contributors
20
 
21
  # -------------------------------------------------
22
  # Environment interface
@@ -26,8 +24,7 @@ interface:
26
  method: POST
27
  path: /reset
28
  request:
29
- task_id: string # task1 | task2 | task3
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 # {observation, reward, done, info}
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
- ticket: TicketInfo
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 # classify | extract | respond | resolve | escalate | submit
98
- category: string | null
99
- priority: string | null
100
- extracted_entities: dict | null
101
- required_actions: list[string] | null
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 # 0.0 – 1.0
132
  breakdown: dict[string, float]
133
  feedback: string
134
 
135
  # -------------------------------------------------
136
- # Tasks
137
  # -------------------------------------------------
138
  tasks:
139
  task1:
140
- name: "Ticket Classification"
141
  difficulty: easy
142
- max_steps: 3
143
  description: >
144
- Given a customer support ticket, classify it by category
145
- (billing | technical | account | feature_request | complaint | general)
146
- and priority (low | medium | high | critical).
147
- scoring:
148
- category_correct: 0.50
149
- priority_correct: 0.40
150
- efficiency: 0.10
151
- tickets: 5
152
-
153
  task2:
154
- name: "Information Extraction"
155
  difficulty: medium
156
- max_steps: 5
157
  description: >
158
- Extract structured entities (account IDs, names, amounts, dates, domains)
159
- from the ticket body and identify the list of required actions.
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: "Resolution Generation"
168
  difficulty: hard
169
- max_steps: 8
170
  description: >
171
- Generate a professional customer-facing response (response_text) and
172
- an ordered list of resolution steps. Scored on keyword coverage,
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.10"
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