jarviscore-framework 0.3.1__py3-none-any.whl → 0.3.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.
- examples/customagent_cognitive_discovery_example.py +49 -8
- examples/customagent_distributed_example.py +140 -1
- examples/fastapi_integration_example.py +70 -7
- jarviscore/__init__.py +1 -1
- jarviscore/core/mesh.py +149 -0
- jarviscore/data/examples/customagent_cognitive_discovery_example.py +49 -8
- jarviscore/data/examples/customagent_distributed_example.py +140 -1
- jarviscore/data/examples/fastapi_integration_example.py +70 -7
- jarviscore/docs/API_REFERENCE.md +547 -5
- jarviscore/docs/CHANGELOG.md +89 -0
- jarviscore/docs/CONFIGURATION.md +1 -1
- jarviscore/docs/CUSTOMAGENT_GUIDE.md +347 -2
- jarviscore/docs/TROUBLESHOOTING.md +1 -1
- jarviscore/docs/USER_GUIDE.md +286 -5
- jarviscore/p2p/coordinator.py +36 -7
- jarviscore/p2p/messages.py +13 -0
- jarviscore/p2p/peer_client.py +355 -23
- jarviscore/p2p/peer_tool.py +17 -11
- jarviscore/profiles/customagent.py +9 -2
- jarviscore/testing/__init__.py +35 -0
- jarviscore/testing/mocks.py +578 -0
- {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/METADATA +2 -2
- {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/RECORD +31 -24
- tests/test_17_session_context.py +489 -0
- tests/test_18_mesh_diagnostics.py +465 -0
- tests/test_19_async_requests.py +516 -0
- tests/test_20_load_balancing.py +546 -0
- tests/test_21_mock_testing.py +776 -0
- {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/WHEEL +0 -0
- {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/top_level.txt +0 -0
jarviscore/p2p/peer_tool.py
CHANGED
|
@@ -185,21 +185,27 @@ class PeerTool:
|
|
|
185
185
|
if not role or not question:
|
|
186
186
|
return "Error: 'role' and 'question' are required"
|
|
187
187
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return (
|
|
193
|
-
f"Error: '{role}' is not online. "
|
|
194
|
-
f"Available: {', '.join(available) if available else 'none'}"
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
# Send request
|
|
188
|
+
self._logger.info(f"ask_peer: Attempting to contact role='{role}'")
|
|
189
|
+
|
|
190
|
+
# Send request (request() will handle resolution of local/remote peers)
|
|
191
|
+
# Use 10min timeout to allow analyst time to query database and analyze
|
|
198
192
|
response = await self._peers.request(
|
|
199
193
|
role,
|
|
200
194
|
{"query": question, "from": self._peers.my_role},
|
|
201
|
-
timeout=
|
|
195
|
+
timeout=600.0
|
|
202
196
|
)
|
|
197
|
+
|
|
198
|
+
self._logger.info(f"ask_peer: Got response: {response}")
|
|
199
|
+
|
|
200
|
+
# If request() returns None, peer wasn't found or didn't respond
|
|
201
|
+
if response is None:
|
|
202
|
+
peers = self._peers.list_peers()
|
|
203
|
+
available_roles = [p['role'] for p in peers]
|
|
204
|
+
self._logger.warning(f"ask_peer: request() returned None. Available peers: {available_roles}")
|
|
205
|
+
return (
|
|
206
|
+
f"Error: '{role}' not found or did not respond. "
|
|
207
|
+
f"Available: {', '.join(available_roles) if available_roles else 'none'}"
|
|
208
|
+
)
|
|
203
209
|
|
|
204
210
|
if response is None:
|
|
205
211
|
return f"Error: {role} did not respond (timeout)"
|
|
@@ -181,15 +181,22 @@ class CustomAgent(Profile):
|
|
|
181
181
|
Handles:
|
|
182
182
|
- REQUEST messages: calls on_peer_request, sends response if auto_respond=True
|
|
183
183
|
- NOTIFY messages: calls on_peer_notify
|
|
184
|
+
- RESPONSE messages: ignored (handled by _deliver_message resolving futures)
|
|
184
185
|
"""
|
|
185
186
|
from jarviscore.p2p.messages import MessageType
|
|
186
187
|
|
|
187
188
|
try:
|
|
189
|
+
# Skip RESPONSE messages - they should be handled by pending request futures
|
|
190
|
+
if msg.type == MessageType.RESPONSE:
|
|
191
|
+
self._logger.debug(
|
|
192
|
+
f"[{self.role}] Ignoring orphaned RESPONSE from {msg.sender} (no pending request)"
|
|
193
|
+
)
|
|
194
|
+
return
|
|
195
|
+
|
|
188
196
|
# Check if this is a request (expects response)
|
|
189
197
|
is_request = (
|
|
190
198
|
msg.type == MessageType.REQUEST or
|
|
191
|
-
getattr(msg, 'is_request', False)
|
|
192
|
-
msg.correlation_id is not None
|
|
199
|
+
getattr(msg, 'is_request', False)
|
|
193
200
|
)
|
|
194
201
|
|
|
195
202
|
if is_request:
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Testing utilities for JarvisCore.
|
|
3
|
+
|
|
4
|
+
Provides mock implementations for unit testing agents without
|
|
5
|
+
requiring real P2P infrastructure or network connections.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
from jarviscore.testing import MockMesh, MockPeerClient
|
|
9
|
+
|
|
10
|
+
# Using MockMesh for full integration testing
|
|
11
|
+
mesh = MockMesh()
|
|
12
|
+
mesh.add(MyAgent)
|
|
13
|
+
await mesh.start()
|
|
14
|
+
|
|
15
|
+
agent = mesh.get_agent("my_role")
|
|
16
|
+
agent.peers.set_mock_response("analyst", {"result": "test"})
|
|
17
|
+
|
|
18
|
+
# Test and verify
|
|
19
|
+
await agent.process_request(...)
|
|
20
|
+
agent.peers.assert_requested("analyst")
|
|
21
|
+
|
|
22
|
+
# Using MockPeerClient for unit testing
|
|
23
|
+
agent = MyAgent()
|
|
24
|
+
agent.peers = MockPeerClient(mock_peers=[
|
|
25
|
+
{"role": "analyst", "capabilities": ["analysis"]}
|
|
26
|
+
])
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from .mocks import MockMesh, MockPeerClient, MockPeerInfo
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
'MockMesh',
|
|
33
|
+
'MockPeerClient',
|
|
34
|
+
'MockPeerInfo',
|
|
35
|
+
]
|
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mock implementations for testing JarvisCore agents.
|
|
3
|
+
|
|
4
|
+
These mocks allow testing agent logic without:
|
|
5
|
+
- Real ZMQ connections
|
|
6
|
+
- SWIM protocol
|
|
7
|
+
- Network operations
|
|
8
|
+
- Multiple processes
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
from jarviscore.testing import MockMesh, MockPeerClient
|
|
12
|
+
|
|
13
|
+
# Create mock mesh with simulated peers
|
|
14
|
+
mesh = MockMesh()
|
|
15
|
+
mesh.add(MyAgent)
|
|
16
|
+
await mesh.start()
|
|
17
|
+
|
|
18
|
+
# Inject mock peer client for testing
|
|
19
|
+
agent = mesh.get_agent("my_role")
|
|
20
|
+
agent.peers.set_mock_response("analyst", {"result": "test"})
|
|
21
|
+
|
|
22
|
+
# Test agent behavior
|
|
23
|
+
response = await agent.peers.request("analyst", {"question": "test"})
|
|
24
|
+
assert response == {"result": "test"}
|
|
25
|
+
|
|
26
|
+
# Verify interactions
|
|
27
|
+
agent.peers.assert_requested("analyst")
|
|
28
|
+
"""
|
|
29
|
+
import asyncio
|
|
30
|
+
import time
|
|
31
|
+
from typing import List, Dict, Any, Optional, Callable
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from uuid import uuid4
|
|
34
|
+
|
|
35
|
+
from jarviscore.core.agent import Agent
|
|
36
|
+
from jarviscore.p2p.messages import PeerInfo, IncomingMessage, MessageType
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class MockPeerInfo:
|
|
41
|
+
"""Mock peer for testing discovery."""
|
|
42
|
+
role: str
|
|
43
|
+
capabilities: List[str] = field(default_factory=list)
|
|
44
|
+
agent_id: str = ""
|
|
45
|
+
node_id: str = "mock-node"
|
|
46
|
+
status: str = "alive"
|
|
47
|
+
description: str = ""
|
|
48
|
+
|
|
49
|
+
def __post_init__(self):
|
|
50
|
+
if not self.agent_id:
|
|
51
|
+
self.agent_id = f"{self.role}-{uuid4().hex[:8]}"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class MockPeerClient:
|
|
55
|
+
"""
|
|
56
|
+
Mock PeerClient for unit testing.
|
|
57
|
+
|
|
58
|
+
Simulates peer discovery and messaging without real P2P.
|
|
59
|
+
Can be configured with mock responses for testing specific scenarios.
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
client = MockPeerClient(
|
|
63
|
+
mock_peers=[
|
|
64
|
+
{"role": "analyst", "capabilities": ["analysis"]},
|
|
65
|
+
{"role": "scout", "capabilities": ["research"]}
|
|
66
|
+
]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Configure mock response
|
|
70
|
+
client.set_mock_response("analyst", {"result": "test data"})
|
|
71
|
+
|
|
72
|
+
# Now test your agent
|
|
73
|
+
response = await agent.peers.request("analyst", {"question": "..."})
|
|
74
|
+
assert response == {"result": "test data"}
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
agent_id: str = "mock-agent",
|
|
80
|
+
agent_role: str = "mock",
|
|
81
|
+
mock_peers: List[Dict[str, Any]] = None,
|
|
82
|
+
auto_respond: bool = True
|
|
83
|
+
):
|
|
84
|
+
"""
|
|
85
|
+
Initialize MockPeerClient.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
agent_id: ID for the mock agent
|
|
89
|
+
agent_role: Role for the mock agent
|
|
90
|
+
mock_peers: List of peer definitions with role, capabilities
|
|
91
|
+
auto_respond: If True, automatically respond to requests with mock data
|
|
92
|
+
"""
|
|
93
|
+
self._agent_id = agent_id
|
|
94
|
+
self._agent_role = agent_role
|
|
95
|
+
self._auto_respond = auto_respond
|
|
96
|
+
|
|
97
|
+
# Build mock peer registry
|
|
98
|
+
self._mock_peers: List[MockPeerInfo] = []
|
|
99
|
+
if mock_peers:
|
|
100
|
+
for peer_def in mock_peers:
|
|
101
|
+
self._mock_peers.append(MockPeerInfo(
|
|
102
|
+
role=peer_def.get("role", "unknown"),
|
|
103
|
+
capabilities=peer_def.get("capabilities", []),
|
|
104
|
+
agent_id=peer_def.get("agent_id", ""),
|
|
105
|
+
node_id=peer_def.get("node_id", "mock-node"),
|
|
106
|
+
description=peer_def.get("description", "")
|
|
107
|
+
))
|
|
108
|
+
|
|
109
|
+
# Mock responses for request()
|
|
110
|
+
self._mock_responses: Dict[str, Dict[str, Any]] = {}
|
|
111
|
+
self._default_response: Dict[str, Any] = {"status": "success", "mock": True}
|
|
112
|
+
|
|
113
|
+
# Message tracking for assertions
|
|
114
|
+
self._sent_notifications: List[Dict[str, Any]] = []
|
|
115
|
+
self._sent_requests: List[Dict[str, Any]] = []
|
|
116
|
+
self._sent_broadcasts: List[Dict[str, Any]] = []
|
|
117
|
+
|
|
118
|
+
# Message queue for receive()
|
|
119
|
+
self._message_queue: asyncio.Queue = asyncio.Queue()
|
|
120
|
+
|
|
121
|
+
# Request handler for custom responses
|
|
122
|
+
self._request_handler: Optional[Callable] = None
|
|
123
|
+
|
|
124
|
+
# Load balancing state (for strategy support)
|
|
125
|
+
self._round_robin_index: Dict[str, int] = {}
|
|
126
|
+
self._peer_last_used: Dict[str, float] = {}
|
|
127
|
+
|
|
128
|
+
# Identity properties
|
|
129
|
+
@property
|
|
130
|
+
def my_role(self) -> str:
|
|
131
|
+
return self._agent_role
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def my_id(self) -> str:
|
|
135
|
+
return self._agent_id
|
|
136
|
+
|
|
137
|
+
# Discovery methods
|
|
138
|
+
def get_peer(self, role: str) -> Optional[PeerInfo]:
|
|
139
|
+
"""Get mock peer by role."""
|
|
140
|
+
for peer in self._mock_peers:
|
|
141
|
+
if peer.role == role:
|
|
142
|
+
return PeerInfo(
|
|
143
|
+
agent_id=peer.agent_id,
|
|
144
|
+
role=peer.role,
|
|
145
|
+
capabilities=peer.capabilities,
|
|
146
|
+
node_id=peer.node_id,
|
|
147
|
+
status=peer.status
|
|
148
|
+
)
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
def discover(
|
|
152
|
+
self,
|
|
153
|
+
capability: str = None,
|
|
154
|
+
role: str = None,
|
|
155
|
+
strategy: str = "first"
|
|
156
|
+
) -> List[PeerInfo]:
|
|
157
|
+
"""Discover mock peers with strategy support."""
|
|
158
|
+
results = []
|
|
159
|
+
for peer in self._mock_peers:
|
|
160
|
+
if role and peer.role != role:
|
|
161
|
+
continue
|
|
162
|
+
if capability and capability not in peer.capabilities:
|
|
163
|
+
continue
|
|
164
|
+
results.append(PeerInfo(
|
|
165
|
+
agent_id=peer.agent_id,
|
|
166
|
+
role=peer.role,
|
|
167
|
+
capabilities=peer.capabilities,
|
|
168
|
+
node_id=peer.node_id,
|
|
169
|
+
status=peer.status
|
|
170
|
+
))
|
|
171
|
+
|
|
172
|
+
# Apply strategy if needed
|
|
173
|
+
if results and strategy != "first":
|
|
174
|
+
import random
|
|
175
|
+
key = capability or role or "all"
|
|
176
|
+
|
|
177
|
+
if strategy == "random":
|
|
178
|
+
random.shuffle(results)
|
|
179
|
+
elif strategy == "round_robin":
|
|
180
|
+
idx = self._round_robin_index.get(key, 0)
|
|
181
|
+
results = results[idx:] + results[:idx]
|
|
182
|
+
self._round_robin_index[key] = (idx + 1) % len(results) if results else 0
|
|
183
|
+
elif strategy == "least_recent":
|
|
184
|
+
results.sort(key=lambda p: self._peer_last_used.get(p.agent_id, 0.0))
|
|
185
|
+
|
|
186
|
+
return results
|
|
187
|
+
|
|
188
|
+
def discover_one(
|
|
189
|
+
self,
|
|
190
|
+
capability: str = None,
|
|
191
|
+
role: str = None,
|
|
192
|
+
strategy: str = "first"
|
|
193
|
+
) -> Optional[PeerInfo]:
|
|
194
|
+
"""Discover single mock peer."""
|
|
195
|
+
peers = self.discover(capability=capability, role=role, strategy=strategy)
|
|
196
|
+
return peers[0] if peers else None
|
|
197
|
+
|
|
198
|
+
def record_peer_usage(self, peer_id: str):
|
|
199
|
+
"""Record peer usage for least_recent strategy."""
|
|
200
|
+
self._peer_last_used[peer_id] = time.time()
|
|
201
|
+
|
|
202
|
+
def list_roles(self) -> List[str]:
|
|
203
|
+
"""List available mock roles."""
|
|
204
|
+
return list(set(p.role for p in self._mock_peers))
|
|
205
|
+
|
|
206
|
+
def list_peers(self) -> List[Dict[str, Any]]:
|
|
207
|
+
"""List all mock peers."""
|
|
208
|
+
return [
|
|
209
|
+
{
|
|
210
|
+
"role": p.role,
|
|
211
|
+
"agent_id": p.agent_id,
|
|
212
|
+
"capabilities": p.capabilities,
|
|
213
|
+
"status": p.status,
|
|
214
|
+
"location": "mock"
|
|
215
|
+
}
|
|
216
|
+
for p in self._mock_peers
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
# Messaging methods
|
|
220
|
+
async def notify(
|
|
221
|
+
self,
|
|
222
|
+
target: str,
|
|
223
|
+
message: Dict[str, Any],
|
|
224
|
+
context: Optional[Dict[str, Any]] = None
|
|
225
|
+
) -> bool:
|
|
226
|
+
"""Send mock notification (tracked for assertions)."""
|
|
227
|
+
self._sent_notifications.append({
|
|
228
|
+
"target": target,
|
|
229
|
+
"message": message,
|
|
230
|
+
"context": context,
|
|
231
|
+
"timestamp": time.time()
|
|
232
|
+
})
|
|
233
|
+
return True
|
|
234
|
+
|
|
235
|
+
async def request(
|
|
236
|
+
self,
|
|
237
|
+
target: str,
|
|
238
|
+
message: Dict[str, Any],
|
|
239
|
+
timeout: float = 30.0,
|
|
240
|
+
context: Optional[Dict[str, Any]] = None
|
|
241
|
+
) -> Optional[Dict[str, Any]]:
|
|
242
|
+
"""Send mock request and return configured response."""
|
|
243
|
+
self._sent_requests.append({
|
|
244
|
+
"target": target,
|
|
245
|
+
"message": message,
|
|
246
|
+
"context": context,
|
|
247
|
+
"timeout": timeout,
|
|
248
|
+
"timestamp": time.time()
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
# Use custom handler if set
|
|
252
|
+
if self._request_handler:
|
|
253
|
+
return await self._request_handler(target, message, context)
|
|
254
|
+
|
|
255
|
+
# Return configured mock response
|
|
256
|
+
if target in self._mock_responses:
|
|
257
|
+
return self._mock_responses[target]
|
|
258
|
+
|
|
259
|
+
if self._auto_respond:
|
|
260
|
+
return self._default_response
|
|
261
|
+
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
async def respond(
|
|
265
|
+
self,
|
|
266
|
+
message: IncomingMessage,
|
|
267
|
+
response: Dict[str, Any],
|
|
268
|
+
context: Optional[Dict[str, Any]] = None
|
|
269
|
+
) -> bool:
|
|
270
|
+
"""Mock respond (no-op, returns True)."""
|
|
271
|
+
return True
|
|
272
|
+
|
|
273
|
+
async def broadcast(
|
|
274
|
+
self,
|
|
275
|
+
message: Dict[str, Any],
|
|
276
|
+
context: Optional[Dict[str, Any]] = None
|
|
277
|
+
) -> int:
|
|
278
|
+
"""Send mock broadcast (tracked for assertions)."""
|
|
279
|
+
self._sent_broadcasts.append({
|
|
280
|
+
"message": message,
|
|
281
|
+
"context": context,
|
|
282
|
+
"timestamp": time.time()
|
|
283
|
+
})
|
|
284
|
+
return len(self._mock_peers)
|
|
285
|
+
|
|
286
|
+
async def receive(self, timeout: float = None) -> Optional[IncomingMessage]:
|
|
287
|
+
"""Receive from mock message queue."""
|
|
288
|
+
try:
|
|
289
|
+
if timeout:
|
|
290
|
+
return await asyncio.wait_for(
|
|
291
|
+
self._message_queue.get(),
|
|
292
|
+
timeout=timeout
|
|
293
|
+
)
|
|
294
|
+
return self._message_queue.get_nowait()
|
|
295
|
+
except (asyncio.TimeoutError, asyncio.QueueEmpty):
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
def has_pending_messages(self) -> bool:
|
|
299
|
+
"""Check mock message queue."""
|
|
300
|
+
return not self._message_queue.empty()
|
|
301
|
+
|
|
302
|
+
# Async request methods (Feature 2 compatibility)
|
|
303
|
+
async def ask_async(
|
|
304
|
+
self,
|
|
305
|
+
target: str,
|
|
306
|
+
message: Dict[str, Any],
|
|
307
|
+
timeout: float = 120.0,
|
|
308
|
+
context: Optional[Dict[str, Any]] = None
|
|
309
|
+
) -> str:
|
|
310
|
+
"""Mock async request."""
|
|
311
|
+
correlation_id = f"mock-{uuid4().hex[:12]}"
|
|
312
|
+
self._sent_requests.append({
|
|
313
|
+
"target": target,
|
|
314
|
+
"message": message,
|
|
315
|
+
"context": context,
|
|
316
|
+
"correlation_id": correlation_id,
|
|
317
|
+
"async": True,
|
|
318
|
+
"timestamp": time.time()
|
|
319
|
+
})
|
|
320
|
+
return correlation_id
|
|
321
|
+
|
|
322
|
+
async def check_inbox(
|
|
323
|
+
self,
|
|
324
|
+
request_id: str,
|
|
325
|
+
timeout: float = 0.0,
|
|
326
|
+
remove: bool = True
|
|
327
|
+
) -> Optional[Dict[str, Any]]:
|
|
328
|
+
"""Mock inbox check - returns default response."""
|
|
329
|
+
if self._auto_respond:
|
|
330
|
+
return self._default_response
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
def get_pending_async_requests(self) -> List[Dict[str, Any]]:
|
|
334
|
+
"""Get pending async requests (always empty for mock)."""
|
|
335
|
+
return []
|
|
336
|
+
|
|
337
|
+
def clear_inbox(self, request_id: Optional[str] = None):
|
|
338
|
+
"""Clear inbox (no-op for mock)."""
|
|
339
|
+
pass
|
|
340
|
+
|
|
341
|
+
# Cognitive context (Feature 4 compatibility)
|
|
342
|
+
def get_cognitive_context(
|
|
343
|
+
self,
|
|
344
|
+
format: str = "markdown",
|
|
345
|
+
include_capabilities: bool = True,
|
|
346
|
+
include_description: bool = True,
|
|
347
|
+
tool_name: str = "ask_peer"
|
|
348
|
+
) -> str:
|
|
349
|
+
"""Generate mock cognitive context."""
|
|
350
|
+
if not self._mock_peers:
|
|
351
|
+
return ""
|
|
352
|
+
|
|
353
|
+
lines = ["## AVAILABLE MESH PEERS (Mock)", ""]
|
|
354
|
+
for peer in self._mock_peers:
|
|
355
|
+
lines.append(f"- **{peer.role}** (`{peer.agent_id}`)")
|
|
356
|
+
if include_capabilities and peer.capabilities:
|
|
357
|
+
lines.append(f" - Capabilities: {', '.join(peer.capabilities)}")
|
|
358
|
+
if include_description and peer.description:
|
|
359
|
+
lines.append(f" - {peer.description}")
|
|
360
|
+
lines.append("")
|
|
361
|
+
lines.append(f"Use the `{tool_name}` tool to communicate with these peers.")
|
|
362
|
+
|
|
363
|
+
return "\n".join(lines)
|
|
364
|
+
|
|
365
|
+
# Test configuration methods
|
|
366
|
+
def set_mock_response(self, target: str, response: Dict[str, Any]):
|
|
367
|
+
"""Configure response for a specific target."""
|
|
368
|
+
self._mock_responses[target] = response
|
|
369
|
+
|
|
370
|
+
def set_default_response(self, response: Dict[str, Any]):
|
|
371
|
+
"""Set default response for all requests."""
|
|
372
|
+
self._default_response = response
|
|
373
|
+
|
|
374
|
+
def set_request_handler(self, handler: Callable):
|
|
375
|
+
"""
|
|
376
|
+
Set custom request handler.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
handler: Async function(target, message, context) -> response
|
|
380
|
+
"""
|
|
381
|
+
self._request_handler = handler
|
|
382
|
+
|
|
383
|
+
def add_mock_peer(self, role: str, capabilities: List[str] = None, **kwargs):
|
|
384
|
+
"""Add a mock peer dynamically."""
|
|
385
|
+
self._mock_peers.append(MockPeerInfo(
|
|
386
|
+
role=role,
|
|
387
|
+
capabilities=capabilities or [],
|
|
388
|
+
**kwargs
|
|
389
|
+
))
|
|
390
|
+
|
|
391
|
+
def inject_message(
|
|
392
|
+
self,
|
|
393
|
+
sender: str,
|
|
394
|
+
message_type: MessageType,
|
|
395
|
+
data: Dict[str, Any],
|
|
396
|
+
correlation_id: str = None,
|
|
397
|
+
context: Optional[Dict[str, Any]] = None
|
|
398
|
+
):
|
|
399
|
+
"""
|
|
400
|
+
Inject a message into the receive queue for testing.
|
|
401
|
+
|
|
402
|
+
Example:
|
|
403
|
+
client.inject_message(
|
|
404
|
+
sender="analyst",
|
|
405
|
+
message_type=MessageType.NOTIFY,
|
|
406
|
+
data={"event": "analysis_complete", "result": {...}}
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
# Agent receives the injected message
|
|
410
|
+
msg = await agent.peers.receive()
|
|
411
|
+
"""
|
|
412
|
+
incoming = IncomingMessage(
|
|
413
|
+
sender=sender,
|
|
414
|
+
sender_node="mock-node",
|
|
415
|
+
type=message_type,
|
|
416
|
+
data=data,
|
|
417
|
+
correlation_id=correlation_id,
|
|
418
|
+
context=context
|
|
419
|
+
)
|
|
420
|
+
self._message_queue.put_nowait(incoming)
|
|
421
|
+
|
|
422
|
+
# Assertion helpers
|
|
423
|
+
def get_sent_notifications(self) -> List[Dict[str, Any]]:
|
|
424
|
+
"""Get all notifications sent during test."""
|
|
425
|
+
return self._sent_notifications.copy()
|
|
426
|
+
|
|
427
|
+
def get_sent_requests(self) -> List[Dict[str, Any]]:
|
|
428
|
+
"""Get all requests sent during test."""
|
|
429
|
+
return self._sent_requests.copy()
|
|
430
|
+
|
|
431
|
+
def get_sent_broadcasts(self) -> List[Dict[str, Any]]:
|
|
432
|
+
"""Get all broadcasts sent during test."""
|
|
433
|
+
return self._sent_broadcasts.copy()
|
|
434
|
+
|
|
435
|
+
def assert_notified(self, target: str, message_contains: Dict[str, Any] = None):
|
|
436
|
+
"""Assert that a notification was sent to target."""
|
|
437
|
+
for notif in self._sent_notifications:
|
|
438
|
+
if notif["target"] == target:
|
|
439
|
+
if message_contains:
|
|
440
|
+
for key, value in message_contains.items():
|
|
441
|
+
if notif["message"].get(key) != value:
|
|
442
|
+
continue
|
|
443
|
+
return True
|
|
444
|
+
raise AssertionError(f"Expected notification to {target} not found")
|
|
445
|
+
|
|
446
|
+
def assert_requested(self, target: str, message_contains: Dict[str, Any] = None):
|
|
447
|
+
"""Assert that a request was sent to target."""
|
|
448
|
+
for req in self._sent_requests:
|
|
449
|
+
if req["target"] == target:
|
|
450
|
+
if message_contains:
|
|
451
|
+
for key, value in message_contains.items():
|
|
452
|
+
if req["message"].get(key) != value:
|
|
453
|
+
continue
|
|
454
|
+
return True
|
|
455
|
+
raise AssertionError(f"Expected request to {target} not found")
|
|
456
|
+
|
|
457
|
+
def assert_broadcasted(self, message_contains: Dict[str, Any] = None):
|
|
458
|
+
"""Assert that a broadcast was sent."""
|
|
459
|
+
if not self._sent_broadcasts:
|
|
460
|
+
raise AssertionError("No broadcasts sent")
|
|
461
|
+
if message_contains:
|
|
462
|
+
for broadcast in self._sent_broadcasts:
|
|
463
|
+
match = all(
|
|
464
|
+
broadcast["message"].get(k) == v
|
|
465
|
+
for k, v in message_contains.items()
|
|
466
|
+
)
|
|
467
|
+
if match:
|
|
468
|
+
return True
|
|
469
|
+
raise AssertionError(f"Broadcast with {message_contains} not found")
|
|
470
|
+
return True
|
|
471
|
+
|
|
472
|
+
def reset(self):
|
|
473
|
+
"""Clear all tracking state."""
|
|
474
|
+
self._sent_notifications.clear()
|
|
475
|
+
self._sent_requests.clear()
|
|
476
|
+
self._sent_broadcasts.clear()
|
|
477
|
+
self._mock_responses.clear()
|
|
478
|
+
self._round_robin_index.clear()
|
|
479
|
+
self._peer_last_used.clear()
|
|
480
|
+
while not self._message_queue.empty():
|
|
481
|
+
try:
|
|
482
|
+
self._message_queue.get_nowait()
|
|
483
|
+
except asyncio.QueueEmpty:
|
|
484
|
+
break
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
class MockMesh:
|
|
488
|
+
"""
|
|
489
|
+
Mock Mesh for unit testing.
|
|
490
|
+
|
|
491
|
+
Provides agent registration and setup without P2P infrastructure.
|
|
492
|
+
|
|
493
|
+
Example:
|
|
494
|
+
mesh = MockMesh()
|
|
495
|
+
mesh.add(MyAgent)
|
|
496
|
+
await mesh.start()
|
|
497
|
+
|
|
498
|
+
agent = mesh.get_agent("my_role")
|
|
499
|
+
# Test agent behavior...
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
def __init__(self, mode: str = "p2p"):
|
|
503
|
+
self.mode = mode
|
|
504
|
+
self.agents: List[Agent] = []
|
|
505
|
+
self._agent_registry: Dict[str, List[Agent]] = {}
|
|
506
|
+
self._started = False
|
|
507
|
+
|
|
508
|
+
def add(self, agent_class_or_instance, agent_id: str = None, **kwargs) -> Agent:
|
|
509
|
+
"""Register agent with mock mesh."""
|
|
510
|
+
if isinstance(agent_class_or_instance, Agent):
|
|
511
|
+
agent = agent_class_or_instance
|
|
512
|
+
else:
|
|
513
|
+
agent = agent_class_or_instance(agent_id=agent_id, **kwargs)
|
|
514
|
+
|
|
515
|
+
agent._mesh = self
|
|
516
|
+
self.agents.append(agent)
|
|
517
|
+
|
|
518
|
+
if agent.role not in self._agent_registry:
|
|
519
|
+
self._agent_registry[agent.role] = []
|
|
520
|
+
self._agent_registry[agent.role].append(agent)
|
|
521
|
+
|
|
522
|
+
return agent
|
|
523
|
+
|
|
524
|
+
async def start(self):
|
|
525
|
+
"""Start mock mesh (runs agent setup)."""
|
|
526
|
+
for agent in self.agents:
|
|
527
|
+
await agent.setup()
|
|
528
|
+
# Inject mock peer client
|
|
529
|
+
agent.peers = MockPeerClient(
|
|
530
|
+
agent_id=agent.agent_id,
|
|
531
|
+
agent_role=agent.role,
|
|
532
|
+
mock_peers=self._build_peer_list(agent)
|
|
533
|
+
)
|
|
534
|
+
self._started = True
|
|
535
|
+
|
|
536
|
+
async def stop(self):
|
|
537
|
+
"""Stop mock mesh."""
|
|
538
|
+
for agent in self.agents:
|
|
539
|
+
await agent.teardown()
|
|
540
|
+
self._started = False
|
|
541
|
+
|
|
542
|
+
def get_agent(self, role: str) -> Optional[Agent]:
|
|
543
|
+
"""Get agent by role."""
|
|
544
|
+
agents = self._agent_registry.get(role, [])
|
|
545
|
+
return agents[0] if agents else None
|
|
546
|
+
|
|
547
|
+
def _build_peer_list(self, exclude_agent: Agent) -> List[Dict[str, Any]]:
|
|
548
|
+
"""Build peer list excluding the specified agent."""
|
|
549
|
+
peers = []
|
|
550
|
+
for agent in self.agents:
|
|
551
|
+
if agent.agent_id != exclude_agent.agent_id:
|
|
552
|
+
peers.append({
|
|
553
|
+
"role": agent.role,
|
|
554
|
+
"agent_id": agent.agent_id,
|
|
555
|
+
"capabilities": list(agent.capabilities),
|
|
556
|
+
"description": getattr(agent, 'description', '')
|
|
557
|
+
})
|
|
558
|
+
return peers
|
|
559
|
+
|
|
560
|
+
def get_diagnostics(self) -> Dict[str, Any]:
|
|
561
|
+
"""Get mock diagnostics."""
|
|
562
|
+
return {
|
|
563
|
+
"local_node": {
|
|
564
|
+
"mode": self.mode,
|
|
565
|
+
"started": self._started,
|
|
566
|
+
"agent_count": len(self.agents)
|
|
567
|
+
},
|
|
568
|
+
"known_peers": [],
|
|
569
|
+
"local_agents": [
|
|
570
|
+
{
|
|
571
|
+
"role": a.role,
|
|
572
|
+
"agent_id": a.agent_id,
|
|
573
|
+
"capabilities": list(a.capabilities)
|
|
574
|
+
}
|
|
575
|
+
for a in self.agents
|
|
576
|
+
],
|
|
577
|
+
"connectivity_status": "mock"
|
|
578
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jarviscore-framework
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Build autonomous AI agents in 3 lines of code. Production-ready orchestration with P2P mesh networking.
|
|
5
5
|
Author-email: Ruth Mutua <mutuandinda82@gmail.com>, Muyukani Kizito <muyukani@prescottdata.io>
|
|
6
6
|
Maintainer-email: Prescott Data <info@prescottdata.io>
|
|
@@ -185,7 +185,7 @@ python -c "import jarviscore; print(jarviscore.__path__[0] + '/docs')"
|
|
|
185
185
|
|
|
186
186
|
## Version
|
|
187
187
|
|
|
188
|
-
**0.
|
|
188
|
+
**0.3.2**
|
|
189
189
|
|
|
190
190
|
## License
|
|
191
191
|
|