File size: 7,588 Bytes
8697ae6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
179
180
181
182
183
184
185
186
187
"""Sample LLM agent for the HR Productivity Environment using Claude API."""

from __future__ import annotations

import json
import logging
from typing import Any, Dict, List, Optional

import anthropic

from agent.prompts import PHASE_PROMPTS, QUARTER_SUMMARY_PROMPT, SYSTEM_PROMPT

logger = logging.getLogger(__name__)


class HRAgent:
    """Claude-based HR agent with phase-aware prompting and memory management."""

    def __init__(
        self,
        model: str = "claude-sonnet-4-6",
        max_tokens: int = 2048,
    ):
        self.client = anthropic.Anthropic()
        self.model = model
        self.max_tokens = max_tokens
        self.quarter_summaries: List[str] = []
        self.current_quarter: int = 1
        self.conversation_history: List[Dict[str, Any]] = []
        self.max_history_messages = 20  # Rolling window

    def decide(self, observation: Dict[str, Any]) -> Dict[str, Any]:
        """Given an observation, decide the next action.

        Args:
            observation: The observation dict from the environment.

        Returns:
            Action dict to send to the environment.
        """
        phase = observation.get("current_phase", "scanning")
        quarter = observation.get("current_quarter", 1)

        # Detect quarter change
        if quarter != self.current_quarter:
            self._summarize_quarter()
            self.current_quarter = quarter
            self.conversation_history = []  # Reset conversation for new quarter

        # Build the prompt
        user_message = self._build_user_message(observation, phase)
        self.conversation_history.append({"role": "user", "content": user_message})

        # Trim history if too long
        if len(self.conversation_history) > self.max_history_messages:
            self.conversation_history = self.conversation_history[-self.max_history_messages:]

        # Build system prompt with memory
        system = self._build_system_prompt(phase)

        try:
            response = self.client.messages.create(
                model=self.model,
                max_tokens=self.max_tokens,
                system=system,
                messages=self.conversation_history,
            )
            assistant_text = response.content[0].text
            self.conversation_history.append({"role": "assistant", "content": assistant_text})

            # Parse action from response
            action = self._parse_action(assistant_text)
            return action

        except Exception as e:
            logger.error(f"Agent error: {e}")
            # Fallback: advance phase or query
            return self._fallback_action(phase, observation)

    def _build_system_prompt(self, phase: str) -> str:
        """Build system prompt with phase guidance and quarter memory."""
        parts = [SYSTEM_PROMPT]

        if self.quarter_summaries:
            parts.append("\n## Previous Quarter Summaries:")
            for i, summary in enumerate(self.quarter_summaries, 1):
                parts.append(f"\n### Q{i}:\n{summary}")

        parts.append(f"\n{PHASE_PROMPTS.get(phase, '')}")
        return "\n".join(parts)

    def _build_user_message(self, observation: Dict[str, Any], phase: str) -> str:
        """Build user message from observation."""
        parts = [f"**Quarter {observation.get('current_quarter', '?')}{phase.upper()} phase**"]
        parts.append(f"Step {observation.get('step_in_quarter', 0)} in quarter")
        parts.append(f"\n**Message:** {observation.get('message', 'No message')}")

        if observation.get("warnings"):
            parts.append(f"\n**Warnings:** {', '.join(observation['warnings'])}")

        if observation.get("data"):
            # Truncate large data
            data_str = json.dumps(observation["data"], indent=2, default=str)
            if len(data_str) > 3000:
                data_str = data_str[:3000] + "\n... (truncated)"
            parts.append(f"\n**Data:**\n```json\n{data_str}\n```")

        available = observation.get("available_actions", [])
        parts.append(f"\n**Available actions:** {', '.join(available)}")
        parts.append("\nRespond with a JSON action object.")

        return "\n".join(parts)

    def _parse_action(self, text: str) -> Dict[str, Any]:
        """Extract JSON action from the LLM response."""
        # Try to find JSON block
        if "```json" in text:
            start = text.index("```json") + 7
            end = text.index("```", start)
            json_str = text[start:end].strip()
        elif "```" in text:
            start = text.index("```") + 3
            end = text.index("```", start)
            json_str = text[start:end].strip()
        elif "{" in text:
            start = text.index("{")
            # Find matching closing brace
            depth = 0
            for i, c in enumerate(text[start:], start):
                if c == "{":
                    depth += 1
                elif c == "}":
                    depth -= 1
                    if depth == 0:
                        json_str = text[start:i + 1]
                        break
            else:
                json_str = text[start:]
        else:
            return {"action_type": "advance_phase", "rationale": "Could not parse action, advancing"}

        try:
            action = json.loads(json_str)
            # Ensure action_type exists
            if "action_type" not in action:
                action["action_type"] = "advance_phase"
            return action
        except json.JSONDecodeError:
            return {"action_type": "advance_phase", "rationale": "JSON parse error, advancing"}

    def _fallback_action(self, phase: str, observation: Dict[str, Any]) -> Dict[str, Any]:
        """Generate a safe fallback action."""
        available = observation.get("available_actions", [])
        if "advance_phase" in available:
            return {"action_type": "advance_phase", "rationale": "Fallback: advancing phase"}
        if "advance_quarter" in available:
            return {"action_type": "advance_quarter", "rationale": "Fallback: advancing quarter"}
        if "query_department" in available:
            return {"action_type": "query_department", "department": "Engineering", "rationale": "Fallback: scanning"}
        if "submit_report" in available:
            return {"action_type": "submit_report", "rationale": "Fallback: submitting report"}
        if available:
            return {"action_type": available[0], "rationale": "Fallback: first available action"}
        return {"action_type": "advance_phase", "rationale": "Fallback: no actions available"}

    def _summarize_quarter(self) -> None:
        """Generate a quarter summary using the LLM for memory compression."""
        if not self.conversation_history:
            self.quarter_summaries.append(f"Q{self.current_quarter}: No actions recorded.")
            return

        try:
            messages = self.conversation_history + [
                {"role": "user", "content": QUARTER_SUMMARY_PROMPT}
            ]
            response = self.client.messages.create(
                model=self.model,
                max_tokens=500,
                system="You are summarizing an HR management quarter. Be concise.",
                messages=messages,
            )
            summary = response.content[0].text
            self.quarter_summaries.append(summary)
        except Exception as e:
            logger.warning(f"Summary generation failed: {e}")
            self.quarter_summaries.append(f"Q{self.current_quarter}: Summary unavailable.")