DecentSanage commited on
Commit
ecb3d9d
Β·
verified Β·
1 Parent(s): f823a82

Upload folder using huggingface_hub

Browse files
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 tiem 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:
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
- {"var": "b", "domain": "branches"},
83
- {"var": "sub", "domain": "subjects"},
84
- {"var": "d", "domain": "days"},
85
- {"var": "s", "domain": "slots"},
86
  ],
87
- "where": "d == 5",
88
- "assert": "schedule(b, sub, d, s) == 0",
 
 
 
 
 
 
 
 
 
 
 
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
- ### Direct Environment Testing
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.15,
54
- "medium": 0.15,
55
- "hard": 0.15
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)