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.
File without changes
File without changes
@@ -0,0 +1,13 @@
1
+ class ParameterException(ValueError):
2
+ """Exception raised for errors in the input parameters."""
3
+ pass
4
+
5
+
6
+ class PeerDisconnectedException(Exception):
7
+ """Exception indicating that the peer has disconnected."""
8
+ pass
9
+
10
+
11
+ class AgentConnectionError(Exception):
12
+ """Exception indicating that we couldn't connect to agent."""
13
+ pass
@@ -0,0 +1,57 @@
1
+ import logging
2
+ from colorlog import ColoredFormatter
3
+
4
+
5
+ class Logger:
6
+ LOG_LEVEL = {
7
+ "critical": (logging.CRITICAL, "%(asctime)s %(levelname)-8s [%(name)s] %(message)s"),
8
+ "error": (logging.ERROR, "%(asctime)s %(levelname)-8s [%(name)s] %(message)s"),
9
+ "warning": (logging.WARNING, "%(asctime)s %(levelname)-8s [%(name)s] %(message)s"),
10
+ "info": (logging.INFO, "%(asctime)s %(levelname)-8s [%(name)s] %(message)s"),
11
+ "debug": (logging.DEBUG, f"%(pathname)s:%(lineno)d:\n%(asctime)s %(levelname)-8s [%(name)s] %(message)s"),
12
+ "notset": (logging.NOTSET, "%(asctime)s %(levelname)-8s [%(name)s] %(message)s")
13
+ }
14
+
15
+ def __init__(self, log_level):
16
+ self._date_fmt = "%d-%m-%Y:%H:%M:%S"
17
+ self.log_level = self._get_log_level(log_level)
18
+ self.log_format = self._get_log_format(log_level)
19
+ self._config()
20
+
21
+ def _get_log_level(self, log_level):
22
+ return self.LOG_LEVEL[log_level][0]
23
+
24
+ def _get_log_format(self, log_level):
25
+ return self.LOG_LEVEL[log_level][1]
26
+
27
+ def _config(self):
28
+ """
29
+ Configures logging using a colored formatter.
30
+ """
31
+ # Get the root logger
32
+ root_logger = logging.getLogger()
33
+
34
+ # Remove existing handlers, if any
35
+ for handler in root_logger.handlers[:]:
36
+ root_logger.removeHandler(handler)
37
+
38
+ # Create a console handler
39
+ stream_handler = logging.StreamHandler()
40
+
41
+ # Define the color scheme for different logging levels
42
+ formatter = ColoredFormatter(
43
+ "%(log_color)s" + self.log_format,
44
+ datefmt=self._date_fmt,
45
+ log_colors={
46
+ 'DEBUG': 'blue',
47
+ 'INFO': 'green',
48
+ 'WARNING': 'yellow',
49
+ 'ERROR': 'red',
50
+ 'CRITICAL': 'red,bg_white',
51
+ }
52
+ )
53
+ stream_handler.setFormatter(formatter)
54
+
55
+ # Configure the root logger
56
+ root_logger.setLevel(self.log_level)
57
+ root_logger.addHandler(stream_handler)
@@ -0,0 +1,14 @@
1
+ from enum import Enum
2
+
3
+
4
+ class MessageTypes(Enum):
5
+ """
6
+ Enum representing different types of messages exchanged between peers.
7
+ """
8
+ HELLO = "hello"
9
+ TEXT = "text"
10
+ SYSTEM_MESSAGE = "system_message"
11
+ HEARTBEAT = "heartbeat"
12
+ FINAL_MESSAGE = "final_message"
13
+ ERROR = "error"
14
+ JOB_LIST = "job_list"
@@ -0,0 +1,146 @@
1
+ import socket
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+ from AIConnector.common.exceptions import ParameterException
7
+
8
+
9
+ @dataclass
10
+ class Config:
11
+ """
12
+ Data class representing the Class network configuration settings.
13
+
14
+ Attributes:
15
+ webserver_port (Optional[int]): Port number for the webserver.
16
+ If not provided, an available port within the autodiscovery range will be used.
17
+ Required in: 'local' and 'remote'.
18
+ azure_endpoint_url (Optional[str]): URL endpoint for Azure connectivity.
19
+ Required in: 'remote'.
20
+ azure_access_key (Optional[str]): Access key for Azure services.
21
+ Required in: 'remote'.
22
+ advertisement_port (int): Port used for network advertisement. Defaults to 9999.
23
+ Required in: 'local' and 'remote'.
24
+ advertisement_ip (str): IP address for network advertisement. Defaults to "224.1.1.1".
25
+ Required in: 'local' and 'remote'.
26
+ advertisement_interval (float): Interval (in seconds) for sending advertisements. Defaults to 15.0.
27
+ Required in: 'local' and 'remote'.
28
+ heartbeat_delay (float): Delay (in seconds) between heartbeat messages. Defaults to 5.0.
29
+ Required in: 'local' and 'remote'.
30
+ webserver_port_min (int): Minimum port number for autodiscovery. Defaults to 5000.
31
+ Required in: 'local'.
32
+ webserver_port_max (int): Maximum port number for autodiscovery. Defaults to 7000.
33
+ Required in: 'local'.
34
+ peer_discovery_timeout (float): Timeout (in seconds) for peer discovery. Defaults to 30.0.
35
+ Required in: 'local' and 'remote'.
36
+ peer_ping_interval (float): Interval (in seconds) for pinging peers. Defaults to 5.0.
37
+ Required in: 'local' and 'remote'.
38
+ azure_api_version (str): API version for Azure communications. Defaults to "1.0".
39
+ Required in: 'remote'.
40
+ connection_type (str): Type of connection, either "local" or "remote". Defaults to "local".
41
+ Required in: 'local' and 'remote'.
42
+ """
43
+ webserver_port: Optional[int] = None
44
+
45
+ azure_endpoint_url: Optional[str] = None
46
+ azure_access_key: Optional[str] = None
47
+
48
+ advertisement_port: int = 9999
49
+ advertisement_ip: str = "224.1.1.1"
50
+ advertisement_interval: float = 15.0
51
+ heartbeat_delay: float = 15.0
52
+ webserver_port_min: int = 5000
53
+ webserver_port_max: int = 65000
54
+ peer_discovery_timeout: float = 30.0
55
+ peer_ping_interval: float = 5.0
56
+ azure_api_version: str = "1.0"
57
+ connection_type: str = "local"
58
+
59
+ def __post_init__(self):
60
+ """
61
+ Validate the network configuration parameters immediately after initialization.
62
+
63
+ Raises:
64
+ ParameterException: If any parameter is invalid.
65
+ """
66
+ # Validate connection type
67
+ if self.connection_type not in ("local", "remote"):
68
+ raise ParameterException("Invalid connection type. Only 'local' and 'remote' are allowed.")
69
+
70
+ # For remote connections, ensure Azure settings are provided
71
+ if self.connection_type == "remote":
72
+ if not (self.azure_endpoint_url and self.azure_access_key):
73
+ raise ParameterException(
74
+ "Azure endpoint URL and access key must be provided for 'remote' sessions."
75
+ )
76
+
77
+ # Define a list of fields to validate (field name, value, expected type)
78
+ fields_to_validate = (
79
+ ("advertisement_port", self.advertisement_port, int),
80
+ ("webserver_port_min", self.webserver_port_min, int),
81
+ ("webserver_port_max", self.webserver_port_max, int),
82
+ ("advertisement_interval", self.advertisement_interval, float),
83
+ ("heartbeat_delay", self.heartbeat_delay, float),
84
+ ("peer_discovery_timeout", self.peer_discovery_timeout, float),
85
+ ("peer_ping_interval", self.peer_ping_interval, float),
86
+ )
87
+
88
+ # Check each field for correct type and positive value
89
+ for field_name, value, expected_type in fields_to_validate:
90
+ if not isinstance(value, expected_type):
91
+ raise ParameterException(f"Parameter '{field_name}' must be of type {expected_type.__name__}.")
92
+ if value <= 0:
93
+ raise ParameterException(f"Parameter '{field_name}' must be greater than 0.")
94
+
95
+ # Ensure autodiscovery port range is valid
96
+ if self.webserver_port_min > self.webserver_port_max:
97
+ raise ParameterException(
98
+ "webserver_port_min cannot be greater than webserver_port_max."
99
+ )
100
+
101
+ # If webserver_port is not set, determine an available port from the autodiscovery range
102
+ if not self.webserver_port:
103
+ self.webserver_port = get_available_port(
104
+ self.webserver_port_min,
105
+ self.webserver_port_max
106
+ )
107
+
108
+
109
+ @dataclass
110
+ class NetworkConfig(Config):
111
+ """
112
+ Data class representing the App network configuration settings.
113
+
114
+ Attributes:
115
+ client_id (Optional[str]): Identifier for the client.
116
+ Required in: 'local' and 'remote'.
117
+ """
118
+ client_id: Optional[str] = None
119
+
120
+
121
+ def get_available_port(start_port: int = 5000, end_port: int = 7000) -> Optional[int]:
122
+ """
123
+ Find and return the first available network port in the given range (inclusive).
124
+
125
+ The function iterates through the port range and attempts to bind a temporary
126
+ socket to each port. If the bind is successful, the port is considered available.
127
+
128
+ Args:
129
+ start_port (int): The beginning of the port range to check. Defaults to 5000.
130
+ end_port (int): The end of the port range to check. Defaults to 7000.
131
+
132
+ Returns:
133
+ Optional[int]: The first available port number, or None if no port is available.
134
+ """
135
+ for port in range(start_port, end_port + 1):
136
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
137
+ try:
138
+ # Attempt to bind the socket to the current port on all interfaces.
139
+ sock.bind(("0.0.0.0", port))
140
+ # Successful bind indicates the port is free.
141
+ return port
142
+ except OSError:
143
+ # Port is already in use; try the next one.
144
+ continue
145
+ # Return None if no available port is found in the specified range.
146
+ return None
File without changes
@@ -0,0 +1,205 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import time
5
+ from typing import Callable, Optional, Dict, Any
6
+
7
+ import websockets
8
+
9
+ from azure.messaging.webpubsubservice import WebPubSubServiceClient
10
+ from AIConnector.connector.base_connector import BaseConnector
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class AzureConnector(BaseConnector):
16
+ """
17
+ AzureConnector implements message exchange via Azure Web PubSub,
18
+ similar to how WsConnector operates for local connections.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ client_id: str,
24
+ on_message_callback: Callable[[Dict[str, Any]], Any],
25
+ azure_endpoint_url: str,
26
+ azure_access_key: str,
27
+ azure_api_version: str,
28
+ messaging_hub: str = "messaging_demo",
29
+ reconnect_attempts: int = 2,
30
+ client_name: Optional[str] = None,
31
+ on_peer_disconnected: Optional[Callable[[str], Any]] = None,
32
+ on_peer_connected: Optional[Callable[[str], Any]] = None,
33
+ ) -> None:
34
+ """
35
+ Initialize the AzureConnector.
36
+
37
+ Args:
38
+ client_id (str): Identifier for the client.
39
+ on_message_callback (Callable[[Dict[str, Any]], Any]): Callback function to handle incoming messages.
40
+ azure_endpoint_url (str): Azure Web PubSub endpoint URL.
41
+ azure_access_key (str): Access key for Azure Web PubSub.
42
+ azure_api_version (str): API version for Azure services.
43
+ messaging_hub (str, optional): Name of the messaging hub. Defaults to "messaging_demo".
44
+ reconnect_attempts (int, optional): Number of reconnection attempts on failure. Defaults to 2.
45
+ client_name (Optional[str], optional): Name of the client. Defaults to None.
46
+ on_peer_disconnected (Optional[Callable[[str], Any]], optional): Callback when a peer disconnects. Defaults to None.
47
+ on_peer_connected (Optional[Callable[[str], Any]], optional): Callback when a peer connects. Defaults to None.
48
+ """
49
+ self.client_id = client_id
50
+ self.on_message_callback = on_message_callback
51
+ self.on_peer_disconnected = on_peer_disconnected
52
+ self.on_peer_connected = on_peer_connected
53
+ self.client_name = client_name
54
+ self.reconnect_attempts = reconnect_attempts
55
+ self.messaging_hub = messaging_hub
56
+
57
+ self.is_connected = False
58
+ self.running = False
59
+ self.active_connections: Dict[str, websockets.WebSocketClientProtocol] = {}
60
+
61
+ # Initialize the service client for the messaging hub.
62
+ connection_string = f"Endpoint={azure_endpoint_url};AccessKey={azure_access_key};ApiVersion={azure_api_version}"
63
+ self.service_client = WebPubSubServiceClient.from_connection_string(
64
+ connection_string,
65
+ hub=messaging_hub,
66
+ )
67
+ self.websocket = None
68
+
69
+ async def start_server(self, host: str = None, port: int = None, allow_discovery: bool = False) -> None:
70
+ """
71
+ Establish a WebSocket connection with the messaging hub.
72
+
73
+ The host and port parameters are provided for interface compatibility,
74
+ but they are not used in AzureConnector.
75
+
76
+ Args:
77
+ host (str, optional): Not used in AzureConnector.
78
+ port (int, optional): Not used in AzureConnector.
79
+ allow_discovery (bool, optional): Not used in AzureConnector.
80
+ """
81
+ self.running = True
82
+ try:
83
+ logger.info(f"Client ID: {self.client_id}")
84
+ token = self.service_client.get_client_access_token(user_id=self.client_id)
85
+ self.websocket = await websockets.connect(token["url"])
86
+ self.is_connected = True
87
+ logger.info(f"[AzureConnector] Connected to Azure Web PubSub messaging hub: {self.messaging_hub}")
88
+
89
+ # Start receiving messages asynchronously.
90
+ asyncio.create_task(self._receive_messages())
91
+
92
+ except Exception as e:
93
+ logger.error(f"[AzureConnector] Connection failed: {e}")
94
+ await self._attempt_reconnect()
95
+
96
+ async def stop_server(self) -> None:
97
+ """
98
+ Stop the WebSocket connection with the messaging hub.
99
+ """
100
+ self.running = False
101
+ self.is_connected = False
102
+ if self.websocket:
103
+ await self.websocket.close()
104
+ logger.info("[AzureConnector] Messaging WebSocket connection closed")
105
+
106
+ async def send_message(self, peer_id: str, msg: Dict[str, Any]) -> None:
107
+ """
108
+ Send a message to a specific peer via the messaging hub.
109
+
110
+ Args:
111
+ peer_id (str): The identifier of the target peer.
112
+ msg (Dict[str, Any]): The message payload to send.
113
+ """
114
+ if not self.is_connected:
115
+ logger.error("[AzureConnector] Cannot send message: not connected")
116
+ return
117
+
118
+ try:
119
+ message = {
120
+ "type": "MESSAGE",
121
+ "from_id": self.client_id,
122
+ "from_name": self.client_name or "Unknown",
123
+ "to_id": peer_id,
124
+ "timestamp": time.time(),
125
+ "payload": msg,
126
+ }
127
+ # Send the message as a JSON string.
128
+ self.service_client.send_to_user(peer_id, json.dumps(message))
129
+ except Exception as e:
130
+ logger.error(f"[AzureConnector] Error sending message to {peer_id}: {e}")
131
+ else:
132
+ logger.info(f"[AzureConnector] Message sent to {peer_id}")
133
+
134
+ async def connect_to_peer(self, ip: str, port: int, peer_id: str) -> None:
135
+ """
136
+ For Azure, a direct connection to peers is not required.
137
+ Instead, a HELLO message is sent to simulate a handshake.
138
+
139
+ Args:
140
+ ip (str): Not used in AzureConnector.
141
+ port (int): Not used in AzureConnector.
142
+ peer_id (str): The identifier of the peer to connect to.
143
+ """
144
+ hello_msg = {
145
+ "type": "HELLO",
146
+ "from_id": self.client_id,
147
+ "from_name": self.client_name or "Unknown",
148
+ "timestamp": time.time(),
149
+ }
150
+ await self.send_message(peer_id, hello_msg)
151
+
152
+ async def _receive_messages(self) -> None:
153
+ """
154
+ Receive incoming messages via the messaging hub.
155
+ """
156
+ while self.running and self.websocket:
157
+ try:
158
+ message = await self.websocket.recv()
159
+ msg = json.loads(message)
160
+ # If the received message is a string, try to parse it again.
161
+ if isinstance(msg, str):
162
+ msg = json.loads(msg)
163
+ payload = msg["payload"]
164
+ # If a HELLO message is received, update active connections and trigger callback.
165
+ if payload.get("type").lower() == "hello":
166
+ peer_id = payload.get("from_id")
167
+ if peer_id:
168
+ self.active_connections[peer_id] = self.websocket
169
+ if self.on_peer_connected:
170
+ await self.on_peer_connected(peer_id)
171
+
172
+ # Process the incoming message with the provided callback.
173
+ await self.on_message_callback(payload)
174
+ except websockets.exceptions.ConnectionClosed:
175
+ logger.error("[AzureConnector] Messaging WebSocket connection closed")
176
+ await self._attempt_reconnect()
177
+ break
178
+ except Exception as e:
179
+ logger.error(f"[AzureConnector] Error processing message: {e}")
180
+
181
+ async def _attempt_reconnect(self) -> None:
182
+ """
183
+ Attempt to reconnect to Azure Web PubSub after a connection loss.
184
+ """
185
+ for attempt in range(self.reconnect_attempts):
186
+ try:
187
+ logger.info(f"[AzureConnector] Reconnection attempt {attempt + 1}/{self.reconnect_attempts}")
188
+ await self.start_server()
189
+ if self.is_connected:
190
+ return
191
+ except Exception as e:
192
+ logger.error(f"[AzureConnector] Reconnection attempt failed: {e}")
193
+ # Exponential backoff before the next attempt.
194
+ await asyncio.sleep(2 ** attempt)
195
+ logger.error("[AzureConnector] Failed to reconnect after multiple attempts")
196
+
197
+
198
+ async def disconnect_from_peer(self, peer_id: str) -> None:
199
+ """
200
+ Close connection
201
+
202
+ Args:
203
+ peer_id (str): Unique identifier of the lost peer.
204
+ """
205
+ pass
@@ -0,0 +1,51 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Dict
3
+
4
+
5
+ class BaseConnector(ABC):
6
+ """
7
+ Abstract base class for network connectors.
8
+
9
+ Classes inheriting from BaseConnector must implement the methods
10
+ to start and stop the server, as well as to send messages.
11
+ """
12
+
13
+ @abstractmethod
14
+ async def start_server(self, host: str, port: int, allow_discovery: bool) -> None:
15
+ """
16
+ Start the server and establish necessary connections.
17
+
18
+ Args:
19
+ host (str): The host address to bind the server.
20
+ port (int): The port number to bind the server.
21
+ allow_discovery (bool): Flag indicating if discovery is allowed.
22
+ """
23
+ pass
24
+
25
+ @abstractmethod
26
+ async def stop_server(self) -> None:
27
+ """
28
+ Stop the server and close all active connections.
29
+ """
30
+ pass
31
+
32
+ @abstractmethod
33
+ async def send_message(self, peer_id: str, msg: Dict[str, Any]) -> None:
34
+ """
35
+ Send a message to a specified peer.
36
+
37
+ Args:
38
+ peer_id (str): Identifier of the target peer.
39
+ msg (Dict[str, Any]): The message payload to be sent.
40
+ """
41
+ pass
42
+
43
+ @abstractmethod
44
+ async def disconnect_from_peer(self, peer_id: str) -> None:
45
+ """
46
+ Disconnect from peer by peer id
47
+
48
+ Args:
49
+ peer_id (str): Identifier of the target peer.
50
+ """
51
+ pass