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,260 @@
1
+ import asyncio
2
+ import time
3
+ import logging
4
+ from typing import Callable, Optional, Dict, Any, List, Tuple
5
+
6
+ from AIConnector.common.network import NetworkConfig
7
+ from AIConnector.connector.base_connector import BaseConnector
8
+ from AIConnector.connector.ws_connector import WsConnector
9
+ from AIConnector.connector.azure_connector import AzureConnector
10
+
11
+ from AIConnector.discovery.base_discovery_service import BaseDiscoveryService
12
+ from AIConnector.discovery.discovery_service import DiscoveryService
13
+ from AIConnector.discovery.azure_discovery_service import AzureDiscoveryService
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class P2PMeta(type):
19
+ """
20
+ Metaclass for PeerConnectionManager that dynamically sets up the appropriate
21
+ connector and discovery service based on the network configuration.
22
+ """
23
+ def __call__(cls, *args, **kwargs):
24
+ # Create the instance using the default __call__ method.
25
+ instance = super().__call__(*args, **kwargs)
26
+ connection_type = instance.network_config.connection_type
27
+
28
+ if connection_type == "local":
29
+ # For local connections, use the WebSocket connector and local discovery service.
30
+ instance.connector = WsConnector(
31
+ on_message_callback=instance.on_message_callback,
32
+ on_peer_disconnected=instance.on_peer_disconnected,
33
+ ssl_context=instance.ssl_context,
34
+ client_name=instance.client_name,
35
+ client_id=instance.client_id,
36
+ )
37
+ instance.discovery_service = DiscoveryService(
38
+ on_peer_discovered_callback=instance._on_peer_discovered,
39
+ on_peer_lost=instance._on_peer_lost,
40
+ client_name=instance.client_name,
41
+ port=instance.port,
42
+ network_config=instance.network_config
43
+ )
44
+ else:
45
+ # For remote connections, use the Azure connector and Azure discovery service.
46
+ instance.connector = AzureConnector(
47
+ client_name=instance.client_name,
48
+ client_id=instance.client_id,
49
+ azure_endpoint_url=instance.network_config.azure_endpoint_url,
50
+ azure_access_key=instance.network_config.azure_access_key,
51
+ azure_api_version=instance.network_config.azure_api_version,
52
+ on_message_callback=instance.on_message_callback,
53
+ on_peer_disconnected=instance.on_peer_disconnected
54
+ )
55
+ instance.discovery_service = AzureDiscoveryService(
56
+ client_name=instance.client_name,
57
+ client_id=instance.client_id,
58
+ azure_endpoint_url=instance.network_config.azure_endpoint_url,
59
+ azure_access_key=instance.network_config.azure_access_key,
60
+ azure_api_version=instance.network_config.azure_api_version,
61
+ on_peer_discovered_callback=instance._on_peer_discovered,
62
+ on_peer_lost=instance._on_peer_lost,
63
+ )
64
+
65
+ return instance
66
+
67
+
68
+ class PeerConnectionManager(metaclass=P2PMeta):
69
+ """
70
+ Handles peer-to-peer connectivity using WebSocket connections and a discovery service.
71
+
72
+ The connector and discovery service used are determined dynamically based on the
73
+ provided network configuration. For a local connection type, the connector uses
74
+ WebSocket-based communication; otherwise, Azure-based implementations are used.
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ on_message_callback: Callable[[Dict[str, Any]], None],
80
+ client_id: str,
81
+ network_config: NetworkConfig,
82
+ on_peer_connected: Optional[Callable[[str], None]] = None,
83
+ on_peer_disconnected: Optional[Callable[[str], None]] = None,
84
+ on_peer_discovered: Optional[Callable[[str], None]] = None,
85
+ on_peer_lost: Optional[Callable[[str], None]] = None,
86
+ ssl_context: Any = None,
87
+ client_name: Optional[str] = None,
88
+ port: Optional[int] = None,
89
+ ) -> None:
90
+ """
91
+ Initialize the PeerConnectionManager.
92
+
93
+ Args:
94
+ on_message_callback (Callable[[Dict[str, Any]], None]):
95
+ Function to call when a message is received.
96
+ client_id (str): Unique identifier for this client.
97
+ network_config (NetworkConfig): Network configuration object.
98
+ on_peer_connected (Optional[Callable[[str], None]], optional):
99
+ Function to call when a peer connects.
100
+ on_peer_disconnected (Optional[Callable[[str], None]], optional):
101
+ Function to call when a peer disconnects.
102
+ on_peer_discovered (Optional[Callable[[str], None]], optional):
103
+ Function to call when a new peer is discovered.
104
+ on_peer_lost (Optional[Callable[[str], None]], optional):
105
+ Function to call when a peer is lost.
106
+ ssl_context (Any, optional): SSL context for secure connections.
107
+ client_name (Optional[str], optional): Client name for identification.
108
+ port (Optional[int], optional): Port number for discovery broadcasting.
109
+ """
110
+ self.on_message_callback = on_message_callback
111
+ self.on_peer_connected = on_peer_connected
112
+ self.on_peer_disconnected = on_peer_disconnected
113
+ self.on_peer_discovered = on_peer_discovered
114
+ self.on_peer_lost = on_peer_lost
115
+ self.ssl_context = ssl_context
116
+ self.client_name = client_name
117
+ self.client_id = client_id
118
+ self.network_config = network_config
119
+ self.port = port
120
+
121
+ self.connector: BaseConnector = None
122
+ self.discovery_service: BaseDiscoveryService = None
123
+
124
+ # Dictionary mapping peer_id to peer info.
125
+ self.peers: Dict[str, Dict[str, Any]] = {}
126
+
127
+ async def start(self, host: str, port: int, allow_discovery: bool) -> None:
128
+ """
129
+ Start the discovery service and the connector's server.
130
+
131
+ Args:
132
+ host (str): Host address to bind the WebSocket server.
133
+ port (int): Port number to bind the WebSocket server.
134
+ allow_discovery (bool): Enable or disable the discovery phase.
135
+ """
136
+ await self.discovery_service.start(allow_discovery=allow_discovery)
137
+ await self.connector.start_server(host=host, port=port, allow_discovery=allow_discovery)
138
+ logger.debug("[PeerConnectionManager] Started")
139
+
140
+ async def stop(self) -> None:
141
+ """
142
+ Stop the discovery service and the connector's server.
143
+ """
144
+ await self.discovery_service.stop()
145
+ await self.connector.stop_server()
146
+ logger.debug("[PeerConnectionManager] Stopped")
147
+
148
+ async def connect_to_peer(self, ip: str, port: int, peer_id: str) -> None:
149
+ """
150
+ Connect to a discovered peer.
151
+
152
+ Args:
153
+ ip (str): IP address of the peer.
154
+ port (int): Port number of the peer.
155
+ peer_id (str): Unique identifier of the peer.
156
+ """
157
+ await self.connector.connect_to_peer(ip, port, peer_id)
158
+ if self.on_peer_connected:
159
+ result = self.on_peer_connected(peer_id)
160
+ if asyncio.iscoroutine(result):
161
+ await result
162
+
163
+ async def send_message(self, peer_id: str, msg: Dict[str, Any]) -> None:
164
+ """
165
+ Send a message to a specific peer.
166
+
167
+ Args:
168
+ peer_id (str): Unique identifier of the recipient peer.
169
+ msg (Dict[str, Any]): Message content as a dictionary.
170
+ """
171
+ # If no active connection exists, try to establish one using stored peer info.
172
+ if peer_id not in self.connector.active_connections:
173
+ peer_info = self.peers.get(peer_id)
174
+ if peer_info:
175
+ await self.connect_to_peer(peer_info["ip"], peer_info["port"], peer_id)
176
+ else:
177
+ logger.error(f"[PeerConnectionManager] Cannot send message: peer {peer_id} not discovered")
178
+ return
179
+ await self.connector.send_message(peer_id, msg)
180
+
181
+ async def _on_peer_discovered(self, msg: Dict[str, Any], addr: Tuple[str, int]) -> None:
182
+ """
183
+ Async callback triggered when a new peer is discovered.
184
+
185
+ Args:
186
+ msg (Dict[str, Any]): Message containing peer details.
187
+ addr (Tuple[str, int]): Tuple with the peer's (IP, port) address.
188
+ """
189
+ peer_id = msg.get("client_id")
190
+ # Ignore invalid peer IDs or self-discovery.
191
+ if not peer_id or peer_id == self.client_id:
192
+ return
193
+
194
+ ip = addr[0]
195
+ port = msg.get("port")
196
+ name = msg.get("display_name", "Unknown")
197
+ now = time.time()
198
+
199
+ # Add or update the peer in the peers dictionary.
200
+ if peer_id not in self.peers:
201
+ self.peers[peer_id] = {
202
+ "ip": ip,
203
+ "port": port,
204
+ "display_name": name,
205
+ "last_seen": now,
206
+ }
207
+ else:
208
+ self.peers[peer_id].update({
209
+ "ip": ip,
210
+ "port": port,
211
+ "last_seen": now,
212
+ })
213
+
214
+ if self.on_peer_discovered:
215
+ try:
216
+ result = self.on_peer_discovered(peer_id)
217
+ if asyncio.iscoroutine(result):
218
+ await result
219
+ except Exception as e:
220
+ logger.error(f"[PeerConnectionManager] Error in on_peer_discovered callback: {e}")
221
+
222
+ async def _on_peer_lost(self, peer_id: str) -> None:
223
+ """
224
+ Async callback triggered when a peer is lost.
225
+
226
+ Args:
227
+ peer_id (str): Unique identifier of the lost peer.
228
+ """
229
+ removed = self.peers.pop(peer_id, None)
230
+
231
+ if removed is not None:
232
+ logger.debug(f"[PeerConnectionManager] Peer {peer_id} removed from peers list")
233
+
234
+ if self.on_peer_lost:
235
+ result = self.on_peer_lost(peer_id)
236
+ if asyncio.iscoroutine(result):
237
+ await result
238
+
239
+ def get_peers_list(self) -> List[Dict[str, str]]:
240
+ """
241
+ Retrieve a list of discovered peers.
242
+
243
+ Returns:
244
+ List[Dict[str, str]]: A list of dictionaries, each containing 'peer_id' and 'display_name'.
245
+ """
246
+ result = [
247
+ {"peer_id": pid, "display_name": info["display_name"]}
248
+ for pid, info in self.peers.items()
249
+ ]
250
+ logger.info(f"[PeerConnectionManager] Get peers list returned {result}")
251
+ return result
252
+
253
+ async def close_connection(self, peer_id: str):
254
+ """
255
+ Close connection
256
+
257
+ Args:
258
+ peer_id (str): Unique identifier of the lost peer.
259
+ """
260
+ await self.connector.disconnect_from_peer(peer_id)
@@ -0,0 +1,213 @@
1
+ import asyncio
2
+ import json
3
+ import time
4
+ import logging
5
+
6
+ import websockets
7
+ from typing import Callable, Optional, Dict, Any
8
+
9
+ from AIConnector.connector.base_connector import BaseConnector
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class WsConnector(BaseConnector):
15
+ """
16
+ Manages WebSocket connections to peers.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ client_id: str,
22
+ on_message_callback: Callable[[Dict[str, Any]], asyncio.Future],
23
+ on_peer_connected: Optional[Callable[[str], asyncio.Future]] = None,
24
+ on_peer_disconnected: Optional[Callable[[str], asyncio.Future]] = None,
25
+ ssl_context: Optional[Any] = None,
26
+ client_name: Optional[str] = None,
27
+ ) -> None:
28
+ """
29
+ Initialize the WebSocket connector.
30
+
31
+ Args:
32
+ client_id (str): Unique identifier for this client.
33
+ on_message_callback (Callable[[Dict[str, Any]], asyncio.Future]):
34
+ Coroutine to be called with incoming messages.
35
+ on_peer_connected (Optional[Callable[[str], asyncio.Future]], optional):
36
+ Coroutine to be called when a peer connects.
37
+ on_peer_disconnected (Optional[Callable[[str], asyncio.Future]], optional):
38
+ Coroutine to be called when a peer disconnects.
39
+ ssl_context (Optional[Any], optional): SSL context for secure connections.
40
+ client_name (Optional[str], optional): Client name for identification.
41
+ """
42
+ self.client_id = client_id
43
+ self.on_message_callback = on_message_callback
44
+ self.on_peer_connected = on_peer_connected
45
+ self.on_peer_disconnected = on_peer_disconnected
46
+ self.ssl_context = ssl_context
47
+ self.client_name = client_name
48
+
49
+ # Dictionary to store active WebSocket connections by peer ID.
50
+ self.active_connections: Dict[str, websockets.WebSocketClientProtocol] = {}
51
+ self.server: Optional[websockets.server.Serve] = None
52
+
53
+ async def start_server(
54
+ self, host: str = "0.0.0.0", port: int = 5000, allow_discovery: bool = True
55
+ ) -> None:
56
+ """
57
+ Start the WebSocket server.
58
+
59
+ Args:
60
+ host (str): The host address to bind the server.
61
+ port (int): The port number to bind the server.
62
+ allow_discovery (bool): If True, start the WebSocket server.
63
+ """
64
+ if not allow_discovery:
65
+ return
66
+
67
+ self.server = await websockets.serve(self._handler, host, port, ssl=self.ssl_context)
68
+ logger.info(f"[WsConnector] WebSocket server started on {host}:{port}")
69
+
70
+ async def stop_server(self) -> None:
71
+ """
72
+ Stop the WebSocket server and close all active connections.
73
+ """
74
+ if self.server:
75
+ self.server.close()
76
+ await self.server.wait_closed()
77
+ logger.info("[WsConnector] Server stopped")
78
+
79
+ async def _handler(self, websocket: websockets.WebSocketServerProtocol, path: str = "") -> None:
80
+ """
81
+ Handle incoming WebSocket connections.
82
+
83
+ Args:
84
+ websocket (websockets.WebSocketServerProtocol): The connected WebSocket.
85
+ path (str): URL path (unused).
86
+ """
87
+ peername = websocket.remote_address
88
+ logger.info(f"[WsServer] Connection established with {peername}")
89
+ try:
90
+ async for message in websocket:
91
+ try:
92
+ msg = json.loads(message)
93
+ except Exception as e:
94
+ logger.error(f"[WsServer] Error parsing message: {e}")
95
+ continue
96
+
97
+ # Register connection upon receiving a HELLO message.
98
+ if msg.get("type") == "HELLO":
99
+ peer_id = msg.get("from_id")
100
+ if peer_id:
101
+ self.active_connections[peer_id] = websocket
102
+ if self.on_peer_connected:
103
+ await self.on_peer_connected(peer_id)
104
+
105
+ # Process the incoming message.
106
+ await self.on_message_callback(msg)
107
+ except websockets.exceptions.ConnectionClosed:
108
+ logger.debug(f"[WsServer] Connection closed: {peername}")
109
+ finally:
110
+ # Remove connection from active connections and notify disconnection.
111
+ for pid, ws in list(self.active_connections.items()):
112
+ if ws == websocket:
113
+ del self.active_connections[pid]
114
+ if self.on_peer_disconnected:
115
+ await self.on_peer_disconnected(pid)
116
+ break
117
+
118
+ async def connect_to_peer(self, ip: str, port: int, peer_id: str) -> None:
119
+ """
120
+ Connect to a peer using a WebSocket connection.
121
+
122
+ Args:
123
+ ip (str): IP address of the peer.
124
+ port (int): Port number of the peer.
125
+ peer_id (str): Unique identifier of the peer.
126
+ """
127
+ if peer_id in self.active_connections:
128
+ return
129
+
130
+ websocket_uri = f"ws://{ip}:{port}"
131
+ try:
132
+ websocket = await websockets.connect(websocket_uri, ssl=self.ssl_context)
133
+ self.active_connections[peer_id] = websocket
134
+ logger.info(f"[WsConnector] Connected to {peer_id} ({ip}:{port})")
135
+
136
+ # Handle messages from the peer asynchronously.
137
+ asyncio.create_task(self._client_handler(websocket, peer_id))
138
+
139
+ # Send a handshake HELLO message.
140
+ handshake_msg = {
141
+ "type": "HELLO",
142
+ "from_id": self.client_id,
143
+ "from_name": self.client_name or "Unknown",
144
+ "timestamp": time.time(),
145
+ }
146
+ await websocket.send(json.dumps(handshake_msg))
147
+ except Exception as e:
148
+ logger.error(f"[WsConnector] Connection error to {ip}:{port} - {e}")
149
+
150
+ async def _client_handler(self, websocket: websockets.WebSocketClientProtocol, peer_id: str) -> None:
151
+ """
152
+ Handle messages received from a connected peer.
153
+
154
+ Args:
155
+ websocket (websockets.WebSocketClientProtocol): The peer's WebSocket connection.
156
+ peer_id (str): Unique identifier of the peer.
157
+ """
158
+ try:
159
+ async for message in websocket:
160
+ try:
161
+ msg = json.loads(message)
162
+ except Exception as e:
163
+ logger.error(f"[WsClient] Error parsing message: {e}")
164
+ continue
165
+
166
+ # Update connection info on HELLO messages.
167
+ if msg.get("type") == "HELLO":
168
+ p_id = msg.get("from_id")
169
+ if p_id:
170
+ self.active_connections[p_id] = websocket
171
+
172
+ # Process the received message.
173
+ await self.on_message_callback(msg)
174
+ except websockets.exceptions.ConnectionClosed:
175
+ logger.error(f"[WsClient] Connection with {peer_id} closed")
176
+ finally:
177
+ if peer_id in self.active_connections:
178
+ del self.active_connections[peer_id]
179
+ if self.on_peer_disconnected:
180
+ await self.on_peer_disconnected(peer_id)
181
+
182
+ async def send_message(self, peer_id: str, msg: Dict[str, Any]) -> None:
183
+ """
184
+ Send a message to a specific peer via an active WebSocket connection.
185
+
186
+ Args:
187
+ peer_id (str): Unique identifier of the peer.
188
+ msg (Dict[str, Any]): The message payload as a dictionary.
189
+ """
190
+ websocket = self.active_connections.get(peer_id)
191
+ if websocket:
192
+ try:
193
+ await websocket.send(json.dumps(msg))
194
+ except Exception as e:
195
+ logger.error(f"[WsConnector] Error sending message to {peer_id}: {e}")
196
+ else:
197
+ logger.info(f"[WsConnector] Message sent to {peer_id}")
198
+ else:
199
+ logger.info(f"[WsConnector] No active connection with: {peer_id}")
200
+
201
+
202
+ async def disconnect_from_peer(self, peer_id: str) -> None:
203
+ """
204
+ Close websocket connection
205
+
206
+ Args:
207
+ peer_id (str): Unique identifier of the lost peer.
208
+ """
209
+ try:
210
+ await self.active_connections[peer_id].close()
211
+ logger.info(f"[WsConnector] Closed WebSocket connection: {peer_id=}")
212
+ except Exception as e:
213
+ logger.error(f"[WsConnector] Error while disconnecting from {peer_id=}: {e}")
File without changes