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.
- AIConnector/__init__.py +0 -0
- AIConnector/common/__init__.py +0 -0
- AIConnector/common/exceptions.py +13 -0
- AIConnector/common/logger.py +57 -0
- AIConnector/common/message.py +14 -0
- AIConnector/common/network.py +146 -0
- AIConnector/connector/__init__.py +0 -0
- AIConnector/connector/azure_connector.py +205 -0
- AIConnector/connector/base_connector.py +51 -0
- AIConnector/connector/peer_connection_manager.py +260 -0
- AIConnector/connector/ws_connector.py +213 -0
- AIConnector/core/__init__.py +0 -0
- AIConnector/core/chat_client.py +505 -0
- AIConnector/core/job.py +48 -0
- AIConnector/core/job_manager.py +219 -0
- AIConnector/core/message_factory.py +44 -0
- AIConnector/discovery/__init__.py +0 -0
- AIConnector/discovery/azure_discovery_service.py +206 -0
- AIConnector/discovery/base_discovery_service.py +27 -0
- AIConnector/discovery/discovery_service.py +226 -0
- AIConnector/session.py +274 -0
- genai_protocol_lite-1.0.0.dist-info/METADATA +186 -0
- genai_protocol_lite-1.0.0.dist-info/RECORD +26 -0
- genai_protocol_lite-1.0.0.dist-info/WHEEL +5 -0
- genai_protocol_lite-1.0.0.dist-info/licenses/LICENSE +201 -0
- genai_protocol_lite-1.0.0.dist-info/top_level.txt +1 -0
@@ -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
|
+
```
|