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,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