Spaces:
Sleeping
Sleeping
File size: 12,806 Bytes
f5d79b8 2ac8bdd f5d79b8 2ac8bdd f5d79b8 2ac8bdd f5d79b8 2ac8bdd f5d79b8 2ac8bdd f5d79b8 2ac8bdd f5d79b8 2ac8bdd f5d79b8 2ac8bdd f5d79b8 2ac8bdd f5d79b8 2ac8bdd f5d79b8 2ac8bdd f5d79b8 2ac8bdd f5d79b8 2ac8bdd f5d79b8 2ac8bdd f5d79b8 2ac8bdd f5d79b8 2ac8bdd f5d79b8 2ac8bdd f5d79b8 2ac8bdd ce6b9af 2ac8bdd 2768eae ce6b9af 2768eae 2ac8bdd f5d79b8 2ac8bdd f5d79b8 2768eae ce6b9af 2768eae ce6b9af 2768eae f5d79b8 2ac8bdd f5d79b8 e0c6328 2ac8bdd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 | """
OpenEnv — Multi-Domain AI Testing Environment
FastAPI server implementing the full OpenEnv spec across 4 domains.
"""
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from typing import Dict, Any, Optional
import os, sys, statistics
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.models import Action, Observation, Reward, AgentAction
from tasks.task1_classification import ClassificationTask
from tasks.task2_drafting import ResponseDraftingTask
from tasks.task3_queue import QueueManagementTask
from tasks.task4_multiturn import MultiTurnTask
from tasks.legal_tasks import LegalIdentifyClauseTask, LegalRiskFlagTask, LegalRedlineTask
from tasks.clinical_tasks import ClinicalTriageTask, ClinicalESITask, ClinicalNoteTask
from tasks.pr_tasks import PRTypeTask, PRBugIdentifyTask, PRReviewTask
# ─── Task Registry ────────────────────────────────────────────────────────────
TASKS = {
"ticket_classification": ClassificationTask,
"response_drafting": ResponseDraftingTask,
"queue_management": QueueManagementTask,
"multi_turn_conversation": MultiTurnTask,
"legal_clause_identification": LegalIdentifyClauseTask,
"legal_risk_flagging": LegalRiskFlagTask,
"legal_clause_redlining": LegalRedlineTask,
"clinical_triage_classification": ClinicalTriageTask,
"clinical_esi_assignment": ClinicalESITask,
"clinical_triage_note": ClinicalNoteTask,
"pr_type_classification": PRTypeTask,
"pr_bug_identification": PRBugIdentifyTask,
"pr_review_comment": PRReviewTask,
}
_active: Dict[str, Any] = {}
_current_task_id: str = "ticket_classification"
def get_task(task_id: str = None):
tid = task_id or _current_task_id
if tid not in _active:
raise HTTPException(
status_code=400,
detail=f"No active episode for task '{tid}'. Call /reset first."
)
return _active[tid]
# ─── App ──────────────────────────────────────────────────────────────────────
@asynccontextmanager
async def lifespan(app: FastAPI):
for tid, TaskClass in TASKS.items():
instance = TaskClass()
instance.reset()
_active[tid] = instance
yield
app = FastAPI(
title="OpenEnv — Multi-Domain AI Testing Environment",
description=(
"Real-world OpenEnv environment testing AI agents across "
"Customer Support, Legal Review, Clinical Triage, and Software Engineering."
),
version="2.0.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# ─── Core OpenEnv Endpoints ───────────────────────────────────────────────────
@app.post("/reset")
async def reset(task_id: Optional[str] = None) -> Dict[str, Any]:
global _current_task_id
tid = task_id or _current_task_id
if tid not in TASKS:
raise HTTPException(
status_code=400,
detail=f"Unknown task '{tid}'. Valid: {list(TASKS.keys())}"
)
_current_task_id = tid
instance = TASKS[tid]()
_active[tid] = instance
obs = instance.reset()
return {"task_id": tid, "observation": obs.model_dump()}
@app.post("/step")
async def step(action: Action, task_id: Optional[str] = None) -> Dict[str, Any]:
task = get_task(task_id)
try:
obs, reward, done, info = task.step(action)
except Exception as e:
raise HTTPException(status_code=422, detail=f"Step failed: {str(e)}")
return {
"observation": obs.model_dump(),
"reward": reward.model_dump(),
"done": done,
"info": info,
}
@app.get("/state")
async def state(task_id: Optional[str] = None) -> Dict[str, Any]:
task = get_task(task_id)
return task.state()
# ─── Extended Endpoints ───────────────────────────────────────────────────────
@app.get("/tasks")
async def list_tasks() -> Dict[str, Any]:
return {
"tasks": [
# ── Customer Support ──
{
"id": "ticket_classification",
"name": "Ticket Classification & Routing",
"difficulty": "easy",
"description": "Classify incoming support tickets by category and priority.",
"action_schema": {
"action_type": "classify",
"ticket_id": "string",
"category": "billing|technical|account|feature_request|abuse|unknown",
"priority": "P1|P2|P3|P4",
},
},
{
"id": "response_drafting",
"name": "Response Drafting & Quality",
"difficulty": "medium",
"description": "Draft professional customer-facing responses using KB context.",
"action_schema": {
"action_type": "draft_response",
"ticket_id": "string",
"response_text": "string (100-300 words)",
},
},
{
"id": "queue_management",
"name": "SLA Queue Management",
"difficulty": "hard",
"description": "Manage 20 tickets across 3 agents. Maximize SLA compliance and FCR.",
"action_schema": {
"action_type": "assign_ticket|resolve|escalate|close|no_op",
"ticket_id": "string",
"target_agent_id": "agent_billing|agent_tech|agent_general",
},
},
{
"id": "multi_turn_conversation",
"name": "Multi-Turn De-escalation",
"difficulty": "very_hard",
"description": "Handle a multi-turn frustrated customer conversation requiring context retention.",
"action_schema": {
"action_type": "draft_response|escalate|resolve|close",
"response_text": "string",
},
},
# ── Legal ──
{
"id": "legal_clause_identification",
"name": "Legal Clause Identification",
"difficulty": "easy",
"description": "Identify the type of each contractual clause.",
"action_schema": {
"action_type": "identify_clause",
"clause_type": "indemnity|liability|ip|termination|unknown",
},
},
{
"id": "legal_risk_flagging",
"name": "Legal Risk Flagging",
"difficulty": "medium",
"description": "Assess and flag risk levels for contractual clauses.",
"action_schema": {
"action_type": "flag_risk",
"risk_level": "low|medium|high|critical",
"reasoning": "string",
},
},
{
"id": "legal_clause_redlining",
"name": "Legal Clause Redlining",
"difficulty": "hard",
"description": "Rewrite risky contract clauses with safer alternative language.",
"action_schema": {
"action_type": "redline",
"redline_text": "string (improved clause text)",
},
},
# ── Clinical ──
{
"id": "clinical_triage_classification",
"name": "Clinical Triage Body System",
"difficulty": "easy",
"description": "Classify patient chief complaints into affected body systems.",
"action_schema": {
"action_type": "classify_triage",
"body_system": "cardiac|respiratory|neurologic|gi|musculoskeletal|other",
},
},
{
"id": "clinical_esi_assignment",
"name": "Clinical ESI Assignment",
"difficulty": "medium",
"description": "Assign Emergency Severity Index (ESI) 1-5 triage levels.",
"action_schema": {
"action_type": "assign_esi",
"esi_level": "integer 1-5",
"reasoning": "string",
},
},
{
"id": "clinical_triage_note",
"name": "Clinical Triage Note Generation",
"difficulty": "hard",
"description": "Write a complete clinical triage note for ED patients.",
"action_schema": {
"action_type": "write_triage_note",
"triage_note": "string (clinical note)",
},
},
# ── Engineering ──
{
"id": "pr_type_classification",
"name": "PR Type Classification",
"difficulty": "easy",
"description": "Classify pull request diffs by type.",
"action_schema": {
"action_type": "classify_pr",
"pr_type": "bug_fix|feature|refactor|security",
},
},
{
"id": "pr_bug_identification",
"name": "PR Bug Identification",
"difficulty": "medium",
"description": "Identify the specific bug or security issue in a code diff.",
"action_schema": {
"action_type": "identify_bug",
"bug_description": "string",
},
},
{
"id": "pr_review_comment",
"name": "PR Code Review",
"difficulty": "hard",
"description": "Write expert code review comments approving or blocking PRs.",
"action_schema": {
"action_type": "review_pr",
"review_comment": "string",
},
},
]
}
@app.post("/grader")
async def grader(task_id: Optional[str] = None) -> Dict[str, Any]:
task = get_task(task_id)
result = task.grader_score()
# Global reward shaping bonuses
reward_history = getattr(task, "reward_history", [])
if reward_history and len(reward_history) >= 3:
first_half = sum(reward_history[:len(reward_history) // 2])
second_half = sum(reward_history[len(reward_history) // 2:])
traj_bonus = 0.05 if second_half > first_half else 0.0
try:
std = statistics.stdev(reward_history)
cons_penalty = -0.05 if std > 0.4 else 0.0
except Exception:
cons_penalty = 0.0
result["final_score"] = max(0.001, min(0.999, result["final_score"] + traj_bonus + cons_penalty))
result.setdefault("metrics", {})
result["metrics"]["trajectory_bonus"] = traj_bonus
result["metrics"]["consistency_penalty"] = cons_penalty
# FORCE clamp — validator requires strictly (0, 1) exclusive
result["final_score"] = max(0.001, min(0.999, float(result["final_score"])))
return result
@app.post("/baseline")
async def baseline() -> Dict[str, Any]:
"""Run heuristic baseline agent across all tasks. No API key needed."""
from graders.baseline_agent import run_baseline_all_tasks
results = await run_baseline_all_tasks()
# Clamp all task scores
for tid in results.get("tasks", {}):
s = results["tasks"][tid].get("final_score", 0.5)
results["tasks"][tid]["final_score"] = max(0.001, min(0.999, float(s)))
overall = results.get("overall_score", 0.5)
results["overall_score"] = max(0.001, min(0.999, float(overall)))
return results
@app.get("/health")
async def health():
return {"status": "ok", "tasks": list(TASKS.keys())}
@app.get("/", response_class=FileResponse)
async def root():
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
static_path = os.path.join(root_dir, "static", "index.html")
return FileResponse(static_path)
|