zyndai-agent 0.1.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.
- p3ai_agent/__init__.py +0 -0
- p3ai_agent/agent.py +75 -0
- p3ai_agent/communication.py +556 -0
- p3ai_agent/identity.py +126 -0
- p3ai_agent/search.py +63 -0
- p3ai_agent/utils.py +369 -0
- zyndai_agent-0.1.0.dist-info/METADATA +409 -0
- zyndai_agent-0.1.0.dist-info/RECORD +10 -0
- zyndai_agent-0.1.0.dist-info/WHEEL +5 -0
- zyndai_agent-0.1.0.dist-info/top_level.txt +1 -0
p3ai_agent/__init__.py
ADDED
|
File without changes
|
p3ai_agent/agent.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import requests
|
|
3
|
+
from p3ai_agent.search import SearchAndDiscoveryManager
|
|
4
|
+
from p3ai_agent.identity import IdentityManager
|
|
5
|
+
from p3ai_agent.communication import AgentCommunicationManager
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from langchain.agents import AgentExecutor
|
|
9
|
+
|
|
10
|
+
class AgentConfig(BaseModel):
|
|
11
|
+
auto_reconnect: bool = True
|
|
12
|
+
message_history_limit: int = 100
|
|
13
|
+
registry_url: str = "http://localhost:3002"
|
|
14
|
+
mqtt_broker_url: str
|
|
15
|
+
identity_credential_path: str
|
|
16
|
+
identity_credential: Optional[dict] = None
|
|
17
|
+
default_outbox_topic: Optional[str] = None
|
|
18
|
+
secret_seed: str = None
|
|
19
|
+
|
|
20
|
+
class P3AIAgent(SearchAndDiscoveryManager, IdentityManager, AgentCommunicationManager):
|
|
21
|
+
|
|
22
|
+
def __init__(self, agent_config: AgentConfig):
|
|
23
|
+
|
|
24
|
+
self.agent_executor = None
|
|
25
|
+
self.agent_config = agent_config
|
|
26
|
+
self.agent_executor: AgentExecutor = None
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
with open(agent_config.identity_credential_path, "r") as f:
|
|
30
|
+
self.identity_credential = json.load(f)
|
|
31
|
+
except FileNotFoundError:
|
|
32
|
+
raise FileNotFoundError(f"Identity credential file not found at {agent_config.identity_credential_path}")
|
|
33
|
+
|
|
34
|
+
IdentityManager.__init__(self,agent_config.registry_url)
|
|
35
|
+
|
|
36
|
+
SearchAndDiscoveryManager.__init__(
|
|
37
|
+
self,
|
|
38
|
+
registry_url=agent_config.registry_url
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
AgentCommunicationManager.__init__(
|
|
42
|
+
self,
|
|
43
|
+
self.identity_credential["issuer"],
|
|
44
|
+
default_inbox_topic=f"{self.identity_credential['issuer']}/inbox",
|
|
45
|
+
default_outbox_topic=agent_config.default_outbox_topic,
|
|
46
|
+
auto_reconnect=True,
|
|
47
|
+
message_history_limit=agent_config.message_history_limit,
|
|
48
|
+
identity_credential=self.identity_credential,
|
|
49
|
+
secret_seed=agent_config.secret_seed,
|
|
50
|
+
mqtt_broker_url=agent_config.mqtt_broker_url
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
self.update_agent_mqtt_info()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def set_agent_executor(self, agent_executor: AgentExecutor):
|
|
58
|
+
"""Set the agent executor for the agent."""
|
|
59
|
+
self.agent_executor = agent_executor
|
|
60
|
+
|
|
61
|
+
def update_agent_mqtt_info(self):
|
|
62
|
+
"""Updates the mqtt connection info of the agent into the registry so other agents can find me"""
|
|
63
|
+
print(self.agent_config.secret_seed, self.agent_config.mqtt_broker_url, f"{self.agent_config.registry_url}/agents/update-mqtt")
|
|
64
|
+
updateResponse = requests.post(
|
|
65
|
+
f"{self.agent_config.registry_url}/agents/update-mqtt",
|
|
66
|
+
data={
|
|
67
|
+
"seed": self.agent_config.secret_seed,
|
|
68
|
+
"mqttUri": self.agent_config.mqtt_broker_url
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
print(updateResponse.status_code,"====")
|
|
72
|
+
if (updateResponse.status_code != 201):
|
|
73
|
+
raise Exception("Failed to update agent connection info in p3 registry.")
|
|
74
|
+
|
|
75
|
+
print("Synced with the registry...")
|
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import uuid
|
|
6
|
+
import paho.mqtt.client as mqtt
|
|
7
|
+
|
|
8
|
+
from p3ai_agent.utils import encrypt_message, decrypt_message
|
|
9
|
+
from typing import List, Callable, Optional, Dict, Any
|
|
10
|
+
from p3ai_agent.search import AgentSearchResponse
|
|
11
|
+
|
|
12
|
+
# Configure logging with a more descriptive format
|
|
13
|
+
logging.basicConfig(
|
|
14
|
+
level=logging.INFO,
|
|
15
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
16
|
+
)
|
|
17
|
+
logger = logging.getLogger("MQTTAgentCommunication")
|
|
18
|
+
|
|
19
|
+
class MQTTMessage:
|
|
20
|
+
"""
|
|
21
|
+
Structured message format for agent communication via MQTT.
|
|
22
|
+
|
|
23
|
+
This class provides a standardized way to format, serialize, and deserialize
|
|
24
|
+
messages exchanged between agents, with support for conversation threading,
|
|
25
|
+
message types, and metadata.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
content: str,
|
|
31
|
+
sender_id: str,
|
|
32
|
+
sender_did: dict = None,
|
|
33
|
+
receiver_id: Optional[str] = None,
|
|
34
|
+
message_type: str = "query",
|
|
35
|
+
message_id: Optional[str] = None,
|
|
36
|
+
conversation_id: Optional[str] = None,
|
|
37
|
+
in_reply_to: Optional[str] = None,
|
|
38
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
39
|
+
):
|
|
40
|
+
"""
|
|
41
|
+
Initialize a new MQTT message.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
content: The main message content
|
|
45
|
+
sender_id: Identifier for the message sender
|
|
46
|
+
receiver_id: Identifier for the intended recipient (None for broadcasts)
|
|
47
|
+
message_type: Type categorization ("query", "response", "broadcast", "system")
|
|
48
|
+
message_id: Unique identifier for this message (auto-generated if None)
|
|
49
|
+
conversation_id: ID grouping related messages (auto-generated if None)
|
|
50
|
+
in_reply_to: ID of the message this is responding to (None if not a reply)
|
|
51
|
+
metadata: Additional contextual information
|
|
52
|
+
"""
|
|
53
|
+
self.content = content
|
|
54
|
+
self.sender_id = sender_id
|
|
55
|
+
self.receiver_id = receiver_id
|
|
56
|
+
self.sender_did = sender_did
|
|
57
|
+
self.message_type = message_type
|
|
58
|
+
self.message_id = message_id or str(uuid.uuid4())
|
|
59
|
+
self.conversation_id = conversation_id or str(uuid.uuid4())
|
|
60
|
+
self.in_reply_to = in_reply_to
|
|
61
|
+
self.metadata = metadata or {}
|
|
62
|
+
self.timestamp = time.time()
|
|
63
|
+
|
|
64
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
65
|
+
"""Convert message to dictionary format."""
|
|
66
|
+
return {
|
|
67
|
+
"content": self.content,
|
|
68
|
+
"sender_id": self.sender_id,
|
|
69
|
+
"sender_did": self.sender_did,
|
|
70
|
+
"receiver_id": self.receiver_id,
|
|
71
|
+
"message_type": self.message_type,
|
|
72
|
+
"message_id": self.message_id,
|
|
73
|
+
"conversation_id": self.conversation_id,
|
|
74
|
+
"in_reply_to": self.in_reply_to,
|
|
75
|
+
"metadata": self.metadata,
|
|
76
|
+
"timestamp": self.timestamp
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def to_json(self) -> str:
|
|
80
|
+
"""Convert message to JSON string for transmission."""
|
|
81
|
+
return json.dumps(self.to_dict())
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'MQTTMessage':
|
|
85
|
+
"""Create message object from dictionary data."""
|
|
86
|
+
return cls(
|
|
87
|
+
content=data.get("content", ""),
|
|
88
|
+
sender_id=data.get("sender_id", "unknown"),
|
|
89
|
+
sender_did=data.get("sender_did", "unknown"),
|
|
90
|
+
receiver_id=data.get("receiver_id"),
|
|
91
|
+
message_type=data.get("message_type", "query"),
|
|
92
|
+
message_id=data.get("message_id"),
|
|
93
|
+
conversation_id=data.get("conversation_id"),
|
|
94
|
+
in_reply_to=data.get("in_reply_to"),
|
|
95
|
+
metadata=data.get("metadata", {})
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_json(cls, json_str: str) -> 'MQTTMessage':
|
|
100
|
+
"""
|
|
101
|
+
Create message object from JSON string.
|
|
102
|
+
|
|
103
|
+
Handles both valid JSON and fallback for plain text messages.
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
data = json.loads(json_str)
|
|
107
|
+
return cls.from_dict(data)
|
|
108
|
+
except json.JSONDecodeError as e:
|
|
109
|
+
logger.error(f"Failed to parse message as JSON: {e}")
|
|
110
|
+
|
|
111
|
+
return cls(
|
|
112
|
+
content=json_str,
|
|
113
|
+
sender_id="unknown",
|
|
114
|
+
message_type="raw"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class AgentCommunicationManager:
|
|
119
|
+
"""
|
|
120
|
+
MQTT-based communication manager for LangChain agents.
|
|
121
|
+
|
|
122
|
+
This class provides tools for LangChain agents to communicate via MQTT,
|
|
123
|
+
enabling multi-agent collaboration through a publish-subscribe pattern.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
identity_credential: dict = None
|
|
127
|
+
identity_credential_connected_agent: dict = None
|
|
128
|
+
secret_seed = None
|
|
129
|
+
|
|
130
|
+
def __init__(
|
|
131
|
+
self,
|
|
132
|
+
agent_id: str,
|
|
133
|
+
default_inbox_topic: Optional[str] = None,
|
|
134
|
+
default_outbox_topic: Optional[str] = None,
|
|
135
|
+
mqtt_broker_url: str = None,
|
|
136
|
+
auto_reconnect: bool = True,
|
|
137
|
+
message_history_limit: int = 100,
|
|
138
|
+
identity_credential: dict = None,
|
|
139
|
+
secret_seed: str = None
|
|
140
|
+
):
|
|
141
|
+
"""
|
|
142
|
+
Initialize the MQTT agent communication manager.
|
|
143
|
+
|
|
144
|
+
Args:default_outbox_topic
|
|
145
|
+
agent_id: Unique identifier for this agent
|
|
146
|
+
default_inbox_topic: Topic to subscribe to by default
|
|
147
|
+
default_outbox_topic: Topic to publish to by default
|
|
148
|
+
auto_reconnect: Whether to attempt reconnection on failure
|
|
149
|
+
message_history_limit: Maximum number of messages to keep in history
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
self.agent_id = agent_id
|
|
153
|
+
self.inbox_topic = default_inbox_topic or f"{agent_id}/inbox"
|
|
154
|
+
self.outbox_topic = default_outbox_topic or f"agents/collaboration"
|
|
155
|
+
self.auto_reconnect = auto_reconnect
|
|
156
|
+
self.message_history_limit = message_history_limit
|
|
157
|
+
|
|
158
|
+
self.identity_credential = identity_credential
|
|
159
|
+
self.secret_seed = secret_seed
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
self.is_connected = False
|
|
163
|
+
self.is_agent_connected = False
|
|
164
|
+
self.subscribed_topics = set()
|
|
165
|
+
self.received_messages = []
|
|
166
|
+
self.message_history = []
|
|
167
|
+
self.pending_responses = {}
|
|
168
|
+
|
|
169
|
+
self.message_handlers = []
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
self.mqtt_client = mqtt.Client(client_id=self.agent_id)
|
|
173
|
+
self.mqtt_client.on_connect = self._handle_connect
|
|
174
|
+
self.mqtt_client.on_message = self._handle_message
|
|
175
|
+
self.mqtt_client.on_disconnect = self._handle_disconnect
|
|
176
|
+
self.mqtt_client.abstract_message_handler = None
|
|
177
|
+
self.default_mqtt_broker_url = mqtt_broker_url
|
|
178
|
+
|
|
179
|
+
self.connect_to_broker(mqtt_broker_url)
|
|
180
|
+
self.subscribe_to_topic(f"{self.agent_id}/inbox")
|
|
181
|
+
print("Agent connected to broker")
|
|
182
|
+
print(f"Subscribed to {self.agent_id}/inbox")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _handle_message(self, client, userdata, mqtt_message: MQTTMessage):
|
|
186
|
+
"""Handle incoming MQTT messages and process them appropriately."""
|
|
187
|
+
try:
|
|
188
|
+
|
|
189
|
+
payload = json.loads(mqtt_message.payload.decode('utf-8'))
|
|
190
|
+
decrypt_payload = decrypt_message(payload, self.secret_seed, self.identity_credential)
|
|
191
|
+
topic = mqtt_message.topic
|
|
192
|
+
|
|
193
|
+
logger.info(f"[{self.agent_id}] Received message on topic '{topic}'")
|
|
194
|
+
|
|
195
|
+
message = MQTTMessage.from_json(decrypt_payload)
|
|
196
|
+
|
|
197
|
+
if not self.is_agent_connected:
|
|
198
|
+
self.connect_agent({
|
|
199
|
+
"mqttUri": self.default_mqtt_broker_url,
|
|
200
|
+
"didIdentifier": message.sender_id,
|
|
201
|
+
"did": json.dumps(message.sender_did)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
structured = True
|
|
205
|
+
|
|
206
|
+
message_with_metadata = {
|
|
207
|
+
"message": message,
|
|
208
|
+
"topic": topic,
|
|
209
|
+
"received_at": time.time(),
|
|
210
|
+
"structured": structured
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
print("\nIncoming Message: ", message.content, "\n")
|
|
214
|
+
|
|
215
|
+
self.received_messages.append(message_with_metadata)
|
|
216
|
+
self.message_history.append(message_with_metadata)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
if len(self.message_history) > self.message_history_limit:
|
|
220
|
+
self.message_history = self.message_history[-self.message_history_limit:]
|
|
221
|
+
|
|
222
|
+
for handler in self.message_handlers:
|
|
223
|
+
try:
|
|
224
|
+
handler(message, topic)
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.error(f"Error in custom message handler: {e}")
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.error(f"Error processing incoming message: {e}")
|
|
230
|
+
|
|
231
|
+
def register_handler(self, handler_fn: Callable[[MQTTMessage, str], None]):
|
|
232
|
+
self.message_handlers.append(handler_fn)
|
|
233
|
+
|
|
234
|
+
def _handle_connect(self, client, userdata, flags, rc):
|
|
235
|
+
"""Handle successful connection to MQTT broker."""
|
|
236
|
+
if rc == 0:
|
|
237
|
+
self.is_connected = True
|
|
238
|
+
logger.info(f"[{self.agent_id}] Connected to MQTT broker successfully")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
self.subscribe_to_topic(self.inbox_topic)
|
|
242
|
+
logger.info(f"[{self.agent_id}] Listening for messages on {self.inbox_topic}")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
for topic in self.subscribed_topics:
|
|
246
|
+
if topic != self.inbox_topic:
|
|
247
|
+
client.subscribe(topic, qos=1)
|
|
248
|
+
logger.info(f"[{self.agent_id}] Resubscribed to {topic}")
|
|
249
|
+
else:
|
|
250
|
+
self.is_connected = False
|
|
251
|
+
error_messages = {
|
|
252
|
+
1: "Connection refused - incorrect protocol version",
|
|
253
|
+
2: "Connection refused - invalid client identifier",
|
|
254
|
+
3: "Connection refused - server unavailable",
|
|
255
|
+
4: "Connection refused - bad username or password",
|
|
256
|
+
5: "Connection refused - not authorized"
|
|
257
|
+
}
|
|
258
|
+
error_msg = error_messages.get(rc, f"Unknown error (code {rc})")
|
|
259
|
+
logger.error(f"[{self.agent_id}] Failed to connect: {error_msg}")
|
|
260
|
+
|
|
261
|
+
def _handle_disconnect(self, client, userdata, rc):
|
|
262
|
+
"""Handle disconnection from MQTT broker."""
|
|
263
|
+
self.is_connected = False
|
|
264
|
+
logger.warning(f"[{self.agent_id}] Disconnected from MQTT broker, code {rc}")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
if self.auto_reconnect:
|
|
268
|
+
logger.info(f"[{self.agent_id}] Attempting to reconnect...")
|
|
269
|
+
try:
|
|
270
|
+
client.reconnect()
|
|
271
|
+
except Exception as e:
|
|
272
|
+
logger.error(f"[{self.agent_id}] Reconnect failed: {e}")
|
|
273
|
+
|
|
274
|
+
def connect_to_broker(self, broker_url: str) -> str:
|
|
275
|
+
"""
|
|
276
|
+
Connect to an MQTT broker.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
broker_url: URL of the MQTT broker (format: mqtt://hostname:port)
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Status message about the connection attempt
|
|
283
|
+
"""
|
|
284
|
+
if self.is_connected:
|
|
285
|
+
return f"Already connected to MQTT broker as '{self.agent_id}'"
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
|
|
289
|
+
if broker_url.startswith("mqtt://"):
|
|
290
|
+
broker_url = broker_url[7:]
|
|
291
|
+
|
|
292
|
+
# Extract host and port
|
|
293
|
+
if ":" in broker_url:
|
|
294
|
+
host, port_str = broker_url.split(":")
|
|
295
|
+
port = int(port_str)
|
|
296
|
+
else:
|
|
297
|
+
host = broker_url
|
|
298
|
+
port = 1883 # Default MQTT port
|
|
299
|
+
|
|
300
|
+
# Connect to the broker
|
|
301
|
+
self.mqtt_client.connect(host, port)
|
|
302
|
+
self.mqtt_client.loop_start()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
connection_timeout = 3 # seconds
|
|
306
|
+
start_time = time.time()
|
|
307
|
+
while not self.is_connected and time.time() - start_time < connection_timeout:
|
|
308
|
+
time.sleep(0.1)
|
|
309
|
+
|
|
310
|
+
if self.is_connected:
|
|
311
|
+
return f"Connected to MQTT broker at {host}:{port} as '{self.agent_id}'"
|
|
312
|
+
else:
|
|
313
|
+
return f"Connection attempt to {host}:{port} timed out"
|
|
314
|
+
|
|
315
|
+
except Exception as e:
|
|
316
|
+
logger.error(f"[{self.agent_id}] Error connecting to MQTT broker: {e}")
|
|
317
|
+
return f"Failed to connect to MQTT broker: {str(e)}"
|
|
318
|
+
|
|
319
|
+
def disconnect_from_broker(self) -> str:
|
|
320
|
+
"""
|
|
321
|
+
Disconnect from the MQTT broker and clean up resources.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Status message about the disconnection
|
|
325
|
+
"""
|
|
326
|
+
if not self.is_connected:
|
|
327
|
+
return "Not currently connected to any MQTT broker"
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
self.mqtt_client.loop_stop()
|
|
331
|
+
self.mqtt_client.disconnect()
|
|
332
|
+
self.is_connected = False
|
|
333
|
+
self.received_messages.clear()
|
|
334
|
+
logger.info(f"[{self.agent_id}] Disconnected from MQTT broker")
|
|
335
|
+
return "Successfully disconnected from MQTT broker"
|
|
336
|
+
except Exception as e:
|
|
337
|
+
logger.error(f"[{self.agent_id}] Error during disconnection: {e}")
|
|
338
|
+
return f"Error during disconnection: {str(e)}"
|
|
339
|
+
|
|
340
|
+
def send_message(self, message_content: str, message_type: str = "query", receiver_id: Optional[str] = None) -> str:
|
|
341
|
+
"""
|
|
342
|
+
Send a message to the current outbox topic.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
message_content: The main content of the message
|
|
346
|
+
message_type: The type of message being sent
|
|
347
|
+
receiver_id: Specific recipient ID (None for broadcast)
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Status message or error
|
|
351
|
+
"""
|
|
352
|
+
if not self.is_connected:
|
|
353
|
+
return "Not connected to MQTT broker. Use connect_to_broker first."
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
# Create a structured message
|
|
357
|
+
message = MQTTMessage(
|
|
358
|
+
content=message_content,
|
|
359
|
+
sender_id=self.agent_id,
|
|
360
|
+
receiver_id=receiver_id,
|
|
361
|
+
message_type=message_type,
|
|
362
|
+
sender_did=self.identity_credential
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# Convert to JSON, encrypt and publish
|
|
366
|
+
json_payload = message.to_json()
|
|
367
|
+
encrypted_message = json.dumps(encrypt_message(json_payload, self.identity_credential_connected_agent))
|
|
368
|
+
result = self.mqtt_client.publish(self.outbox_topic, encrypted_message, qos=1)
|
|
369
|
+
|
|
370
|
+
if result.rc == 0:
|
|
371
|
+
logger.info(f"[{self.agent_id}] Message sent to '{self.outbox_topic}'")
|
|
372
|
+
|
|
373
|
+
# Add to history
|
|
374
|
+
self.message_history.append({
|
|
375
|
+
"message": message,
|
|
376
|
+
"topic": self.outbox_topic,
|
|
377
|
+
"sent_at": time.time(),
|
|
378
|
+
"direction": "outgoing"
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
# Maintain history limit
|
|
382
|
+
if len(self.message_history) > self.message_history_limit:
|
|
383
|
+
self.message_history = self.message_history[-self.message_history_limit:]
|
|
384
|
+
|
|
385
|
+
return f"Message sent successfully to topic '{self.outbox_topic}'"
|
|
386
|
+
else:
|
|
387
|
+
error_msg = f"Failed to send message, error code: {result.rc}"
|
|
388
|
+
logger.error(f"[{self.agent_id}] {error_msg}")
|
|
389
|
+
return error_msg
|
|
390
|
+
|
|
391
|
+
except Exception as e:
|
|
392
|
+
error_msg = f"Error sending message: {str(e)}"
|
|
393
|
+
logger.error(f"[{self.agent_id}] {error_msg}")
|
|
394
|
+
return error_msg
|
|
395
|
+
|
|
396
|
+
def read_messages(self) -> str:
|
|
397
|
+
"""
|
|
398
|
+
Read and clear the current message queue.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Formatted string of received messages
|
|
402
|
+
"""
|
|
403
|
+
if not self.is_connected:
|
|
404
|
+
return "Not connected to MQTT broker. Use connect_to_broker first."
|
|
405
|
+
|
|
406
|
+
if not self.received_messages:
|
|
407
|
+
return "No new messages in the queue."
|
|
408
|
+
|
|
409
|
+
# Format messages for output
|
|
410
|
+
formatted_messages = []
|
|
411
|
+
for item in self.received_messages:
|
|
412
|
+
message = item["message"]
|
|
413
|
+
topic = item["topic"]
|
|
414
|
+
|
|
415
|
+
formatted_msg = (
|
|
416
|
+
f"Topic: {topic}\n"
|
|
417
|
+
f"From: {message.sender_id}\n"
|
|
418
|
+
f"Type: {message.message_type}\n"
|
|
419
|
+
f"Content: {message.content}\n"
|
|
420
|
+
)
|
|
421
|
+
formatted_messages.append(formatted_msg)
|
|
422
|
+
|
|
423
|
+
# Create a combined output
|
|
424
|
+
output = "Messages received:\n\n" + "\n---\n".join(formatted_messages)
|
|
425
|
+
|
|
426
|
+
# Clear the received messages queue but keep them in history
|
|
427
|
+
self.received_messages.clear()
|
|
428
|
+
|
|
429
|
+
return output
|
|
430
|
+
|
|
431
|
+
def subscribe_to_topic(self, topic_name: str) -> str:
|
|
432
|
+
"""
|
|
433
|
+
Subscribe to a specific MQTT topic.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
topic_name: The MQTT topic name to subscribe to
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
Status message
|
|
440
|
+
"""
|
|
441
|
+
if not self.is_connected:
|
|
442
|
+
return "Not connected to MQTT broker. Use connect_to_broker first."
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
result = self.mqtt_client.subscribe(topic_name, qos=1)
|
|
446
|
+
if result[0] == 0:
|
|
447
|
+
self.subscribed_topics.add(topic_name)
|
|
448
|
+
logger.info(f"[{self.agent_id}] Subscribed to topic: {topic_name}")
|
|
449
|
+
return f"Successfully subscribed to topic '{topic_name}'"
|
|
450
|
+
else:
|
|
451
|
+
return f"Failed to subscribe to topic '{topic_name}', error code: {result[0]}"
|
|
452
|
+
except Exception as e:
|
|
453
|
+
logger.error(f"[{self.agent_id}] Error subscribing to topic: {e}")
|
|
454
|
+
return f"Error subscribing to topic: {str(e)}"
|
|
455
|
+
|
|
456
|
+
def unsubscribe_from_topic(self, topic_name: str) -> str:
|
|
457
|
+
"""
|
|
458
|
+
Unsubscribe from a specific MQTT topic.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
topic_name: The MQTT topic name to unsubscribe from
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Status message
|
|
465
|
+
"""
|
|
466
|
+
if not self.is_connected:
|
|
467
|
+
return "Not connected to MQTT broker. Use connect_to_broker first."
|
|
468
|
+
|
|
469
|
+
# Prevent unsubscribing from the primary inbox
|
|
470
|
+
if topic_name == self.inbox_topic:
|
|
471
|
+
return f"Cannot unsubscribe from primary inbox topic '{self.inbox_topic}'"
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
result = self.mqtt_client.unsubscribe(topic_name)
|
|
475
|
+
if result[0] == 0:
|
|
476
|
+
if topic_name in self.subscribed_topics:
|
|
477
|
+
self.subscribed_topics.remove(topic_name)
|
|
478
|
+
logger.info(f"[{self.agent_id}] Unsubscribed from topic: {topic_name}")
|
|
479
|
+
return f"Successfully unsubscribed from topic '{topic_name}'"
|
|
480
|
+
else:
|
|
481
|
+
return f"Failed to unsubscribe from topic '{topic_name}', error code: {result[0]}"
|
|
482
|
+
except Exception as e:
|
|
483
|
+
logger.error(f"[{self.agent_id}] Error unsubscribing from topic: {e}")
|
|
484
|
+
return f"Error unsubscribing from topic: {str(e)}"
|
|
485
|
+
|
|
486
|
+
def change_outbox_topic(self, topic_name: str) -> str:
|
|
487
|
+
"""
|
|
488
|
+
Change the default topic for outgoing messages.
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
topic_name: The new MQTT topic name for publishing
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
Status message
|
|
495
|
+
"""
|
|
496
|
+
previous_topic = self.outbox_topic
|
|
497
|
+
self.outbox_topic = topic_name
|
|
498
|
+
logger.info(f"[{self.agent_id}] Changed outbox topic from '{previous_topic}' to '{topic_name}'")
|
|
499
|
+
return f"Changed outbox topic to '{topic_name}'"
|
|
500
|
+
|
|
501
|
+
def add_message_handler(self, handler_function: Callable) -> None:
|
|
502
|
+
"""
|
|
503
|
+
Add a custom message handler function.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
handler_function: Function to call when messages arrive
|
|
507
|
+
Should accept (message, topic) parameters
|
|
508
|
+
"""
|
|
509
|
+
self.message_handlers.append(handler_function)
|
|
510
|
+
logger.info(f"[{self.agent_id}] Added custom message handler")
|
|
511
|
+
|
|
512
|
+
def get_connection_status(self) -> Dict[str, Any]:
|
|
513
|
+
"""
|
|
514
|
+
Get the current connection status and statistics.
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
Dictionary with connection information
|
|
518
|
+
"""
|
|
519
|
+
return {
|
|
520
|
+
"agent_id": self.agent_id,
|
|
521
|
+
"is_connected": self.is_connected,
|
|
522
|
+
"inbox_topic": self.inbox_topic,
|
|
523
|
+
"outbox_topic": self.outbox_topic,
|
|
524
|
+
"subscribed_topics": list(self.subscribed_topics),
|
|
525
|
+
"pending_messages": len(self.received_messages),
|
|
526
|
+
"message_history_count": len(self.message_history)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
def get_message_history(self, limit: int = None, filter_by_topic: str = None) -> List[Dict[str, Any]]:
|
|
530
|
+
"""
|
|
531
|
+
Get the message history with optional filtering.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
limit: Maximum number of messages to return (None for all)
|
|
535
|
+
filter_by_topic: Only return messages from this topic
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
List of message history entries
|
|
539
|
+
"""
|
|
540
|
+
history = self.message_history
|
|
541
|
+
|
|
542
|
+
# Apply topic filter if specified
|
|
543
|
+
if filter_by_topic:
|
|
544
|
+
history = [msg for msg in history if msg["topic"] == filter_by_topic]
|
|
545
|
+
|
|
546
|
+
# Apply limit if specified
|
|
547
|
+
if limit is not None:
|
|
548
|
+
history = history[-limit:]
|
|
549
|
+
|
|
550
|
+
return history
|
|
551
|
+
|
|
552
|
+
def connect_agent(self, agent: AgentSearchResponse):
|
|
553
|
+
self.connect_to_broker(agent["mqttUri"])
|
|
554
|
+
self.change_outbox_topic(f"{agent["didIdentifier"]}/inbox")
|
|
555
|
+
self.identity_credential_connected_agent = json.loads(agent["did"])
|
|
556
|
+
self.is_agent_connected = True
|