Deadman922 commited on
Commit
059e0e6
·
verified ·
1 Parent(s): f17387b

Upload main.py

Browse files
Files changed (1) hide show
  1. server/main.py +217 -0
server/main.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+
3
+ from fastapi import FastAPI, HTTPException
4
+ from fastapi.responses import HTMLResponse
5
+
6
+ from server.environment import SQLQualityEnvironment
7
+ from server.models import (
8
+ HealthResponse,
9
+ ResetRequest,
10
+ SQLAction,
11
+ SQLObservation,
12
+ SQLState,
13
+ StepResponse,
14
+ )
15
+
16
+ app = FastAPI(
17
+ title="SQL Quality Audit Agent Environment",
18
+ description="OpenEnv-compatible environment for training AI agents on SQL data quality tasks",
19
+ version="0.1.0",
20
+ )
21
+
22
+ # Module-level singleton — ONE instance shared across all requests
23
+ _env = SQLQualityEnvironment()
24
+
25
+ # HTTP-level request lock to serialize concurrent requests
26
+ _request_lock = asyncio.Lock()
27
+
28
+
29
+ @app.get("/", response_class=HTMLResponse)
30
+ async def root():
31
+ """Root endpoint for HF Space App section - provides testing interface."""
32
+ return """
33
+ <!DOCTYPE html>
34
+ <html>
35
+ <head>
36
+ <title>SQL Quality Environment</title>
37
+ <style>
38
+ body { font-family: system-ui, sans-serif; max-width: 800px; margin: 40px auto; padding: 20px; }
39
+ h1 { color: #2563eb; }
40
+ .endpoint { background: #f3f4f6; padding: 15px; margin: 10px 0; border-radius: 8px; }
41
+ .method { font-weight: bold; color: #059669; }
42
+ code { background: #e5e7eb; padding: 2px 6px; border-radius: 4px; }
43
+ button { background: #2563eb; color: white; border: none; padding: 10px 20px;
44
+ border-radius: 6px; cursor: pointer; margin: 5px; }
45
+ button:hover { background: #1d4ed8; }
46
+ #result { background: #1f2937; color: #10b981; padding: 15px; border-radius: 8px;
47
+ margin-top: 20px; font-family: monospace; white-space: pre-wrap; display: none; }
48
+ </style>
49
+ </head>
50
+ <body>
51
+ <h1>📊 SQL Quality Audit Agent Environment</h1>
52
+ <p>OpenEnv-compatible environment for training AI agents on SQL data quality tasks.</p>
53
+
54
+ <h2>Available Endpoints</h2>
55
+ <div class="endpoint">
56
+ <span class="method">GET</span> <code>/health</code> - Health check
57
+ </div>
58
+ <div class="endpoint">
59
+ <span class="method">POST</span> <code>/ping</code> - Ping for validator
60
+ </div>
61
+ <div class="endpoint">
62
+ <span class="method">POST</span> <code>/reset</code> - Reset environment (requires task_id)
63
+ </div>
64
+ <div class="endpoint">
65
+ <span class="method">POST</span> <code>/step</code> - Execute action
66
+ </div>
67
+ <div class="endpoint">
68
+ <span class="method">GET</span> <code>/state</code> - Get current state
69
+ </div>
70
+
71
+ <h2>Quick Test</h2>
72
+ <button onclick="testHealth()">Test /health</button>
73
+ <button onclick="testReset()">Test /reset (null_audit)</button>
74
+
75
+ <div id="result"></div>
76
+
77
+ <script>
78
+ async function testHealth() {
79
+ const res = await fetch('/health');
80
+ const data = await res.json();
81
+ showResult('GET /health\\nStatus: ' + res.status + '\\nResponse: ' + JSON.stringify(data, null, 2));
82
+ }
83
+
84
+ async function testReset() {
85
+ const res = await fetch('/reset', {
86
+ method: 'POST',
87
+ headers: {'Content-Type': 'application/json'},
88
+ body: JSON.stringify({task_id: 'null_audit', seed: 42})
89
+ });
90
+ const data = await res.json();
91
+ showResult('POST /reset\\nStatus: ' + res.status + '\\nResponse: ' + JSON.stringify(data, null, 2).slice(0, 500) + '...');
92
+ }
93
+
94
+ function showResult(text) {
95
+ const el = document.getElementById('result');
96
+ el.textContent = text;
97
+ el.style.display = 'block';
98
+ }
99
+ </script>
100
+ </body>
101
+ </html>
102
+ """
103
+
104
+
105
+ @app.get("/health", response_model=HealthResponse)
106
+ async def health() -> HealthResponse:
107
+ """Health check endpoint for HF Space ping validation.
108
+
109
+ Returns:
110
+ dict: Status response indicating the server is running
111
+ """
112
+ return HealthResponse(status="ok")
113
+
114
+
115
+ @app.post("/ping", response_model=HealthResponse)
116
+ async def ping() -> HealthResponse:
117
+ """Ping endpoint for submission validator — accepts empty body.
118
+
119
+ Returns:
120
+ dict: Status response indicating the server is responsive
121
+ """
122
+ return HealthResponse(status="ok")
123
+
124
+
125
+ @app.post("/reset", response_model=SQLObservation)
126
+ async def reset(request: ResetRequest) -> SQLObservation:
127
+ """Reset environment and return initial observation for specified task.
128
+
129
+ Args:
130
+ request: ResetRequest containing task_id and optional deterministic seed
131
+
132
+ Returns:
133
+ SQLObservation: Initial state of database for requested task
134
+
135
+ Raises:
136
+ HTTPException 422: If task_id is not a valid task identifier
137
+ HTTPException 500: If environment initialization fails
138
+ """
139
+ async with _request_lock:
140
+ VALID_TASKS = {
141
+ "null_audit",
142
+ "type_repair",
143
+ "dedup_bug",
144
+ "fk_integrity",
145
+ "schema_drift",
146
+ }
147
+ if request.task_id not in VALID_TASKS:
148
+ raise HTTPException(
149
+ status_code=422,
150
+ detail=f"Invalid task_id '{request.task_id}'. Must be one of: {VALID_TASKS}",
151
+ )
152
+ try:
153
+ return _env.reset(request.task_id, seed=request.seed)
154
+ except Exception as e:
155
+ raise HTTPException(status_code=500, detail=str(e))
156
+
157
+
158
+ @app.post("/step", response_model=StepResponse)
159
+ async def step(action: SQLAction) -> StepResponse:
160
+ """Submit an action and receive observation, reward, done flag, and info.
161
+
162
+ Args:
163
+ action: SQLAction containing task_id and fix_query or answer
164
+
165
+ Returns:
166
+ StepResponse: Updated observation, reward, done status, and info
167
+
168
+ Raises:
169
+ HTTPException 422: If action is invalid or episode is already done
170
+ HTTPException 500: If environment step execution fails
171
+ """
172
+ async with _request_lock:
173
+ try:
174
+ return _env.step(action)
175
+ except HTTPException:
176
+ raise
177
+ except ValueError as e:
178
+ raise HTTPException(status_code=422, detail=str(e))
179
+ except Exception as e:
180
+ raise HTTPException(status_code=500, detail=str(e))
181
+
182
+
183
+ @app.get("/state", response_model=SQLState)
184
+ async def state() -> SQLState:
185
+ """Get current episode state.
186
+
187
+ Returns:
188
+ SQLState: Current episode information including task_id, episode_id, attempt_number, cumulative_reward, and done status
189
+
190
+ Raises:
191
+ HTTPException 422: If no active episode exists
192
+ """
193
+ try:
194
+ return _env.state()
195
+ except ValueError as e:
196
+ raise HTTPException(status_code=422, detail=str(e))
197
+
198
+
199
+ def run_server() -> None:
200
+ """
201
+ Entry point for 'uv run server' command.
202
+
203
+ Uses PORT environment variable if set, otherwise defaults to 7860
204
+ (matching the Dockerfile EXPOSE and HF Spaces convention).
205
+ Override locally: PORT=8000 uv run server
206
+ """
207
+ import os
208
+
209
+ import uvicorn
210
+
211
+ port = int(os.getenv("PORT", "7860"))
212
+ uvicorn.run("server.main:app", host="0.0.0.0", port=port, reload=False)
213
+
214
+
215
+ def main() -> None:
216
+ """Primary CLI entrypoint expected by OpenEnv validators."""
217
+ run_server()