genai-protocol-lite 1.0.0__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,226 @@
1
+ import asyncio
2
+ import json
3
+ import socket
4
+ import struct
5
+ import time
6
+ import logging
7
+ from typing import Callable, Tuple, Dict, Any, Optional
8
+
9
+ from AIConnector.common.network import NetworkConfig
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class DiscoveryProtocol(asyncio.DatagramProtocol):
15
+ """
16
+ Datagram protocol for handling UDP discovery messages.
17
+ """
18
+
19
+ def __init__(self, on_message_callback: Callable[[Dict[str, Any], Tuple[str, int]], None]) -> None:
20
+ """
21
+ Initialize the protocol with a callback for processing received messages.
22
+
23
+ Args:
24
+ on_message_callback: Callback function invoked with the parsed message and sender's address.
25
+ """
26
+ self.on_message_callback = on_message_callback
27
+
28
+ def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None:
29
+ """
30
+ Process an incoming UDP datagram.
31
+
32
+ Args:
33
+ data: The received data as bytes.
34
+ addr: A tuple containing the sender's IP address and port.
35
+ """
36
+ try:
37
+ msg = json.loads(data.decode("utf-8"))
38
+ # Process only DISCOVERY messages with a valid client_id.
39
+ if msg.get("type") == "DISCOVERY" and msg.get("client_id"):
40
+ asyncio.create_task(self.on_message_callback(msg, addr))
41
+ except Exception as e:
42
+ logger.debug(f"[DiscoveryProtocol] UDP parsing error: {e}")
43
+
44
+
45
+ class DiscoveryService:
46
+ """
47
+ Service for broadcasting and listening for peer discovery messages using multicast UDP.
48
+
49
+ This service supports events for when peers appear (are discovered) and when peers are lost.
50
+ """
51
+
52
+ _PEER_DISAPPEAR_TIMEOUT = 30 # Seconds after which a peer is considered lost.
53
+
54
+ def __init__(
55
+ self,
56
+ on_peer_discovered_callback: Callable[[Dict[str, Any], Tuple[str, int]], None],
57
+ client_name: str,
58
+ port: int,
59
+ network_config: NetworkConfig,
60
+ on_peer_lost: Optional[Callable[[str], None]] = None,
61
+ ) -> None:
62
+ """
63
+ Initialize the DiscoveryService.
64
+
65
+ Args:
66
+ on_peer_discovered_callback: Callback invoked when a peer is discovered.
67
+ client_name: Display name of the client.
68
+ port: Port number used by the service.
69
+ network_config: Network configuration settings.
70
+ on_peer_lost: Optional callback invoked when a peer is lost.
71
+ """
72
+ self.on_peer_discovered_callback = on_peer_discovered_callback
73
+ self.on_peer_lost_callback = on_peer_lost
74
+
75
+ self.transport: Optional[asyncio.DatagramTransport] = None
76
+ self.running: bool = False
77
+ self.client_name: str = client_name
78
+ self.network_config: NetworkConfig = network_config
79
+ self.port: int = port
80
+
81
+ # Store peer information: key is client_id, value is a dict with last_seen timestamp and message data.
82
+ self.peers: Dict[str, Dict[str, Any]] = {}
83
+
84
+ self._cleanup_task: Optional[asyncio.Task] = None
85
+
86
+ @staticmethod
87
+ def get_socket(advertisement_port: int, advertisement_ip: str) -> socket.socket:
88
+ """
89
+ Create and configure a multicast UDP socket.
90
+
91
+ Args:
92
+ advertisement_port: The port to bind for receiving multicast messages.
93
+ advertisement_ip: The multicast IP address.
94
+
95
+ Returns:
96
+ A configured, non-blocking UDP socket.
97
+ """
98
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
99
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
100
+ try:
101
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
102
+ except AttributeError:
103
+ pass
104
+ sock.bind(("", advertisement_port))
105
+ mreq = struct.pack("4sl", socket.inet_aton(advertisement_ip), socket.INADDR_ANY)
106
+ sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
107
+ sock.setblocking(False)
108
+ return sock
109
+
110
+ async def start(self, allow_discovery: bool = True) -> None:
111
+ """
112
+ Start the discovery service and begin periodic announcements.
113
+
114
+ Args:
115
+ allow_discovery: If True, the service actively announces and listens for discovery messages.
116
+ """
117
+ self.running = True
118
+
119
+ loop = asyncio.get_event_loop()
120
+ sock = self.get_socket(
121
+ self.network_config.advertisement_port,
122
+ self.network_config.advertisement_ip,
123
+ )
124
+
125
+ self.transport, _ = await loop.create_datagram_endpoint(
126
+ lambda: DiscoveryProtocol(self._process_discovery_message),
127
+ sock=sock
128
+ )
129
+ self._cleanup_task = asyncio.create_task(self._cleanup_stale_peers())
130
+
131
+ if allow_discovery:
132
+ asyncio.create_task(self._periodic_announce())
133
+
134
+ async def stop(self) -> None:
135
+ """
136
+ Stop the discovery service and close the underlying transport.
137
+ """
138
+ self.running = False
139
+ if self.transport:
140
+ self.transport.close()
141
+ if self._cleanup_task:
142
+ self._cleanup_task.cancel()
143
+ try:
144
+ await self._cleanup_task
145
+ except asyncio.CancelledError:
146
+ pass
147
+
148
+ async def _periodic_announce(self) -> None:
149
+ """
150
+ Periodically send discovery messages via multicast.
151
+ """
152
+ while self.running:
153
+ try:
154
+ msg = {
155
+ "type": "DISCOVERY",
156
+ "client_id": self.network_config.client_id,
157
+ "display_name": self.client_name,
158
+ "port": self.port,
159
+ }
160
+ encoded = json.dumps(msg).encode("utf-8")
161
+ self.transport.sendto(
162
+ encoded,
163
+ (
164
+ self.network_config.advertisement_ip,
165
+ self.network_config.advertisement_port,
166
+ )
167
+ )
168
+ except Exception as e:
169
+ logger.debug(f"[DiscoveryService] Sending error: {e}")
170
+ await asyncio.sleep(self.network_config.advertisement_interval)
171
+
172
+ async def _process_discovery_message(self, msg: Dict[str, Any], addr: Tuple[str, int]) -> None:
173
+ """
174
+ Process an incoming discovery message.
175
+
176
+ Updates the last_seen timestamp for peers and invokes the on_peer_discovered_callback.
177
+
178
+ Args:
179
+ msg: The parsed discovery message.
180
+ addr: The sender's address.
181
+ """
182
+ peer_id = msg.get("client_id")
183
+ if not peer_id:
184
+ return
185
+
186
+ # Update or add peer information.
187
+ self.peers[peer_id] = {
188
+ "last_seen": time.time(),
189
+ "msg": msg,
190
+ "addr": addr,
191
+ }
192
+
193
+ if self.on_peer_discovered_callback:
194
+ try:
195
+ result = self.on_peer_discovered_callback(msg, addr)
196
+ if asyncio.iscoroutine(result):
197
+ await result
198
+ except Exception as e:
199
+ logger.error(f"[DiscoveryService] Error in on_peer_discovered_callback: {e}")
200
+
201
+ async def _cleanup_stale_peers(self) -> None:
202
+ """
203
+ Periodically check the list of discovered peers.
204
+
205
+ If a peer's last update is older than _PEER_DISAPPEAR_TIMEOUT seconds,
206
+ it is considered lost and removed, triggering the on_peer_lost_callback.
207
+ """
208
+ while self.running:
209
+ try:
210
+ now = time.time()
211
+ stale_peers = [
212
+ peer_id for peer_id, info in self.peers.items()
213
+ if now - info.get("last_seen", now) > self._PEER_DISAPPEAR_TIMEOUT
214
+ ]
215
+ for peer_id in stale_peers:
216
+ del self.peers[peer_id]
217
+ if self.on_peer_lost_callback:
218
+ try:
219
+ result = self.on_peer_lost_callback(peer_id)
220
+ if asyncio.iscoroutine(result):
221
+ await result
222
+ except Exception as e:
223
+ logger.error(f"[DiscoveryService] Error in on_peer_lost callback: {e}")
224
+ except Exception as e:
225
+ logger.error(f"[DiscoveryService] Error in _cleanup_stale_peers: {e}")
226
+ await asyncio.sleep(1)
AIConnector/session.py ADDED
@@ -0,0 +1,274 @@
1
+ import asyncio
2
+ import uuid
3
+ import json
4
+ from typing import Any, Dict, Optional, Callable, Awaitable, List, Tuple
5
+ from AIConnector.common.logger import Logger
6
+ from AIConnector.common.exceptions import ParameterException
7
+ from AIConnector.common.network import NetworkConfig, get_available_port, Config
8
+ from AIConnector.core.chat_client import ChatClient
9
+
10
+
11
+ class Session:
12
+ """
13
+ Represents a session for a client. This class manages configuration, unique client ID generation,
14
+ and initializes a ChatClient to handle peer-to-peer messaging.
15
+ """
16
+
17
+ FILENAME = "client_id.json"
18
+ ALLOWED_LOG_LEVELS = ("debug", "info", "warning", "error", "critical")
19
+
20
+ def __init__(
21
+ self,
22
+ client_name: str,
23
+ config: Config,
24
+ client_id: Optional[str] = None,
25
+ log_level: Optional[str] = "critical"
26
+ ) -> None:
27
+ """
28
+ Initialize a new Session.
29
+
30
+ Args:
31
+ client_name (str): The display name of the client.
32
+ config (Config): Network configuration settings.
33
+ client_id (Optional[str]): Optional pre-defined client identifier.
34
+ log_level (Optional[str]): Logging level. Defaults to "info".
35
+ """
36
+ self.client_name = client_name
37
+ self.__config = config
38
+ self.__client_id = client_id
39
+
40
+ self.chat_client: Optional[ChatClient] = None
41
+
42
+ # Callback lists for various peer events.
43
+ self._peer_connected_callbacks: List[Callable[[str], Awaitable[None]]] = []
44
+ self._peer_disconnected_callbacks: List[Callable[[str], Awaitable[None]]] = []
45
+ self._peer_discovered_callbacks: List[Callable[[str], Awaitable[None]]] = []
46
+ self._peer_lost_callbacks: List[Callable[[str], Awaitable[None]]] = []
47
+
48
+ self._send_lock = asyncio.Lock()
49
+
50
+ if log_level.lower() not in self.ALLOWED_LOG_LEVELS:
51
+ raise ParameterException(
52
+ f"Invalid log level '{log_level}'. Allowed values: {self.ALLOWED_LOG_LEVELS}"
53
+ )
54
+
55
+ Logger(log_level=log_level)
56
+
57
+ def __get_client_id(self) -> str:
58
+ """
59
+ Retrieve a unique client identifier from file, or generate and store a new one if necessary.
60
+
61
+ Returns:
62
+ str: The client identifier.
63
+ """
64
+ if self.__client_id:
65
+ return self.__client_id
66
+
67
+ try:
68
+ with open(self.FILENAME, "r", encoding="utf-8") as f:
69
+ data = json.load(f)
70
+ except FileNotFoundError:
71
+ data = {}
72
+
73
+ if stored_client_id := data.get(self.client_name):
74
+ self.__client_id = stored_client_id
75
+ return stored_client_id
76
+
77
+ # Generate a new client ID if not found.
78
+ new_id = str(uuid.uuid4())
79
+ data[self.client_name] = new_id
80
+
81
+ with open(self.FILENAME, "w", encoding="utf-8") as f:
82
+ json.dump(data, f, indent=4)
83
+
84
+ self.__client_id = new_id
85
+ return new_id
86
+
87
+ @property
88
+ def client_id(self) -> str:
89
+ """
90
+ Get the unique client identifier, generating it if needed.
91
+
92
+ Returns:
93
+ str: The client identifier.
94
+ """
95
+ if self.__client_id is None:
96
+ self.__client_id = self.__get_client_id()
97
+ return self.__client_id
98
+
99
+ @property
100
+ def config(self) -> NetworkConfig:
101
+ """
102
+ Retrieve the network configuration, ensuring the client_id is updated.
103
+
104
+ Returns:
105
+ NetworkConfig: The network configuration.
106
+ """
107
+ network_config = NetworkConfig(**self.__config.__dict__)
108
+ network_config.client_id = self.client_id
109
+ return network_config
110
+
111
+ async def start(self, message_handler: Optional[Callable[[str], Awaitable[str]]] = None) -> None:
112
+ """
113
+ Start the session by initializing and starting the ChatClient.
114
+ Optionally bind a message handler for incoming messages.
115
+
116
+ Args:
117
+ message_handler (Optional[Callable[[str], Awaitable[str]]]): Async function to process incoming messages.
118
+ """
119
+ self.chat_client = ChatClient(
120
+ client_name=self.client_name,
121
+ client_id=self.client_id,
122
+ network_config=self.config
123
+ )
124
+ self._register_chat_client_on_peer_connected_callback()
125
+ self._register_chat_client_on_peer_disconnected_callback()
126
+ self._register_chat_client_on_peer_discovered_callback()
127
+ self._register_chat_client_on_peer_lost_callback()
128
+
129
+ if message_handler:
130
+ await self.bind(message_handler)
131
+
132
+ # Attempt to start the chat client; if the port is unavailable, assign a new one.
133
+ while True:
134
+ try:
135
+ await self.chat_client.start()
136
+ break
137
+ except OSError:
138
+ self.config.webserver_port = get_available_port(
139
+ self.config.webserver_port_min,
140
+ self.config.webserver_port_max
141
+ )
142
+
143
+ async def stop(self) -> None:
144
+ """
145
+ Stop the session by stopping the ChatClient.
146
+ """
147
+ await self.chat_client.stop()
148
+
149
+ async def bind(self, handler: Callable[[str], Awaitable[str]]) -> None:
150
+ """
151
+ Bind a message handler to the ChatClient for processing incoming messages.
152
+
153
+ Args:
154
+ handler (Callable[[str], Awaitable[str]]): Async function that processes an incoming message.
155
+ """
156
+ if self.chat_client is None:
157
+ raise Exception("ChatClient is not started yet. Call start() before binding a handler.")
158
+ await self.chat_client.register_job(handler)
159
+
160
+ async def send(self, target_client_name: str, text: str, timeout: Optional[float] = None) -> Tuple[bool, str]:
161
+ """
162
+ Sends a message to a peer by display name and waits for a response.
163
+
164
+ Args:
165
+ target_client_name (str): The display name of the target peer.
166
+ text (str): The message text to send.
167
+ timeout (Optional[float], optional): The timeout in seconds for waiting for a response. Defaults to None.
168
+
169
+ Returns:
170
+ Tuple[bool, str]: A tuple where the first value indicates success (True if the message was sent successfully, False otherwise),
171
+ and the second value contains the response text from the remote peer.
172
+ """
173
+ if self.chat_client is None:
174
+ raise Exception("ChatClient is not started yet. Call start() before sending messages.")
175
+
176
+ async with self._send_lock:
177
+ # Wait for a peer with the specified display name.
178
+ peer = await self.chat_client.wait_for_peers(
179
+ target_client_name=target_client_name,
180
+ timeout=self.config.peer_discovery_timeout,
181
+ interval=self.config.peer_ping_interval,
182
+ )
183
+ if not peer:
184
+ raise Exception(f"Peer '{target_client_name}' not found.")
185
+
186
+ peer_id = peer["peer_id"]
187
+ queue_id = await self.chat_client.send_message(peer_id, text)
188
+ is_success, result = await self.chat_client.wait_for_result(queue_id, timeout=timeout)
189
+ await self.chat_client.close_connection(peer_id)
190
+ return is_success, result
191
+
192
+ async def get_client_list(self, timeout: int = 30) -> List[Dict[str, Any]]:
193
+ """
194
+ Retrieve a list of currently connected peers.
195
+
196
+ Args:
197
+ timeout (int): Maximum time to wait (in seconds) for peers.
198
+
199
+ Returns:
200
+ List[Dict[str, Any]]: A list of peer information dictionaries.
201
+ """
202
+ return await self.chat_client.wait_all_peers(timeout=timeout)
203
+
204
+ def _register_chat_client_on_peer_connected_callback(self) -> None:
205
+ """
206
+ Register all stored peer-connected callbacks with the ChatClient.
207
+ """
208
+ for registered_callback in self._peer_connected_callbacks:
209
+ self.chat_client.register_on_peer_connected_callback(callback=registered_callback)
210
+
211
+ def _register_chat_client_on_peer_disconnected_callback(self) -> None:
212
+ """
213
+ Register all stored peer-disconnected callbacks with the ChatClient.
214
+ """
215
+ for registered_callback in self._peer_disconnected_callbacks:
216
+ self.chat_client.register_on_peer_disconnected_callback(callback=registered_callback)
217
+
218
+ def register_on_peer_connected_callback(self, callback: Callable[[str], Awaitable[None]]) -> None:
219
+ """
220
+ Register an async callback to be called when a peer connects.
221
+
222
+ Args:
223
+ callback (Callable[[str], Awaitable[None]]): Async function accepting a peer ID.
224
+ """
225
+ self._peer_connected_callbacks.append(callback)
226
+ if self.chat_client is not None:
227
+ self._register_chat_client_on_peer_connected_callback()
228
+
229
+ def register_on_peer_disconnected_callback(self, callback: Callable[[str], Awaitable[None]]) -> None:
230
+ """
231
+ Register an async callback to be called when a peer disconnects.
232
+
233
+ Args:
234
+ callback (Callable[[str], Awaitable[None]]): Async function accepting a peer ID.
235
+ """
236
+ self._peer_disconnected_callbacks.append(callback)
237
+ if self.chat_client is not None:
238
+ self._register_chat_client_on_peer_disconnected_callback()
239
+
240
+ def _register_chat_client_on_peer_discovered_callback(self) -> None:
241
+ """
242
+ Register all stored peer-discovered callbacks with the ChatClient.
243
+ """
244
+ for registered_callback in self._peer_discovered_callbacks:
245
+ self.chat_client.register_on_peer_discovered_callback(callback=registered_callback)
246
+
247
+ def _register_chat_client_on_peer_lost_callback(self) -> None:
248
+ """
249
+ Register all stored peer-lost callbacks with the ChatClient.
250
+ """
251
+ for registered_callback in self._peer_lost_callbacks:
252
+ self.chat_client.register_on_peer_lost_callback(callback=registered_callback)
253
+
254
+ def register_on_peer_discovered_callback(self, callback: Callable[[str], Awaitable[None]]) -> None:
255
+ """
256
+ Register an async callback to be called when a peer is discovered.
257
+
258
+ Args:
259
+ callback (Callable[[str], Awaitable[None]]): Async function accepting a peer ID.
260
+ """
261
+ self._peer_discovered_callbacks.append(callback)
262
+ if self.chat_client is not None:
263
+ self._register_chat_client_on_peer_discovered_callback()
264
+
265
+ def register_on_peer_lost_callback(self, callback: Callable[[str], Awaitable[None]]) -> None:
266
+ """
267
+ Register an async callback to be called when a peer is lost.
268
+
269
+ Args:
270
+ callback (Callable[[str], Awaitable[None]]): Async function accepting a peer ID.
271
+ """
272
+ self._peer_lost_callbacks.append(callback)
273
+ if self.chat_client is not None:
274
+ self._register_chat_client_on_peer_lost_callback()
@@ -0,0 +1,186 @@
1
+ Metadata-Version: 2.4
2
+ Name: genai-protocol-lite
3
+ Version: 1.0.0
4
+ Summary: GenAI Python project for direct agents connection either locally via websockets or remote via Azure Web PubSub
5
+ Home-page: https://github.com/genai-works-org/genai-protocol-lite
6
+ Author: Yaroslav Oliinyk, Yaroslav Motalov, Ivan Kuzlo
7
+ Author-email: yaroslav.oliinyk@chisw.com, yaroslav.motalov@chisw.com, ivan.kuzlo@chisw.com
8
+ License: Apache License 2.0
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: wcwidth==0.2.13
16
+ Requires-Dist: websockets~=15.0
17
+ Requires-Dist: colorlog==6.9.0
18
+ Requires-Dist: azure-messaging-webpubsubservice==1.2.1
19
+ Provides-Extra: dev
20
+ Requires-Dist: twine>=6.1.0; extra == "dev"
21
+ Dynamic: author
22
+ Dynamic: author-email
23
+ Dynamic: classifier
24
+ Dynamic: description
25
+ Dynamic: description-content-type
26
+ Dynamic: home-page
27
+ Dynamic: license
28
+ Dynamic: license-file
29
+ Dynamic: provides-extra
30
+ Dynamic: requires-dist
31
+ Dynamic: requires-python
32
+ Dynamic: summary
33
+
34
+ ########################################
35
+
36
+ ```
37
+ # Multi-Agent Communication Library
38
+
39
+ This library provides a unified, session-based API for building distributed multi-agent communication systems. It
40
+ supports two connection modes:
41
+
42
+ - **Local Mode:** Uses UDP/WebSocket for local communication.
43
+ - **Remote Mode:** Uses Azure Pub/Sub for cloud-based messaging.
44
+
45
+ The `Session` class is the main entry point, allowing agents to start a session, send messages to peers, and process
46
+ incoming messages asynchronously.
47
+
48
+ ## Features
49
+
50
+ - **Dual Connection Modes:**
51
+ - **Local Mode:** Communication using UDP/WebSocket.
52
+ - **Remote Mode:** Communication via Azure Pub/Sub.
53
+ - **Session Management:**
54
+ Persistent client identification with an optional display name.
55
+ - **Asynchronous Messaging:**
56
+ Send requests to other clients and wait for their responses.
57
+ - **Custom Workload Processing:**
58
+ Easily bind your own asynchronous message handler to process incoming requests.
59
+ - **Flexible Configuration:**
60
+ Advanced settings available via the `config` parameter.
61
+
62
+ ## Installation
63
+
64
+ ```bash
65
+ python pip install multi-agent-communication-library
66
+ ```
67
+
68
+ ## Usage Examples
69
+
70
+ ### Agent Example (Providing Services)
71
+
72
+ This example demonstrates an agent that provides a service. The agent binds a custom asynchronous message handler to
73
+ process incoming requests with an abstract workload.
74
+
75
+ ```python
76
+ import asyncio
77
+ from library.session import Session
78
+
79
+ async def process(message: str) -> str:
80
+ # Simulate an abstract workload (e.g., data processing or computation)
81
+ print(f"Processing workload for message: {message}")
82
+ await asyncio.sleep(2) # Simulate time-consuming work
83
+ return f"Processed result for: {message}"
84
+
85
+ async def main():
86
+ # Create a session in local mode (use "remote" for Azure Pub/Sub mode)
87
+ session = Session(
88
+ connection_type="local",
89
+ client_name="worker_agent"
90
+ )
91
+ # Start the session with the message handler bound to process incoming requests
92
+ await session.start(message_handler=process)
93
+ # Keep the agent running indefinitely to process incoming tasks
94
+ await asyncio.Future()
95
+
96
+ if __name__ == '__main__':
97
+ asyncio.run(main())
98
+ ```
99
+
100
+ ### Client Example (Using Services)
101
+
102
+ This example shows a client that sends a task request to a service provider identified by its `client_name`.
103
+
104
+ ```python
105
+ import asyncio
106
+
107
+ from library.session import Session
108
+
109
+ async def main():
110
+ session = Session(
111
+ connection_type="local",
112
+ client_name="client"
113
+ )
114
+ await session.start()
115
+ request = "Ipsum tempor ut adipisicing do magna ut excepteur deserunt non irure veniam dolore."
116
+ result = await session.send("scrapper", request)
117
+ print("Result:", result)
118
+ if __name__ == '__main__':
119
+ asyncio.run(main())
120
+
121
+ ```
122
+
123
+ ## API Reference
124
+
125
+ ### `Session` Class
126
+
127
+ #### Constructor
128
+
129
+ - **connection_type:**
130
+ - `'local'` – Uses UDP/WebSocket for local communication.
131
+ - `'remote'` – Uses Azure Pub/Sub for remote communication.
132
+ - **client_name:** Optional display name for the client.
133
+ - **client_id:** Optional unique identifier; if not provided, one will be generated.
134
+ - **config:** Optional configuration dictionary for advanced settings.
135
+ - **log_level:** Logging level (default is `'info'`).
136
+
137
+ #### Methods
138
+
139
+ - **`async start(message_handler: Optional[Callable[[str], Awaitable[str]]] = None) -> None`**
140
+ Starts the session. If a `message_handler` is provided (for local mode), it will be bound to process incoming
141
+ messages.
142
+ - **`async send(target_client_name: str, text: str, timeout: Optional[float] = None) -> str`**
143
+ Sends a message to a peer identified by `target_client_name` and waits for a response
144
+
145
+ ## Configuration Parameters
146
+
147
+
148
+ | Parameter | Type | Default Value | Description | Required In |
149
+ | ------------------------ | --------------- | ------------- | -------------------------------------------------------------------------------------------------- | ----------------- |
150
+ | `webserver_port` | `Optional[int]` | `None` | Port number for the webserver. If not set, an available port from the autodiscovery range is used. | `local` |
151
+ | `azure_endpoint_url` | `Optional[str]` | `None` | URL endpoint for Azure connectivity. | `remote` |
152
+ | `azure_access_key` | `Optional[str]` | `None` | Access key for Azure services. | `remote` |
153
+ | `advertisement_port` | `int` | `9999` | Port used for network advertisement. | `local` |
154
+ | `advertisement_ip` | `str` | `"224.1.1.1"` | IP address for network advertisement. | `local` |
155
+ | `advertisement_interval` | `float` | `15.0` | Interval (in seconds) for sending advertisements. | `local`, `remote` |
156
+ | `heartbeat_delay` | `float` | `5.0` | Delay (in seconds) between heartbeat messages. | `local`, `remote` |
157
+ | `webserver_port_min` | `int` | `5000` | Minimum port number for autodiscovery. | `local` |
158
+ | `webserver_port_max` | `int` | `65000` | Maximum port number for autodiscovery. | `local` |
159
+ | `peer_discovery_timeout` | `float` | `30.0` | Timeout (in seconds) for peer discovery. | `local`, `remote` |
160
+ | `peer_ping_interval` | `float` | `5.0` | Interval (in seconds) for pinging peers. | `local`, `remote` |
161
+ | `azure_api_version` | `str` | `"1.0"` | API version for Azure communications. | `remote` |
162
+ | `connection_type` | `str` | `"local"` | Type of connection, either`"local"` or `"remote"`. | `local`, `remote` |
163
+
164
+ ### Notes:
165
+
166
+ - For **remote** connections, `azure_endpoint_url` and `azure_access_key` are required.
167
+ - If `webserver_port` is not set, it will be selected from the range [`webserver_port_min`, `webserver_port_max`].
168
+ - `connection_type` must be either `"local"` or `"remote"`.
169
+
170
+ ## Customization
171
+
172
+ - **Message Handler:**
173
+ Provide your own asynchronous function to process incoming messages. This function should accept a string message and
174
+ return an awaitable string result.
175
+ - **Configuration Options:**
176
+ Use the `config` parameter in the `Session` constructor for advanced settings.
177
+
178
+ ## License
179
+
180
+ [Specify your license here]
181
+
182
+ ---
183
+
184
+ ```
185
+
186
+ ```