Deadman922 commited on
Commit
586dbd8
·
verified ·
1 Parent(s): 9bdab6c

Upload main.py

Browse files
Files changed (1) hide show
  1. server/main.py +178 -0
server/main.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import os
3
+
4
+ from fastapi import FastAPI, HTTPException
5
+ from fastapi.responses import HTMLResponse
6
+
7
+ from server.environment import SQLQualityEnvironment
8
+ from server.models import (
9
+ HealthResponse,
10
+ ResetRequest,
11
+ SQLAction,
12
+ SQLObservation,
13
+ SQLState,
14
+ StepResponse,
15
+ )
16
+
17
+ app = FastAPI(
18
+ title="SQL Quality Audit Agent Environment",
19
+ description="OpenEnv-compatible environment for training AI agents on SQL data quality tasks",
20
+ version="0.1.0",
21
+ )
22
+
23
+ # Module-level singleton — ONE instance shared across all requests
24
+ _env = SQLQualityEnvironment()
25
+
26
+ # HTTP-level request lock to serialize concurrent requests
27
+ _request_lock = asyncio.Lock()
28
+
29
+
30
+ def load_static_html() -> str:
31
+ """Load the static HTML interface for the App section."""
32
+ # Look for static/index.html in various locations
33
+ possible_paths = [
34
+ os.path.join(os.path.dirname(__file__), "..", "static", "index.html"),
35
+ os.path.join(os.path.dirname(__file__), "static", "index.html"),
36
+ "/app/static/index.html",
37
+ "static/index.html",
38
+ ]
39
+ for path in possible_paths:
40
+ if os.path.exists(path):
41
+ with open(path, "r", encoding="utf-8") as f:
42
+ return f.read()
43
+ # Fallback simple HTML if file not found
44
+ return """<!DOCTYPE html>
45
+ <html>
46
+ <head><title>SQL Quality Environment</title></head>
47
+ <body>
48
+ <h1>SQL Quality Audit Agent Environment</h1>
49
+ <p>Environment is running. Use API endpoints directly:</p>
50
+ <ul>
51
+ <li>GET /health - Health check</li>
52
+ <li>POST /reset - Reset task</li>
53
+ <li>POST /step - Execute action</li>
54
+ <li>GET /state - Get current state</li>
55
+ </ul>
56
+ </body>
57
+ </html>"""
58
+
59
+
60
+ @app.get("/", response_class=HTMLResponse)
61
+ async def root():
62
+ """Root endpoint for HF Space App section - provides interactive testing interface."""
63
+ return load_static_html()
64
+
65
+
66
+ @app.get("/health", response_model=HealthResponse)
67
+ async def health() -> HealthResponse:
68
+ """Health check endpoint for HF Space ping validation.
69
+
70
+ Returns:
71
+ dict: Status response indicating the server is running
72
+ """
73
+ return HealthResponse(status="ok")
74
+
75
+
76
+ @app.post("/ping", response_model=HealthResponse)
77
+ async def ping() -> HealthResponse:
78
+ """Ping endpoint for submission validator — accepts empty body.
79
+
80
+ Returns:
81
+ dict: Status response indicating the server is responsive
82
+ """
83
+ return HealthResponse(status="ok")
84
+
85
+
86
+ @app.post("/reset", response_model=SQLObservation)
87
+ async def reset(request: ResetRequest) -> SQLObservation:
88
+ """Reset environment and return initial observation for specified task.
89
+
90
+ Args:
91
+ request: ResetRequest containing task_id and optional deterministic seed
92
+
93
+ Returns:
94
+ SQLObservation: Initial state of database for requested task
95
+
96
+ Raises:
97
+ HTTPException 422: If task_id is not a valid task identifier
98
+ HTTPException 500: If environment initialization fails
99
+ """
100
+ async with _request_lock:
101
+ VALID_TASKS = {
102
+ "null_audit",
103
+ "type_repair",
104
+ "dedup_bug",
105
+ "fk_integrity",
106
+ "schema_drift",
107
+ }
108
+ if request.task_id not in VALID_TASKS:
109
+ raise HTTPException(
110
+ status_code=422,
111
+ detail=f"Invalid task_id '{request.task_id}'. Must be one of: {VALID_TASKS}",
112
+ )
113
+ try:
114
+ return _env.reset(request.task_id, seed=request.seed)
115
+ except Exception as e:
116
+ raise HTTPException(status_code=500, detail=str(e))
117
+
118
+
119
+ @app.post("/step", response_model=StepResponse)
120
+ async def step(action: SQLAction) -> StepResponse:
121
+ """Submit an action and receive observation, reward, done flag, and info.
122
+
123
+ Args:
124
+ action: SQLAction containing task_id and fix_query or answer
125
+
126
+ Returns:
127
+ StepResponse: Updated observation, reward, done status, and info
128
+
129
+ Raises:
130
+ HTTPException 422: If action is invalid or episode is already done
131
+ HTTPException 500: If environment step execution fails
132
+ """
133
+ async with _request_lock:
134
+ try:
135
+ return _env.step(action)
136
+ except HTTPException:
137
+ raise
138
+ except ValueError as e:
139
+ raise HTTPException(status_code=422, detail=str(e))
140
+ except Exception as e:
141
+ raise HTTPException(status_code=500, detail=str(e))
142
+
143
+
144
+ @app.get("/state", response_model=SQLState)
145
+ async def state() -> SQLState:
146
+ """Get current episode state.
147
+
148
+ Returns:
149
+ SQLState: Current episode information including task_id, episode_id, attempt_number, cumulative_reward, and done status
150
+
151
+ Raises:
152
+ HTTPException 422: If no active episode exists
153
+ """
154
+ try:
155
+ return _env.state()
156
+ except ValueError as e:
157
+ raise HTTPException(status_code=422, detail=str(e))
158
+
159
+
160
+ def run_server() -> None:
161
+ """
162
+ Entry point for 'uv run server' command.
163
+
164
+ Uses PORT environment variable if set, otherwise defaults to 7860
165
+ (matching the Dockerfile EXPOSE and HF Spaces convention).
166
+ Override locally: PORT=8000 uv run server
167
+ """
168
+ import os
169
+
170
+ import uvicorn
171
+
172
+ port = int(os.getenv("PORT", "7860"))
173
+ uvicorn.run("server.main:app", host="0.0.0.0", port=port, reload=False)
174
+
175
+
176
+ def main() -> None:
177
+ """Primary CLI entrypoint expected by OpenEnv validators."""
178
+ run_server()