jarviscore-framework 0.2.0__py3-none-any.whl → 0.3.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/cloud_deployment_example.py +162 -0
- examples/customagent_p2p_example.py +566 -183
- examples/fastapi_integration_example.py +570 -0
- examples/listeneragent_cognitive_discovery_example.py +343 -0
- jarviscore/__init__.py +22 -5
- jarviscore/cli/smoketest.py +8 -4
- jarviscore/core/agent.py +227 -0
- jarviscore/data/examples/cloud_deployment_example.py +162 -0
- jarviscore/data/examples/customagent_p2p_example.py +566 -183
- jarviscore/data/examples/fastapi_integration_example.py +570 -0
- jarviscore/data/examples/listeneragent_cognitive_discovery_example.py +343 -0
- jarviscore/docs/API_REFERENCE.md +296 -3
- jarviscore/docs/CHANGELOG.md +97 -0
- jarviscore/docs/CONFIGURATION.md +2 -2
- jarviscore/docs/CUSTOMAGENT_GUIDE.md +2021 -255
- jarviscore/docs/GETTING_STARTED.md +112 -8
- jarviscore/docs/TROUBLESHOOTING.md +3 -3
- jarviscore/docs/USER_GUIDE.md +152 -6
- jarviscore/integrations/__init__.py +16 -0
- jarviscore/integrations/fastapi.py +247 -0
- jarviscore/p2p/broadcaster.py +10 -3
- jarviscore/p2p/coordinator.py +310 -14
- jarviscore/p2p/keepalive.py +45 -23
- jarviscore/p2p/peer_client.py +282 -10
- jarviscore/p2p/swim_manager.py +9 -4
- jarviscore/profiles/__init__.py +10 -2
- jarviscore/profiles/listeneragent.py +292 -0
- {jarviscore_framework-0.2.0.dist-info → jarviscore_framework-0.3.0.dist-info}/METADATA +42 -8
- {jarviscore_framework-0.2.0.dist-info → jarviscore_framework-0.3.0.dist-info}/RECORD +36 -22
- {jarviscore_framework-0.2.0.dist-info → jarviscore_framework-0.3.0.dist-info}/WHEEL +1 -1
- tests/test_13_dx_improvements.py +554 -0
- tests/test_14_cloud_deployment.py +403 -0
- tests/test_15_llm_cognitive_discovery.py +684 -0
- tests/test_16_unified_dx_flow.py +947 -0
- {jarviscore_framework-0.2.0.dist-info → jarviscore_framework-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {jarviscore_framework-0.2.0.dist-info → jarviscore_framework-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test 14: Cloud Deployment - Agent Self-Registration
|
|
3
|
+
|
|
4
|
+
Tests the cloud deployment patterns:
|
|
5
|
+
- agent.join_mesh() for self-registration
|
|
6
|
+
- agent.leave_mesh() for graceful departure
|
|
7
|
+
- Remote agent visibility across nodes
|
|
8
|
+
- Capability deannouncement
|
|
9
|
+
|
|
10
|
+
Run with: pytest tests/test_14_cloud_deployment.py -v -s
|
|
11
|
+
"""
|
|
12
|
+
import asyncio
|
|
13
|
+
import sys
|
|
14
|
+
import os
|
|
15
|
+
import pytest
|
|
16
|
+
import logging
|
|
17
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
18
|
+
|
|
19
|
+
sys.path.insert(0, '.')
|
|
20
|
+
|
|
21
|
+
# Setup logging
|
|
22
|
+
logging.basicConfig(level=logging.INFO)
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
27
|
+
# TEST: REMOTE AGENT PROXY
|
|
28
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
29
|
+
|
|
30
|
+
class TestRemoteAgentProxy:
|
|
31
|
+
"""Test RemoteAgentProxy class."""
|
|
32
|
+
|
|
33
|
+
def test_remote_agent_proxy_creation(self):
|
|
34
|
+
"""Test RemoteAgentProxy can be created with all attributes."""
|
|
35
|
+
from jarviscore.p2p.peer_client import RemoteAgentProxy
|
|
36
|
+
|
|
37
|
+
proxy = RemoteAgentProxy(
|
|
38
|
+
agent_id="analyst-abc123",
|
|
39
|
+
role="analyst",
|
|
40
|
+
node_id="192.168.1.10:7946",
|
|
41
|
+
capabilities=["analysis", "charting"]
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
assert proxy.agent_id == "analyst-abc123"
|
|
45
|
+
assert proxy.role == "analyst"
|
|
46
|
+
assert proxy.node_id == "192.168.1.10:7946"
|
|
47
|
+
assert proxy.capabilities == ["analysis", "charting"]
|
|
48
|
+
assert proxy.peers is None # Remote agents don't have local PeerClient
|
|
49
|
+
|
|
50
|
+
def test_remote_agent_proxy_repr(self):
|
|
51
|
+
"""Test RemoteAgentProxy string representation."""
|
|
52
|
+
from jarviscore.p2p.peer_client import RemoteAgentProxy
|
|
53
|
+
|
|
54
|
+
proxy = RemoteAgentProxy(
|
|
55
|
+
agent_id="scout-123",
|
|
56
|
+
role="scout",
|
|
57
|
+
node_id="10.0.0.5:7946",
|
|
58
|
+
capabilities=["research"]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
repr_str = repr(proxy)
|
|
62
|
+
assert "RemoteAgentProxy" in repr_str
|
|
63
|
+
assert "scout" in repr_str
|
|
64
|
+
assert "10.0.0.5:7946" in repr_str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
68
|
+
# TEST: PEER CLIENT REMOTE VISIBILITY
|
|
69
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
70
|
+
|
|
71
|
+
class TestPeerClientRemoteVisibility:
|
|
72
|
+
"""Test PeerClient can see remote agents."""
|
|
73
|
+
|
|
74
|
+
def test_resolve_target_finds_remote_agent(self):
|
|
75
|
+
"""Test _resolve_target returns RemoteAgentProxy for remote agents."""
|
|
76
|
+
from jarviscore.p2p.peer_client import PeerClient, RemoteAgentProxy
|
|
77
|
+
|
|
78
|
+
# Mock coordinator with remote agent
|
|
79
|
+
mock_coordinator = MagicMock()
|
|
80
|
+
mock_coordinator.get_remote_agent.return_value = {
|
|
81
|
+
'agent_id': 'remote-analyst-123',
|
|
82
|
+
'role': 'analyst',
|
|
83
|
+
'node_id': '192.168.1.20:7946',
|
|
84
|
+
'capabilities': ['analysis']
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
client = PeerClient(
|
|
88
|
+
coordinator=mock_coordinator,
|
|
89
|
+
agent_id="local-agent",
|
|
90
|
+
agent_role="processor",
|
|
91
|
+
agent_registry={}, # Empty local registry
|
|
92
|
+
node_id="localhost:7946"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Resolve target should find remote agent
|
|
96
|
+
result = client._resolve_target("analyst")
|
|
97
|
+
|
|
98
|
+
assert result is not None
|
|
99
|
+
assert isinstance(result, RemoteAgentProxy)
|
|
100
|
+
assert result.role == "analyst"
|
|
101
|
+
assert result.node_id == "192.168.1.20:7946"
|
|
102
|
+
|
|
103
|
+
def test_resolve_target_prefers_local_agent(self):
|
|
104
|
+
"""Test _resolve_target returns local agent when available."""
|
|
105
|
+
from jarviscore.p2p.peer_client import PeerClient, RemoteAgentProxy
|
|
106
|
+
|
|
107
|
+
# Create mock local agent
|
|
108
|
+
mock_local_agent = MagicMock()
|
|
109
|
+
mock_local_agent.agent_id = "local-analyst"
|
|
110
|
+
mock_local_agent.role = "analyst"
|
|
111
|
+
|
|
112
|
+
# Mock coordinator with remote agent
|
|
113
|
+
mock_coordinator = MagicMock()
|
|
114
|
+
mock_coordinator.get_remote_agent.return_value = {
|
|
115
|
+
'agent_id': 'remote-analyst',
|
|
116
|
+
'role': 'analyst',
|
|
117
|
+
'node_id': '192.168.1.20:7946'
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
client = PeerClient(
|
|
121
|
+
coordinator=mock_coordinator,
|
|
122
|
+
agent_id="local-agent",
|
|
123
|
+
agent_role="processor",
|
|
124
|
+
agent_registry={"analyst": [mock_local_agent]},
|
|
125
|
+
node_id="localhost:7946"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Should return local agent, not remote
|
|
129
|
+
result = client._resolve_target("analyst")
|
|
130
|
+
|
|
131
|
+
assert result is mock_local_agent
|
|
132
|
+
assert not isinstance(result, RemoteAgentProxy)
|
|
133
|
+
|
|
134
|
+
def test_list_peers_includes_remote_agents(self):
|
|
135
|
+
"""Test list_peers includes both local and remote agents."""
|
|
136
|
+
from jarviscore.p2p.peer_client import PeerClient
|
|
137
|
+
|
|
138
|
+
# Create mock local agent
|
|
139
|
+
mock_local_agent = MagicMock()
|
|
140
|
+
mock_local_agent.agent_id = "local-scout"
|
|
141
|
+
mock_local_agent.role = "scout"
|
|
142
|
+
mock_local_agent.capabilities = ["research"]
|
|
143
|
+
|
|
144
|
+
# Mock coordinator with remote agents
|
|
145
|
+
mock_coordinator = MagicMock()
|
|
146
|
+
mock_coordinator.list_remote_agents.return_value = [
|
|
147
|
+
{
|
|
148
|
+
'agent_id': 'remote-analyst',
|
|
149
|
+
'role': 'analyst',
|
|
150
|
+
'capabilities': ['analysis'],
|
|
151
|
+
'node_id': '192.168.1.20:7946'
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
client = PeerClient(
|
|
156
|
+
coordinator=mock_coordinator,
|
|
157
|
+
agent_id="my-agent",
|
|
158
|
+
agent_role="processor",
|
|
159
|
+
agent_registry={"scout": [mock_local_agent]},
|
|
160
|
+
node_id="localhost:7946"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
peers = client.list_peers()
|
|
164
|
+
|
|
165
|
+
# Should have both local and remote
|
|
166
|
+
assert len(peers) == 2
|
|
167
|
+
|
|
168
|
+
roles = [p['role'] for p in peers]
|
|
169
|
+
assert 'scout' in roles
|
|
170
|
+
assert 'analyst' in roles
|
|
171
|
+
|
|
172
|
+
# Check location markers
|
|
173
|
+
local_peer = next(p for p in peers if p['role'] == 'scout')
|
|
174
|
+
remote_peer = next(p for p in peers if p['role'] == 'analyst')
|
|
175
|
+
|
|
176
|
+
assert local_peer['location'] == 'local'
|
|
177
|
+
assert remote_peer['location'] == 'remote'
|
|
178
|
+
assert remote_peer['node_id'] == '192.168.1.20:7946'
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
182
|
+
# TEST: COORDINATOR REMOTE REGISTRY
|
|
183
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
184
|
+
|
|
185
|
+
class TestCoordinatorRemoteRegistry:
|
|
186
|
+
"""Test P2PCoordinator remote agent registry."""
|
|
187
|
+
|
|
188
|
+
def test_get_remote_agent_by_role(self):
|
|
189
|
+
"""Test get_remote_agent finds agent by role."""
|
|
190
|
+
from jarviscore.p2p.coordinator import P2PCoordinator
|
|
191
|
+
|
|
192
|
+
coordinator = P2PCoordinator([], {})
|
|
193
|
+
|
|
194
|
+
# Manually populate remote registry
|
|
195
|
+
coordinator._remote_agent_registry = {
|
|
196
|
+
'analyst-123': {
|
|
197
|
+
'role': 'analyst',
|
|
198
|
+
'capabilities': ['analysis'],
|
|
199
|
+
'node_id': '192.168.1.10:7946'
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
result = coordinator.get_remote_agent('analyst')
|
|
204
|
+
|
|
205
|
+
assert result is not None
|
|
206
|
+
assert result['role'] == 'analyst'
|
|
207
|
+
assert result['node_id'] == '192.168.1.10:7946'
|
|
208
|
+
|
|
209
|
+
def test_get_remote_agent_by_id(self):
|
|
210
|
+
"""Test get_remote_agent finds agent by agent_id."""
|
|
211
|
+
from jarviscore.p2p.coordinator import P2PCoordinator
|
|
212
|
+
|
|
213
|
+
coordinator = P2PCoordinator([], {})
|
|
214
|
+
|
|
215
|
+
coordinator._remote_agent_registry = {
|
|
216
|
+
'analyst-abc123': {
|
|
217
|
+
'role': 'analyst',
|
|
218
|
+
'capabilities': ['analysis'],
|
|
219
|
+
'node_id': '192.168.1.10:7946'
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
result = coordinator.get_remote_agent('analyst-abc123')
|
|
224
|
+
|
|
225
|
+
assert result is not None
|
|
226
|
+
assert result['role'] == 'analyst'
|
|
227
|
+
|
|
228
|
+
def test_list_remote_agents(self):
|
|
229
|
+
"""Test list_remote_agents returns all remote agents."""
|
|
230
|
+
from jarviscore.p2p.coordinator import P2PCoordinator
|
|
231
|
+
|
|
232
|
+
coordinator = P2PCoordinator([], {})
|
|
233
|
+
|
|
234
|
+
coordinator._remote_agent_registry = {
|
|
235
|
+
'analyst-1': {'role': 'analyst', 'node_id': 'node1'},
|
|
236
|
+
'scout-1': {'role': 'scout', 'node_id': 'node2'},
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
agents = coordinator.list_remote_agents()
|
|
240
|
+
|
|
241
|
+
assert len(agents) == 2
|
|
242
|
+
assert any(a['role'] == 'analyst' for a in agents)
|
|
243
|
+
assert any(a['role'] == 'scout' for a in agents)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
247
|
+
# TEST: AGENT JOIN/LEAVE MESH
|
|
248
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
249
|
+
|
|
250
|
+
class TestAgentJoinLeaveMesh:
|
|
251
|
+
"""Test agent.join_mesh() and agent.leave_mesh()."""
|
|
252
|
+
|
|
253
|
+
def test_join_mesh_requires_endpoint(self):
|
|
254
|
+
"""Test join_mesh raises error if no endpoint provided."""
|
|
255
|
+
from jarviscore.profiles import CustomAgent
|
|
256
|
+
|
|
257
|
+
class TestAgent(CustomAgent):
|
|
258
|
+
role = "test"
|
|
259
|
+
capabilities = ["testing"]
|
|
260
|
+
async def execute_task(self, task):
|
|
261
|
+
return {"status": "success"}
|
|
262
|
+
|
|
263
|
+
agent = TestAgent()
|
|
264
|
+
|
|
265
|
+
# Clear any env vars
|
|
266
|
+
os.environ.pop("JARVISCORE_MESH_ENDPOINT", None)
|
|
267
|
+
os.environ.pop("JARVISCORE_SEED_NODES", None)
|
|
268
|
+
|
|
269
|
+
with pytest.raises(ValueError) as exc_info:
|
|
270
|
+
asyncio.get_event_loop().run_until_complete(agent.join_mesh())
|
|
271
|
+
|
|
272
|
+
assert "JARVISCORE_MESH_ENDPOINT" in str(exc_info.value)
|
|
273
|
+
|
|
274
|
+
def test_is_mesh_connected_property(self):
|
|
275
|
+
"""Test is_mesh_connected property."""
|
|
276
|
+
from jarviscore.profiles import CustomAgent
|
|
277
|
+
|
|
278
|
+
class TestAgent(CustomAgent):
|
|
279
|
+
role = "test"
|
|
280
|
+
capabilities = ["testing"]
|
|
281
|
+
async def execute_task(self, task):
|
|
282
|
+
return {"status": "success"}
|
|
283
|
+
|
|
284
|
+
agent = TestAgent()
|
|
285
|
+
|
|
286
|
+
# Initially not connected
|
|
287
|
+
assert agent.is_mesh_connected is False
|
|
288
|
+
|
|
289
|
+
# After setting flag
|
|
290
|
+
agent._mesh_connected = True
|
|
291
|
+
assert agent.is_mesh_connected is True
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
295
|
+
# TEST: CAPABILITY DEANNOUNCEMENT
|
|
296
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
297
|
+
|
|
298
|
+
class TestCapabilityDeannouncement:
|
|
299
|
+
"""Test capability deannouncement handler."""
|
|
300
|
+
|
|
301
|
+
@pytest.mark.asyncio
|
|
302
|
+
async def test_handle_capability_deannouncement(self):
|
|
303
|
+
"""Test _handle_capability_deannouncement removes agents."""
|
|
304
|
+
from jarviscore.p2p.coordinator import P2PCoordinator
|
|
305
|
+
|
|
306
|
+
coordinator = P2PCoordinator([], {})
|
|
307
|
+
|
|
308
|
+
# Setup initial state
|
|
309
|
+
coordinator._capability_map = {
|
|
310
|
+
'analysis': ['analyst-1', 'analyst-2'],
|
|
311
|
+
'research': ['scout-1']
|
|
312
|
+
}
|
|
313
|
+
coordinator._remote_agent_registry = {
|
|
314
|
+
'analyst-1': {'role': 'analyst', 'node_id': 'node1'},
|
|
315
|
+
'analyst-2': {'role': 'analyst', 'node_id': 'node2'},
|
|
316
|
+
'scout-1': {'role': 'scout', 'node_id': 'node1'}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
# Simulate deannouncement from node1 (analyst-1 and scout-1 leaving)
|
|
320
|
+
message = {
|
|
321
|
+
'payload': {
|
|
322
|
+
'node_id': 'node1',
|
|
323
|
+
'agent_ids': ['analyst-1', 'scout-1']
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
await coordinator._handle_capability_deannouncement('node1', message)
|
|
328
|
+
|
|
329
|
+
# analyst-1 should be removed from capability map
|
|
330
|
+
assert 'analyst-1' not in coordinator._capability_map['analysis']
|
|
331
|
+
assert 'analyst-2' in coordinator._capability_map['analysis']
|
|
332
|
+
|
|
333
|
+
# research capability should be removed (empty)
|
|
334
|
+
assert 'research' not in coordinator._capability_map
|
|
335
|
+
|
|
336
|
+
# Remote registry should be updated
|
|
337
|
+
assert 'analyst-1' not in coordinator._remote_agent_registry
|
|
338
|
+
assert 'scout-1' not in coordinator._remote_agent_registry
|
|
339
|
+
assert 'analyst-2' in coordinator._remote_agent_registry
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
343
|
+
# TEST: INTEGRATION - FULL MESH JOIN/LEAVE CYCLE
|
|
344
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
345
|
+
|
|
346
|
+
class TestMeshJoinLeaveCycle:
|
|
347
|
+
"""Integration test for full mesh join/leave cycle."""
|
|
348
|
+
|
|
349
|
+
@pytest.mark.asyncio
|
|
350
|
+
async def test_join_mesh_initializes_peers(self):
|
|
351
|
+
"""Test join_mesh sets up peers attribute."""
|
|
352
|
+
from jarviscore.profiles import CustomAgent
|
|
353
|
+
from unittest.mock import patch, AsyncMock
|
|
354
|
+
|
|
355
|
+
class TestAgent(CustomAgent):
|
|
356
|
+
role = "standalone_test"
|
|
357
|
+
capabilities = ["testing"]
|
|
358
|
+
async def execute_task(self, task):
|
|
359
|
+
return {"status": "success"}
|
|
360
|
+
|
|
361
|
+
agent = TestAgent()
|
|
362
|
+
|
|
363
|
+
# Mock the P2P coordinator - patch where it's imported
|
|
364
|
+
with patch('jarviscore.p2p.coordinator.P2PCoordinator') as MockCoordinator:
|
|
365
|
+
mock_coord_instance = MagicMock()
|
|
366
|
+
mock_coord_instance.start = AsyncMock()
|
|
367
|
+
mock_coord_instance.announce_capabilities = AsyncMock()
|
|
368
|
+
mock_coord_instance.register_peer_client = MagicMock()
|
|
369
|
+
mock_coord_instance.swim_manager = MagicMock()
|
|
370
|
+
mock_coord_instance.swim_manager.bind_addr = ('127.0.0.1', 7999)
|
|
371
|
+
MockCoordinator.return_value = mock_coord_instance
|
|
372
|
+
|
|
373
|
+
# Also need to patch the import in agent.py
|
|
374
|
+
with patch.dict('sys.modules', {'jarviscore.p2p.coordinator': MagicMock(P2PCoordinator=MockCoordinator)}):
|
|
375
|
+
# Manually set up to bypass actual P2P initialization
|
|
376
|
+
agent._standalone_p2p = mock_coord_instance
|
|
377
|
+
agent._mesh_connected = True
|
|
378
|
+
|
|
379
|
+
# Create mock peers
|
|
380
|
+
from jarviscore.p2p.peer_client import PeerClient
|
|
381
|
+
agent.peers = PeerClient(
|
|
382
|
+
coordinator=mock_coord_instance,
|
|
383
|
+
agent_id=agent.agent_id,
|
|
384
|
+
agent_role=agent.role,
|
|
385
|
+
agent_registry={},
|
|
386
|
+
node_id="localhost:7999"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Verify state
|
|
390
|
+
assert agent._mesh_connected is True
|
|
391
|
+
assert agent.peers is not None
|
|
392
|
+
assert agent._standalone_p2p is mock_coord_instance
|
|
393
|
+
|
|
394
|
+
# Cleanup
|
|
395
|
+
agent._mesh_connected = False
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
399
|
+
# RUN TESTS
|
|
400
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
401
|
+
|
|
402
|
+
if __name__ == "__main__":
|
|
403
|
+
pytest.main([__file__, "-v", "-s"])
|