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.
@@ -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)