zyndai-agent 0.1.5__py3-none-any.whl → 0.2.2__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 CHANGED
@@ -0,0 +1,22 @@
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
+ from zyndai_agent.config_manager import ConfigManager
9
+
10
+ __all__ = [
11
+ "ZyndAIAgent",
12
+ "AgentConfig",
13
+ "AgentCommunicationManager",
14
+ "WebhookCommunicationManager",
15
+ "MQTTMessage",
16
+ "AgentMessage",
17
+ "SearchAndDiscoveryManager",
18
+ "AgentSearchResponse",
19
+ "IdentityManager",
20
+ "X402PaymentProcessor",
21
+ "ConfigManager",
22
+ ]
zyndai_agent/agent.py CHANGED
@@ -3,56 +3,102 @@ 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
8
+ from zyndai_agent.config_manager import ConfigManager
7
9
  from pydantic import BaseModel
8
10
  from typing import Optional
9
11
  from langchain.agents import create_agent
10
12
  from langgraph.graph.state import CompiledStateGraph
11
13
 
12
14
  class AgentConfig(BaseModel):
15
+ name: str = ""
16
+ description: str = ""
17
+ capabilities: Optional[dict] = None
18
+
13
19
  auto_reconnect: bool = True
14
20
  message_history_limit: int = 100
15
21
  registry_url: str = "http://localhost:3002"
16
- mqtt_broker_url: str
17
- identity_credential_path: str
18
- identity_credential: Optional[dict] = None
22
+
23
+ # Webhook configuration (new)
24
+ webhook_host: Optional[str] = "0.0.0.0"
25
+ webhook_port: Optional[int] = 5000
26
+ webhook_url: Optional[str] = None # Public URL if behind NAT
27
+ api_key: Optional[str] = None # API key for webhook registration
28
+
29
+ # MQTT configuration (deprecated, kept for backward compatibility)
30
+ mqtt_broker_url: Optional[str] = None
19
31
  default_outbox_topic: Optional[str] = None
20
- secret_seed: str = None
21
32
 
22
- class ZyndAIAgent(SearchAndDiscoveryManager, IdentityManager, AgentCommunicationManager, X402PaymentProcessor):
33
+ price: Optional[str] = None
23
34
 
24
- def __init__(self, agent_config: AgentConfig):
35
+ # Config directory for agent identity (allows multiple agents in same project)
36
+ config_dir: Optional[str] = None # e.g., ".agent-stock" or ".agent-user"
37
+
38
+ class ZyndAIAgent(SearchAndDiscoveryManager, IdentityManager, X402PaymentProcessor, WebhookCommunicationManager):
39
+
40
+ def __init__(self, agent_config: AgentConfig):
25
41
 
26
42
  self.agent_executor: CompiledStateGraph = None
27
- self.agent_config = agent_config
28
- self.x402_processor = X402PaymentProcessor(agent_config.secret_seed)
43
+ self.agent_config = agent_config
44
+ self.communication_mode = None # Track which mode is active
45
+
46
+ # Load or create agent config from .agent/config.json
47
+ config = ConfigManager.load_or_create(agent_config)
48
+ self.registry_agent_id = config["id"]
49
+ self.agent_id = config["id"]
50
+ self.secret_seed = config["seed"]
51
+ self.identity_credential = config["did"]
29
52
 
30
- try:
31
- with open(agent_config.identity_credential_path, "r") as f:
32
- self.identity_credential = json.load(f)
33
- except FileNotFoundError:
34
- raise FileNotFoundError(f"Identity credential file not found at {agent_config.identity_credential_path}")
35
-
36
- IdentityManager.__init__(self,agent_config.registry_url)
53
+ self.x402_processor = X402PaymentProcessor(self.secret_seed)
54
+ self.pay_to_address = self.x402_processor.account.address
55
+
56
+ IdentityManager.__init__(self, agent_config.registry_url)
37
57
 
38
58
  SearchAndDiscoveryManager.__init__(
39
59
  self,
40
60
  registry_url=agent_config.registry_url
41
61
  )
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
62
 
55
- self.update_agent_mqtt_info()
63
+ # Determine communication mode: webhook or MQTT
64
+ # Prefer webhook if webhook_port is configured
65
+ if agent_config.webhook_port is not None and agent_config.mqtt_broker_url is None:
66
+ # Use webhook mode
67
+ self.communication_mode = "webhook"
68
+ WebhookCommunicationManager.__init__(
69
+ self,
70
+ agent_id=self.identity_credential["issuer"],
71
+ webhook_host=agent_config.webhook_host,
72
+ webhook_port=agent_config.webhook_port,
73
+ webhook_url=agent_config.webhook_url,
74
+ auto_restart=agent_config.auto_reconnect,
75
+ message_history_limit=agent_config.message_history_limit,
76
+ identity_credential=self.identity_credential,
77
+ price=agent_config.price,
78
+ pay_to_address=self.pay_to_address
79
+ )
80
+ self.update_agent_webhook_info()
81
+
82
+ elif agent_config.mqtt_broker_url is not None:
83
+ # Use MQTT mode (backward compatibility)
84
+ self.communication_mode = "mqtt"
85
+ AgentCommunicationManager.__init__(
86
+ self,
87
+ self.identity_credential["issuer"],
88
+ default_inbox_topic=f"{self.identity_credential['issuer']}/inbox",
89
+ default_outbox_topic=agent_config.default_outbox_topic,
90
+ auto_reconnect=True,
91
+ message_history_limit=agent_config.message_history_limit,
92
+ identity_credential=self.identity_credential,
93
+ secret_seed=self.secret_seed,
94
+ mqtt_broker_url=agent_config.mqtt_broker_url
95
+ )
96
+ self.update_agent_mqtt_info()
97
+ else:
98
+ raise ValueError("Either webhook_port or mqtt_broker_url must be configured")
99
+
100
+ # Display agent info
101
+ self._display_agent_info()
56
102
 
57
103
 
58
104
 
@@ -62,16 +108,79 @@ class ZyndAIAgent(SearchAndDiscoveryManager, IdentityManager, AgentCommunication
62
108
 
63
109
  def update_agent_mqtt_info(self):
64
110
  """Updates the mqtt connection info of the agent into the registry so other agents can find me"""
65
- print(self.agent_config.secret_seed, self.agent_config.mqtt_broker_url, f"{self.agent_config.registry_url}/agents/update-mqtt")
66
- updateResponse = requests.post(
67
- f"{self.agent_config.registry_url}/agents/update-mqtt",
111
+
112
+ updateResponse = requests.patch(
113
+ f"{self.agent_config.registry_url}/agents/update-mqtt",
68
114
  data={
69
- "seed": self.agent_config.secret_seed,
115
+ "seed": self.secret_seed,
70
116
  "mqttUri": self.agent_config.mqtt_broker_url
71
117
  }
72
118
  )
73
- print(updateResponse.status_code,"====")
119
+
74
120
  if (updateResponse.status_code != 201):
75
121
  raise Exception("Failed to update agent connection info in p3 registry.")
76
122
 
77
- print("Synced with the registry...")
123
+ print("Synced with the registry...")
124
+
125
+ def update_agent_webhook_info(self):
126
+ """Updates the webhook URL of the agent into the registry so other agents can find me"""
127
+ if not self.agent_config.api_key:
128
+ raise ValueError("API key is required for webhook registration. Please provide api_key in AgentConfig.")
129
+
130
+ headers = {
131
+ "accept": "*/*",
132
+ "X-API-KEY": self.agent_config.api_key
133
+ }
134
+
135
+ payload = {
136
+ "agentId": self.registry_agent_id,
137
+ "httpWebhookUrl": self.webhook_url
138
+ }
139
+
140
+ print(f"Updating webhook URL: {payload}")
141
+
142
+ updateResponse = requests.patch(
143
+ f"{self.agent_config.registry_url}/agents/update-webhook",
144
+ json=payload,
145
+ headers=headers
146
+ )
147
+
148
+ if updateResponse.status_code != 200:
149
+ raise Exception(f"Failed to update agent webhook info in Zynd registry. Status: {updateResponse.status_code}, Response: {updateResponse.text}")
150
+
151
+ print("Synced webhook URL with the registry...")
152
+
153
+ def _display_agent_info(self):
154
+ """Display agent information in a pretty format on startup."""
155
+ name = self.agent_config.name or "Unnamed Agent"
156
+ description = self.agent_config.description or "-"
157
+ agent_id = self.agent_id
158
+ address = self.pay_to_address
159
+ did = self.identity_credential.get("issuer", "-")
160
+ mode = self.communication_mode or "-"
161
+ webhook_url = getattr(self, "webhook_url", None)
162
+ price = self.agent_config.price or "Free"
163
+
164
+ border = "=" * 60
165
+ print(f"\n{border}")
166
+ print(f" ZYND AI AGENT")
167
+ print(f"{border}")
168
+ print(f" Name : {name}")
169
+ print(f" Description : {description}")
170
+ print(f" Agent ID : {agent_id}")
171
+ print(f" DID : {did}")
172
+ print(f" Address : {address}")
173
+ print(f" Mode : {mode}")
174
+ if webhook_url:
175
+ print(f" Webhook URL : {webhook_url}")
176
+ print(f" Price : {price}")
177
+ print(f"{border}\n")
178
+
179
+ def update_agent_connection_info(self):
180
+ """Updates the agent connection info (webhook or MQTT) in the registry based on communication mode"""
181
+ if self.communication_mode == "webhook":
182
+ self.update_agent_webhook_info()
183
+ elif self.communication_mode == "mqtt":
184
+ self.update_agent_mqtt_info()
185
+ else:
186
+ raise ValueError(f"Unknown communication mode: {self.communication_mode}")
@@ -0,0 +1,153 @@
1
+ import os
2
+ import json
3
+ import requests
4
+
5
+
6
+ class ConfigManager:
7
+ """
8
+ Manages agent configuration stored in .agent/config.json.
9
+
10
+ On first run, provisions a new agent via the registry API and saves
11
+ the identity credentials locally. On subsequent runs, loads the
12
+ saved config so the user doesn't need to provide identity credentials manually.
13
+ """
14
+
15
+ DEFAULT_CONFIG_DIR = ".agent"
16
+ CONFIG_FILE = "config.json"
17
+
18
+ @staticmethod
19
+ def _config_path(config_dir: str = None):
20
+ dir_name = config_dir or ConfigManager.DEFAULT_CONFIG_DIR
21
+ return os.path.join(os.getcwd(), dir_name, ConfigManager.CONFIG_FILE)
22
+
23
+ @staticmethod
24
+ def _config_dir(config_dir: str = None):
25
+ dir_name = config_dir or ConfigManager.DEFAULT_CONFIG_DIR
26
+ return os.path.join(os.getcwd(), dir_name)
27
+
28
+ @staticmethod
29
+ def load_config(config_dir: str = None):
30
+ """Load existing config from .agent/config.json. Returns None if not found."""
31
+ config_path = ConfigManager._config_path(config_dir)
32
+ if not os.path.exists(config_path):
33
+ return None
34
+
35
+ with open(config_path, "r") as f:
36
+ config = json.load(f)
37
+
38
+ print(f"Loaded agent config from {config_path}")
39
+ return config
40
+
41
+ @staticmethod
42
+ def save_config(config: dict, config_dir: str = None):
43
+ """Save config to .agent/config.json, creating the directory if needed."""
44
+ dir_path = ConfigManager._config_dir(config_dir)
45
+ os.makedirs(dir_path, exist_ok=True)
46
+
47
+ config_path = ConfigManager._config_path(config_dir)
48
+ with open(config_path, "w") as f:
49
+ json.dump(config, f, indent=2)
50
+
51
+ print(f"Saved agent config to {config_path}")
52
+
53
+ @staticmethod
54
+ def create_agent(registry_url: str, api_key: str, name: str, description: str, capabilities: dict, config_dir: str = None):
55
+ """
56
+ Create a new agent via the registry API.
57
+
58
+ Args:
59
+ registry_url: Base URL of the agent registry
60
+ api_key: API key for authentication
61
+ name: Agent display name
62
+ description: Agent description
63
+ capabilities: Agent capabilities dict (e.g. {"ai": ["nlp"], "protocols": ["http"]})
64
+ config_dir: Custom config directory (e.g., ".agent-stock")
65
+
66
+ Returns:
67
+ dict: The saved config with id, didIdentifier, did, name, description, seed
68
+ """
69
+ headers = {
70
+ "accept": "application/json",
71
+ "Content-Type": "application/json",
72
+ "x-api-key": api_key
73
+ }
74
+
75
+ payload = {
76
+ "name": name,
77
+ "description": description,
78
+ "capabilities": capabilities,
79
+ "status": "ACTIVE"
80
+ }
81
+
82
+ response = requests.post(
83
+ f"{registry_url}/agents",
84
+ json=payload,
85
+ headers=headers
86
+ )
87
+
88
+ if response.status_code not in (200, 201):
89
+ raise RuntimeError(
90
+ f"Failed to create agent via registry API. "
91
+ f"Status: {response.status_code}, Response: {response.text}"
92
+ )
93
+
94
+ data = response.json()
95
+
96
+ # The 'did' field in the API response is a JSON string; parse it
97
+ did = data["did"]
98
+ if isinstance(did, str):
99
+ did = json.loads(did)
100
+
101
+ config = {
102
+ "id": data["id"],
103
+ "didIdentifier": data["didIdentifier"],
104
+ "did": did,
105
+ "name": data["name"],
106
+ "description": data["description"],
107
+ "seed": data["seed"]
108
+ }
109
+
110
+ ConfigManager.save_config(config, config_dir)
111
+ return config
112
+
113
+ @staticmethod
114
+ def load_or_create(agent_config):
115
+ """
116
+ Load existing agent config or create a new agent.
117
+
118
+ If .agent/config.json exists, returns stored values.
119
+ Otherwise, calls the registry API to provision a new agent.
120
+
121
+ Args:
122
+ agent_config: AgentConfig instance with registry_url, api_key, name,
123
+ description, capabilities, and optional config_dir
124
+
125
+ Returns:
126
+ dict with keys: id, didIdentifier, did, name, description, seed
127
+ """
128
+ config_dir = getattr(agent_config, 'config_dir', None)
129
+ config = ConfigManager.load_config(config_dir)
130
+ if config is not None:
131
+ return config
132
+
133
+ # Validate required fields for agent creation
134
+ if not agent_config.api_key:
135
+ raise ValueError(
136
+ "api_key is required in AgentConfig to create a new agent. "
137
+ "Provide an API key or place a .agent/config.json in the working directory."
138
+ )
139
+ if not agent_config.name:
140
+ raise ValueError("name is required in AgentConfig to create a new agent.")
141
+ if not agent_config.capabilities:
142
+ raise ValueError("capabilities is required in AgentConfig to create a new agent.")
143
+
144
+ dir_name = config_dir or ConfigManager.DEFAULT_CONFIG_DIR
145
+ print(f"No {dir_name}/config.json found. Creating a new agent...")
146
+ return ConfigManager.create_agent(
147
+ registry_url=agent_config.registry_url,
148
+ api_key=agent_config.api_key,
149
+ name=agent_config.name,
150
+ description=agent_config.description,
151
+ capabilities=agent_config.capabilities,
152
+ config_dir=config_dir
153
+ )
@@ -0,0 +1,112 @@
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
+ "prompt": self.content,
62
+ "sender_id": self.sender_id,
63
+ "sender_did": self.sender_did,
64
+ "receiver_id": self.receiver_id,
65
+ "message_type": self.message_type,
66
+ "message_id": self.message_id,
67
+ "conversation_id": self.conversation_id,
68
+ "in_reply_to": self.in_reply_to,
69
+ "metadata": self.metadata,
70
+ "timestamp": self.timestamp
71
+ }
72
+
73
+ def to_json(self) -> str:
74
+ """Convert message to JSON string for transmission."""
75
+ return json.dumps(self.to_dict())
76
+
77
+ @classmethod
78
+ def from_dict(cls, data: Dict[str, Any]) -> 'AgentMessage':
79
+ """Create message object from dictionary data."""
80
+ return cls(
81
+ content=data.get("prompt", data.get("content", "")),
82
+ sender_id=data.get("sender_id", "unknown"),
83
+ sender_did=data.get("sender_did", "unknown"),
84
+ receiver_id=data.get("receiver_id"),
85
+ message_type=data.get("message_type", "query"),
86
+ message_id=data.get("message_id"),
87
+ conversation_id=data.get("conversation_id"),
88
+ in_reply_to=data.get("in_reply_to"),
89
+ metadata=data.get("metadata", {})
90
+ )
91
+
92
+ @classmethod
93
+ def from_json(cls, json_str: str) -> 'AgentMessage':
94
+ """
95
+ Create message object from JSON string.
96
+
97
+ Handles both valid JSON and fallback for plain text messages.
98
+ """
99
+ try:
100
+ data = json.loads(json_str)
101
+ return cls.from_dict(data)
102
+ except json.JSONDecodeError as e:
103
+ logger.error(f"Failed to parse message as JSON: {e}")
104
+ return cls(
105
+ content=json_str,
106
+ sender_id="unknown",
107
+ message_type="raw"
108
+ )
109
+
110
+
111
+ # Backward compatibility alias
112
+ MQTTMessage = AgentMessage