HNS8273 commited on
Commit
a2fe35b
·
1 Parent(s): b2e9980

next update

Browse files
agent/llm_agent.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import textwrap
3
+ from openai import OpenAI
4
+ from typing import Dict, Any
5
+
6
+ SYSTEM_PROMPT_NORMAL = textwrap.dedent(
7
+ """
8
+ You are an expert workforce scheduling agent.
9
+ Your job: assign employees to shifts optimally.
10
+
11
+ RULES (must follow):
12
+ - Employee skills must include the shift's required_skill
13
+ - Employee must be available on the shift's day (availability[day] == 1)
14
+ - Employee cannot exceed max_hours_per_week
15
+ - No two shifts for the same employee on the same (day, period)
16
+
17
+ Respond with ONLY a valid JSON action object — no explanation, no markdown.
18
+ Valid action formats:
19
+ {"action_type": "assign", "employee_id": "emp001", "shift_id": "shf042"}
20
+ {"action_type": "noop"}
21
+ """
22
+ ).strip()
23
+
24
+ SYSTEM_PROMPT_RECOVERY = textwrap.dedent(
25
+ """
26
+ You are an expert workforce scheduling agent.
27
+ The previous approach failed. Try a completely different strategy to assign employees.
28
+
29
+ RULES (must follow):
30
+ - Employee skills must include the shift's required_skill
31
+ - Employee must be available on the shift's day (availability[day] == 1)
32
+ - Employee cannot exceed max_hours_per_week
33
+ - No two shifts for the same employee on the same (day, period)
34
+
35
+ Respond with ONLY a valid JSON action object — no explanation, no markdown.
36
+ Valid action formats:
37
+ {"action_type": "assign", "employee_id": "emp001", "shift_id": "shf042"}
38
+ {"action_type": "noop"}
39
+ """
40
+ ).strip()
41
+
42
+ class LLMAgent:
43
+ def __init__(self, client: OpenAI, model_name: str):
44
+ self.client = client
45
+ self.model_name = model_name
46
+ self.temperature = 0.0
47
+ self.max_tokens = 150
48
+ self.actions = []
49
+ self.rewards = []
50
+ self.errors = []
51
+
52
+ def reset(self):
53
+ self.actions = []
54
+ self.rewards = []
55
+ self.errors = []
56
+
57
+ def _sanitize_action(self, text: str) -> dict:
58
+ text = text.replace("```json", "").replace("```", "").strip()
59
+ return json.loads(text)
60
+
61
+ def generate_action(self, obs_dict: Dict[str, Any], last_reward: float = None, last_error: str = None) -> dict:
62
+ if last_reward is not None:
63
+ self.rewards.append(last_reward)
64
+ if last_error:
65
+ self.errors.append(last_error)
66
+
67
+ slim = {
68
+ "unassigned_shifts": obs_dict["unassigned_shifts"][:8],
69
+ "employees": [
70
+ {k: e[k] for k in
71
+ ("id", "name", "skills", "availability", "assigned_hours", "max_hours_per_week", "preferred_shift")}
72
+ for e in obs_dict["employees"]
73
+ ],
74
+ "shifts": [
75
+ {k: s[k] for k in ("id", "day", "period", "required_skill", "duration_hours")}
76
+ for s in obs_dict["shifts"]
77
+ if s["id"] in obs_dict["unassigned_shifts"][:8]
78
+ ],
79
+ }
80
+
81
+ # Check recovery state
82
+ prompt = SYSTEM_PROMPT_NORMAL
83
+ if (len(self.rewards) >= 2 and self.rewards[-1] == 0.00 and self.rewards[-2] == 0.00) or (self.errors and self.errors[-1] is not None and self.errors[-1] != "null"):
84
+ prompt = SYSTEM_PROMPT_RECOVERY
85
+
86
+ user_content = json.dumps({
87
+ "TASK": "Assign next employee to shift optimally.",
88
+ "CURRENT STATE": slim,
89
+ "PREVIOUS ACTIONS": self.actions[-3:],
90
+ "LAST REWARD": last_reward,
91
+ "LAST ERROR": last_error
92
+ })
93
+
94
+ messages = [
95
+ {"role": "system", "content": prompt},
96
+ {"role": "user", "content": user_content},
97
+ ]
98
+
99
+ attempt_text = ""
100
+ action_dict = {"action_type": "noop"}
101
+ try:
102
+ completion = self.client.chat.completions.create(
103
+ model=self.model_name,
104
+ messages=messages,
105
+ temperature=self.temperature,
106
+ max_tokens=self.max_tokens,
107
+ stream=False,
108
+ )
109
+ attempt_text = completion.choices[0].message.content or ""
110
+ action_dict = self._sanitize_action(attempt_text)
111
+
112
+ # Anti-repetition logic
113
+ if action_dict in self.actions[-3:]:
114
+ # Retry once
115
+ messages.append({"role": "assistant", "content": attempt_text})
116
+ messages.append({"role": "user", "content": "Do NOT repeat previous actions. Try a different strategy."})
117
+
118
+ completion_retry = self.client.chat.completions.create(
119
+ model=self.model_name,
120
+ messages=messages,
121
+ temperature=self.temperature,
122
+ max_tokens=self.max_tokens,
123
+ stream=False,
124
+ )
125
+ attempt_text = completion_retry.choices[0].message.content or ""
126
+ action_dict = self._sanitize_action(attempt_text)
127
+
128
+ except Exception:
129
+ # Empty LLM response or invalid action -> fallback safe action
130
+ action_dict = {"action_type": "noop"}
131
+
132
+ self.actions.append(action_dict)
133
+ # Keep memory size bounded to last 5
134
+ if len(self.actions) > 5:
135
+ self.actions.pop(0)
136
+ if len(self.rewards) > 5:
137
+ self.rewards.pop(0)
138
+ if len(self.errors) > 5:
139
+ self.errors.pop(0)
140
+
141
+ return action_dict
inference.py CHANGED
@@ -17,6 +17,7 @@ from openai import OpenAI
17
 
18
  from server.engine import FlexTimeEnv, TASK_CONFIGS
19
  from server.models import Action
 
20
 
21
  # Mandatory environment variables with defaults
22
  API_BASE_URL = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
@@ -28,26 +29,6 @@ MAX_STEPS = 120
28
  TEMPERATURE = 0.0
29
  MAX_TOKENS = 120
30
 
31
- SYSTEM_PROMPT = textwrap.dedent(
32
- """
33
- You are an expert workforce scheduling agent.
34
- Your job: assign employees to shifts optimally.
35
-
36
- RULES (must follow):
37
- - Employee skills must include the shift's required_skill
38
- - Employee must be available on the shift's day (availability[day] == 1)
39
- - Employee cannot exceed max_hours_per_week
40
- - No two shifts for the same employee on the same (day, period)
41
-
42
- You receive the current schedule state as JSON.
43
- Respond with ONLY a valid JSON action object — no explanation, no markdown.
44
-
45
- Valid action formats:
46
- {"action_type": "assign", "employee_id": "emp001", "shift_id": "shf042"}
47
- {"action_type": "noop"}
48
- """
49
- ).strip()
50
-
51
 
52
  def log_start(task: str, env: str, model: str) -> None:
53
  print(f"[START] task={task} env={env} model={model}", flush=True)
@@ -70,43 +51,7 @@ def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> No
70
  )
71
 
72
 
73
- def get_model_action(client: OpenAI, obs_dict: dict) -> dict:
74
- # Trim Observation to ensure it fits context and removes noisy metrics
75
- slim = {
76
- "unassigned_shifts": obs_dict["unassigned_shifts"][:8],
77
- "employees": [
78
- {k: e[k] for k in
79
- ("id", "name", "skills", "availability", "assigned_hours", "max_hours_per_week", "preferred_shift")}
80
- for e in obs_dict["employees"]
81
- ],
82
- "shifts": [
83
- {k: s[k] for k in ("id", "day", "period", "required_skill", "duration_hours")}
84
- for s in obs_dict["shifts"]
85
- if s["id"] in obs_dict["unassigned_shifts"][:8]
86
- ],
87
- }
88
-
89
- try:
90
- completion = client.chat.completions.create(
91
- model=MODEL_NAME,
92
- messages=[
93
- {"role": "system", "content": SYSTEM_PROMPT},
94
- {"role": "user", "content": json.dumps(slim)},
95
- ],
96
- temperature=TEMPERATURE,
97
- max_tokens=MAX_TOKENS,
98
- stream=False,
99
- )
100
- text = (completion.choices[0].message.content or "").strip()
101
- text = text.replace("```json", "").replace("```", "").strip()
102
- return json.loads(text)
103
- except Exception as exc:
104
- # Emit an error logically, but fallback to noop to preserve the run bounds instead of crashing
105
- print(f"[DEBUG] Model request failed: {exc}", flush=True)
106
- # Note: If no token is provided, this handles graceful skip
107
- return {"action_type": "noop"}
108
-
109
- def run_task(client: OpenAI, env: FlexTimeEnv, task_id: str):
110
  log_start(task=task_id, env=BENCHMARK, model=MODEL_NAME)
111
 
112
  rewards: List[float] = []
@@ -124,12 +69,23 @@ def run_task(client: OpenAI, env: FlexTimeEnv, task_id: str):
124
  cur_max_steps = min(MAX_STEPS, cfg["max_steps"])
125
  target_score = cfg["target_score"]
126
 
 
 
 
 
 
127
  for step in range(1, cur_max_steps + 1):
128
  if done:
129
  break
130
-
 
 
 
 
 
 
131
  # Predict
132
- action_dict = get_model_action(client, obs_dict)
133
  action_str = json.dumps(action_dict).replace(' ', '')
134
 
135
  # Execute
@@ -149,6 +105,9 @@ def run_task(client: OpenAI, env: FlexTimeEnv, task_id: str):
149
  rewards.append(reward)
150
  steps_taken = step
151
 
 
 
 
152
  log_step(step=step, action=action_str, reward=reward, done=done, error=error)
153
 
154
  # Grading
@@ -164,12 +123,17 @@ def run_task(client: OpenAI, env: FlexTimeEnv, task_id: str):
164
 
165
 
166
  def main():
167
- client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
 
 
 
 
168
  env = FlexTimeEnv()
 
169
 
170
  tasks = ["task_easy", "task_medium", "task_hard"]
171
  for t_id in tasks:
172
- run_task(client, env, t_id)
173
 
174
 
175
  if __name__ == "__main__":
 
17
 
18
  from server.engine import FlexTimeEnv, TASK_CONFIGS
19
  from server.models import Action
20
+ from agent.llm_agent import LLMAgent
21
 
22
  # Mandatory environment variables with defaults
23
  API_BASE_URL = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
 
29
  TEMPERATURE = 0.0
30
  MAX_TOKENS = 120
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  def log_start(task: str, env: str, model: str) -> None:
34
  print(f"[START] task={task} env={env} model={model}", flush=True)
 
51
  )
52
 
53
 
54
+ def run_task(agent: LLMAgent, env: FlexTimeEnv, task_id: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  log_start(task=task_id, env=BENCHMARK, model=MODEL_NAME)
56
 
57
  rewards: List[float] = []
 
69
  cur_max_steps = min(MAX_STEPS, cfg["max_steps"])
70
  target_score = cfg["target_score"]
71
 
72
+ agent.reset()
73
+
74
+ last_reward = None
75
+ last_error = None
76
+
77
  for step in range(1, cur_max_steps + 1):
78
  if done:
79
  break
80
+
81
+ # Smart Early Termination
82
+ if len(rewards) >= 3 and rewards[-1] == rewards[-2] == rewards[-3]:
83
+ # Terminate if same reward repeats 3 times (no progress)
84
+ done = True
85
+ break
86
+
87
  # Predict
88
+ action_dict = agent.generate_action(obs_dict, last_reward, last_error)
89
  action_str = json.dumps(action_dict).replace(' ', '')
90
 
91
  # Execute
 
105
  rewards.append(reward)
106
  steps_taken = step
107
 
108
+ last_reward = reward
109
+ last_error = error
110
+
111
  log_step(step=step, action=action_str, reward=reward, done=done, error=error)
112
 
113
  # Grading
 
123
 
124
 
125
  def main():
126
+ if not HF_TOKEN:
127
+ print("[DEBUG] HF_TOKEN is missing. This will crash. Please set HF_TOKEN.", flush=True)
128
+ # We allow client initialization crash if token is missing as it enforces the constraint.
129
+
130
+ client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN or "dummy-key")
131
  env = FlexTimeEnv()
132
+ agent = LLMAgent(client, MODEL_NAME)
133
 
134
  tasks = ["task_easy", "task_medium", "task_hard"]
135
  for t_id in tasks:
136
+ run_task(agent, env, t_id)
137
 
138
 
139
  if __name__ == "__main__":
server/__pycache__/__init__.cpython-313.pyc CHANGED
Binary files a/server/__pycache__/__init__.cpython-313.pyc and b/server/__pycache__/__init__.cpython-313.pyc differ
 
server/__pycache__/engine.cpython-313.pyc CHANGED
Binary files a/server/__pycache__/engine.cpython-313.pyc and b/server/__pycache__/engine.cpython-313.pyc differ
 
server/__pycache__/models.cpython-313.pyc CHANGED
Binary files a/server/__pycache__/models.cpython-313.pyc and b/server/__pycache__/models.cpython-313.pyc differ