econagents 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- econagents/__init__.py +31 -0
- econagents/_c_extension.pyi +5 -0
- econagents/core/__init__.py +7 -0
- econagents/core/agent_role.py +360 -0
- econagents/core/events.py +14 -0
- econagents/core/game_runner.py +358 -0
- econagents/core/logging_mixin.py +45 -0
- econagents/core/manager/__init__.py +5 -0
- econagents/core/manager/base.py +430 -0
- econagents/core/manager/phase.py +498 -0
- econagents/core/state/__init__.py +0 -0
- econagents/core/state/fields.py +51 -0
- econagents/core/state/game.py +222 -0
- econagents/core/state/market.py +124 -0
- econagents/core/transport.py +132 -0
- econagents/llm/__init__.py +3 -0
- econagents/llm/openai.py +61 -0
- econagents/py.typed +0 -0
- econagents-0.0.1.dist-info/METADATA +90 -0
- econagents-0.0.1.dist-info/RECORD +21 -0
- econagents-0.0.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,498 @@
|
|
1
|
+
import asyncio
|
2
|
+
import json
|
3
|
+
import logging
|
4
|
+
import random
|
5
|
+
from abc import ABC, abstractmethod
|
6
|
+
from typing import Any, Callable, Optional
|
7
|
+
from pathlib import Path
|
8
|
+
|
9
|
+
from econagents.core.agent_role import AgentRole
|
10
|
+
from econagents.core.events import Message
|
11
|
+
from econagents.core.manager.base import AgentManager
|
12
|
+
from econagents.core.state.game import GameState
|
13
|
+
from econagents.core.transport import AuthenticationMechanism, SimpleLoginPayloadAuth
|
14
|
+
|
15
|
+
|
16
|
+
class PhaseManager(AgentManager, ABC):
|
17
|
+
"""
|
18
|
+
Abstract manager that handles the concept of 'phases' in a game.
|
19
|
+
|
20
|
+
This manager standardizes the interface for phase-based games with hooks for
|
21
|
+
phase transitions and optional continuous phase handling.
|
22
|
+
|
23
|
+
Features:
|
24
|
+
1. Standardized interface for starting a phase
|
25
|
+
|
26
|
+
2. Optional continuous "tick loop" for phases
|
27
|
+
|
28
|
+
3. Hooks for "on phase start," "on phase end," and "on phase transition event"
|
29
|
+
|
30
|
+
All configuration parameters can be:
|
31
|
+
|
32
|
+
1. Provided at initialization time
|
33
|
+
|
34
|
+
2. Injected later using property setters
|
35
|
+
|
36
|
+
Args:
|
37
|
+
url (Optional[str]): WebSocket server URL
|
38
|
+
phase_transition_event (Optional[str]): Event name for phase transitions
|
39
|
+
phase_identifier_key (Optional[str]): Key in the event data that identifies the phase
|
40
|
+
continuous_phases (Optional[set[int]]): set of phase numbers that should be treated as continuous
|
41
|
+
min_action_delay (Optional[int]): Minimum delay in seconds between actions in continuous phases
|
42
|
+
max_action_delay (Optional[int]): Maximum delay in seconds between actions in continuous phases
|
43
|
+
state (Optional[GameState]): Game state object to track game state
|
44
|
+
agent_role (Optional[AgentRole]): Agent role instance to handle game phases
|
45
|
+
auth_mechanism (Optional[AuthenticationMechanism]): Authentication mechanism to use
|
46
|
+
auth_mechanism_kwargs (Optional[dict[str, Any]]): Keyword arguments for the authentication mechanism
|
47
|
+
logger (Optional[logging.Logger]): Logger instance for tracking events
|
48
|
+
prompts_dir (Optional[Path]): Directory containing the prompt templates
|
49
|
+
"""
|
50
|
+
|
51
|
+
def __init__(
|
52
|
+
self,
|
53
|
+
url: Optional[str] = None,
|
54
|
+
phase_transition_event: Optional[str] = None,
|
55
|
+
phase_identifier_key: Optional[str] = None,
|
56
|
+
continuous_phases: Optional[set[int]] = None,
|
57
|
+
min_action_delay: Optional[int] = None,
|
58
|
+
max_action_delay: Optional[int] = None,
|
59
|
+
state: Optional[GameState] = None,
|
60
|
+
agent_role: Optional[AgentRole] = None,
|
61
|
+
auth_mechanism: Optional[AuthenticationMechanism] = None,
|
62
|
+
auth_mechanism_kwargs: Optional[dict[str, Any]] = None,
|
63
|
+
logger: Optional[logging.Logger] = None,
|
64
|
+
prompts_dir: Optional[Path] = None,
|
65
|
+
):
|
66
|
+
super().__init__(
|
67
|
+
url=url,
|
68
|
+
logger=logger,
|
69
|
+
auth_mechanism=auth_mechanism,
|
70
|
+
auth_mechanism_kwargs=auth_mechanism_kwargs,
|
71
|
+
)
|
72
|
+
self._agent_role = agent_role
|
73
|
+
self._state = state
|
74
|
+
self.current_phase: Optional[int] = None
|
75
|
+
self._phase_transition_event = phase_transition_event
|
76
|
+
self._phase_identifier_key = phase_identifier_key
|
77
|
+
self._continuous_phases = continuous_phases
|
78
|
+
self._min_action_delay = min_action_delay
|
79
|
+
self._max_action_delay = max_action_delay
|
80
|
+
self._prompts_dir = prompts_dir
|
81
|
+
self._continuous_task: Optional[asyncio.Task] = None
|
82
|
+
self.in_continuous_phase = False
|
83
|
+
|
84
|
+
# Register the phase transition handler if we have an event name
|
85
|
+
if self._phase_transition_event:
|
86
|
+
self.register_event_handler(self._phase_transition_event, self._on_phase_transition_event)
|
87
|
+
|
88
|
+
# set up global pre-event hook for state updates if state is provided
|
89
|
+
if self._state:
|
90
|
+
self.register_global_pre_event_hook(self._update_state)
|
91
|
+
|
92
|
+
@property
|
93
|
+
def agent_role(self) -> Optional[AgentRole]:
|
94
|
+
"""Get the current agent role instance."""
|
95
|
+
return self._agent_role
|
96
|
+
|
97
|
+
@agent_role.setter
|
98
|
+
def agent_role(self, value: AgentRole):
|
99
|
+
"""Set the agent role instance."""
|
100
|
+
self._agent_role = value
|
101
|
+
if self._agent_role:
|
102
|
+
self._agent_role.logger = self.logger
|
103
|
+
|
104
|
+
@property
|
105
|
+
def state(self) -> GameState:
|
106
|
+
"""Get the current game state."""
|
107
|
+
return self._state # type: ignore
|
108
|
+
|
109
|
+
@state.setter
|
110
|
+
def state(self, value: Optional[GameState]):
|
111
|
+
"""Set the game state."""
|
112
|
+
old_state = self._state
|
113
|
+
self._state = value
|
114
|
+
|
115
|
+
# If we didn't have a state before but now we do, set up the state update hook
|
116
|
+
if not old_state and self._state:
|
117
|
+
self.register_global_pre_event_hook(self._update_state)
|
118
|
+
|
119
|
+
@property
|
120
|
+
def phase_transition_event(self) -> str:
|
121
|
+
"""Get the phase transition event name."""
|
122
|
+
return self._phase_transition_event # type: ignore
|
123
|
+
|
124
|
+
@phase_transition_event.setter
|
125
|
+
def phase_transition_event(self, value: str):
|
126
|
+
"""Set the phase transition event name."""
|
127
|
+
old_event = self._phase_transition_event
|
128
|
+
self._phase_transition_event = value
|
129
|
+
|
130
|
+
# Update the event handler if the event name changed
|
131
|
+
if old_event != self._phase_transition_event:
|
132
|
+
if old_event:
|
133
|
+
self.unregister_event_handler(old_event)
|
134
|
+
self.register_event_handler(self._phase_transition_event, self._on_phase_transition_event)
|
135
|
+
|
136
|
+
@property
|
137
|
+
def phase_identifier_key(self) -> str:
|
138
|
+
"""Get the phase identifier key."""
|
139
|
+
return self._phase_identifier_key # type: ignore
|
140
|
+
|
141
|
+
@phase_identifier_key.setter
|
142
|
+
def phase_identifier_key(self, value: str):
|
143
|
+
"""Set the phase identifier key."""
|
144
|
+
self._phase_identifier_key = value
|
145
|
+
|
146
|
+
@property
|
147
|
+
def continuous_phases(self) -> set[int]:
|
148
|
+
"""Get the set of continuous phases."""
|
149
|
+
return self._continuous_phases # type: ignore
|
150
|
+
|
151
|
+
@continuous_phases.setter
|
152
|
+
def continuous_phases(self, value: set[int]):
|
153
|
+
"""Set the continuous phases."""
|
154
|
+
self._continuous_phases = value
|
155
|
+
|
156
|
+
@property
|
157
|
+
def min_action_delay(self) -> int:
|
158
|
+
"""Get the minimum action delay."""
|
159
|
+
return self._min_action_delay # type: ignore
|
160
|
+
|
161
|
+
@min_action_delay.setter
|
162
|
+
def min_action_delay(self, value: int):
|
163
|
+
"""Set the minimum action delay."""
|
164
|
+
self._min_action_delay = value
|
165
|
+
|
166
|
+
@property
|
167
|
+
def max_action_delay(self) -> int:
|
168
|
+
"""Get the maximum action delay."""
|
169
|
+
return self._max_action_delay # type: ignore
|
170
|
+
|
171
|
+
@max_action_delay.setter
|
172
|
+
def max_action_delay(self, value: int):
|
173
|
+
"""Set the maximum action delay."""
|
174
|
+
self._max_action_delay = value
|
175
|
+
|
176
|
+
@property
|
177
|
+
def prompts_dir(self) -> Path:
|
178
|
+
"""Get the prompts directory."""
|
179
|
+
return self._prompts_dir # type: ignore
|
180
|
+
|
181
|
+
@prompts_dir.setter
|
182
|
+
def prompts_dir(self, value: Path):
|
183
|
+
"""Set the prompts directory."""
|
184
|
+
self._prompts_dir = value
|
185
|
+
|
186
|
+
async def start(self):
|
187
|
+
"""Start the manager."""
|
188
|
+
# TODO: is there a better place to do this?
|
189
|
+
if self._agent_role:
|
190
|
+
self._agent_role.logger = self.logger
|
191
|
+
await super().start()
|
192
|
+
|
193
|
+
async def _update_state(self, message: Message):
|
194
|
+
"""Update the game state when an event is received.
|
195
|
+
|
196
|
+
Args:
|
197
|
+
message (Message): The message containing the event data
|
198
|
+
"""
|
199
|
+
if self._state:
|
200
|
+
self._state.update(message)
|
201
|
+
self.logger.debug(f"Updated state: {self._state}")
|
202
|
+
|
203
|
+
async def _on_phase_transition_event(self, message: Message):
|
204
|
+
"""
|
205
|
+
Process a phase transition event.
|
206
|
+
|
207
|
+
Extracts the new phase from the message and calls handle_phase_transition.
|
208
|
+
|
209
|
+
Args:
|
210
|
+
message (Message): The message containing the event data
|
211
|
+
"""
|
212
|
+
if not self.phase_identifier_key:
|
213
|
+
raise ValueError("Phase identifier key is not set")
|
214
|
+
|
215
|
+
new_phase = message.data.get(self.phase_identifier_key)
|
216
|
+
await self.handle_phase_transition(new_phase)
|
217
|
+
|
218
|
+
async def handle_phase_transition(self, new_phase: Optional[int]):
|
219
|
+
"""
|
220
|
+
Handle a phase transition.
|
221
|
+
|
222
|
+
This method is the main orchestrator for phase transitions:
|
223
|
+
1. If leaving a continuous phase, stops the continuous task
|
224
|
+
2. Calls the on_phase_end hook for the old phase
|
225
|
+
3. Updates the current phase
|
226
|
+
4. Calls the on_phase_start hook for the new phase
|
227
|
+
5. Starts a continuous task if entering a continuous phase
|
228
|
+
6. Executes a single action if entering a non-continuous phase
|
229
|
+
|
230
|
+
Args:
|
231
|
+
new_phase (Optional[int]): The new phase number
|
232
|
+
"""
|
233
|
+
self.logger.info(f"Transitioning to phase {new_phase}")
|
234
|
+
|
235
|
+
# If we were in a continuous phase, stop it
|
236
|
+
if self.in_continuous_phase and new_phase != self.current_phase:
|
237
|
+
self.logger.info(f"Stopping continuous phase {self.current_phase}")
|
238
|
+
self.in_continuous_phase = False
|
239
|
+
if self._continuous_task:
|
240
|
+
self._continuous_task.cancel()
|
241
|
+
self._continuous_task = None
|
242
|
+
|
243
|
+
# Call the on_phase_end hook for the old phase
|
244
|
+
old_phase = self.current_phase
|
245
|
+
if old_phase is not None:
|
246
|
+
await self.on_phase_end(old_phase)
|
247
|
+
|
248
|
+
# Update current phase
|
249
|
+
self.current_phase = new_phase
|
250
|
+
|
251
|
+
if new_phase is not None:
|
252
|
+
# Call the on_phase_start hook for the new phase
|
253
|
+
await self.on_phase_start(new_phase)
|
254
|
+
|
255
|
+
# If the new phase is continuous, start a continuous task
|
256
|
+
if self.continuous_phases and new_phase in self.continuous_phases:
|
257
|
+
self.in_continuous_phase = True
|
258
|
+
self._continuous_task = asyncio.create_task(self._continuous_phase_loop(new_phase))
|
259
|
+
|
260
|
+
# Execute an initial action
|
261
|
+
await self.execute_phase_action(new_phase)
|
262
|
+
else:
|
263
|
+
# Execute a single action for non-continuous phases
|
264
|
+
await self.execute_phase_action(new_phase)
|
265
|
+
|
266
|
+
async def _continuous_phase_loop(self, phase: int):
|
267
|
+
"""
|
268
|
+
Run a loop that periodically executes actions for a continuous phase.
|
269
|
+
|
270
|
+
Args:
|
271
|
+
phase (int): The phase number
|
272
|
+
"""
|
273
|
+
try:
|
274
|
+
while self.in_continuous_phase:
|
275
|
+
# Wait for a random delay before executing the next action
|
276
|
+
delay = random.randint(self.min_action_delay, self.max_action_delay)
|
277
|
+
self.logger.debug(f"Waiting {delay} seconds before next action in phase {phase}")
|
278
|
+
await asyncio.sleep(delay)
|
279
|
+
|
280
|
+
# Check if we're still in the same continuous phase
|
281
|
+
if not self.in_continuous_phase or self.current_phase != phase:
|
282
|
+
break
|
283
|
+
|
284
|
+
# Execute the action
|
285
|
+
await self.execute_phase_action(phase)
|
286
|
+
except asyncio.CancelledError:
|
287
|
+
self.logger.info(f"Continuous phase {phase} loop cancelled")
|
288
|
+
except Exception as e:
|
289
|
+
self.logger.exception(f"Error in continuous phase {phase} loop: {e}")
|
290
|
+
|
291
|
+
@abstractmethod
|
292
|
+
async def execute_phase_action(self, phase: int):
|
293
|
+
"""
|
294
|
+
Execute one action for the current phase.
|
295
|
+
|
296
|
+
This is the core method that subclasses must implement to define
|
297
|
+
how to handle actions for a specific phase.
|
298
|
+
|
299
|
+
Args:
|
300
|
+
phase (int): The phase number
|
301
|
+
"""
|
302
|
+
pass
|
303
|
+
|
304
|
+
async def on_phase_start(self, phase: int):
|
305
|
+
"""
|
306
|
+
Hook that is called when a phase starts.
|
307
|
+
|
308
|
+
Subclasses can override this to implement custom behavior.
|
309
|
+
|
310
|
+
Args:
|
311
|
+
phase (int): The phase number
|
312
|
+
"""
|
313
|
+
pass
|
314
|
+
|
315
|
+
async def on_phase_end(self, phase: int):
|
316
|
+
"""
|
317
|
+
Hook that is called when a phase ends.
|
318
|
+
|
319
|
+
Subclasses can override this to implement custom behavior.
|
320
|
+
|
321
|
+
Args:
|
322
|
+
phase (int): The phase number
|
323
|
+
"""
|
324
|
+
pass
|
325
|
+
|
326
|
+
async def stop(self):
|
327
|
+
"""Stop the manager and cancel any continuous phase tasks."""
|
328
|
+
self.in_continuous_phase = False
|
329
|
+
if self._continuous_task:
|
330
|
+
self._continuous_task.cancel()
|
331
|
+
self._continuous_task = None
|
332
|
+
await super().stop()
|
333
|
+
|
334
|
+
|
335
|
+
class TurnBasedPhaseManager(PhaseManager):
|
336
|
+
"""
|
337
|
+
A manager for turn-based games that handles phase transitions.
|
338
|
+
|
339
|
+
This manager inherits from PhaseManager and provides a concrete implementation
|
340
|
+
for executing actions in each phase.
|
341
|
+
|
342
|
+
Args:
|
343
|
+
url (Optional[str]): WebSocket server URL
|
344
|
+
phase_transition_event (Optional[str]): Event name for phase transitions
|
345
|
+
phase_identifier_key (Optional[str]): Key in the event data that identifies the phase
|
346
|
+
auth_mechanism (Optional[AuthenticationMechanism]): Authentication mechanism to use
|
347
|
+
auth_mechanism_kwargs (Optional[dict[str, Any]]): Keyword arguments for the authentication mechanism
|
348
|
+
state (Optional[GameState]): Game state object to track game state
|
349
|
+
agent_role (Optional[AgentRole]): Agent role instance to handle game phases
|
350
|
+
logger (Optional[logging.Logger]): Logger instance for tracking events
|
351
|
+
prompts_dir (Optional[Path]): Directory containing the prompt templates
|
352
|
+
"""
|
353
|
+
|
354
|
+
def __init__(
|
355
|
+
self,
|
356
|
+
url: Optional[str] = None,
|
357
|
+
phase_transition_event: Optional[str] = None,
|
358
|
+
phase_identifier_key: Optional[str] = None,
|
359
|
+
auth_mechanism: Optional[AuthenticationMechanism] = None,
|
360
|
+
auth_mechanism_kwargs: Optional[dict[str, Any]] = None,
|
361
|
+
state: Optional[GameState] = None,
|
362
|
+
agent_role: Optional[AgentRole] = None,
|
363
|
+
logger: Optional[logging.Logger] = None,
|
364
|
+
prompts_dir: Optional[Path] = None,
|
365
|
+
):
|
366
|
+
super().__init__(
|
367
|
+
url=url,
|
368
|
+
phase_transition_event=phase_transition_event,
|
369
|
+
phase_identifier_key=phase_identifier_key,
|
370
|
+
auth_mechanism=auth_mechanism,
|
371
|
+
auth_mechanism_kwargs=auth_mechanism_kwargs,
|
372
|
+
continuous_phases=set(),
|
373
|
+
state=state,
|
374
|
+
agent_role=agent_role,
|
375
|
+
logger=logger,
|
376
|
+
prompts_dir=prompts_dir,
|
377
|
+
)
|
378
|
+
# Register phase handlers
|
379
|
+
self._phase_handlers: dict[int, Callable[[int, Any], Any]] = {}
|
380
|
+
|
381
|
+
async def execute_phase_action(self, phase: int):
|
382
|
+
"""
|
383
|
+
Execute an action for the given phase by delegating to the registered handler or agent.
|
384
|
+
|
385
|
+
Args:
|
386
|
+
phase (int): The phase number
|
387
|
+
"""
|
388
|
+
payload = None
|
389
|
+
|
390
|
+
if phase in self._phase_handlers:
|
391
|
+
# If we have a registered handler for this phase, use it
|
392
|
+
self.logger.debug(f"Using registered handler for phase {phase}")
|
393
|
+
payload = await self._phase_handlers[phase](phase, self.state)
|
394
|
+
elif self.agent_role:
|
395
|
+
# If we don't have a registered handler but we have an agent, use the agent
|
396
|
+
self.logger.debug(f"Using agent {self.agent_role.name} handle_phase for phase {phase}")
|
397
|
+
payload = await self.agent_role.handle_phase(phase, self.state, self.prompts_dir)
|
398
|
+
|
399
|
+
if payload:
|
400
|
+
await self.send_message(json.dumps(payload))
|
401
|
+
|
402
|
+
def register_phase_handler(self, phase: int, handler: Callable[[int, Any], Any]):
|
403
|
+
"""
|
404
|
+
Register a custom handler for a specific phase.
|
405
|
+
|
406
|
+
Args:
|
407
|
+
phase (int): The phase number
|
408
|
+
handler (Callable[[int, Any], Any]): The function to call when this phase is active
|
409
|
+
"""
|
410
|
+
self._phase_handlers[phase] = handler
|
411
|
+
self.logger.debug(f"Registered handler for phase {phase}")
|
412
|
+
|
413
|
+
|
414
|
+
class HybridPhaseManager(PhaseManager):
|
415
|
+
"""
|
416
|
+
A manager for games that combine turn-based and continuous action phases.
|
417
|
+
|
418
|
+
This manager extends PhaseManager and configures it with specific phases
|
419
|
+
that should be treated as continuous.
|
420
|
+
|
421
|
+
Args:
|
422
|
+
continuous_phases (Optional[set[int]]): Set of phase numbers that should be treated as continuous
|
423
|
+
url (Optional[str]): WebSocket server URL
|
424
|
+
auth_mechanism (Optional[AuthenticationMechanism]): Authentication mechanism to use
|
425
|
+
auth_mechanism_kwargs (Optional[dict[str, Any]]): Keyword arguments for the authentication mechanism
|
426
|
+
phase_transition_event (Optional[str]): Event name for phase transitions
|
427
|
+
phase_identifier_key (Optional[str]): Key in the event data that identifies the phase
|
428
|
+
min_action_delay (Optional[int]): Minimum delay in seconds between actions in continuous phases
|
429
|
+
max_action_delay (Optional[int]): Maximum delay in seconds between actions in continuous phases
|
430
|
+
state (Optional[GameState]): Game state object to track game state
|
431
|
+
agent_role (Optional[AgentRole]): Agent role instance to handle game phases
|
432
|
+
logger (Optional[logging.Logger]): Logger instance for tracking events
|
433
|
+
prompts_dir (Optional[Path]): Directory containing the prompt templates
|
434
|
+
"""
|
435
|
+
|
436
|
+
def __init__(
|
437
|
+
self,
|
438
|
+
continuous_phases: Optional[set[int]] = None,
|
439
|
+
url: Optional[str] = None,
|
440
|
+
auth_mechanism: Optional[AuthenticationMechanism] = None,
|
441
|
+
auth_mechanism_kwargs: Optional[dict[str, Any]] = None,
|
442
|
+
phase_transition_event: Optional[str] = None,
|
443
|
+
phase_identifier_key: Optional[str] = None,
|
444
|
+
min_action_delay: Optional[int] = None,
|
445
|
+
max_action_delay: Optional[int] = None,
|
446
|
+
state: Optional[GameState] = None,
|
447
|
+
agent_role: Optional[AgentRole] = None,
|
448
|
+
logger: Optional[logging.Logger] = None,
|
449
|
+
prompts_dir: Optional[Path] = None,
|
450
|
+
):
|
451
|
+
super().__init__(
|
452
|
+
url=url,
|
453
|
+
phase_transition_event=phase_transition_event,
|
454
|
+
phase_identifier_key=phase_identifier_key,
|
455
|
+
auth_mechanism=auth_mechanism,
|
456
|
+
auth_mechanism_kwargs=auth_mechanism_kwargs,
|
457
|
+
continuous_phases=continuous_phases,
|
458
|
+
min_action_delay=min_action_delay,
|
459
|
+
max_action_delay=max_action_delay,
|
460
|
+
state=state,
|
461
|
+
agent_role=agent_role,
|
462
|
+
logger=logger,
|
463
|
+
prompts_dir=prompts_dir,
|
464
|
+
)
|
465
|
+
# Register phase handlers
|
466
|
+
self._phase_handlers: dict[int, Callable[[int, Any], Any]] = {}
|
467
|
+
|
468
|
+
async def execute_phase_action(self, phase: int):
|
469
|
+
"""
|
470
|
+
Execute an action for the given phase by delegating to the registered handler or agent.
|
471
|
+
|
472
|
+
Args:
|
473
|
+
phase (int): The phase number
|
474
|
+
"""
|
475
|
+
payload = None
|
476
|
+
|
477
|
+
if phase in self._phase_handlers:
|
478
|
+
# If we have a registered handler for this phase, use it
|
479
|
+
self.logger.debug(f"Using registered handler for phase {phase}")
|
480
|
+
payload = await self._phase_handlers[phase](phase, self.state)
|
481
|
+
elif self.agent_role:
|
482
|
+
# If we don't have a registered handler but we have an agent, use the agent
|
483
|
+
self.logger.debug(f"Using agent {self.agent_role.name} handle_phase for phase {phase}")
|
484
|
+
payload = await self.agent_role.handle_phase(phase, self.state, self.prompts_dir)
|
485
|
+
|
486
|
+
if payload:
|
487
|
+
await self.send_message(json.dumps(payload))
|
488
|
+
|
489
|
+
def register_phase_handler(self, phase: int, handler: Callable[[int, Any], Any]):
|
490
|
+
"""
|
491
|
+
Register a custom handler for a specific phase.
|
492
|
+
|
493
|
+
Args:
|
494
|
+
phase (int): The phase number
|
495
|
+
handler (Callable[[int, Any], Any]): The function to call when this phase is active
|
496
|
+
"""
|
497
|
+
self._phase_handlers[phase] = handler
|
498
|
+
self.logger.debug(f"Registered handler for phase {phase}")
|
File without changes
|
@@ -0,0 +1,51 @@
|
|
1
|
+
from typing import Any, Callable, Optional
|
2
|
+
|
3
|
+
from pydantic import Field
|
4
|
+
|
5
|
+
|
6
|
+
def EventField(
|
7
|
+
default: Any = ...,
|
8
|
+
*,
|
9
|
+
default_factory: Optional[Callable[[], Any]] = None,
|
10
|
+
event_key: Optional[str] = None,
|
11
|
+
exclude_from_mapping: bool = False,
|
12
|
+
events: Optional[list[str]] = None,
|
13
|
+
exclude_events: Optional[list[str]] = None,
|
14
|
+
**kwargs: Any,
|
15
|
+
) -> Any:
|
16
|
+
"""
|
17
|
+
Create a field with event mapping metadata.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
default (Any): Default value for the field
|
21
|
+
default_factory (Callable[[], Any]): Factory function to generate default value
|
22
|
+
event_key (Optional[str]): The key in event data that maps to this field
|
23
|
+
exclude_from_mapping (bool): Whether to exclude this field from event mapping
|
24
|
+
events (Optional[list[str]]): Optional list of events where this mapping should be applied
|
25
|
+
exclude_events (Optional[list[str]]): Optional list of events where this mapping should not be applied
|
26
|
+
**kwargs: Additional arguments to pass to Pydantic's Field
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
FieldInfo: A Pydantic FieldInfo object with event mapping metadata
|
30
|
+
"""
|
31
|
+
# Create a dictionary for custom metadata
|
32
|
+
event_metadata = {
|
33
|
+
"event_key": event_key,
|
34
|
+
"exclude_from_mapping": exclude_from_mapping,
|
35
|
+
"events": events,
|
36
|
+
"exclude_events": exclude_events,
|
37
|
+
}
|
38
|
+
|
39
|
+
# Store metadata in json_schema_extra
|
40
|
+
if "json_schema_extra" in kwargs:
|
41
|
+
kwargs["json_schema_extra"].update({"event_metadata": event_metadata})
|
42
|
+
else:
|
43
|
+
kwargs["json_schema_extra"] = {"event_metadata": event_metadata}
|
44
|
+
|
45
|
+
# Create the field with Pydantic's Field
|
46
|
+
if default is not ...:
|
47
|
+
return Field(default=default, **kwargs)
|
48
|
+
elif default_factory is not None:
|
49
|
+
return Field(default_factory=default_factory, **kwargs)
|
50
|
+
else:
|
51
|
+
return Field(**kwargs)
|