slim-bindings 0.3.6__cp310-cp310-macosx_10_12_x86_64.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.

Potentially problematic release.


This version of slim-bindings might be problematic. Click here for more details.

@@ -0,0 +1,708 @@
1
+ # Copyright AGNTCY Contributors (https://github.com/agntcy)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import asyncio
5
+ from typing import Optional
6
+
7
+ from ._slim_bindings import ( # type: ignore[attr-defined]
8
+ SESSION_UNSPECIFIED,
9
+ PyAgentType,
10
+ PyService,
11
+ PySessionConfiguration,
12
+ PySessionInfo,
13
+ PySessionType,
14
+ __version__,
15
+ build_info,
16
+ build_profile,
17
+ connect,
18
+ create_pyservice,
19
+ create_session,
20
+ delete_session,
21
+ disconnect,
22
+ get_default_session_config,
23
+ get_session_config,
24
+ publish,
25
+ receive,
26
+ remove_route,
27
+ run_server,
28
+ set_default_session_config,
29
+ set_route,
30
+ set_session_config,
31
+ stop_server,
32
+ subscribe,
33
+ unsubscribe,
34
+ )
35
+ from ._slim_bindings import (
36
+ PySessionDirection as PySessionDirection,
37
+ )
38
+ from ._slim_bindings import (
39
+ init_tracing as init_tracing,
40
+ )
41
+
42
+
43
+ def get_version():
44
+ """
45
+ Get the version of the SLIM bindings.
46
+
47
+ Returns:
48
+ str: The version of the SLIM bindings.
49
+ """
50
+ return __version__
51
+
52
+
53
+ def get_build_profile():
54
+ """
55
+ Get the build profile of the SLIM bindings.
56
+
57
+ Returns:
58
+ str: The build profile of the SLIM bindings.
59
+ """
60
+ return build_profile
61
+
62
+
63
+ def get_build_info():
64
+ """
65
+ Get the build information of the SLIM bindings.
66
+
67
+ Returns:
68
+ str: The build information of the SLIM bindings.
69
+ """
70
+ return build_info
71
+
72
+
73
+ class SLIMTimeoutError(TimeoutError):
74
+ """
75
+ Exception raised for SLIM timeout errors.
76
+
77
+ This exception is raised when an operation in an SLIM session times out.
78
+ It encapsulates detailed information about the timeout event, including the
79
+ ID of the message that caused the timeout and the session identifier. An
80
+ optional underlying exception can also be provided to offer additional context.
81
+
82
+ Attributes:
83
+ message_id (int): The identifier associated with the message triggering the timeout.
84
+ session_id (int): The identifier of the session where the timeout occurred.
85
+ message (str): A brief description of the timeout error.
86
+ original_exception (Exception, optional): The underlying exception that caused the timeout, if any.
87
+
88
+ The string representation of the exception (via __str__) returns a full message that
89
+ includes the custom message, session ID, and message ID, as well as details of the
90
+ original exception (if present). This provides a richer context when the exception is logged
91
+ or printed.
92
+ """
93
+
94
+ def __init__(
95
+ self,
96
+ message_id: int,
97
+ session_id: int,
98
+ message: str = "SLIM timeout error",
99
+ original_exception: Optional[Exception] = None,
100
+ ):
101
+ self.message_id = message_id
102
+ self.session_id = session_id
103
+ self.message = message
104
+ self.original_exception = original_exception
105
+ full_message = f"{message} for session {session_id} and message {message_id}"
106
+ if original_exception:
107
+ full_message = f"{full_message}. Caused by: {original_exception!r}"
108
+ super().__init__(full_message)
109
+
110
+ def __str__(self):
111
+ return self.args[0]
112
+
113
+ def __repr__(self):
114
+ return (
115
+ f"{self.__class__.__name__}(session_id={self.session_id!r}, "
116
+ f"message_id={self.message_id!r}, "
117
+ f"message={self.message!r}, original_exception={self.original_exception!r})"
118
+ )
119
+
120
+
121
+ class Slim:
122
+ def __init__(
123
+ self,
124
+ svc: PyService,
125
+ organization: str,
126
+ namespace: str,
127
+ agent: str,
128
+ ):
129
+ """
130
+ Initialize a new SLIM instance. A SLIM instance is associated with a single
131
+ local agent. The agent is identified by its organization, namespace, and name.
132
+ The agent ID is determined by the provided service (svc).
133
+
134
+ Args:
135
+ svc (PyService): The Python service instance for SLIM.
136
+ organization (str): The organization of the agent.
137
+ namespace (str): The namespace of the agent.
138
+ agent (str): The name of the agent.
139
+ """
140
+
141
+ # Initialize service
142
+ self.svc = svc
143
+
144
+ # Create sessions map
145
+ self.sessions: dict[int, tuple[Optional[PySessionInfo], asyncio.Queue]] = {
146
+ SESSION_UNSPECIFIED: (None, asyncio.Queue()),
147
+ }
148
+
149
+ # Save local names
150
+ self.local_name = PyAgentType(organization, namespace, agent)
151
+ self.local_id = self.svc.id
152
+
153
+ # Create connection ID map
154
+ self.conn_ids: dict[str, int] = {}
155
+
156
+ async def __aenter__(self):
157
+ """
158
+ Start the receiver loop in the background.
159
+ This function is called when the SLIM instance is used in a
160
+ context manager (with statement).
161
+ It will start the receiver loop in the background and return the
162
+ SLIM instance.
163
+ Args:
164
+ None
165
+ Returns:
166
+ Slim: The SLIM instance.
167
+
168
+ """
169
+
170
+ # Run receiver loop in the background
171
+ self.task = asyncio.create_task(self._receive_loop())
172
+ return self
173
+
174
+ async def __aexit__(self, exc_type, exc_value, traceback):
175
+ """
176
+ Stop the receiver loop.
177
+ This function is called when the Slim instance is used in a
178
+ context manager (with statement).
179
+ It will stop the receiver loop and wait for it to finish.
180
+ Args:
181
+ exc_type: The exception type.
182
+ exc_value: The exception value.
183
+ traceback: The traceback object.
184
+ Returns:
185
+ None
186
+ """
187
+
188
+ # Cancel the receiver loop task
189
+ self.task.cancel()
190
+
191
+ # Wait for the task to finish
192
+ try:
193
+ await self.task
194
+ except asyncio.CancelledError:
195
+ pass
196
+
197
+ @classmethod
198
+ async def new(
199
+ cls,
200
+ organization: str,
201
+ namespace: str,
202
+ agent: str,
203
+ agent_id: Optional[int] = None,
204
+ ) -> "Slim":
205
+ """
206
+ Create a new SLIM instance. A SLIM instamce is associated to one single
207
+ local agent. The agent is identified by its organization, namespace and name.
208
+ The agent ID is optional. If not provided, the agent will be created with a new ID.
209
+
210
+ Args:
211
+ organization (str): The organization of the agent.
212
+ namespace (str): The namespace of the agent.
213
+ agent (str): The name of the agent.
214
+ agent_id (int): The ID of the agent. If not provided, a new ID will be created.
215
+
216
+ Returns:
217
+ Slim: A new SLIM instance
218
+ """
219
+
220
+ return cls(
221
+ await create_pyservice(organization, namespace, agent, agent_id),
222
+ organization,
223
+ namespace,
224
+ agent,
225
+ )
226
+
227
+ def get_agent_id(self) -> int:
228
+ """
229
+ Get the ID of the agent.
230
+
231
+ Args:
232
+ None
233
+
234
+ Returns:
235
+ int: The ID of the agent.
236
+ """
237
+
238
+ return self.svc.id
239
+
240
+ async def create_session(
241
+ self,
242
+ session_config: PySessionConfiguration,
243
+ queue_size: int = 0,
244
+ ) -> PySessionInfo:
245
+ """
246
+ Create a new streaming session.
247
+
248
+ Args:
249
+ session_config (PySessionConfiguration): The session configuration.
250
+ queue_size (int): The size of the queue for the session.
251
+ If 0, the queue will be unbounded.
252
+ If a positive integer, the queue will be bounded to that size.
253
+
254
+ Returns:
255
+ ID of the session
256
+ """
257
+
258
+ session = await create_session(self.svc, session_config)
259
+ self.sessions[session.id] = (session, asyncio.Queue(queue_size))
260
+ return session
261
+
262
+ async def delete_session(self, session_id: int):
263
+ """
264
+ Delete a session.
265
+
266
+ Args:
267
+ session_id (int): The ID of the session to delete.
268
+
269
+ Returns:
270
+ None
271
+
272
+ Raises:
273
+ ValueError: If the session ID is not found.
274
+ """
275
+
276
+ # Check if the session ID is in the sessions map
277
+ if session_id not in self.sessions:
278
+ raise ValueError(f"session not found: {session_id}")
279
+
280
+ # Remove the session from the map
281
+ del self.sessions[session_id]
282
+
283
+ # Remove the session from SLIM
284
+ await delete_session(self.svc, session_id)
285
+
286
+ async def set_session_config(
287
+ self,
288
+ session_id: int,
289
+ session_config: PySessionConfiguration,
290
+ ):
291
+ """
292
+ Set the session configuration for a specific session.
293
+
294
+ Args:
295
+ session_id (int): The ID of the session.
296
+ session_config (PySessionConfiguration): The new configuration for the session.
297
+
298
+ Returns:
299
+ None
300
+
301
+ Raises:
302
+ ValueError: If the session ID is not found.
303
+ """
304
+
305
+ # Check if the session ID is in the sessions map
306
+ if session_id not in self.sessions:
307
+ raise ValueError(f"session not found: {session_id}")
308
+
309
+ # Set the session configuration
310
+ await set_session_config(self.svc, session_id, session_config)
311
+
312
+ async def get_session_config(
313
+ self,
314
+ session_id: int,
315
+ ) -> PySessionConfiguration:
316
+ """
317
+ Get the session configuration for a specific session.
318
+
319
+ Args:
320
+ session_id (int): The ID of the session.
321
+
322
+ Returns:
323
+ PySessionConfiguration: The configuration of the session.
324
+
325
+ Raises:
326
+ ValueError: If the session ID is not found.
327
+ """
328
+
329
+ # Check if the session ID is in the sessions map
330
+ if session_id not in self.sessions:
331
+ raise ValueError(f"session not found: {session_id}")
332
+
333
+ # Get the session configuration
334
+ return await get_session_config(self.svc, session_id)
335
+
336
+ async def set_default_session_config(
337
+ self,
338
+ session_config: PySessionConfiguration,
339
+ ):
340
+ """
341
+ Set the default session configuration.
342
+
343
+ Args:
344
+ session_config (PySessionConfiguration): The new default session configuration.
345
+
346
+ Returns:
347
+ None
348
+ """
349
+
350
+ await set_default_session_config(self.svc, session_config)
351
+
352
+ async def get_default_session_config(
353
+ self,
354
+ session_type: PySessionType,
355
+ ) -> PySessionConfiguration:
356
+ """
357
+ Get the default session configuration.
358
+
359
+ Args:
360
+ session_id (int): The ID of the session.
361
+
362
+ Returns:
363
+ PySessionConfiguration: The default configuration of the session.
364
+ """
365
+
366
+ return await get_default_session_config(self.svc, session_type)
367
+
368
+ async def run_server(self, config: dict):
369
+ """
370
+ Start the server part of the SLIM service. The server will be started only
371
+ if its configuration is set. Otherwise, it will raise an error.
372
+
373
+ Args:
374
+ None
375
+
376
+ Returns:
377
+ None
378
+ """
379
+
380
+ await run_server(self.svc, config)
381
+
382
+ async def stop_server(self, endpoint: str):
383
+ """
384
+ Stop the server part of the SLIM service.
385
+
386
+ Args:
387
+ None
388
+
389
+ Returns:
390
+ None
391
+ """
392
+
393
+ await stop_server(self.svc, endpoint)
394
+
395
+ async def connect(self, client_config: dict) -> int:
396
+ """
397
+ Connect to a remote SLIM service.
398
+ This function will block until the connection is established.
399
+
400
+ Args:
401
+ None
402
+
403
+ Returns:
404
+ int: The connection ID.
405
+ """
406
+
407
+ conn_id = await connect(
408
+ self.svc,
409
+ client_config,
410
+ )
411
+
412
+ # Save the connection ID
413
+ self.conn_ids[client_config["endpoint"]] = conn_id
414
+
415
+ # For the moment we manage one connection only
416
+ self.conn_id = conn_id
417
+
418
+ # Subscribe to the local name
419
+ await subscribe(self.svc, conn_id, self.local_name, self.local_id)
420
+
421
+ # return the connection ID
422
+ return conn_id
423
+
424
+ async def disconnect(self, endpoint: str):
425
+ """
426
+ Disconnect from a remote SLIM service.
427
+ This function will block until the disconnection is complete.
428
+
429
+ Args:
430
+ None
431
+
432
+ Returns:
433
+ None
434
+
435
+ """
436
+ conn = self.conn_ids[endpoint]
437
+ await disconnect(self.svc, conn)
438
+
439
+ async def set_route(
440
+ self,
441
+ organization: str,
442
+ namespace: str,
443
+ agent: str,
444
+ id: Optional[int] = None,
445
+ ):
446
+ """
447
+ Set route for outgoing messages via the connected SLIM instance.
448
+
449
+ Args:
450
+ organization (str): The organization of the agent.
451
+ namespace (str): The namespace of the agent.
452
+ agent (str): The name of the agent.
453
+ id (int): Optional ID of the agent.
454
+
455
+ Returns:
456
+ None
457
+ """
458
+
459
+ name = PyAgentType(organization, namespace, agent)
460
+ await set_route(self.svc, self.conn_id, name, id)
461
+
462
+ async def remove_route(
463
+ self, organization: str, namespace: str, agent: str, id: Optional[int] = None
464
+ ):
465
+ """
466
+ Remove route for outgoing messages via the connected SLIM instance.
467
+
468
+ Args:
469
+ organization (str): The organization of the agent.
470
+ namespace (str): The namespace of the agent.
471
+ agent (str): The name of the agent.
472
+ id (int): Optional ID of the agent.
473
+
474
+ Returns:
475
+ None
476
+ """
477
+
478
+ name = PyAgentType(organization, namespace, agent)
479
+ await remove_route(self.svc, self.conn_id, name, id)
480
+
481
+ async def subscribe(
482
+ self, organization: str, namespace: str, agent: str, id: Optional[int] = None
483
+ ):
484
+ """
485
+ Subscribe to receive messages for the given agent.
486
+
487
+ Args:
488
+ organization (str): The organization of the agent.
489
+ namespace (str): The namespace of the agent.
490
+ agent (str): The name of the agent.
491
+ id (int): Optional ID of the agent.
492
+
493
+ Returns:
494
+ None
495
+ """
496
+
497
+ sub = PyAgentType(organization, namespace, agent)
498
+ await subscribe(self.svc, self.conn_id, sub, id)
499
+
500
+ async def unsubscribe(
501
+ self, organization: str, namespace: str, agent: str, id: Optional[int] = None
502
+ ):
503
+ """
504
+ Unsubscribe from receiving messages for the given agent.
505
+
506
+ Args:
507
+ organization (str): The organization of the agent.
508
+ namespace (str): The namespace of the agent.
509
+ agent (str): The name of the agent.
510
+ id (int): Optional ID of the agent.
511
+
512
+ Returns:
513
+ None
514
+ """
515
+
516
+ unsub = PyAgentType(organization, namespace, agent)
517
+ await unsubscribe(self.svc, self.conn_id, unsub, id)
518
+
519
+ async def publish(
520
+ self,
521
+ session: PySessionInfo,
522
+ msg: bytes,
523
+ organization: str,
524
+ namespace: str,
525
+ agent: str,
526
+ agent_id: Optional[int] = None,
527
+ ):
528
+ """
529
+ Publish a message to an agent via normal matching in subscription table.
530
+
531
+ Args:
532
+ session (PySessionInfo): The session information.
533
+ msg (str): The message to publish.
534
+ organization (str): The organization of the agent.
535
+ namespace (str): The namespace of the agent.
536
+ agent (str): The name of the agent.
537
+ agent_id (int): Optional ID of the agent.
538
+
539
+ Returns:
540
+ None
541
+ """
542
+
543
+ # Make sure the sessions exists
544
+ if session.id not in self.sessions:
545
+ raise Exception("session not found", session.id)
546
+
547
+ dest = PyAgentType(organization, namespace, agent)
548
+ await publish(self.svc, session, 1, msg, dest, agent_id)
549
+
550
+ async def request_reply(
551
+ self,
552
+ session: PySessionInfo,
553
+ msg: bytes,
554
+ organization: str,
555
+ namespace: str,
556
+ agent: str,
557
+ agent_id: Optional[int] = None,
558
+ ) -> tuple[PySessionInfo, Optional[bytes]]:
559
+ """
560
+ Publish a message and wait for the first response.
561
+
562
+ Args:
563
+ msg (str): The message to publish.
564
+ session (PySessionInfo): The session information.
565
+ organization (str): The organization of the agent.
566
+ namespace (str): The namespace of the agent.
567
+ agent (str): The name of the agent.
568
+ agent_id (int): Optional ID of the agent.
569
+
570
+ Returns:
571
+ tuple: The PySessionInfo and the message.
572
+ """
573
+
574
+ # Make sure the sessions exists
575
+ if session.id not in self.sessions:
576
+ raise Exception("Session ID not found")
577
+
578
+ dest = PyAgentType(organization, namespace, agent)
579
+ await publish(self.svc, session, 1, msg, dest, agent_id)
580
+
581
+ # Wait for a reply in the corresponding session queue
582
+ session_info, message = await self.receive(session.id)
583
+
584
+ return session_info, message
585
+
586
+ async def publish_to(self, session, msg):
587
+ """
588
+ Publish a message back to the agent that sent it.
589
+ The information regarding the source agent is stored in the session.
590
+
591
+ Args:
592
+ session (PySessionInfo): The session information.
593
+ msg (str): The message to publish.
594
+
595
+ Returns:
596
+ None
597
+ """
598
+
599
+ await publish(self.svc, session, 1, msg)
600
+
601
+ async def receive(
602
+ self, session: Optional[int] = None
603
+ ) -> tuple[PySessionInfo, Optional[bytes]]:
604
+ """
605
+ Receive a message , optionally waiting for a specific session ID.
606
+ If session ID is None, it will wait for new sessions to be created.
607
+ This function will block until a message is received (if the session id is specified)
608
+ or until a new session is created (if the session id is None).
609
+
610
+ Args:
611
+ session (int): The session ID. If None, the function will wait for any message.
612
+
613
+ Returns:
614
+ tuple: The PySessionInfo and the message.
615
+
616
+ Raise:
617
+ Exception: If the session ID is not found.
618
+ """
619
+
620
+ # If session is None, wait for any message
621
+ if session is None:
622
+ return await self.sessions[SESSION_UNSPECIFIED][1].get()
623
+ else:
624
+ # Check if the session ID is in the sessions map
625
+ if session not in self.sessions:
626
+ raise Exception("Session ID not found")
627
+
628
+ # Get the queue for the session
629
+ queue = self.sessions[session][1]
630
+
631
+ # Wait for a message from the queue
632
+ ret = await queue.get()
633
+
634
+ # If message is am exception, raise it
635
+ if isinstance(ret, Exception):
636
+ raise ret
637
+
638
+ # Otherwise, return the message
639
+ return ret
640
+
641
+ async def _receive_loop(self) -> None:
642
+ """
643
+ Receive messages in a loop running in the background.
644
+
645
+ Returns:
646
+ None
647
+ """
648
+
649
+ while True:
650
+ try:
651
+ session_info_msg = await receive(self.svc)
652
+
653
+ id: int = session_info_msg[0].id
654
+
655
+ # Check if the session ID is in the sessions map
656
+ if id not in self.sessions:
657
+ # Create the entry in the sessions map
658
+ self.sessions[id] = (
659
+ session_info_msg,
660
+ asyncio.Queue(),
661
+ )
662
+
663
+ # Also add a queue for the session
664
+ await self.sessions[SESSION_UNSPECIFIED][1].put(session_info_msg)
665
+
666
+ await self.sessions[id][1].put(session_info_msg)
667
+ except asyncio.CancelledError:
668
+ raise
669
+ except Exception as e:
670
+ print("Error receiving message:", e)
671
+ # Try to parse the error message
672
+ try:
673
+ message_id, session_id, reason = parse_error_message(str(e))
674
+
675
+ # figure out what exception to raise based on the reason
676
+ if reason == "timeout":
677
+ err = SLIMTimeoutError(message_id, session_id)
678
+ else:
679
+ # we don't know the reason, just raise the original exception
680
+ raise e
681
+
682
+ if session_id in self.sessions:
683
+ await self.sessions[session_id][1].put(
684
+ err,
685
+ )
686
+ else:
687
+ print(self.sessions.keys())
688
+ except Exception:
689
+ raise e
690
+
691
+
692
+ def parse_error_message(error_message):
693
+ import re
694
+
695
+ # Define the regular expression pattern
696
+ pattern = r"message=(\d+) session=(\d+): (.+)"
697
+
698
+ # Use re.search to find the pattern in the string
699
+ match = re.search(pattern, error_message)
700
+
701
+ if match:
702
+ # Extract message_id, session_id, and reason from the match groups
703
+ message_id = match.group(1)
704
+ session_id = match.group(2)
705
+ reason = match.group(3)
706
+ return int(message_id), int(session_id), reason
707
+ else:
708
+ raise ValueError("error message does not match the expected format.")