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
@@ -563,4 +563,4 @@ If significantly slower:
563
563
 
564
564
  ## Version
565
565
 
566
- Troubleshooting Guide for JarvisCore v0.2.1
566
+ Troubleshooting Guide for JarvisCore v0.3.2
@@ -19,9 +19,14 @@ Practical guide to building agent systems with JarvisCore.
19
19
  11. [FastAPI Integration (v0.3.0)](#fastapi-integration-v030)
20
20
  12. [Cloud Deployment (v0.3.0)](#cloud-deployment-v030)
21
21
  13. [Cognitive Discovery (v0.3.0)](#cognitive-discovery-v030)
22
- 14. [Best Practices](#best-practices)
23
- 15. [Common Patterns](#common-patterns)
24
- 16. [Troubleshooting](#troubleshooting)
22
+ 14. [Session Context (v0.3.2)](#session-context-v032)
23
+ 15. [Async Requests (v0.3.2)](#async-requests-v032)
24
+ 16. [Load Balancing (v0.3.2)](#load-balancing-v032)
25
+ 17. [Mesh Diagnostics (v0.3.2)](#mesh-diagnostics-v032)
26
+ 18. [Testing with MockMesh (v0.3.2)](#testing-with-mockmesh-v032)
27
+ 19. [Best Practices](#best-practices)
28
+ 20. [Common Patterns](#common-patterns)
29
+ 21. [Troubleshooting](#troubleshooting)
25
30
 
26
31
  ---
27
32
 
@@ -146,8 +151,7 @@ mesh = Mesh(mode="distributed", config={'bind_port': 7950})
146
151
  | Profile | Best For | How It Works |
147
152
  |---------|----------|--------------|
148
153
  | **AutoAgent** | Rapid prototyping | LLM generates + executes code from prompts |
149
- | **CustomAgent** | Existing code | You provide `execute_task()` or `run()` |
150
- | **ListenerAgent** | API-first agents | Just implement `on_peer_request()` handlers |
154
+ | **CustomAgent** | Your own code | Implement `on_peer_request()` for P2P or `execute_task()` for workflows |
151
155
 
152
156
  See [AutoAgent Guide](AUTOAGENT_GUIDE.md) and [CustomAgent Guide](CUSTOMAGENT_GUIDE.md) for details.
153
157
 
@@ -227,7 +231,7 @@ asyncio.run(data_analyst_demo())
227
231
 
228
232
  ## Custom Profile Tutorial
229
233
 
230
- The **Custom Profile** (decorator/wrap approach) is deprecated. Use **CustomAgent** instead.
234
+ Use **CustomAgent** profile.
231
235
 
232
236
  See [CustomAgent Guide](CUSTOMAGENT_GUIDE.md) for:
233
237
  - Converting standalone agents to JarvisCore
@@ -546,10 +550,10 @@ Deploy agents as FastAPI services with minimal boilerplate:
546
550
 
547
551
  ```python
548
552
  from fastapi import FastAPI, Request
549
- from jarviscore.profiles import ListenerAgent
553
+ from jarviscore.profiles import CustomAgent
550
554
  from jarviscore.integrations.fastapi import JarvisLifespan
551
555
 
552
- class ProcessorAgent(ListenerAgent):
556
+ class ProcessorAgent(CustomAgent):
553
557
  role = "processor"
554
558
  capabilities = ["processing"]
555
559
 
@@ -583,9 +587,9 @@ Deploy agents to containers without a central orchestrator:
583
587
  ```python
584
588
  # In your container entrypoint
585
589
  import asyncio
586
- from jarviscore.profiles import ListenerAgent
590
+ from jarviscore.profiles import CustomAgent
587
591
 
588
- class MyAgent(ListenerAgent):
592
+ class MyAgent(CustomAgent):
589
593
  role = "worker"
590
594
  capabilities = ["processing"]
591
595
 
@@ -680,6 +684,282 @@ Use the `ask_peer` tool to delegate tasks to these specialists.
680
684
 
681
685
  ---
682
686
 
687
+ ## Session Context (v0.3.2)
688
+
689
+ Pass metadata through your message flows for tracing, priority, and session tracking:
690
+
691
+ ### Basic Usage
692
+
693
+ ```python
694
+ # Send request with context
695
+ response = await self.peers.request(
696
+ "analyst",
697
+ {"query": "analyze sales data"},
698
+ context={"mission_id": "m-123", "priority": "high", "user_id": "u-456"}
699
+ )
700
+
701
+ # Context is available in the handler
702
+ async def on_peer_request(self, msg):
703
+ mission_id = msg.context.get("mission_id") # "m-123"
704
+ print(f"Processing request for mission: {mission_id}")
705
+ return {"result": "analysis complete"}
706
+ ```
707
+
708
+ ### Auto-Propagation
709
+
710
+ When you `respond()`, context automatically propagates from the request:
711
+
712
+ ```python
713
+ async def on_peer_request(self, msg):
714
+ # msg.context = {"mission_id": "m-123", ...}
715
+ result = process(msg.data)
716
+
717
+ # Context auto-propagates - no need to pass it!
718
+ await self.peers.respond(msg, {"result": result})
719
+
720
+ # Or override with custom context
721
+ await self.peers.respond(msg, {"result": result},
722
+ context={"status": "completed"})
723
+ ```
724
+
725
+ ### All Methods Support Context
726
+
727
+ ```python
728
+ # notify
729
+ await self.peers.notify("logger", {"event": "started"}, context={"trace_id": "t-1"})
730
+
731
+ # request
732
+ response = await self.peers.request("worker", {"task": "..."}, context={"priority": "low"})
733
+
734
+ # broadcast
735
+ await self.peers.broadcast({"alert": "system ready"}, context={"source": "coordinator"})
736
+
737
+ # ask_async
738
+ req_id = await self.peers.ask_async("analyst", {"q": "..."}, context={"batch_id": "b-1"})
739
+ ```
740
+
741
+ ---
742
+
743
+ ## Async Requests (v0.3.2)
744
+
745
+ Fire-and-collect pattern for parallel requests without blocking:
746
+
747
+ ### Basic Pattern
748
+
749
+ ```python
750
+ # Fire off multiple requests (non-blocking)
751
+ request_ids = []
752
+ for analyst in self.peers.discover(role="analyst"):
753
+ req_id = await self.peers.ask_async(analyst.agent_id, {"task": "analyze"})
754
+ request_ids.append(req_id)
755
+
756
+ # Do other work while analysts process...
757
+ await self.do_other_work()
758
+
759
+ # Collect responses
760
+ results = []
761
+ for req_id in request_ids:
762
+ response = await self.peers.check_inbox(req_id, timeout=10)
763
+ if response:
764
+ results.append(response)
765
+ ```
766
+
767
+ ### API Reference
768
+
769
+ ```python
770
+ # Send async request - returns immediately
771
+ req_id = await self.peers.ask_async(target, message, timeout=120, context=None)
772
+
773
+ # Check for response (non-blocking if timeout=0)
774
+ response = await self.peers.check_inbox(req_id, timeout=0)
775
+
776
+ # Check with wait
777
+ response = await self.peers.check_inbox(req_id, timeout=5)
778
+
779
+ # List pending requests
780
+ pending = self.peers.get_pending_async_requests()
781
+ # [{"request_id": "...", "target": "analyst", "sent_at": 1234567890.0}]
782
+
783
+ # Clear inbox
784
+ self.peers.clear_inbox(req_id) # Specific
785
+ self.peers.clear_inbox() # All
786
+ ```
787
+
788
+ ---
789
+
790
+ ## Load Balancing (v0.3.2)
791
+
792
+ Distribute requests across multiple peers with discovery strategies:
793
+
794
+ ### Strategies
795
+
796
+ ```python
797
+ # Default: first in discovery order
798
+ peers = self.peers.discover(role="worker", strategy="first")
799
+
800
+ # Random: shuffle for basic load distribution
801
+ peers = self.peers.discover(role="worker", strategy="random")
802
+
803
+ # Round-robin: rotate through peers on each call
804
+ peers = self.peers.discover(role="worker", strategy="round_robin")
805
+
806
+ # Least-recent: prefer peers not used recently
807
+ peers = self.peers.discover(role="worker", strategy="least_recent")
808
+ ```
809
+
810
+ ### Convenience Method
811
+
812
+ ```python
813
+ # Get single peer with strategy
814
+ worker = self.peers.discover_one(role="worker", strategy="round_robin")
815
+ if worker:
816
+ await self.peers.request(worker.agent_id, {"task": "..."})
817
+ ```
818
+
819
+ ### Track Usage for least_recent
820
+
821
+ ```python
822
+ peer = self.peers.discover_one(role="worker", strategy="least_recent")
823
+ response = await self.peers.request(peer.agent_id, {"task": "..."})
824
+
825
+ # Update usage timestamp after successful communication
826
+ self.peers.record_peer_usage(peer.agent_id)
827
+ ```
828
+
829
+ ### Example: Round-Robin Work Distribution
830
+
831
+ ```python
832
+ async def distribute_tasks(self, tasks):
833
+ results = []
834
+ for task in tasks:
835
+ # Each call rotates to next worker
836
+ worker = self.peers.discover_one(role="worker", strategy="round_robin")
837
+ if worker:
838
+ response = await self.peers.request(worker.agent_id, {"task": task})
839
+ results.append(response)
840
+ return results
841
+ ```
842
+
843
+ ---
844
+
845
+ ## Mesh Diagnostics (v0.3.2)
846
+
847
+ Monitor mesh health and debug connectivity issues:
848
+
849
+ ### Get Diagnostics
850
+
851
+ ```python
852
+ diag = mesh.get_diagnostics()
853
+
854
+ print(f"Mode: {diag['local_node']['mode']}")
855
+ print(f"Status: {diag['connectivity_status']}")
856
+ print(f"Agents: {diag['local_node']['agent_count']}")
857
+
858
+ for agent in diag['local_agents']:
859
+ print(f" - {agent['role']}: {agent['capabilities']}")
860
+
861
+ for peer in diag['known_peers']:
862
+ print(f" - {peer['role']} @ {peer['node_id']}: {peer['status']}")
863
+ ```
864
+
865
+ ### Connectivity Status Values
866
+
867
+ | Status | Meaning |
868
+ |--------|---------|
869
+ | `healthy` | P2P active with connected peers |
870
+ | `isolated` | P2P active but no peers found |
871
+ | `degraded` | Some connectivity issues |
872
+ | `not_started` | Mesh not yet started |
873
+ | `local_only` | Autonomous mode (no P2P) |
874
+
875
+ ### FastAPI Health Endpoint
876
+
877
+ ```python
878
+ @app.get("/health")
879
+ async def health(request: Request):
880
+ mesh = request.app.state.jarvis_mesh
881
+ diag = mesh.get_diagnostics()
882
+ return {
883
+ "status": diag["connectivity_status"],
884
+ "agents": diag["local_node"]["agent_count"],
885
+ "peers": len(diag["known_peers"])
886
+ }
887
+ ```
888
+
889
+ ---
890
+
891
+ ## Testing with MockMesh (v0.3.2)
892
+
893
+ Unit test your agents without real P2P infrastructure:
894
+
895
+ ### Basic Setup
896
+
897
+ ```python
898
+ import pytest
899
+ from jarviscore.testing import MockMesh
900
+ from jarviscore.profiles import CustomAgent
901
+
902
+ class MyAgent(CustomAgent):
903
+ role = "processor"
904
+ capabilities = ["processing"]
905
+
906
+ async def on_peer_request(self, msg):
907
+ # Delegate to analyst
908
+ analysis = await self.peers.request("analyst", {"data": msg.data})
909
+ return {"processed": True, "analysis": analysis}
910
+
911
+ @pytest.mark.asyncio
912
+ async def test_processor_delegates():
913
+ mesh = MockMesh()
914
+ mesh.add(MyAgent)
915
+ await mesh.start()
916
+
917
+ agent = mesh.get_agent("processor")
918
+
919
+ # Configure mock response for analyst
920
+ agent.peers.set_mock_response("analyst", {"result": "analyzed"})
921
+
922
+ # Test the agent
923
+ response = await agent.peers.request("analyst", {"test": "data"})
924
+
925
+ # Verify
926
+ assert response["result"] == "analyzed"
927
+ agent.peers.assert_requested("analyst")
928
+
929
+ await mesh.stop()
930
+ ```
931
+
932
+ ### MockPeerClient Features
933
+
934
+ ```python
935
+ # Configure responses
936
+ agent.peers.set_mock_response("analyst", {"result": "..."})
937
+ agent.peers.set_default_response({"status": "ok"})
938
+
939
+ # Custom handler for dynamic responses
940
+ async def handler(target, message, context):
941
+ return {"echo": message, "target": target}
942
+ agent.peers.set_request_handler(handler)
943
+
944
+ # Inject messages for handler testing
945
+ from jarviscore.p2p.messages import MessageType
946
+ agent.peers.inject_message("sender", MessageType.REQUEST, {"data": "test"})
947
+
948
+ # Assertions
949
+ agent.peers.assert_notified("target")
950
+ agent.peers.assert_requested("analyst", message_contains={"query": "test"})
951
+ agent.peers.assert_broadcasted()
952
+
953
+ # Track what was sent
954
+ notifications = agent.peers.get_sent_notifications()
955
+ requests = agent.peers.get_sent_requests()
956
+
957
+ # Reset between tests
958
+ agent.peers.reset()
959
+ ```
960
+
961
+ ---
962
+
683
963
  ## Best Practices
684
964
 
685
965
  ### 1. Always Use Context Managers
@@ -905,6 +1185,6 @@ mesh = Mesh(config=config)
905
1185
 
906
1186
  ## Version
907
1187
 
908
- User Guide for JarvisCore v0.3.0
1188
+ User Guide for JarvisCore v0.3.2
909
1189
 
910
- Last Updated: 2026-01-29
1190
+ Last Updated: 2026-02-03
@@ -49,9 +49,9 @@ class JarvisLifespan:
49
49
  Example - Single Agent:
50
50
  from fastapi import FastAPI, Request
51
51
  from jarviscore.integrations.fastapi import JarvisLifespan
52
- from jarviscore.profiles import ListenerAgent
52
+ from jarviscore.profiles import CustomAgent
53
53
 
54
- class MyAgent(ListenerAgent):
54
+ class MyAgent(CustomAgent):
55
55
  role = "processor"
56
56
  capabilities = ["processing"]
57
57
 
@@ -220,9 +220,9 @@ def create_jarvis_app(
220
220
 
221
221
  Example:
222
222
  from jarviscore.integrations.fastapi import create_jarvis_app
223
- from jarviscore.profiles import ListenerAgent
223
+ from jarviscore.profiles import CustomAgent
224
224
 
225
- class MyAgent(ListenerAgent):
225
+ class MyAgent(CustomAgent):
226
226
  role = "processor"
227
227
  capabilities = ["processing"]
228
228
 
@@ -702,7 +702,8 @@ class P2PCoordinator:
702
702
  type=MessageType.NOTIFY,
703
703
  data=payload.get('data', {}),
704
704
  correlation_id=payload.get('correlation_id'),
705
- timestamp=payload.get('timestamp', 0)
705
+ timestamp=payload.get('timestamp', 0),
706
+ context=payload.get('context')
706
707
  )
707
708
 
708
709
  await target_client._deliver_message(incoming)
@@ -714,15 +715,34 @@ class P2PCoordinator:
714
715
  async def _handle_peer_request(self, sender, message):
715
716
  """Handle peer request message (expects response)."""
716
717
  try:
717
- payload = message.get('payload', {})
718
+ logger.info(f"[COORDINATOR] Received PEER_REQUEST from {sender}")
719
+
720
+ # Parse payload - it comes as JSON string in message['payload']
721
+ import json
722
+ payload_raw = message.get('payload', {})
723
+ if isinstance(payload_raw, str):
724
+ try:
725
+ payload = json.loads(payload_raw)
726
+ logger.info(f"[COORDINATOR] Parsed JSON payload")
727
+ except json.JSONDecodeError as e:
728
+ logger.error(f"[COORDINATOR] Failed to parse payload JSON: {e}")
729
+ return
730
+ else:
731
+ payload = payload_raw
732
+
718
733
  target = payload.get('target')
734
+ logger.info(f"[COORDINATOR] Target: {target}, Payload keys: {list(payload.keys())}")
719
735
 
720
736
  # Find target agent's PeerClient
721
737
  target_client = self._find_peer_client_by_role_or_id(target)
722
738
  if not target_client:
723
739
  logger.warning(f"Peer request: target '{target}' not found")
740
+ logger.warning(f"Available agents: {[a.agent_id for a in self.agents]}")
741
+ logger.warning(f"Available peer clients: {list(self._agent_peer_clients.keys())}")
724
742
  return
725
743
 
744
+ logger.info(f"[COORDINATOR] Found target_client for {target}, delivering message...")
745
+
726
746
  # Create incoming message and deliver
727
747
  incoming = IncomingMessage(
728
748
  sender=payload.get('sender', sender),
@@ -730,19 +750,27 @@ class P2PCoordinator:
730
750
  type=MessageType.REQUEST,
731
751
  data=payload.get('data', {}),
732
752
  correlation_id=payload.get('correlation_id'),
733
- timestamp=payload.get('timestamp', 0)
753
+ timestamp=payload.get('timestamp', 0),
754
+ context=payload.get('context')
734
755
  )
735
756
 
736
757
  await target_client._deliver_message(incoming)
737
- logger.debug(f"Delivered peer request to {target}")
758
+ logger.info(f"[COORDINATOR] Delivered peer request to {target}")
738
759
 
739
760
  except Exception as e:
740
- logger.error(f"Error handling peer request: {e}")
761
+ logger.error(f"Error handling peer request: {e}", exc_info=True)
741
762
 
742
763
  async def _handle_peer_response(self, sender, message):
743
764
  """Handle peer response message."""
744
765
  try:
745
- payload = message.get('payload', {})
766
+ # Parse payload - it comes as JSON string
767
+ import json
768
+ payload_raw = message.get('payload', {})
769
+ if isinstance(payload_raw, str):
770
+ payload = json.loads(payload_raw)
771
+ else:
772
+ payload = payload_raw
773
+
746
774
  target = payload.get('target')
747
775
 
748
776
  # Find target agent's PeerClient
@@ -758,7 +786,8 @@ class P2PCoordinator:
758
786
  type=MessageType.RESPONSE,
759
787
  data=payload.get('data', {}),
760
788
  correlation_id=payload.get('correlation_id'),
761
- timestamp=payload.get('timestamp', 0)
789
+ timestamp=payload.get('timestamp', 0),
790
+ context=payload.get('context')
762
791
  )
763
792
 
764
793
  await target_client._deliver_message(incoming)
@@ -52,6 +52,7 @@ class IncomingMessage:
52
52
  data: Message payload
53
53
  correlation_id: ID linking request to response (for request-response pattern)
54
54
  timestamp: When the message was sent
55
+ context: Optional metadata for the message (mission_id, priority, trace_id, etc.)
55
56
  """
56
57
  sender: str
57
58
  sender_node: str
@@ -59,6 +60,7 @@ class IncomingMessage:
59
60
  data: Dict[str, Any]
60
61
  correlation_id: Optional[str] = None
61
62
  timestamp: float = field(default_factory=time.time)
63
+ context: Optional[Dict[str, Any]] = None
62
64
 
63
65
  @property
64
66
  def is_request(self) -> bool:
@@ -77,6 +79,16 @@ class OutgoingMessage:
77
79
  A message to be sent to a peer agent.
78
80
 
79
81
  Used internally by PeerClient for message construction.
82
+
83
+ Attributes:
84
+ target: Target agent role or ID
85
+ type: Message type
86
+ data: Message payload
87
+ correlation_id: ID linking request to response
88
+ timestamp: When the message was created
89
+ sender: Agent ID of sender (filled by PeerClient)
90
+ sender_node: P2P node ID of sender (filled by PeerClient)
91
+ context: Optional metadata for the message (mission_id, priority, trace_id, etc.)
80
92
  """
81
93
  target: str # Target agent role or ID
82
94
  type: MessageType
@@ -85,3 +97,4 @@ class OutgoingMessage:
85
97
  timestamp: float = field(default_factory=time.time)
86
98
  sender: str = "" # Filled in by PeerClient
87
99
  sender_node: str = "" # Filled in by PeerClient
100
+ context: Optional[Dict[str, Any]] = None