root commited on
Commit
9496a0e
·
1 Parent(s): 38819be
.dockerignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Secrets — never bake into image
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+
6
+ # Git / IDE
7
+ .git
8
+ .gitignore
9
+ .cursor
10
+
11
+ # Python noise
12
+ __pycache__
13
+ *.py[cod]
14
+ *$py.class
15
+ .pytest_cache
16
+ .mypy_cache
17
+ .ruff_cache
18
+ .venv
19
+ venv
20
+ *.egg-info
21
+
22
+ # Local / OS
23
+ Thumbs.db
24
+ .DS_Store
.env.example ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copy this file to `.env` and fill in your values.
2
+ # Never commit `.env` — it is listed in `.gitignore`.
3
+
4
+ # --- Hackathon / inference.py (OpenAI-compatible client) ---
5
+ # Primary key (Hugging Face token OR OpenAI key, depending on API_BASE_URL)
6
+ HF_TOKEN=
7
+
8
+ # If you use OpenAI directly instead of HF router:
9
+ # OPENAI_API_KEY=
10
+
11
+ # Generic fallback used by some samples:
12
+ # API_KEY=
13
+
14
+ # LLM endpoint (OpenAI: https://api.openai.com/v1 | HF router: https://router.huggingface.co/v1)
15
+ API_BASE_URL=https://api.openai.com/v1
16
+
17
+ # Model id for chat completions (must match your provider)
18
+ MODEL_NAME=gpt-4o-mini
19
+
20
+ # --- Optional inference tuning ---
21
+ # OPENAI_SEED=42
22
+ # MAX_STEPS=24
23
+ # ENV_CONTAINER_START=false
24
+ # IMAGE_NAME=sql-agent-env:latest
25
+ # SQL_AGENT_TASK=sql_analyst_episode
26
+ # SQL_AGENT_BENCHMARK=sql_agent_openenv
27
+
28
+ # --- Hugging Face Space / server ---
29
+ # PORT=7860
.gitignore CHANGED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .env
2
+ .env.local
3
+ .env.*.local
4
+ __pycache__/
5
+ *.pyc
6
+ .Python
7
+ *.py[cod]
8
+ .venv/
9
+ venv/
10
+ *.egg-info/
11
+ .pytest_cache/
Dockerfile CHANGED
@@ -14,12 +14,14 @@ COPY . /app/
14
 
15
  # Environment configurations setup
16
  ENV PYTHONPATH="/app"
17
-
18
- # Health check
19
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
20
- CMD curl -f http://localhost:7860/health || exit 1
21
 
22
  EXPOSE 7860
23
 
24
- # Run server with updated root OOP Path
25
- CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860"]
 
 
 
 
 
14
 
15
  # Environment configurations setup
16
  ENV PYTHONPATH="/app"
17
+ # Hugging Face Spaces may set PORT; must match README `app_port` (7860).
18
+ ENV PORT=7860
 
 
19
 
20
  EXPOSE 7860
21
 
22
+ # Health check uses same default PORT as CMD (Spaces usually leave PORT=7860)
23
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=5 \
24
+ CMD sh -c 'curl -fsS "http://127.0.0.1:${PORT}/health" || exit 1'
25
+
26
+ # OpenEnv app: FastAPI ASGI at server.app:app (see openenv.yaml)
27
+ CMD ["sh", "-c", "exec uvicorn server.app:app --host 0.0.0.0 --port ${PORT}"]
README.md CHANGED
@@ -67,31 +67,59 @@ Episode ends when all tasks are solved or max attempts for a task are exhausted.
67
  ```bash
68
  pip install -r requirements.txt
69
  ```
70
- 2. Set API key for baseline:
71
  ```bash
72
- export OPENAI_API_KEY="your-key"
 
 
73
  ```
74
- 3. Run baseline:
 
75
  ```bash
76
  python inference.py
77
  ```
78
 
79
- The baseline uses the OpenAI API client with deterministic settings:
80
- - `temperature=0.0`
81
- - fixed seed (`OPENAI_SEED`, default `42`)
82
- - fixed task order and deterministic grader
83
 
84
- ## Baseline Score Reporting
85
- `inference.py` prints:
86
- - per-task best score
87
- - average task score
88
- - episode return
89
- - model and seed used
 
 
 
 
 
 
 
 
 
90
 
91
- This creates reproducible benchmark records for easy/medium/hard tasks.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
  ## Docker and Hugging Face Space
94
- - `Dockerfile` is included and starts `uvicorn server.app:app`.
 
95
  - Space metadata is configured in this `README.md` frontmatter with `sdk: docker`.
96
  - Health endpoint: `/health`
97
  - **Web playground:** open the Space URL (`/`). Click **Connect session** to open a WebSocket to `/ws`, then **Start episode (reset)** and **Run query (step)**. OpenEnv’s HTTP `POST /reset` and `POST /step` each use a fresh environment instance (stateless); the WebSocket session keeps one episode alive for the UI.
 
67
  ```bash
68
  pip install -r requirements.txt
69
  ```
70
+ 2. Set **hackathon-required** variables (OpenAI-compatible client):
71
  ```bash
72
+ export HF_TOKEN="your-api-key"
73
+ export API_BASE_URL="https://api.openai.com/v1"
74
+ export MODEL_NAME="gpt-4o-mini"
75
  ```
76
+ `HF_TOKEN` is preferred; `OPENAI_API_KEY` or `API_KEY` are accepted as fallbacks.
77
+ 3. Run baseline (root `inference.py`):
78
  ```bash
79
  python inference.py
80
  ```
81
 
82
+ The baseline uses the **OpenAI Python client** (`openai.OpenAI`) with `base_url=API_BASE_URL` and `api_key=HF_TOKEN` (or fallback). For reproducibility on the official OpenAI API, `temperature=0.0` and `seed=OPENAI_SEED` (default `42`) are used when `API_BASE_URL` points at OpenAI.
 
 
 
83
 
84
+ ### Mandatory stdout format (for automated judging)
85
+ `inference.py` prints **only** these structured lines to **stdout** (debug goes to **stderr**):
86
+
87
+ - `[START] task=<name> env=<benchmark> model=<model>`
88
+ - `[STEP] step=<n> action=<sql> reward=<0.00> done=<true|false> error=<msg|null>`
89
+ - `[END] success=<true|false> steps=<n> score=<0.000> rewards=<r1,r2,...>`
90
+
91
+ `[END] score` is the mean of best grader scores seen for the three tasks (each in `[0, 1]`). `success` is `true` when the episode ends with `done` and that mean score is ≥ `0.95`.
92
+
93
+ ## Pre-submission validation
94
+ Before submitting, run:
95
+ ```bash
96
+ python hf/pre-validation-script.py
97
+ ```
98
+ This checks files, the stdout contract in `inference.py`, syntax, and `openenv validate` (if the CLI is installed).
99
 
100
+ ## Baseline Score Reporting
101
+ Structured `[END]` line carries the aggregate score and per-step rewards; use stderr `[DEBUG]` lines only for local troubleshooting.
102
+
103
+ ## Deploy to Hugging Face Spaces (spec checklist)
104
+ 1. **Create a Space** → **Docker** template, or link this GitHub repo to a Space.
105
+ 2. **README frontmatter** (top of this file) must stay valid YAML:
106
+ - `sdk: docker`
107
+ - `app_port: 7860` (must match `openenv.yaml` `port` and the container listen port)
108
+ - `tags:` includes `openenv`
109
+ 3. **Build**: HF runs `docker build` on the repo root; entrypoint is `Dockerfile` `CMD` → Uvicorn on `0.0.0.0:$PORT` (default **7860**).
110
+ 4. **Health**: platform probes your app; this repo exposes **`GET /health`** and OpenEnv **`POST /reset`** for automated checks.
111
+ 5. **Push** the same commit you validated locally (`openenv validate`, `python hf/pre-validation-script.py`).
112
+
113
+ Local smoke test (matches CI-style build):
114
+ ```bash
115
+ docker build -t sql-agent-env .
116
+ docker run --rm -p 7860:7860 -e PORT=7860 sql-agent-env
117
+ # Then open http://localhost:7860/health and http://localhost:7860/
118
+ ```
119
 
120
  ## Docker and Hugging Face Space
121
+ - `Dockerfile` runs `uvicorn server.app:app` with **`PORT`** from the environment (default **7860**).
122
+ - `.dockerignore` excludes `.env` and build junk so secrets are not copied into the image.
123
  - Space metadata is configured in this `README.md` frontmatter with `sdk: docker`.
124
  - Health endpoint: `/health`
125
  - **Web playground:** open the Space URL (`/`). Click **Connect session** to open a WebSocket to `/ws`, then **Start episode (reset)** and **Run query (step)**. OpenEnv’s HTTP `POST /reset` and `POST /step` each use a fresh environment instance (stateless); the WebSocket session keeps one episode alive for the UI.
__pycache__/inference.cpython-313.pyc CHANGED
Binary files a/__pycache__/inference.cpython-313.pyc and b/__pycache__/inference.cpython-313.pyc differ
 
hf/pre-validation-script.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Pre-submission checks for the OpenEnv hackathon SQL agent project.
4
+
5
+ Run from repo root:
6
+ python hf/pre-validation-script.py
7
+
8
+ Or:
9
+ cd hf && python pre-validation-script.py
10
+
11
+ Checks:
12
+ - Required files exist (inference.py, Dockerfile, openenv.yaml, README, models, etc.)
13
+ - inference.py contains mandatory stdout format helpers and tags
14
+ - Python syntax compiles
15
+ - openenv validate (if CLI installed)
16
+
17
+ Does not call external LLM APIs (no HF_TOKEN required for this script).
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import ast
23
+ import subprocess
24
+ import sys
25
+ from pathlib import Path
26
+
27
+ ROOT = Path(__file__).resolve().parent.parent
28
+
29
+
30
+ def ok(msg: str) -> None:
31
+ print(f"[OK] {msg}", flush=True)
32
+
33
+
34
+ def fail(msg: str) -> bool:
35
+ print(f"[FAIL] {msg}", flush=True)
36
+ return False
37
+
38
+
39
+ def check_files() -> bool:
40
+ good = True
41
+ required = [
42
+ ROOT / "inference.py",
43
+ ROOT / "Dockerfile",
44
+ ROOT / "openenv.yaml",
45
+ ROOT / "README.md",
46
+ ROOT / "models.py",
47
+ ROOT / "client.py",
48
+ ROOT / "server" / "app.py",
49
+ ROOT / "server" / "environment.py",
50
+ ROOT / "core" / "tasks.py",
51
+ ROOT / "core" / "grader.py",
52
+ ]
53
+ for p in required:
54
+ if not p.is_file():
55
+ good &= fail(f"Missing file: {p.relative_to(ROOT)}")
56
+ if good:
57
+ ok("Required project files present")
58
+ return good
59
+
60
+
61
+ def check_inference_stdout_contract() -> bool:
62
+ path = ROOT / "inference.py"
63
+ text = path.read_text(encoding="utf-8")
64
+ tokens = [
65
+ "def log_start",
66
+ "def log_step",
67
+ "def log_end",
68
+ "[START]",
69
+ "[STEP]",
70
+ "[END]",
71
+ "OpenAI(",
72
+ "chat.completions.create",
73
+ "HF_TOKEN",
74
+ "API_BASE_URL",
75
+ "MODEL_NAME",
76
+ ]
77
+ good = True
78
+ for t in tokens:
79
+ if t not in text:
80
+ good &= fail(f"inference.py missing required fragment: {t!r}")
81
+ if good:
82
+ ok("inference.py stdout contract + OpenAI client config markers")
83
+ return good
84
+
85
+
86
+ def check_syntax() -> bool:
87
+ good = True
88
+ for py in [
89
+ ROOT / "inference.py",
90
+ ROOT / "server" / "app.py",
91
+ ROOT / "server" / "environment.py",
92
+ ROOT / "models.py",
93
+ ]:
94
+ try:
95
+ ast.parse(py.read_text(encoding="utf-8"), filename=str(py))
96
+ except SyntaxError as e:
97
+ good &= fail(f"Syntax error in {py.relative_to(ROOT)}: {e}")
98
+ if good:
99
+ ok("Core Python files parse (syntax)")
100
+ return good
101
+
102
+
103
+ def check_openenv_validate() -> bool:
104
+ try:
105
+ r = subprocess.run(
106
+ ["openenv", "validate"],
107
+ cwd=str(ROOT),
108
+ capture_output=True,
109
+ text=True,
110
+ timeout=120,
111
+ )
112
+ except FileNotFoundError:
113
+ print(
114
+ "[WARN] openenv CLI not on PATH — install openenv-core and ensure `openenv` is available",
115
+ flush=True,
116
+ )
117
+ return True
118
+ sys.stdout.write(r.stdout)
119
+ sys.stderr.write(r.stderr)
120
+ if r.returncode != 0:
121
+ return fail("openenv validate returned non-zero")
122
+ ok("openenv validate")
123
+ return True
124
+
125
+
126
+ def main() -> int:
127
+ print("=== Pre-submission validation ===", flush=True)
128
+ print(f"ROOT: {ROOT}", flush=True)
129
+ all_ok = True
130
+ all_ok &= check_files()
131
+ all_ok &= check_inference_stdout_contract()
132
+ all_ok &= check_syntax()
133
+ all_ok &= check_openenv_validate()
134
+ if all_ok:
135
+ print("\n=== All automated checks passed ===", flush=True)
136
+ return 0
137
+ print("\n=== Fix failures above before submitting ===", flush=True)
138
+ return 1
139
+
140
+
141
+ if __name__ == "__main__":
142
+ sys.exit(main())
hf/simple-interface-script.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Inference Script Example
3
+ ===================================
4
+ MANDATORY
5
+ - Before submitting, ensure the following variables are defined in your environment configuration:
6
+ API_BASE_URL The API endpoint for the LLM.
7
+ MODEL_NAME The model identifier to use for inference.
8
+ HF_TOKEN Your Hugging Face / API key.
9
+ LOCAL_IMAGE_NAME The name of the local image to use for the environment if you are using from_docker_image()
10
+ method
11
+
12
+ - Defaults are set only for API_BASE_URL and MODEL_NAME
13
+ (and should reflect your active inference setup):
14
+ API_BASE_URL = os.getenv("API_BASE_URL", "<your-active-endpoint>")
15
+ MODEL_NAME = os.getenv("MODEL_NAME", "<your-active-model>")
16
+
17
+ - The inference script must be named `inference.py` and placed in the root directory of the project
18
+ - Participants must use OpenAI Client for all LLM calls using above variables
19
+
20
+ STDOUT FORMAT
21
+ - The script must emit exactly three line types to stdout, in this order:
22
+
23
+ [START] task=<task_name> env=<benchmark> model=<model_name>
24
+ [STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>
25
+ [END] success=<true|false> steps=<n> score=<score> rewards=<r1,r2,...,rn>
26
+
27
+ Rules:
28
+ - One [START] line at episode begin.
29
+ - One [STEP] line per step, immediately after env.step() returns.
30
+ - One [END] line after env.close(), always emitted (even on exception).
31
+ - reward and rewards are formatted to 2 decimal places.
32
+ - done and success are lowercase booleans: true or false.
33
+ - error is the raw last_action_error string, or null if none.
34
+ - All fields on a single line with no newlines within a line.
35
+ - Each tasks should return score in [0, 1]
36
+
37
+ Example:
38
+ [START] task=click-test env=miniwob model=Qwen3-VL-30B
39
+ [STEP] step=1 action=click('123') reward=0.00 done=false error=null
40
+ [STEP] step=2 action=fill('456','text') reward=0.00 done=false error=null
41
+ [STEP] step=3 action=click('789') reward=1.00 done=true error=null
42
+ [END] success=true steps=3 score=1.00 rewards=0.00,0.00,1.00
43
+ """
44
+
45
+ import asyncio
46
+ import os
47
+ import textwrap
48
+ from typing import List, Optional
49
+
50
+ from openai import OpenAI
51
+
52
+ from my_env_v4 import MyEnvV4Action, MyEnvV4Env
53
+ IMAGE_NAME = os.getenv("IMAGE_NAME") # If you are using docker image
54
+ API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
55
+
56
+ API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
57
+ MODEL_NAME = os.getenv("MODEL_NAME") or "Qwen/Qwen2.5-72B-Instruct"
58
+ TASK_NAME = os.getenv("MY_ENV_V4_TASK", "echo")
59
+ BENCHMARK = os.getenv("MY_ENV_V4_BENCHMARK", "my_env_v4")
60
+ MAX_STEPS = 8
61
+ TEMPERATURE = 0.7
62
+ MAX_TOKENS = 150
63
+ SUCCESS_SCORE_THRESHOLD = 0.1 # normalized score in [0, 1]
64
+
65
+ # Max possible reward: each token contributes 0.1, across all steps
66
+ _MAX_REWARD_PER_STEP = MAX_TOKENS * 0.1
67
+ MAX_TOTAL_REWARD = MAX_STEPS * _MAX_REWARD_PER_STEP
68
+
69
+ SYSTEM_PROMPT = textwrap.dedent(
70
+ """
71
+ You are interacting with a simple echo environment.
72
+ Each turn you must send a message. The environment will echo it back.
73
+ Reward is proportional to message length: reward = len(message) * 0.1
74
+ Your goal is to maximize total reward by sending meaningful, substantive messages.
75
+ Reply with exactly one message string — no quotes, no prefixes, just the message text.
76
+ """
77
+ ).strip()
78
+
79
+
80
+ def log_start(task: str, env: str, model: str) -> None:
81
+ print(f"[START] task={task} env={env} model={model}", flush=True)
82
+
83
+
84
+ def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
85
+ error_val = error if error else "null"
86
+ done_val = str(done).lower()
87
+ print(
88
+ f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}",
89
+ flush=True,
90
+ )
91
+
92
+
93
+ def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
94
+ rewards_str = ",".join(f"{r:.2f}" for r in rewards)
95
+ print(f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}", flush=True)
96
+
97
+
98
+ def build_user_prompt(step: int, last_echoed: str, last_reward: float, history: List[str]) -> str:
99
+ history_block = "\n".join(history[-4:]) if history else "None"
100
+ return textwrap.dedent(
101
+ f"""
102
+ Step: {step}
103
+ Last echoed message: {last_echoed!r}
104
+ Last reward: {last_reward:.2f}
105
+ Previous steps:
106
+ {history_block}
107
+ Send your next message.
108
+ """
109
+ ).strip()
110
+
111
+
112
+ def get_model_message(client: OpenAI, step: int, last_echoed: str, last_reward: float, history: List[str]) -> str:
113
+ user_prompt = build_user_prompt(step, last_echoed, last_reward, history)
114
+ try:
115
+ completion = client.chat.completions.create(
116
+ model=MODEL_NAME,
117
+ messages=[
118
+ {"role": "system", "content": SYSTEM_PROMPT},
119
+ {"role": "user", "content": user_prompt},
120
+ ],
121
+ temperature=TEMPERATURE,
122
+ max_tokens=MAX_TOKENS,
123
+ stream=False,
124
+ )
125
+ text = (completion.choices[0].message.content or "").strip()
126
+ return text if text else "hello"
127
+ except Exception as exc:
128
+ print(f"[DEBUG] Model request failed: {exc}", flush=True)
129
+ return "hello"
130
+
131
+
132
+ async def main() -> None:
133
+ client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
134
+
135
+ env = await MyEnvV4Env.from_docker_image(IMAGE_NAME)
136
+
137
+ history: List[str] = []
138
+ rewards: List[float] = []
139
+ steps_taken = 0
140
+ score = 0.0
141
+ success = False
142
+
143
+ log_start(task=TASK_NAME, env=BENCHMARK, model=MODEL_NAME)
144
+
145
+ try:
146
+ result = await env.reset() # OpenENV.reset()
147
+ last_echoed = result.observation.echoed_message
148
+ last_reward = 0.0
149
+
150
+ for step in range(1, MAX_STEPS + 1):
151
+ if result.done:
152
+ break
153
+
154
+ message = get_model_message(client, step, last_echoed, last_reward, history)
155
+
156
+ result = await env.step(MyEnvV4Action(message=message))
157
+ obs = result.observation
158
+
159
+ reward = result.reward or 0.0
160
+ done = result.done
161
+ error = None
162
+
163
+ rewards.append(reward)
164
+ steps_taken = step
165
+ last_echoed = obs.echoed_message
166
+ last_reward = reward
167
+
168
+ log_step(step=step, action=message, reward=reward, done=done, error=error)
169
+
170
+ history.append(f"Step {step}: {message!r} -> reward {reward:+.2f}")
171
+
172
+ if done:
173
+ break
174
+
175
+ score = sum(rewards) / MAX_TOTAL_REWARD if MAX_TOTAL_REWARD > 0 else 0.0
176
+ score = min(max(score, 0.0), 1.0) # clamp to [0, 1]
177
+ success = score >= SUCCESS_SCORE_THRESHOLD
178
+
179
+ finally:
180
+ try:
181
+ await env.close()
182
+ except Exception as e:
183
+ print(f"[DEBUG] env.close() error (container cleanup): {e}", flush=True)
184
+ log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
185
+
186
+
187
+ if __name__ == "__main__":
188
+ asyncio.run(main())
inference.py CHANGED
@@ -1,60 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
 
 
2
  import textwrap
3
- from openai import OpenAI
4
- from typing import List, Dict, Any
5
  from dotenv import load_dotenv
 
6
 
7
  load_dotenv()
8
 
9
  from client import SqlEnvClient
10
  from models import SqlAction
11
 
12
- # OpenEnv execution environment config
13
- ENV_CONTAINER_START = os.getenv("ENV_CONTAINER_START", "false").lower() == "true"
14
- ENV_IMAGE_NAME = "sql-agent-env:latest"
15
-
16
- # LLM inference config as per instructions
17
  API_BASE_URL = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
18
- API_KEY = os.getenv("OPENAI_API_KEY")
19
  MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4o-mini")
20
- MAX_STEPS = int(os.getenv("MAX_STEPS", "20"))
 
 
 
 
 
 
 
 
 
21
  OPENAI_SEED = int(os.getenv("OPENAI_SEED", "42"))
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  SYSTEM_PROMPT = textwrap.dedent(
24
  """
25
  You are an expert Data Analyst and SQL Agent.
26
  You will be provided with a SQL Schema and an instruction.
27
- Reply with exactly one JSON block containing the SQL query to execute.
28
- Do NOT provide markdown formatting or explanations. DO NOT wrap in ```sql.
29
- The response should be the pure string of the query itself.
30
  """
31
  ).strip()
32
 
33
- def build_user_prompt(step: int, observation, history: List[str]) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  schema = observation.schema_info
35
  instruction = observation.current_task_instruction
36
  exec_result = observation.execution_result or "None"
37
  error = observation.execution_error or "None"
38
-
39
- prompt = textwrap.dedent(
40
  f"""
41
  Step: {step}
42
  Database Schema:
43
  {schema}
44
-
45
  Task Instruction:
46
  {instruction}
47
-
48
  Previous Execution Result: {exec_result}
49
  Previous Error: {error}
50
-
 
 
 
51
  Write the precise SQL query string to accomplish the task instruction. Return nothing else.
52
  """
53
  ).strip()
54
- return prompt
55
 
56
  def parse_model_action(response_text: str) -> str:
57
- query = response_text.strip()
58
  if query.startswith("```sql"):
59
  query = query[6:]
60
  if query.startswith("```"):
@@ -63,120 +144,141 @@ def parse_model_action(response_text: str) -> str:
63
  query = query[:-3]
64
  return query.strip()
65
 
66
- def main():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  if not API_KEY:
68
- raise RuntimeError("OPENAI_API_KEY is required for reproducible baseline runs.")
 
 
 
 
 
 
 
69
 
70
  client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
71
 
72
- print("--- SQL Data Analyst OpenEnv Baseline ---")
73
-
74
  if ENV_CONTAINER_START:
75
- print(f"Starting docker image: {ENV_IMAGE_NAME}")
76
  env = SqlEnvClient.from_docker_image(ENV_IMAGE_NAME).sync()
77
  else:
78
- # Fallback to local memory-bound initialization
79
- from server.environment import SqlEnvironment
80
- base_env = SqlEnvironment()
81
-
82
- # Direct wrapper
83
- class DirectClient:
84
- def __init__(self, target):
85
- self.target = target
86
- def reset(self):
87
- obs = self.target.reset()
88
- return type('StepResult', (), {'observation': obs, 'reward': 0.0, 'done': False})()
89
- def step(self, action):
90
- obs = self.target.step(action)
91
- return type(
92
- 'StepResult',
93
- (),
94
- {
95
- 'observation': obs,
96
- 'reward': obs.reward if obs.reward is not None else 0.0,
97
- 'done': obs.done,
98
- },
99
- )()
100
- def close(self):
101
- pass
102
- env = DirectClient(base_env)
103
 
104
  history: List[str] = []
105
- task_scores: Dict[str, float] = {}
106
- run_trace: List[Dict[str, Any]] = []
107
-
 
 
 
 
 
 
 
108
  try:
109
  result = env.reset()
110
  observation = result.observation
111
- print(f"\n[Episode Start] Initial Task: {observation.current_task_instruction}")
112
-
113
  for step in range(1, MAX_STEPS + 1):
114
  if result.done:
115
- print("Environment signalled done. Stopping early.")
116
  break
117
-
118
  user_prompt = build_user_prompt(step, observation, history)
119
-
120
  messages = [
121
  {"role": "system", "content": SYSTEM_PROMPT},
122
  {"role": "user", "content": user_prompt},
123
  ]
124
-
125
- completion = client.chat.completions.create(
126
- model=MODEL_NAME,
127
- messages=messages,
128
- temperature=0.0,
129
- seed=OPENAI_SEED,
130
- stream=False,
131
- )
132
- response_text = completion.choices[0].message.content or "SELECT 1"
 
 
 
 
 
 
 
133
 
134
  action_str = parse_model_action(response_text)
135
- print(f"\n[Step {step}] Model suggested: {action_str}")
136
- current_task_id = observation.task_id
137
  result = env.step(SqlAction(query=action_str))
138
  observation = result.observation
139
- reward = result.reward
140
- done = result.done
141
-
142
- print(f" Reward: {reward:+.2f} | Done: {done}")
143
- print(f" Task Score: {observation.task_score:.3f} | Difficulty: {observation.difficulty}")
144
- if observation.grader_feedback:
145
- print(f" Grader: {observation.grader_feedback}")
146
- if observation.execution_error:
147
- print(f" Error: {observation.execution_error[:200]}")
148
-
149
- if observation.task_score > 0:
150
- task_scores[current_task_id] = max(task_scores.get(current_task_id, 0.0), observation.task_score)
151
-
152
- history.append(f"Q: {action_str} -> R: {reward:+.2f}")
153
- run_trace.append(
154
- {
155
- "step": step,
156
- "task_id": current_task_id,
157
- "action": action_str,
158
- "reward": reward,
159
- "task_score": observation.task_score,
160
- "done": done,
161
- }
162
- )
163
-
164
  if done:
165
- print("\nAll tasks complete!")
166
  break
167
 
168
- print("\n=== Baseline Summary ===")
169
- for task_id in sorted(task_scores):
170
- print(f"{task_id}: {task_scores[task_id]:.3f}")
171
- task_avg = (sum(task_scores.values()) / len(task_scores)) if task_scores else 0.0
172
- total_return = sum(item["reward"] for item in run_trace)
173
- print(f"average_task_score: {task_avg:.3f}")
174
- print(f"episode_return: {total_return:.3f}")
175
- print(f"model: {MODEL_NAME} | seed: {OPENAI_SEED}")
176
- print("reproducibility_note: deterministic prompt, temperature=0.0, fixed seed")
177
 
 
 
 
178
  finally:
179
- env.close()
 
 
 
 
 
180
 
181
  if __name__ == "__main__":
182
  main()
 
1
+ """
2
+ SQL Data Analyst OpenEnv — baseline inference (hackathon format).
3
+
4
+ Environment variables (set before running):
5
+ HF_TOKEN Primary API key (Hugging Face / OpenAI-compatible).
6
+ API_BASE_URL LLM base URL (e.g. https://api.openai.com/v1 or HF router).
7
+ MODEL_NAME Model id for chat completions.
8
+ OPENAI_API_KEY Optional fallback if HF_TOKEN is unset.
9
+ API_KEY Optional second fallback.
10
+
11
+ Optional:
12
+ SQL_AGENT_TASK Logged as task= in [START] (default: sql_analyst_episode).
13
+ SQL_AGENT_BENCHMARK Logged as env= in [START] (default: sql_agent_openenv).
14
+ MAX_STEPS Max env.step calls (default: 24).
15
+ OPENAI_SEED Passed to OpenAI only when base URL looks like OpenAI.
16
+ ENV_CONTAINER_START true to use SqlEnvClient.from_docker_image (default: false).
17
+ IMAGE_NAME / LOCAL_IMAGE_NAME / ENV_IMAGE_NAME Docker image tag when using container.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
  import os
23
+ import re
24
+ import sys
25
  import textwrap
26
+ from typing import Any, Dict, List, Optional
27
+
28
  from dotenv import load_dotenv
29
+ from openai import OpenAI
30
 
31
  load_dotenv()
32
 
33
  from client import SqlEnvClient
34
  from models import SqlAction
35
 
36
+ # --- Mandatory hackathon configuration ---
 
 
 
 
37
  API_BASE_URL = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
 
38
  MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4o-mini")
39
+ API_KEY = (
40
+ os.getenv("HF_TOKEN")
41
+ or os.getenv("OPENAI_API_KEY")
42
+ or os.getenv("API_KEY")
43
+ )
44
+
45
+ TASK_NAME = os.getenv("SQL_AGENT_TASK", "sql_analyst_episode")
46
+ BENCHMARK = os.getenv("SQL_AGENT_BENCHMARK", "sql_agent_openenv")
47
+
48
+ MAX_STEPS = int(os.getenv("MAX_STEPS", "24"))
49
  OPENAI_SEED = int(os.getenv("OPENAI_SEED", "42"))
50
 
51
+ ENV_CONTAINER_START = os.getenv("ENV_CONTAINER_START", "false").lower() == "true"
52
+ ENV_IMAGE_NAME = (
53
+ os.getenv("LOCAL_IMAGE_NAME")
54
+ or os.getenv("IMAGE_NAME")
55
+ or os.getenv("ENV_IMAGE_NAME", "sql-agent-env:latest")
56
+ )
57
+
58
+ # Grader task ids (must match core/tasks.py) for normalized [END] score in [0, 1]
59
+ TASK_IDS = [
60
+ "employee_payroll_overview",
61
+ "department_budget_summary",
62
+ "senior_engineering_comp_review",
63
+ ]
64
+
65
  SYSTEM_PROMPT = textwrap.dedent(
66
  """
67
  You are an expert Data Analyst and SQL Agent.
68
  You will be provided with a SQL Schema and an instruction.
69
+ Reply with exactly one SQL query string to execute.
70
+ Do NOT use markdown fences or explanations only the raw SQL text.
 
71
  """
72
  ).strip()
73
 
74
+
75
+ def log_start(task: str, env: str, model: str) -> None:
76
+ print(f"[START] task={task} env={env} model={model}", flush=True)
77
+
78
+
79
+ def log_step(
80
+ step: int,
81
+ action: str,
82
+ reward: float,
83
+ done: bool,
84
+ error: Optional[str],
85
+ ) -> None:
86
+ err = error if error else "null"
87
+ err_one = sanitize_one_line(err) if err != "null" else "null"
88
+ act_one = sanitize_one_line(action)
89
+ done_val = str(done).lower()
90
+ print(
91
+ f"[STEP] step={step} action={act_one} reward={reward:.2f} done={done_val} error={err_one}",
92
+ flush=True,
93
+ )
94
+
95
+
96
+ def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
97
+ # Evaluators expect a comma-separated list; use 0.00 if no steps ran.
98
+ rewards_str = ",".join(f"{r:.2f}" for r in rewards) if rewards else "0.00"
99
+ print(
100
+ f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}",
101
+ flush=True,
102
+ )
103
+
104
+
105
+ def sanitize_one_line(s: str) -> str:
106
+ if not s:
107
+ return ""
108
+ return " ".join(s.split())
109
+
110
+
111
+ def build_user_prompt(step: int, observation: Any, history: List[str]) -> str:
112
  schema = observation.schema_info
113
  instruction = observation.current_task_instruction
114
  exec_result = observation.execution_result or "None"
115
  error = observation.execution_error or "None"
116
+ history_block = "\n".join(history[-6:]) if history else "None"
117
+ return textwrap.dedent(
118
  f"""
119
  Step: {step}
120
  Database Schema:
121
  {schema}
122
+
123
  Task Instruction:
124
  {instruction}
125
+
126
  Previous Execution Result: {exec_result}
127
  Previous Error: {error}
128
+
129
+ Recent history:
130
+ {history_block}
131
+
132
  Write the precise SQL query string to accomplish the task instruction. Return nothing else.
133
  """
134
  ).strip()
135
+
136
 
137
  def parse_model_action(response_text: str) -> str:
138
+ query = (response_text or "").strip()
139
  if query.startswith("```sql"):
140
  query = query[6:]
141
  if query.startswith("```"):
 
144
  query = query[:-3]
145
  return query.strip()
146
 
147
+
148
+ def _make_direct_client():
149
+ from server.environment import SqlEnvironment
150
+
151
+ base_env = SqlEnvironment()
152
+
153
+ class DirectClient:
154
+ def __init__(self, target):
155
+ self.target = target
156
+
157
+ def reset(self):
158
+ obs = self.target.reset()
159
+ return type(
160
+ "StepResult",
161
+ (),
162
+ {"observation": obs, "reward": 0.0, "done": False},
163
+ )()
164
+
165
+ def step(self, action):
166
+ obs = self.target.step(action)
167
+ return type(
168
+ "StepResult",
169
+ (),
170
+ {
171
+ "observation": obs,
172
+ "reward": obs.reward if obs.reward is not None else 0.0,
173
+ "done": obs.done,
174
+ },
175
+ )()
176
+
177
+ def close(self):
178
+ pass
179
+
180
+ return DirectClient(base_env)
181
+
182
+
183
+ def main() -> None:
184
  if not API_KEY:
185
+ print(
186
+ "[DEBUG] Set HF_TOKEN (or OPENAI_API_KEY) before running.",
187
+ file=sys.stderr,
188
+ flush=True,
189
+ )
190
+ raise RuntimeError(
191
+ "Missing API key: set HF_TOKEN or OPENAI_API_KEY for OpenAI-compatible client."
192
+ )
193
 
194
  client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
195
 
 
 
196
  if ENV_CONTAINER_START:
197
+ print(f"[DEBUG] Using docker image: {ENV_IMAGE_NAME}", file=sys.stderr, flush=True)
198
  env = SqlEnvClient.from_docker_image(ENV_IMAGE_NAME).sync()
199
  else:
200
+ env = _make_direct_client()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
  history: List[str] = []
203
+ rewards: List[float] = []
204
+ best_task_score: Dict[str, float] = {tid: 0.0 for tid in TASK_IDS}
205
+
206
+ steps_taken = 0
207
+ score = 0.0
208
+ success = False
209
+ last_done = False
210
+
211
+ log_start(task=TASK_NAME, env=BENCHMARK, model=MODEL_NAME)
212
+
213
  try:
214
  result = env.reset()
215
  observation = result.observation
216
+
 
217
  for step in range(1, MAX_STEPS + 1):
218
  if result.done:
 
219
  break
220
+
221
  user_prompt = build_user_prompt(step, observation, history)
 
222
  messages = [
223
  {"role": "system", "content": SYSTEM_PROMPT},
224
  {"role": "user", "content": user_prompt},
225
  ]
226
+
227
+ create_kwargs: Dict[str, Any] = {
228
+ "model": MODEL_NAME,
229
+ "messages": messages,
230
+ "temperature": 0.0,
231
+ "stream": False,
232
+ }
233
+ if re.search(r"openai\.com", API_BASE_URL, re.I):
234
+ create_kwargs["seed"] = OPENAI_SEED
235
+
236
+ try:
237
+ completion = client.chat.completions.create(**create_kwargs)
238
+ response_text = completion.choices[0].message.content or "SELECT 1"
239
+ except Exception as exc:
240
+ print(f"[DEBUG] Model request failed: {exc}", file=sys.stderr, flush=True)
241
+ response_text = "SELECT 1"
242
 
243
  action_str = parse_model_action(response_text)
 
 
244
  result = env.step(SqlAction(query=action_str))
245
  observation = result.observation
246
+ reward = float(result.reward if result.reward is not None else 0.0)
247
+ done = bool(result.done)
248
+ last_done = done
249
+
250
+ err_raw = observation.execution_error
251
+ rewards.append(reward)
252
+ steps_taken = step
253
+
254
+ tid = getattr(observation, "task_id", None)
255
+ if tid in best_task_score:
256
+ best_task_score[tid] = max(
257
+ best_task_score[tid], float(observation.task_score or 0.0)
258
+ )
259
+
260
+ log_step(step=step, action=action_str, reward=reward, done=done, error=err_raw)
261
+
262
+ history.append(f"Q: {action_str[:200]} -> R: {reward:+.2f}")
263
+
 
 
 
 
 
 
 
264
  if done:
 
265
  break
266
 
267
+ score = sum(best_task_score[t] for t in TASK_IDS) / len(TASK_IDS)
268
+ score = min(max(score, 0.0), 1.0)
269
+ # Success: episode ended in terminal success state with strong average task score
270
+ success = last_done and score >= 0.95
 
 
 
 
 
271
 
272
+ except Exception as exc:
273
+ print(f"[DEBUG] Episode error: {exc}", file=sys.stderr, flush=True)
274
+ success = False
275
  finally:
276
+ try:
277
+ env.close()
278
+ except Exception as exc:
279
+ print(f"[DEBUG] env.close() error: {exc}", file=sys.stderr, flush=True)
280
+ log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
281
+
282
 
283
  if __name__ == "__main__":
284
  main()
openenv.yaml CHANGED
@@ -1,5 +1,6 @@
1
  spec_version: 1
2
  name: sql-agent-env
 
3
  type: space
4
  runtime: fastapi
5
  app: server.app:app
 
1
  spec_version: 1
2
  name: sql-agent-env
3
+ description: OpenEnv SQL data analyst — schema → SQL tasks with deterministic graders
4
  type: space
5
  runtime: fastapi
6
  app: server.app:app