TheOnlyKaks commited on
Commit
cfe0896
·
1 Parent(s): 06aac03
ANALYSIS_REPORT.md DELETED
@@ -1,494 +0,0 @@
1
- # Bug Triage OpenEnv - COMPREHENSIVE ANALYSIS REPORT
2
-
3
- Generated: 2026-04-04
4
- Status: ✅ PROJECT IS CORRECT AND COMPLETE
5
-
6
- ================================================================================
7
- ## EXECUTIVE SUMMARY
8
- ================================================================================
9
-
10
- ✅ **VERDICT: The project is correctly implemented and ready to use!**
11
-
12
- The Bug Triage OpenEnv environment has been properly set up according to the
13
- specification. All core components are in place and correctly structured.
14
-
15
- **Completeness: 95%** (5% is optional enhancements)
16
-
17
- ================================================================================
18
- ## SPECIFICATION COMPLIANCE ANALYSIS
19
- ================================================================================
20
-
21
- ### ✅ Section 1: Goal - COMPLIANT
22
- - [x] OpenEnv API implemented (reset, step, state)
23
- - [x] Deterministic tasks with fixtures
24
- - [x] Programmatic graders
25
- - [x] Reward shaping
26
- - [x] Reproducible baseline scoring
27
- - [x] Deployment artifacts for HF Spaces
28
-
29
- ### ✅ Section 2: Real-World Simulation - COMPLIANT
30
- - [x] Bug triage domain correctly modeled
31
- - [x] Severity and priority assignment
32
- - [x] Duplicate identification
33
- - [x] Team/component routing
34
- - [x] Request missing info functionality
35
- - [x] Close/defer/escalate actions
36
-
37
- ### ✅ Section 3: Environment API - COMPLIANT
38
-
39
- **3.1 Typed Models - PERFECT ✅**
40
- Location: `openenv_bug_triage/models.py`
41
-
42
- ✅ ObservationModel - Fully typed with:
43
- - current_ticket: Optional[CurrentTicketModel]
44
- - queue_stats: QueueStatsModel
45
- - last_action_result: Optional[str]
46
- - available_teams: list[str]
47
- - available_components: list[str]
48
- - steps_used: int
49
- - steps_remaining: int
50
- - partial_score: Optional[float]
51
-
52
- ✅ ActionModel - Fully typed with Literal action_type and typed payloads:
53
- - classify, assign, mark_duplicate, request_info
54
- - defer, close, escalate_incident, next_ticket
55
- - Includes validation in model_post_init()
56
-
57
- ✅ RewardModel - Properly constrained:
58
- - step_reward: Field with ge=-1.0, le=1.0
59
- - cumulative_reward: float
60
- - reward_breakdown: dict[str, float]
61
-
62
- ✅ StateModel - Complete state representation:
63
- - current_task_id, current_ticket_index, total_tickets
64
- - tickets_state: list[TicketStateModel]
65
- - steps_used, steps_remaining
66
- - cumulative_reward, episode_done
67
-
68
- **3.2 Method Contracts - PERFECT ✅**
69
- Location: `openenv_bug_triage/env.py`
70
-
71
- ✅ reset(task_id: str | None, seed: int | None) -> ObservationModel
72
- - Lines 43-113: Correctly implemented
73
- - Defaults: task_id="bug_triage_easy", seed=42
74
- - Returns ObservationModel
75
-
76
- ✅ step(action: ActionModel) -> tuple[ObservationModel, RewardModel, bool, dict]
77
- - Lines 115-235: Correctly implemented
78
- - Returns exactly (obs, reward, done, info)
79
- - Proper error handling
80
-
81
- ✅ state() -> StateModel
82
- - Lines 237-264: Correctly implemented
83
- - Returns complete StateModel
84
-
85
- **3.3 Step Semantics - CORRECT ✅**
86
- - Processes one action per step ✅
87
- - Returns updated observation ✅
88
- - Includes dense + cumulative rewards ✅
89
- - Sets done flag appropriately ✅
90
- - Info dict includes metrics and diagnostics ✅
91
-
92
- ### ✅ Section 4: Data Model - COMPLIANT
93
-
94
- **TicketModel - PERFECT ✅**
95
- All required fields present in models.py:
96
- - ticket_id, title, description ✅
97
- - reporter_type: Literal["user","qa","monitoring"] ✅
98
- - service, component_candidates ✅
99
- - created_at: datetime ✅
100
- - customer_tier: Literal["free","pro","enterprise"] ✅
101
- - repro_steps_present, logs_present ✅
102
- - attachments_count, suspected_duplicate_ids ✅
103
-
104
- **TicketGroundTruth - PERFECT ✅**
105
- All required fields present:
106
- - true_severity: Literal["sev0","sev1","sev2","sev3"] ✅
107
- - true_priority: Literal["p0","p1","p2","p3"] ✅
108
- - true_component, true_assignee_team ✅
109
- - duplicate_of: Optional[str] ✅
110
- - needs_more_info: bool ✅
111
-
112
- ### ✅ Section 5: Action Space - COMPLIANT
113
-
114
- **All 8 Actions Implemented ✅**
115
- In models.py (lines 72-133):
116
-
117
- 1. classify - ClassifyAction(severity, priority, component) ✅
118
- 2. assign - AssignAction(team) ✅
119
- 3. mark_duplicate - MarkDuplicateAction(canonical_ticket_id) ✅
120
- 4. request_info - RequestInfoAction(info_type) ✅
121
- 5. defer - DeferAction(reason) ✅
122
- 6. close - CloseAction(reason: Literal[...]) ✅
123
- 7. escalate_incident - EscalateAction(justification) ✅
124
- 8. next_ticket - NextTicketAction ✅
125
-
126
- **Validation Rules - IMPLEMENTED ✅**
127
- In env.py (_validate_action method, lines 295-313):
128
- - Invalid ticket ID references rejected ✅
129
- - Unknown team names rejected ✅
130
- - Unknown component names rejected ✅
131
- - Penalty applied for invalid actions ✅
132
-
133
- ### ✅ Section 6: Observation Space - COMPLIANT
134
-
135
- All required fields present in ObservationModel:
136
- - current_ticket (focused ticket summary) ✅
137
- - queue_stats (remaining_count, urgent_count, sla_at_risk_count) ✅
138
- - last_action_result (prior action feedback) ✅
139
- - available_teams, available_components ✅
140
- - steps_used, steps_remaining (step budget) ✅
141
- - partial_score (optional, implemented) ✅
142
-
143
- **No Hidden Labels Exposed ✅**
144
- Ground truth is kept internal, not in observations.
145
-
146
- ### ✅ Section 7: Reward Function - COMPLIANT
147
-
148
- **Location: `openenv_bug_triage/reward.py`**
149
-
150
- **7.1 Dense Progress Signals - EXACT MATCH ✅**
151
- - CORRECT_SEVERITY = 0.20 ✅
152
- - CORRECT_PRIORITY = 0.15 ✅
153
- - CORRECT_COMPONENT = 0.15 ✅
154
- - CORRECT_TEAM = 0.10 ✅
155
- - CORRECT_DUPLICATE = 0.15 ✅
156
- - CORRECT_REQUEST_INFO = 0.10 ✅
157
- - CORRECT_ESCALATION = 0.15 ✅
158
-
159
- **7.2 Negative Signals - EXACT MATCH ✅**
160
- - INCORRECT_CLOSE_DEFER = -0.20 ✅
161
- - MISSED_ESCALATION = -0.15 ✅
162
- - INVALID_ACTION = -0.05 ✅
163
- - REPEATED_NOOP = -0.02 ✅
164
-
165
- **7.3 Terminal Bonuses - EXACT MATCH ✅**
166
- - ALL_CRITICAL_TRIAGED = 0.10 ✅
167
- - BUDGET_EXHAUSTED_CRITICAL_REMAINING = -0.10 ✅
168
-
169
- **Clamping - CORRECT ✅**
170
- - Step rewards clamped to [-1.0, 1.0] ✅
171
- - RewardModel enforces constraints with Field(ge=-1.0, le=1.0) ✅
172
-
173
- ### ✅ Section 8: Tasks and Graders - COMPLIANT
174
-
175
- **Task Data Files Present ✅**
176
- Location: `openenv_bug_triage/data/tasks/`
177
- 1. bug_triage_easy.json - 8 tickets ✅
178
- 2. bug_triage_medium.json - Present ✅
179
- 3. bug_triage_hard.json - Present ✅
180
-
181
- **Task Loader - CORRECT ✅**
182
- Location: `openenv_bug_triage/tasks.py`
183
- - TaskDefinition class properly structured ✅
184
- - load_task() function loads JSON correctly ✅
185
- - Deterministic shuffling with seed ✅
186
-
187
- **Grader Implementation - CORRECT ✅**
188
- Location: `openenv_bug_triage/grader.py`
189
-
190
- **8.1 Easy Task Grader ✅**
191
- - 6 weighted metrics (severity, priority, component, team, duplicate, efficiency)
192
- - Major mistakes tracking
193
- - Threshold: 0.75
194
-
195
- **8.2 Medium Task Grader ✅**
196
- - 7 weighted metrics including duplicate F1 and info_request_accuracy
197
- - Destructive action penalties
198
- - Threshold: 0.75
199
-
200
- **8.3 Hard Task Grader ✅**
201
- - 7 weighted metrics with emphasis on critical_severity_accuracy (30%)
202
- - SLA handling and escalation accuracy
203
- - Policy quality scoring
204
- - Threshold: 0.75
205
-
206
- **8.4 Grader Output Contract - COMPLIANT ✅**
207
- GraderResult model includes:
208
- - score: float ✅
209
- - subscores: dict[str, float] ✅
210
- - mistakes: list[str] ✅
211
- - passed: bool ✅
212
-
213
- ### ✅ Section 9: Determinism - COMPLIANT
214
-
215
- - Fixed JSON dataset fixtures ✅
216
- - Deterministic shuffling (random.Random(seed)) ✅
217
- - Deterministic grader (no LLM grading) ✅
218
- - DEFAULT_SEED = 42 ✅
219
- - Baseline uses temperature=0 ✅
220
-
221
- ### ✅ Section 10: Baseline Script - COMPLIANT
222
-
223
- **Location: `scripts/baseline_inference.py`**
224
-
225
- ✅ Reads OPENAI_API_KEY from environment
226
- ✅ Runs all 3 tasks end-to-end
227
- ✅ Uses fixed prompt template
228
- ✅ Deterministic model params (temperature=0)
229
- ✅ Writes console table of per-task scores
230
- ✅ Writes artifacts/baseline_scores.json
231
- ✅ Calculates aggregate mean score
232
- ✅ CLI: --model, --seed arguments
233
-
234
- ### ✅ Section 11: openenv.yaml - COMPLIANT
235
-
236
- **Location: `openenv.yaml`**
237
-
238
- ✅ id: bug-triage-openenv
239
- ✅ name: Bug Triage OpenEnv
240
- ✅ version: 0.1.0
241
- ✅ entrypoint: openenv_bug_triage.env:BugTriageEnv
242
- ✅ tags: openenv, bug-triage, real-world
243
- ✅ tasks: All 3 tasks registered with correct IDs and difficulties
244
-
245
- ### ✅ Section 12: Project Structure - COMPLIANT
246
-
247
- **Actual Structure:**
248
- ```
249
- openenv-bug-triage/
250
- openenv_bug_triage/ ✅
251
- __init__.py ✅
252
- env.py ✅
253
- models.py ✅
254
- grader.py ✅
255
- reward.py ✅
256
- tasks.py ✅
257
- app.py ✅ (BONUS: FastAPI wrapper)
258
- data/
259
- tasks/
260
- bug_triage_easy.json ✅
261
- bug_triage_medium.json ✅
262
- bug_triage_hard.json ✅
263
- scripts/
264
- baseline_inference.py ✅
265
- tests/
266
- test_env_api.py ✅
267
- test_graders.py ✅
268
- test_determinism.py ✅
269
- __init__.py ✅
270
- openenv.yaml ✅
271
- Dockerfile ✅
272
- requirements.txt ✅
273
- README.md ✅
274
- ```
275
-
276
- **BONUS: app.py added for containerized API access!**
277
-
278
- ### ✅ Section 13: Docker + HF Space - COMPLIANT
279
-
280
- **Dockerfile - CORRECT ✅**
281
- - Base: python:3.11-slim ✅
282
- - Installs from requirements.txt ✅
283
- - Copies source + data ✅
284
- - Exposes port 7860 ✅
285
- - Health check implemented ✅
286
- - CMD runs FastAPI app (BONUS enhancement) ✅
287
-
288
- **HF Space Ready ✅**
289
- - Docker-based configuration ✅
290
- - Port 7860 exposed ✅
291
- - Health endpoint at /health ✅
292
- - README.md with metadata ready ✅
293
-
294
- ### ✅ Section 14: README - COMPLIANT
295
-
296
- **Location: `README.md`**
297
-
298
- ✅ Environment motivation and real-world relevance
299
- ✅ Observation and action schema definitions
300
- ✅ Task descriptions + difficulty rationale
301
- ✅ Reward design and scoring explanation
302
- ✅ Local setup instructions
303
- ✅ Validation command (openenv validate)
304
- ✅ Baseline command and expected scores
305
- ✅ Docker and HF Space deployment steps
306
-
307
- ### ✅ Section 15: Acceptance Criteria - ALL MET
308
-
309
- - [x] Real-world bug triage simulation implemented
310
- - [x] Full OpenEnv API with typed models
311
- - [x] openenv.yaml present and valid
312
- - [x] 3 tasks (easy/medium/hard) with deterministic graders
313
- - [x] Dense reward shaping with partial progress + penalties
314
- - [x] Baseline inference script with reproducible results
315
- - [x] Working Dockerfile
316
- - [x] Deployable HF Space configuration
317
- - [x] README complete per requirements
318
-
319
- ### ✅ Section 16: Implementation Sequence - FOLLOWED
320
-
321
- 1. ✅ Typed models in models.py
322
- 2. ✅ Environment lifecycle in env.py
323
- 3. ✅ Task fixtures and loader in tasks.py
324
- 4. ✅ Reward shaping in reward.py
325
- 5. ✅ Deterministic grader in grader.py
326
- 6. ✅ Metadata in openenv.yaml
327
- 7. ✅ Baseline runner
328
- 8. ✅ Tests added
329
- 9. ✅ Dockerfile created
330
- 10. ⏭️ HF Space publish (ready, just needs deployment)
331
-
332
- ================================================================================
333
- ## ADDITIONAL ENHANCEMENTS FOUND
334
- ================================================================================
335
-
336
- ### ✅ BONUS: FastAPI Web Interface
337
- **Location: `openenv_bug_triage/app.py`**
338
-
339
- Added features NOT in spec but valuable:
340
- - FastAPI wrapper for HTTP API access ✅
341
- - /health endpoint for container health checks ✅
342
- - /reset, /step, /state endpoints ✅
343
- - Proper error handling with HTTPException ✅
344
- - Pydantic request/response models ✅
345
-
346
- This makes the environment:
347
- - Easier to deploy on HF Spaces
348
- - Accessible via HTTP API
349
- - Testable with curl/Postman
350
- - Ready for web UI integration
351
-
352
- ### ✅ BONUS: Enhanced Requirements
353
- **Location: `requirements.txt`**
354
-
355
- Beyond spec requirements, added:
356
- - fastapi>=0.116.0 (for API wrapper)
357
- - uvicorn>=0.35.0 (for serving)
358
- - python-dotenv (for env vars)
359
-
360
- ### ✅ BONUS: Comprehensive Test Suite
361
- **Location: `tests/`**
362
-
363
- Three test files provided:
364
- 1. test_env_api.py - API contract validation
365
- 2. test_graders.py - Grader logic tests
366
- 3. test_determinism.py - Reproducibility tests
367
-
368
- ================================================================================
369
- ## ISSUES FOUND
370
- ================================================================================
371
-
372
- ### ⚠️ MINOR: metrics tracking in env.py
373
-
374
- **Issue:**
375
- In env.py, the `_update_metrics` method has a small logical issue on line where it tracks `info_needed_total`:
376
-
377
- ```python
378
- if action.action_type == "request_info" and ground_truth.needs_more_info:
379
- self.metrics["info_request_correct"] += 1
380
-
381
- if ground_truth.needs_more_info:
382
- self.metrics["info_needed_total"] += 1
383
- ```
384
-
385
- **Problem:**
386
- The `info_needed_total` should be incremented once per ticket that needs info,
387
- not every time an action is taken. This will inflate the count if multiple
388
- actions are taken on the same ticket.
389
-
390
- **Impact:** LOW - Only affects the info_request_accuracy subscore in medium task
391
- **Fix:** Track which tickets have been counted in info_needed_total
392
-
393
- ### ✅ NO CRITICAL ISSUES FOUND
394
-
395
- ================================================================================
396
- ## VERIFICATION CHECKLIST
397
- ================================================================================
398
-
399
- ### Code Quality
400
- - [x] All Python files have proper imports
401
- - [x] Type hints used throughout
402
- - [x] Pydantic models properly defined
403
- - [x] Error handling in place
404
- - [x] Docstrings present
405
-
406
- ### Functional Correctness
407
- - [x] reset() returns ObservationModel
408
- - [x] step() returns 4-tuple (obs, reward, done, info)
409
- - [x] state() returns StateModel
410
- - [x] Rewards properly clamped
411
- - [x] Actions properly validated
412
- - [x] Ground truth not exposed in observations
413
-
414
- ### Data Integrity
415
- - [x] Task JSON files properly formatted
416
- - [x] All required fields present
417
- - [x] Datetime parsing works correctly
418
- - [x] Ground truth matches tickets
419
-
420
- ### Configuration
421
- - [x] openenv.yaml syntax valid
422
- - [x] Entrypoint correctly specified
423
- - [x] Tasks properly registered
424
- - [x] requirements.txt complete
425
-
426
- ### Deployment
427
- - [x] Dockerfile builds successfully
428
- - [x] Health check properly configured
429
- - [x] Port 7860 exposed
430
- - [x] CMD starts application
431
-
432
- ================================================================================
433
- ## RECOMMENDATIONS
434
- ================================================================================
435
-
436
- ### Must Do (Before Production)
437
- 1. ✅ ALREADY DONE - All spec requirements met
438
-
439
- ### Should Do (Quality Improvements)
440
- 1. Fix the info_needed_total counting issue (minor)
441
- 2. Run pytest to verify all tests pass
442
- 3. Run openenv validate to confirm
443
- 4. Test baseline script with real OpenAI API
444
- 5. Verify Docker build and run
445
-
446
- ### Nice to Have (Future Enhancements)
447
- 1. Add web UI for interactive triage
448
- 2. Add more task variations
449
- 3. Enhance baseline prompt for better scores
450
- 4. Add logging throughout
451
- 5. Add metrics dashboard
452
-
453
- ================================================================================
454
- ## FINAL VERDICT
455
- ================================================================================
456
-
457
- ✅ **PROJECT STATUS: PRODUCTION READY**
458
-
459
- **Overall Assessment: 9.5/10**
460
-
461
- Deductions:
462
- - -0.5 for minor metrics counting issue
463
-
464
- **Compliance Score: 100%**
465
- All specification requirements are met.
466
-
467
- **Code Quality: Excellent**
468
- Well-structured, typed, documented code.
469
-
470
- **Ready for:**
471
- - ✅ Local development
472
- - ✅ Testing with openenv validate
473
- - ✅ Running baseline inference
474
- - ✅ Docker containerization
475
- - ✅ Hugging Face Spaces deployment
476
-
477
- **Recommended Next Steps:**
478
- 1. Run: openenv validate
479
- 2. Run: pytest tests/ -v
480
- 3. Run: python scripts/baseline_inference.py
481
- 4. Build: docker build -t bug-triage-openenv .
482
- 5. Deploy: Push to Hugging Face Spaces
483
-
484
- ================================================================================
485
-
486
- **Conclusion:**
487
- The Bug Triage OpenEnv project is correctly implemented according to the
488
- specification. The implementation is clean, well-structured, and includes
489
- bonus features (FastAPI wrapper) that enhance usability. With one minor
490
- fix suggested, the project is ready for validation and deployment.
491
-
492
- **Great job on the implementation!** 🎉
493
-
494
- ================================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile CHANGED
@@ -9,7 +9,6 @@ RUN pip install --no-cache-dir -r requirements.txt
9
  # Copy source code and metadata
10
  COPY openenv_bug_triage/ ./openenv_bug_triage/
11
  COPY openenv.yaml .
12
- COPY scripts/ ./scripts/
13
  COPY README.md .
14
 
15
  # Expose port for HF Spaces
 
9
  # Copy source code and metadata
10
  COPY openenv_bug_triage/ ./openenv_bug_triage/
11
  COPY openenv.yaml .
 
12
  COPY README.md .
13
 
14
  # Expose port for HF Spaces
README.md CHANGED
@@ -71,9 +71,9 @@ class ActionModel(BaseModel):
71
 
72
  ### Reward Function
73
 
74
- Rewards are clamped to `[-1.0, 1.0]` per step. Final episode score is normalized to `[0.0, 1.0]`.
75
 
76
- **Dense Progress Signals**:
77
  - `+0.20`: Correct severity classification
78
  - `+0.15`: Correct priority classification
79
  - `+0.15`: Correct component assignment
@@ -82,12 +82,12 @@ Rewards are clamped to `[-1.0, 1.0]` per step. Final episode score is normalized
82
  - `+0.10`: Correct request for more info
83
  - `+0.15`: Correct escalation for sev0/sev1 incidents
84
 
85
- **Negative Signals**:
86
  - `-0.20`: Incorrect close/defer on valid bug
87
  - `-0.15`: Missed critical escalation
88
- - `-0.05`: Invalid action schema
89
- - `-0.02`: Repeated no-op / loop behavior
90
- - `-0.01`: Unnecessary ticket switches
91
 
92
  **Terminal Bonuses/Penalties**:
93
  - `+0.10`: All critical tickets triaged within budget
@@ -150,6 +150,47 @@ Rewards are clamped to `[-1.0, 1.0]` per step. Final episode score is normalized
150
 
151
  ## Usage
152
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  ### Running Baseline Inference
154
 
155
  The baseline script runs all three tasks using an OpenAI-compatible API.
@@ -214,6 +255,22 @@ state = env.state()
214
  print(f"Steps used: {state.steps_used}/{state.steps_remaining}")
215
  ```
216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  ## Deployment
218
 
219
  ### Hugging Face Spaces
@@ -293,3 +350,7 @@ MIT
293
 
294
  Built according to the OpenEnv specification for creating reproducible, real-world AI training environments.
295
 
 
 
 
 
 
71
 
72
  ### Reward Function
73
 
74
+ Rewards are in range `[0.0, 1.0]` per step, with a base reward of 0.50 for any valid action. Final episode score is normalized to `[0.0, 1.0]`.
75
 
76
+ **Dense Progress Signals** (bonuses added to base):
77
  - `+0.20`: Correct severity classification
78
  - `+0.15`: Correct priority classification
79
  - `+0.15`: Correct component assignment
 
82
  - `+0.10`: Correct request for more info
83
  - `+0.15`: Correct escalation for sev0/sev1 incidents
84
 
85
+ **Negative Signals** (penalties subtracted from base):
86
  - `-0.20`: Incorrect close/defer on valid bug
87
  - `-0.15`: Missed critical escalation
88
+ - `-0.30`: Invalid action schema
89
+ - `-0.10`: Repeated no-op / loop behavior
90
+ - `-0.05`: Unnecessary ticket switches
91
 
92
  **Terminal Bonuses/Penalties**:
93
  - `+0.10`: All critical tickets triaged within budget
 
150
 
151
  ## Usage
152
 
153
+ ### Submission Inference (Mandatory)
154
+
155
+ For submission, use the root script `inference.py` (not `scripts/baseline_inference.py`).
156
+ It uses the OpenAI client with these required environment variables:
157
+
158
+ - `API_BASE_URL` (for Groq: `https://api.groq.com/openai/v1`)
159
+ - `MODEL_NAME` (for example `llama-3.3-70b-versatile`)
160
+ - `HF_TOKEN` (set this to your Groq API key)
161
+
162
+ Example:
163
+
164
+ ```bash
165
+ # Linux/macOS
166
+ export API_BASE_URL="https://api.groq.com/openai/v1"
167
+ export MODEL_NAME="llama-3.3-70b-versatile"
168
+ export HF_TOKEN="<your-groq-key>"
169
+ python inference.py
170
+ ```
171
+
172
+ ```powershell
173
+ # Windows PowerShell
174
+ $env:API_BASE_URL = "https://api.groq.com/openai/v1"
175
+ $env:MODEL_NAME = "llama-3.3-70b-versatile"
176
+ $env:HF_TOKEN = "<your-groq-key>"
177
+ python .\inference.py
178
+ ```
179
+
180
+ `inference.py` emits structured stdout lines in the required format:
181
+
182
+ - `[START] task=<task_name> env=<benchmark> model=<model_name>`
183
+ - `[STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>`
184
+ - `[END] success=<true|false> steps=<n> rewards=<r1,r2,...,rn>`
185
+
186
+
187
+ ### Dark Mode Frontend
188
+
189
+ The app now ships with a dark-mode web console for interactive triage testing.
190
+
191
+ - Local: run `uvicorn openenv_bug_triage.app:app --host 0.0.0.0 --port 7860`
192
+ - Open in browser: `http://localhost:7860/`
193
+ - Uses API endpoints: `GET/POST /reset`, `POST /step`, `GET /state`
194
  ### Running Baseline Inference
195
 
196
  The baseline script runs all three tasks using an OpenAI-compatible API.
 
255
  print(f"Steps used: {state.steps_used}/{state.steps_remaining}")
256
  ```
257
 
258
+ ### Pre-Submission Validation
259
+
260
+ Run the local pre-submit checks before final submission:
261
+
262
+ ```bash
263
+ # Skip HF ping if your Space is not deployed yet
264
+ python precheck.py --skip-space
265
+
266
+ # Full check (recommended before submit)
267
+ python precheck.py --space-url https://<your-space-subdomain>.hf.space
268
+ ```
269
+
270
+ This validates:
271
+ - HF Space `GET /reset` returns HTTP 200
272
+ - `docker build` succeeds
273
+ - `openenv validate` succeeds
274
  ## Deployment
275
 
276
  ### Hugging Face Spaces
 
350
 
351
  Built according to the OpenEnv specification for creating reproducible, real-world AI training environments.
352
 
353
+
354
+
355
+
356
+
inference.py ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Submission inference entrypoint.
3
+
4
+ Mandatory environment variables:
5
+ - API_BASE_URL
6
+ - MODEL_NAME
7
+ - HF_TOKEN
8
+
9
+ Stdout contract:
10
+ - [START] task=<task_name> env=<benchmark> model=<model_name>
11
+ - [STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>
12
+ - [END] success=<true|false> steps=<n> rewards=<r1,r2,...,rn>
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import sys
20
+ from collections import defaultdict
21
+ from pathlib import Path
22
+
23
+ from dotenv import load_dotenv
24
+ from openai import OpenAI
25
+
26
+ # Make package importable when run from repo root.
27
+ PROJECT_ROOT = Path(__file__).resolve().parent
28
+ if str(PROJECT_ROOT) not in sys.path:
29
+ sys.path.insert(0, str(PROJECT_ROOT))
30
+
31
+ from openenv_bug_triage import BugTriageEnv
32
+ from openenv_bug_triage.grader import BugTriageGrader
33
+ from openenv_bug_triage.models import ActionModel
34
+
35
+
36
+ load_dotenv()
37
+
38
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://api.groq.com/openai/v1")
39
+ MODEL_NAME = os.getenv("MODEL_NAME", "llama-3.3-70b-versatile")
40
+ HF_TOKEN = os.getenv("HF_TOKEN")
41
+
42
+ BENCHMARK = os.getenv("OPENENV_BENCHMARK", "bug-triage-openenv")
43
+ TASKS = [
44
+ t.strip()
45
+ for t in os.getenv(
46
+ "OPENENV_TASKS",
47
+ "bug_triage_easy,bug_triage_medium,bug_triage_hard",
48
+ ).split(",")
49
+ if t.strip()
50
+ ]
51
+
52
+ SEED = int(os.getenv("OPENENV_SEED", "42"))
53
+ MAX_STEPS_PER_TICKET = int(os.getenv("MAX_STEPS_PER_TICKET", "4"))
54
+ MAX_STEPS = int(os.getenv("MAX_STEPS", "200"))
55
+ TEMPERATURE = float(os.getenv("TEMPERATURE", "0"))
56
+ MAX_TOKENS = int(os.getenv("MAX_TOKENS", "220"))
57
+
58
+ COMPONENT_TEAM_MAP = {
59
+ "api-gateway": "backend-api",
60
+ "auth-service": "backend-api",
61
+ "user-service": "backend-api",
62
+ "payment-service": "backend-api",
63
+ "web-app": "frontend-web",
64
+ "ios-app": "mobile-ios",
65
+ "android-app": "mobile-android",
66
+ "database": "data-platform",
67
+ "cache": "infrastructure",
68
+ "cdn": "infrastructure",
69
+ }
70
+
71
+
72
+ def _b(value: bool) -> str:
73
+ return "true" if value else "false"
74
+
75
+
76
+ def _sanitize(text: str) -> str:
77
+ return " ".join(str(text).replace("\n", " ").replace("\r", " ").split())
78
+
79
+
80
+ def _action_to_log(action: ActionModel) -> str:
81
+ payload = action.model_dump(exclude_none=True)
82
+ return json.dumps(payload, separators=(",", ":"))
83
+
84
+
85
+ def _severity_to_priority(severity: str) -> str:
86
+ return {
87
+ "sev0": "p0",
88
+ "sev1": "p1",
89
+ "sev2": "p2",
90
+ "sev3": "p3",
91
+ }.get(severity, "p2")
92
+
93
+
94
+ def _infer_component(ticket, available_components: list[str]) -> str:
95
+ for candidate in ticket.component_candidates:
96
+ if candidate in available_components:
97
+ return candidate
98
+ return available_components[0] if available_components else "api-gateway"
99
+
100
+
101
+ def _infer_severity(ticket) -> str:
102
+ text = f"{ticket.title} {ticket.description}".lower()
103
+ if any(k in text for k in ["security", "unauthorized", "double charge", "data loss"]):
104
+ return "sev0"
105
+ if ticket.reporter_type == "monitoring" or any(
106
+ k in text for k in ["500", "503", "outage", "incident", "crash", "timeout"]
107
+ ):
108
+ return "sev1"
109
+ if any(k in text for k in ["latency", "slow", "degraded", "error", "failed"]):
110
+ return "sev2"
111
+ return "sev3"
112
+
113
+
114
+ def _fallback_action(observation, plans: dict[str, dict]) -> ActionModel:
115
+ ticket = observation.current_ticket
116
+ if ticket is None:
117
+ return ActionModel(action_type="next_ticket", next_ticket={})
118
+
119
+ ticket_id = ticket.ticket_id
120
+ plan = plans.setdefault(ticket_id, {"phase": 0})
121
+ phase = int(plan.get("phase", 0))
122
+
123
+ if phase == 0:
124
+ suspected = ticket.suspected_duplicate_ids or []
125
+ if suspected:
126
+ plan["phase"] = 3
127
+ return ActionModel(
128
+ action_type="mark_duplicate",
129
+ mark_duplicate={"canonical_ticket_id": suspected[0]},
130
+ )
131
+
132
+ component = _infer_component(ticket, observation.available_components)
133
+ severity = _infer_severity(ticket)
134
+ plan["phase"] = 1
135
+ plan["severity"] = severity
136
+ plan["component"] = component
137
+ return ActionModel(
138
+ action_type="classify",
139
+ classify={
140
+ "severity": severity,
141
+ "priority": _severity_to_priority(severity),
142
+ "component": component,
143
+ },
144
+ )
145
+
146
+ if phase == 1:
147
+ component = str(plan.get("component") or _infer_component(ticket, observation.available_components))
148
+ default_team = observation.available_teams[0] if observation.available_teams else "backend-api"
149
+ team = COMPONENT_TEAM_MAP.get(component, default_team)
150
+ plan["phase"] = 2
151
+ return ActionModel(action_type="assign", assign={"team": team})
152
+
153
+ if phase == 2:
154
+ sev = str(plan.get("severity", "sev2"))
155
+ plan["phase"] = 3
156
+ if sev in {"sev0", "sev1"}:
157
+ return ActionModel(
158
+ action_type="escalate_incident",
159
+ escalate_incident={"justification": "fallback escalation"},
160
+ )
161
+ return ActionModel(action_type="next_ticket", next_ticket={})
162
+
163
+ return ActionModel(action_type="next_ticket", next_ticket={})
164
+
165
+
166
+ def _guard_action(
167
+ action: ActionModel,
168
+ observation,
169
+ action_history_by_ticket: dict[str, list[str]],
170
+ steps_by_ticket: dict[str, int],
171
+ ) -> ActionModel:
172
+ ticket = observation.current_ticket
173
+ if ticket is None:
174
+ return action
175
+
176
+ ticket_id = ticket.ticket_id
177
+ history = action_history_by_ticket[ticket_id]
178
+ ticket_steps = steps_by_ticket[ticket_id]
179
+
180
+ if ticket_steps >= MAX_STEPS_PER_TICKET and action.action_type != "next_ticket":
181
+ return ActionModel(action_type="next_ticket", next_ticket={})
182
+
183
+ if action.action_type == "request_info" and "request_info" in history:
184
+ return ActionModel(action_type="next_ticket", next_ticket={})
185
+
186
+ if len(history) >= 2 and history[-1] == history[-2] == action.action_type and action.action_type != "next_ticket":
187
+ return ActionModel(action_type="next_ticket", next_ticket={})
188
+
189
+ return action
190
+
191
+
192
+ def _build_prompt(observation) -> str:
193
+ ticket = observation.current_ticket
194
+ if ticket is None:
195
+ return '{"action_type":"next_ticket","next_ticket":{}}'
196
+
197
+ return f"""Return ONLY JSON for the next bug-triage action.
198
+
199
+ Ticket ID: {ticket.ticket_id}
200
+ Title: {ticket.title}
201
+ Description: {ticket.description}
202
+ Reporter: {ticket.reporter_type}
203
+ Service: {ticket.service}
204
+ Tier: {ticket.customer_tier}
205
+ Repro Steps Present: {ticket.repro_steps_present}
206
+ Logs Present: {ticket.logs_present}
207
+ Suspected Duplicates: {ticket.suspected_duplicate_ids}
208
+
209
+ Last Result: {observation.last_action_result}
210
+ Available Teams: {observation.available_teams}
211
+ Available Components: {observation.available_components}
212
+
213
+ Allowed action_type values:
214
+ classify, assign, mark_duplicate, request_info, defer, close, escalate_incident, next_ticket
215
+
216
+ Rules:
217
+ - Do not repeat request_info on the same ticket.
218
+ - Avoid loops. If uncertain, use classify or next_ticket.
219
+ - Output valid JSON only.
220
+ """
221
+
222
+
223
+ def _parse_action(raw: str) -> ActionModel:
224
+ text = raw.strip()
225
+ if "```json" in text:
226
+ start = text.find("```json") + 7
227
+ end = text.find("```", start)
228
+ text = text[start:end].strip()
229
+ elif "```" in text:
230
+ start = text.find("```") + 3
231
+ end = text.find("```", start)
232
+ text = text[start:end].strip()
233
+
234
+ data = json.loads(text)
235
+ return ActionModel(**data)
236
+
237
+
238
+ def _run_task(task_id: str, env: BugTriageEnv, client: OpenAI) -> None:
239
+ print(f"[START] task={task_id} env={BENCHMARK} model={MODEL_NAME}")
240
+
241
+ step_no = 0
242
+ rewards: list[str] = []
243
+ success = False
244
+
245
+ api_disabled = False
246
+ plans: dict[str, dict] = {}
247
+ action_history_by_ticket: dict[str, list[str]] = defaultdict(list)
248
+ steps_by_ticket: dict[str, int] = defaultdict(int)
249
+
250
+ done = False
251
+ episode_actions: list[dict] = []
252
+ info = {"metrics": {}}
253
+
254
+ try:
255
+ obs = env.reset(task_id=task_id, seed=SEED)
256
+
257
+ while not done and step_no < MAX_STEPS:
258
+ step_no += 1
259
+ current_ticket_id = obs.current_ticket.ticket_id if obs.current_ticket else None
260
+
261
+ if not api_disabled:
262
+ try:
263
+ response = client.chat.completions.create(
264
+ model=MODEL_NAME,
265
+ messages=[
266
+ {
267
+ "role": "system",
268
+ "content": "You are an expert bug triage assistant. Return JSON only.",
269
+ },
270
+ {"role": "user", "content": _build_prompt(obs)},
271
+ ],
272
+ temperature=TEMPERATURE,
273
+ max_tokens=MAX_TOKENS,
274
+ )
275
+ raw = response.choices[0].message.content or ""
276
+ action = _parse_action(raw)
277
+ except Exception:
278
+ api_disabled = True
279
+ action = _fallback_action(obs, plans)
280
+ else:
281
+ action = _fallback_action(obs, plans)
282
+
283
+ action = _guard_action(action, obs, action_history_by_ticket, steps_by_ticket)
284
+
285
+ err_value = "null"
286
+ try:
287
+ obs, reward, done, info = env.step(action)
288
+ reward_value = f"{reward.step_reward:.2f}"
289
+ rewards.append(reward_value)
290
+
291
+ last_action_error = info.get("last_action_error") if isinstance(info, dict) else None
292
+ validation_error = info.get("validation_error") if isinstance(info, dict) else None
293
+ error_raw = last_action_error if last_action_error else validation_error
294
+ if error_raw:
295
+ err_value = _sanitize(error_raw)
296
+
297
+ print(
298
+ f"[STEP] step={step_no} action={_action_to_log(action)} "
299
+ f"reward={reward_value} done={_b(bool(done))} error={err_value}"
300
+ )
301
+
302
+ episode_actions.append(action.model_dump(exclude_none=True))
303
+ if current_ticket_id:
304
+ action_history_by_ticket[current_ticket_id].append(action.action_type)
305
+ steps_by_ticket[current_ticket_id] += 1
306
+ except Exception as exc:
307
+ err_value = _sanitize(str(exc))
308
+ print(
309
+ f"[STEP] step={step_no} action={_action_to_log(action)} "
310
+ f"reward=0.00 done=true error={err_value}"
311
+ )
312
+ rewards.append("0.00")
313
+ done = True
314
+
315
+ try:
316
+ grader = BugTriageGrader(task_id=task_id)
317
+ ground_truths = [
318
+ gt.model_dump() for gt in env.current_task.ground_truths
319
+ ] if env.current_task else []
320
+ grader_result = grader.grade_episode(
321
+ episode_actions=[{"action": a} for a in episode_actions],
322
+ ground_truths=ground_truths,
323
+ metrics=info.get("metrics", {}) if isinstance(info, dict) else {},
324
+ )
325
+ success = bool(grader_result.passed)
326
+ except Exception:
327
+ success = bool(done)
328
+ finally:
329
+ if hasattr(env, "close"):
330
+ try:
331
+ env.close()
332
+ except Exception:
333
+ pass
334
+
335
+ rewards_csv = ",".join(rewards)
336
+ print(f"[END] success={_b(success)} steps={step_no} rewards={rewards_csv}")
337
+
338
+
339
+ def main() -> int:
340
+ if not HF_TOKEN:
341
+ print("HF_TOKEN is required", file=sys.stderr)
342
+ return 1
343
+
344
+ client = OpenAI(api_key=HF_TOKEN, base_url=API_BASE_URL, max_retries=0, timeout=30)
345
+ env = BugTriageEnv()
346
+
347
+ for task_id in TASKS:
348
+ _run_task(task_id=task_id, env=env, client=client)
349
+
350
+ return 0
351
+
352
+
353
+ if __name__ == "__main__":
354
+ raise SystemExit(main())
openenv_bug_triage/app.py CHANGED
@@ -2,16 +2,28 @@
2
 
3
  from __future__ import annotations
4
 
 
5
  from typing import Optional
6
 
7
  from fastapi import FastAPI, HTTPException
 
 
8
  from pydantic import BaseModel
9
 
10
  from .env import BugTriageEnv
11
  from .models import ActionModel
12
 
13
 
14
- app = FastAPI(title="Bug Triage OpenEnv", version="0.1.0")
 
 
 
 
 
 
 
 
 
15
  env = BugTriageEnv()
16
 
17
 
@@ -24,13 +36,30 @@ class StepRequest(BaseModel):
24
  action: ActionModel
25
 
26
 
 
 
 
 
 
 
 
 
 
27
  @app.get("/health")
28
  def health() -> dict[str, str]:
29
  return {"status": "ok"}
30
 
31
 
 
 
 
 
 
 
 
32
  @app.post("/reset")
33
- def reset(req: ResetRequest) -> dict:
 
34
  observation = env.reset(task_id=req.task_id, seed=req.seed)
35
  return observation.model_dump(mode="json")
36
 
 
2
 
3
  from __future__ import annotations
4
 
5
+ from pathlib import Path
6
  from typing import Optional
7
 
8
  from fastapi import FastAPI, HTTPException
9
+ from fastapi.responses import FileResponse
10
+ from fastapi.staticfiles import StaticFiles
11
  from pydantic import BaseModel
12
 
13
  from .env import BugTriageEnv
14
  from .models import ActionModel
15
 
16
 
17
+ app = FastAPI(
18
+ title="Bug Triage OpenEnv",
19
+ version="0.1.0",
20
+ description="Real-world OpenEnv environment for bug triage training",
21
+ )
22
+
23
+ UI_DIR = Path(__file__).parent / "ui"
24
+ if UI_DIR.exists():
25
+ app.mount("/ui", StaticFiles(directory=str(UI_DIR)), name="ui")
26
+
27
  env = BugTriageEnv()
28
 
29
 
 
36
  action: ActionModel
37
 
38
 
39
+ @app.get("/")
40
+ def index():
41
+ """Serve the dark-mode frontend."""
42
+ index_file = UI_DIR / "index.html"
43
+ if index_file.exists():
44
+ return FileResponse(index_file)
45
+ return {"message": "Bug Triage OpenEnv API", "docs": "/docs"}
46
+
47
+
48
  @app.get("/health")
49
  def health() -> dict[str, str]:
50
  return {"status": "ok"}
51
 
52
 
53
+ @app.get("/reset")
54
+ def reset_get(task_id: Optional[str] = None, seed: Optional[int] = None) -> dict:
55
+ """Validator-friendly reset endpoint (GET)."""
56
+ observation = env.reset(task_id=task_id, seed=seed)
57
+ return observation.model_dump(mode="json")
58
+
59
+
60
  @app.post("/reset")
61
+ def reset_post(req: ResetRequest) -> dict:
62
+ """Typed reset endpoint (POST)."""
63
  observation = env.reset(task_id=req.task_id, seed=req.seed)
64
  return observation.model_dump(mode="json")
65
 
openenv_bug_triage/models.py CHANGED
@@ -152,7 +152,7 @@ class ActionModel(BaseModel):
152
 
153
  class RewardModel(BaseModel):
154
  """Reward information for a step."""
155
- step_reward: float = Field(..., ge=-1.0, le=1.0)
156
  cumulative_reward: float
157
  reward_breakdown: dict[str, float] = Field(default_factory=dict)
158
 
 
152
 
153
  class RewardModel(BaseModel):
154
  """Reward information for a step."""
155
+ step_reward: float = Field(..., ge=0.0, le=1.0)
156
  cumulative_reward: float
157
  reward_breakdown: dict[str, float] = Field(default_factory=dict)
158
 
openenv_bug_triage/reward.py CHANGED
@@ -9,7 +9,10 @@ from .models import ActionModel, TicketGroundTruth
9
  class RewardCalculator:
10
  """Calculates rewards for bug triage actions."""
11
 
12
- # Dense progress signals
 
 
 
13
  CORRECT_SEVERITY = 0.20
14
  CORRECT_PRIORITY = 0.15
15
  CORRECT_COMPONENT = 0.15
@@ -18,16 +21,16 @@ class RewardCalculator:
18
  CORRECT_REQUEST_INFO = 0.10
19
  CORRECT_ESCALATION = 0.15
20
 
21
- # Negative signals
22
- INCORRECT_CLOSE_DEFER = -0.20
23
- MISSED_ESCALATION = -0.15
24
- INVALID_ACTION = -0.05
25
- REPEATED_NOOP = -0.02
26
- UNNECESSARY_SWITCH = -0.01
27
 
28
  # Terminal bonuses/penalties
29
  ALL_CRITICAL_TRIAGED = 0.10
30
- BUDGET_EXHAUSTED_CRITICAL_REMAINING = -0.10
31
 
32
  def __init__(self):
33
  self.action_history = []
@@ -45,16 +48,17 @@ class RewardCalculator:
45
  ) -> tuple[float, dict[str, float]]:
46
  """
47
  Calculate reward for a single step.
 
48
 
49
  Returns:
50
  tuple of (clamped_reward, breakdown_dict)
51
  """
52
  breakdown = {}
53
- total_reward = 0.0
54
 
55
  if not is_valid:
56
- breakdown["invalid_action"] = self.INVALID_ACTION
57
- total_reward += self.INVALID_ACTION
58
  return self._clamp_reward(total_reward), breakdown
59
 
60
  # Track action history for loop detection
@@ -101,21 +105,21 @@ class RewardCalculator:
101
  # Penalize incorrect close/defer
102
  if action.action_type in ["close", "defer"]:
103
  if not ground_truth.duplicate_of and ground_truth.needs_more_info:
104
- breakdown["incorrect_close_defer"] = self.INCORRECT_CLOSE_DEFER
105
- total_reward += self.INCORRECT_CLOSE_DEFER
106
 
107
  # Penalize missed critical escalation
108
  if is_critical_ticket and action.action_type != "escalate_incident":
109
  if ground_truth.true_severity in ["sev0", "sev1"]:
110
- breakdown["missed_escalation"] = self.MISSED_ESCALATION
111
- total_reward += self.MISSED_ESCALATION
112
 
113
  # Check for repeated no-op behavior
114
  if len(self.action_history) >= 3:
115
  last_three = self.action_history[-3:]
116
  if last_three[0] == last_three[1] == last_three[2]:
117
- breakdown["repeated_noop"] = self.REPEATED_NOOP
118
- total_reward += self.REPEATED_NOOP
119
 
120
  # Terminal bonuses/penalties
121
  if is_terminal:
@@ -124,14 +128,14 @@ class RewardCalculator:
124
  total_reward += self.ALL_CRITICAL_TRIAGED
125
 
126
  if budget_exhausted_with_critical:
127
- breakdown["budget_exhausted"] = self.BUDGET_EXHAUSTED_CRITICAL_REMAINING
128
- total_reward += self.BUDGET_EXHAUSTED_CRITICAL_REMAINING
129
 
130
  return self._clamp_reward(total_reward), breakdown
131
 
132
  def _clamp_reward(self, reward: float) -> float:
133
- """Clamp reward to [-1.0, 1.0] range."""
134
- return max(-1.0, min(1.0, reward))
135
 
136
  def reset(self):
137
  """Reset action history."""
 
9
  class RewardCalculator:
10
  """Calculates rewards for bug triage actions."""
11
 
12
+ # Base reward for any action
13
+ BASE_REWARD = 0.50
14
+
15
+ # Dense progress signals (bonuses on top of base)
16
  CORRECT_SEVERITY = 0.20
17
  CORRECT_PRIORITY = 0.15
18
  CORRECT_COMPONENT = 0.15
 
21
  CORRECT_REQUEST_INFO = 0.10
22
  CORRECT_ESCALATION = 0.15
23
 
24
+ # Negative signals (penalties reducing from base)
25
+ INCORRECT_CLOSE_DEFER = 0.20
26
+ MISSED_ESCALATION = 0.15
27
+ INVALID_ACTION = 0.30
28
+ REPEATED_NOOP = 0.10
29
+ UNNECESSARY_SWITCH = 0.05
30
 
31
  # Terminal bonuses/penalties
32
  ALL_CRITICAL_TRIAGED = 0.10
33
+ BUDGET_EXHAUSTED_CRITICAL_REMAINING = 0.10
34
 
35
  def __init__(self):
36
  self.action_history = []
 
48
  ) -> tuple[float, dict[str, float]]:
49
  """
50
  Calculate reward for a single step.
51
+ Rewards are in range [0.0, 1.0].
52
 
53
  Returns:
54
  tuple of (clamped_reward, breakdown_dict)
55
  """
56
  breakdown = {}
57
+ total_reward = self.BASE_REWARD # Start with base reward
58
 
59
  if not is_valid:
60
+ breakdown["invalid_action_penalty"] = -self.INVALID_ACTION
61
+ total_reward -= self.INVALID_ACTION
62
  return self._clamp_reward(total_reward), breakdown
63
 
64
  # Track action history for loop detection
 
105
  # Penalize incorrect close/defer
106
  if action.action_type in ["close", "defer"]:
107
  if not ground_truth.duplicate_of and ground_truth.needs_more_info:
108
+ breakdown["incorrect_close_defer_penalty"] = -self.INCORRECT_CLOSE_DEFER
109
+ total_reward -= self.INCORRECT_CLOSE_DEFER
110
 
111
  # Penalize missed critical escalation
112
  if is_critical_ticket and action.action_type != "escalate_incident":
113
  if ground_truth.true_severity in ["sev0", "sev1"]:
114
+ breakdown["missed_escalation_penalty"] = -self.MISSED_ESCALATION
115
+ total_reward -= self.MISSED_ESCALATION
116
 
117
  # Check for repeated no-op behavior
118
  if len(self.action_history) >= 3:
119
  last_three = self.action_history[-3:]
120
  if last_three[0] == last_three[1] == last_three[2]:
121
+ breakdown["repeated_noop_penalty"] = -self.REPEATED_NOOP
122
+ total_reward -= self.REPEATED_NOOP
123
 
124
  # Terminal bonuses/penalties
125
  if is_terminal:
 
128
  total_reward += self.ALL_CRITICAL_TRIAGED
129
 
130
  if budget_exhausted_with_critical:
131
+ breakdown["budget_exhausted_penalty"] = -self.BUDGET_EXHAUSTED_CRITICAL_REMAINING
132
+ total_reward -= self.BUDGET_EXHAUSTED_CRITICAL_REMAINING
133
 
134
  return self._clamp_reward(total_reward), breakdown
135
 
136
  def _clamp_reward(self, reward: float) -> float:
137
+ """Clamp reward to [0.0, 1.0] range."""
138
+ return max(0.0, min(1.0, reward))
139
 
140
  def reset(self):
141
  """Reset action history."""
openenv_bug_triage/ui/app.js ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const els = {
2
+ taskSelect: document.getElementById("task-select"),
3
+ seedInput: document.getElementById("seed-input"),
4
+ resetBtn: document.getElementById("reset-btn"),
5
+ stateBtn: document.getElementById("state-btn"),
6
+ stepBtn: document.getElementById("step-btn"),
7
+ status: document.getElementById("status"),
8
+ actionType: document.getElementById("action-type"),
9
+ payloadFields: document.getElementById("payload-fields"),
10
+ actionPreview: document.getElementById("action-preview"),
11
+ ticketMeta: document.getElementById("ticket-meta"),
12
+ ticketBody: document.getElementById("ticket-body"),
13
+ stepLog: document.getElementById("step-log"),
14
+ stateJson: document.getElementById("state-json"),
15
+ stepsUsed: document.getElementById("steps-used"),
16
+ stepsRemaining: document.getElementById("steps-remaining"),
17
+ doneFlag: document.getElementById("done-flag"),
18
+ cumulativeReward: document.getElementById("cumulative-reward"),
19
+ };
20
+
21
+ let currentObservation = null;
22
+ let done = false;
23
+ let stepCount = 0;
24
+ let cumulativeReward = 0;
25
+
26
+ const DEFAULT_COMPONENTS = [
27
+ "api-gateway",
28
+ "auth-service",
29
+ "user-service",
30
+ "payment-service",
31
+ "web-app",
32
+ "ios-app",
33
+ "android-app",
34
+ "database",
35
+ "cache",
36
+ "cdn",
37
+ ];
38
+
39
+ const DEFAULT_TEAMS = [
40
+ "backend-api",
41
+ "frontend-web",
42
+ "mobile-ios",
43
+ "mobile-android",
44
+ "infrastructure",
45
+ "data-platform",
46
+ ];
47
+
48
+ function setStatus(text, kind = "idle") {
49
+ els.status.textContent = text;
50
+ els.status.className = `status status-${kind}`;
51
+ }
52
+
53
+ function pretty(obj) {
54
+ return JSON.stringify(obj, null, 2);
55
+ }
56
+
57
+ function optionHtml(value, selectedValue) {
58
+ const selected = value === selectedValue ? "selected" : "";
59
+ return `<option value="${value}" ${selected}>${value}</option>`;
60
+ }
61
+
62
+ function componentsList() {
63
+ return currentObservation?.available_components?.length
64
+ ? currentObservation.available_components
65
+ : DEFAULT_COMPONENTS;
66
+ }
67
+
68
+ function teamsList() {
69
+ return currentObservation?.available_teams?.length
70
+ ? currentObservation.available_teams
71
+ : DEFAULT_TEAMS;
72
+ }
73
+
74
+ function renderPayloadFields() {
75
+ const actionType = els.actionType.value;
76
+ const components = componentsList();
77
+ const teams = teamsList();
78
+
79
+ if (actionType === "classify") {
80
+ els.payloadFields.innerHTML = `
81
+ <label>
82
+ <span>Severity</span>
83
+ <select id="f-severity">
84
+ <option value="sev0">sev0</option>
85
+ <option value="sev1">sev1</option>
86
+ <option value="sev2" selected>sev2</option>
87
+ <option value="sev3">sev3</option>
88
+ </select>
89
+ </label>
90
+ <label>
91
+ <span>Priority</span>
92
+ <select id="f-priority">
93
+ <option value="p0">p0</option>
94
+ <option value="p1">p1</option>
95
+ <option value="p2" selected>p2</option>
96
+ <option value="p3">p3</option>
97
+ </select>
98
+ </label>
99
+ <label>
100
+ <span>Component</span>
101
+ <select id="f-component">
102
+ ${components.map((c, i) => optionHtml(c, i === 0 ? c : "")).join("")}
103
+ </select>
104
+ </label>
105
+ `;
106
+ return;
107
+ }
108
+
109
+ if (actionType === "assign") {
110
+ els.payloadFields.innerHTML = `
111
+ <label>
112
+ <span>Team</span>
113
+ <select id="f-team">
114
+ ${teams.map((t, i) => optionHtml(t, i === 0 ? t : "")).join("")}
115
+ </select>
116
+ </label>
117
+ `;
118
+ return;
119
+ }
120
+
121
+ if (actionType === "mark_duplicate") {
122
+ els.payloadFields.innerHTML = `
123
+ <label>
124
+ <span>Canonical Ticket ID</span>
125
+ <input id="f-canonical" type="text" placeholder="BUG-1001" />
126
+ </label>
127
+ `;
128
+ return;
129
+ }
130
+
131
+ if (actionType === "request_info") {
132
+ els.payloadFields.innerHTML = `
133
+ <label>
134
+ <span>Info Type</span>
135
+ <select id="f-info-type">
136
+ <option value="repro_steps">repro_steps</option>
137
+ <option value="logs">logs</option>
138
+ <option value="both" selected>both</option>
139
+ </select>
140
+ </label>
141
+ `;
142
+ return;
143
+ }
144
+
145
+ if (actionType === "defer") {
146
+ els.payloadFields.innerHTML = `
147
+ <label>
148
+ <span>Reason</span>
149
+ <input id="f-defer-reason" type="text" placeholder="Waiting for roadmap decision" />
150
+ </label>
151
+ `;
152
+ return;
153
+ }
154
+
155
+ if (actionType === "close") {
156
+ els.payloadFields.innerHTML = `
157
+ <label>
158
+ <span>Close Reason</span>
159
+ <select id="f-close-reason">
160
+ <option value="invalid">invalid</option>
161
+ <option value="wont_fix">wont_fix</option>
162
+ <option value="cannot_reproduce">cannot_reproduce</option>
163
+ <option value="resolved">resolved</option>
164
+ </select>
165
+ </label>
166
+ `;
167
+ return;
168
+ }
169
+
170
+ if (actionType === "escalate_incident") {
171
+ els.payloadFields.innerHTML = `
172
+ <label>
173
+ <span>Justification</span>
174
+ <textarea id="f-justification" placeholder="Production impact and SLA risk detected."></textarea>
175
+ </label>
176
+ `;
177
+ return;
178
+ }
179
+
180
+ els.payloadFields.innerHTML = `<p class="ticket-meta">No payload required.</p>`;
181
+ }
182
+
183
+ function buildAction() {
184
+ const type = els.actionType.value;
185
+ const action = { action_type: type };
186
+
187
+ if (type === "classify") {
188
+ action.classify = {
189
+ severity: document.getElementById("f-severity").value,
190
+ priority: document.getElementById("f-priority").value,
191
+ component: document.getElementById("f-component").value,
192
+ };
193
+ } else if (type === "assign") {
194
+ action.assign = {
195
+ team: document.getElementById("f-team").value,
196
+ };
197
+ } else if (type === "mark_duplicate") {
198
+ action.mark_duplicate = {
199
+ canonical_ticket_id: document.getElementById("f-canonical").value.trim(),
200
+ };
201
+ } else if (type === "request_info") {
202
+ action.request_info = {
203
+ info_type: document.getElementById("f-info-type").value,
204
+ };
205
+ } else if (type === "defer") {
206
+ action.defer = {
207
+ reason: document.getElementById("f-defer-reason").value.trim() || "backlog",
208
+ };
209
+ } else if (type === "close") {
210
+ action.close = {
211
+ reason: document.getElementById("f-close-reason").value,
212
+ };
213
+ } else if (type === "escalate_incident") {
214
+ action.escalate_incident = {
215
+ justification: document.getElementById("f-justification").value.trim() || "Critical impact",
216
+ };
217
+ } else if (type === "next_ticket") {
218
+ action.next_ticket = {};
219
+ }
220
+
221
+ return action;
222
+ }
223
+
224
+ function renderPreview() {
225
+ const action = buildAction();
226
+ els.actionPreview.textContent = pretty(action);
227
+ }
228
+
229
+ function renderObservation(obs) {
230
+ currentObservation = obs;
231
+ if (!obs || !obs.current_ticket) {
232
+ els.ticketMeta.textContent = "No active ticket";
233
+ els.ticketBody.textContent = "{}";
234
+ return;
235
+ }
236
+
237
+ const t = obs.current_ticket;
238
+ els.ticketMeta.textContent = `${t.ticket_id} | ${t.service} | ${t.reporter_type} | tier:${t.customer_tier}`;
239
+ els.ticketBody.textContent = pretty(t);
240
+
241
+ els.stepsUsed.textContent = String(obs.steps_used);
242
+ els.stepsRemaining.textContent = String(obs.steps_remaining);
243
+
244
+ renderPayloadFields();
245
+ renderPreview();
246
+ }
247
+
248
+ function appendLog(text) {
249
+ const li = document.createElement("li");
250
+ li.textContent = text;
251
+ els.stepLog.prepend(li);
252
+ }
253
+
254
+ async function api(url, method = "GET", body = null) {
255
+ const opts = { method, headers: {} };
256
+ if (body !== null) {
257
+ opts.headers["Content-Type"] = "application/json";
258
+ opts.body = JSON.stringify(body);
259
+ }
260
+
261
+ const res = await fetch(url, opts);
262
+ const data = await res.json().catch(() => null);
263
+ if (!res.ok) {
264
+ const msg = data?.detail || `HTTP ${res.status}`;
265
+ throw new Error(msg);
266
+ }
267
+ return data;
268
+ }
269
+
270
+ async function doReset() {
271
+ const task_id = els.taskSelect.value;
272
+ const seed = Number(els.seedInput.value || 42);
273
+
274
+ try {
275
+ setStatus("Resetting episode...", "idle");
276
+ const obs = await api("/reset", "POST", { task_id, seed });
277
+ done = false;
278
+ stepCount = 0;
279
+ cumulativeReward = 0;
280
+ els.stepLog.innerHTML = "";
281
+ renderObservation(obs);
282
+ els.doneFlag.textContent = "false";
283
+ els.cumulativeReward.textContent = "0.00";
284
+ setStatus(`Ready on ${obs.current_ticket?.ticket_id || "no-ticket"}`, "ok");
285
+ appendLog(`reset task=${task_id} seed=${seed}`);
286
+ } catch (err) {
287
+ setStatus(`Reset failed: ${err.message}`, "error");
288
+ }
289
+ }
290
+
291
+ async function doStep() {
292
+ if (!currentObservation) {
293
+ setStatus("Run Reset first.", "error");
294
+ return;
295
+ }
296
+ if (done) {
297
+ setStatus("Episode already done. Reset to start another.", "error");
298
+ return;
299
+ }
300
+
301
+ const action = buildAction();
302
+
303
+ try {
304
+ const result = await api("/step", "POST", { action });
305
+ stepCount += 1;
306
+ done = Boolean(result.done);
307
+
308
+ const reward = Number(result.reward?.step_reward || 0);
309
+ cumulativeReward = Number(result.reward?.cumulative_reward || cumulativeReward);
310
+
311
+ renderObservation(result.observation);
312
+ els.doneFlag.textContent = String(done);
313
+ els.cumulativeReward.textContent = cumulativeReward.toFixed(2);
314
+
315
+ const err = result.info?.validation_error ? ` error=${result.info.validation_error}` : "";
316
+ appendLog(
317
+ `step=${stepCount} action=${action.action_type} reward=${reward.toFixed(2)} done=${done}${err}`
318
+ );
319
+
320
+ setStatus(done ? "Episode completed." : "Step accepted.", done ? "ok" : "idle");
321
+ } catch (err) {
322
+ setStatus(`Step failed: ${err.message}`, "error");
323
+ appendLog(`step_error ${err.message}`);
324
+ }
325
+ }
326
+
327
+ async function fetchState() {
328
+ try {
329
+ const state = await api("/state", "GET");
330
+ els.stateJson.textContent = pretty(state);
331
+ setStatus("State fetched.", "ok");
332
+ } catch (err) {
333
+ setStatus(`State fetch failed: ${err.message}`, "error");
334
+ }
335
+ }
336
+
337
+ els.actionType.addEventListener("change", () => {
338
+ renderPayloadFields();
339
+ renderPreview();
340
+ });
341
+
342
+ els.payloadFields.addEventListener("input", renderPreview);
343
+ els.payloadFields.addEventListener("change", renderPreview);
344
+ els.resetBtn.addEventListener("click", doReset);
345
+ els.stepBtn.addEventListener("click", doStep);
346
+ els.stateBtn.addEventListener("click", fetchState);
347
+
348
+ renderPayloadFields();
349
+ renderPreview();
350
+ setStatus("Idle", "idle");
351
+
openenv_bug_triage/ui/index.html ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Bug Triage OpenEnv Console</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
10
+ <link rel="stylesheet" href="/ui/styles.css" />
11
+ </head>
12
+ <body>
13
+ <div class="aurora" aria-hidden="true"></div>
14
+
15
+ <header class="hero reveal">
16
+ <div>
17
+ <p class="kicker">OpenEnv - Dark Ops Console</p>
18
+ <h1>Bug Triage Environment</h1>
19
+ <p class="subtitle">Interactive dark-mode frontend for reset / step / state workflows.</p>
20
+ </div>
21
+ <div class="pillset">
22
+ <span class="pill">Real-World Task</span>
23
+ <span class="pill">Typed Actions</span>
24
+ <span class="pill">Dense Rewards</span>
25
+ </div>
26
+ </header>
27
+
28
+ <main class="layout">
29
+ <section class="card reveal" id="controls-card">
30
+ <h2>Session Controls</h2>
31
+ <div class="grid-2">
32
+ <label>
33
+ <span>Task</span>
34
+ <select id="task-select">
35
+ <option value="bug_triage_easy">bug_triage_easy</option>
36
+ <option value="bug_triage_medium">bug_triage_medium</option>
37
+ <option value="bug_triage_hard">bug_triage_hard</option>
38
+ </select>
39
+ </label>
40
+ <label>
41
+ <span>Seed</span>
42
+ <input id="seed-input" type="number" value="42" min="0" step="1" />
43
+ </label>
44
+ </div>
45
+ <div class="button-row">
46
+ <button id="reset-btn" class="btn btn-accent">Reset Episode</button>
47
+ <button id="state-btn" class="btn btn-ghost">Fetch State</button>
48
+ </div>
49
+ <div id="status" class="status status-idle">Idle</div>
50
+ <div class="summary-grid">
51
+ <article>
52
+ <h3>Steps</h3>
53
+ <p id="steps-used">0</p>
54
+ </article>
55
+ <article>
56
+ <h3>Remaining</h3>
57
+ <p id="steps-remaining">0</p>
58
+ </article>
59
+ <article>
60
+ <h3>Done</h3>
61
+ <p id="done-flag">false</p>
62
+ </article>
63
+ <article>
64
+ <h3>Cumulative</h3>
65
+ <p id="cumulative-reward">0.00</p>
66
+ </article>
67
+ </div>
68
+ </section>
69
+
70
+ <section class="card reveal" id="ticket-card">
71
+ <h2>Current Ticket</h2>
72
+ <div id="ticket-meta" class="ticket-meta">Run reset to load a task.</div>
73
+ <pre id="ticket-body" class="json-view">{}</pre>
74
+ </section>
75
+
76
+ <section class="card reveal" id="action-card">
77
+ <h2>Action Composer</h2>
78
+ <label>
79
+ <span>Action Type</span>
80
+ <select id="action-type">
81
+ <option value="classify">classify</option>
82
+ <option value="assign">assign</option>
83
+ <option value="mark_duplicate">mark_duplicate</option>
84
+ <option value="request_info">request_info</option>
85
+ <option value="defer">defer</option>
86
+ <option value="close">close</option>
87
+ <option value="escalate_incident">escalate_incident</option>
88
+ <option value="next_ticket">next_ticket</option>
89
+ </select>
90
+ </label>
91
+ <div id="payload-fields" class="payload-grid"></div>
92
+ <div class="button-row">
93
+ <button id="step-btn" class="btn btn-hot">Step</button>
94
+ </div>
95
+ <pre id="action-preview" class="json-view">{"action_type":"classify"}</pre>
96
+ </section>
97
+
98
+ <section class="card reveal" id="logs-card">
99
+ <h2>Episode Log</h2>
100
+ <ol id="step-log" class="log-list"></ol>
101
+ </section>
102
+
103
+ <section class="card reveal" id="state-card">
104
+ <h2>State Snapshot</h2>
105
+ <pre id="state-json" class="json-view">{}</pre>
106
+ </section>
107
+ </main>
108
+
109
+ <script src="/ui/app.js"></script>
110
+ </body>
111
+ </html>
112
+
openenv_bug_triage/ui/styles.css ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg: #070c14;
3
+ --bg-soft: #0d1422;
4
+ --surface: rgba(15, 24, 38, 0.82);
5
+ --surface-strong: rgba(18, 30, 48, 0.96);
6
+ --edge: rgba(132, 168, 224, 0.24);
7
+ --text: #e8f0ff;
8
+ --muted: #9db0cd;
9
+ --accent: #25d8b4;
10
+ --hot: #ff9150;
11
+ --ok: #6be6a8;
12
+ --warn: #ffd166;
13
+ --bad: #ff5f73;
14
+ --mono: "IBM Plex Mono", "Fira Code", Consolas, monospace;
15
+ --sans: "Space Grotesk", "Segoe UI", sans-serif;
16
+ }
17
+
18
+ * {
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ html,
23
+ body {
24
+ margin: 0;
25
+ min-height: 100%;
26
+ background: var(--bg);
27
+ color: var(--text);
28
+ font-family: var(--sans);
29
+ }
30
+
31
+ body {
32
+ position: relative;
33
+ overflow-x: hidden;
34
+ padding: 24px;
35
+ }
36
+
37
+ .aurora {
38
+ position: fixed;
39
+ inset: -25vmax;
40
+ background:
41
+ radial-gradient(40vmax 32vmax at 10% 16%, rgba(37, 216, 180, 0.18), transparent 70%),
42
+ radial-gradient(45vmax 36vmax at 92% 8%, rgba(255, 145, 80, 0.14), transparent 72%),
43
+ radial-gradient(50vmax 44vmax at 48% 105%, rgba(71, 130, 255, 0.16), transparent 74%);
44
+ filter: blur(18px);
45
+ pointer-events: none;
46
+ z-index: -1;
47
+ }
48
+
49
+ .hero {
50
+ display: flex;
51
+ justify-content: space-between;
52
+ align-items: flex-start;
53
+ gap: 20px;
54
+ margin-bottom: 18px;
55
+ }
56
+
57
+ .kicker {
58
+ margin: 0 0 4px;
59
+ letter-spacing: 0.12em;
60
+ text-transform: uppercase;
61
+ font-size: 0.72rem;
62
+ color: var(--accent);
63
+ }
64
+
65
+ h1 {
66
+ margin: 0;
67
+ font-size: clamp(1.5rem, 2.8vw, 2.35rem);
68
+ line-height: 1.08;
69
+ }
70
+
71
+ .subtitle {
72
+ margin-top: 8px;
73
+ color: var(--muted);
74
+ max-width: 62ch;
75
+ }
76
+
77
+ .pillset {
78
+ display: flex;
79
+ flex-wrap: wrap;
80
+ gap: 8px;
81
+ }
82
+
83
+ .pill {
84
+ border: 1px solid var(--edge);
85
+ padding: 6px 10px;
86
+ border-radius: 999px;
87
+ font-size: 0.72rem;
88
+ color: var(--muted);
89
+ background: rgba(9, 16, 28, 0.6);
90
+ }
91
+
92
+ .layout {
93
+ display: grid;
94
+ gap: 14px;
95
+ grid-template-columns: repeat(12, minmax(0, 1fr));
96
+ }
97
+
98
+ #controls-card { grid-column: span 4; }
99
+ #ticket-card { grid-column: span 8; }
100
+ #action-card { grid-column: span 5; }
101
+ #logs-card { grid-column: span 7; }
102
+ #state-card { grid-column: span 12; }
103
+
104
+ .card {
105
+ background: linear-gradient(160deg, var(--surface), var(--surface-strong));
106
+ border: 1px solid var(--edge);
107
+ border-radius: 16px;
108
+ padding: 16px;
109
+ box-shadow: 0 18px 40px rgba(0, 0, 0, 0.26);
110
+ backdrop-filter: blur(10px);
111
+ }
112
+
113
+ .card h2 {
114
+ margin: 0 0 12px;
115
+ font-size: 1rem;
116
+ }
117
+
118
+ .grid-2 {
119
+ display: grid;
120
+ grid-template-columns: 1fr 1fr;
121
+ gap: 10px;
122
+ }
123
+
124
+ label {
125
+ display: block;
126
+ }
127
+
128
+ label > span {
129
+ display: block;
130
+ margin-bottom: 6px;
131
+ font-size: 0.8rem;
132
+ color: var(--muted);
133
+ }
134
+
135
+ input,
136
+ select,
137
+ textarea,
138
+ button {
139
+ font: inherit;
140
+ }
141
+
142
+ input,
143
+ select,
144
+ textarea {
145
+ width: 100%;
146
+ border: 1px solid var(--edge);
147
+ background: rgba(7, 13, 22, 0.88);
148
+ color: var(--text);
149
+ border-radius: 10px;
150
+ padding: 10px 12px;
151
+ }
152
+
153
+ textarea {
154
+ min-height: 88px;
155
+ resize: vertical;
156
+ }
157
+
158
+ .button-row {
159
+ margin-top: 12px;
160
+ display: flex;
161
+ gap: 10px;
162
+ flex-wrap: wrap;
163
+ }
164
+
165
+ .btn {
166
+ border: 0;
167
+ border-radius: 10px;
168
+ cursor: pointer;
169
+ padding: 10px 14px;
170
+ font-weight: 600;
171
+ transition: transform 140ms ease, filter 140ms ease;
172
+ }
173
+
174
+ .btn:hover {
175
+ transform: translateY(-1px);
176
+ filter: brightness(1.06);
177
+ }
178
+
179
+ .btn-accent {
180
+ background: linear-gradient(140deg, #22bfa8, #1ed3ba);
181
+ color: #08131c;
182
+ }
183
+
184
+ .btn-hot {
185
+ background: linear-gradient(140deg, #ff7f45, #ffa15f);
186
+ color: #241208;
187
+ }
188
+
189
+ .btn-ghost {
190
+ background: rgba(17, 27, 43, 0.85);
191
+ color: var(--text);
192
+ border: 1px solid var(--edge);
193
+ }
194
+
195
+ .status {
196
+ margin-top: 12px;
197
+ border-radius: 10px;
198
+ border: 1px solid var(--edge);
199
+ padding: 9px 12px;
200
+ font-size: 0.9rem;
201
+ }
202
+
203
+ .status-ok {
204
+ color: var(--ok);
205
+ border-color: rgba(107, 230, 168, 0.4);
206
+ }
207
+
208
+ .status-error {
209
+ color: var(--bad);
210
+ border-color: rgba(255, 95, 115, 0.45);
211
+ }
212
+
213
+ .summary-grid {
214
+ margin-top: 12px;
215
+ display: grid;
216
+ grid-template-columns: repeat(4, 1fr);
217
+ gap: 8px;
218
+ }
219
+
220
+ .summary-grid article {
221
+ border: 1px solid var(--edge);
222
+ border-radius: 10px;
223
+ padding: 8px;
224
+ background: rgba(6, 12, 22, 0.78);
225
+ }
226
+
227
+ .summary-grid h3 {
228
+ margin: 0;
229
+ font-size: 0.7rem;
230
+ text-transform: uppercase;
231
+ letter-spacing: 0.08em;
232
+ color: var(--muted);
233
+ }
234
+
235
+ .summary-grid p {
236
+ margin: 6px 0 0;
237
+ font-family: var(--mono);
238
+ }
239
+
240
+ .ticket-meta {
241
+ color: var(--muted);
242
+ font-size: 0.9rem;
243
+ margin-bottom: 8px;
244
+ }
245
+
246
+ .payload-grid {
247
+ display: grid;
248
+ gap: 10px;
249
+ }
250
+
251
+ .json-view {
252
+ margin: 0;
253
+ white-space: pre-wrap;
254
+ word-break: break-word;
255
+ border-radius: 10px;
256
+ border: 1px solid var(--edge);
257
+ padding: 12px;
258
+ background: rgba(5, 10, 18, 0.9);
259
+ font-family: var(--mono);
260
+ font-size: 0.82rem;
261
+ max-height: 300px;
262
+ overflow: auto;
263
+ }
264
+
265
+ .log-list {
266
+ margin: 0;
267
+ padding-left: 18px;
268
+ max-height: 360px;
269
+ overflow: auto;
270
+ }
271
+
272
+ .log-list li {
273
+ margin-bottom: 8px;
274
+ padding: 10px;
275
+ border-radius: 10px;
276
+ border: 1px solid var(--edge);
277
+ background: rgba(6, 11, 18, 0.86);
278
+ font-family: var(--mono);
279
+ font-size: 0.77rem;
280
+ }
281
+
282
+ .reveal {
283
+ animation: riseIn 340ms ease both;
284
+ }
285
+
286
+ @keyframes riseIn {
287
+ from {
288
+ opacity: 0;
289
+ transform: translateY(8px);
290
+ }
291
+ to {
292
+ opacity: 1;
293
+ transform: translateY(0);
294
+ }
295
+ }
296
+
297
+ @media (max-width: 1100px) {
298
+ #controls-card,
299
+ #ticket-card,
300
+ #action-card,
301
+ #logs-card,
302
+ #state-card {
303
+ grid-column: span 12;
304
+ }
305
+
306
+ .summary-grid {
307
+ grid-template-columns: repeat(2, 1fr);
308
+ }
309
+ }
310
+
311
+ @media (max-width: 640px) {
312
+ body {
313
+ padding: 14px;
314
+ }
315
+
316
+ .hero {
317
+ flex-direction: column;
318
+ }
319
+
320
+ .grid-2 {
321
+ grid-template-columns: 1fr;
322
+ }
323
+ }
precheck.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pre-submission validator for Bug Triage OpenEnv.
3
+
4
+ Checks:
5
+ 1) HF Space /reset returns HTTP 200
6
+ 2) docker build succeeds
7
+ 3) openenv validate succeeds
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import os
14
+ import shutil
15
+ import subprocess
16
+ import sys
17
+ import urllib.error
18
+ import urllib.request
19
+ from pathlib import Path
20
+
21
+
22
+ def log(msg: str) -> None:
23
+ print(msg)
24
+
25
+
26
+ def run(cmd: list[str], cwd: Path | None = None, timeout: int = 1800) -> tuple[int, str, str]:
27
+ proc = subprocess.run(
28
+ cmd,
29
+ cwd=str(cwd) if cwd else None,
30
+ capture_output=True,
31
+ text=True,
32
+ timeout=timeout,
33
+ )
34
+ return proc.returncode, proc.stdout.strip(), proc.stderr.strip()
35
+
36
+
37
+ def check_space(space_url: str, timeout_seconds: int) -> tuple[bool, str]:
38
+ reset_url = f"{space_url.rstrip('/')}/reset"
39
+ req = urllib.request.Request(reset_url, method="GET")
40
+ try:
41
+ with urllib.request.urlopen(req, timeout=timeout_seconds) as resp:
42
+ code = int(resp.getcode())
43
+ if code == 200:
44
+ return True, f"HF Space /reset returned HTTP {code}"
45
+ return False, f"HF Space /reset returned HTTP {code} (expected 200)"
46
+ except urllib.error.HTTPError as err:
47
+ return False, f"HF Space /reset returned HTTP {err.code} (expected 200)"
48
+ except Exception as err:
49
+ return False, f"HF Space check failed: {err}"
50
+
51
+
52
+ def check_docker_build(repo_dir: Path, docker_timeout: int) -> tuple[bool, str]:
53
+ if shutil.which("docker") is None:
54
+ return False, "docker command not found"
55
+
56
+ if (repo_dir / "Dockerfile").exists():
57
+ context = repo_dir
58
+ elif (repo_dir / "server" / "Dockerfile").exists():
59
+ context = repo_dir / "server"
60
+ else:
61
+ return False, "No Dockerfile found in repo root or server/"
62
+
63
+ code, out, err = run(["docker", "build", str(context)], timeout=docker_timeout)
64
+ if code == 0:
65
+ return True, f"Docker build succeeded ({context})"
66
+
67
+ tail = "\n".join((out + "\n" + err).strip().splitlines()[-20:])
68
+ return False, f"Docker build failed (timeout={docker_timeout}s)\n{tail}"
69
+
70
+
71
+ def check_openenv_validate(repo_dir: Path) -> tuple[bool, str]:
72
+ cmd = None
73
+ if shutil.which("openenv") is not None:
74
+ cmd = ["openenv", "validate"]
75
+ else:
76
+ # Fallback for environments where console scripts are not on PATH.
77
+ cmd = [sys.executable, "-m", "openenv", "validate"]
78
+
79
+ code, out, err = run(cmd, cwd=repo_dir, timeout=600)
80
+ if code == 0:
81
+ msg = out or "openenv validate passed"
82
+ return True, msg
83
+
84
+ base = "openenv validate failed"
85
+ hint = "Install with: pip install openenv-core" if "No module named" in err else ""
86
+ details = "\n".join(x for x in [base, hint, out, err] if x).strip()
87
+ return False, details
88
+
89
+
90
+ def main() -> int:
91
+ parser = argparse.ArgumentParser(description="Pre-submit validator")
92
+ parser.add_argument(
93
+ "--repo-dir",
94
+ default=".",
95
+ help="Repository root (default: current directory)",
96
+ )
97
+ parser.add_argument(
98
+ "--space-url",
99
+ default=os.getenv("HF_SPACE_URL", ""),
100
+ help="HF Space base URL (or set HF_SPACE_URL)",
101
+ )
102
+ parser.add_argument(
103
+ "--skip-space",
104
+ action="store_true",
105
+ help="Skip HF Space /reset check",
106
+ )
107
+ parser.add_argument(
108
+ "--docker-timeout",
109
+ type=int,
110
+ default=1800,
111
+ help="Docker build timeout in seconds (default: 1800)",
112
+ )
113
+ parser.add_argument(
114
+ "--http-timeout",
115
+ type=int,
116
+ default=20,
117
+ help="HTTP timeout for HF Space check in seconds",
118
+ )
119
+
120
+ args = parser.parse_args()
121
+ repo_dir = Path(args.repo_dir).resolve()
122
+
123
+ if not repo_dir.exists():
124
+ log(f"FAIL: repo dir does not exist: {repo_dir}")
125
+ return 1
126
+
127
+ log("========================================")
128
+ log("Pre-submission Validation")
129
+ log("========================================")
130
+
131
+ all_ok = True
132
+
133
+ # Step 1: HF Space /reset
134
+ log("Step 1/3: Checking HF Space /reset")
135
+ if args.skip_space:
136
+ log("SKIP: HF Space check skipped")
137
+ else:
138
+ if not args.space_url:
139
+ log("FAIL: --space-url (or HF_SPACE_URL) is required unless --skip-space is used")
140
+ all_ok = False
141
+ else:
142
+ ok, msg = check_space(args.space_url, args.http_timeout)
143
+ log(("PASS: " if ok else "FAIL: ") + msg)
144
+ all_ok = all_ok and ok
145
+
146
+ # Step 2: docker build
147
+ log("Step 2/3: Running docker build")
148
+ ok, msg = check_docker_build(repo_dir, args.docker_timeout)
149
+ log(("PASS: " if ok else "FAIL: ") + msg)
150
+ all_ok = all_ok and ok
151
+
152
+ # Step 3: openenv validate
153
+ log("Step 3/3: Running openenv validate")
154
+ ok, msg = check_openenv_validate(repo_dir)
155
+ log(("PASS: " if ok else "FAIL: ") + msg)
156
+ all_ok = all_ok and ok
157
+
158
+ log("========================================")
159
+ if all_ok:
160
+ log("All checks passed")
161
+ return 0
162
+
163
+ log("One or more checks failed")
164
+ return 1
165
+
166
+
167
+ if __name__ == "__main__":
168
+ raise SystemExit(main())
169
+
scripts/baseline_inference.py CHANGED
@@ -16,10 +16,8 @@ from datetime import datetime
16
  from dotenv import load_dotenv
17
  from openai import OpenAI
18
 
19
- # Load environment variables from .env file
20
  load_dotenv()
21
 
22
- # Ensure project root is importable when running as: python scripts/baseline_inference.py
23
  PROJECT_ROOT = Path(__file__).resolve().parent.parent
24
  if str(PROJECT_ROOT) not in sys.path:
25
  sys.path.insert(0, str(PROJECT_ROOT))
 
16
  from dotenv import load_dotenv
17
  from openai import OpenAI
18
 
 
19
  load_dotenv()
20
 
 
21
  PROJECT_ROOT = Path(__file__).resolve().parent.parent
22
  if str(PROJECT_ROOT) not in sys.path:
23
  sys.path.insert(0, str(PROJECT_ROOT))