File size: 7,588 Bytes
a2fe35b
 
 
 
 
de2b771
a2fe35b
 
 
 
 
 
 
 
 
 
de2b771
a2fe35b
 
 
 
 
 
 
 
de2b771
 
 
a2fe35b
 
 
 
 
 
de2b771
a2fe35b
 
 
 
de2b771
 
 
 
 
a2fe35b
 
 
 
de2b771
 
 
 
 
 
 
 
 
 
a2fe35b
de2b771
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a2fe35b
 
de2b771
 
 
 
 
a2fe35b
 
de2b771
 
 
 
 
a2fe35b
 
 
de2b771
a2fe35b
de2b771
 
 
 
a2fe35b
 
 
de2b771
 
a2fe35b
 
 
de2b771
 
 
 
 
 
 
 
 
 
 
a2fe35b
de2b771
 
 
 
 
 
 
 
a2fe35b
de2b771
 
 
 
a2fe35b
 
de2b771
 
a2fe35b
 
de2b771
 
 
a2fe35b
 
 
 
 
 
 
 
 
de2b771
a2fe35b
de2b771
 
 
 
 
 
 
a2fe35b
 
de2b771
 
a2fe35b
 
 
de2b771
 
 
 
a2fe35b
de2b771
a2fe35b
de2b771
 
 
a2fe35b
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import json
import textwrap
from openai import OpenAI
from typing import Dict, Any

SYSTEM_PROMPT_BASE = textwrap.dedent(
    """
    You are an expert workforce scheduling agent.
    Your job: assign employees to shifts optimally.
    
    RULES (must follow):
    - Employee skills must include the shift's required_skill
    - Employee must be available on the shift's day (availability[day] == 1)
    - Employee cannot exceed max_hours_per_week
    - No two shifts for the same employee on the same (day, period)
    
    {EFFICIENCY_BIAS}
    
    Respond with ONLY a valid JSON action object — no explanation, no markdown.
    Valid action formats:
      {"action_type": "assign", "employee_id": "emp001", "shift_id": "shf042"}
      {"action_type": "noop"}
    """
).strip()

RECOVERY_BIAS = "The previous approach failed. Try a completely different strategy to assign employees."
EFFICIENCY_BIAS = "Prefer actions that move directly toward task completion in fewer steps. Avoid redundant exploration."

class LLMAgent:
    def __init__(self, client: OpenAI, model_name: str):
        self.client = client
        self.model_name = model_name
        self.temperature = 0.0
        self.max_tokens = 150
        
        self.actions = []
        self.rewards = []
        self.errors = []
        
        # Elite state tracking
        self.best_reward = -float('inf')
        self.last_successful_action = None
        self.task_type = None

    def reset(self):
        self.actions = []
        self.rewards = []
        self.errors = []
        self.best_reward = -float('inf')
        self.last_successful_action = None
        self.task_type = None

    def _infer_task_type(self, obs_dict: Dict[str, Any]) -> str:
        # Task type awareness smart edge
        keys_str = str(obs_dict.keys()).lower()
        if "ui" in keys_str or "element" in keys_str or "screen" in keys_str or "viewport" in keys_str:
            return "navigation"
        return "reasoning"

    def _validate_action_text(self, text: str) -> dict:
        if not text or len(text.strip()) == 0:
            raise ValueError("Empty output from generation.")
        if len(text) > 300: # Fast proxy heuristic for detecting raw text / explanations
            raise ValueError("Output too long, likely contains explanation text.")
            
        clean_text = text.replace("```json", "").replace("```", "").strip()
        try:
            parsed = json.loads(clean_text)
        except json.JSONDecodeError:
            raise ValueError("Invalid JSON format.")
            
        if not isinstance(parsed, dict) or "action_type" not in parsed:
            raise ValueError("Missing 'action_type' required key.")
            
        return parsed

    def generate_action(self, obs_dict: Dict[str, Any], last_reward: float = None, last_error: str = None) -> dict:
        # 1. Edge/Task Check
        if len(self.actions) == 0:
            self.task_type = self._infer_task_type(obs_dict)
            
        # 2. Reward-Aware Tracking / Micro Memory Biasing
        if last_reward is not None:
            self.rewards.append(last_reward)
            if self.best_reward != -float('inf') and last_reward > self.best_reward:
                self.last_successful_action = self.actions[-1] if self.actions else {"action_type": "noop"}
            if last_reward > self.best_reward:
                self.best_reward = last_reward
                
        if last_error:
            self.errors.append(last_error)
            
        # Output Compression: Minimal token structuring
        slim = {
            "unassigned": obs_dict.get("unassigned_shifts", [])[:8],
            "emps": [
                {k: e[k] for k in ("id", "skills", "availability", "assigned_hours", "max_hours_per_week")}
                for e in obs_dict.get("employees", [])
            ],
            "shifts": [
                {k: s[k] for k in ("id", "day", "period", "required_skill", "duration_hours")}
                for s in obs_dict.get("shifts", [])
                if s["id"] in obs_dict.get("unassigned_shifts", [])[:8]
            ],
        }
        
        # State Context Construct
        prompt_injections = []
        
        if self.task_type == "navigation":
            prompt_injections.append("Treat this as visual navigation: Use precise, minimal actions targeting interactive elements.")
        else:
            prompt_injections.append("Treat this as logical reasoning: Use structured rational progression.")
            
        # Strategy Bias based on Performance drop
        if last_reward is not None and last_reward < self.best_reward:
            prompt_injections.append("Avoid strategies that previously reduced reward.")
            
        # Failure Check for Context Switch
        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"):
            prompt_injections.append(RECOVERY_BIAS)
            
        base_prompt = SYSTEM_PROMPT_BASE.replace("{EFFICIENCY_BIAS}", EFFICIENCY_BIAS)
        compiled_prompt = base_prompt + "\n\nCRITICAL CONTEXT:\n" + "\n".join(prompt_injections)
        
        user_payload = {
            "CURRENT STATE": slim,
            "PREVIOUS ACTIONS": self.actions[-3:], # Short context to compress
        }
        if self.last_successful_action:
            user_payload["BEST PAST DIRECTIVE"] = self.last_successful_action

        messages = [
            {"role": "system", "content": compiled_prompt},
            {"role": "user", "content": json.dumps(user_payload)},
        ]
        
        action_dict = {"action_type": "noop"} # Safe default mapping
        
        # Primary Action Generation Block
        try:
            completion = self.client.chat.completions.create(
                model=self.model_name,
                messages=messages,
                temperature=self.temperature,
                max_tokens=self.max_tokens,
                stream=False,
            )
            attempt_text = completion.choices[0].message.content or ""
            action_dict = self._validate_action_text(attempt_text)  # Strict validation layer
            
        except Exception as initial_err:
            # Confidence-Based Retry System -> Single fast retry loop with strict penalty prompts
            try:
                retry_messages = messages + [
                    {"role": "assistant", "content": str(attempt_text) if 'attempt_text' in locals() else "Error"},
                    {"role": "user", "content": "Return ONLY a valid executable action JSON constraint. No explanation."}
                ]
                completion_retry = self.client.chat.completions.create(
                    model=self.model_name,
                    messages=retry_messages,
                    temperature=0.0,
                    max_tokens=self.max_tokens,
                    stream=False,
                )
                action_dict = self._validate_action_text(completion_retry.choices[0].message.content or "")
            except Exception:
                # Safe Fallback Action - Double failure defaults seamlessly safely without loop crash
                action_dict = {"action_type": "noop"}
                
        # Micro-Memory tracking caps
        self.actions.append(action_dict)
        if len(self.actions) > 5: self.actions.pop(0)
        if len(self.rewards) > 5: self.rewards.pop(0)
        if len(self.errors) > 5: self.errors.pop(0)
            
        return action_dict