Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- README.md +98 -23
- server/constraint_env_environment.py +2 -0
- server/graders.py +3 -3
README.md
CHANGED
|
@@ -9,10 +9,11 @@ license: mit
|
|
| 9 |
short_description: RL training env β natural language to constraint AST
|
| 10 |
base_path: /web
|
| 11 |
---
|
| 12 |
-
[Space ](https://huggingface.co/spaces/DecentSanage/constraint-env)
|
|
|
|
| 13 |
# Constraint Environment
|
| 14 |
|
| 15 |
-
This is the environment for training LLMs to learn a specific DSL made for time table scheduling. Model can then directly output constraints from natural language. Why this is needed? Usually time table generation is an NP hard problem, for humans it could take weeks to generate a conflict free
|
| 16 |
|
| 17 |
```
|
| 18 |
program ::= { constraint }
|
|
@@ -64,11 +65,10 @@ number ::= digit { digit }
|
|
| 64 |
|
| 65 |
```
|
| 66 |
|
| 67 |
-
The model outputs a json which follows the above format which can directly be converted into CP-SAT constraints.
|
| 68 |
|
| 69 |
## Action and Observation
|
| 70 |
-
|
| 71 |
-
The dataset for the training is in this format:
|
| 72 |
|
| 73 |
```python
|
| 74 |
{
|
|
@@ -79,32 +79,95 @@ The dataset for the training is in this format:
|
|
| 79 |
"type": "hard",
|
| 80 |
"name": "no_saturday_classes",
|
| 81 |
"forall": [
|
| 82 |
-
{"
|
| 83 |
-
{"
|
| 84 |
-
{"
|
| 85 |
-
{"
|
| 86 |
],
|
| 87 |
-
"where":
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
},
|
| 90 |
}
|
| 91 |
```
|
| 92 |
|
| 93 |
-
|
| 94 |
-
The model will get the prompt from the environment and will guess the target_ast. It will get the rewards based on how good the guess is.
|
| 95 |
-
|
| 96 |
|
| 97 |
| Output | Reward |
|
| 98 |
| ------------------- | -------- |
|
| 99 |
| Valid Json | 0.125 |
|
| 100 |
| Correct Structure | 0.250 |
|
| 101 |
| Same as the target | 0.625 |
|
|
|
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
## Development & Testing
|
| 105 |
|
| 106 |
-
###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
Test the environment logic directly without starting the HTTP server:
|
| 109 |
|
| 110 |
```bash
|
|
@@ -112,19 +175,29 @@ Test the environment logic directly without starting the HTTP server:
|
|
| 112 |
python3 server/constraint_env_environment.py
|
| 113 |
```
|
| 114 |
|
| 115 |
-
This verifies that:
|
| 116 |
-
- Environment resets correctly
|
| 117 |
-
- Step executes actions properly
|
| 118 |
-
- State tracking works
|
| 119 |
-
- Rewards are calculated correctly
|
| 120 |
-
|
| 121 |
### Running Locally
|
| 122 |
-
|
| 123 |
-
Run the server locally for development:
|
| 124 |
|
| 125 |
```bash
|
| 126 |
uvicorn server.app:app --reload
|
| 127 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
## Project Structure
|
| 130 |
|
|
@@ -137,9 +210,11 @@ constraint_env/
|
|
| 137 |
βββ pyproject.toml # Project metadata and dependencies
|
| 138 |
βββ uv.lock # Locked dependencies (generated)
|
| 139 |
βββ client.py # ConstraintEnv client
|
|
|
|
| 140 |
βββ models.py # Action and Observation models
|
| 141 |
βββ server/
|
| 142 |
βββ __init__.py # Server module exports
|
|
|
|
| 143 |
βββ constraint_env_environment.py # Core environment logic
|
| 144 |
βββ app.py # FastAPI application (HTTP + WebSocket endpoints)
|
| 145 |
βββ Dockerfile # Container image definition
|
|
|
|
| 9 |
short_description: RL training env β natural language to constraint AST
|
| 10 |
base_path: /web
|
| 11 |
---
|
| 12 |
+
[Space UI & Interface](https://huggingface.co/spaces/DecentSanage/constraint-env)
|
| 13 |
+
|
| 14 |
# Constraint Environment
|
| 15 |
|
| 16 |
+
This is the environment for training LLMs to learn a specific DSL made for time table scheduling. Model can then directly output constraints from natural language. Why this is needed? Usually time table generation is an NP hard problem, for humans it could take weeks to generate a conflict free time table. To solve this problem, tools are created to generate them in reasonable time. One example of those tools is CP SAT. Users can write the hardcoded constraints and the solver will generate a time table based on those constraints. Well, what happens when you want to add new constraints? Yes, you have to directly change the code. What if there is a way to directly define constraints in natural language and the solver understands that automatically? That's what we have tried to do with this project. LLM might not be good at scheduling time tables which have dozens of constraints but what it is good at is understanding natural language. For the specific purpose of defining constraints for university time tables a DSL was created whose specification is as follows:
|
| 17 |
|
| 18 |
```
|
| 19 |
program ::= { constraint }
|
|
|
|
| 65 |
|
| 66 |
```
|
| 67 |
|
| 68 |
+
The model outputs a json which follows the above format which can directly be converted into CP-SAT constraints. We have also included `generator.py` which implements the DSL compiler engine directly into the timetable matrix grid natively!
|
| 69 |
|
| 70 |
## Action and Observation
|
| 71 |
+
The dataset for the training operates using dynamic deep AST JSON nodes:
|
|
|
|
| 72 |
|
| 73 |
```python
|
| 74 |
{
|
|
|
|
| 79 |
"type": "hard",
|
| 80 |
"name": "no_saturday_classes",
|
| 81 |
"forall": [
|
| 82 |
+
{"b": "branches"},
|
| 83 |
+
{"sub": {"subjects": "b"}},
|
| 84 |
+
{"d": "days"},
|
| 85 |
+
{"s": "slots"},
|
| 86 |
],
|
| 87 |
+
"where": {
|
| 88 |
+
"operator": "==",
|
| 89 |
+
"left": "d",
|
| 90 |
+
"right": 5
|
| 91 |
+
},
|
| 92 |
+
"assert": {
|
| 93 |
+
"operator": "==",
|
| 94 |
+
"left": {
|
| 95 |
+
"target": "schedule",
|
| 96 |
+
"args": [{"name": "b"}, {"name": "sub"}, "d", "s"]
|
| 97 |
+
},
|
| 98 |
+
"right": 0
|
| 99 |
+
}
|
| 100 |
},
|
| 101 |
}
|
| 102 |
```
|
| 103 |
|
| 104 |
+
The model will get the prompt from the environment and will guess the target_ast across a **multi-step interactive compiler loop**. It will get the rewards based on how good the guess is, suffering a `-0.05` penalty for every step required beyond the first attempting to compile invalid expressions!
|
|
|
|
|
|
|
| 105 |
|
| 106 |
| Output | Reward |
|
| 107 |
| ------------------- | -------- |
|
| 108 |
| Valid Json | 0.125 |
|
| 109 |
| Correct Structure | 0.250 |
|
| 110 |
| Same as the target | 0.625 |
|
| 111 |
+
| **Step Penalty** | **-0.05 per extra step** |
|
| 112 |
|
| 113 |
+
**Final Reward Calculation:** `(Base Reward) - (Steps - 1) * 0.05`. Minimum reward is clamped at 0.01.
|
| 114 |
+
## Task Descriptions, Graders, and Difficulty
|
| 115 |
+
We implemented 3 deterministic tasks escalating in complexity. Each task is aggressively graded dynamically. Every step required by an LLM over a multi-turn compilation loop subtracts 0.05 points from the final step score.
|
| 116 |
+
|
| 117 |
+
1. **EASY (Direct Equality Match)**: Instructs the solver to block out a specific day/slot for an entity. Single assertions, direct variables. (Grader Threshold for Success: **0.80**)
|
| 118 |
+
2. **MEDIUM (Aggregate Objectives)**: Requires the use of `sum` over sub-domains natively to assign specific frequencies to a property attribute check. (Grader Threshold for Success: **0.65**)
|
| 119 |
+
3. **HARD (Deep Conditional Traversal)**: Requires tracking multiple parent nested variables with dense constraints, compound boolean logics in `where`, AND explicit NOT filters natively injected into the dataset arrays. (Grader Threshold for Success: **0.45** due to base LLM difficulty and step penalty accumulation).
|
| 120 |
|
| 121 |
## Development & Testing
|
| 122 |
|
| 123 |
+
### Running Inference Baseline & Scores
|
| 124 |
+
Our baseline framework queries `openai/gpt-oss-120b`.
|
| 125 |
+
**Current Baseline Scores (GPT OSS 120B):**
|
| 126 |
+
- Easy: **0.900** (Success, 1 Step)
|
| 127 |
+
- Medium: **0.800** (Success, 5 Steps)
|
| 128 |
+
- Hard: **0.175** (Failure , 5 Steps)
|
| 129 |
+
|
| 130 |
+
The model integrates exactly according to standard OpenEnv structures. When invoking the `inference.py` framework, the baseline `openai/gpt-oss-120b` navigates our compiler natively:
|
| 131 |
+
|
| 132 |
+
```bash
|
| 133 |
+
[START] task=easy env=constraint_env model=openai/gpt-oss-120b
|
| 134 |
+
[STEP] step=1 action={"type":"hard","name":"cs_branch_no_classes_wed_12","forall":[{"b":"branches"},{"sub":{"subjects":"b"}},{"d":"days"},{"s":"slots"}],"where":{"operator":"AND","left":{"operator":"==","left":{"name":"b"},"right":"CS"},"right":{"operator":"AND","left":{"operator":"==","left":"d","right":2},"right":{"operator":"==","left":"s","right":3}}},"assert":{"operator":"==","left":{"target":"schedule","args":[{"name":"b"},{"name":"sub"},"d","s"]},"right":0}} reward=0.90 done=true error=null
|
| 135 |
+
[END] success=true steps=1 score=0.900 rewards=0.90
|
| 136 |
+
[START] task=medium env=constraint_env model=openai/gpt-oss-120b
|
| 137 |
+
[STEP] step=1 action={"type":"hard","name":"subject_frequency_match","forall":[{"sub":"subjects"}],"assert":{"operator":"==","left":{"operator":"sum","over":[{"b":"branches"},{"d":"days"},{"s":"slots"}],"expression":{"target":"schedule","args":[{"name":"b"},{"name":"sub"},"d","s"]}},"right":{"frequency":"sub"}}} reward=0.01 done=false error=bad_structure
|
| 138 |
+
[STEP] step=2 action={"type":"hard","name":"subject_frequency_match","forall":[{"b":"branches"},{"sub":{"subjects":"b"}},{"d":"days"},{"s":"slots"}],"assert":{"operator":"==","left":{"operator":"sum","over":[{"d":"days"},{"s":"slots"}],"expression":{"target":"schedule","args":[{"name":"b"},{"name":"sub"},"d","s"]}},"right":{"frequency":"sub"}}} reward=0.01 done=false error=bad_structure
|
| 139 |
+
[STEP] step=3 action={"type":"hard","name":"subject_frequency_match","forall":[{"sub":"subjects"}],"assert":{"operator":"==","left":{"operator":"sum","over":[{"b":"branches"},{"d":"days"},{"s":"slots"}],"expression":{"target":"schedule","args":[{"name":"b"},{"name":"sub"},"d","s"]}},"right":{"frequency":"sub"}}} reward=0.01 done=false error=bad_structure
|
| 140 |
+
[STEP] step=4 action={"type":"hard","name":"subject_frequency_match","forall":[{"b":"branches"},{"sub":{"subjects":"b"}},{"d":"days"},{"s":"slots"}],"assert":{"operator":"==","left":{"operator":"sum","over":[{"d":"days"},{"s":"slots"}],"expression":{"target":"schedule","args":[{"name":"b"},{"name":"sub"},"d","s"]}},"right":{"frequency":"sub"}}} reward=0.01 done=false error=bad_structure
|
| 141 |
+
[STEP] step=5 action={"type":"hard","name":"subject_frequency_match","forall":[{"b":"branches"},{"sub":{"subjects":"b"}}],"assert":{"operator":"==","left":{"operator":"sum","over":[{"d":"days"},{"s":"slots"}],"expression":{"target":"schedule","args":[{"name":"b"},{"name":"sub"},"d","s"]}},"right":{"frequency":"sub"}}} reward=0.80 done=true error=null
|
| 142 |
+
[END] success=true steps=5 score=0.800 rewards=0.01,0.01,0.01,0.01,0.80
|
| 143 |
+
[START] task=hard env=constraint_env model=openai/gpt-oss-120b
|
| 144 |
+
[STEP] step=1 action={"type":"hard","name":"no_classes_on_saturday","forall":[{"b":"branches"},{"sub":{"subjects":"b"}},{"d":"days"},{"s":"slots"}],"where":{"operator":"==","left":"d","right":5},"assert":{"operator":"==","left":{"target":"schedule","args":[{"name":"b"},{"name":"sub"},"d","s"]},"right":0}} reward=0.38 done=false error=logic_mismatch
|
| 145 |
+
[STEP] step=2 action={"type":"hard","name":"no_classes_on_saturday","forall":[{"b":"branches"},{"sub":{"subjects":"b"}},{"d":"days"},{"s":"slots"}],"where":{"operator":"==","left":"d","right":5},"assert":{"operator":"==","left":{"target":"schedule","args":[{"name":"b"},{"name":"sub"},"d","s"]},"right":0}} reward=0.33 done=false error=logic_mismatch
|
| 146 |
+
[STEP] step=3 action={"type":"hard","name":"no_classes_on_saturday","forall":[{"b":"branches"},{"sub":{"subjects":"b"}},{"d":"days"},{"s":"slots"}],"where":{"operator":"==","left":"d","right":5},"assert":{"operator":"==","left":{"target":"schedule","args":[{"name":"b"},{"name":"sub"},"d","s"]},"right":0}} reward=0.28 done=false error=logic_mismatch
|
| 147 |
+
[STEP] step=4 action={"type":"hard","name":"no_classes_on_saturday","forall":[{"b":"branches"},{"sub":{"subjects":"b"}},{"d":"days"},{"s":"slots"}],"where":{"operator":"==","left":"d","right":5},"assert":{"operator":"==","left":{"target":"schedule","args":[{"name":"b"},{"name":"sub"},"d","s"]},"right":0}} reward=0.23 done=false error=logic_mismatch
|
| 148 |
+
[STEP] step=5 action={"type":"hard","name":"no_classes_on_saturday","forall":[{"b":"branches"},{"sub":{"subjects":"b"}},{"d":"days"},{"s":"slots"}],"where":{"operator":"==","left":"d","right":5},"assert":{"operator":"==","left":{"target":"schedule","args":[{"name":"b"},{"name":"sub"},"d","s"]},"right":0}} reward=0.17 done=true error=logic_mismatch
|
| 149 |
+
[END] success=true steps=5 score=0.175 rewards=0.38,0.33,0.28,0.23,0.17
|
| 150 |
+
```
|
| 151 |
+
|
| 152 |
+
> **Research Warning - Failure on Hard:** The baseline LLM fundamentally struggles with synthesizing deep AST networks iteratively for harder targets. It eventually fell back to terminal truncation (step 5 failure, penalty accumulation down to 0.175). In the future, solving these deep constraint synthesises dynamically will require **advanced RL tuning methodologies like PPO or GRPO (Group Relative Policy Optimization)**.
|
| 153 |
+
|
| 154 |
+
### Submitting Models & Environment Checks
|
| 155 |
+
|
| 156 |
+
Your `inference.py` must run natively in the root directory and absorb the base OpenEnv formats.
|
| 157 |
|
| 158 |
+
Ensure these environment variables are set during your model tests:
|
| 159 |
+
- `API_BASE_URL` (default: `https://openrouter.ai/api/v1`)
|
| 160 |
+
- `MODEL_NAME` (default: `openai/gpt-oss-120b`)
|
| 161 |
+
- `HF_TOKEN` (Mandatory - execution fails without it, powering hosted inference queries).
|
| 162 |
+
|
| 163 |
+
**Inference Output Pattern Expectation:**
|
| 164 |
+
```
|
| 165 |
+
[START] task=<task_name> env=<benchmark> model=<model_name>
|
| 166 |
+
[STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>
|
| 167 |
+
[END] success=<true|false> steps=<n> rewards=<r1,r2,...,rn>
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
### Direct Environment Testing
|
| 171 |
Test the environment logic directly without starting the HTTP server:
|
| 172 |
|
| 173 |
```bash
|
|
|
|
| 175 |
python3 server/constraint_env_environment.py
|
| 176 |
```
|
| 177 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
### Running Locally
|
| 179 |
+
Run the server locally via the main standard startup:
|
|
|
|
| 180 |
|
| 181 |
```bash
|
| 182 |
uvicorn server.app:app --reload
|
| 183 |
```
|
| 184 |
+
or
|
| 185 |
+
```bash
|
| 186 |
+
$env:ENABLE_WEB_INTERFACE="true"; uv run server/app.py
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
**Known Good Execution Logs Example:**
|
| 190 |
+
```bash
|
| 191 |
+
===== Application Startup at 2026-04-12 16:53:42 =====
|
| 192 |
+
INFO: Started server process [7]
|
| 193 |
+
INFO: Waiting for application startup.
|
| 194 |
+
INFO: Application startup complete.
|
| 195 |
+
INFO: Uvicorn running on http://0.0.0.0:7860 (Press CTRL+C to quit)
|
| 196 |
+
INFO: 10.16.24.44:32462 - "POST /web/gradio_api/queue/join HTTP/1.1" 200 OK
|
| 197 |
+
INFO: 10.16.12.178:19351 - "GET /web HTTP/1.1" 307 Temporary Redirect
|
| 198 |
+
INFO: 10.16.33.124:5187 - "GET /web HTTP/1.1" 307 Temporary Redirect
|
| 199 |
+
INFO: 10.16.24.44:32462 - "GET /web/ HTTP/1.1" 200 OK
|
| 200 |
+
```
|
| 201 |
|
| 202 |
## Project Structure
|
| 203 |
|
|
|
|
| 210 |
βββ pyproject.toml # Project metadata and dependencies
|
| 211 |
βββ uv.lock # Locked dependencies (generated)
|
| 212 |
βββ client.py # ConstraintEnv client
|
| 213 |
+
βββ generator.py # Native Time Table generator for target compiling
|
| 214 |
βββ models.py # Action and Observation models
|
| 215 |
βββ server/
|
| 216 |
βββ __init__.py # Server module exports
|
| 217 |
+
βββ graders.py # Advanced threshold limits scaling for evaluations
|
| 218 |
βββ constraint_env_environment.py # Core environment logic
|
| 219 |
βββ app.py # FastAPI application (HTTP + WebSocket endpoints)
|
| 220 |
βββ Dockerfile # Container image definition
|
server/constraint_env_environment.py
CHANGED
|
@@ -141,6 +141,8 @@ class ConstraintEnvironment(Environment):
|
|
| 141 |
# ββ 1. Parse JSON ββββββββββββββββββββββββββββββββββββββββββββ
|
| 142 |
try:
|
| 143 |
ast = json.loads(action.ast_output)
|
|
|
|
|
|
|
| 144 |
is_valid_json = True
|
| 145 |
except (json.JSONDecodeError, TypeError):
|
| 146 |
info["error"] = "invalid_json"
|
|
|
|
| 141 |
# ββ 1. Parse JSON ββββββββββββββββββββββββββββββββββββββββββββ
|
| 142 |
try:
|
| 143 |
ast = json.loads(action.ast_output)
|
| 144 |
+
if isinstance(ast, str):
|
| 145 |
+
ast = json.loads(ast)
|
| 146 |
is_valid_json = True
|
| 147 |
except (json.JSONDecodeError, TypeError):
|
| 148 |
info["error"] = "invalid_json"
|
server/graders.py
CHANGED
|
@@ -50,9 +50,9 @@ def check_passed(difficulty: str, score: float) -> bool:
|
|
| 50 |
Temporary defaults to be calibrated dynamically.
|
| 51 |
"""
|
| 52 |
thresholds = {
|
| 53 |
-
"easy": 0.
|
| 54 |
-
"medium": 0.
|
| 55 |
-
"hard": 0.
|
| 56 |
}
|
| 57 |
|
| 58 |
req_threshold = thresholds.get(difficulty, 0.8)
|
|
|
|
| 50 |
Temporary defaults to be calibrated dynamically.
|
| 51 |
"""
|
| 52 |
thresholds = {
|
| 53 |
+
"easy": 0.80,
|
| 54 |
+
"medium": 0.65,
|
| 55 |
+
"hard": 0.45
|
| 56 |
}
|
| 57 |
|
| 58 |
req_threshold = thresholds.get(difficulty, 0.8)
|