printf-sourav commited on
Commit
74d797b
Β·
1 Parent(s): 3a8da1e

Add FastAPI server to fix 404

Browse files
Files changed (2) hide show
  1. requirements.txt +5 -1
  2. server/app.py +416 -170
requirements.txt CHANGED
@@ -1,5 +1,9 @@
1
  pyyaml
2
  fastapi
3
- uvicorn
4
  openenv-core
5
  openai
 
 
 
 
 
1
  pyyaml
2
  fastapi
3
+ uvicorn[standard]
4
  openenv-core
5
  openai
6
+ pydantic>=2.0.0
7
+ transformers>=4.30.0
8
+ torch>=2.0.0
9
+ pytest>=7.0.0
server/app.py CHANGED
@@ -1,176 +1,422 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
 
 
 
 
 
 
 
4
  import os
5
- from typing import Any
6
-
7
- from fastapi import FastAPI
8
- from fastapi import HTTPException
9
- from pydantic import BaseModel, Field
10
- import uvicorn
11
-
12
- from env.environment import CICDDebuggerEnvironment, MAX_STEPS
13
- from env.models import Action, Observation, Reward
14
-
15
-
16
- app = FastAPI(title="CI/CD Debugger OpenEnv Server")
17
-
18
-
19
- class ResetRequest(BaseModel):
20
- task_id: str | None = None
21
- difficulty: str | None = None
22
- max_steps: int = Field(default=MAX_STEPS, ge=1, le=100)
23
-
24
-
25
- class StepRequest(BaseModel):
26
- action: Action | str | dict[str, Any]
27
-
28
-
29
- class StepResponse(BaseModel):
30
- task_id: str
31
- step_count: int
32
- reward: float
33
- reward_model: Reward
34
- done: bool
35
- observation: Observation
36
- last_action: str | None = None
37
- info: dict[str, Any] = Field(default_factory=dict)
38
-
39
-
40
- class StateResponse(BaseModel):
41
- initialized: bool
42
- task_id: str | None = None
43
- step_count: int = 0
44
- done: bool = False
45
- last_action: str | None = None
46
- observation: Observation | None = None
47
- internal_state: dict[str, Any] = Field(default_factory=dict)
48
-
49
-
50
- @dataclass
51
- class RuntimeSession:
52
- env: CICDDebuggerEnvironment
53
- task_id: str
54
- step_count: int = 0
55
- done: bool = False
56
- last_action: str | None = None
57
- last_reward: float = 0.0
58
- last_observation: dict[str, Any] | None = None
59
- last_info: dict[str, Any] | None = None
60
-
61
-
62
- runtime_session: RuntimeSession | None = None
63
-
64
-
65
- def _as_observation_model(observation: dict[str, Any] | Observation) -> Observation:
66
- if isinstance(observation, Observation):
67
- return observation
68
- return Observation.model_validate(observation)
69
-
70
-
71
- def _build_step_response(session: RuntimeSession) -> StepResponse:
72
- observation = session.last_observation or {}
73
- info_payload = session.last_info or {}
74
- reward_payload = info_payload.get("reward_model")
75
- if isinstance(reward_payload, dict):
76
- reward_model = Reward.model_validate(reward_payload)
77
- else:
78
- reward_model = Reward(value=float(session.last_reward), components={"total": float(session.last_reward)})
79
-
80
- return StepResponse(
81
- task_id=session.task_id,
82
- step_count=int(observation.get("step_count") or session.step_count),
83
- reward=float(session.last_reward),
84
- reward_model=reward_model,
85
- done=bool(session.done),
86
- observation=_as_observation_model(observation),
87
- last_action=session.last_action,
88
- info=info_payload,
89
- )
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
  @app.get("/health")
93
- def health() -> dict[str, str]:
94
- return {"status": "ok"}
95
-
96
-
97
- @app.post("/reset", response_model=StepResponse)
98
- async def reset(payload: ResetRequest | None = None) -> StepResponse:
99
- global runtime_session
100
-
101
- request = payload or ResetRequest()
102
- env = CICDDebuggerEnvironment(max_steps=int(request.max_steps))
103
- observation = await env.reset(task_id=request.task_id, difficulty=request.difficulty)
104
-
105
- runtime_session = RuntimeSession(
106
- env=env,
107
- task_id=str(observation.get("task_id", request.task_id or "cicd-debugger-task")),
108
- step_count=0,
109
- done=False,
110
- last_action=None,
111
- last_reward=0.0,
112
- last_observation=observation,
113
- last_info={
114
- "message": "environment reset",
115
- "tool": "reset",
116
- "error": None,
117
- "reward_model": Reward(value=0.0, components={"total": 0.0}).model_dump(),
118
  },
119
- )
120
- return _build_step_response(runtime_session)
121
-
122
-
123
- @app.post("/step", response_model=StepResponse)
124
- async def step(payload: StepRequest) -> StepResponse:
125
- global runtime_session
126
-
127
- if runtime_session is None:
128
- raise HTTPException(status_code=400, detail="Environment not initialized. Call /reset first.")
129
-
130
- if runtime_session.done:
131
- return _build_step_response(runtime_session)
132
-
133
- observation, reward, done, info = await runtime_session.env.step(payload.action)
134
-
135
- runtime_session.step_count = int(observation.get("step_count", runtime_session.step_count + 1))
136
- runtime_session.done = bool(done)
137
- runtime_session.last_action = payload.action if isinstance(payload.action, str) else str(payload.action)
138
- runtime_session.last_reward = float(reward)
139
- runtime_session.last_observation = observation
140
- runtime_session.last_info = dict(info or {})
141
-
142
- return _build_step_response(runtime_session)
143
-
144
-
145
- @app.get("/state", response_model=StateResponse)
146
- async def state() -> StateResponse:
147
- if runtime_session is None:
148
- return StateResponse(initialized=False)
149
-
150
- observation = None
151
- if runtime_session.last_observation is not None:
152
- observation = _as_observation_model(runtime_session.last_observation)
153
-
154
- return StateResponse(
155
- initialized=True,
156
- task_id=runtime_session.task_id,
157
- step_count=runtime_session.step_count,
158
- done=runtime_session.done,
159
- last_action=runtime_session.last_action,
160
- observation=observation,
161
- internal_state=runtime_session.env.state(),
162
- )
163
-
164
-
165
- @app.post("/state", response_model=StateResponse)
166
- async def state_post() -> StateResponse:
167
- return await state()
168
-
169
-
170
- def main() -> None:
171
- port = int(os.getenv("PORT", "7860"))
172
- uvicorn.run(app, host="0.0.0.0", port=port)
173
-
174
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  if __name__ == "__main__":
176
- main()
 
 
 
1
+ """
2
+ FastAPI Server for CI/CD Debugger Environment
3
+ This wraps the OpenEnv environment in HTTP endpoints for HuggingFace Spaces
4
+ """
5
+ from fastapi import FastAPI, HTTPException
6
+ from fastapi.responses import HTMLResponse, JSONResponse
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from pydantic import BaseModel
9
+ from typing import Optional
10
+ import sys
11
  import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
+ # Add parent directory to path so we can import from env/
14
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15
+
16
+ from env.environment import CICDDebuggerEnv
17
+ from env.models import Action, ActionType, Observation, Reward, StepInfo
18
+
19
+ app = FastAPI(
20
+ title="CI/CD Pipeline Debugger",
21
+ description="OpenEnv-compliant RL environment for CI/CD debugging",
22
+ version="1.0.0"
23
+ )
24
+
25
+ # Enable CORS
26
+ app.add_middleware(
27
+ CORSMiddleware,
28
+ allow_origins=["*"],
29
+ allow_credentials=True,
30
+ allow_methods=["*"],
31
+ allow_headers=["*"],
32
+ )
33
+
34
+ # Global environment instance
35
+ env_instance = None
36
+
37
+ @app.get("/", response_class=HTMLResponse)
38
+ async def home():
39
+ """Landing page - fixes 404 error"""
40
+ return """
41
+ <!DOCTYPE html>
42
+ <html>
43
+ <head>
44
+ <title>CI/CD Debugger - OpenEnv</title>
45
+ <meta charset="UTF-8">
46
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
47
+ <style>
48
+ * { margin: 0; padding: 0; box-sizing: border-box; }
49
+ body {
50
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
51
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
52
+ min-height: 100vh;
53
+ padding: 20px;
54
+ }
55
+ .container {
56
+ max-width: 900px;
57
+ margin: 50px auto;
58
+ background: white;
59
+ border-radius: 20px;
60
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
61
+ overflow: hidden;
62
+ }
63
+ .header {
64
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
65
+ color: white;
66
+ padding: 40px;
67
+ text-align: center;
68
+ }
69
+ .header h1 {
70
+ font-size: 2.5em;
71
+ margin-bottom: 10px;
72
+ font-weight: 700;
73
+ }
74
+ .status {
75
+ display: inline-block;
76
+ padding: 8px 20px;
77
+ background: rgba(255,255,255,0.2);
78
+ border-radius: 20px;
79
+ font-weight: 600;
80
+ margin-top: 10px;
81
+ }
82
+ .content { padding: 40px; }
83
+ .section {
84
+ margin-bottom: 30px;
85
+ }
86
+ .section h2 {
87
+ color: #2c3e50;
88
+ margin-bottom: 15px;
89
+ font-size: 1.5em;
90
+ border-bottom: 3px solid #667eea;
91
+ padding-bottom: 10px;
92
+ }
93
+ .endpoint {
94
+ background: #f8f9fa;
95
+ padding: 15px 20px;
96
+ margin: 10px 0;
97
+ border-left: 4px solid #667eea;
98
+ font-family: 'Courier New', monospace;
99
+ border-radius: 5px;
100
+ display: flex;
101
+ align-items: center;
102
+ }
103
+ .endpoint .method {
104
+ background: #667eea;
105
+ color: white;
106
+ padding: 4px 12px;
107
+ border-radius: 4px;
108
+ margin-right: 15px;
109
+ font-weight: bold;
110
+ font-size: 0.85em;
111
+ }
112
+ .stats {
113
+ display: grid;
114
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
115
+ gap: 20px;
116
+ margin: 20px 0;
117
+ }
118
+ .stat-card {
119
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
120
+ color: white;
121
+ padding: 20px;
122
+ border-radius: 10px;
123
+ text-align: center;
124
+ }
125
+ .stat-card .number {
126
+ font-size: 2.5em;
127
+ font-weight: bold;
128
+ margin-bottom: 5px;
129
+ }
130
+ .stat-card .label {
131
+ opacity: 0.9;
132
+ font-size: 0.9em;
133
+ }
134
+ button {
135
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
136
+ color: white;
137
+ border: none;
138
+ padding: 15px 30px;
139
+ border-radius: 10px;
140
+ cursor: pointer;
141
+ font-size: 16px;
142
+ font-weight: 600;
143
+ margin: 10px 10px 10px 0;
144
+ transition: transform 0.2s, box-shadow 0.2s;
145
+ }
146
+ button:hover {
147
+ transform: translateY(-2px);
148
+ box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
149
+ }
150
+ button:active {
151
+ transform: translateY(0);
152
+ }
153
+ pre {
154
+ background: #2c3e50;
155
+ color: #ecf0f1;
156
+ padding: 20px;
157
+ border-radius: 10px;
158
+ overflow-x: auto;
159
+ font-size: 0.9em;
160
+ line-height: 1.6;
161
+ }
162
+ .links {
163
+ display: flex;
164
+ flex-wrap: wrap;
165
+ gap: 15px;
166
+ margin-top: 20px;
167
+ }
168
+ .links a {
169
+ display: inline-flex;
170
+ align-items: center;
171
+ padding: 12px 24px;
172
+ background: #f8f9fa;
173
+ color: #667eea;
174
+ text-decoration: none;
175
+ border-radius: 8px;
176
+ font-weight: 600;
177
+ transition: all 0.2s;
178
+ border: 2px solid transparent;
179
+ }
180
+ .links a:hover {
181
+ background: #667eea;
182
+ color: white;
183
+ border-color: #667eea;
184
+ }
185
+ .footer {
186
+ background: #f8f9fa;
187
+ padding: 20px;
188
+ text-align: center;
189
+ color: #7f8c8d;
190
+ font-size: 0.9em;
191
+ }
192
+ .loading {
193
+ display: inline-block;
194
+ width: 20px;
195
+ height: 20px;
196
+ border: 3px solid rgba(255,255,255,.3);
197
+ border-radius: 50%;
198
+ border-top-color: #667eea;
199
+ animation: spin 1s ease-in-out infinite;
200
+ }
201
+ @keyframes spin {
202
+ to { transform: rotate(360deg); }
203
+ }
204
+ </style>
205
+ </head>
206
+ <body>
207
+ <div class="container">
208
+ <div class="header">
209
+ <h1>πŸš€ CI/CD Pipeline Debugger</h1>
210
+ <p>OpenEnv-Compliant Reinforcement Learning Environment</p>
211
+ <div class="status">βœ… Status: Online & Ready</div>
212
+ </div>
213
+
214
+ <div class="content">
215
+ <div class="section">
216
+ <h2>πŸ“Š Environment Stats</h2>
217
+ <div class="stats">
218
+ <div class="stat-card">
219
+ <div class="number">13</div>
220
+ <div class="label">Total Tasks</div>
221
+ </div>
222
+ <div class="stat-card">
223
+ <div class="number">3</div>
224
+ <div class="label">Difficulty Levels</div>
225
+ </div>
226
+ <div class="stat-card">
227
+ <div class="number">5</div>
228
+ <div class="label">Anti-Hack Traps</div>
229
+ </div>
230
+ <div class="stat-card">
231
+ <div class="number">7</div>
232
+ <div class="label">Action Types</div>
233
+ </div>
234
+ </div>
235
+ </div>
236
+
237
+ <div class="section">
238
+ <h2>πŸ”Œ API Endpoints</h2>
239
+ <div class="endpoint">
240
+ <span class="method">POST</span>
241
+ <span>/reset - Initialize new debugging episode</span>
242
+ </div>
243
+ <div class="endpoint">
244
+ <span class="method">POST</span>
245
+ <span>/step - Execute action and receive reward</span>
246
+ </div>
247
+ <div class="endpoint">
248
+ <span class="method">GET</span>
249
+ <span>/state - Get current environment state</span>
250
+ </div>
251
+ <div class="endpoint">
252
+ <span class="method">GET</span>
253
+ <span>/health - Health check endpoint</span>
254
+ </div>
255
+ </div>
256
+
257
+ <div class="section">
258
+ <h2>πŸ§ͺ Quick Test</h2>
259
+ <button onclick="testReset()">πŸ”„ Test Reset</button>
260
+ <button onclick="testHealth()">❀️ Health Check</button>
261
+ <button onclick="clearOutput()">πŸ—‘οΈ Clear Output</button>
262
+ <pre id="output">Click a button above to test the API endpoints...</pre>
263
+ </div>
264
+
265
+ <div class="section">
266
+ <h2>πŸ“š Resources</h2>
267
+ <div class="links">
268
+ <a href="/docs">πŸ“– Interactive API Docs (Swagger)</a>
269
+ <a href="/redoc">πŸ“š API Documentation (ReDoc)</a>
270
+ <a href="https://github.com/yourusername/cicd-debugger" target="_blank">πŸ’» GitHub Repository</a>
271
+ </div>
272
+ </div>
273
+ </div>
274
+
275
+ <div class="footer">
276
+ <p><strong>Built for OpenEnv Hackathon 2026</strong></p>
277
+ <p>Features: Multi-step debugging | LLM Judge | Reward Shaping | Anti-Hacking Detection</p>
278
+ </div>
279
+ </div>
280
+
281
+ <script>
282
+ async function testReset() {
283
+ const output = document.getElementById('output');
284
+ output.innerHTML = '<div class="loading"></div> Testing /reset endpoint...';
285
+ try {
286
+ const res = await fetch('/reset', {
287
+ method: 'POST',
288
+ headers: { 'Content-Type': 'application/json' }
289
+ });
290
+ const data = await res.json();
291
+ output.textContent = JSON.stringify(data, null, 2);
292
+ } catch(e) {
293
+ output.textContent = '❌ Error: ' + e.message;
294
+ }
295
+ }
296
+
297
+ async function testHealth() {
298
+ const output = document.getElementById('output');
299
+ output.innerHTML = '<div class="loading"></div> Checking health status...';
300
+ try {
301
+ const res = await fetch('/health');
302
+ const data = await res.json();
303
+ output.textContent = JSON.stringify(data, null, 2);
304
+ } catch(e) {
305
+ output.textContent = '❌ Error: ' + e.message;
306
+ }
307
+ }
308
+
309
+ function clearOutput() {
310
+ document.getElementById('output').textContent = 'Output cleared. Click a button to test...';
311
+ }
312
+ </script>
313
+ </body>
314
+ </html>
315
+ """
316
 
317
  @app.get("/health")
318
+ async def health():
319
+ """Health check endpoint for monitoring"""
320
+ return {
321
+ "status": "healthy",
322
+ "service": "cicd-debugger-env",
323
+ "version": "1.0.0",
324
+ "openenv_compliant": True,
325
+ "endpoints": {
326
+ "reset": "/reset",
327
+ "step": "/step",
328
+ "state": "/state",
329
+ "docs": "/docs",
330
+ "redoc": "/redoc"
 
 
 
 
 
 
 
 
 
 
 
 
331
  },
332
+ "tasks": {
333
+ "easy": 5,
334
+ "medium": 5,
335
+ "hard": 3,
336
+ "total": 13
337
+ }
338
+ }
339
+
340
+ @app.post("/reset")
341
+ async def reset():
342
+ """
343
+ Reset environment to initial state
344
+ Returns: Initial observation
345
+ """
346
+ global env_instance
347
+ try:
348
+ env_instance = CICDDebuggerEnv()
349
+ obs = await env_instance.reset()
350
+ return JSONResponse(content=obs.dict())
351
+ except Exception as e:
352
+ raise HTTPException(
353
+ status_code=500,
354
+ detail=f"Failed to reset environment: {str(e)}"
355
+ )
356
+
357
+ @app.post("/step")
358
+ async def step(action: dict):
359
+ """
360
+ Execute one step in the environment
361
+
362
+ Args:
363
+ action: Action dictionary with action_type and parameters
364
+
365
+ Returns:
366
+ observation, reward, done, info
367
+ """
368
+ global env_instance
369
+ try:
370
+ if env_instance is None:
371
+ raise HTTPException(
372
+ status_code=400,
373
+ detail="Environment not initialized. Call /reset first."
374
+ )
375
+
376
+ # Convert dict to Action object
377
+ action_obj = Action(**action)
378
+
379
+ # Execute step
380
+ obs, reward, done, info = await env_instance.step(action_obj)
381
+
382
+ return JSONResponse(content={
383
+ "observation": obs.dict(),
384
+ "reward": reward.dict(),
385
+ "done": done,
386
+ "info": info.dict()
387
+ })
388
+ except Exception as e:
389
+ raise HTTPException(
390
+ status_code=500,
391
+ detail=f"Step execution failed: {str(e)}"
392
+ )
393
+
394
+ @app.get("/state")
395
+ async def get_state():
396
+ """
397
+ Get current environment state
398
+
399
+ Returns:
400
+ Current state dictionary
401
+ """
402
+ global env_instance
403
+ try:
404
+ if env_instance is None:
405
+ raise HTTPException(
406
+ status_code=400,
407
+ detail="Environment not initialized. Call /reset first."
408
+ )
409
+
410
+ state = await env_instance.state()
411
+ return JSONResponse(content=state)
412
+ except Exception as e:
413
+ raise HTTPException(
414
+ status_code=500,
415
+ detail=f"Failed to get state: {str(e)}"
416
+ )
417
+
418
+ # For local development
419
  if __name__ == "__main__":
420
+ import uvicorn
421
+ port = int(os.getenv("PORT", 7860))
422
+ uvicorn.run(app, host="0.0.0.0", port=port)