JayeshCC commited on
Commit
cc25e97
·
verified ·
1 Parent(s): 63927f8

Upload folder using huggingface_hub

Browse files
Files changed (5) hide show
  1. README.md +19 -6
  2. inference.py +86 -60
  3. models.py +1 -0
  4. openenv.yaml +3 -3
  5. server/token_optimiser_environment.py +177 -7
README.md CHANGED
@@ -6,6 +6,8 @@ colorTo: purple
6
  sdk: docker
7
  app_port: 8000
8
  pinned: false
 
 
9
  base_path: /web
10
  ---
11
 
@@ -192,14 +194,25 @@ uv run inference.py
192
  Expected output:
193
  ```
194
  [START] task=token_optimization env=token_optimiser model=Qwen/Qwen2.5-72B-Instruct
195
- [STEP] step=1 action='Analyze renewable energy trends...' reward=0.62 done=false error=null
196
- [STEP] step=2 action='Summarize 2013-2023 renewable energy...' reward=0.81 done=false error=null
197
- [STEP] step=3 action='Renewable energy trends 2013-2023...' reward=0.84 done=false error=null
198
- [STEP] step=4 action='Renewable energy 2013-2023 trends...' reward=0.82 done=false error=null
199
- [STEP] step=5 action='Energy trends 2013-2023: solar, wind...' reward=0.81 done=true error=null
200
- [END] success=true steps=5 score=0.780 rewards=0.62,0.81,0.84,0.82,0.81
201
  ```
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  ### Step 3 — (Optional) Run via Docker
204
 
205
  ```bash
 
6
  sdk: docker
7
  app_port: 8000
8
  pinned: false
9
+ tags:
10
+ - openenv
11
  base_path: /web
12
  ---
13
 
 
194
  Expected output:
195
  ```
196
  [START] task=token_optimization env=token_optimiser model=Qwen/Qwen2.5-72B-Instruct
197
+ [STEP] step=1 action='...' reward=0.71 done=false error=null
198
+ [STEP] step=4 action='...' reward=0.70 done=false error=null
199
+ [STEP] step=7 action='...' reward=0.17 done=false error=null
200
+ [END] success=false steps=9 score=0.487 rewards=0.71,0.66,0.60,0.70,0.68,0.64,0.17,0.14,0.10
 
 
201
  ```
202
 
203
+ ### 📊 Baseline Performance
204
+
205
+ Current reproducible local baseline, measured with `HF_TOKEN` unset and the deterministic fallback path against the three-task cycle:
206
+
207
+ | Task | Score |
208
+ |------|-------|
209
+ | Easy | 0.657 |
210
+ | Medium | 0.673 |
211
+ | Hard | 0.137 |
212
+ | Aggregate | 0.487 |
213
+
214
+ When `HF_TOKEN` is available, `inference.py` uses the OpenAI client against the Hugging Face router and can be rerun to regenerate model-backed scores.
215
+
216
  ### Step 3 — (Optional) Run via Docker
217
 
218
  ```bash
inference.py CHANGED
@@ -17,6 +17,7 @@ Environment variables required:
17
  import asyncio
18
  import logging
19
  import os
 
20
  import textwrap
21
  from typing import List, Optional
22
 
@@ -31,7 +32,7 @@ from token_optimiser import TokenOptimiserEnv, TokenOptimiserAction
31
  logger = logging.getLogger("TokenOptimiserFrontend")
32
  logger.setLevel(logging.INFO)
33
  if not logger.handlers:
34
- handler = logging.StreamHandler()
35
  formatter = logging.Formatter('\033[96m%(asctime)s\033[0m | \033[93m%(levelname)-7s\033[0m | \033[1mCLIENT\033[0m | %(message)s', datefmt='%H:%M:%S')
36
  handler.setFormatter(formatter)
37
  logger.addHandler(handler)
@@ -47,7 +48,8 @@ HF_TOKEN: Optional[str] = os.getenv("HF_TOKEN")
47
  TASK_NAME: str = "token_optimization"
48
  BENCHMARK: str = "token_optimiser"
49
  MAX_STEPS: int = 5
50
- TEMPERATURE: float = 0.3
 
51
  MAX_TOKENS: int = 200
52
  SUCCESS_THRESHOLD: float = 0.6
53
 
@@ -92,11 +94,11 @@ def log_start(task: str, env: str, model: str) -> None:
92
 
93
 
94
  def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
95
- # Truncate action for readability but keep it on one line
96
- action_short = action.replace("\n", " ")[:120]
97
  error_val = error if error else "null"
98
  print(
99
- f"[STEP] step={step} action={action_short!r} "
100
  f"reward={reward:.2f} done={str(done).lower()} error={error_val}",
101
  flush=True,
102
  )
@@ -105,8 +107,7 @@ def log_step(step: int, action: str, reward: float, done: bool, error: Optional[
105
  def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
106
  rewards_str = ",".join(f"{r:.2f}" for r in rewards)
107
  print(
108
- f"[END] success={str(success).lower()} steps={steps} "
109
- f"score={score:.3f} rewards={rewards_str}",
110
  flush=True,
111
  )
112
 
@@ -139,13 +140,16 @@ def _build_user_message(original_prompt: str, step: int,
139
 
140
 
141
  def get_optimized_prompt(
142
- llm: OpenAI,
143
  original_prompt: str,
144
  step: int,
145
  prev_reward: float,
146
  prev_response: str,
147
  history: List[str],
148
  ) -> str:
 
 
 
149
  user_msg = _build_user_message(original_prompt, step, prev_reward, prev_response, history)
150
  try:
151
  completion = llm.chat.completions.create(
@@ -196,11 +200,71 @@ def _rule_based_compress(original_prompt: str, step: int = 1) -> str:
196
  # Main episode loop
197
  # ---------------------------------------------------------------------------
198
 
199
- async def run_episode(llm: OpenAI) -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  rewards: List[float] = []
201
  steps_taken = 0
202
- score = 0.0
203
  success = False
 
204
 
205
  log_start(task=TASK_NAME, env=BENCHMARK, model=MODEL_NAME)
206
 
@@ -212,55 +276,16 @@ async def run_episode(llm: OpenAI) -> None:
212
  await env.connect()
213
 
214
  try:
215
- # Reset get initial observation
216
- reset_result = await env.reset()
217
-
218
- # Fetch original prompt from server state
219
- env_state = await env.state()
220
- original_prompt: str = env_state.original_prompt or "Explain machine learning briefly."
221
-
222
- logger.info(f"Task connected. Difficulty: {env_state.task_difficulty.upper()}")
223
- logger.info(f"Original prompt ({len(original_prompt.split())} words): {original_prompt[:80]}...")
224
-
225
- prev_reward = 0.0
226
- prev_response = ""
227
- history: List[str] = []
228
-
229
- for step in range(1, MAX_STEPS + 1):
230
- # Ask LLM to optimize the prompt
231
- optimized = get_optimized_prompt(
232
- llm, original_prompt, step, prev_reward, prev_response, history
233
  )
 
 
234
 
235
- # Step the environment with the optimized prompt
236
- error_msg: Optional[str] = None
237
- reward = 0.0
238
- done = False
239
- try:
240
- result = await env.step(TokenOptimiserAction(optimized_prompt=optimized))
241
- obs = result.observation
242
- reward = result.reward # server puts reward at top-level, not inside obs
243
- done = result.done or (step >= MAX_STEPS)
244
- prev_response = obs.llm_response
245
- logger.info(f"Step {step} Tokens => Input: {obs.input_tokens}, Output: {obs.output_tokens}")
246
- except Exception as exc:
247
- error_msg = str(exc)
248
- done = True
249
-
250
- rewards.append(reward)
251
- steps_taken = step
252
- prev_reward = reward
253
- history.append(f"step={step} prompt={optimized!r:.60} reward={reward:.2f}")
254
-
255
- log_step(step=step, action=optimized, reward=reward, done=done, error=error_msg)
256
-
257
- if done:
258
- break
259
-
260
- # Wait between steps to avoid rate limiting
261
- await asyncio.sleep(2.5)
262
-
263
- # Score = average reward across steps, clamped to [0, 1]
264
  score = sum(rewards) / len(rewards) if rewards else 0.0
265
  score = max(0.0, min(1.0, score))
266
  success = score >= SUCCESS_THRESHOLD
@@ -277,10 +302,11 @@ async def run_episode(llm: OpenAI) -> None:
277
 
278
  async def main() -> None:
279
  if not HF_TOKEN:
280
- logger.error("HF_TOKEN environment variable not set. Exiting.")
281
- return
 
 
282
 
283
- llm = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
284
  await run_episode(llm)
285
 
286
 
 
17
  import asyncio
18
  import logging
19
  import os
20
+ import sys
21
  import textwrap
22
  from typing import List, Optional
23
 
 
32
  logger = logging.getLogger("TokenOptimiserFrontend")
33
  logger.setLevel(logging.INFO)
34
  if not logger.handlers:
35
+ handler = logging.StreamHandler(stream=sys.stderr)
36
  formatter = logging.Formatter('\033[96m%(asctime)s\033[0m | \033[93m%(levelname)-7s\033[0m | \033[1mCLIENT\033[0m | %(message)s', datefmt='%H:%M:%S')
37
  handler.setFormatter(formatter)
38
  logger.addHandler(handler)
 
48
  TASK_NAME: str = "token_optimization"
49
  BENCHMARK: str = "token_optimiser"
50
  MAX_STEPS: int = 5
51
+ TASK_EVAL_ROUNDS: int = 3
52
+ TEMPERATURE: float = 0.0
53
  MAX_TOKENS: int = 200
54
  SUCCESS_THRESHOLD: float = 0.6
55
 
 
94
 
95
 
96
  def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
97
+ # Keep the action on one line, but do not change its content otherwise.
98
+ action_short = action.replace("\n", " ")
99
  error_val = error if error else "null"
100
  print(
101
+ f"[STEP] step={step} action={action_short} "
102
  f"reward={reward:.2f} done={str(done).lower()} error={error_val}",
103
  flush=True,
104
  )
 
107
  def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
108
  rewards_str = ",".join(f"{r:.2f}" for r in rewards)
109
  print(
110
+ f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}",
 
111
  flush=True,
112
  )
113
 
 
140
 
141
 
142
  def get_optimized_prompt(
143
+ llm: Optional[OpenAI],
144
  original_prompt: str,
145
  step: int,
146
  prev_reward: float,
147
  prev_response: str,
148
  history: List[str],
149
  ) -> str:
150
+ if llm is None:
151
+ return _rule_based_compress(original_prompt, step)
152
+
153
  user_msg = _build_user_message(original_prompt, step, prev_reward, prev_response, history)
154
  try:
155
  completion = llm.chat.completions.create(
 
200
  # Main episode loop
201
  # ---------------------------------------------------------------------------
202
 
203
+ async def _run_single_task_episode(llm: Optional[OpenAI], env: TokenOptimiserEnv, step_offset: int) -> tuple[List[float], int]:
204
+ rewards: List[float] = []
205
+ steps_taken = 0
206
+
207
+ # Reset — get initial observation
208
+ reset_result = await env.reset()
209
+
210
+ # Fetch original prompt from server state
211
+ env_state = await env.state()
212
+ original_prompt: str = env_state.original_prompt or "Explain machine learning briefly."
213
+ task_label = getattr(env_state, "task_difficulty", "unknown")
214
+
215
+ logger.info(f"Task connected. Difficulty: {task_label.upper()}")
216
+ logger.info(f"Original prompt ({len(original_prompt.split())} words): {original_prompt[:80]}...")
217
+
218
+ prev_reward = 0.0
219
+ prev_response = ""
220
+ history: List[str] = []
221
+
222
+ for local_step in range(1, MAX_STEPS + 1):
223
+ global_step = step_offset + local_step
224
+
225
+ # Ask LLM to optimize the prompt
226
+ optimized = get_optimized_prompt(
227
+ llm, original_prompt, local_step, prev_reward, prev_response, history
228
+ )
229
+
230
+ # Step the environment with the optimized prompt
231
+ error_msg: Optional[str] = None
232
+ reward = 0.0
233
+ done = False
234
+ try:
235
+ result = await env.step(TokenOptimiserAction(optimized_prompt=optimized))
236
+ obs = result.observation
237
+ reward = result.reward # server puts reward at top-level, not inside obs
238
+ done = result.done or (local_step >= MAX_STEPS)
239
+ prev_response = obs.llm_response
240
+ logger.info(f"Step {global_step} Tokens => Input: {obs.input_tokens}, Output: {obs.output_tokens}")
241
+ if getattr(obs, "done_reason", ""):
242
+ logger.info(f"Step {global_step} DoneReason => {obs.done_reason}")
243
+ except Exception as exc:
244
+ error_msg = str(exc)
245
+ done = True
246
+
247
+ rewards.append(reward)
248
+ steps_taken = local_step
249
+ prev_reward = reward
250
+ history.append(f"step={global_step} prompt={optimized!r:.60} reward={reward:.2f}")
251
+
252
+ log_step(step=global_step, action=optimized, reward=reward, done=done, error=error_msg)
253
+
254
+ if done:
255
+ break
256
+
257
+ # Wait between steps to avoid rate limiting
258
+ await asyncio.sleep(2.5)
259
+
260
+ return rewards, steps_taken
261
+
262
+
263
+ async def run_episode(llm: Optional[OpenAI]) -> None:
264
  rewards: List[float] = []
265
  steps_taken = 0
 
266
  success = False
267
+ score = 0.0
268
 
269
  log_start(task=TASK_NAME, env=BENCHMARK, model=MODEL_NAME)
270
 
 
276
  await env.connect()
277
 
278
  try:
279
+ for episode_index in range(TASK_EVAL_ROUNDS):
280
+ episode_rewards, episode_steps = await _run_single_task_episode(
281
+ llm=llm,
282
+ env=env,
283
+ step_offset=steps_taken,
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  )
285
+ rewards.extend(episode_rewards)
286
+ steps_taken += episode_steps
287
 
288
+ # Success is based on average reward across steps, clamped to [0, 1].
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  score = sum(rewards) / len(rewards) if rewards else 0.0
290
  score = max(0.0, min(1.0, score))
291
  success = score >= SUCCESS_THRESHOLD
 
302
 
303
  async def main() -> None:
304
  if not HF_TOKEN:
305
+ logger.warning("HF_TOKEN environment variable not set. Using deterministic fallback prompts.")
306
+ llm: Optional[OpenAI] = None
307
+ else:
308
+ llm = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
309
 
 
310
  await run_episode(llm)
311
 
312
 
models.py CHANGED
@@ -25,6 +25,7 @@ class TokenOptimiserObservation(Observation):
25
  input_tokens: int = Field(default=0, description="Number of tokens in the optimized prompt")
26
  output_tokens: int = Field(default=0, description="Number of tokens in the LLM response")
27
  reward: float = Field(default=0.0, description="Reward score for this step (0.0-1.0)")
 
28
 
29
 
30
  class TokenOptimiserState(State):
 
25
  input_tokens: int = Field(default=0, description="Number of tokens in the optimized prompt")
26
  output_tokens: int = Field(default=0, description="Number of tokens in the LLM response")
27
  reward: float = Field(default=0.0, description="Reward score for this step (0.0-1.0)")
28
+ done_reason: str = Field(default="", description="Why the episode terminated (if done=true)")
29
 
30
 
31
  class TokenOptimiserState(State):
openenv.yaml CHANGED
@@ -9,14 +9,14 @@ tasks:
9
  name: "Redundancy Stripping"
10
  description: "Strip politeness filler and redundancy"
11
  difficulty: "easy"
12
- grader: "server.token_optimiser_environment:grade"
13
  - id: "constraint_injection"
14
  name: "Constraint Injection"
15
  description: "Compress input AND inject exact bullet point constraints"
16
  difficulty: "medium"
17
- grader: "server.token_optimiser_environment:grade"
18
  - id: "multi_key_json_extraction"
19
  name: "Multi-Key JSON Extraction"
20
  description: "Force structured JSON output with 5 exact keys"
21
  difficulty: "hard"
22
- grader: "server.token_optimiser_environment:grade"
 
9
  name: "Redundancy Stripping"
10
  description: "Strip politeness filler and redundancy"
11
  difficulty: "easy"
12
+ grader: "server.token_optimiser_environment:grade_redundancy_stripping"
13
  - id: "constraint_injection"
14
  name: "Constraint Injection"
15
  description: "Compress input AND inject exact bullet point constraints"
16
  difficulty: "medium"
17
+ grader: "server.token_optimiser_environment:grade_constraint_injection"
18
  - id: "multi_key_json_extraction"
19
  name: "Multi-Key JSON Extraction"
20
  description: "Force structured JSON output with 5 exact keys"
21
  difficulty: "hard"
22
+ grader: "server.token_optimiser_environment:grade_multi_key_json_extraction"
server/token_optimiser_environment.py CHANGED
@@ -14,7 +14,6 @@ and expected output responses to minimize total token usage while maintaining co
14
  import json
15
  import logging
16
  import os
17
- import random
18
  import re
19
  from uuid import uuid4
20
 
@@ -58,6 +57,9 @@ class TokenOptimiserEnvironment(Environment):
58
  self._task_bank = self._load_task_bank()
59
  self._current_task = None
60
  self._reset_count = 0
 
 
 
61
 
62
  # Hybrid LLM client — reads credentials from env vars at startup
63
  api_key = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
@@ -65,8 +67,10 @@ class TokenOptimiserEnvironment(Environment):
65
  self._model = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-7B-Instruct")
66
  if OpenAI and api_key:
67
  self._llm = OpenAI(base_url=api_base, api_key=api_key)
 
68
  else:
69
  self._llm = None
 
70
 
71
  def _load_task_bank(self):
72
  """Load the bank of prompt optimization tasks."""
@@ -108,8 +112,9 @@ class TokenOptimiserEnvironment(Environment):
108
  Returns:
109
  TokenOptimiserObservation with initial state
110
  """
111
- # Select a random task
112
- self._current_task = random.choice(self._task_bank)
 
113
  self._state = TokenOptimiserState(
114
  episode_id=str(uuid4()),
115
  step_count=0,
@@ -118,6 +123,9 @@ class TokenOptimiserEnvironment(Environment):
118
  task_index=self._task_bank.index(self._current_task)
119
  )
120
  self._reset_count += 1
 
 
 
121
 
122
  logger.info(f"------ ENVIRONMENT RESET ------")
123
  logger.info(f"Loaded Task: [{self._current_task['difficulty'].upper()}] Index: {self._state.task_index}")
@@ -128,7 +136,9 @@ class TokenOptimiserEnvironment(Environment):
128
  llm_response="",
129
  input_tokens=0,
130
  output_tokens=0,
131
- reward=0.0
 
 
132
  )
133
 
134
  def step(self, action: TokenOptimiserAction) -> TokenOptimiserObservation: # type: ignore[override]
@@ -143,6 +153,8 @@ class TokenOptimiserEnvironment(Environment):
143
  """
144
  self._state.step_count += 1
145
  optimized_prompt = action.optimized_prompt
 
 
146
  original_prompt = self._current_task["prompt"]
147
 
148
  # 1. Call real LLM (or fallback) to get the actual response + token counts
@@ -191,20 +203,53 @@ class TokenOptimiserEnvironment(Environment):
191
  + format_score # 0.0 - 0.2 (format compliance)
192
  + length_penalty # 0.0 or -0.1 (penalty)
193
  )
 
 
 
 
194
  reward = max(0.0, min(1.0, reward))
195
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  logger.info(f"[STEP {self._state.step_count}] Optimized Prompt Length: {len(optimized_prompt.split())} words")
197
  logger.info(f" └─ Tokens => In: {int(input_tokens)}, Out: {int(output_tokens)}")
198
  logger.info(
199
  f" └─ Reward => Tok_Eff:{token_efficiency:.2f} | Semantic:{semantic_score*0.3:.2f} | "
200
  f"Fmt:{format_score:.2f} | Penalty:{length_penalty:.2f} || TOTAL: {reward:.3f}"
201
  )
 
 
 
 
202
 
203
  return TokenOptimiserObservation(
204
  llm_response=llm_response,
205
  input_tokens=int(input_tokens),
206
  output_tokens=int(output_tokens),
207
- reward=reward
 
 
208
  )
209
 
210
  def _call_llm(self, prompt: str) -> tuple[str, int, int]:
@@ -272,6 +317,7 @@ class TokenOptimiserEnvironment(Environment):
272
  Returns a float 0.0-1.0.
273
  """
274
  if self._llm is None:
 
275
  return self._keyword_fallback_score(original_prompt, response)
276
 
277
  judge_prompt = (
@@ -289,7 +335,8 @@ class TokenOptimiserEnvironment(Environment):
289
  raw = (resp.choices[0].message.content or "5").strip()
290
  score = int("".join(c for c in raw if c.isdigit())[:2] or "5")
291
  return min(max(score / 10.0, 0.0), 1.0)
292
- except Exception:
 
293
  return self._keyword_fallback_score(original_prompt, response)
294
 
295
  def _keyword_fallback_score(self, original_prompt: str, response: str) -> float:
@@ -298,9 +345,12 @@ class TokenOptimiserEnvironment(Environment):
298
  "machine", "learning", "ai", "data", "predict", "solar", "wind",
299
  "energy", "renewable", "sales", "customer", "product", "market",
300
  "budget", "analysis", "trend", "growth", "json", "bullet",
 
301
  }
302
  orig = set(original_prompt.lower().split()) & key_concepts
303
  resp = set(response.lower().split()) & key_concepts
 
 
304
  raw = (len(resp) / len(orig)) if orig else 0.5
305
  return min(raw, 1.0)
306
 
@@ -407,4 +457,124 @@ def grade(*args, **kwargs) -> float:
407
  Entry point for OpenEnv offline task validation.
408
  Returns a unified float. Core RL grading is dynamically calculated in TokenOptimiserEnvironment.step().
409
  """
410
- return 1.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  import json
15
  import logging
16
  import os
 
17
  import re
18
  from uuid import uuid4
19
 
 
57
  self._task_bank = self._load_task_bank()
58
  self._current_task = None
59
  self._reset_count = 0
60
+ self._last_prompt_norm = ""
61
+ self._best_reward = 0.0
62
+ self._stagnation_steps = 0
63
 
64
  # Hybrid LLM client — reads credentials from env vars at startup
65
  api_key = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
 
67
  self._model = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-7B-Instruct")
68
  if OpenAI and api_key:
69
  self._llm = OpenAI(base_url=api_base, api_key=api_key)
70
+ logger.info(f"LLM backend enabled (model={self._model})")
71
  else:
72
  self._llm = None
73
+ logger.warning("LLM backend unavailable; using deterministic fallback simulation and keyword judge.")
74
 
75
  def _load_task_bank(self):
76
  """Load the bank of prompt optimization tasks."""
 
112
  Returns:
113
  TokenOptimiserObservation with initial state
114
  """
115
+ # Cycle tasks in a fixed order so baseline runs are reproducible.
116
+ task_index = self._reset_count % len(self._task_bank)
117
+ self._current_task = self._task_bank[task_index]
118
  self._state = TokenOptimiserState(
119
  episode_id=str(uuid4()),
120
  step_count=0,
 
123
  task_index=self._task_bank.index(self._current_task)
124
  )
125
  self._reset_count += 1
126
+ self._last_prompt_norm = ""
127
+ self._best_reward = 0.0
128
+ self._stagnation_steps = 0
129
 
130
  logger.info(f"------ ENVIRONMENT RESET ------")
131
  logger.info(f"Loaded Task: [{self._current_task['difficulty'].upper()}] Index: {self._state.task_index}")
 
136
  llm_response="",
137
  input_tokens=0,
138
  output_tokens=0,
139
+ reward=0.0,
140
+ done=False,
141
+ done_reason=""
142
  )
143
 
144
  def step(self, action: TokenOptimiserAction) -> TokenOptimiserObservation: # type: ignore[override]
 
153
  """
154
  self._state.step_count += 1
155
  optimized_prompt = action.optimized_prompt
156
+ prompt_norm = " ".join(optimized_prompt.strip().lower().split())
157
+ prompt_changed = bool(prompt_norm) and prompt_norm != self._last_prompt_norm
158
  original_prompt = self._current_task["prompt"]
159
 
160
  # 1. Call real LLM (or fallback) to get the actual response + token counts
 
203
  + format_score # 0.0 - 0.2 (format compliance)
204
  + length_penalty # 0.0 or -0.1 (penalty)
205
  )
206
+ # Penalize no-op actions so each step requires substantive work.
207
+ if self._state.step_count > 1 and not prompt_changed:
208
+ reward -= 0.10
209
+
210
  reward = max(0.0, min(1.0, reward))
211
 
212
+ # Done logic: terminate on strong convergence or repeated non-improving/no-op steps.
213
+ done = False
214
+ done_reason = ""
215
+
216
+ improved = reward > (self._best_reward + 0.01)
217
+ if improved:
218
+ self._best_reward = reward
219
+ self._stagnation_steps = 0
220
+ else:
221
+ self._stagnation_steps += 1
222
+
223
+ if reward >= 0.90:
224
+ done = True
225
+ done_reason = "converged_high_reward"
226
+ elif self._state.step_count >= 2 and self._stagnation_steps >= 2:
227
+ done = True
228
+ done_reason = "stagnated_no_improvement"
229
+ elif self._state.step_count >= 2 and not prompt_changed:
230
+ done = True
231
+ done_reason = "no_substantive_action_change"
232
+
233
+ self._last_prompt_norm = prompt_norm
234
+
235
  logger.info(f"[STEP {self._state.step_count}] Optimized Prompt Length: {len(optimized_prompt.split())} words")
236
  logger.info(f" └─ Tokens => In: {int(input_tokens)}, Out: {int(output_tokens)}")
237
  logger.info(
238
  f" └─ Reward => Tok_Eff:{token_efficiency:.2f} | Semantic:{semantic_score*0.3:.2f} | "
239
  f"Fmt:{format_score:.2f} | Penalty:{length_penalty:.2f} || TOTAL: {reward:.3f}"
240
  )
241
+ logger.info(
242
+ f" └─ Progress => PromptChanged:{str(prompt_changed).lower()} | "
243
+ f"Stagnation:{self._stagnation_steps} | Done:{str(done).lower()} | Reason:{done_reason or 'null'}"
244
+ )
245
 
246
  return TokenOptimiserObservation(
247
  llm_response=llm_response,
248
  input_tokens=int(input_tokens),
249
  output_tokens=int(output_tokens),
250
+ reward=reward,
251
+ done=done,
252
+ done_reason=done_reason
253
  )
254
 
255
  def _call_llm(self, prompt: str) -> tuple[str, int, int]:
 
317
  Returns a float 0.0-1.0.
318
  """
319
  if self._llm is None:
320
+ logger.warning("Semantic judge fallback: no LLM client available.")
321
  return self._keyword_fallback_score(original_prompt, response)
322
 
323
  judge_prompt = (
 
335
  raw = (resp.choices[0].message.content or "5").strip()
336
  score = int("".join(c for c in raw if c.isdigit())[:2] or "5")
337
  return min(max(score / 10.0, 0.0), 1.0)
338
+ except Exception as exc:
339
+ logger.warning(f"Semantic judge fallback: judge call failed ({exc}).")
340
  return self._keyword_fallback_score(original_prompt, response)
341
 
342
  def _keyword_fallback_score(self, original_prompt: str, response: str) -> float:
 
345
  "machine", "learning", "ai", "data", "predict", "solar", "wind",
346
  "energy", "renewable", "sales", "customer", "product", "market",
347
  "budget", "analysis", "trend", "growth", "json", "bullet",
348
+ "python", "javascript", "typing", "performance", "syntax", "ecosystem",
349
  }
350
  orig = set(original_prompt.lower().split()) & key_concepts
351
  resp = set(response.lower().split()) & key_concepts
352
+ if not orig:
353
+ logger.warning("Keyword fallback is using neutral score because no tracked concepts were found in original prompt.")
354
  raw = (len(resp) / len(orig)) if orig else 0.5
355
  return min(raw, 1.0)
356
 
 
457
  Entry point for OpenEnv offline task validation.
458
  Returns a unified float. Core RL grading is dynamically calculated in TokenOptimiserEnvironment.step().
459
  """
460
+ return _grade_redundancy_stripping(*args, **kwargs)
461
+
462
+
463
+ def grade_redundancy_stripping(*args, **kwargs) -> float:
464
+ """Task-specific grader for redundancy_stripping."""
465
+ return _grade_redundancy_stripping(*args, **kwargs)
466
+
467
+
468
+ def grade_constraint_injection(*args, **kwargs) -> float:
469
+ """Task-specific grader for constraint_injection."""
470
+ return _grade_constraint_injection(*args, **kwargs)
471
+
472
+
473
+ def grade_multi_key_json_extraction(*args, **kwargs) -> float:
474
+ """Task-specific grader for multi_key_json_extraction."""
475
+ return _grade_multi_key_json_extraction(*args, **kwargs)
476
+
477
+
478
+ def _extract_action_observation(*args, **kwargs) -> tuple[str, str, float, str]:
479
+ """
480
+ Extract optimized prompt, llm response, and base reward from flexible grader args.
481
+
482
+ Supports object-style and dict-style inputs since validation harnesses can vary.
483
+ """
484
+ action = kwargs.get("action")
485
+ observation = kwargs.get("observation") or kwargs.get("obs")
486
+
487
+ if action is None and len(args) >= 1:
488
+ action = args[0]
489
+ if observation is None and len(args) >= 2:
490
+ observation = args[1]
491
+
492
+ optimized_prompt = ""
493
+ llm_response = ""
494
+ reward = 0.0
495
+ done_reason = ""
496
+
497
+ if isinstance(action, dict):
498
+ optimized_prompt = str(action.get("optimized_prompt", ""))
499
+ elif action is not None:
500
+ optimized_prompt = str(getattr(action, "optimized_prompt", "") or "")
501
+
502
+ if isinstance(observation, dict):
503
+ llm_response = str(observation.get("llm_response", ""))
504
+ reward = float(observation.get("reward", 0.0) or 0.0)
505
+ done_reason = str(observation.get("done_reason", "") or "")
506
+ elif observation is not None:
507
+ llm_response = str(getattr(observation, "llm_response", "") or "")
508
+ reward = float(getattr(observation, "reward", 0.0) or 0.0)
509
+ done_reason = str(getattr(observation, "done_reason", "") or "")
510
+
511
+ return optimized_prompt, llm_response, max(0.0, min(1.0, reward)), done_reason
512
+
513
+
514
+ def _grade_redundancy_stripping(*args, **kwargs) -> float:
515
+ optimized_prompt, llm_response, base_reward, done_reason = _extract_action_observation(*args, **kwargs)
516
+
517
+ # Reward concise rewrites that still produce plain, brief answers.
518
+ prompt_tokens = len(optimized_prompt.split()) if optimized_prompt else 0
519
+ concision_bonus = 0.2 if 1 <= prompt_tokens <= 30 else 0.0
520
+
521
+ sentences = len([s for s in re.split(r"[.!?]+", llm_response) if s.strip()])
522
+ plain_text_bonus = 0.2 if sentences <= 2 and not any(c in llm_response for c in ("•", "-", "*")) else 0.0
523
+
524
+ noop_penalty = 0.15 if done_reason in ("no_substantive_action_change", "stagnated_no_improvement") else 0.0
525
+
526
+ if optimized_prompt or llm_response:
527
+ return max(0.0, min(1.0, base_reward * 0.6 + concision_bonus + plain_text_bonus - noop_penalty))
528
+ return base_reward
529
+
530
+
531
+ def _grade_constraint_injection(*args, **kwargs) -> float:
532
+ optimized_prompt, llm_response, base_reward, done_reason = _extract_action_observation(*args, **kwargs)
533
+
534
+ # Enforce the medium-task structure: exactly 5 bullet points.
535
+ bullet_count = llm_response.count("•")
536
+ if bullet_count == 0:
537
+ # Fallback for hyphen or asterisk bullets
538
+ lines = [line.strip() for line in llm_response.splitlines() if line.strip()]
539
+ bullet_count = sum(1 for line in lines if line.startswith("-") or line.startswith("*"))
540
+
541
+ format_bonus = 0.25 if bullet_count == 5 else (0.1 if 3 <= bullet_count <= 4 else 0.0)
542
+ brevity_bonus = 0.15 if len(optimized_prompt.split()) <= 45 and optimized_prompt else 0.0
543
+
544
+ noop_penalty = 0.15 if done_reason in ("no_substantive_action_change", "stagnated_no_improvement") else 0.0
545
+
546
+ if optimized_prompt or llm_response:
547
+ return max(0.0, min(1.0, base_reward * 0.6 + format_bonus + brevity_bonus - noop_penalty))
548
+ return base_reward
549
+
550
+
551
+ def _grade_multi_key_json_extraction(*args, **kwargs) -> float:
552
+ optimized_prompt, llm_response, base_reward, done_reason = _extract_action_observation(*args, **kwargs)
553
+
554
+ required_keys = {
555
+ "top_categories",
556
+ "growth_regions",
557
+ "responsive_segments",
558
+ "budget_allocation",
559
+ "risks_watch",
560
+ }
561
+
562
+ key_bonus = 0.0
563
+ parse_bonus = 0.0
564
+ if llm_response:
565
+ try:
566
+ parsed = json.loads(llm_response.strip())
567
+ if isinstance(parsed, dict):
568
+ present = len(required_keys & set(parsed.keys()))
569
+ key_bonus = 0.3 * (present / len(required_keys))
570
+ parse_bonus = 0.15
571
+ except Exception:
572
+ key_bonus = 0.0
573
+
574
+ compression_bonus = 0.15 if len(optimized_prompt.split()) <= 65 and optimized_prompt else 0.0
575
+
576
+ noop_penalty = 0.15 if done_reason in ("no_substantive_action_change", "stagnated_no_improvement") else 0.0
577
+
578
+ if optimized_prompt or llm_response:
579
+ return max(0.0, min(1.0, base_reward * 0.4 + parse_bonus + key_bonus + compression_bonus - noop_penalty))
580
+ return base_reward