jarviscore-framework 0.2.1__py3-none-any.whl → 0.3.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.
Files changed (37) hide show
  1. examples/cloud_deployment_example.py +162 -0
  2. examples/customagent_cognitive_discovery_example.py +343 -0
  3. examples/fastapi_integration_example.py +570 -0
  4. jarviscore/__init__.py +19 -5
  5. jarviscore/cli/smoketest.py +8 -4
  6. jarviscore/core/agent.py +227 -0
  7. jarviscore/core/mesh.py +9 -0
  8. jarviscore/data/examples/cloud_deployment_example.py +162 -0
  9. jarviscore/data/examples/custom_profile_decorator.py +134 -0
  10. jarviscore/data/examples/custom_profile_wrap.py +168 -0
  11. jarviscore/data/examples/customagent_cognitive_discovery_example.py +343 -0
  12. jarviscore/data/examples/fastapi_integration_example.py +570 -0
  13. jarviscore/docs/API_REFERENCE.md +283 -3
  14. jarviscore/docs/CHANGELOG.md +139 -0
  15. jarviscore/docs/CONFIGURATION.md +1 -1
  16. jarviscore/docs/CUSTOMAGENT_GUIDE.md +997 -85
  17. jarviscore/docs/GETTING_STARTED.md +228 -267
  18. jarviscore/docs/TROUBLESHOOTING.md +1 -1
  19. jarviscore/docs/USER_GUIDE.md +153 -8
  20. jarviscore/integrations/__init__.py +16 -0
  21. jarviscore/integrations/fastapi.py +247 -0
  22. jarviscore/p2p/broadcaster.py +10 -3
  23. jarviscore/p2p/coordinator.py +310 -14
  24. jarviscore/p2p/keepalive.py +45 -23
  25. jarviscore/p2p/peer_client.py +311 -12
  26. jarviscore/p2p/swim_manager.py +9 -4
  27. jarviscore/profiles/__init__.py +7 -1
  28. jarviscore/profiles/customagent.py +295 -74
  29. {jarviscore_framework-0.2.1.dist-info → jarviscore_framework-0.3.1.dist-info}/METADATA +66 -18
  30. {jarviscore_framework-0.2.1.dist-info → jarviscore_framework-0.3.1.dist-info}/RECORD +37 -22
  31. {jarviscore_framework-0.2.1.dist-info → jarviscore_framework-0.3.1.dist-info}/WHEEL +1 -1
  32. tests/test_13_dx_improvements.py +554 -0
  33. tests/test_14_cloud_deployment.py +403 -0
  34. tests/test_15_llm_cognitive_discovery.py +684 -0
  35. tests/test_16_unified_dx_flow.py +947 -0
  36. {jarviscore_framework-0.2.1.dist-info → jarviscore_framework-0.3.1.dist-info}/licenses/LICENSE +0 -0
  37. {jarviscore_framework-0.2.1.dist-info → jarviscore_framework-0.3.1.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"])