jarviscore-framework 0.1.1__py3-none-any.whl → 0.2.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.
- examples/autoagent_distributed_example.py +211 -0
- examples/custom_profile_decorator.py +134 -0
- examples/custom_profile_wrap.py +168 -0
- examples/customagent_distributed_example.py +362 -0
- examples/customagent_p2p_example.py +347 -0
- jarviscore/__init__.py +49 -36
- jarviscore/adapter/__init__.py +15 -9
- jarviscore/adapter/decorator.py +23 -19
- jarviscore/adapter/wrapper.py +303 -0
- jarviscore/cli/scaffold.py +1 -1
- jarviscore/cli/smoketest.py +3 -2
- jarviscore/core/agent.py +44 -1
- jarviscore/core/mesh.py +196 -35
- jarviscore/data/examples/autoagent_distributed_example.py +211 -0
- jarviscore/data/examples/customagent_distributed_example.py +362 -0
- jarviscore/data/examples/customagent_p2p_example.py +347 -0
- jarviscore/docs/API_REFERENCE.md +264 -51
- jarviscore/docs/AUTOAGENT_GUIDE.md +198 -0
- jarviscore/docs/CONFIGURATION.md +35 -21
- jarviscore/docs/CUSTOMAGENT_GUIDE.md +415 -0
- jarviscore/docs/GETTING_STARTED.md +106 -13
- jarviscore/docs/TROUBLESHOOTING.md +144 -6
- jarviscore/docs/USER_GUIDE.md +138 -361
- jarviscore/orchestration/engine.py +20 -8
- jarviscore/p2p/__init__.py +10 -0
- jarviscore/p2p/coordinator.py +129 -0
- jarviscore/p2p/messages.py +87 -0
- jarviscore/p2p/peer_client.py +576 -0
- jarviscore/p2p/peer_tool.py +268 -0
- {jarviscore_framework-0.1.1.dist-info → jarviscore_framework-0.2.0.dist-info}/METADATA +60 -54
- jarviscore_framework-0.2.0.dist-info/RECORD +132 -0
- {jarviscore_framework-0.1.1.dist-info → jarviscore_framework-0.2.0.dist-info}/WHEEL +1 -1
- {jarviscore_framework-0.1.1.dist-info → jarviscore_framework-0.2.0.dist-info}/top_level.txt +1 -0
- test_logs/code_registry/functions/data_generator-558779ed_560ebc37.py +7 -0
- test_logs/code_registry/functions/data_generator-5ed3609e_560ebc37.py +7 -0
- test_logs/code_registry/functions/data_generator-66da0356_43970bb9.py +25 -0
- test_logs/code_registry/functions/data_generator-7a2fac83_583709d9.py +36 -0
- test_logs/code_registry/functions/data_generator-888b670f_aa235863.py +9 -0
- test_logs/code_registry/functions/data_generator-9ca5f642_aa235863.py +9 -0
- test_logs/code_registry/functions/data_generator-bfd90775_560ebc37.py +7 -0
- test_logs/code_registry/functions/data_generator-e95d2f7d_aa235863.py +9 -0
- test_logs/code_registry/functions/data_generator-f60ca8a2_327eb8c2.py +29 -0
- test_logs/code_registry/functions/mathematician-02adf9ee_958658d9.py +19 -0
- test_logs/code_registry/functions/mathematician-0706fb57_5df13441.py +23 -0
- test_logs/code_registry/functions/mathematician-153c9c4a_ba59c918.py +83 -0
- test_logs/code_registry/functions/mathematician-287e61c0_41daa793.py +18 -0
- test_logs/code_registry/functions/mathematician-2967af5a_863c2cc6.py +17 -0
- test_logs/code_registry/functions/mathematician-303ca6d6_5df13441.py +23 -0
- test_logs/code_registry/functions/mathematician-308a4afd_cbf5064d.py +73 -0
- test_logs/code_registry/functions/mathematician-353f16e2_0968bcf5.py +18 -0
- test_logs/code_registry/functions/mathematician-3c22475a_41daa793.py +17 -0
- test_logs/code_registry/functions/mathematician-5bac1029_0968bcf5.py +18 -0
- test_logs/code_registry/functions/mathematician-640f76b2_9198780b.py +19 -0
- test_logs/code_registry/functions/mathematician-752fa7ea_863c2cc6.py +17 -0
- test_logs/code_registry/functions/mathematician-baf9ef39_0968bcf5.py +18 -0
- test_logs/code_registry/functions/mathematician-bc8b2a2f_5df13441.py +23 -0
- test_logs/code_registry/functions/mathematician-c31e4686_41daa793.py +18 -0
- test_logs/code_registry/functions/mathematician-cc84c84c_863c2cc6.py +17 -0
- test_logs/code_registry/functions/mathematician-dd7c7144_9198780b.py +19 -0
- test_logs/code_registry/functions/mathematician-e671c256_41ea4487.py +74 -0
- test_logs/code_registry/functions/report_generator-1a878fcc_18d44bdc.py +47 -0
- test_logs/code_registry/functions/report_generator-25c1c331_cea57d0d.py +35 -0
- test_logs/code_registry/functions/report_generator-37552117_e711c2b9.py +35 -0
- test_logs/code_registry/functions/report_generator-bc662768_e711c2b9.py +35 -0
- test_logs/code_registry/functions/report_generator-d6c0e76b_5e7722ec.py +44 -0
- test_logs/code_registry/functions/report_generator-f270fb02_680529c3.py +44 -0
- test_logs/code_registry/functions/text_processor-11393b14_4370d3ed.py +40 -0
- test_logs/code_registry/functions/text_processor-7d02dfc3_d3b569be.py +37 -0
- test_logs/code_registry/functions/text_processor-8adb5e32_9168c5fe.py +13 -0
- test_logs/code_registry/functions/text_processor-c58ffc19_78b4ceac.py +42 -0
- test_logs/code_registry/functions/text_processor-cd5977b1_9168c5fe.py +13 -0
- test_logs/code_registry/functions/text_processor-ec1c8773_9168c5fe.py +13 -0
- tests/test_01_analyst_standalone.py +124 -0
- tests/test_02_assistant_standalone.py +164 -0
- tests/test_03_analyst_with_framework.py +945 -0
- tests/test_04_assistant_with_framework.py +1002 -0
- tests/test_05_integration.py +1301 -0
- tests/test_06_real_llm_integration.py +760 -0
- tests/test_07_distributed_single_node.py +578 -0
- tests/test_08_distributed_multi_node.py +454 -0
- tests/test_09_distributed_autoagent.py +509 -0
- tests/test_10_distributed_customagent.py +787 -0
- tests/test_mesh.py +35 -4
- jarviscore_framework-0.1.1.dist-info/RECORD +0 -69
- {jarviscore_framework-0.1.1.dist-info → jarviscore_framework-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PeerClient - Direct peer-to-peer communication for agents.
|
|
3
|
+
|
|
4
|
+
Provides a simple API for agents to discover and communicate
|
|
5
|
+
with other agents in the mesh without going through workflow orchestration.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
class MyAgent(JarvisAgent):
|
|
9
|
+
async def run(self):
|
|
10
|
+
# Discovery
|
|
11
|
+
analyst = self.peers.get_peer(role="analyst")
|
|
12
|
+
|
|
13
|
+
# Notify (fire-and-forget)
|
|
14
|
+
await self.peers.notify("analyst", {"event": "done", "data": result})
|
|
15
|
+
|
|
16
|
+
# Request-response
|
|
17
|
+
response = await self.peers.request("scout", {"need": "clarification"}, timeout=30)
|
|
18
|
+
|
|
19
|
+
# Receive incoming messages
|
|
20
|
+
message = await self.peers.receive(timeout=5)
|
|
21
|
+
"""
|
|
22
|
+
import asyncio
|
|
23
|
+
import logging
|
|
24
|
+
from typing import List, Dict, Any, Optional, TYPE_CHECKING
|
|
25
|
+
from uuid import uuid4
|
|
26
|
+
|
|
27
|
+
from .messages import PeerInfo, IncomingMessage, OutgoingMessage, MessageType
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from .coordinator import P2PCoordinator
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PeerClient:
|
|
36
|
+
"""
|
|
37
|
+
Client for peer-to-peer agent communication.
|
|
38
|
+
|
|
39
|
+
Injected into agents during mesh startup, provides direct access
|
|
40
|
+
to peer discovery and messaging without workflow orchestration.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
coordinator: 'P2PCoordinator',
|
|
46
|
+
agent_id: str,
|
|
47
|
+
agent_role: str,
|
|
48
|
+
agent_registry: Dict[str, List],
|
|
49
|
+
node_id: str = ""
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
Initialize PeerClient.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
coordinator: P2P coordinator for message routing
|
|
56
|
+
agent_id: This agent's unique ID
|
|
57
|
+
agent_role: This agent's role
|
|
58
|
+
agent_registry: Registry mapping roles to agent lists
|
|
59
|
+
node_id: This node's P2P identifier (host:port)
|
|
60
|
+
"""
|
|
61
|
+
self._coordinator = coordinator
|
|
62
|
+
self._agent_id = agent_id
|
|
63
|
+
self._agent_role = agent_role
|
|
64
|
+
self._agent_registry = agent_registry
|
|
65
|
+
self._node_id = node_id
|
|
66
|
+
|
|
67
|
+
# Message queue for incoming messages
|
|
68
|
+
self._message_queue: asyncio.Queue[IncomingMessage] = asyncio.Queue()
|
|
69
|
+
|
|
70
|
+
# Pending requests waiting for responses (correlation_id -> Future)
|
|
71
|
+
self._pending_requests: Dict[str, asyncio.Future] = {}
|
|
72
|
+
|
|
73
|
+
self._logger = logging.getLogger(f"jarviscore.peer_client.{agent_id}")
|
|
74
|
+
|
|
75
|
+
# ─────────────────────────────────────────────────────────────────
|
|
76
|
+
# DISCOVERY
|
|
77
|
+
# ─────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
def get_peer(self, role: str) -> Optional[PeerInfo]:
|
|
80
|
+
"""
|
|
81
|
+
Get information about a peer by role.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
role: The role to look for (e.g., "analyst", "scout")
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
PeerInfo if found, None otherwise
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
analyst = self.peers.get_peer(role="analyst")
|
|
91
|
+
if analyst:
|
|
92
|
+
print(f"Found analyst: {analyst.agent_id}")
|
|
93
|
+
"""
|
|
94
|
+
agents = self._agent_registry.get(role, [])
|
|
95
|
+
if not agents:
|
|
96
|
+
self._logger.debug(f"No peer found with role: {role}")
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
# Return first agent with this role
|
|
100
|
+
agent = agents[0]
|
|
101
|
+
return PeerInfo(
|
|
102
|
+
agent_id=agent.agent_id,
|
|
103
|
+
role=agent.role,
|
|
104
|
+
capabilities=list(agent.capabilities),
|
|
105
|
+
node_id=self._node_id,
|
|
106
|
+
status="alive"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def discover(
|
|
110
|
+
self,
|
|
111
|
+
capability: str = None,
|
|
112
|
+
role: str = None
|
|
113
|
+
) -> List[PeerInfo]:
|
|
114
|
+
"""
|
|
115
|
+
Discover peers by capability or role.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
capability: Filter by capability (e.g., "analysis")
|
|
119
|
+
role: Filter by role (e.g., "analyst")
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
List of matching PeerInfo objects
|
|
123
|
+
|
|
124
|
+
Example:
|
|
125
|
+
analysts = self.peers.discover(capability="analysis")
|
|
126
|
+
for peer in analysts:
|
|
127
|
+
print(f"Found: {peer.role} - {peer.capabilities}")
|
|
128
|
+
"""
|
|
129
|
+
results = []
|
|
130
|
+
|
|
131
|
+
if role:
|
|
132
|
+
agents = self._agent_registry.get(role, [])
|
|
133
|
+
for agent in agents:
|
|
134
|
+
if agent.agent_id != self._agent_id: # Exclude self
|
|
135
|
+
results.append(PeerInfo(
|
|
136
|
+
agent_id=agent.agent_id,
|
|
137
|
+
role=agent.role,
|
|
138
|
+
capabilities=list(agent.capabilities),
|
|
139
|
+
node_id=self._node_id,
|
|
140
|
+
status="alive"
|
|
141
|
+
))
|
|
142
|
+
|
|
143
|
+
elif capability:
|
|
144
|
+
# Search all agents for capability
|
|
145
|
+
for role_name, agents in self._agent_registry.items():
|
|
146
|
+
for agent in agents:
|
|
147
|
+
if agent.agent_id != self._agent_id: # Exclude self
|
|
148
|
+
if capability in agent.capabilities:
|
|
149
|
+
results.append(PeerInfo(
|
|
150
|
+
agent_id=agent.agent_id,
|
|
151
|
+
role=agent.role,
|
|
152
|
+
capabilities=list(agent.capabilities),
|
|
153
|
+
node_id=self._node_id,
|
|
154
|
+
status="alive"
|
|
155
|
+
))
|
|
156
|
+
|
|
157
|
+
else:
|
|
158
|
+
# Return all peers
|
|
159
|
+
for role_name, agents in self._agent_registry.items():
|
|
160
|
+
for agent in agents:
|
|
161
|
+
if agent.agent_id != self._agent_id: # Exclude self
|
|
162
|
+
results.append(PeerInfo(
|
|
163
|
+
agent_id=agent.agent_id,
|
|
164
|
+
role=agent.role,
|
|
165
|
+
capabilities=list(agent.capabilities),
|
|
166
|
+
node_id=self._node_id,
|
|
167
|
+
status="alive"
|
|
168
|
+
))
|
|
169
|
+
|
|
170
|
+
return results
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def registry(self) -> Dict[str, PeerInfo]:
|
|
174
|
+
"""
|
|
175
|
+
Read-only access to the full agent registry.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Dictionary mapping agent_id to PeerInfo
|
|
179
|
+
|
|
180
|
+
Example:
|
|
181
|
+
for agent_id, info in self.peers.registry.items():
|
|
182
|
+
print(f"{agent_id}: {info.role}")
|
|
183
|
+
"""
|
|
184
|
+
result = {}
|
|
185
|
+
for role_name, agents in self._agent_registry.items():
|
|
186
|
+
for agent in agents:
|
|
187
|
+
if agent.agent_id != self._agent_id: # Exclude self
|
|
188
|
+
result[agent.agent_id] = PeerInfo(
|
|
189
|
+
agent_id=agent.agent_id,
|
|
190
|
+
role=agent.role,
|
|
191
|
+
capabilities=list(agent.capabilities),
|
|
192
|
+
node_id=self._node_id,
|
|
193
|
+
status="alive"
|
|
194
|
+
)
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
# ─────────────────────────────────────────────────────────────────
|
|
198
|
+
# IDENTITY
|
|
199
|
+
# ─────────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def my_role(self) -> str:
|
|
203
|
+
"""This agent's role."""
|
|
204
|
+
return self._agent_role
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def my_id(self) -> str:
|
|
208
|
+
"""This agent's unique ID."""
|
|
209
|
+
return self._agent_id
|
|
210
|
+
|
|
211
|
+
# ─────────────────────────────────────────────────────────────────
|
|
212
|
+
# DISCOVERY (simplified for tool use)
|
|
213
|
+
# ─────────────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
def list_roles(self) -> List[str]:
|
|
216
|
+
"""
|
|
217
|
+
Get list of available peer roles (excluding self).
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
List of role strings like ["scout", "analyst"]
|
|
221
|
+
|
|
222
|
+
Example:
|
|
223
|
+
roles = self.peers.list_roles()
|
|
224
|
+
# ["scout", "analyst", "reporter"]
|
|
225
|
+
"""
|
|
226
|
+
roles = set()
|
|
227
|
+
for role_name, agents in self._agent_registry.items():
|
|
228
|
+
for agent in agents:
|
|
229
|
+
if agent.agent_id != self._agent_id:
|
|
230
|
+
roles.add(role_name)
|
|
231
|
+
return sorted(list(roles))
|
|
232
|
+
|
|
233
|
+
def list_peers(self) -> List[Dict[str, Any]]:
|
|
234
|
+
"""
|
|
235
|
+
Get detailed list of peers with capabilities.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
List of dicts with role, agent_id, capabilities, status
|
|
239
|
+
|
|
240
|
+
Example:
|
|
241
|
+
peers = self.peers.list_peers()
|
|
242
|
+
# [{"role": "scout", "capabilities": ["reasoning"], ...}]
|
|
243
|
+
"""
|
|
244
|
+
seen = set()
|
|
245
|
+
peers = []
|
|
246
|
+
for role_name, agents in self._agent_registry.items():
|
|
247
|
+
for agent in agents:
|
|
248
|
+
if agent.agent_id != self._agent_id and agent.agent_id not in seen:
|
|
249
|
+
seen.add(agent.agent_id)
|
|
250
|
+
peers.append({
|
|
251
|
+
"role": agent.role,
|
|
252
|
+
"agent_id": agent.agent_id,
|
|
253
|
+
"capabilities": list(agent.capabilities),
|
|
254
|
+
"status": "online"
|
|
255
|
+
})
|
|
256
|
+
return peers
|
|
257
|
+
|
|
258
|
+
# ─────────────────────────────────────────────────────────────────
|
|
259
|
+
# MESSAGING - SEND
|
|
260
|
+
# ─────────────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
async def notify(self, target: str, message: Dict[str, Any]) -> bool:
|
|
263
|
+
"""
|
|
264
|
+
Send a fire-and-forget notification to a peer.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
target: Target agent role (e.g., "analyst") or agent_id
|
|
268
|
+
message: Message payload (any JSON-serializable dict)
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
True if message was sent successfully
|
|
272
|
+
|
|
273
|
+
Example:
|
|
274
|
+
await self.peers.notify("analyst", {
|
|
275
|
+
"event": "scouting_complete",
|
|
276
|
+
"data": {"findings": 42}
|
|
277
|
+
})
|
|
278
|
+
"""
|
|
279
|
+
target_agent = self._resolve_target(target)
|
|
280
|
+
if not target_agent:
|
|
281
|
+
self._logger.warning(f"Cannot notify: no peer found for '{target}'")
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
outgoing = OutgoingMessage(
|
|
285
|
+
target=target,
|
|
286
|
+
type=MessageType.NOTIFY,
|
|
287
|
+
data=message,
|
|
288
|
+
sender=self._agent_id,
|
|
289
|
+
sender_node=self._node_id
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
return await self._send_message(target_agent, outgoing)
|
|
293
|
+
|
|
294
|
+
async def request(
|
|
295
|
+
self,
|
|
296
|
+
target: str,
|
|
297
|
+
message: Dict[str, Any],
|
|
298
|
+
timeout: float = 30.0
|
|
299
|
+
) -> Optional[Dict[str, Any]]:
|
|
300
|
+
"""
|
|
301
|
+
Send a request and wait for a response.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
target: Target agent role (e.g., "scout") or agent_id
|
|
305
|
+
message: Request payload
|
|
306
|
+
timeout: Max seconds to wait for response (default: 30)
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Response data dict, or None if timeout/failure
|
|
310
|
+
|
|
311
|
+
Example:
|
|
312
|
+
response = await self.peers.request("scout", {
|
|
313
|
+
"need": "clarification",
|
|
314
|
+
"entity": "Entity_X"
|
|
315
|
+
}, timeout=10)
|
|
316
|
+
|
|
317
|
+
if response:
|
|
318
|
+
print(f"Got clarification: {response}")
|
|
319
|
+
"""
|
|
320
|
+
target_agent = self._resolve_target(target)
|
|
321
|
+
if not target_agent:
|
|
322
|
+
self._logger.warning(f"Cannot request: no peer found for '{target}'")
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
# Generate correlation ID for request-response matching
|
|
326
|
+
correlation_id = f"req-{uuid4().hex[:12]}"
|
|
327
|
+
|
|
328
|
+
outgoing = OutgoingMessage(
|
|
329
|
+
target=target,
|
|
330
|
+
type=MessageType.REQUEST,
|
|
331
|
+
data=message,
|
|
332
|
+
correlation_id=correlation_id,
|
|
333
|
+
sender=self._agent_id,
|
|
334
|
+
sender_node=self._node_id
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Create future to wait for response
|
|
338
|
+
response_future: asyncio.Future = asyncio.get_event_loop().create_future()
|
|
339
|
+
self._pending_requests[correlation_id] = response_future
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
# Send request
|
|
343
|
+
sent = await self._send_message(target_agent, outgoing)
|
|
344
|
+
if not sent:
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
# Wait for response with timeout
|
|
348
|
+
response = await asyncio.wait_for(response_future, timeout=timeout)
|
|
349
|
+
return response
|
|
350
|
+
|
|
351
|
+
except asyncio.TimeoutError:
|
|
352
|
+
self._logger.debug(f"Request to '{target}' timed out after {timeout}s")
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
finally:
|
|
356
|
+
# Cleanup pending request
|
|
357
|
+
self._pending_requests.pop(correlation_id, None)
|
|
358
|
+
|
|
359
|
+
async def respond(self, message: IncomingMessage, response: Dict[str, Any]) -> bool:
|
|
360
|
+
"""
|
|
361
|
+
Respond to an incoming request.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
message: The incoming request message
|
|
365
|
+
response: Response data to send back
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
True if response was sent successfully
|
|
369
|
+
|
|
370
|
+
Example:
|
|
371
|
+
message = await self.peers.receive()
|
|
372
|
+
if message and message.is_request:
|
|
373
|
+
await self.peers.respond(message, {"result": "done"})
|
|
374
|
+
"""
|
|
375
|
+
if not message.correlation_id:
|
|
376
|
+
self._logger.warning("Cannot respond: message has no correlation_id")
|
|
377
|
+
return False
|
|
378
|
+
|
|
379
|
+
# Find target agent
|
|
380
|
+
target_agent = self._resolve_target(message.sender)
|
|
381
|
+
if not target_agent:
|
|
382
|
+
self._logger.warning(f"Cannot respond: sender '{message.sender}' not found")
|
|
383
|
+
return False
|
|
384
|
+
|
|
385
|
+
outgoing = OutgoingMessage(
|
|
386
|
+
target=message.sender,
|
|
387
|
+
type=MessageType.RESPONSE,
|
|
388
|
+
data=response,
|
|
389
|
+
correlation_id=message.correlation_id,
|
|
390
|
+
sender=self._agent_id,
|
|
391
|
+
sender_node=self._node_id
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
return await self._send_message(target_agent, outgoing)
|
|
395
|
+
|
|
396
|
+
async def broadcast(self, message: Dict[str, Any]) -> int:
|
|
397
|
+
"""
|
|
398
|
+
Broadcast notification to ALL peers.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
message: Message payload to broadcast
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
Number of peers successfully notified
|
|
405
|
+
|
|
406
|
+
Example:
|
|
407
|
+
count = await self.peers.broadcast({
|
|
408
|
+
"event": "status_update",
|
|
409
|
+
"status": "completed"
|
|
410
|
+
})
|
|
411
|
+
print(f"Notified {count} peers")
|
|
412
|
+
"""
|
|
413
|
+
count = 0
|
|
414
|
+
for peer in self.discover():
|
|
415
|
+
if await self.notify(peer.role, message):
|
|
416
|
+
count += 1
|
|
417
|
+
return count
|
|
418
|
+
|
|
419
|
+
# ─────────────────────────────────────────────────────────────────
|
|
420
|
+
# TOOL ADAPTER
|
|
421
|
+
# ─────────────────────────────────────────────────────────────────
|
|
422
|
+
|
|
423
|
+
def as_tool(self) -> 'PeerTool':
|
|
424
|
+
"""
|
|
425
|
+
Get LLM tool adapter for this PeerClient.
|
|
426
|
+
|
|
427
|
+
Returns a PeerTool that wraps this client, providing:
|
|
428
|
+
- Tool definitions for LLM injection
|
|
429
|
+
- Tool execution dispatch
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
PeerTool instance
|
|
433
|
+
|
|
434
|
+
Example:
|
|
435
|
+
# In your agent
|
|
436
|
+
tools = [SearchTool(), self.peers.as_tool()]
|
|
437
|
+
response = llm.chat(task, tools=[t.schema for t in tools])
|
|
438
|
+
"""
|
|
439
|
+
from .peer_tool import PeerTool
|
|
440
|
+
return PeerTool(self)
|
|
441
|
+
|
|
442
|
+
# ─────────────────────────────────────────────────────────────────
|
|
443
|
+
# MESSAGING - RECEIVE
|
|
444
|
+
# ─────────────────────────────────────────────────────────────────
|
|
445
|
+
|
|
446
|
+
async def receive(self, timeout: float = None) -> Optional[IncomingMessage]:
|
|
447
|
+
"""
|
|
448
|
+
Receive the next incoming message.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
timeout: Max seconds to wait (None = wait forever)
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
IncomingMessage if received, None if timeout
|
|
455
|
+
|
|
456
|
+
Example:
|
|
457
|
+
# Wait up to 5 seconds for a message
|
|
458
|
+
message = await self.peers.receive(timeout=5)
|
|
459
|
+
if message:
|
|
460
|
+
print(f"Got message from {message.sender}: {message.data}")
|
|
461
|
+
"""
|
|
462
|
+
try:
|
|
463
|
+
if timeout is not None:
|
|
464
|
+
message = await asyncio.wait_for(
|
|
465
|
+
self._message_queue.get(),
|
|
466
|
+
timeout=timeout
|
|
467
|
+
)
|
|
468
|
+
else:
|
|
469
|
+
message = await self._message_queue.get()
|
|
470
|
+
|
|
471
|
+
return message
|
|
472
|
+
|
|
473
|
+
except asyncio.TimeoutError:
|
|
474
|
+
return None
|
|
475
|
+
|
|
476
|
+
def has_pending_messages(self) -> bool:
|
|
477
|
+
"""Check if there are messages waiting to be received."""
|
|
478
|
+
return not self._message_queue.empty()
|
|
479
|
+
|
|
480
|
+
# ─────────────────────────────────────────────────────────────────
|
|
481
|
+
# INTERNAL METHODS
|
|
482
|
+
# ─────────────────────────────────────────────────────────────────
|
|
483
|
+
|
|
484
|
+
def _resolve_target(self, target: str):
|
|
485
|
+
"""
|
|
486
|
+
Resolve target string to agent.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
target: Role name or agent_id
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
Agent instance or None
|
|
493
|
+
"""
|
|
494
|
+
# First try as role
|
|
495
|
+
agents = self._agent_registry.get(target, [])
|
|
496
|
+
if agents:
|
|
497
|
+
return agents[0]
|
|
498
|
+
|
|
499
|
+
# Try as agent_id
|
|
500
|
+
for role_name, agents in self._agent_registry.items():
|
|
501
|
+
for agent in agents:
|
|
502
|
+
if agent.agent_id == target:
|
|
503
|
+
return agent
|
|
504
|
+
|
|
505
|
+
return None
|
|
506
|
+
|
|
507
|
+
async def _send_message(self, target_agent, message: OutgoingMessage) -> bool:
|
|
508
|
+
"""
|
|
509
|
+
Send message to target agent via coordinator.
|
|
510
|
+
|
|
511
|
+
For local agents (same mesh), delivers directly to their queue.
|
|
512
|
+
For remote agents, sends via P2P coordinator.
|
|
513
|
+
"""
|
|
514
|
+
try:
|
|
515
|
+
# Check if target has a peer client (local agent)
|
|
516
|
+
if hasattr(target_agent, 'peers') and target_agent.peers:
|
|
517
|
+
# Direct local delivery
|
|
518
|
+
incoming = IncomingMessage(
|
|
519
|
+
sender=message.sender,
|
|
520
|
+
sender_node=message.sender_node,
|
|
521
|
+
type=message.type,
|
|
522
|
+
data=message.data,
|
|
523
|
+
correlation_id=message.correlation_id,
|
|
524
|
+
timestamp=message.timestamp
|
|
525
|
+
)
|
|
526
|
+
await target_agent.peers._deliver_message(incoming)
|
|
527
|
+
self._logger.debug(
|
|
528
|
+
f"Delivered {message.type.value} to local agent {target_agent.agent_id}"
|
|
529
|
+
)
|
|
530
|
+
return True
|
|
531
|
+
|
|
532
|
+
# Remote delivery via P2P coordinator
|
|
533
|
+
if self._coordinator:
|
|
534
|
+
msg_type = f"PEER_{message.type.value.upper()}"
|
|
535
|
+
payload = {
|
|
536
|
+
'sender': message.sender,
|
|
537
|
+
'sender_node': message.sender_node,
|
|
538
|
+
'target': message.target,
|
|
539
|
+
'data': message.data,
|
|
540
|
+
'correlation_id': message.correlation_id,
|
|
541
|
+
'timestamp': message.timestamp
|
|
542
|
+
}
|
|
543
|
+
return await self._coordinator._send_p2p_message(
|
|
544
|
+
target_agent.node_id or self._node_id,
|
|
545
|
+
msg_type,
|
|
546
|
+
payload
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
self._logger.warning("No delivery mechanism available")
|
|
550
|
+
return False
|
|
551
|
+
|
|
552
|
+
except Exception as e:
|
|
553
|
+
self._logger.error(f"Failed to send message: {e}")
|
|
554
|
+
return False
|
|
555
|
+
|
|
556
|
+
async def _deliver_message(self, message: IncomingMessage):
|
|
557
|
+
"""
|
|
558
|
+
Deliver an incoming message to this client.
|
|
559
|
+
|
|
560
|
+
Called by other PeerClients (local) or coordinator (remote).
|
|
561
|
+
"""
|
|
562
|
+
# Check if this is a response to a pending request
|
|
563
|
+
if message.type == MessageType.RESPONSE and message.correlation_id:
|
|
564
|
+
future = self._pending_requests.get(message.correlation_id)
|
|
565
|
+
if future and not future.done():
|
|
566
|
+
future.set_result(message.data)
|
|
567
|
+
self._logger.debug(
|
|
568
|
+
f"Delivered response for {message.correlation_id}"
|
|
569
|
+
)
|
|
570
|
+
return
|
|
571
|
+
|
|
572
|
+
# Otherwise queue for receive()
|
|
573
|
+
await self._message_queue.put(message)
|
|
574
|
+
self._logger.debug(
|
|
575
|
+
f"Queued {message.type.value} from {message.sender}"
|
|
576
|
+
)
|