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.
- examples/cloud_deployment_example.py +3 -3
- examples/{listeneragent_cognitive_discovery_example.py → customagent_cognitive_discovery_example.py} +55 -14
- examples/customagent_distributed_example.py +140 -1
- examples/fastapi_integration_example.py +74 -11
- jarviscore/__init__.py +8 -11
- jarviscore/cli/smoketest.py +1 -1
- jarviscore/core/mesh.py +158 -0
- jarviscore/data/examples/cloud_deployment_example.py +3 -3
- jarviscore/data/examples/custom_profile_decorator.py +134 -0
- jarviscore/data/examples/custom_profile_wrap.py +168 -0
- jarviscore/data/examples/{listeneragent_cognitive_discovery_example.py → customagent_cognitive_discovery_example.py} +55 -14
- jarviscore/data/examples/customagent_distributed_example.py +140 -1
- jarviscore/data/examples/fastapi_integration_example.py +74 -11
- jarviscore/docs/API_REFERENCE.md +576 -47
- jarviscore/docs/CHANGELOG.md +131 -0
- jarviscore/docs/CONFIGURATION.md +1 -1
- jarviscore/docs/CUSTOMAGENT_GUIDE.md +591 -153
- jarviscore/docs/GETTING_STARTED.md +186 -329
- jarviscore/docs/TROUBLESHOOTING.md +1 -1
- jarviscore/docs/USER_GUIDE.md +292 -12
- jarviscore/integrations/fastapi.py +4 -4
- jarviscore/p2p/coordinator.py +36 -7
- jarviscore/p2p/messages.py +13 -0
- jarviscore/p2p/peer_client.py +380 -21
- jarviscore/p2p/peer_tool.py +17 -11
- jarviscore/profiles/__init__.py +2 -4
- jarviscore/profiles/customagent.py +302 -74
- jarviscore/testing/__init__.py +35 -0
- jarviscore/testing/mocks.py +578 -0
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/METADATA +61 -46
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/RECORD +42 -34
- tests/test_13_dx_improvements.py +37 -37
- tests/test_15_llm_cognitive_discovery.py +18 -18
- tests/test_16_unified_dx_flow.py +3 -3
- 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/profiles/listeneragent.py +0 -292
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/WHEEL +0 -0
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/top_level.txt +0 -0
jarviscore/docs/API_REFERENCE.md
CHANGED
|
@@ -14,7 +14,6 @@ Complete API documentation for JarvisCore framework components.
|
|
|
14
14
|
- [AutoAgent](#autoagent)
|
|
15
15
|
- [Custom Profile](#custom-profile)
|
|
16
16
|
- [CustomAgent](#customagent)
|
|
17
|
-
- [ListenerAgent (v0.3.0)](#listeneragent-v030)
|
|
18
17
|
3. [P2P Communication (v0.3.0)](#p2p-communication-v030)
|
|
19
18
|
- [PeerClient](#peerclient)
|
|
20
19
|
- [IncomingMessage](#incomingmessage)
|
|
@@ -149,6 +148,50 @@ await mesh.run_forever() # Blocks until SIGINT/SIGTERM
|
|
|
149
148
|
|
|
150
149
|
---
|
|
151
150
|
|
|
151
|
+
#### `get_diagnostics() -> dict` (v0.3.2)
|
|
152
|
+
|
|
153
|
+
Get diagnostic information about mesh health and P2P connectivity.
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
diag = mesh.get_diagnostics()
|
|
157
|
+
print(f"Status: {diag['connectivity_status']}")
|
|
158
|
+
print(f"Agents: {len(diag['local_agents'])}")
|
|
159
|
+
|
|
160
|
+
for peer in diag['known_peers']:
|
|
161
|
+
print(f" {peer['role']} at {peer['node_id']}: {peer['status']}")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Returns:**
|
|
165
|
+
```python
|
|
166
|
+
{
|
|
167
|
+
"local_node": {
|
|
168
|
+
"mode": "p2p", # Current mesh mode
|
|
169
|
+
"started": True, # Whether mesh is started
|
|
170
|
+
"agent_count": 3, # Number of local agents
|
|
171
|
+
"bind_address": "127.0.0.1:7950" # P2P bind address (if P2P)
|
|
172
|
+
},
|
|
173
|
+
"known_peers": [ # Remote peers (if P2P enabled)
|
|
174
|
+
{"role": "analyst", "node_id": "10.0.0.2:7950", "status": "alive"}
|
|
175
|
+
],
|
|
176
|
+
"local_agents": [ # Local agent info
|
|
177
|
+
{"role": "scout", "agent_id": "scout-abc", "capabilities": ["research"]}
|
|
178
|
+
],
|
|
179
|
+
"connectivity_status": "healthy", # Overall health
|
|
180
|
+
"keepalive_status": {...}, # Keepalive manager status (P2P only)
|
|
181
|
+
"swim_status": {...}, # SWIM protocol status (P2P only)
|
|
182
|
+
"capability_map": {...} # Capability to agent mapping (P2P only)
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Connectivity Status Values:**
|
|
187
|
+
- `"healthy"` - P2P active with connected peers
|
|
188
|
+
- `"isolated"` - P2P active but no peers found
|
|
189
|
+
- `"degraded"` - Some connectivity issues detected
|
|
190
|
+
- `"not_started"` - Mesh not yet started
|
|
191
|
+
- `"local_only"` - Autonomous mode (no P2P)
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
152
195
|
### Agent
|
|
153
196
|
|
|
154
197
|
Base class for all agents. Inherit from this to create custom agents.
|
|
@@ -564,33 +607,9 @@ See [CustomAgent Guide](CUSTOMAGENT_GUIDE.md) for P2P and distributed mode detai
|
|
|
564
607
|
|
|
565
608
|
---
|
|
566
609
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
Handler-based agent for P2P communication without manual run loops.
|
|
570
|
-
|
|
571
|
-
#### Class: `ListenerAgent(CustomAgent)`
|
|
572
|
-
|
|
573
|
-
```python
|
|
574
|
-
from jarviscore.profiles import ListenerAgent
|
|
575
|
-
|
|
576
|
-
class MyAgent(ListenerAgent):
|
|
577
|
-
role = "processor"
|
|
578
|
-
capabilities = ["processing"]
|
|
579
|
-
|
|
580
|
-
async def on_peer_request(self, msg):
|
|
581
|
-
"""Handle incoming requests from peers."""
|
|
582
|
-
return {"result": msg.data.get("task", "").upper()}
|
|
583
|
-
|
|
584
|
-
async def on_peer_notify(self, msg):
|
|
585
|
-
"""Handle broadcast notifications."""
|
|
586
|
-
print(f"Notification: {msg.data}")
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
**Class Attributes:**
|
|
590
|
-
- `role` (str): Agent role identifier (required)
|
|
591
|
-
- `capabilities` (list): List of capability strings (required)
|
|
610
|
+
#### P2P Message Handlers
|
|
592
611
|
|
|
593
|
-
|
|
612
|
+
CustomAgent includes built-in P2P message handlers:
|
|
594
613
|
|
|
595
614
|
#### `async on_peer_request(msg) -> dict`
|
|
596
615
|
|
|
@@ -604,9 +623,9 @@ async def on_peer_request(self, msg):
|
|
|
604
623
|
```
|
|
605
624
|
|
|
606
625
|
**Parameters:**
|
|
607
|
-
- `msg` (IncomingMessage): Incoming message with `data`, `
|
|
626
|
+
- `msg` (IncomingMessage): Incoming message with `data`, `sender`, `correlation_id`
|
|
608
627
|
|
|
609
|
-
**Returns:** dict - Response sent back to requester
|
|
628
|
+
**Returns:** dict - Response sent back to requester (if `auto_respond=True`)
|
|
610
629
|
|
|
611
630
|
---
|
|
612
631
|
|
|
@@ -628,7 +647,29 @@ async def on_peer_notify(self, msg):
|
|
|
628
647
|
|
|
629
648
|
---
|
|
630
649
|
|
|
631
|
-
|
|
650
|
+
#### `async on_error(error, msg) -> None`
|
|
651
|
+
|
|
652
|
+
Handle errors during message processing.
|
|
653
|
+
|
|
654
|
+
```python
|
|
655
|
+
async def on_error(self, error, msg):
|
|
656
|
+
self._logger.error(f"Error processing message: {error}")
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
**Parameters:**
|
|
660
|
+
- `error` (Exception): The exception that occurred
|
|
661
|
+
- `msg` (IncomingMessage, optional): The message being processed
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
#### Configuration Attributes
|
|
666
|
+
|
|
667
|
+
- `listen_timeout` (float): Seconds to wait for messages in run loop (default: 1.0)
|
|
668
|
+
- `auto_respond` (bool): Automatically send on_peer_request return value (default: True)
|
|
669
|
+
|
|
670
|
+
---
|
|
671
|
+
|
|
672
|
+
#### Self-Registration Methods
|
|
632
673
|
|
|
633
674
|
#### `async join_mesh(seed_nodes, advertise_endpoint=None)`
|
|
634
675
|
|
|
@@ -657,25 +698,232 @@ await agent.leave_mesh()
|
|
|
657
698
|
|
|
658
699
|
---
|
|
659
700
|
|
|
660
|
-
|
|
701
|
+
## P2P Communication (v0.3.0)
|
|
702
|
+
|
|
703
|
+
### PeerClient
|
|
704
|
+
|
|
705
|
+
Client for peer-to-peer communication, available as `self.peers` on agents.
|
|
706
|
+
|
|
707
|
+
#### Class: `PeerClient`
|
|
708
|
+
|
|
709
|
+
**Methods:**
|
|
710
|
+
|
|
711
|
+
---
|
|
712
|
+
|
|
713
|
+
### Discovery Methods (v0.3.2)
|
|
714
|
+
|
|
715
|
+
#### `discover(capability=None, role=None, strategy="first") -> List[PeerInfo]`
|
|
716
|
+
|
|
717
|
+
Discover peers with optional load balancing strategy.
|
|
718
|
+
|
|
719
|
+
```python
|
|
720
|
+
# Default: return in discovery order
|
|
721
|
+
peers = self.peers.discover(role="worker")
|
|
722
|
+
|
|
723
|
+
# Random: shuffle for load distribution
|
|
724
|
+
peers = self.peers.discover(role="worker", strategy="random")
|
|
725
|
+
|
|
726
|
+
# Round robin: rotate through peers on each call
|
|
727
|
+
peers = self.peers.discover(role="worker", strategy="round_robin")
|
|
728
|
+
|
|
729
|
+
# Least recent: return least recently used peers first
|
|
730
|
+
peers = self.peers.discover(role="worker", strategy="least_recent")
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
**Parameters:**
|
|
734
|
+
- `capability` (str, optional): Filter by capability
|
|
735
|
+
- `role` (str, optional): Filter by role
|
|
736
|
+
- `strategy` (str): Selection strategy - `"first"`, `"random"`, `"round_robin"`, `"least_recent"`
|
|
737
|
+
|
|
738
|
+
**Returns:** List of PeerInfo objects ordered by strategy
|
|
739
|
+
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
#### `discover_one(capability=None, role=None, strategy="first") -> Optional[PeerInfo]`
|
|
661
743
|
|
|
662
|
-
|
|
744
|
+
Discover a single peer (convenience wrapper for discover).
|
|
663
745
|
|
|
664
746
|
```python
|
|
665
|
-
|
|
747
|
+
worker = self.peers.discover_one(role="worker", strategy="round_robin")
|
|
748
|
+
if worker:
|
|
749
|
+
await self.peers.request(worker.agent_id, {"task": "..."})
|
|
666
750
|
```
|
|
667
751
|
|
|
752
|
+
**Returns:** Single PeerInfo or None if no match
|
|
753
|
+
|
|
668
754
|
---
|
|
669
755
|
|
|
670
|
-
|
|
756
|
+
#### `record_peer_usage(peer_id: str)`
|
|
671
757
|
|
|
672
|
-
|
|
758
|
+
Record that a peer was used (for `least_recent` strategy tracking).
|
|
673
759
|
|
|
674
|
-
|
|
760
|
+
```python
|
|
761
|
+
peer = self.peers.discover_one(role="worker", strategy="least_recent")
|
|
762
|
+
response = await self.peers.request(peer.agent_id, {"task": "..."})
|
|
763
|
+
self.peers.record_peer_usage(peer.agent_id) # Update usage timestamp
|
|
764
|
+
```
|
|
675
765
|
|
|
676
|
-
|
|
766
|
+
---
|
|
677
767
|
|
|
678
|
-
|
|
768
|
+
### Messaging Methods (v0.3.2 - Context Support)
|
|
769
|
+
|
|
770
|
+
#### `async notify(target, message, context=None) -> bool`
|
|
771
|
+
|
|
772
|
+
Send a fire-and-forget notification with optional context.
|
|
773
|
+
|
|
774
|
+
```python
|
|
775
|
+
await self.peers.notify("analyst", {
|
|
776
|
+
"event": "task_complete",
|
|
777
|
+
"data": result
|
|
778
|
+
}, context={"mission_id": "m-123", "priority": "high"})
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
**Parameters:**
|
|
782
|
+
- `target` (str): Target agent role or agent_id
|
|
783
|
+
- `message` (dict): Message payload
|
|
784
|
+
- `context` (dict, optional): Metadata (mission_id, priority, trace_id, etc.)
|
|
785
|
+
|
|
786
|
+
---
|
|
787
|
+
|
|
788
|
+
#### `async request(target, message, timeout=30.0, context=None) -> Optional[dict]`
|
|
789
|
+
|
|
790
|
+
Send request and wait for response with optional context.
|
|
791
|
+
|
|
792
|
+
```python
|
|
793
|
+
response = await self.peers.request("analyst", {
|
|
794
|
+
"query": "analyze this data"
|
|
795
|
+
}, timeout=10, context={"mission_id": "m-123"})
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
**Parameters:**
|
|
799
|
+
- `target` (str): Target agent role or agent_id
|
|
800
|
+
- `message` (dict): Request payload
|
|
801
|
+
- `timeout` (float): Max seconds to wait (default: 30)
|
|
802
|
+
- `context` (dict, optional): Metadata propagated with request
|
|
803
|
+
|
|
804
|
+
---
|
|
805
|
+
|
|
806
|
+
#### `async respond(message, response, context=None) -> bool`
|
|
807
|
+
|
|
808
|
+
Respond to an incoming request. Auto-propagates context if not overridden.
|
|
809
|
+
|
|
810
|
+
```python
|
|
811
|
+
async def on_peer_request(self, msg):
|
|
812
|
+
result = process(msg.data)
|
|
813
|
+
# Context auto-propagated from msg.context
|
|
814
|
+
await self.peers.respond(msg, {"result": result})
|
|
815
|
+
|
|
816
|
+
# Or override with custom context
|
|
817
|
+
await self.peers.respond(msg, {"result": result},
|
|
818
|
+
context={"status": "completed"})
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
**Parameters:**
|
|
822
|
+
- `message` (IncomingMessage): The incoming request
|
|
823
|
+
- `response` (dict): Response data
|
|
824
|
+
- `context` (dict, optional): Override context (defaults to request's context)
|
|
825
|
+
|
|
826
|
+
---
|
|
827
|
+
|
|
828
|
+
#### `async broadcast(message, context=None) -> int`
|
|
829
|
+
|
|
830
|
+
Broadcast notification to all peers with optional context.
|
|
831
|
+
|
|
832
|
+
```python
|
|
833
|
+
count = await self.peers.broadcast({
|
|
834
|
+
"event": "status_update",
|
|
835
|
+
"status": "ready"
|
|
836
|
+
}, context={"broadcast_id": "bc-123"})
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
**Returns:** Number of peers notified
|
|
840
|
+
|
|
841
|
+
---
|
|
842
|
+
|
|
843
|
+
### Async Request Pattern (v0.3.2)
|
|
844
|
+
|
|
845
|
+
#### `async ask_async(target, message, timeout=120.0, context=None) -> str`
|
|
846
|
+
|
|
847
|
+
Send request without blocking for response. Returns request_id immediately.
|
|
848
|
+
|
|
849
|
+
```python
|
|
850
|
+
# Fire off multiple requests in parallel
|
|
851
|
+
request_ids = []
|
|
852
|
+
for analyst in analysts:
|
|
853
|
+
req_id = await self.peers.ask_async(analyst, {"task": "analyze"})
|
|
854
|
+
request_ids.append(req_id)
|
|
855
|
+
|
|
856
|
+
# Do other work while waiting...
|
|
857
|
+
await process_other_tasks()
|
|
858
|
+
|
|
859
|
+
# Collect responses later
|
|
860
|
+
for req_id in request_ids:
|
|
861
|
+
response = await self.peers.check_inbox(req_id, timeout=5)
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
**Parameters:**
|
|
865
|
+
- `target` (str): Target agent role or agent_id
|
|
866
|
+
- `message` (dict): Request payload
|
|
867
|
+
- `timeout` (float): Max time to keep request active (default: 120s)
|
|
868
|
+
- `context` (dict, optional): Request context
|
|
869
|
+
|
|
870
|
+
**Returns:** Request ID string for use with `check_inbox()`
|
|
871
|
+
|
|
872
|
+
**Raises:** `ValueError` if target not found or send fails
|
|
873
|
+
|
|
874
|
+
---
|
|
875
|
+
|
|
876
|
+
#### `async check_inbox(request_id, timeout=0.0, remove=True) -> Optional[dict]`
|
|
877
|
+
|
|
878
|
+
Check for response to an async request.
|
|
879
|
+
|
|
880
|
+
```python
|
|
881
|
+
# Non-blocking check
|
|
882
|
+
response = await self.peers.check_inbox(req_id)
|
|
883
|
+
|
|
884
|
+
# Wait up to 5 seconds
|
|
885
|
+
response = await self.peers.check_inbox(req_id, timeout=5)
|
|
886
|
+
|
|
887
|
+
# Peek without removing
|
|
888
|
+
response = await self.peers.check_inbox(req_id, remove=False)
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
**Parameters:**
|
|
892
|
+
- `request_id` (str): ID returned by `ask_async()`
|
|
893
|
+
- `timeout` (float): Seconds to wait (0 = immediate return)
|
|
894
|
+
- `remove` (bool): Remove from inbox after reading (default: True)
|
|
895
|
+
|
|
896
|
+
**Returns:** Response dict if available, None if not ready or timed out
|
|
897
|
+
|
|
898
|
+
---
|
|
899
|
+
|
|
900
|
+
#### `get_pending_async_requests() -> List[dict]`
|
|
901
|
+
|
|
902
|
+
Get list of pending async requests.
|
|
903
|
+
|
|
904
|
+
```python
|
|
905
|
+
pending = self.peers.get_pending_async_requests()
|
|
906
|
+
for req in pending:
|
|
907
|
+
print(f"Waiting for {req['target']} since {req['sent_at']}")
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
**Returns:** List of dicts with `request_id`, `target`, `sent_at`, `timeout`
|
|
911
|
+
|
|
912
|
+
---
|
|
913
|
+
|
|
914
|
+
#### `clear_inbox(request_id=None)`
|
|
915
|
+
|
|
916
|
+
Clear async request inbox.
|
|
917
|
+
|
|
918
|
+
```python
|
|
919
|
+
# Clear specific request
|
|
920
|
+
self.peers.clear_inbox(req_id)
|
|
921
|
+
|
|
922
|
+
# Clear all
|
|
923
|
+
self.peers.clear_inbox()
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
---
|
|
679
927
|
|
|
680
928
|
#### `get_cognitive_context() -> str`
|
|
681
929
|
|
|
@@ -755,16 +1003,28 @@ Message received from a peer.
|
|
|
755
1003
|
#### Class: `IncomingMessage`
|
|
756
1004
|
|
|
757
1005
|
**Attributes:**
|
|
1006
|
+
- `sender` (str): Agent ID of the sender
|
|
1007
|
+
- `sender_node` (str): P2P node ID of the sender
|
|
1008
|
+
- `type` (MessageType): Message type (NOTIFY, REQUEST, RESPONSE)
|
|
758
1009
|
- `data` (dict): Message payload
|
|
759
|
-
- `
|
|
760
|
-
- `
|
|
1010
|
+
- `correlation_id` (str, optional): ID linking request to response
|
|
1011
|
+
- `timestamp` (float): When the message was sent
|
|
1012
|
+
- `context` (dict, optional): Metadata (mission_id, priority, trace_id, etc.) - *v0.3.2*
|
|
1013
|
+
|
|
1014
|
+
**Properties:**
|
|
761
1015
|
- `is_request` (bool): True if this is a request expecting response
|
|
762
1016
|
- `is_notify` (bool): True if this is a notification
|
|
763
1017
|
|
|
764
1018
|
```python
|
|
765
1019
|
async def on_peer_request(self, msg):
|
|
766
|
-
print(f"From: {msg.
|
|
1020
|
+
print(f"From: {msg.sender}")
|
|
767
1021
|
print(f"Data: {msg.data}")
|
|
1022
|
+
|
|
1023
|
+
# Access context metadata (v0.3.2)
|
|
1024
|
+
if msg.context:
|
|
1025
|
+
mission_id = msg.context.get("mission_id")
|
|
1026
|
+
priority = msg.context.get("priority")
|
|
1027
|
+
|
|
768
1028
|
return {"received": True}
|
|
769
1029
|
```
|
|
770
1030
|
|
|
@@ -811,7 +1071,7 @@ app = FastAPI(lifespan=JarvisLifespan(agent, mode="p2p"))
|
|
|
811
1071
|
```
|
|
812
1072
|
|
|
813
1073
|
**Parameters:**
|
|
814
|
-
- `agent`:
|
|
1074
|
+
- `agent`: CustomAgent instance (or list of agents)
|
|
815
1075
|
- `mode` (str): "p2p" or "distributed"
|
|
816
1076
|
- `bind_port` (int, optional): P2P port (default: 7950)
|
|
817
1077
|
- `seed_nodes` (str, optional): Comma-separated seed node addresses
|
|
@@ -820,11 +1080,11 @@ app = FastAPI(lifespan=JarvisLifespan(agent, mode="p2p"))
|
|
|
820
1080
|
|
|
821
1081
|
```python
|
|
822
1082
|
from fastapi import FastAPI
|
|
823
|
-
from jarviscore.profiles import
|
|
1083
|
+
from jarviscore.profiles import CustomAgent
|
|
824
1084
|
from jarviscore.integrations.fastapi import JarvisLifespan
|
|
825
1085
|
|
|
826
1086
|
|
|
827
|
-
class ProcessorAgent(
|
|
1087
|
+
class ProcessorAgent(CustomAgent):
|
|
828
1088
|
role = "processor"
|
|
829
1089
|
capabilities = ["processing"]
|
|
830
1090
|
|
|
@@ -845,7 +1105,7 @@ async def process(data: dict):
|
|
|
845
1105
|
**Handles:**
|
|
846
1106
|
- Agent setup and teardown
|
|
847
1107
|
- Mesh initialization
|
|
848
|
-
- Background run loop (
|
|
1108
|
+
- Background run loop (runs agent.run())
|
|
849
1109
|
- Graceful shutdown
|
|
850
1110
|
|
|
851
1111
|
---
|
|
@@ -1431,8 +1691,277 @@ async def execute_task(self, task: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1431
1691
|
|
|
1432
1692
|
---
|
|
1433
1693
|
|
|
1694
|
+
## Testing Utilities (v0.3.2)
|
|
1695
|
+
|
|
1696
|
+
### MockMesh
|
|
1697
|
+
|
|
1698
|
+
Simplified mesh for unit testing without real P2P infrastructure.
|
|
1699
|
+
|
|
1700
|
+
#### Class: `MockMesh`
|
|
1701
|
+
|
|
1702
|
+
```python
|
|
1703
|
+
from jarviscore.testing import MockMesh
|
|
1704
|
+
|
|
1705
|
+
mesh = MockMesh(mode="p2p")
|
|
1706
|
+
mesh.add(MyAgent)
|
|
1707
|
+
await mesh.start()
|
|
1708
|
+
|
|
1709
|
+
agent = mesh.get_agent("my_role")
|
|
1710
|
+
# Test agent behavior...
|
|
1711
|
+
|
|
1712
|
+
await mesh.stop()
|
|
1713
|
+
```
|
|
1714
|
+
|
|
1715
|
+
**Methods:**
|
|
1716
|
+
|
|
1717
|
+
#### `add(agent_class_or_instance, agent_id=None) -> Agent`
|
|
1718
|
+
|
|
1719
|
+
Register an agent with the mock mesh.
|
|
1720
|
+
|
|
1721
|
+
```python
|
|
1722
|
+
mesh.add(MyAgent) # Add class
|
|
1723
|
+
mesh.add(my_instance) # Add instance
|
|
1724
|
+
```
|
|
1725
|
+
|
|
1726
|
+
---
|
|
1727
|
+
|
|
1728
|
+
#### `async start()`
|
|
1729
|
+
|
|
1730
|
+
Start the mock mesh. Runs agent setup and injects MockPeerClient into each agent.
|
|
1731
|
+
|
|
1732
|
+
---
|
|
1733
|
+
|
|
1734
|
+
#### `async stop()`
|
|
1735
|
+
|
|
1736
|
+
Stop the mock mesh and run agent teardown.
|
|
1737
|
+
|
|
1738
|
+
---
|
|
1739
|
+
|
|
1740
|
+
#### `get_agent(role) -> Optional[Agent]`
|
|
1741
|
+
|
|
1742
|
+
Get agent by role.
|
|
1743
|
+
|
|
1744
|
+
```python
|
|
1745
|
+
agent = mesh.get_agent("analyst")
|
|
1746
|
+
```
|
|
1747
|
+
|
|
1748
|
+
---
|
|
1749
|
+
|
|
1750
|
+
#### `get_diagnostics() -> dict`
|
|
1751
|
+
|
|
1752
|
+
Get mock diagnostics (compatible structure with real Mesh).
|
|
1753
|
+
|
|
1754
|
+
```python
|
|
1755
|
+
diag = mesh.get_diagnostics()
|
|
1756
|
+
assert diag["connectivity_status"] == "mock"
|
|
1757
|
+
```
|
|
1758
|
+
|
|
1759
|
+
---
|
|
1760
|
+
|
|
1761
|
+
### MockPeerClient
|
|
1762
|
+
|
|
1763
|
+
Full mock replacement for PeerClient with test configuration and assertions.
|
|
1764
|
+
|
|
1765
|
+
#### Class: `MockPeerClient`
|
|
1766
|
+
|
|
1767
|
+
```python
|
|
1768
|
+
from jarviscore.testing import MockPeerClient
|
|
1769
|
+
|
|
1770
|
+
client = MockPeerClient(
|
|
1771
|
+
agent_id="test-agent",
|
|
1772
|
+
agent_role="tester",
|
|
1773
|
+
mock_peers=[
|
|
1774
|
+
{"role": "analyst", "capabilities": ["analysis"]},
|
|
1775
|
+
{"role": "scout", "capabilities": ["research"]}
|
|
1776
|
+
],
|
|
1777
|
+
auto_respond=True
|
|
1778
|
+
)
|
|
1779
|
+
```
|
|
1780
|
+
|
|
1781
|
+
**Parameters:**
|
|
1782
|
+
- `agent_id` (str): ID for the mock agent
|
|
1783
|
+
- `agent_role` (str): Role for the mock agent
|
|
1784
|
+
- `mock_peers` (list): List of peer definitions with role, capabilities
|
|
1785
|
+
- `auto_respond` (bool): Auto-respond to requests with mock data (default: True)
|
|
1786
|
+
|
|
1787
|
+
---
|
|
1788
|
+
|
|
1789
|
+
#### Configuration Methods
|
|
1790
|
+
|
|
1791
|
+
#### `set_mock_response(target, response)`
|
|
1792
|
+
|
|
1793
|
+
Configure response for a specific target.
|
|
1794
|
+
|
|
1795
|
+
```python
|
|
1796
|
+
client.set_mock_response("analyst", {"result": "analysis complete", "score": 95})
|
|
1797
|
+
response = await client.request("analyst", {"query": "test"})
|
|
1798
|
+
assert response["score"] == 95
|
|
1799
|
+
```
|
|
1800
|
+
|
|
1801
|
+
---
|
|
1802
|
+
|
|
1803
|
+
#### `set_default_response(response)`
|
|
1804
|
+
|
|
1805
|
+
Set default response for unconfigured targets.
|
|
1806
|
+
|
|
1807
|
+
```python
|
|
1808
|
+
client.set_default_response({"status": "success", "mock": True})
|
|
1809
|
+
```
|
|
1810
|
+
|
|
1811
|
+
---
|
|
1812
|
+
|
|
1813
|
+
#### `set_request_handler(handler)`
|
|
1814
|
+
|
|
1815
|
+
Set custom async handler for dynamic responses.
|
|
1816
|
+
|
|
1817
|
+
```python
|
|
1818
|
+
async def custom_handler(target, message, context):
|
|
1819
|
+
return {"echo": message.get("query"), "target": target}
|
|
1820
|
+
|
|
1821
|
+
client.set_request_handler(custom_handler)
|
|
1822
|
+
```
|
|
1823
|
+
|
|
1824
|
+
---
|
|
1825
|
+
|
|
1826
|
+
#### `add_mock_peer(role, capabilities=None, **kwargs)`
|
|
1827
|
+
|
|
1828
|
+
Add a mock peer dynamically.
|
|
1829
|
+
|
|
1830
|
+
```python
|
|
1831
|
+
client.add_mock_peer("reporter", capabilities=["reporting", "formatting"])
|
|
1832
|
+
```
|
|
1833
|
+
|
|
1834
|
+
---
|
|
1835
|
+
|
|
1836
|
+
#### `inject_message(sender, message_type, data, correlation_id=None, context=None)`
|
|
1837
|
+
|
|
1838
|
+
Inject a message into the receive queue for testing message handlers.
|
|
1839
|
+
|
|
1840
|
+
```python
|
|
1841
|
+
from jarviscore.p2p.messages import MessageType
|
|
1842
|
+
|
|
1843
|
+
client.inject_message(
|
|
1844
|
+
sender="external_agent",
|
|
1845
|
+
message_type=MessageType.NOTIFY,
|
|
1846
|
+
data={"event": "test_event", "value": 42},
|
|
1847
|
+
context={"mission_id": "m-123"}
|
|
1848
|
+
)
|
|
1849
|
+
|
|
1850
|
+
msg = await client.receive(timeout=1)
|
|
1851
|
+
assert msg.data["value"] == 42
|
|
1852
|
+
```
|
|
1853
|
+
|
|
1854
|
+
---
|
|
1855
|
+
|
|
1856
|
+
#### Assertion Helpers
|
|
1857
|
+
|
|
1858
|
+
#### `assert_notified(target, message_contains=None)`
|
|
1859
|
+
|
|
1860
|
+
Assert a notification was sent to target.
|
|
1861
|
+
|
|
1862
|
+
```python
|
|
1863
|
+
await client.notify("analyst", {"event": "done"})
|
|
1864
|
+
client.assert_notified("analyst")
|
|
1865
|
+
client.assert_notified("analyst", message_contains={"event": "done"})
|
|
1866
|
+
```
|
|
1867
|
+
|
|
1868
|
+
---
|
|
1869
|
+
|
|
1870
|
+
#### `assert_requested(target, message_contains=None)`
|
|
1871
|
+
|
|
1872
|
+
Assert a request was sent to target.
|
|
1873
|
+
|
|
1874
|
+
```python
|
|
1875
|
+
await client.request("analyst", {"query": "test"})
|
|
1876
|
+
client.assert_requested("analyst")
|
|
1877
|
+
```
|
|
1878
|
+
|
|
1879
|
+
---
|
|
1880
|
+
|
|
1881
|
+
#### `assert_broadcasted(message_contains=None)`
|
|
1882
|
+
|
|
1883
|
+
Assert a broadcast was sent.
|
|
1884
|
+
|
|
1885
|
+
```python
|
|
1886
|
+
await client.broadcast({"alert": "important"})
|
|
1887
|
+
client.assert_broadcasted()
|
|
1888
|
+
client.assert_broadcasted(message_contains={"alert": "important"})
|
|
1889
|
+
```
|
|
1890
|
+
|
|
1891
|
+
---
|
|
1892
|
+
|
|
1893
|
+
#### Tracking Methods
|
|
1894
|
+
|
|
1895
|
+
#### `get_sent_notifications() -> List[dict]`
|
|
1896
|
+
|
|
1897
|
+
Get all notifications sent during test.
|
|
1898
|
+
|
|
1899
|
+
---
|
|
1900
|
+
|
|
1901
|
+
#### `get_sent_requests() -> List[dict]`
|
|
1902
|
+
|
|
1903
|
+
Get all requests sent during test.
|
|
1904
|
+
|
|
1905
|
+
---
|
|
1906
|
+
|
|
1907
|
+
#### `get_sent_broadcasts() -> List[dict]`
|
|
1908
|
+
|
|
1909
|
+
Get all broadcasts sent during test.
|
|
1910
|
+
|
|
1911
|
+
---
|
|
1912
|
+
|
|
1913
|
+
#### `reset()`
|
|
1914
|
+
|
|
1915
|
+
Clear all tracking state (notifications, requests, broadcasts, mock responses).
|
|
1916
|
+
|
|
1917
|
+
```python
|
|
1918
|
+
client.reset()
|
|
1919
|
+
assert len(client.get_sent_notifications()) == 0
|
|
1920
|
+
```
|
|
1921
|
+
|
|
1922
|
+
---
|
|
1923
|
+
|
|
1924
|
+
### Testing Pattern Example
|
|
1925
|
+
|
|
1926
|
+
```python
|
|
1927
|
+
import pytest
|
|
1928
|
+
from jarviscore.testing import MockMesh
|
|
1929
|
+
from jarviscore.profiles import CustomAgent
|
|
1930
|
+
|
|
1931
|
+
class MyAgent(CustomAgent):
|
|
1932
|
+
role = "processor"
|
|
1933
|
+
capabilities = ["processing"]
|
|
1934
|
+
|
|
1935
|
+
async def on_peer_request(self, msg):
|
|
1936
|
+
# Ask analyst for help
|
|
1937
|
+
analysis = await self.peers.request("analyst", {"data": msg.data})
|
|
1938
|
+
return {"processed": True, "analysis": analysis}
|
|
1939
|
+
|
|
1940
|
+
@pytest.mark.asyncio
|
|
1941
|
+
async def test_processor_delegates_to_analyst():
|
|
1942
|
+
mesh = MockMesh()
|
|
1943
|
+
mesh.add(MyAgent)
|
|
1944
|
+
await mesh.start()
|
|
1945
|
+
|
|
1946
|
+
processor = mesh.get_agent("processor")
|
|
1947
|
+
|
|
1948
|
+
# Configure mock response
|
|
1949
|
+
processor.peers.set_mock_response("analyst", {"result": "analyzed"})
|
|
1950
|
+
|
|
1951
|
+
# Test the flow
|
|
1952
|
+
response = await processor.peers.request("analyst", {"test": "data"})
|
|
1953
|
+
|
|
1954
|
+
# Verify
|
|
1955
|
+
assert response["result"] == "analyzed"
|
|
1956
|
+
processor.peers.assert_requested("analyst")
|
|
1957
|
+
|
|
1958
|
+
await mesh.stop()
|
|
1959
|
+
```
|
|
1960
|
+
|
|
1961
|
+
---
|
|
1962
|
+
|
|
1434
1963
|
## Version
|
|
1435
1964
|
|
|
1436
|
-
API Reference for JarvisCore v0.3.
|
|
1965
|
+
API Reference for JarvisCore v0.3.2
|
|
1437
1966
|
|
|
1438
|
-
Last Updated: 2026-
|
|
1967
|
+
Last Updated: 2026-02-03
|