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.
Files changed (31) hide show
  1. examples/customagent_cognitive_discovery_example.py +49 -8
  2. examples/customagent_distributed_example.py +140 -1
  3. examples/fastapi_integration_example.py +70 -7
  4. jarviscore/__init__.py +1 -1
  5. jarviscore/core/mesh.py +149 -0
  6. jarviscore/data/examples/customagent_cognitive_discovery_example.py +49 -8
  7. jarviscore/data/examples/customagent_distributed_example.py +140 -1
  8. jarviscore/data/examples/fastapi_integration_example.py +70 -7
  9. jarviscore/docs/API_REFERENCE.md +547 -5
  10. jarviscore/docs/CHANGELOG.md +89 -0
  11. jarviscore/docs/CONFIGURATION.md +1 -1
  12. jarviscore/docs/CUSTOMAGENT_GUIDE.md +347 -2
  13. jarviscore/docs/TROUBLESHOOTING.md +1 -1
  14. jarviscore/docs/USER_GUIDE.md +286 -5
  15. jarviscore/p2p/coordinator.py +36 -7
  16. jarviscore/p2p/messages.py +13 -0
  17. jarviscore/p2p/peer_client.py +355 -23
  18. jarviscore/p2p/peer_tool.py +17 -11
  19. jarviscore/profiles/customagent.py +9 -2
  20. jarviscore/testing/__init__.py +35 -0
  21. jarviscore/testing/mocks.py +578 -0
  22. {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/METADATA +2 -2
  23. {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/RECORD +31 -24
  24. tests/test_17_session_context.py +489 -0
  25. tests/test_18_mesh_diagnostics.py +465 -0
  26. tests/test_19_async_requests.py +516 -0
  27. tests/test_20_load_balancing.py +546 -0
  28. tests/test_21_mock_testing.py +776 -0
  29. {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/WHEEL +0 -0
  30. {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/licenses/LICENSE +0 -0
  31. {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,516 @@
1
+ """
2
+ Test 19: Async Request Pattern (Feature F2)
3
+
4
+ Tests the async request methods:
5
+ - ask_async() returns request_id immediately
6
+ - check_inbox() returns None when not ready
7
+ - check_inbox() returns response when available
8
+ - check_inbox() with timeout
9
+ - get_pending_async_requests()
10
+ - clear_inbox()
11
+
12
+ Run with: pytest tests/test_19_async_requests.py -v -s
13
+ """
14
+ import asyncio
15
+ import sys
16
+ import pytest
17
+ import logging
18
+ from unittest.mock import MagicMock, AsyncMock
19
+
20
+ sys.path.insert(0, '.')
21
+
22
+ # Setup logging
23
+ logging.basicConfig(level=logging.INFO)
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ # =============================================================================
28
+ # TEST: ASK_ASYNC BASICS
29
+ # =============================================================================
30
+
31
+ class TestAskAsync:
32
+ """Test ask_async() method."""
33
+
34
+ @pytest.mark.asyncio
35
+ async def test_ask_async_returns_request_id(self):
36
+ """Test ask_async() returns a request ID string."""
37
+ from jarviscore.testing import MockPeerClient
38
+
39
+ client = MockPeerClient(
40
+ agent_id="requester-1",
41
+ agent_role="requester",
42
+ mock_peers=[{"role": "analyst", "capabilities": ["analysis"]}]
43
+ )
44
+
45
+ request_id = await client.ask_async("analyst", {"query": "test"})
46
+
47
+ assert request_id is not None
48
+ assert isinstance(request_id, str)
49
+ assert len(request_id) > 0
50
+
51
+ @pytest.mark.asyncio
52
+ async def test_ask_async_request_id_format(self):
53
+ """Test ask_async() returns properly formatted request ID."""
54
+ from jarviscore.testing import MockPeerClient
55
+
56
+ client = MockPeerClient(
57
+ agent_id="requester-1",
58
+ agent_role="requester",
59
+ mock_peers=[{"role": "analyst", "capabilities": ["analysis"]}]
60
+ )
61
+
62
+ request_id = await client.ask_async("analyst", {"query": "test"})
63
+
64
+ # MockPeerClient uses "mock-" prefix
65
+ assert request_id.startswith("mock-")
66
+
67
+ @pytest.mark.asyncio
68
+ async def test_ask_async_tracks_request(self):
69
+ """Test ask_async() tracks the request in sent_requests."""
70
+ from jarviscore.testing import MockPeerClient
71
+
72
+ client = MockPeerClient(
73
+ agent_id="requester-1",
74
+ agent_role="requester",
75
+ mock_peers=[{"role": "analyst", "capabilities": ["analysis"]}]
76
+ )
77
+
78
+ request_id = await client.ask_async("analyst", {"query": "important"})
79
+
80
+ requests = client.get_sent_requests()
81
+ assert len(requests) == 1
82
+ assert requests[0]["target"] == "analyst"
83
+ assert requests[0]["message"] == {"query": "important"}
84
+ assert requests[0].get("async") is True
85
+
86
+ @pytest.mark.asyncio
87
+ async def test_ask_async_with_context(self):
88
+ """Test ask_async() accepts context parameter."""
89
+ from jarviscore.testing import MockPeerClient
90
+
91
+ client = MockPeerClient(
92
+ agent_id="requester-1",
93
+ agent_role="requester",
94
+ mock_peers=[{"role": "analyst", "capabilities": ["analysis"]}]
95
+ )
96
+
97
+ context = {"mission_id": "m-123", "priority": "high"}
98
+ request_id = await client.ask_async("analyst", {"query": "test"}, context=context)
99
+
100
+ requests = client.get_sent_requests()
101
+ assert requests[0]["context"] == context
102
+
103
+
104
+ # =============================================================================
105
+ # TEST: CHECK_INBOX
106
+ # =============================================================================
107
+
108
+ class TestCheckInbox:
109
+ """Test check_inbox() method."""
110
+
111
+ @pytest.mark.asyncio
112
+ async def test_check_inbox_returns_response(self):
113
+ """Test check_inbox() returns response when auto_respond is True."""
114
+ from jarviscore.testing import MockPeerClient
115
+
116
+ client = MockPeerClient(
117
+ agent_id="requester-1",
118
+ agent_role="requester",
119
+ mock_peers=[{"role": "analyst", "capabilities": ["analysis"]}],
120
+ auto_respond=True
121
+ )
122
+
123
+ request_id = await client.ask_async("analyst", {"query": "test"})
124
+ response = await client.check_inbox(request_id)
125
+
126
+ # MockPeerClient with auto_respond returns default response
127
+ assert response is not None
128
+ assert "mock" in response or "status" in response
129
+
130
+ @pytest.mark.asyncio
131
+ async def test_check_inbox_returns_none_when_no_auto_respond(self):
132
+ """Test check_inbox() returns None when auto_respond is False."""
133
+ from jarviscore.testing import MockPeerClient
134
+
135
+ client = MockPeerClient(
136
+ agent_id="requester-1",
137
+ agent_role="requester",
138
+ mock_peers=[{"role": "analyst", "capabilities": ["analysis"]}],
139
+ auto_respond=False
140
+ )
141
+
142
+ request_id = await client.ask_async("analyst", {"query": "test"})
143
+ response = await client.check_inbox(request_id, timeout=0)
144
+
145
+ assert response is None
146
+
147
+ @pytest.mark.asyncio
148
+ async def test_check_inbox_with_timeout(self):
149
+ """Test check_inbox() with timeout parameter."""
150
+ from jarviscore.testing import MockPeerClient
151
+
152
+ client = MockPeerClient(
153
+ agent_id="requester-1",
154
+ agent_role="requester",
155
+ mock_peers=[{"role": "analyst", "capabilities": ["analysis"]}],
156
+ auto_respond=True
157
+ )
158
+
159
+ request_id = await client.ask_async("analyst", {"query": "test"})
160
+
161
+ # With timeout, should return response immediately (mock)
162
+ response = await client.check_inbox(request_id, timeout=5)
163
+
164
+ assert response is not None
165
+
166
+
167
+ # =============================================================================
168
+ # TEST: REAL PEER CLIENT ASYNC REQUESTS
169
+ # =============================================================================
170
+
171
+ class TestRealPeerClientAsyncRequests:
172
+ """Test async requests with real PeerClient."""
173
+
174
+ @pytest.mark.asyncio
175
+ async def test_ask_async_sends_request(self):
176
+ """Test ask_async() sends request to target."""
177
+ from jarviscore.p2p.peer_client import PeerClient
178
+ from jarviscore.p2p.messages import MessageType
179
+
180
+ mock_coordinator = MagicMock()
181
+ mock_coordinator._remote_agent_registry = {}
182
+
183
+ # Create target agent with PeerClient
184
+ class MockTargetAgent:
185
+ def __init__(self):
186
+ self.agent_id = "analyst-1"
187
+ self.role = "analyst"
188
+ self.capabilities = ["analysis"]
189
+ self.peers = None
190
+
191
+ target = MockTargetAgent()
192
+ target_client = PeerClient(
193
+ coordinator=mock_coordinator,
194
+ agent_id="analyst-1",
195
+ agent_role="analyst",
196
+ agent_registry={},
197
+ node_id="localhost:7946"
198
+ )
199
+ target.peers = target_client
200
+
201
+ # Create requester with target in registry
202
+ agent_registry = {"analyst": [target]}
203
+ requester_client = PeerClient(
204
+ coordinator=mock_coordinator,
205
+ agent_id="requester-1",
206
+ agent_role="requester",
207
+ agent_registry=agent_registry,
208
+ node_id="localhost:7946"
209
+ )
210
+
211
+ # Send async request
212
+ request_id = await requester_client.ask_async("analyst", {"query": "analyze this"})
213
+
214
+ assert request_id is not None
215
+ assert request_id.startswith("async-")
216
+
217
+ # Verify message was delivered to target
218
+ msg = await target_client.receive(timeout=1)
219
+ assert msg is not None
220
+ assert msg.type == MessageType.REQUEST
221
+ assert msg.data == {"query": "analyze this"}
222
+
223
+ @pytest.mark.asyncio
224
+ async def test_ask_async_raises_on_invalid_target(self):
225
+ """Test ask_async() raises ValueError for invalid target."""
226
+ from jarviscore.p2p.peer_client import PeerClient
227
+
228
+ mock_coordinator = MagicMock()
229
+ mock_coordinator._remote_agent_registry = {}
230
+ mock_coordinator.get_remote_agent = MagicMock(return_value=None)
231
+
232
+ client = PeerClient(
233
+ coordinator=mock_coordinator,
234
+ agent_id="requester-1",
235
+ agent_role="requester",
236
+ agent_registry={},
237
+ node_id="localhost:7946"
238
+ )
239
+
240
+ with pytest.raises(ValueError, match="No peer found"):
241
+ await client.ask_async("nonexistent", {"query": "test"})
242
+
243
+
244
+ # =============================================================================
245
+ # TEST: GET_PENDING_ASYNC_REQUESTS
246
+ # =============================================================================
247
+
248
+ class TestGetPendingAsyncRequests:
249
+ """Test get_pending_async_requests() method."""
250
+
251
+ @pytest.mark.asyncio
252
+ async def test_pending_requests_initially_empty(self):
253
+ """Test pending requests list is empty initially."""
254
+ from jarviscore.testing import MockPeerClient
255
+
256
+ client = MockPeerClient()
257
+ pending = client.get_pending_async_requests()
258
+
259
+ assert pending == []
260
+
261
+ @pytest.mark.asyncio
262
+ async def test_real_client_tracks_pending_requests(self):
263
+ """Test real PeerClient tracks pending async requests."""
264
+ from jarviscore.p2p.peer_client import PeerClient
265
+
266
+ mock_coordinator = MagicMock()
267
+ mock_coordinator._remote_agent_registry = {}
268
+
269
+ # Create target agent
270
+ class MockTargetAgent:
271
+ def __init__(self):
272
+ self.agent_id = "target-1"
273
+ self.role = "target"
274
+ self.capabilities = []
275
+ self.peers = None
276
+
277
+ target = MockTargetAgent()
278
+ target_client = PeerClient(
279
+ coordinator=mock_coordinator,
280
+ agent_id="target-1",
281
+ agent_role="target",
282
+ agent_registry={},
283
+ node_id="localhost:7946"
284
+ )
285
+ target.peers = target_client
286
+
287
+ agent_registry = {"target": [target]}
288
+ client = PeerClient(
289
+ coordinator=mock_coordinator,
290
+ agent_id="requester-1",
291
+ agent_role="requester",
292
+ agent_registry=agent_registry,
293
+ node_id="localhost:7946"
294
+ )
295
+
296
+ # Send async request
297
+ request_id = await client.ask_async("target", {"query": "test"})
298
+
299
+ pending = client.get_pending_async_requests()
300
+
301
+ assert len(pending) == 1
302
+ assert pending[0]["request_id"] == request_id
303
+ assert pending[0]["target"] == "target"
304
+
305
+
306
+ # =============================================================================
307
+ # TEST: CLEAR_INBOX
308
+ # =============================================================================
309
+
310
+ class TestClearInbox:
311
+ """Test clear_inbox() method."""
312
+
313
+ @pytest.mark.asyncio
314
+ async def test_clear_inbox_all(self):
315
+ """Test clear_inbox() with no argument clears all."""
316
+ from jarviscore.testing import MockPeerClient
317
+
318
+ client = MockPeerClient(
319
+ mock_peers=[{"role": "target", "capabilities": []}]
320
+ )
321
+
322
+ # Create some async requests
323
+ await client.ask_async("target", {"q": 1})
324
+ await client.ask_async("target", {"q": 2})
325
+
326
+ # Clear all
327
+ client.clear_inbox()
328
+
329
+ # Verify cleared (no error)
330
+ # MockPeerClient.clear_inbox is a no-op but should not raise
331
+
332
+ @pytest.mark.asyncio
333
+ async def test_clear_inbox_specific(self):
334
+ """Test clear_inbox() with specific request_id."""
335
+ from jarviscore.testing import MockPeerClient
336
+
337
+ client = MockPeerClient(
338
+ mock_peers=[{"role": "target", "capabilities": []}]
339
+ )
340
+
341
+ req_id = await client.ask_async("target", {"q": 1})
342
+
343
+ # Clear specific request
344
+ client.clear_inbox(req_id)
345
+
346
+ # Should not raise
347
+
348
+ @pytest.mark.asyncio
349
+ async def test_real_client_clear_inbox(self):
350
+ """Test real PeerClient clear_inbox removes entries."""
351
+ from jarviscore.p2p.peer_client import PeerClient
352
+
353
+ mock_coordinator = MagicMock()
354
+ mock_coordinator._remote_agent_registry = {}
355
+
356
+ # Create target
357
+ class MockTargetAgent:
358
+ def __init__(self):
359
+ self.agent_id = "target-1"
360
+ self.role = "target"
361
+ self.capabilities = []
362
+ self.peers = None
363
+
364
+ target = MockTargetAgent()
365
+ target_client = PeerClient(
366
+ coordinator=mock_coordinator,
367
+ agent_id="target-1",
368
+ agent_role="target",
369
+ agent_registry={},
370
+ node_id="localhost:7946"
371
+ )
372
+ target.peers = target_client
373
+
374
+ agent_registry = {"target": [target]}
375
+ client = PeerClient(
376
+ coordinator=mock_coordinator,
377
+ agent_id="requester-1",
378
+ agent_role="requester",
379
+ agent_registry=agent_registry,
380
+ node_id="localhost:7946"
381
+ )
382
+
383
+ # Send async requests
384
+ req1 = await client.ask_async("target", {"q": 1})
385
+ req2 = await client.ask_async("target", {"q": 2})
386
+
387
+ assert len(client.get_pending_async_requests()) == 2
388
+
389
+ # Clear specific
390
+ client.clear_inbox(req1)
391
+ assert len(client.get_pending_async_requests()) == 1
392
+
393
+ # Clear all
394
+ client.clear_inbox()
395
+ assert len(client.get_pending_async_requests()) == 0
396
+
397
+
398
+ # =============================================================================
399
+ # TEST: ASYNC REQUEST WITH RESPONSE
400
+ # =============================================================================
401
+
402
+ class TestAsyncRequestResponse:
403
+ """Test complete async request-response flow."""
404
+
405
+ @pytest.mark.asyncio
406
+ async def test_async_request_receives_response(self):
407
+ """Test async request receives response via check_inbox."""
408
+ from jarviscore.p2p.peer_client import PeerClient
409
+ from jarviscore.p2p.messages import MessageType
410
+
411
+ mock_coordinator = MagicMock()
412
+ mock_coordinator._remote_agent_registry = {}
413
+
414
+ # Create responder
415
+ class MockResponder:
416
+ def __init__(self):
417
+ self.agent_id = "responder-1"
418
+ self.role = "responder"
419
+ self.capabilities = []
420
+ self.peers = None
421
+
422
+ responder = MockResponder()
423
+ responder_client = PeerClient(
424
+ coordinator=mock_coordinator,
425
+ agent_id="responder-1",
426
+ agent_role="responder",
427
+ agent_registry={},
428
+ node_id="localhost:7946"
429
+ )
430
+ responder.peers = responder_client
431
+
432
+ # Create requester
433
+ class MockRequester:
434
+ def __init__(self):
435
+ self.agent_id = "requester-1"
436
+ self.role = "requester"
437
+ self.capabilities = []
438
+ self.peers = None
439
+
440
+ requester = MockRequester()
441
+ requester_client = PeerClient(
442
+ coordinator=mock_coordinator,
443
+ agent_id="requester-1",
444
+ agent_role="requester",
445
+ agent_registry={"responder": [responder], "requester": [requester]},
446
+ node_id="localhost:7946"
447
+ )
448
+ requester.peers = requester_client
449
+
450
+ # Also add requester to responder's registry for response routing
451
+ responder_client._agent_registry = {"requester": [requester], "responder": [responder]}
452
+
453
+ # Send async request
454
+ request_id = await requester_client.ask_async("responder", {"query": "what is 2+2?"})
455
+
456
+ # Responder receives and responds
457
+ msg = await responder_client.receive(timeout=1)
458
+ assert msg is not None
459
+ assert msg.is_request
460
+
461
+ await responder_client.respond(msg, {"answer": "4"})
462
+
463
+ # Give time for async handler to process response
464
+ await asyncio.sleep(0.2)
465
+
466
+ # Requester checks inbox
467
+ response = await requester_client.check_inbox(request_id, timeout=1)
468
+
469
+ assert response is not None
470
+ assert response == {"answer": "4"}
471
+
472
+
473
+ # =============================================================================
474
+ # TEST: MULTIPLE ASYNC REQUESTS
475
+ # =============================================================================
476
+
477
+ class TestMultipleAsyncRequests:
478
+ """Test handling multiple concurrent async requests."""
479
+
480
+ @pytest.mark.asyncio
481
+ async def test_multiple_async_requests_tracked(self):
482
+ """Test multiple async requests are tracked independently."""
483
+ from jarviscore.testing import MockPeerClient
484
+
485
+ client = MockPeerClient(
486
+ agent_id="requester-1",
487
+ agent_role="requester",
488
+ mock_peers=[
489
+ {"role": "analyst1", "capabilities": ["analysis"]},
490
+ {"role": "analyst2", "capabilities": ["analysis"]},
491
+ {"role": "analyst3", "capabilities": ["analysis"]}
492
+ ]
493
+ )
494
+
495
+ # Send multiple async requests
496
+ req1 = await client.ask_async("analyst1", {"query": "q1"})
497
+ req2 = await client.ask_async("analyst2", {"query": "q2"})
498
+ req3 = await client.ask_async("analyst3", {"query": "q3"})
499
+
500
+ # All should have unique IDs
501
+ assert req1 != req2 != req3
502
+
503
+ requests = client.get_sent_requests()
504
+ assert len(requests) == 3
505
+
506
+ # Targets should be correct
507
+ targets = {r["target"] for r in requests}
508
+ assert targets == {"analyst1", "analyst2", "analyst3"}
509
+
510
+
511
+ # =============================================================================
512
+ # RUN TESTS
513
+ # =============================================================================
514
+
515
+ if __name__ == "__main__":
516
+ pytest.main([__file__, "-v", "-s"])