Spaces:
Sleeping
Sleeping
Commit ·
1a1713a
1
Parent(s): 51264be
Fix server entrypoint packaging and align environment docs
Browse files- README.md +21 -20
- openenv.yaml +2 -2
- pyproject.toml +1 -1
- server.py +5 -107
- server/app.py +0 -8
- sql_env/models.py +1 -1
- sql_env/server.py +116 -0
README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
---
|
| 2 |
title: SQL Correction RL Environment
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: docker
|
|
@@ -12,15 +12,16 @@ tags:
|
|
| 12 |
# SQL Correction RL Environment
|
| 13 |
|
| 14 |
An **OpenEnv-compliant** reinforcement learning environment where an AI agent
|
| 15 |
-
learns to fix broken SQL queries
|
| 16 |
|
| 17 |
---
|
| 18 |
|
| 19 |
## Description & Motivation
|
| 20 |
|
| 21 |
-
SQL errors are one of the most common and costly mistakes in software
|
| 22 |
-
This environment trains agents to identify and correct SQL syntax
|
| 23 |
-
errors, ranging from simple typos to complex multi-join query
|
|
|
|
| 24 |
|
| 25 |
The environment provides **partial progress signals** at every step; the agent
|
| 26 |
receives graded feedback even for near-correct answers, enabling meaningful
|
|
@@ -34,7 +35,7 @@ learning across the full trajectory rather than sparse end-of-episode rewards.
|
|
| 34 |
|--------------------|-----------------|----------------------------------------------------------|
|
| 35 |
| `task_id` | string | Unique identifier for the current task instance |
|
| 36 |
| `broken_query` | string | The malformed SQL query the agent must fix |
|
| 37 |
-
| `schema_context` | string or null | Table
|
| 38 |
| `error_hint` | string or null | Plain-language hint about the error (easy tasks only) |
|
| 39 |
| `step_number` | integer | Current step within the episode |
|
| 40 |
| `previous_attempt` | string or null | The agent's SQL output from the previous step |
|
|
@@ -42,9 +43,9 @@ learning across the full trajectory rather than sparse end-of-episode rewards.
|
|
| 42 |
|
| 43 |
## Action Space
|
| 44 |
|
| 45 |
-
| Field | Type | Description
|
| 46 |
-
|--------------------|--------|---------------------------------
|
| 47 |
-
| `corrected_query` | string | The agent's corrected SQL query
|
| 48 |
|
| 49 |
---
|
| 50 |
|
|
@@ -52,9 +53,9 @@ learning across the full trajectory rather than sparse end-of-episode rewards.
|
|
| 52 |
|
| 53 |
| Name | Difficulty | Max Steps | Description |
|
| 54 |
|----------|------------|-----------|-------------|
|
| 55 |
-
| `easy` | Easy | 5 | Fix a single syntax error (
|
| 56 |
-
| `medium` | Medium |
|
| 57 |
-
| `hard` | Hard |
|
| 58 |
|
| 59 |
---
|
| 60 |
|
|
@@ -64,11 +65,11 @@ learning across the full trajectory rather than sparse end-of-episode rewards.
|
|
| 64 |
|-------|-----------|
|
| 65 |
| `1.0` | Exact match after normalization (perfect fix) |
|
| 66 |
| `0.7` | All correct tokens present, structure slightly off |
|
| 67 |
-
| `0.
|
| 68 |
-
| `0.
|
| 69 |
| `0.0` | Query still incorrect |
|
| 70 |
|
| 71 |
-
Episodes terminate when reward = 1.0 (success) or
|
| 72 |
|
| 73 |
---
|
| 74 |
|
|
@@ -125,11 +126,11 @@ SQL_ENV_TASK=hard python inference.py
|
|
| 125 |
|
| 126 |
## Baseline Scores
|
| 127 |
|
| 128 |
-
| Task | Model
|
| 129 |
-
|--------|------------------
|
| 130 |
-
| easy | Qwen/Qwen2.5-72B
|
| 131 |
-
| medium | Qwen/Qwen2.5-72B
|
| 132 |
-
| hard | Qwen/Qwen2.5-72B
|
| 133 |
|
| 134 |
*Run `inference.py` against the live Space to reproduce these scores.*
|
| 135 |
|
|
|
|
| 1 |
---
|
| 2 |
title: SQL Correction RL Environment
|
| 3 |
+
emoji: "🛠️"
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: docker
|
|
|
|
| 12 |
# SQL Correction RL Environment
|
| 13 |
|
| 14 |
An **OpenEnv-compliant** reinforcement learning environment where an AI agent
|
| 15 |
+
learns to fix broken SQL queries, a real task that developers face every day.
|
| 16 |
|
| 17 |
---
|
| 18 |
|
| 19 |
## Description & Motivation
|
| 20 |
|
| 21 |
+
SQL errors are one of the most common and costly mistakes in software
|
| 22 |
+
development. This environment trains agents to identify and correct SQL syntax
|
| 23 |
+
and logical errors, ranging from simple typos to complex multi-join query
|
| 24 |
+
reconstruction.
|
| 25 |
|
| 26 |
The environment provides **partial progress signals** at every step; the agent
|
| 27 |
receives graded feedback even for near-correct answers, enabling meaningful
|
|
|
|
| 35 |
|--------------------|-----------------|----------------------------------------------------------|
|
| 36 |
| `task_id` | string | Unique identifier for the current task instance |
|
| 37 |
| `broken_query` | string | The malformed SQL query the agent must fix |
|
| 38 |
+
| `schema_context` | string or null | Table and column definitions when a task includes them |
|
| 39 |
| `error_hint` | string or null | Plain-language hint about the error (easy tasks only) |
|
| 40 |
| `step_number` | integer | Current step within the episode |
|
| 41 |
| `previous_attempt` | string or null | The agent's SQL output from the previous step |
|
|
|
|
| 43 |
|
| 44 |
## Action Space
|
| 45 |
|
| 46 |
+
| Field | Type | Description |
|
| 47 |
+
|--------------------|--------|---------------------------------|
|
| 48 |
+
| `corrected_query` | string | The agent's corrected SQL query |
|
| 49 |
|
| 50 |
---
|
| 51 |
|
|
|
|
| 53 |
|
| 54 |
| Name | Difficulty | Max Steps | Description |
|
| 55 |
|----------|------------|-----------|-------------|
|
| 56 |
+
| `easy` | Easy | 5 | Fix a single syntax error (for example `FORM` -> `FROM`). Hint provided. |
|
| 57 |
+
| `medium` | Medium | 5 | Fix multiple errors including missing keywords and wrong clauses. No hint. |
|
| 58 |
+
| `hard` | Hard | 4 | Fix complex multi-join queries with subtle errors and wrong clause ordering. Schema provided, no hint. |
|
| 59 |
|
| 60 |
---
|
| 61 |
|
|
|
|
| 65 |
|-------|-----------|
|
| 66 |
| `1.0` | Exact match after normalization (perfect fix) |
|
| 67 |
| `0.7` | All correct tokens present, structure slightly off |
|
| 68 |
+
| `0.4` | Most keywords correct and token overlap is high |
|
| 69 |
+
| `0.2` | Basic `SELECT ... FROM ...` structure present |
|
| 70 |
| `0.0` | Query still incorrect |
|
| 71 |
|
| 72 |
+
Episodes terminate when reward = 1.0 (success) or max steps is reached.
|
| 73 |
|
| 74 |
---
|
| 75 |
|
|
|
|
| 126 |
|
| 127 |
## Baseline Scores
|
| 128 |
|
| 129 |
+
| Task | Model | Avg Score | Notes |
|
| 130 |
+
|--------|------------------|-----------|-------|
|
| 131 |
+
| easy | Qwen/Qwen2.5-72B | ~0.85 | Single typo fix, hint provided |
|
| 132 |
+
| medium | Qwen/Qwen2.5-72B | ~0.62 | Multi-error correction |
|
| 133 |
+
| hard | Qwen/Qwen2.5-72B | ~0.38 | Complex multi-join, schema-guided |
|
| 134 |
|
| 135 |
*Run `inference.py` against the live Space to reproduce these scores.*
|
| 136 |
|
openenv.yaml
CHANGED
|
@@ -23,7 +23,7 @@ observation_space:
|
|
| 23 |
schema_context:
|
| 24 |
type: string
|
| 25 |
nullable: true
|
| 26 |
-
description: Table and column definitions
|
| 27 |
error_hint:
|
| 28 |
type: string
|
| 29 |
nullable: true
|
|
@@ -73,4 +73,4 @@ tasks:
|
|
| 73 |
endpoints:
|
| 74 |
reset: POST /reset
|
| 75 |
step: POST /step
|
| 76 |
-
state:
|
|
|
|
| 23 |
schema_context:
|
| 24 |
type: string
|
| 25 |
nullable: true
|
| 26 |
+
description: Table and column definitions when a task includes schema context
|
| 27 |
error_hint:
|
| 28 |
type: string
|
| 29 |
nullable: true
|
|
|
|
| 73 |
endpoints:
|
| 74 |
reset: POST /reset
|
| 75 |
step: POST /step
|
| 76 |
+
state: POST /state
|
pyproject.toml
CHANGED
|
@@ -20,4 +20,4 @@ build-backend = "hatchling.build"
|
|
| 20 |
packages = ["sql_env"]
|
| 21 |
|
| 22 |
[project.scripts]
|
| 23 |
-
server = "
|
|
|
|
| 20 |
packages = ["sql_env"]
|
| 21 |
|
| 22 |
[project.scripts]
|
| 23 |
+
server = "sql_env.server:main"
|
server.py
CHANGED
|
@@ -1,111 +1,9 @@
|
|
| 1 |
-
"""
|
| 2 |
-
server.py — FastAPI HTTP wrapper for SQLCorrectionEnv
|
| 3 |
-
Exposes the OpenEnv-required endpoints: /reset, /step, /state
|
| 4 |
-
"""
|
| 5 |
|
| 6 |
-
import
|
| 7 |
-
from contextlib import asynccontextmanager
|
| 8 |
-
from typing import Optional
|
| 9 |
|
| 10 |
-
|
| 11 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 12 |
-
from pydantic import BaseModel
|
| 13 |
|
| 14 |
-
from sql_env import SQLCorrectionEnv, SQLAction
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
class ResetRequest(BaseModel):
|
| 20 |
-
difficulty: Optional[str] = "easy"
|
| 21 |
-
task_name: Optional[str] = None
|
| 22 |
-
task_index: Optional[int] = None
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
class StepRequest(BaseModel):
|
| 26 |
-
corrected_query: str
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
# ── App setup ─────────────────────────────────────────────────
|
| 30 |
-
|
| 31 |
-
env: Optional[SQLCorrectionEnv] = None
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
@asynccontextmanager
|
| 35 |
-
async def lifespan(app: FastAPI):
|
| 36 |
-
global env
|
| 37 |
-
env = SQLCorrectionEnv(difficulty="easy")
|
| 38 |
-
yield
|
| 39 |
-
if env:
|
| 40 |
-
await env.close()
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
app = FastAPI(
|
| 44 |
-
title="SQL Correction RL Environment",
|
| 45 |
-
description="OpenEnv-compliant environment for SQL query correction tasks.",
|
| 46 |
-
version="1.0.0",
|
| 47 |
-
lifespan=lifespan,
|
| 48 |
-
)
|
| 49 |
-
|
| 50 |
-
app.add_middleware(
|
| 51 |
-
CORSMiddleware,
|
| 52 |
-
allow_origins=["*"],
|
| 53 |
-
allow_methods=["*"],
|
| 54 |
-
allow_headers=["*"],
|
| 55 |
-
)
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
# ── Endpoints ─────────────────────────────────────────────────
|
| 59 |
-
|
| 60 |
-
@app.post("/reset")
|
| 61 |
-
async def reset(request: ResetRequest = ResetRequest()):
|
| 62 |
-
"""Reset the environment. Returns initial observation."""
|
| 63 |
-
global env
|
| 64 |
-
difficulty = request.task_name or request.difficulty or "easy"
|
| 65 |
-
if difficulty not in ("easy", "medium", "hard"):
|
| 66 |
-
raise HTTPException(status_code=400, detail="difficulty must be easy, medium, or hard")
|
| 67 |
-
|
| 68 |
-
env = SQLCorrectionEnv(
|
| 69 |
-
difficulty=difficulty,
|
| 70 |
-
task_index=request.task_index,
|
| 71 |
-
)
|
| 72 |
-
obs = await env.reset()
|
| 73 |
-
return obs.model_dump()
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
@app.post("/step")
|
| 77 |
-
async def step(request: StepRequest):
|
| 78 |
-
"""Take one step. Returns observation, reward, done, info."""
|
| 79 |
-
global env
|
| 80 |
-
if env is None:
|
| 81 |
-
raise HTTPException(status_code=400, detail="Call /reset first.")
|
| 82 |
-
try:
|
| 83 |
-
action = SQLAction(corrected_query=request.corrected_query)
|
| 84 |
-
result = await env.step(action)
|
| 85 |
-
return result.model_dump()
|
| 86 |
-
except RuntimeError as e:
|
| 87 |
-
raise HTTPException(status_code=400, detail=str(e))
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
@app.post("/state")
|
| 91 |
-
async def state():
|
| 92 |
-
"""Return current environment state."""
|
| 93 |
-
global env
|
| 94 |
-
if env is None:
|
| 95 |
-
return {"status": "not_initialized"}
|
| 96 |
-
return await env.state()
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
@app.get("/health")
|
| 100 |
-
async def health():
|
| 101 |
-
return {"status": "ok", "service": "sql-correction-env"}
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
@app.get("/")
|
| 105 |
-
async def root():
|
| 106 |
-
return {
|
| 107 |
-
"name": "SQL Correction RL Environment",
|
| 108 |
-
"version": "1.0.0",
|
| 109 |
-
"endpoints": ["/reset", "/step", "/state", "/health"],
|
| 110 |
-
"tasks": ["easy", "medium", "hard"],
|
| 111 |
-
}
|
|
|
|
| 1 |
+
"""Compatibility wrapper for local `python server.py` runs."""
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
from sql_env.server import app, main
|
|
|
|
|
|
|
| 4 |
|
| 5 |
+
__all__ = ["app", "main"]
|
|
|
|
|
|
|
| 6 |
|
|
|
|
| 7 |
|
| 8 |
+
if __name__ == "__main__":
|
| 9 |
+
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/app.py
DELETED
|
@@ -1,8 +0,0 @@
|
|
| 1 |
-
from server import app
|
| 2 |
-
import uvicorn
|
| 3 |
-
|
| 4 |
-
def main():
|
| 5 |
-
uvicorn.run(app, host="0.0.0.0", port=7860)
|
| 6 |
-
|
| 7 |
-
if __name__ == "__main__":
|
| 8 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sql_env/models.py
CHANGED
|
@@ -35,4 +35,4 @@ class StepResult(BaseModel):
|
|
| 35 |
observation: SQLObservation
|
| 36 |
reward: float
|
| 37 |
done: bool
|
| 38 |
-
info: Dict[str, Any] =
|
|
|
|
| 35 |
observation: SQLObservation
|
| 36 |
reward: float
|
| 37 |
done: bool
|
| 38 |
+
info: Dict[str, Any] = Field(default_factory=dict)
|
sql_env/server.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FastAPI HTTP wrapper for SQLCorrectionEnv.
|
| 3 |
+
|
| 4 |
+
Exposes the OpenEnv-required endpoints: /reset, /step, /state.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from contextlib import asynccontextmanager
|
| 8 |
+
from typing import Optional
|
| 9 |
+
|
| 10 |
+
from fastapi import FastAPI, HTTPException
|
| 11 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 12 |
+
from pydantic import BaseModel
|
| 13 |
+
|
| 14 |
+
from sql_env import SQLAction, SQLCorrectionEnv
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class ResetRequest(BaseModel):
|
| 18 |
+
difficulty: Optional[str] = "easy"
|
| 19 |
+
task_name: Optional[str] = None
|
| 20 |
+
task_index: Optional[int] = None
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class StepRequest(BaseModel):
|
| 24 |
+
corrected_query: str
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
env: Optional[SQLCorrectionEnv] = None
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@asynccontextmanager
|
| 31 |
+
async def lifespan(_: FastAPI):
|
| 32 |
+
global env
|
| 33 |
+
env = SQLCorrectionEnv(difficulty="easy")
|
| 34 |
+
yield
|
| 35 |
+
if env is not None:
|
| 36 |
+
await env.close()
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
app = FastAPI(
|
| 40 |
+
title="SQL Correction RL Environment",
|
| 41 |
+
description="OpenEnv-compliant environment for SQL query correction tasks.",
|
| 42 |
+
version="1.0.0",
|
| 43 |
+
lifespan=lifespan,
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
app.add_middleware(
|
| 47 |
+
CORSMiddleware,
|
| 48 |
+
allow_origins=["*"],
|
| 49 |
+
allow_methods=["*"],
|
| 50 |
+
allow_headers=["*"],
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@app.post("/reset")
|
| 55 |
+
async def reset(request: ResetRequest = ResetRequest()):
|
| 56 |
+
"""Reset the environment and return the initial observation."""
|
| 57 |
+
global env
|
| 58 |
+
difficulty = request.task_name or request.difficulty or "easy"
|
| 59 |
+
if difficulty not in {"easy", "medium", "hard"}:
|
| 60 |
+
raise HTTPException(status_code=400, detail="difficulty must be easy, medium, or hard")
|
| 61 |
+
|
| 62 |
+
env = SQLCorrectionEnv(
|
| 63 |
+
difficulty=difficulty,
|
| 64 |
+
task_index=request.task_index,
|
| 65 |
+
)
|
| 66 |
+
obs = await env.reset()
|
| 67 |
+
return obs.model_dump()
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@app.post("/step")
|
| 71 |
+
async def step(request: StepRequest):
|
| 72 |
+
"""Take one step and return the new observation, reward, done flag, and info."""
|
| 73 |
+
global env
|
| 74 |
+
if env is None:
|
| 75 |
+
raise HTTPException(status_code=400, detail="Call /reset first.")
|
| 76 |
+
|
| 77 |
+
try:
|
| 78 |
+
action = SQLAction(corrected_query=request.corrected_query)
|
| 79 |
+
result = await env.step(action)
|
| 80 |
+
return result.model_dump()
|
| 81 |
+
except RuntimeError as exc:
|
| 82 |
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
@app.post("/state")
|
| 86 |
+
async def state():
|
| 87 |
+
"""Return current environment state."""
|
| 88 |
+
global env
|
| 89 |
+
if env is None:
|
| 90 |
+
return {"status": "not_initialized"}
|
| 91 |
+
return await env.state()
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
@app.get("/health")
|
| 95 |
+
async def health():
|
| 96 |
+
return {"status": "ok", "service": "sql-correction-env"}
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
@app.get("/")
|
| 100 |
+
async def root():
|
| 101 |
+
return {
|
| 102 |
+
"name": "SQL Correction RL Environment",
|
| 103 |
+
"version": "1.0.0",
|
| 104 |
+
"endpoints": ["/reset", "/step", "/state", "/health"],
|
| 105 |
+
"tasks": ["easy", "medium", "hard"],
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def main():
|
| 110 |
+
import uvicorn
|
| 111 |
+
|
| 112 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
if __name__ == "__main__":
|
| 116 |
+
main()
|