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,430 @@
|
|
1
|
+
import asyncio
|
2
|
+
import json
|
3
|
+
import logging
|
4
|
+
import traceback
|
5
|
+
from typing import Any, Callable, Optional
|
6
|
+
|
7
|
+
|
8
|
+
from econagents.core.events import Message
|
9
|
+
from econagents.core.transport import WebSocketTransport, AuthenticationMechanism, SimpleLoginPayloadAuth
|
10
|
+
from econagents.core.logging_mixin import LoggerMixin
|
11
|
+
|
12
|
+
|
13
|
+
class AgentManager(LoggerMixin):
|
14
|
+
"""
|
15
|
+
Agent Manager for handling connections, message routing, and event handling.
|
16
|
+
|
17
|
+
The AgentManager provides a high-level interface for connecting to a server,
|
18
|
+
sending messages, and routing received messages to appropriate handlers. It also
|
19
|
+
supports pre- and post-event hooks for intercepting and processing messages.
|
20
|
+
|
21
|
+
Connection parameters (URL and authentication mechanism) can be:
|
22
|
+
|
23
|
+
1. Provided at initialization time
|
24
|
+
|
25
|
+
2. Injected later using property setters:
|
26
|
+
|
27
|
+
- manager.url = "wss://example.com/ws"
|
28
|
+
|
29
|
+
- manager.auth_mechanism = SimpleLoginPayloadAuth()
|
30
|
+
|
31
|
+
- manager.auth_mechanism_kwargs = {"username": "user", "password": "pass"}
|
32
|
+
|
33
|
+
This delayed injection pattern allows for more flexible configuration and testing.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
url (Optional[str]): WebSocket URL to connect to
|
37
|
+
auth_mechanism (Optional[AuthenticationMechanism]): Authentication mechanism
|
38
|
+
auth_mechanism_kwargs (Optional[dict[str, Any]]): Keyword arguments to pass to auth_mechanism
|
39
|
+
logger (Optional[logging.Logger]): Logger instance
|
40
|
+
"""
|
41
|
+
|
42
|
+
def __init__(
|
43
|
+
self,
|
44
|
+
url: Optional[str] = None,
|
45
|
+
auth_mechanism: Optional[AuthenticationMechanism] = None,
|
46
|
+
auth_mechanism_kwargs: Optional[dict[str, Any]] = None,
|
47
|
+
logger: Optional[logging.Logger] = None,
|
48
|
+
):
|
49
|
+
if logger:
|
50
|
+
self.logger = logger
|
51
|
+
|
52
|
+
self._url = url
|
53
|
+
self._auth_mechanism = auth_mechanism
|
54
|
+
self._auth_mechanism_kwargs = auth_mechanism_kwargs
|
55
|
+
self.transport = None
|
56
|
+
self.running = False
|
57
|
+
|
58
|
+
# Dictionary to store event handlers: {event_type: handler_function}
|
59
|
+
self._event_handlers: dict[str, list[Callable[[Message], Any]]] = {}
|
60
|
+
# Handler for all events (will be called for every event)
|
61
|
+
self._global_event_handlers: list[Callable[[Message], Any]] = []
|
62
|
+
|
63
|
+
# Pre and post event hooks
|
64
|
+
# For specific events: {event_type: [hook_functions]}
|
65
|
+
self._pre_event_hooks: dict[str, list[Callable[[Message], Any]]] = {}
|
66
|
+
self._post_event_hooks: dict[str, list[Callable[[Message], Any]]] = {}
|
67
|
+
# For all events
|
68
|
+
self._global_pre_event_hooks: list[Callable[[Message], Any]] = []
|
69
|
+
self._global_post_event_hooks: list[Callable[[Message], Any]] = []
|
70
|
+
|
71
|
+
# Initialize transport if URL is provided
|
72
|
+
if url:
|
73
|
+
self._initialize_transport()
|
74
|
+
|
75
|
+
@property
|
76
|
+
def url(self) -> Optional[str]:
|
77
|
+
"""Get the WebSocket URL."""
|
78
|
+
return self._url
|
79
|
+
|
80
|
+
@url.setter
|
81
|
+
def url(self, value: str):
|
82
|
+
"""
|
83
|
+
Set the WebSocket URL.
|
84
|
+
|
85
|
+
Args:
|
86
|
+
value (str): WebSocket URL to connect to
|
87
|
+
"""
|
88
|
+
self._url = value
|
89
|
+
if self.transport is None:
|
90
|
+
self._initialize_transport()
|
91
|
+
else:
|
92
|
+
self.transport.url = value
|
93
|
+
|
94
|
+
@property
|
95
|
+
def auth_mechanism(self) -> Optional[AuthenticationMechanism]:
|
96
|
+
"""Get the authentication mechanism."""
|
97
|
+
return self._auth_mechanism
|
98
|
+
|
99
|
+
@auth_mechanism.setter
|
100
|
+
def auth_mechanism(self, value: AuthenticationMechanism):
|
101
|
+
"""
|
102
|
+
Set the authentication mechanism.
|
103
|
+
|
104
|
+
Args:
|
105
|
+
value (AuthenticationMechanism): Authentication mechanism
|
106
|
+
"""
|
107
|
+
self._auth_mechanism = value
|
108
|
+
if self.transport is not None:
|
109
|
+
self.transport.auth_mechanism = value
|
110
|
+
|
111
|
+
@property
|
112
|
+
def auth_mechanism_kwargs(self) -> Optional[dict[str, Any]]:
|
113
|
+
"""Get the authentication mechanism keyword arguments."""
|
114
|
+
return self._auth_mechanism_kwargs
|
115
|
+
|
116
|
+
@auth_mechanism_kwargs.setter
|
117
|
+
def auth_mechanism_kwargs(self, value: Optional[dict[str, Any]]):
|
118
|
+
"""
|
119
|
+
Set the authentication mechanism keyword arguments.
|
120
|
+
|
121
|
+
Args:
|
122
|
+
value (Optional[dict[str, Any]]): Keyword arguments to pass to auth_mechanism
|
123
|
+
"""
|
124
|
+
self._auth_mechanism_kwargs = value
|
125
|
+
if self.transport is not None:
|
126
|
+
self.transport.auth_mechanism_kwargs = value
|
127
|
+
|
128
|
+
def _initialize_transport(self):
|
129
|
+
"""Initialize the WebSocketTransport with current configuration."""
|
130
|
+
if not self._url:
|
131
|
+
raise ValueError("URL must be set before initializing transport")
|
132
|
+
|
133
|
+
self.transport = WebSocketTransport(
|
134
|
+
url=self._url,
|
135
|
+
logger=self.logger,
|
136
|
+
on_message_callback=self._raw_message_received,
|
137
|
+
auth_mechanism=self._auth_mechanism,
|
138
|
+
auth_mechanism_kwargs=self._auth_mechanism_kwargs,
|
139
|
+
)
|
140
|
+
|
141
|
+
def _raw_message_received(self, raw_message: str):
|
142
|
+
"""Process raw message from the transport layer"""
|
143
|
+
msg = self._extract_message_data(raw_message)
|
144
|
+
if msg:
|
145
|
+
asyncio.create_task(self.on_message(msg))
|
146
|
+
return None
|
147
|
+
|
148
|
+
def _extract_message_data(self, raw_message: str) -> Optional[Message]:
|
149
|
+
try:
|
150
|
+
msg = json.loads(raw_message)
|
151
|
+
message_type = msg.get("type", "")
|
152
|
+
event_type = msg.get("eventType", "")
|
153
|
+
data = msg.get("data", {})
|
154
|
+
except json.JSONDecodeError:
|
155
|
+
self.logger.error("Invalid JSON received.")
|
156
|
+
return None
|
157
|
+
return Message(message_type=message_type, event_type=event_type, data=data)
|
158
|
+
|
159
|
+
async def on_message(self, message: Message):
|
160
|
+
"""
|
161
|
+
Default implementation to handle incoming messages from the server.
|
162
|
+
|
163
|
+
For event-type messages, routes them to on_event.
|
164
|
+
Subclasses can override this method for custom handling.
|
165
|
+
|
166
|
+
Args:
|
167
|
+
message (Message): Incoming message from the server
|
168
|
+
"""
|
169
|
+
self.logger.debug(f"<-- AgentManager received message: {message}")
|
170
|
+
if message.message_type == "event":
|
171
|
+
await self.on_event(message)
|
172
|
+
|
173
|
+
async def send_message(self, message: str):
|
174
|
+
"""Send a message through the transport layer.
|
175
|
+
|
176
|
+
Args:
|
177
|
+
message (str): Message to send
|
178
|
+
"""
|
179
|
+
if self.transport is None:
|
180
|
+
self.logger.error("Cannot send message: transport not initialized")
|
181
|
+
return
|
182
|
+
await self.transport.send(message)
|
183
|
+
|
184
|
+
async def start(self):
|
185
|
+
"""Start the agent manager and connect to the server."""
|
186
|
+
if self.transport is None:
|
187
|
+
if self.url:
|
188
|
+
self._initialize_transport()
|
189
|
+
else:
|
190
|
+
raise ValueError("URL must be set before starting the agent manager")
|
191
|
+
|
192
|
+
self.running = True
|
193
|
+
connected = await self.transport.connect()
|
194
|
+
if connected:
|
195
|
+
self.logger.info("Connected to WebSocket server. Receiving messages...")
|
196
|
+
await self.transport.start_listening()
|
197
|
+
else:
|
198
|
+
self.logger.error("Failed to connect to WebSocket server")
|
199
|
+
|
200
|
+
async def stop(self):
|
201
|
+
"""Stop the agent manager and close the connection."""
|
202
|
+
self.running = False
|
203
|
+
if self.transport:
|
204
|
+
await self.transport.stop()
|
205
|
+
|
206
|
+
async def on_event(self, message: Message):
|
207
|
+
"""
|
208
|
+
Handle event messages by routing to specific handlers.
|
209
|
+
|
210
|
+
The execution flow is:
|
211
|
+
|
212
|
+
1. Global pre-event hooks
|
213
|
+
|
214
|
+
2. Event-specific pre-event hooks
|
215
|
+
|
216
|
+
3. Global event handlers
|
217
|
+
|
218
|
+
4. Event-specific handlers
|
219
|
+
|
220
|
+
5. Event-specific post-event hooks
|
221
|
+
|
222
|
+
6. Global post-event hooks
|
223
|
+
|
224
|
+
Subclasses can override this method for custom event handling.
|
225
|
+
|
226
|
+
Args:
|
227
|
+
message (Message): Incoming event message from the server
|
228
|
+
"""
|
229
|
+
event_type = message.event_type
|
230
|
+
has_specific_handlers = event_type in self._event_handlers
|
231
|
+
|
232
|
+
# Execute global pre-event hooks
|
233
|
+
await self._execute_hooks(self._global_pre_event_hooks, message, "global pre-event")
|
234
|
+
|
235
|
+
# Execute specific pre-event hooks if they exist
|
236
|
+
if event_type in self._pre_event_hooks:
|
237
|
+
await self._execute_hooks(self._pre_event_hooks[event_type], message, f"{event_type} pre-event")
|
238
|
+
|
239
|
+
# Call global event handlers
|
240
|
+
await self._execute_hooks(self._global_event_handlers, message, "global event")
|
241
|
+
|
242
|
+
# Call specific event handlers if they exist
|
243
|
+
if has_specific_handlers:
|
244
|
+
await self._execute_hooks(self._event_handlers[event_type], message, f"{event_type} event")
|
245
|
+
|
246
|
+
# Execute specific post-event hooks if they exist
|
247
|
+
if event_type in self._post_event_hooks:
|
248
|
+
await self._execute_hooks(self._post_event_hooks[event_type], message, f"{event_type} post-event")
|
249
|
+
|
250
|
+
# Execute global post-event hooks
|
251
|
+
await self._execute_hooks(self._global_post_event_hooks, message, "global post-event")
|
252
|
+
|
253
|
+
async def _execute_hooks(self, hooks: list[Callable], message: Message, hook_type: str) -> None:
|
254
|
+
"""Execute a list of hooks/handlers with proper error handling."""
|
255
|
+
for hook in hooks:
|
256
|
+
try:
|
257
|
+
await self._call_handler(hook, message)
|
258
|
+
except Exception as e:
|
259
|
+
self.logger.error(
|
260
|
+
f"Error in {hook_type} ({hook.__name__}) hook: {e}, message: {message.model_dump()}",
|
261
|
+
extra={
|
262
|
+
"traceback": traceback.format_exc(),
|
263
|
+
},
|
264
|
+
)
|
265
|
+
|
266
|
+
async def _call_handler(self, handler: Callable, message: Message):
|
267
|
+
"""Helper method to call a handler with proper async support"""
|
268
|
+
if callable(handler):
|
269
|
+
result = handler(message)
|
270
|
+
if hasattr(result, "__await__"):
|
271
|
+
await result
|
272
|
+
|
273
|
+
# Event handler registration
|
274
|
+
def register_event_handler(self, event_type: str, handler: Callable[[Message], Any]):
|
275
|
+
"""
|
276
|
+
Register a handler function for a specific event type.
|
277
|
+
|
278
|
+
Args:
|
279
|
+
event_type (str): The type of event to handle
|
280
|
+
handler (Callable[[Message], Any]): Function that takes a Message object and handles the event
|
281
|
+
"""
|
282
|
+
if event_type not in self._event_handlers:
|
283
|
+
self._event_handlers[event_type] = []
|
284
|
+
self._event_handlers[event_type].append(handler)
|
285
|
+
return self # Allow for method chaining
|
286
|
+
|
287
|
+
def register_global_event_handler(self, handler: Callable[[Message], Any]):
|
288
|
+
"""
|
289
|
+
Register a handler function for all events.
|
290
|
+
|
291
|
+
Args:
|
292
|
+
handler (Callable[[Message], Any]): Function that takes a Message object and handles any event
|
293
|
+
"""
|
294
|
+
self._global_event_handlers.append(handler)
|
295
|
+
return self # Allow for method chaining
|
296
|
+
|
297
|
+
# Pre-event hook registration
|
298
|
+
def register_pre_event_hook(self, event_type: str, hook: Callable[[Message], Any]):
|
299
|
+
"""
|
300
|
+
Register a hook to execute before handlers for a specific event type.
|
301
|
+
|
302
|
+
Args:
|
303
|
+
event_type (str): The type of event to hook
|
304
|
+
hook (Callable[[Message], Any]): Function that takes a Message object and runs before handlers
|
305
|
+
"""
|
306
|
+
if event_type not in self._pre_event_hooks:
|
307
|
+
self._pre_event_hooks[event_type] = []
|
308
|
+
self._pre_event_hooks[event_type].append(hook)
|
309
|
+
return self
|
310
|
+
|
311
|
+
def register_global_pre_event_hook(self, hook: Callable[[Message], Any]):
|
312
|
+
"""
|
313
|
+
Register a hook to execute before handlers for all events.
|
314
|
+
|
315
|
+
Args:
|
316
|
+
hook (Callable[[Message], Any]): Function that takes a Message object and runs before any handlers
|
317
|
+
"""
|
318
|
+
self._global_pre_event_hooks.append(hook)
|
319
|
+
return self
|
320
|
+
|
321
|
+
# Post-event hook registration
|
322
|
+
def register_post_event_hook(self, event_type: str, hook: Callable[[Message], Any]):
|
323
|
+
"""
|
324
|
+
Register a hook to execute after handlers for a specific event type.
|
325
|
+
|
326
|
+
Args:
|
327
|
+
event_type (str): The type of event to hook
|
328
|
+
hook (Callable[[Message], Any]): Function that takes a Message object and runs after handlers
|
329
|
+
"""
|
330
|
+
if event_type not in self._post_event_hooks:
|
331
|
+
self._post_event_hooks[event_type] = []
|
332
|
+
self._post_event_hooks[event_type].append(hook)
|
333
|
+
return self
|
334
|
+
|
335
|
+
def register_global_post_event_hook(self, hook: Callable[[Message], Any]):
|
336
|
+
"""
|
337
|
+
Register a hook to execute after handlers for all events.
|
338
|
+
|
339
|
+
Args:
|
340
|
+
hook (Callable[[Message], Any]): Function that takes a Message object and runs after all handlers
|
341
|
+
"""
|
342
|
+
self._global_post_event_hooks.append(hook)
|
343
|
+
return self
|
344
|
+
|
345
|
+
# Unregister handlers
|
346
|
+
def unregister_event_handler(self, event_type: str, handler: Optional[Callable] = None):
|
347
|
+
"""
|
348
|
+
Unregister handler(s) for a specific event type.
|
349
|
+
|
350
|
+
Args:
|
351
|
+
event_type (str): The type of event
|
352
|
+
handler (Optional[Callable]): Optional handler to remove. If None, removes all handlers for this event type.
|
353
|
+
"""
|
354
|
+
if event_type in self._event_handlers:
|
355
|
+
if handler is None:
|
356
|
+
self._event_handlers.pop(event_type)
|
357
|
+
else:
|
358
|
+
self._event_handlers[event_type] = [h for h in self._event_handlers[event_type] if h != handler]
|
359
|
+
return self
|
360
|
+
|
361
|
+
def unregister_global_event_handler(self, handler: Optional[Callable] = None):
|
362
|
+
"""
|
363
|
+
Unregister global event handler(s).
|
364
|
+
|
365
|
+
Args:
|
366
|
+
handler (Optional[Callable]): Optional handler to remove. If None, removes all global handlers.
|
367
|
+
"""
|
368
|
+
if handler is None:
|
369
|
+
self._global_event_handlers.clear()
|
370
|
+
else:
|
371
|
+
self._global_event_handlers = [h for h in self._global_event_handlers if h != handler]
|
372
|
+
return self
|
373
|
+
|
374
|
+
# Unregister pre-event hooks
|
375
|
+
def unregister_pre_event_hook(self, event_type: str, hook: Optional[Callable] = None):
|
376
|
+
"""
|
377
|
+
Unregister pre-event hook(s) for a specific event type.
|
378
|
+
|
379
|
+
Args:
|
380
|
+
event_type (str): The type of event
|
381
|
+
hook (Optional[Callable]): Optional hook to remove. If None, removes all pre-event hooks for this event type.
|
382
|
+
"""
|
383
|
+
if event_type in self._pre_event_hooks:
|
384
|
+
if hook is None:
|
385
|
+
self._pre_event_hooks.pop(event_type)
|
386
|
+
else:
|
387
|
+
self._pre_event_hooks[event_type] = [h for h in self._pre_event_hooks[event_type] if h != hook]
|
388
|
+
return self
|
389
|
+
|
390
|
+
def unregister_global_pre_event_hook(self, hook: Optional[Callable] = None):
|
391
|
+
"""
|
392
|
+
Unregister global pre-event hook(s).
|
393
|
+
|
394
|
+
Args:
|
395
|
+
hook (Optional[Callable]): Optional hook to remove. If None, removes all global pre-event hooks.
|
396
|
+
"""
|
397
|
+
if hook is None:
|
398
|
+
self._global_pre_event_hooks.clear()
|
399
|
+
else:
|
400
|
+
self._global_pre_event_hooks = [h for h in self._global_pre_event_hooks if h != hook]
|
401
|
+
return self
|
402
|
+
|
403
|
+
# Unregister post-event hooks
|
404
|
+
def unregister_post_event_hook(self, event_type: str, hook: Optional[Callable] = None):
|
405
|
+
"""
|
406
|
+
Unregister post-event hook(s) for a specific event type.
|
407
|
+
|
408
|
+
Args:
|
409
|
+
event_type (str): The type of event
|
410
|
+
hook (Optional[Callable]): Optional hook to remove. If None, removes all post-event hooks for this event type.
|
411
|
+
"""
|
412
|
+
if event_type in self._post_event_hooks:
|
413
|
+
if hook is None:
|
414
|
+
self._post_event_hooks.pop(event_type)
|
415
|
+
else:
|
416
|
+
self._post_event_hooks[event_type] = [h for h in self._post_event_hooks[event_type] if h != hook]
|
417
|
+
return self
|
418
|
+
|
419
|
+
def unregister_global_post_event_hook(self, hook: Optional[Callable] = None):
|
420
|
+
"""
|
421
|
+
Unregister global post-event hook(s).
|
422
|
+
|
423
|
+
Args:
|
424
|
+
hook (Optional[Callable]): Optional hook to remove. If None, removes all global post-event hooks.
|
425
|
+
"""
|
426
|
+
if hook is None:
|
427
|
+
self._global_post_event_hooks.clear()
|
428
|
+
else:
|
429
|
+
self._global_post_event_hooks = [h for h in self._global_post_event_hooks if h != hook]
|
430
|
+
return self
|