zyndai-agent 0.1.4__py3-none-any.whl → 0.2.1__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.
- zyndai_agent/__init__.py +20 -0
- zyndai_agent/agent.py +101 -25
- zyndai_agent/message.py +111 -0
- zyndai_agent/search.py +2 -1
- zyndai_agent/webhook_communication.py +466 -0
- {zyndai_agent-0.1.4.dist-info → zyndai_agent-0.2.1.dist-info}/METADATA +225 -29
- zyndai_agent-0.2.1.dist-info/RECORD +13 -0
- zyndai_agent-0.1.4.dist-info/RECORD +0 -11
- {zyndai_agent-0.1.4.dist-info → zyndai_agent-0.2.1.dist-info}/WHEEL +0 -0
- {zyndai_agent-0.1.4.dist-info → zyndai_agent-0.2.1.dist-info}/top_level.txt +0 -0
zyndai_agent/__init__.py
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from zyndai_agent.agent import ZyndAIAgent, AgentConfig
|
|
2
|
+
from zyndai_agent.communication import AgentCommunicationManager, MQTTMessage
|
|
3
|
+
from zyndai_agent.webhook_communication import WebhookCommunicationManager
|
|
4
|
+
from zyndai_agent.message import AgentMessage
|
|
5
|
+
from zyndai_agent.search import SearchAndDiscoveryManager, AgentSearchResponse
|
|
6
|
+
from zyndai_agent.identity import IdentityManager
|
|
7
|
+
from zyndai_agent.payment import X402PaymentProcessor
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"ZyndAIAgent",
|
|
11
|
+
"AgentConfig",
|
|
12
|
+
"AgentCommunicationManager",
|
|
13
|
+
"WebhookCommunicationManager",
|
|
14
|
+
"MQTTMessage",
|
|
15
|
+
"AgentMessage",
|
|
16
|
+
"SearchAndDiscoveryManager",
|
|
17
|
+
"AgentSearchResponse",
|
|
18
|
+
"IdentityManager",
|
|
19
|
+
"X402PaymentProcessor",
|
|
20
|
+
]
|
zyndai_agent/agent.py
CHANGED
|
@@ -3,6 +3,7 @@ import requests
|
|
|
3
3
|
from zyndai_agent.search import SearchAndDiscoveryManager
|
|
4
4
|
from zyndai_agent.identity import IdentityManager
|
|
5
5
|
from zyndai_agent.communication import AgentCommunicationManager
|
|
6
|
+
from zyndai_agent.webhook_communication import WebhookCommunicationManager
|
|
6
7
|
from zyndai_agent.payment import X402PaymentProcessor
|
|
7
8
|
from pydantic import BaseModel
|
|
8
9
|
from typing import Optional
|
|
@@ -13,46 +14,84 @@ class AgentConfig(BaseModel):
|
|
|
13
14
|
auto_reconnect: bool = True
|
|
14
15
|
message_history_limit: int = 100
|
|
15
16
|
registry_url: str = "http://localhost:3002"
|
|
16
|
-
|
|
17
|
+
|
|
18
|
+
# Webhook configuration (new)
|
|
19
|
+
webhook_host: Optional[str] = "0.0.0.0"
|
|
20
|
+
webhook_port: Optional[int] = 5000
|
|
21
|
+
webhook_url: Optional[str] = None # Public URL if behind NAT
|
|
22
|
+
api_key: Optional[str] = None # API key for webhook registration
|
|
23
|
+
|
|
24
|
+
# MQTT configuration (deprecated, kept for backward compatibility)
|
|
25
|
+
mqtt_broker_url: Optional[str] = None
|
|
26
|
+
default_outbox_topic: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
# Common configuration
|
|
17
29
|
identity_credential_path: str
|
|
18
30
|
identity_credential: Optional[dict] = None
|
|
19
|
-
default_outbox_topic: Optional[str] = None
|
|
20
31
|
secret_seed: str = None
|
|
32
|
+
agent_id: str = None
|
|
21
33
|
|
|
22
|
-
|
|
34
|
+
price: Optional[str] = None
|
|
35
|
+
pay_to_address: Optional[str] = None
|
|
23
36
|
|
|
24
|
-
|
|
37
|
+
class ZyndAIAgent(SearchAndDiscoveryManager, IdentityManager, X402PaymentProcessor, WebhookCommunicationManager):
|
|
38
|
+
|
|
39
|
+
def __init__(self, agent_config: AgentConfig):
|
|
25
40
|
|
|
26
41
|
self.agent_executor: CompiledStateGraph = None
|
|
27
|
-
self.agent_config = agent_config
|
|
42
|
+
self.agent_config = agent_config
|
|
28
43
|
self.x402_processor = X402PaymentProcessor(agent_config.secret_seed)
|
|
44
|
+
self.communication_mode = None # Track which mode is active
|
|
29
45
|
|
|
30
46
|
try:
|
|
31
47
|
with open(agent_config.identity_credential_path, "r") as f:
|
|
32
48
|
self.identity_credential = json.load(f)
|
|
33
49
|
except FileNotFoundError:
|
|
34
50
|
raise FileNotFoundError(f"Identity credential file not found at {agent_config.identity_credential_path}")
|
|
35
|
-
|
|
36
|
-
IdentityManager.__init__(self,agent_config.registry_url)
|
|
51
|
+
|
|
52
|
+
IdentityManager.__init__(self, agent_config.registry_url)
|
|
37
53
|
|
|
38
54
|
SearchAndDiscoveryManager.__init__(
|
|
39
55
|
self,
|
|
40
56
|
registry_url=agent_config.registry_url
|
|
41
57
|
)
|
|
42
|
-
|
|
43
|
-
AgentCommunicationManager.__init__(
|
|
44
|
-
self,
|
|
45
|
-
self.identity_credential["issuer"],
|
|
46
|
-
default_inbox_topic=f"{self.identity_credential['issuer']}/inbox",
|
|
47
|
-
default_outbox_topic=agent_config.default_outbox_topic,
|
|
48
|
-
auto_reconnect=True,
|
|
49
|
-
message_history_limit=agent_config.message_history_limit,
|
|
50
|
-
identity_credential=self.identity_credential,
|
|
51
|
-
secret_seed=agent_config.secret_seed,
|
|
52
|
-
mqtt_broker_url=agent_config.mqtt_broker_url
|
|
53
|
-
)
|
|
54
58
|
|
|
55
|
-
|
|
59
|
+
# Determine communication mode: webhook or MQTT
|
|
60
|
+
# Prefer webhook if webhook_port is configured
|
|
61
|
+
if agent_config.webhook_port is not None and agent_config.mqtt_broker_url is None:
|
|
62
|
+
# Use webhook mode
|
|
63
|
+
self.communication_mode = "webhook"
|
|
64
|
+
WebhookCommunicationManager.__init__(
|
|
65
|
+
self,
|
|
66
|
+
agent_id=self.identity_credential["issuer"],
|
|
67
|
+
webhook_host=agent_config.webhook_host,
|
|
68
|
+
webhook_port=agent_config.webhook_port,
|
|
69
|
+
webhook_url=agent_config.webhook_url,
|
|
70
|
+
auto_restart=agent_config.auto_reconnect,
|
|
71
|
+
message_history_limit=agent_config.message_history_limit,
|
|
72
|
+
identity_credential=self.identity_credential,
|
|
73
|
+
price=agent_config.price,
|
|
74
|
+
pay_to_address=agent_config.pay_to_address
|
|
75
|
+
)
|
|
76
|
+
self.update_agent_webhook_info()
|
|
77
|
+
|
|
78
|
+
elif agent_config.mqtt_broker_url is not None:
|
|
79
|
+
# Use MQTT mode (backward compatibility)
|
|
80
|
+
self.communication_mode = "mqtt"
|
|
81
|
+
AgentCommunicationManager.__init__(
|
|
82
|
+
self,
|
|
83
|
+
self.identity_credential["issuer"],
|
|
84
|
+
default_inbox_topic=f"{self.identity_credential['issuer']}/inbox",
|
|
85
|
+
default_outbox_topic=agent_config.default_outbox_topic,
|
|
86
|
+
auto_reconnect=True,
|
|
87
|
+
message_history_limit=agent_config.message_history_limit,
|
|
88
|
+
identity_credential=self.identity_credential,
|
|
89
|
+
secret_seed=agent_config.secret_seed,
|
|
90
|
+
mqtt_broker_url=agent_config.mqtt_broker_url
|
|
91
|
+
)
|
|
92
|
+
self.update_agent_mqtt_info()
|
|
93
|
+
else:
|
|
94
|
+
raise ValueError("Either webhook_port or mqtt_broker_url must be configured")
|
|
56
95
|
|
|
57
96
|
|
|
58
97
|
|
|
@@ -62,16 +101,53 @@ class ZyndAIAgent(SearchAndDiscoveryManager, IdentityManager, AgentCommunication
|
|
|
62
101
|
|
|
63
102
|
def update_agent_mqtt_info(self):
|
|
64
103
|
"""Updates the mqtt connection info of the agent into the registry so other agents can find me"""
|
|
65
|
-
|
|
66
|
-
updateResponse = requests.
|
|
67
|
-
f"{self.agent_config.registry_url}/agents/update-mqtt",
|
|
104
|
+
|
|
105
|
+
updateResponse = requests.patch(
|
|
106
|
+
f"{self.agent_config.registry_url}/agents/update-mqtt",
|
|
68
107
|
data={
|
|
69
108
|
"seed": self.agent_config.secret_seed,
|
|
70
109
|
"mqttUri": self.agent_config.mqtt_broker_url
|
|
71
110
|
}
|
|
72
111
|
)
|
|
73
|
-
|
|
112
|
+
|
|
74
113
|
if (updateResponse.status_code != 201):
|
|
75
114
|
raise Exception("Failed to update agent connection info in p3 registry.")
|
|
76
115
|
|
|
77
|
-
print("Synced with the registry...")
|
|
116
|
+
print("Synced with the registry...")
|
|
117
|
+
|
|
118
|
+
def update_agent_webhook_info(self):
|
|
119
|
+
"""Updates the webhook URL of the agent into the registry so other agents can find me"""
|
|
120
|
+
if not self.agent_config.api_key:
|
|
121
|
+
raise ValueError("API key is required for webhook registration. Please provide api_key in AgentConfig.")
|
|
122
|
+
|
|
123
|
+
# Prepare headers with API key
|
|
124
|
+
headers = {
|
|
125
|
+
"X-API-KEY": self.agent_config.api_key,
|
|
126
|
+
"Content-Type": "application/json"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Prepare request body
|
|
130
|
+
payload = {
|
|
131
|
+
"agentId": self.agent_config.agent_id,
|
|
132
|
+
"httpWebhookUrl": self.webhook_url
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
updateResponse = requests.patch(
|
|
136
|
+
f"{self.agent_config.registry_url}/agents/update-webhook",
|
|
137
|
+
json=payload,
|
|
138
|
+
headers=headers
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if updateResponse.status_code != 200:
|
|
142
|
+
raise Exception(f"Failed to update agent webhook info in Zynd registry. Status: {updateResponse.status_code}, Response: {updateResponse.text}")
|
|
143
|
+
|
|
144
|
+
print("Synced webhook URL with the registry...")
|
|
145
|
+
|
|
146
|
+
def update_agent_connection_info(self):
|
|
147
|
+
"""Updates the agent connection info (webhook or MQTT) in the registry based on communication mode"""
|
|
148
|
+
if self.communication_mode == "webhook":
|
|
149
|
+
self.update_agent_webhook_info()
|
|
150
|
+
elif self.communication_mode == "mqtt":
|
|
151
|
+
self.update_agent_mqtt_info()
|
|
152
|
+
else:
|
|
153
|
+
raise ValueError(f"Unknown communication mode: {self.communication_mode}")
|
zyndai_agent/message.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import json
|
|
3
|
+
import uuid
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Optional, Dict, Any
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
class AgentMessage:
|
|
10
|
+
"""
|
|
11
|
+
Structured message format for agent communication.
|
|
12
|
+
|
|
13
|
+
This class provides a standardized way to format, serialize, and deserialize
|
|
14
|
+
messages exchanged between agents, with support for conversation threading,
|
|
15
|
+
message types, and metadata.
|
|
16
|
+
|
|
17
|
+
Protocol-agnostic: Can be used with MQTT, HTTP webhooks, or other transports.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
content: str,
|
|
23
|
+
sender_id: str,
|
|
24
|
+
sender_did: dict = None,
|
|
25
|
+
receiver_id: Optional[str] = None,
|
|
26
|
+
message_type: str = "query",
|
|
27
|
+
message_id: Optional[str] = None,
|
|
28
|
+
conversation_id: Optional[str] = None,
|
|
29
|
+
in_reply_to: Optional[str] = None,
|
|
30
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
Initialize a new agent message.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
content: The main message content
|
|
37
|
+
sender_id: Identifier for the message sender
|
|
38
|
+
sender_did: DID credential of the sender
|
|
39
|
+
receiver_id: Identifier for the intended recipient (None for broadcasts)
|
|
40
|
+
message_type: Type categorization ("query", "response", "broadcast", "system")
|
|
41
|
+
message_id: Unique identifier for this message (auto-generated if None)
|
|
42
|
+
conversation_id: ID grouping related messages (auto-generated if None)
|
|
43
|
+
in_reply_to: ID of the message this is responding to (None if not a reply)
|
|
44
|
+
metadata: Additional contextual information
|
|
45
|
+
"""
|
|
46
|
+
self.content = content
|
|
47
|
+
self.sender_id = sender_id
|
|
48
|
+
self.receiver_id = receiver_id
|
|
49
|
+
self.sender_did = sender_did
|
|
50
|
+
self.message_type = message_type
|
|
51
|
+
self.message_id = message_id or str(uuid.uuid4())
|
|
52
|
+
self.conversation_id = conversation_id or str(uuid.uuid4())
|
|
53
|
+
self.in_reply_to = in_reply_to
|
|
54
|
+
self.metadata = metadata or {}
|
|
55
|
+
self.timestamp = time.time()
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
58
|
+
"""Convert message to dictionary format."""
|
|
59
|
+
return {
|
|
60
|
+
"content": self.content,
|
|
61
|
+
"sender_id": self.sender_id,
|
|
62
|
+
"sender_did": self.sender_did,
|
|
63
|
+
"receiver_id": self.receiver_id,
|
|
64
|
+
"message_type": self.message_type,
|
|
65
|
+
"message_id": self.message_id,
|
|
66
|
+
"conversation_id": self.conversation_id,
|
|
67
|
+
"in_reply_to": self.in_reply_to,
|
|
68
|
+
"metadata": self.metadata,
|
|
69
|
+
"timestamp": self.timestamp
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def to_json(self) -> str:
|
|
73
|
+
"""Convert message to JSON string for transmission."""
|
|
74
|
+
return json.dumps(self.to_dict())
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'AgentMessage':
|
|
78
|
+
"""Create message object from dictionary data."""
|
|
79
|
+
return cls(
|
|
80
|
+
content=data.get("content", ""),
|
|
81
|
+
sender_id=data.get("sender_id", "unknown"),
|
|
82
|
+
sender_did=data.get("sender_did", "unknown"),
|
|
83
|
+
receiver_id=data.get("receiver_id"),
|
|
84
|
+
message_type=data.get("message_type", "query"),
|
|
85
|
+
message_id=data.get("message_id"),
|
|
86
|
+
conversation_id=data.get("conversation_id"),
|
|
87
|
+
in_reply_to=data.get("in_reply_to"),
|
|
88
|
+
metadata=data.get("metadata", {})
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def from_json(cls, json_str: str) -> 'AgentMessage':
|
|
93
|
+
"""
|
|
94
|
+
Create message object from JSON string.
|
|
95
|
+
|
|
96
|
+
Handles both valid JSON and fallback for plain text messages.
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
data = json.loads(json_str)
|
|
100
|
+
return cls.from_dict(data)
|
|
101
|
+
except json.JSONDecodeError as e:
|
|
102
|
+
logger.error(f"Failed to parse message as JSON: {e}")
|
|
103
|
+
return cls(
|
|
104
|
+
content=json_str,
|
|
105
|
+
sender_id="unknown",
|
|
106
|
+
message_type="raw"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# Backward compatibility alias
|
|
111
|
+
MQTTMessage = AgentMessage
|
zyndai_agent/search.py
CHANGED
|
@@ -15,7 +15,8 @@ class AgentSearchResponse(TypedDict):
|
|
|
15
15
|
id: str
|
|
16
16
|
name: str
|
|
17
17
|
description: str
|
|
18
|
-
mqttUri: Optional[str]
|
|
18
|
+
mqttUri: Optional[str] # Deprecated, kept for backward compatibility
|
|
19
|
+
webhookUrl: Optional[str] # New field for webhook communication
|
|
19
20
|
inboxTopic: Optional[str]
|
|
20
21
|
matchScore: int
|
|
21
22
|
didIdentifier: str
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
import requests
|
|
5
|
+
from flask import Flask, request, jsonify
|
|
6
|
+
from typing import List, Callable, Optional, Dict, Any
|
|
7
|
+
from zyndai_agent.message import AgentMessage
|
|
8
|
+
from zyndai_agent.search import AgentSearchResponse
|
|
9
|
+
from x402.flask.middleware import PaymentMiddleware
|
|
10
|
+
|
|
11
|
+
# Configure logging with a more descriptive format
|
|
12
|
+
logging.basicConfig(
|
|
13
|
+
level=logging.INFO,
|
|
14
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
15
|
+
)
|
|
16
|
+
logger = logging.getLogger("WebhookAgentCommunication")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WebhookCommunicationManager:
|
|
20
|
+
"""
|
|
21
|
+
HTTP Webhook-based communication manager for LangChain agents.
|
|
22
|
+
|
|
23
|
+
This class provides tools for LangChain agents to communicate via HTTP webhooks,
|
|
24
|
+
enabling multi-agent collaboration through a request-response pattern.
|
|
25
|
+
Each agent runs an embedded Flask server to receive messages.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
identity_credential: dict = None
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
agent_id: str,
|
|
33
|
+
webhook_host: str = "0.0.0.0",
|
|
34
|
+
webhook_port: int = 5000,
|
|
35
|
+
webhook_url: Optional[str] = None,
|
|
36
|
+
auto_restart: bool = True,
|
|
37
|
+
message_history_limit: int = 100,
|
|
38
|
+
identity_credential: dict = None,
|
|
39
|
+
price: Optional[str] = "$0.01",
|
|
40
|
+
pay_to_address: Optional[str] = None
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Initialize the webhook agent communication manager.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
agent_id: Unique identifier for this agent
|
|
47
|
+
webhook_host: Host address to bind the webhook server (default: 0.0.0.0)
|
|
48
|
+
webhook_port: Port number for the webhook server (default: 5000)
|
|
49
|
+
webhook_url: Public webhook URL (auto-generated if None)
|
|
50
|
+
auto_restart: Whether to attempt restart on failure
|
|
51
|
+
message_history_limit: Maximum number of messages to keep in history
|
|
52
|
+
identity_credential: DID credential for this agent
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
self.agent_id = agent_id
|
|
56
|
+
self.webhook_host = webhook_host
|
|
57
|
+
self.webhook_port = webhook_port
|
|
58
|
+
self.webhook_url = webhook_url
|
|
59
|
+
self.auto_restart = auto_restart
|
|
60
|
+
self.message_history_limit = message_history_limit
|
|
61
|
+
|
|
62
|
+
self.identity_credential = identity_credential
|
|
63
|
+
|
|
64
|
+
self.is_running = False
|
|
65
|
+
self.is_agent_connected = False
|
|
66
|
+
self.received_messages = []
|
|
67
|
+
self.message_history = []
|
|
68
|
+
self.message_handlers = []
|
|
69
|
+
self.target_webhook_url = None
|
|
70
|
+
self.pending_responses = {} # Store responses by message_id
|
|
71
|
+
|
|
72
|
+
# Thread safety
|
|
73
|
+
self._lock = threading.Lock()
|
|
74
|
+
|
|
75
|
+
# Create Flask app
|
|
76
|
+
self.flask_app = Flask(f"agent_{agent_id}")
|
|
77
|
+
self.flask_app.logger.setLevel(logging.ERROR) # Suppress Flask logging
|
|
78
|
+
|
|
79
|
+
if price != None and pay_to_address != None:
|
|
80
|
+
self.payment_middleware = PaymentMiddleware(self.flask_app)
|
|
81
|
+
self.payment_middleware.add(
|
|
82
|
+
price,
|
|
83
|
+
pay_to_address,
|
|
84
|
+
path="/webhook"
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
print("Disabling x402, X402 payment config not provided")
|
|
88
|
+
|
|
89
|
+
self._setup_routes()
|
|
90
|
+
|
|
91
|
+
# Start webhook server
|
|
92
|
+
self.start_webhook_server()
|
|
93
|
+
|
|
94
|
+
print("Agent webhook server started")
|
|
95
|
+
print(f"Listening on {self.webhook_url}")
|
|
96
|
+
|
|
97
|
+
def _setup_routes(self):
|
|
98
|
+
"""Setup Flask routes for webhook endpoints."""
|
|
99
|
+
|
|
100
|
+
@self.flask_app.route('/webhook', methods=['POST'])
|
|
101
|
+
def webhook_handler():
|
|
102
|
+
return self._handle_webhook_request(sync=False)
|
|
103
|
+
|
|
104
|
+
@self.flask_app.route('/webhook/sync', methods=['POST'])
|
|
105
|
+
def webhook_sync_handler():
|
|
106
|
+
return self._handle_webhook_request(sync=True)
|
|
107
|
+
|
|
108
|
+
@self.flask_app.route('/health', methods=['GET'])
|
|
109
|
+
def health_check():
|
|
110
|
+
return jsonify({
|
|
111
|
+
"status": "ok",
|
|
112
|
+
"agent_id": self.agent_id,
|
|
113
|
+
"timestamp": time.time()
|
|
114
|
+
}), 200
|
|
115
|
+
|
|
116
|
+
def _handle_webhook_request(self, sync=False):
|
|
117
|
+
"""Handle incoming webhook POST requests."""
|
|
118
|
+
try:
|
|
119
|
+
# Verify request is JSON
|
|
120
|
+
if not request.is_json:
|
|
121
|
+
logger.error("Received non-JSON request")
|
|
122
|
+
return jsonify({"error": "Content-Type must be application/json"}), 400
|
|
123
|
+
|
|
124
|
+
payload = request.get_json()
|
|
125
|
+
|
|
126
|
+
# Parse message from dict (request.get_json() returns a dict, not a string)
|
|
127
|
+
message = AgentMessage.from_dict(payload)
|
|
128
|
+
|
|
129
|
+
logger.info(f"[{self.agent_id}] Received message from {message.sender_id}")
|
|
130
|
+
|
|
131
|
+
# Auto-connect to sender if not connected
|
|
132
|
+
if not self.is_agent_connected:
|
|
133
|
+
self.is_agent_connected = True
|
|
134
|
+
|
|
135
|
+
# Store in history
|
|
136
|
+
message_with_metadata = {
|
|
137
|
+
"message": message,
|
|
138
|
+
"received_at": time.time(),
|
|
139
|
+
"structured": True,
|
|
140
|
+
"source_ip": request.remote_addr
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
print("\nIncoming Message: ", message.content, "\n")
|
|
144
|
+
|
|
145
|
+
with self._lock:
|
|
146
|
+
self.received_messages.append(message_with_metadata)
|
|
147
|
+
self.message_history.append(message_with_metadata)
|
|
148
|
+
|
|
149
|
+
# Maintain history limit
|
|
150
|
+
if len(self.message_history) > self.message_history_limit:
|
|
151
|
+
self.message_history = self.message_history[-self.message_history_limit:]
|
|
152
|
+
|
|
153
|
+
# Check if synchronous response is requested
|
|
154
|
+
if sync:
|
|
155
|
+
# Wait for handler to process and store response
|
|
156
|
+
# Invoke message handlers synchronously
|
|
157
|
+
for handler in self.message_handlers:
|
|
158
|
+
try:
|
|
159
|
+
handler(message, None) # No topic in webhook context
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.error(f"Error in message handler: {e}")
|
|
162
|
+
|
|
163
|
+
# Wait for response (with timeout)
|
|
164
|
+
timeout = 30 # 30 seconds
|
|
165
|
+
start_time = time.time()
|
|
166
|
+
while time.time() - start_time < timeout:
|
|
167
|
+
with self._lock:
|
|
168
|
+
if message.message_id in self.pending_responses:
|
|
169
|
+
response = self.pending_responses.pop(message.message_id)
|
|
170
|
+
return jsonify({
|
|
171
|
+
"status": "success",
|
|
172
|
+
"message_id": message.message_id,
|
|
173
|
+
"response": response,
|
|
174
|
+
"timestamp": time.time()
|
|
175
|
+
}), 200
|
|
176
|
+
time.sleep(0.1) # Small delay to avoid busy waiting
|
|
177
|
+
|
|
178
|
+
# Timeout - no response received
|
|
179
|
+
return jsonify({
|
|
180
|
+
"status": "timeout",
|
|
181
|
+
"message_id": message.message_id,
|
|
182
|
+
"error": "Agent did not respond within timeout period",
|
|
183
|
+
"timestamp": time.time()
|
|
184
|
+
}), 408
|
|
185
|
+
else:
|
|
186
|
+
# Async mode - invoke handlers and return immediately
|
|
187
|
+
for handler in self.message_handlers:
|
|
188
|
+
try:
|
|
189
|
+
handler(message, None) # No topic in webhook context
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error(f"Error in message handler: {e}")
|
|
192
|
+
|
|
193
|
+
# Return success
|
|
194
|
+
return jsonify({
|
|
195
|
+
"status": "received",
|
|
196
|
+
"message_id": message.message_id,
|
|
197
|
+
"timestamp": time.time()
|
|
198
|
+
}), 200
|
|
199
|
+
|
|
200
|
+
except Exception as e:
|
|
201
|
+
logger.error(f"Error handling webhook request: {e}")
|
|
202
|
+
return jsonify({"error": str(e)}), 500
|
|
203
|
+
|
|
204
|
+
def start_webhook_server(self):
|
|
205
|
+
"""Start Flask webhook server in background thread."""
|
|
206
|
+
if self.is_running:
|
|
207
|
+
logger.warning("Webhook server already running")
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
# Try to bind to configured port, retry with different ports if needed
|
|
211
|
+
max_retries = 10
|
|
212
|
+
server_started = False
|
|
213
|
+
|
|
214
|
+
for attempt in range(max_retries):
|
|
215
|
+
try:
|
|
216
|
+
port = self.webhook_port + attempt
|
|
217
|
+
|
|
218
|
+
def run_flask():
|
|
219
|
+
self.flask_app.run(
|
|
220
|
+
host=self.webhook_host,
|
|
221
|
+
port=port,
|
|
222
|
+
debug=False,
|
|
223
|
+
use_reloader=False,
|
|
224
|
+
threaded=True
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
self.flask_thread = threading.Thread(
|
|
228
|
+
target=run_flask,
|
|
229
|
+
daemon=True,
|
|
230
|
+
name=f"WebhookServer-{self.agent_id}"
|
|
231
|
+
)
|
|
232
|
+
self.flask_thread.start()
|
|
233
|
+
|
|
234
|
+
# Update actual port used
|
|
235
|
+
self.webhook_port = port
|
|
236
|
+
|
|
237
|
+
# Update webhook URL if not manually configured
|
|
238
|
+
if self.webhook_url is None:
|
|
239
|
+
# Use localhost for local development, can be overridden
|
|
240
|
+
self.webhook_url = f"http://localhost:{port}/webhook"
|
|
241
|
+
|
|
242
|
+
self.is_running = True
|
|
243
|
+
server_started = True
|
|
244
|
+
|
|
245
|
+
# Wait for server to start
|
|
246
|
+
time.sleep(1.5)
|
|
247
|
+
|
|
248
|
+
logger.info(f"Webhook server started on {self.webhook_host}:{port}")
|
|
249
|
+
break
|
|
250
|
+
|
|
251
|
+
except OSError as e:
|
|
252
|
+
if "Address already in use" in str(e) and attempt < max_retries - 1:
|
|
253
|
+
logger.warning(f"Port {port} already in use, trying next port...")
|
|
254
|
+
continue
|
|
255
|
+
else:
|
|
256
|
+
logger.error(f"Failed to start webhook server: {e}")
|
|
257
|
+
raise
|
|
258
|
+
|
|
259
|
+
if not server_started:
|
|
260
|
+
raise RuntimeError("Failed to start webhook server after multiple attempts")
|
|
261
|
+
|
|
262
|
+
def stop_webhook_server(self):
|
|
263
|
+
"""Stop the webhook server and cleanup resources."""
|
|
264
|
+
if not self.is_running:
|
|
265
|
+
logger.warning("Webhook server not running")
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
self.is_running = False
|
|
269
|
+
logger.info(f"[{self.agent_id}] Webhook server stopped")
|
|
270
|
+
|
|
271
|
+
def send_message(
|
|
272
|
+
self,
|
|
273
|
+
message_content: str,
|
|
274
|
+
message_type: str = "query",
|
|
275
|
+
receiver_id: Optional[str] = None
|
|
276
|
+
) -> str:
|
|
277
|
+
"""
|
|
278
|
+
Send a message to another agent via HTTP POST.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
message_content: The main content of the message
|
|
282
|
+
message_type: The type of message being sent
|
|
283
|
+
receiver_id: Specific recipient ID
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Status message or error
|
|
287
|
+
"""
|
|
288
|
+
if not self.is_running:
|
|
289
|
+
return "Webhook server not running. Cannot send messages."
|
|
290
|
+
|
|
291
|
+
if not self.target_webhook_url:
|
|
292
|
+
return "No target agent connected. Use connect_agent() first."
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
# Create structured message
|
|
296
|
+
message = AgentMessage(
|
|
297
|
+
content=message_content,
|
|
298
|
+
sender_id=self.agent_id,
|
|
299
|
+
receiver_id=receiver_id,
|
|
300
|
+
message_type=message_type,
|
|
301
|
+
sender_did=self.identity_credential
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Convert to JSON and send directly
|
|
305
|
+
json_payload = message.to_json()
|
|
306
|
+
|
|
307
|
+
# Send HTTP POST request with plain JSON
|
|
308
|
+
response = requests.post(
|
|
309
|
+
self.target_webhook_url,
|
|
310
|
+
json=json_payload,
|
|
311
|
+
headers={"Content-Type": "application/json"},
|
|
312
|
+
timeout=30 # 30 second timeout
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Check response
|
|
316
|
+
if response.status_code == 200:
|
|
317
|
+
logger.info(f"Message sent successfully to {self.target_webhook_url}")
|
|
318
|
+
|
|
319
|
+
# Add to history
|
|
320
|
+
with self._lock:
|
|
321
|
+
self.message_history.append({
|
|
322
|
+
"message": message,
|
|
323
|
+
"sent_at": time.time(),
|
|
324
|
+
"direction": "outgoing",
|
|
325
|
+
"target_url": self.target_webhook_url
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
if len(self.message_history) > self.message_history_limit:
|
|
329
|
+
self.message_history = self.message_history[-self.message_history_limit:]
|
|
330
|
+
|
|
331
|
+
return f"Message sent successfully to topic '{self.target_webhook_url}'"
|
|
332
|
+
else:
|
|
333
|
+
error_msg = f"Failed to send message. HTTP {response.status_code}: {response.text}"
|
|
334
|
+
logger.error(error_msg)
|
|
335
|
+
return error_msg
|
|
336
|
+
|
|
337
|
+
except requests.exceptions.Timeout:
|
|
338
|
+
error_msg = "Error: Request timed out. Target agent may be offline."
|
|
339
|
+
logger.error(error_msg)
|
|
340
|
+
return error_msg
|
|
341
|
+
except requests.exceptions.ConnectionError:
|
|
342
|
+
error_msg = "Error: Could not connect to target agent. Agent may be offline."
|
|
343
|
+
logger.error(error_msg)
|
|
344
|
+
return error_msg
|
|
345
|
+
except Exception as e:
|
|
346
|
+
error_msg = f"Error sending message: {str(e)}"
|
|
347
|
+
logger.error(f"[{self.agent_id}] {error_msg}")
|
|
348
|
+
return error_msg
|
|
349
|
+
|
|
350
|
+
def read_messages(self) -> str:
|
|
351
|
+
"""
|
|
352
|
+
Read and clear the current message queue.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Formatted string of received messages
|
|
356
|
+
"""
|
|
357
|
+
if not self.is_running:
|
|
358
|
+
return "Webhook server not running."
|
|
359
|
+
|
|
360
|
+
if not self.received_messages:
|
|
361
|
+
return "No new messages in the queue."
|
|
362
|
+
|
|
363
|
+
# Format messages for output
|
|
364
|
+
formatted_messages = []
|
|
365
|
+
for item in self.received_messages:
|
|
366
|
+
message = item["message"]
|
|
367
|
+
|
|
368
|
+
formatted_msg = (
|
|
369
|
+
f"From: {message.sender_id}\n"
|
|
370
|
+
f"Type: {message.message_type}\n"
|
|
371
|
+
f"Content: {message.content}\n"
|
|
372
|
+
)
|
|
373
|
+
formatted_messages.append(formatted_msg)
|
|
374
|
+
|
|
375
|
+
# Create a combined output
|
|
376
|
+
output = "Messages received:\n\n" + "\n---\n".join(formatted_messages)
|
|
377
|
+
|
|
378
|
+
# Clear the received messages queue but keep them in history
|
|
379
|
+
with self._lock:
|
|
380
|
+
self.received_messages.clear()
|
|
381
|
+
|
|
382
|
+
return output
|
|
383
|
+
|
|
384
|
+
def add_message_handler(self, handler_function: Callable) -> None:
|
|
385
|
+
"""
|
|
386
|
+
Add a custom message handler function.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
handler_function: Function to call when messages arrive
|
|
390
|
+
Should accept (message, topic) parameters
|
|
391
|
+
"""
|
|
392
|
+
with self._lock:
|
|
393
|
+
self.message_handlers.append(handler_function)
|
|
394
|
+
logger.info(f"[{self.agent_id}] Added custom message handler")
|
|
395
|
+
|
|
396
|
+
def register_handler(self, handler_fn: Callable[[AgentMessage, str], None]):
|
|
397
|
+
"""Alias for add_message_handler for backward compatibility."""
|
|
398
|
+
self.add_message_handler(handler_fn)
|
|
399
|
+
|
|
400
|
+
def set_response(self, message_id: str, response: str):
|
|
401
|
+
"""
|
|
402
|
+
Set a response for a specific message ID (for synchronous responses).
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
message_id: The ID of the message being responded to
|
|
406
|
+
response: The response content
|
|
407
|
+
"""
|
|
408
|
+
with self._lock:
|
|
409
|
+
self.pending_responses[message_id] = response
|
|
410
|
+
logger.info(f"[{self.agent_id}] Set response for message {message_id}")
|
|
411
|
+
|
|
412
|
+
def get_connection_status(self) -> Dict[str, Any]:
|
|
413
|
+
"""
|
|
414
|
+
Get the current webhook server status and statistics.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Dictionary with connection information
|
|
418
|
+
"""
|
|
419
|
+
return {
|
|
420
|
+
"agent_id": self.agent_id,
|
|
421
|
+
"is_running": self.is_running,
|
|
422
|
+
"webhook_url": self.webhook_url,
|
|
423
|
+
"webhook_port": self.webhook_port,
|
|
424
|
+
"target_webhook_url": self.target_webhook_url,
|
|
425
|
+
"pending_messages": len(self.received_messages),
|
|
426
|
+
"message_history_count": len(self.message_history)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
def get_message_history(
|
|
430
|
+
self,
|
|
431
|
+
limit: int = None,
|
|
432
|
+
filter_by_topic: str = None
|
|
433
|
+
) -> List[Dict[str, Any]]:
|
|
434
|
+
"""
|
|
435
|
+
Get the message history with optional filtering.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
limit: Maximum number of messages to return (None for all)
|
|
439
|
+
filter_by_topic: Only return messages from this topic (not applicable for webhooks)
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
List of message history entries
|
|
443
|
+
"""
|
|
444
|
+
with self._lock:
|
|
445
|
+
history = self.message_history.copy()
|
|
446
|
+
|
|
447
|
+
# Apply limit if specified
|
|
448
|
+
if limit is not None:
|
|
449
|
+
history = history[-limit:]
|
|
450
|
+
|
|
451
|
+
return history
|
|
452
|
+
|
|
453
|
+
def connect_agent(self, agent: AgentSearchResponse):
|
|
454
|
+
"""
|
|
455
|
+
Connect to another agent using their webhook URL.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
agent: Agent search response containing webhookUrl
|
|
459
|
+
"""
|
|
460
|
+
if "webhookUrl" not in agent:
|
|
461
|
+
raise ValueError("Agent does not have webhookUrl. Cannot connect via webhook.")
|
|
462
|
+
|
|
463
|
+
self.target_webhook_url = agent["webhookUrl"]
|
|
464
|
+
self.is_agent_connected = True
|
|
465
|
+
|
|
466
|
+
logger.info(f"Connected to agent {agent['didIdentifier']} at {self.target_webhook_url}")
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zyndai-agent
|
|
3
|
-
Version: 0.1
|
|
4
|
-
Summary: A Langchain and Autogen wrapper that enables agents to communicate and establish identity on the Zynd AI Network. This SDK provides three core capabilities: Identity Management, Agent Discovery & Search, and MQTT-based Communication.
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: A Langchain and Autogen wrapper that enables agents to communicate and establish identity on the Zynd AI Network. This SDK provides three core capabilities: Identity Management, Agent Discovery & Search, and HTTP Webhook or MQTT-based Communication.
|
|
5
5
|
Author-email: Swapnil Shinde <swapnilshinde9382@gmail.com>
|
|
6
6
|
Requires-Python: >=3.12
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Requires-Dist: base58>=2.1.1
|
|
9
9
|
Requires-Dist: cryptography>=46.0.3
|
|
10
10
|
Requires-Dist: eth-account>=0.13.7
|
|
11
|
-
Requires-Dist:
|
|
12
|
-
Requires-Dist: langchain
|
|
13
|
-
Requires-Dist: langchain-openai>=1.0.2
|
|
11
|
+
Requires-Dist: flask>=3.0.0
|
|
12
|
+
Requires-Dist: langchain>=1.1.0
|
|
14
13
|
Requires-Dist: paho-mqtt>=2.1.0
|
|
15
|
-
Requires-Dist:
|
|
14
|
+
Requires-Dist: requests>=2.31.0
|
|
15
|
+
Requires-Dist: x402==1.0.0
|
|
16
16
|
|
|
17
17
|
# ZyndAI Agent SDK
|
|
18
18
|
|
|
@@ -22,7 +22,7 @@ A powerful Python SDK that enables AI agents to communicate securely and discove
|
|
|
22
22
|
|
|
23
23
|
- 🔐 **Secure Identity Management**: Verify and manage agent identities using Polygon ID credentials
|
|
24
24
|
- 🔍 **Smart Agent Discovery**: Search and discover agents based on their capabilities with ML-powered semantic matching
|
|
25
|
-
- 💬 **
|
|
25
|
+
- 💬 **Flexible Communication**: Choose between HTTP Webhooks or MQTT for encrypted real-time messaging between agents
|
|
26
26
|
- 🤖 **LangChain Integration**: Seamlessly works with LangChain agents and any LLM
|
|
27
27
|
- 💰 **x402 Micropayments**: Built-in support for pay-per-use API endpoints with automatic payment handling
|
|
28
28
|
- 🌐 **Decentralized Network**: Connect to the global ZyndAI agent network
|
|
@@ -37,7 +37,7 @@ pip install zyndai-agent
|
|
|
37
37
|
|
|
38
38
|
Or install from source:
|
|
39
39
|
```bash
|
|
40
|
-
git clone https://github.com/
|
|
40
|
+
git clone https://github.com/Zynd-AI-Network/zyndai-agent.git
|
|
41
41
|
cd zyndai-agent
|
|
42
42
|
pip install -r requirements.txt
|
|
43
43
|
```
|
|
@@ -46,18 +46,46 @@ pip install -r requirements.txt
|
|
|
46
46
|
|
|
47
47
|
### 1. Get Your Credentials
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
Follow these steps to set up your agent credentials from the ZyndAI Dashboard:
|
|
50
|
+
|
|
51
|
+
1. **Visit the Dashboard**
|
|
52
|
+
- Go to [dashboard.zynd.ai](https://dashboard.zynd.ai)
|
|
53
|
+
- Click "Get Started"
|
|
54
|
+
|
|
55
|
+
2. **Connect Your Wallet**
|
|
56
|
+
- Connect your MetaMask wallet
|
|
57
|
+
- Ensure you're on the correct network
|
|
58
|
+
|
|
59
|
+
3. **Create Your Agent**
|
|
60
|
+
- Navigate to the "Agents" section
|
|
61
|
+
- Click "Create New Agent"
|
|
62
|
+
- Fill in your agent's details (name, description, capabilities)
|
|
63
|
+
- Submit to create your agent
|
|
64
|
+
|
|
65
|
+
4. **Get Your Agent Seed**
|
|
66
|
+
- After creating the agent, view your agent's details
|
|
67
|
+
- Copy the **Agent Seed** (secret seed phrase)
|
|
68
|
+
- Save this securely - you'll need it for your `.env` file
|
|
69
|
+
|
|
70
|
+
5. **Download DID Credential Document**
|
|
71
|
+
- In your agent's view, go to the **Credentials** tab
|
|
72
|
+
- Copy or download the **DID Document Credential**
|
|
73
|
+
- Save this as `identity_credential.json` in your project directory
|
|
52
74
|
|
|
53
75
|
### 2. Environment Setup
|
|
54
76
|
|
|
55
|
-
Create a `.env` file:
|
|
77
|
+
Create a `.env` file in your project root:
|
|
56
78
|
```env
|
|
57
|
-
AGENT_SEED=
|
|
79
|
+
AGENT_SEED=your_agent_seed_from_dashboard
|
|
80
|
+
API_KEY=your_api_key_from_dashboard
|
|
58
81
|
OPENAI_API_KEY=your_openai_api_key_here
|
|
59
82
|
```
|
|
60
83
|
|
|
84
|
+
**Important Notes:**
|
|
85
|
+
- Keep your `AGENT_SEED` and `identity_credential.json` secure and never commit them to version control
|
|
86
|
+
- The agent seed and DID credential must match - they are cryptographically linked
|
|
87
|
+
- Add both `.env` and `identity_credential.json` to your `.gitignore` file
|
|
88
|
+
|
|
61
89
|
### 3. Basic Agent Example
|
|
62
90
|
```python
|
|
63
91
|
from zyndai_agent.agent import AgentConfig, ZyndAIAgent
|
|
@@ -206,7 +234,7 @@ Create LangChain tools that leverage x402-enabled paid APIs:
|
|
|
206
234
|
```python
|
|
207
235
|
from langchain_core.tools import tool
|
|
208
236
|
from langchain_openai import ChatOpenAI
|
|
209
|
-
from
|
|
237
|
+
from langchain_classic.agents import create_tool_calling_agent, AgentExecutor
|
|
210
238
|
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
|
211
239
|
from zyndai_agent.agent import AgentConfig, ZyndAIAgent
|
|
212
240
|
import os
|
|
@@ -318,12 +346,37 @@ for agent in agents:
|
|
|
318
346
|
|
|
319
347
|
### 💬 Secure Communication
|
|
320
348
|
|
|
321
|
-
|
|
349
|
+
The SDK supports two communication modes: **HTTP Webhooks** (recommended) and **MQTT** (legacy). Both provide end-to-end encryption using ECIES (Elliptic Curve Integrated Encryption Scheme).
|
|
350
|
+
|
|
351
|
+
#### HTTP Webhook Mode (Recommended)
|
|
352
|
+
|
|
353
|
+
Each agent runs an embedded Flask server to receive webhook requests. This mode is simpler, doesn't require external MQTT brokers, and works well for most use cases.
|
|
354
|
+
|
|
322
355
|
```python
|
|
356
|
+
from zyndai_agent.agent import AgentConfig, ZyndAIAgent
|
|
357
|
+
import os
|
|
358
|
+
|
|
359
|
+
# Configure with webhook mode
|
|
360
|
+
agent_config = AgentConfig(
|
|
361
|
+
webhook_host="0.0.0.0", # Listen on all interfaces
|
|
362
|
+
webhook_port=5000, # Port for webhook server
|
|
363
|
+
webhook_url=None, # Auto-generated or specify public URL
|
|
364
|
+
api_key=os.environ["API_KEY"], # API key for webhook registration
|
|
365
|
+
auto_reconnect=True,
|
|
366
|
+
message_history_limit=100,
|
|
367
|
+
registry_url="https://registry.zynd.ai",
|
|
368
|
+
identity_credential_path="./identity_credential.json",
|
|
369
|
+
secret_seed=os.environ["AGENT_SEED"]
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Agent automatically starts webhook server
|
|
373
|
+
zyndai_agent = ZyndAIAgent(agent_config=agent_config)
|
|
374
|
+
print(f"Webhook server running at: {zyndai_agent.webhook_url}")
|
|
375
|
+
|
|
323
376
|
# Connect to a discovered agent
|
|
324
377
|
zyndai_agent.connect_agent(selected_agent)
|
|
325
378
|
|
|
326
|
-
# Send encrypted message
|
|
379
|
+
# Send encrypted message via HTTP POST
|
|
327
380
|
result = zyndai_agent.send_message(
|
|
328
381
|
message_content="Can you help me analyze this dataset?",
|
|
329
382
|
message_type="query"
|
|
@@ -333,6 +386,44 @@ result = zyndai_agent.send_message(
|
|
|
333
386
|
messages = zyndai_agent.read_messages()
|
|
334
387
|
```
|
|
335
388
|
|
|
389
|
+
**Webhook Mode Features:**
|
|
390
|
+
- ✅ No external broker required
|
|
391
|
+
- ✅ Standard HTTP/HTTPS communication
|
|
392
|
+
- ✅ Easy to deploy and debug
|
|
393
|
+
- ✅ Works behind firewalls with port forwarding
|
|
394
|
+
- ✅ Auto-retry on port conflicts (tries ports 5000-5010)
|
|
395
|
+
- ✅ Built-in health check endpoint (`/health`)
|
|
396
|
+
|
|
397
|
+
#### MQTT Mode (Legacy)
|
|
398
|
+
|
|
399
|
+
Traditional MQTT broker-based communication. Requires a running MQTT broker.
|
|
400
|
+
|
|
401
|
+
```python
|
|
402
|
+
agent_config = AgentConfig(
|
|
403
|
+
mqtt_broker_url="mqtt://registry.zynd.ai:1883", # MQTT broker
|
|
404
|
+
default_outbox_topic=None,
|
|
405
|
+
auto_reconnect=True,
|
|
406
|
+
message_history_limit=100,
|
|
407
|
+
registry_url="https://registry.zynd.ai",
|
|
408
|
+
identity_credential_path="./identity_credential.json",
|
|
409
|
+
secret_seed=os.environ["AGENT_SEED"]
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
zyndai_agent = ZyndAIAgent(agent_config=agent_config)
|
|
413
|
+
|
|
414
|
+
# Connect to a discovered agent
|
|
415
|
+
zyndai_agent.connect_agent(selected_agent)
|
|
416
|
+
|
|
417
|
+
# Send encrypted message via MQTT
|
|
418
|
+
result = zyndai_agent.send_message(
|
|
419
|
+
message_content="Can you help me analyze this dataset?",
|
|
420
|
+
message_type="query"
|
|
421
|
+
)
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Migration from MQTT to Webhooks:**
|
|
425
|
+
To migrate existing agents, simply change your configuration from `mqtt_broker_url` to `webhook_host` and `webhook_port`. All other code remains the same!
|
|
426
|
+
|
|
336
427
|
### 🔐 Identity Verification
|
|
337
428
|
|
|
338
429
|
Verify other agents' identities before trusting them:
|
|
@@ -442,9 +533,9 @@ from zyndai_agent.agent import AgentConfig, ZyndAIAgent
|
|
|
442
533
|
from zyndai_agent.communication import MQTTMessage
|
|
443
534
|
from langchain_openai import ChatOpenAI
|
|
444
535
|
from langchain_core.tools import tool
|
|
445
|
-
from
|
|
536
|
+
from langchain_classic.agents import create_tool_calling_agent, AgentExecutor
|
|
446
537
|
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
|
447
|
-
from
|
|
538
|
+
from langchain_core.chat_history import InMemoryChatMessageHistory
|
|
448
539
|
import json
|
|
449
540
|
|
|
450
541
|
@tool
|
|
@@ -496,10 +587,12 @@ zyndai_agent = ZyndAIAgent(agent_config=agent_config)
|
|
|
496
587
|
# Create LangChain agent with custom tool
|
|
497
588
|
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
|
|
498
589
|
tools = [compare_stocks]
|
|
499
|
-
|
|
590
|
+
|
|
591
|
+
# Create message history store
|
|
592
|
+
message_history = InMemoryChatMessageHistory()
|
|
500
593
|
|
|
501
594
|
prompt = ChatPromptTemplate.from_messages([
|
|
502
|
-
("system", """You are a Stock Comparison Agent.
|
|
595
|
+
("system", """You are a Stock Comparison Agent.
|
|
503
596
|
Use the compare_stocks tool to analyze stock data.
|
|
504
597
|
Capabilities: stock_comparison, financial_analysis, investment_advice"""),
|
|
505
598
|
MessagesPlaceholder(variable_name="chat_history"),
|
|
@@ -508,14 +601,25 @@ prompt = ChatPromptTemplate.from_messages([
|
|
|
508
601
|
])
|
|
509
602
|
|
|
510
603
|
agent = create_tool_calling_agent(llm, tools, prompt)
|
|
511
|
-
agent_executor = AgentExecutor(agent=agent, tools=tools,
|
|
604
|
+
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
|
|
512
605
|
|
|
513
606
|
zyndai_agent.set_agent_executor(agent_executor)
|
|
514
607
|
|
|
515
608
|
# Message handler
|
|
516
609
|
def message_handler(message: MQTTMessage, topic: str):
|
|
517
610
|
print(f"Received: {message.content}")
|
|
518
|
-
|
|
611
|
+
|
|
612
|
+
# Add user message to history
|
|
613
|
+
message_history.add_user_message(message.content)
|
|
614
|
+
|
|
615
|
+
response = zyndai_agent.agent_executor.invoke({
|
|
616
|
+
"input": message.content,
|
|
617
|
+
"chat_history": message_history.messages
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
# Add AI response to history
|
|
621
|
+
message_history.add_ai_message(response["output"])
|
|
622
|
+
|
|
519
623
|
zyndai_agent.send_message(response["output"])
|
|
520
624
|
|
|
521
625
|
zyndai_agent.add_message_handler(message_handler)
|
|
@@ -535,13 +639,21 @@ while True:
|
|
|
535
639
|
|
|
536
640
|
| Parameter | Type | Default | Description |
|
|
537
641
|
|-----------|------|---------|-------------|
|
|
538
|
-
| `
|
|
642
|
+
| `webhook_host` | `str` | `"0.0.0.0"` | **Webhook mode**: Host address to bind webhook server |
|
|
643
|
+
| `webhook_port` | `int` | `5000` | **Webhook mode**: Port number for webhook server |
|
|
644
|
+
| `webhook_url` | `str` | `None` | **Webhook mode**: Public URL (auto-generated if None) |
|
|
645
|
+
| `api_key` | `str` | `None` | **Webhook mode**: API key for webhook registration (required for webhook mode) |
|
|
646
|
+
| `mqtt_broker_url` | `str` | `None` | **MQTT mode**: MQTT broker connection URL |
|
|
647
|
+
| `default_outbox_topic` | `str` | `None` | **MQTT mode**: Default topic for outgoing messages |
|
|
648
|
+
| `auto_reconnect` | `bool` | `True` | Auto-reconnect/restart on disconnect |
|
|
539
649
|
| `message_history_limit` | `int` | `100` | Maximum messages to keep in history |
|
|
540
650
|
| `registry_url` | `str` | `"http://localhost:3002"` | ZyndAI registry service URL |
|
|
541
|
-
| `
|
|
542
|
-
| `identity_credential_path` | `str` | Required | Path to your credential file |
|
|
651
|
+
| `identity_credential_path` | `str` | Required | Path to your DID credential file |
|
|
543
652
|
| `secret_seed` | `str` | Required | Your agent's secret seed |
|
|
544
|
-
|
|
653
|
+
|
|
654
|
+
**Note**:
|
|
655
|
+
- Configure either `webhook_port` (recommended) OR `mqtt_broker_url`, not both.
|
|
656
|
+
- When using webhook mode, `api_key` is required for registering your webhook URL with the registry.
|
|
545
657
|
|
|
546
658
|
### Message Types
|
|
547
659
|
|
|
@@ -777,7 +889,7 @@ We welcome contributions! Here's how to get started:
|
|
|
777
889
|
|
|
778
890
|
### Development Setup
|
|
779
891
|
```bash
|
|
780
|
-
git clone https://github.com/
|
|
892
|
+
git clone https://github.com/Zynd-AI-Network/zyndai-agent.git
|
|
781
893
|
cd zyndai-agent
|
|
782
894
|
python -m venv venv
|
|
783
895
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
@@ -812,11 +924,92 @@ Create agents for real-time market data (x402), technical analysis by agents, se
|
|
|
812
924
|
### 6. Content Generation with Fact-Checking
|
|
813
925
|
Orchestrate agents for research, writing, accessing paid fact-checking APIs via x402, and publishing verified content.
|
|
814
926
|
|
|
927
|
+
## 🔧 Troubleshooting
|
|
928
|
+
|
|
929
|
+
### Webhook Mode Issues
|
|
930
|
+
|
|
931
|
+
**Port Already in Use**
|
|
932
|
+
```
|
|
933
|
+
The SDK automatically tries ports 5000-5010 if the configured port is busy.
|
|
934
|
+
Check the console output for the actual port being used.
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
**Agent Behind NAT/Firewall**
|
|
938
|
+
```python
|
|
939
|
+
# Specify your public webhook URL manually
|
|
940
|
+
agent_config = AgentConfig(
|
|
941
|
+
webhook_host="0.0.0.0",
|
|
942
|
+
webhook_port=5000,
|
|
943
|
+
webhook_url="https://my-public-domain.com/webhook", # Your public URL
|
|
944
|
+
...
|
|
945
|
+
)
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
**Running Multiple Agents Locally**
|
|
949
|
+
```python
|
|
950
|
+
# Agent 1: Port 5000
|
|
951
|
+
agent1_config = AgentConfig(webhook_port=5000, ...)
|
|
952
|
+
|
|
953
|
+
# Agent 2: Port 5001
|
|
954
|
+
agent2_config = AgentConfig(webhook_port=5001, ...)
|
|
955
|
+
|
|
956
|
+
# Agent 3: Port 5002
|
|
957
|
+
agent3_config = AgentConfig(webhook_port=5002, ...)
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
**Target Agent Offline**
|
|
961
|
+
```
|
|
962
|
+
When sending messages, you'll receive clear error messages:
|
|
963
|
+
- "Error: Could not connect to target agent. Agent may be offline."
|
|
964
|
+
- "Error: Request timed out. Target agent may be offline."
|
|
965
|
+
|
|
966
|
+
The SDK does not automatically retry failed webhooks.
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
**Health Check**
|
|
970
|
+
```bash
|
|
971
|
+
# Check if webhook server is running
|
|
972
|
+
curl http://localhost:5000/health
|
|
973
|
+
|
|
974
|
+
# Response:
|
|
975
|
+
# {"status": "ok", "agent_id": "did:polygonid:...", "timestamp": 1234567890}
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
### MQTT Mode Issues
|
|
979
|
+
|
|
980
|
+
**Connection Refused**
|
|
981
|
+
```
|
|
982
|
+
Ensure your MQTT broker URL is correct and the broker is running.
|
|
983
|
+
Default: mqtt://registry.zynd.ai:1883
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
**Messages Not Being Received**
|
|
987
|
+
```
|
|
988
|
+
Check that agents are subscribed to the correct topics.
|
|
989
|
+
Verify encryption credentials match between agents.
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
### General Issues
|
|
993
|
+
|
|
994
|
+
**Decryption Failures**
|
|
995
|
+
```
|
|
996
|
+
Ensure both agents have the correct DID credentials.
|
|
997
|
+
Verify secret_seed matches the identity_credential_path.
|
|
998
|
+
Check that credentials haven't been regenerated.
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
**Registry Connection Errors**
|
|
1002
|
+
```
|
|
1003
|
+
Verify registry_url is correct.
|
|
1004
|
+
Check network connectivity to registry.
|
|
1005
|
+
Ensure webhook URL or MQTT broker info was successfully registered.
|
|
1006
|
+
```
|
|
1007
|
+
|
|
815
1008
|
## 🆘 Support & Community
|
|
816
1009
|
|
|
817
|
-
- **GitHub Issues**: [Report bugs or request features](https://github.com/
|
|
1010
|
+
- **GitHub Issues**: [Report bugs or request features](https://github.com/Zynd-AI-Network/zyndai-agent/issues)
|
|
818
1011
|
- **Documentation**: [Full API Documentation](https://docs.zynd.ai)
|
|
819
|
-
- **Email**:
|
|
1012
|
+
- **Email**: zyndainetwork@gmail.com
|
|
820
1013
|
- **Twitter**: [@ZyndAI](https://x.com/ZyndAI)
|
|
821
1014
|
|
|
822
1015
|
## 📄 License
|
|
@@ -838,6 +1031,8 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
838
1031
|
- [x] End-to-end encryption
|
|
839
1032
|
- [x] LangChain integration
|
|
840
1033
|
- [x] x402 micropayment support
|
|
1034
|
+
- [x] HTTP Webhook communication mode
|
|
1035
|
+
- [ ] WebSocket support for real-time bidirectional communication
|
|
841
1036
|
- [ ] Support for additional LLM providers (Anthropic, Cohere, etc.)
|
|
842
1037
|
- [ ] Web dashboard for agent monitoring and payment tracking
|
|
843
1038
|
- [ ] Advanced orchestration patterns (workflows, state machines)
|
|
@@ -846,6 +1041,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
846
1041
|
- [ ] Enhanced security features (rate limiting, access control)
|
|
847
1042
|
- [ ] Performance optimizations for high-throughput scenarios
|
|
848
1043
|
- [ ] x402 payment analytics and budgeting tools
|
|
1044
|
+
- [ ] Webhook authentication and rate limiting
|
|
849
1045
|
|
|
850
1046
|
---
|
|
851
1047
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
zyndai_agent/__init__.py,sha256=HQIL1JElpM14oOWl_7LKQeSxbDSeFMnt7goBFpI7D7I,709
|
|
2
|
+
zyndai_agent/agent.py,sha256=bko2GuGJ4Cd8Ra9IaFncs5j2-OgEcvlR5KW-4sWk1rQ,6262
|
|
3
|
+
zyndai_agent/communication.py,sha256=kMHvlSoj5aL3pfVxfiQImQQDl5VFC1zXUxX-_PwcFrM,21118
|
|
4
|
+
zyndai_agent/identity.py,sha256=9W9iDcrAg07jxE4llrubW1poYBTVtONddyDULGUSnV8,3906
|
|
5
|
+
zyndai_agent/message.py,sha256=nXpKboqAyv-V2bDbgyZ84NerZhGLjYt-lAeM7ndFLTs,3974
|
|
6
|
+
zyndai_agent/payment.py,sha256=Yxnm8rbSB0B2t78jJwGobtcpRbQlM3lSLpUljohhDgc,6238
|
|
7
|
+
zyndai_agent/search.py,sha256=cSLoD4NCXYGo2YiGYE7xYrJr90c_6WjEjoWPQBgu78g,2129
|
|
8
|
+
zyndai_agent/utils.py,sha256=YN1EXGawaUPiPRyPszYvZ7lwTgimmca2DQeW_8nFjRo,16634
|
|
9
|
+
zyndai_agent/webhook_communication.py,sha256=UgyXlNYqooGVoeWqn3uLke8G9vqKBqE1fcFpe1C_mpw,16912
|
|
10
|
+
zyndai_agent-0.2.1.dist-info/METADATA,sha256=FDNvaFcQSzma2Vm8dbWr7d7ERrXacOViaI_d8d7HHMc,37007
|
|
11
|
+
zyndai_agent-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
zyndai_agent-0.2.1.dist-info/top_level.txt,sha256=6jE9hyvpa18fstxa4omi9X2c97rawKydn6NwMwVSql4,13
|
|
13
|
+
zyndai_agent-0.2.1.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
zyndai_agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
zyndai_agent/agent.py,sha256=j2ZKpZAmZ23g5GHa1l61CDpOXrG-w0KWIqH9Bgvkytg,3102
|
|
3
|
-
zyndai_agent/communication.py,sha256=kMHvlSoj5aL3pfVxfiQImQQDl5VFC1zXUxX-_PwcFrM,21118
|
|
4
|
-
zyndai_agent/identity.py,sha256=9W9iDcrAg07jxE4llrubW1poYBTVtONddyDULGUSnV8,3906
|
|
5
|
-
zyndai_agent/payment.py,sha256=Yxnm8rbSB0B2t78jJwGobtcpRbQlM3lSLpUljohhDgc,6238
|
|
6
|
-
zyndai_agent/search.py,sha256=rYIs39ZM8iXFrv6Zb548UMeWQYjiTO7Ou_2vhvscLPM,2013
|
|
7
|
-
zyndai_agent/utils.py,sha256=YN1EXGawaUPiPRyPszYvZ7lwTgimmca2DQeW_8nFjRo,16634
|
|
8
|
-
zyndai_agent-0.1.4.dist-info/METADATA,sha256=MLKGZCW72HucmuQ1xzbqFE4fIYHPXqTLxVJgEzGVnQA,30709
|
|
9
|
-
zyndai_agent-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
-
zyndai_agent-0.1.4.dist-info/top_level.txt,sha256=6jE9hyvpa18fstxa4omi9X2c97rawKydn6NwMwVSql4,13
|
|
11
|
-
zyndai_agent-0.1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|