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 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