jarviscore-framework 0.3.0__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 (43) hide show
  1. examples/cloud_deployment_example.py +3 -3
  2. examples/{listeneragent_cognitive_discovery_example.py → customagent_cognitive_discovery_example.py} +55 -14
  3. examples/customagent_distributed_example.py +140 -1
  4. examples/fastapi_integration_example.py +74 -11
  5. jarviscore/__init__.py +8 -11
  6. jarviscore/cli/smoketest.py +1 -1
  7. jarviscore/core/mesh.py +158 -0
  8. jarviscore/data/examples/cloud_deployment_example.py +3 -3
  9. jarviscore/data/examples/custom_profile_decorator.py +134 -0
  10. jarviscore/data/examples/custom_profile_wrap.py +168 -0
  11. jarviscore/data/examples/{listeneragent_cognitive_discovery_example.py → customagent_cognitive_discovery_example.py} +55 -14
  12. jarviscore/data/examples/customagent_distributed_example.py +140 -1
  13. jarviscore/data/examples/fastapi_integration_example.py +74 -11
  14. jarviscore/docs/API_REFERENCE.md +576 -47
  15. jarviscore/docs/CHANGELOG.md +131 -0
  16. jarviscore/docs/CONFIGURATION.md +1 -1
  17. jarviscore/docs/CUSTOMAGENT_GUIDE.md +591 -153
  18. jarviscore/docs/GETTING_STARTED.md +186 -329
  19. jarviscore/docs/TROUBLESHOOTING.md +1 -1
  20. jarviscore/docs/USER_GUIDE.md +292 -12
  21. jarviscore/integrations/fastapi.py +4 -4
  22. jarviscore/p2p/coordinator.py +36 -7
  23. jarviscore/p2p/messages.py +13 -0
  24. jarviscore/p2p/peer_client.py +380 -21
  25. jarviscore/p2p/peer_tool.py +17 -11
  26. jarviscore/profiles/__init__.py +2 -4
  27. jarviscore/profiles/customagent.py +302 -74
  28. jarviscore/testing/__init__.py +35 -0
  29. jarviscore/testing/mocks.py +578 -0
  30. {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/METADATA +61 -46
  31. {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/RECORD +42 -34
  32. tests/test_13_dx_improvements.py +37 -37
  33. tests/test_15_llm_cognitive_discovery.py +18 -18
  34. tests/test_16_unified_dx_flow.py +3 -3
  35. tests/test_17_session_context.py +489 -0
  36. tests/test_18_mesh_diagnostics.py +465 -0
  37. tests/test_19_async_requests.py +516 -0
  38. tests/test_20_load_balancing.py +546 -0
  39. tests/test_21_mock_testing.py +776 -0
  40. jarviscore/profiles/listeneragent.py +0 -292
  41. {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/WHEEL +0 -0
  42. {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/licenses/LICENSE +0 -0
  43. {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,546 @@
1
+ """
2
+ Test 20: Load Balancing Strategies (Feature F7)
3
+
4
+ Tests the discovery load balancing strategies:
5
+ - strategy="first" (default behavior)
6
+ - strategy="random" (shuffled order)
7
+ - strategy="round_robin" (rotates each call)
8
+ - strategy="least_recent" (oldest used first)
9
+ - discover_one() convenience method
10
+ - record_peer_usage()
11
+
12
+ Run with: pytest tests/test_20_load_balancing.py -v -s
13
+ """
14
+ import asyncio
15
+ import sys
16
+ import time
17
+ import pytest
18
+ import logging
19
+ from unittest.mock import MagicMock, patch
20
+ from collections import Counter
21
+
22
+ sys.path.insert(0, '.')
23
+
24
+ # Setup logging
25
+ logging.basicConfig(level=logging.INFO)
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ # =============================================================================
30
+ # TEST: STRATEGY FIRST (DEFAULT)
31
+ # =============================================================================
32
+
33
+ class TestStrategyFirst:
34
+ """Test strategy='first' (default behavior)."""
35
+
36
+ def test_discover_first_returns_consistent_order(self):
37
+ """Test 'first' strategy returns peers in consistent order."""
38
+ from jarviscore.testing import MockPeerClient
39
+
40
+ client = MockPeerClient(
41
+ agent_id="test-agent",
42
+ agent_role="test",
43
+ mock_peers=[
44
+ {"role": "worker", "agent_id": "worker-a", "capabilities": ["work"]},
45
+ {"role": "worker", "agent_id": "worker-b", "capabilities": ["work"]},
46
+ {"role": "worker", "agent_id": "worker-c", "capabilities": ["work"]}
47
+ ]
48
+ )
49
+
50
+ # Multiple calls should return same order
51
+ result1 = client.discover(role="worker", strategy="first")
52
+ result2 = client.discover(role="worker", strategy="first")
53
+ result3 = client.discover(role="worker", strategy="first")
54
+
55
+ ids1 = [p.agent_id for p in result1]
56
+ ids2 = [p.agent_id for p in result2]
57
+ ids3 = [p.agent_id for p in result3]
58
+
59
+ assert ids1 == ids2 == ids3
60
+
61
+ def test_discover_default_strategy_is_first(self):
62
+ """Test default strategy is 'first'."""
63
+ from jarviscore.testing import MockPeerClient
64
+
65
+ client = MockPeerClient(
66
+ agent_id="test-agent",
67
+ agent_role="test",
68
+ mock_peers=[
69
+ {"role": "worker", "agent_id": "worker-1", "capabilities": ["work"]},
70
+ {"role": "worker", "agent_id": "worker-2", "capabilities": ["work"]}
71
+ ]
72
+ )
73
+
74
+ # No strategy specified
75
+ result_default = client.discover(role="worker")
76
+ # Explicit first
77
+ result_first = client.discover(role="worker", strategy="first")
78
+
79
+ ids_default = [p.agent_id for p in result_default]
80
+ ids_first = [p.agent_id for p in result_first]
81
+
82
+ assert ids_default == ids_first
83
+
84
+
85
+ # =============================================================================
86
+ # TEST: STRATEGY RANDOM
87
+ # =============================================================================
88
+
89
+ class TestStrategyRandom:
90
+ """Test strategy='random' (shuffled order)."""
91
+
92
+ def test_discover_random_returns_all_peers(self):
93
+ """Test 'random' strategy returns all peers."""
94
+ from jarviscore.testing import MockPeerClient
95
+
96
+ client = MockPeerClient(
97
+ agent_id="test-agent",
98
+ agent_role="test",
99
+ mock_peers=[
100
+ {"role": "worker", "agent_id": "worker-1", "capabilities": ["work"]},
101
+ {"role": "worker", "agent_id": "worker-2", "capabilities": ["work"]},
102
+ {"role": "worker", "agent_id": "worker-3", "capabilities": ["work"]}
103
+ ]
104
+ )
105
+
106
+ result = client.discover(role="worker", strategy="random")
107
+
108
+ assert len(result) == 3
109
+ ids = {p.agent_id for p in result}
110
+ assert ids == {"worker-1", "worker-2", "worker-3"}
111
+
112
+ def test_discover_random_varies_order(self):
113
+ """Test 'random' strategy produces different orders over many calls."""
114
+ from jarviscore.testing import MockPeerClient
115
+
116
+ client = MockPeerClient(
117
+ agent_id="test-agent",
118
+ agent_role="test",
119
+ mock_peers=[
120
+ {"role": "worker", "agent_id": "w-1", "capabilities": ["work"]},
121
+ {"role": "worker", "agent_id": "w-2", "capabilities": ["work"]},
122
+ {"role": "worker", "agent_id": "w-3", "capabilities": ["work"]},
123
+ {"role": "worker", "agent_id": "w-4", "capabilities": ["work"]}
124
+ ]
125
+ )
126
+
127
+ # Collect first elements over many iterations
128
+ first_peers = []
129
+ for _ in range(50):
130
+ result = client.discover(role="worker", strategy="random")
131
+ first_peers.append(result[0].agent_id)
132
+
133
+ # Should have variation in first position
134
+ unique_first = set(first_peers)
135
+ # With 4 workers and 50 iterations, should see at least 2 different first peers
136
+ assert len(unique_first) >= 2, "Random strategy should vary the order"
137
+
138
+
139
+ # =============================================================================
140
+ # TEST: STRATEGY ROUND ROBIN
141
+ # =============================================================================
142
+
143
+ class TestStrategyRoundRobin:
144
+ """Test strategy='round_robin' (rotates each call)."""
145
+
146
+ def test_discover_round_robin_rotates(self):
147
+ """Test 'round_robin' strategy rotates through peers."""
148
+ from jarviscore.testing import MockPeerClient
149
+
150
+ client = MockPeerClient(
151
+ agent_id="test-agent",
152
+ agent_role="test",
153
+ mock_peers=[
154
+ {"role": "worker", "agent_id": "w-0", "capabilities": ["work"]},
155
+ {"role": "worker", "agent_id": "w-1", "capabilities": ["work"]},
156
+ {"role": "worker", "agent_id": "w-2", "capabilities": ["work"]}
157
+ ]
158
+ )
159
+
160
+ # First call
161
+ result1 = client.discover(role="worker", strategy="round_robin")
162
+ first1 = result1[0].agent_id
163
+
164
+ # Second call - should rotate
165
+ result2 = client.discover(role="worker", strategy="round_robin")
166
+ first2 = result2[0].agent_id
167
+
168
+ # Third call - should rotate again
169
+ result3 = client.discover(role="worker", strategy="round_robin")
170
+ first3 = result3[0].agent_id
171
+
172
+ # Fourth call - should wrap around
173
+ result4 = client.discover(role="worker", strategy="round_robin")
174
+ first4 = result4[0].agent_id
175
+
176
+ # Should have rotated through all three
177
+ firsts = [first1, first2, first3]
178
+ assert len(set(firsts)) == 3, "Round robin should cycle through all peers"
179
+
180
+ # Fourth should match first (wrapped around)
181
+ assert first4 == first1, "Round robin should wrap around"
182
+
183
+ def test_round_robin_independent_keys(self):
184
+ """Test round robin maintains separate indices per discovery key."""
185
+ from jarviscore.testing import MockPeerClient
186
+
187
+ client = MockPeerClient(
188
+ agent_id="test-agent",
189
+ agent_role="test",
190
+ mock_peers=[
191
+ {"role": "analyst", "agent_id": "a-0", "capabilities": ["analysis"]},
192
+ {"role": "analyst", "agent_id": "a-1", "capabilities": ["analysis"]},
193
+ {"role": "worker", "agent_id": "w-0", "capabilities": ["work"]},
194
+ {"role": "worker", "agent_id": "w-1", "capabilities": ["work"]}
195
+ ]
196
+ )
197
+
198
+ # Query analysts
199
+ analysts1 = client.discover(role="analyst", strategy="round_robin")
200
+ analyst_first1 = analysts1[0].agent_id
201
+
202
+ # Query workers
203
+ workers1 = client.discover(role="worker", strategy="round_robin")
204
+ worker_first1 = workers1[0].agent_id
205
+
206
+ # Query analysts again - should have rotated independently
207
+ analysts2 = client.discover(role="analyst", strategy="round_robin")
208
+ analyst_first2 = analysts2[0].agent_id
209
+
210
+ # Query workers again
211
+ workers2 = client.discover(role="worker", strategy="round_robin")
212
+ worker_first2 = workers2[0].agent_id
213
+
214
+ # Each role should have rotated independently
215
+ assert analyst_first1 != analyst_first2
216
+ assert worker_first1 != worker_first2
217
+
218
+
219
+ # =============================================================================
220
+ # TEST: STRATEGY LEAST RECENT
221
+ # =============================================================================
222
+
223
+ class TestStrategyLeastRecent:
224
+ """Test strategy='least_recent' (oldest used first)."""
225
+
226
+ def test_discover_least_recent_prefers_unused(self):
227
+ """Test 'least_recent' returns unused peers first."""
228
+ from jarviscore.testing import MockPeerClient
229
+
230
+ client = MockPeerClient(
231
+ agent_id="test-agent",
232
+ agent_role="test",
233
+ mock_peers=[
234
+ {"role": "worker", "agent_id": "w-1", "capabilities": ["work"]},
235
+ {"role": "worker", "agent_id": "w-2", "capabilities": ["work"]},
236
+ {"role": "worker", "agent_id": "w-3", "capabilities": ["work"]}
237
+ ]
238
+ )
239
+
240
+ # Mark w-2 as recently used
241
+ client.record_peer_usage("w-2")
242
+
243
+ result = client.discover(role="worker", strategy="least_recent")
244
+
245
+ # w-2 should be last (most recently used)
246
+ assert result[-1].agent_id == "w-2"
247
+
248
+ def test_discover_least_recent_ordering(self):
249
+ """Test 'least_recent' orders by usage time."""
250
+ from jarviscore.testing import MockPeerClient
251
+
252
+ client = MockPeerClient(
253
+ agent_id="test-agent",
254
+ agent_role="test",
255
+ mock_peers=[
256
+ {"role": "worker", "agent_id": "w-1", "capabilities": ["work"]},
257
+ {"role": "worker", "agent_id": "w-2", "capabilities": ["work"]},
258
+ {"role": "worker", "agent_id": "w-3", "capabilities": ["work"]}
259
+ ]
260
+ )
261
+
262
+ # Mark usage in specific order
263
+ client.record_peer_usage("w-3") # First used
264
+ time.sleep(0.01)
265
+ client.record_peer_usage("w-1") # Second used
266
+ time.sleep(0.01)
267
+ client.record_peer_usage("w-2") # Most recently used
268
+
269
+ result = client.discover(role="worker", strategy="least_recent")
270
+
271
+ ids = [p.agent_id for p in result]
272
+
273
+ # w-3 should be first (least recently used), w-2 last (most recent)
274
+ assert ids[0] == "w-3"
275
+ assert ids[-1] == "w-2"
276
+
277
+
278
+ # =============================================================================
279
+ # TEST: RECORD_PEER_USAGE
280
+ # =============================================================================
281
+
282
+ class TestRecordPeerUsage:
283
+ """Test record_peer_usage() method."""
284
+
285
+ def test_record_peer_usage_updates_timestamp(self):
286
+ """Test record_peer_usage() updates internal timestamp."""
287
+ from jarviscore.testing import MockPeerClient
288
+
289
+ client = MockPeerClient()
290
+
291
+ # Initially no usage recorded
292
+ assert client._peer_last_used.get("peer-1") is None
293
+
294
+ client.record_peer_usage("peer-1")
295
+
296
+ assert client._peer_last_used.get("peer-1") is not None
297
+ assert isinstance(client._peer_last_used["peer-1"], float)
298
+
299
+ def test_record_peer_usage_updates_on_repeated_calls(self):
300
+ """Test record_peer_usage() updates timestamp on repeated calls."""
301
+ from jarviscore.testing import MockPeerClient
302
+
303
+ client = MockPeerClient()
304
+
305
+ client.record_peer_usage("peer-1")
306
+ first_time = client._peer_last_used["peer-1"]
307
+
308
+ time.sleep(0.01)
309
+
310
+ client.record_peer_usage("peer-1")
311
+ second_time = client._peer_last_used["peer-1"]
312
+
313
+ assert second_time > first_time
314
+
315
+
316
+ # =============================================================================
317
+ # TEST: DISCOVER_ONE
318
+ # =============================================================================
319
+
320
+ class TestDiscoverOne:
321
+ """Test discover_one() convenience method."""
322
+
323
+ def test_discover_one_returns_single_peer(self):
324
+ """Test discover_one() returns single PeerInfo."""
325
+ from jarviscore.testing import MockPeerClient
326
+ from jarviscore.p2p.messages import PeerInfo
327
+
328
+ client = MockPeerClient(
329
+ agent_id="test-agent",
330
+ agent_role="test",
331
+ mock_peers=[
332
+ {"role": "analyst", "agent_id": "a-1", "capabilities": ["analysis"]},
333
+ {"role": "analyst", "agent_id": "a-2", "capabilities": ["analysis"]}
334
+ ]
335
+ )
336
+
337
+ result = client.discover_one(role="analyst")
338
+
339
+ assert result is not None
340
+ assert isinstance(result, PeerInfo)
341
+ assert result.role == "analyst"
342
+
343
+ def test_discover_one_returns_none_if_no_match(self):
344
+ """Test discover_one() returns None if no peers match."""
345
+ from jarviscore.testing import MockPeerClient
346
+
347
+ client = MockPeerClient(
348
+ agent_id="test-agent",
349
+ agent_role="test",
350
+ mock_peers=[{"role": "worker", "capabilities": ["work"]}]
351
+ )
352
+
353
+ result = client.discover_one(role="analyst")
354
+
355
+ assert result is None
356
+
357
+ def test_discover_one_with_strategy(self):
358
+ """Test discover_one() respects strategy parameter."""
359
+ from jarviscore.testing import MockPeerClient
360
+
361
+ client = MockPeerClient(
362
+ agent_id="test-agent",
363
+ agent_role="test",
364
+ mock_peers=[
365
+ {"role": "worker", "agent_id": "w-0", "capabilities": ["work"]},
366
+ {"role": "worker", "agent_id": "w-1", "capabilities": ["work"]},
367
+ {"role": "worker", "agent_id": "w-2", "capabilities": ["work"]}
368
+ ]
369
+ )
370
+
371
+ # Round robin should rotate
372
+ first1 = client.discover_one(role="worker", strategy="round_robin")
373
+ first2 = client.discover_one(role="worker", strategy="round_robin")
374
+ first3 = client.discover_one(role="worker", strategy="round_robin")
375
+
376
+ ids = [first1.agent_id, first2.agent_id, first3.agent_id]
377
+ assert len(set(ids)) == 3, "Round robin via discover_one should rotate"
378
+
379
+ def test_discover_one_with_capability(self):
380
+ """Test discover_one() filters by capability."""
381
+ from jarviscore.testing import MockPeerClient
382
+
383
+ client = MockPeerClient(
384
+ agent_id="test-agent",
385
+ agent_role="test",
386
+ mock_peers=[
387
+ {"role": "agent1", "agent_id": "a-1", "capabilities": ["analysis"]},
388
+ {"role": "agent2", "agent_id": "a-2", "capabilities": ["reporting"]},
389
+ {"role": "agent3", "agent_id": "a-3", "capabilities": ["analysis", "reporting"]}
390
+ ]
391
+ )
392
+
393
+ result = client.discover_one(capability="reporting")
394
+
395
+ assert result is not None
396
+ assert "reporting" in result.capabilities
397
+
398
+
399
+ # =============================================================================
400
+ # TEST: REAL PEER CLIENT STRATEGIES
401
+ # =============================================================================
402
+
403
+ class TestRealPeerClientStrategies:
404
+ """Test strategies with real PeerClient."""
405
+
406
+ def test_real_client_round_robin(self):
407
+ """Test real PeerClient round robin strategy."""
408
+ from jarviscore.p2p.peer_client import PeerClient
409
+
410
+ mock_coordinator = MagicMock()
411
+ mock_coordinator._remote_agent_registry = {}
412
+
413
+ # Create mock agents
414
+ class MockAgent:
415
+ def __init__(self, aid, role):
416
+ self.agent_id = aid
417
+ self.role = role
418
+ self.capabilities = ["work"]
419
+
420
+ agents = [
421
+ MockAgent("w-0", "worker"),
422
+ MockAgent("w-1", "worker"),
423
+ MockAgent("w-2", "worker")
424
+ ]
425
+
426
+ agent_registry = {"worker": agents}
427
+
428
+ client = PeerClient(
429
+ coordinator=mock_coordinator,
430
+ agent_id="client-1",
431
+ agent_role="client",
432
+ agent_registry=agent_registry,
433
+ node_id="localhost:7946"
434
+ )
435
+
436
+ # Collect first results
437
+ firsts = []
438
+ for _ in range(6): # 2 full cycles
439
+ result = client.discover(role="worker", strategy="round_robin")
440
+ firsts.append(result[0].agent_id)
441
+
442
+ # Should cycle: 0,1,2,0,1,2
443
+ assert firsts[:3] != firsts[3:] or len(set(firsts[:3])) == 3
444
+
445
+ def test_real_client_least_recent(self):
446
+ """Test real PeerClient least_recent strategy."""
447
+ from jarviscore.p2p.peer_client import PeerClient
448
+
449
+ mock_coordinator = MagicMock()
450
+ mock_coordinator._remote_agent_registry = {}
451
+
452
+ class MockAgent:
453
+ def __init__(self, aid, role):
454
+ self.agent_id = aid
455
+ self.role = role
456
+ self.capabilities = ["work"]
457
+
458
+ agents = [
459
+ MockAgent("w-1", "worker"),
460
+ MockAgent("w-2", "worker"),
461
+ MockAgent("w-3", "worker")
462
+ ]
463
+
464
+ agent_registry = {"worker": agents}
465
+
466
+ client = PeerClient(
467
+ coordinator=mock_coordinator,
468
+ agent_id="client-1",
469
+ agent_role="client",
470
+ agent_registry=agent_registry,
471
+ node_id="localhost:7946"
472
+ )
473
+
474
+ # Mark some as used
475
+ client.record_peer_usage("w-2")
476
+
477
+ result = client.discover(role="worker", strategy="least_recent")
478
+
479
+ # w-2 should be last
480
+ assert result[-1].agent_id == "w-2"
481
+
482
+
483
+ # =============================================================================
484
+ # TEST: EDGE CASES
485
+ # =============================================================================
486
+
487
+ class TestLoadBalancingEdgeCases:
488
+ """Test edge cases for load balancing."""
489
+
490
+ def test_single_peer_all_strategies(self):
491
+ """Test all strategies work with single peer."""
492
+ from jarviscore.testing import MockPeerClient
493
+
494
+ client = MockPeerClient(
495
+ agent_id="test-agent",
496
+ agent_role="test",
497
+ mock_peers=[{"role": "worker", "agent_id": "w-1", "capabilities": ["work"]}]
498
+ )
499
+
500
+ for strategy in ["first", "random", "round_robin", "least_recent"]:
501
+ result = client.discover(role="worker", strategy=strategy)
502
+ assert len(result) == 1
503
+ assert result[0].agent_id == "w-1"
504
+
505
+ def test_empty_results_all_strategies(self):
506
+ """Test all strategies handle empty results."""
507
+ from jarviscore.testing import MockPeerClient
508
+
509
+ client = MockPeerClient(
510
+ agent_id="test-agent",
511
+ agent_role="test",
512
+ mock_peers=[{"role": "analyst", "capabilities": ["analysis"]}]
513
+ )
514
+
515
+ for strategy in ["first", "random", "round_robin", "least_recent"]:
516
+ result = client.discover(role="nonexistent", strategy=strategy)
517
+ assert result == []
518
+
519
+ def test_unknown_strategy_falls_back_to_first(self):
520
+ """Test unknown strategy falls back to 'first' behavior."""
521
+ from jarviscore.testing import MockPeerClient
522
+
523
+ client = MockPeerClient(
524
+ agent_id="test-agent",
525
+ agent_role="test",
526
+ mock_peers=[
527
+ {"role": "worker", "agent_id": "w-1", "capabilities": ["work"]},
528
+ {"role": "worker", "agent_id": "w-2", "capabilities": ["work"]}
529
+ ]
530
+ )
531
+
532
+ result1 = client.discover(role="worker", strategy="unknown_strategy")
533
+ result2 = client.discover(role="worker", strategy="first")
534
+
535
+ ids1 = [p.agent_id for p in result1]
536
+ ids2 = [p.agent_id for p in result2]
537
+
538
+ assert ids1 == ids2
539
+
540
+
541
+ # =============================================================================
542
+ # RUN TESTS
543
+ # =============================================================================
544
+
545
+ if __name__ == "__main__":
546
+ pytest.main([__file__, "-v", "-s"])