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/core/mesh.py
CHANGED
|
@@ -256,9 +256,18 @@ class Mesh:
|
|
|
256
256
|
await self._p2p_coordinator.start()
|
|
257
257
|
self._logger.info("✓ P2P coordinator started")
|
|
258
258
|
|
|
259
|
+
# Wait for mesh to stabilize before announcing
|
|
260
|
+
# Increased delay to ensure SWIM fully connects all nodes
|
|
261
|
+
await asyncio.sleep(5)
|
|
262
|
+
self._logger.info("Waited for mesh stabilization")
|
|
263
|
+
|
|
259
264
|
# Announce capabilities to network
|
|
260
265
|
await self._p2p_coordinator.announce_capabilities()
|
|
261
266
|
self._logger.info("✓ Capabilities announced to mesh")
|
|
267
|
+
|
|
268
|
+
# Request capabilities from existing peers (for late-joiners)
|
|
269
|
+
await self._p2p_coordinator.request_peer_capabilities()
|
|
270
|
+
self._logger.info("✓ Requested capabilities from existing peers")
|
|
262
271
|
|
|
263
272
|
# Inject PeerClients for p2p mode
|
|
264
273
|
if self.mode == MeshMode.P2P:
|
|
@@ -615,6 +624,155 @@ class Mesh:
|
|
|
615
624
|
"""
|
|
616
625
|
return self._capability_index.get(capability, [])
|
|
617
626
|
|
|
627
|
+
# ─────────────────────────────────────────────────────────────────
|
|
628
|
+
# DIAGNOSTICS
|
|
629
|
+
# ─────────────────────────────────────────────────────────────────
|
|
630
|
+
|
|
631
|
+
def get_diagnostics(self) -> Dict[str, Any]:
|
|
632
|
+
"""
|
|
633
|
+
Get diagnostic information about the mesh and P2P connectivity.
|
|
634
|
+
|
|
635
|
+
Useful for debugging P2P issues, monitoring mesh health,
|
|
636
|
+
and understanding the current state of the distributed system.
|
|
637
|
+
|
|
638
|
+
Returns:
|
|
639
|
+
Dictionary containing:
|
|
640
|
+
- local_node: This node's configuration and status
|
|
641
|
+
- known_peers: List of discovered remote peers
|
|
642
|
+
- local_agents: List of local agents with capabilities
|
|
643
|
+
- connectivity_status: Overall health assessment
|
|
644
|
+
- keepalive_status: Keepalive manager health (if P2P enabled)
|
|
645
|
+
- swim_status: SWIM protocol status (if P2P enabled)
|
|
646
|
+
- capability_map: Mapping of capabilities to agent IDs
|
|
647
|
+
|
|
648
|
+
Example:
|
|
649
|
+
diagnostics = mesh.get_diagnostics()
|
|
650
|
+
print(f"Status: {diagnostics['connectivity_status']}")
|
|
651
|
+
for peer in diagnostics['known_peers']:
|
|
652
|
+
print(f" {peer['role']} at {peer['node_id']}: {peer['status']}")
|
|
653
|
+
"""
|
|
654
|
+
result = {
|
|
655
|
+
"local_node": self._get_local_node_info(),
|
|
656
|
+
"known_peers": self._get_peer_list(),
|
|
657
|
+
"local_agents": self._get_local_agents_info(),
|
|
658
|
+
"connectivity_status": self._assess_connectivity_status()
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
# Add P2P-specific diagnostics if coordinator is available
|
|
662
|
+
if self._p2p_coordinator:
|
|
663
|
+
result["keepalive_status"] = self._get_keepalive_status()
|
|
664
|
+
result["swim_status"] = self._get_swim_status()
|
|
665
|
+
result["capability_map"] = self._get_capability_map()
|
|
666
|
+
|
|
667
|
+
return result
|
|
668
|
+
|
|
669
|
+
def _get_local_node_info(self) -> Dict[str, Any]:
|
|
670
|
+
"""Get local node information."""
|
|
671
|
+
info = {
|
|
672
|
+
"mode": self.mode.value,
|
|
673
|
+
"started": self._started,
|
|
674
|
+
"agent_count": len(self.agents)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if self._p2p_coordinator and self._p2p_coordinator.swim_manager:
|
|
678
|
+
addr = self._p2p_coordinator.swim_manager.bind_addr
|
|
679
|
+
if addr:
|
|
680
|
+
info["bind_address"] = f"{addr[0]}:{addr[1]}"
|
|
681
|
+
|
|
682
|
+
return info
|
|
683
|
+
|
|
684
|
+
def _get_peer_list(self) -> List[Dict[str, Any]]:
|
|
685
|
+
"""Get list of known remote peers."""
|
|
686
|
+
peers = []
|
|
687
|
+
|
|
688
|
+
if self._p2p_coordinator:
|
|
689
|
+
for agent in self._p2p_coordinator.list_remote_agents():
|
|
690
|
+
peers.append({
|
|
691
|
+
"role": agent.get("role", "unknown"),
|
|
692
|
+
"agent_id": agent.get("agent_id", "unknown"),
|
|
693
|
+
"node_id": agent.get("node_id", "unknown"),
|
|
694
|
+
"capabilities": agent.get("capabilities", []),
|
|
695
|
+
"status": "connected"
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
return peers
|
|
699
|
+
|
|
700
|
+
def _get_local_agents_info(self) -> List[Dict[str, Any]]:
|
|
701
|
+
"""Get information about local agents."""
|
|
702
|
+
return [
|
|
703
|
+
{
|
|
704
|
+
"role": agent.role,
|
|
705
|
+
"agent_id": agent.agent_id,
|
|
706
|
+
"capabilities": list(agent.capabilities),
|
|
707
|
+
"description": getattr(agent, 'description', ''),
|
|
708
|
+
"has_peers": hasattr(agent, 'peers') and agent.peers is not None
|
|
709
|
+
}
|
|
710
|
+
for agent in self.agents
|
|
711
|
+
]
|
|
712
|
+
|
|
713
|
+
def _assess_connectivity_status(self) -> str:
|
|
714
|
+
"""
|
|
715
|
+
Assess overall connectivity status.
|
|
716
|
+
|
|
717
|
+
Returns:
|
|
718
|
+
"healthy" - P2P fully operational with peers
|
|
719
|
+
"isolated" - No peers connected
|
|
720
|
+
"degraded" - Some connectivity issues detected
|
|
721
|
+
"not_started" - Mesh not yet started
|
|
722
|
+
"local_only" - Not in distributed/p2p mode
|
|
723
|
+
"""
|
|
724
|
+
if not self._started:
|
|
725
|
+
return "not_started"
|
|
726
|
+
|
|
727
|
+
if self.mode == MeshMode.AUTONOMOUS:
|
|
728
|
+
return "local_only"
|
|
729
|
+
|
|
730
|
+
if not self._p2p_coordinator:
|
|
731
|
+
return "local_only"
|
|
732
|
+
|
|
733
|
+
# Check SWIM health
|
|
734
|
+
if self._p2p_coordinator.swim_manager:
|
|
735
|
+
if not self._p2p_coordinator.swim_manager.is_healthy():
|
|
736
|
+
return "degraded"
|
|
737
|
+
|
|
738
|
+
# Check for connected peers
|
|
739
|
+
remote_agents = self._p2p_coordinator.list_remote_agents()
|
|
740
|
+
if not remote_agents:
|
|
741
|
+
return "isolated"
|
|
742
|
+
|
|
743
|
+
# Check keepalive health if available
|
|
744
|
+
if hasattr(self._p2p_coordinator, 'keepalive_manager') and self._p2p_coordinator.keepalive_manager:
|
|
745
|
+
health = self._p2p_coordinator.keepalive_manager.get_health_status()
|
|
746
|
+
if health.get('circuit_state') == 'OPEN':
|
|
747
|
+
return "degraded"
|
|
748
|
+
|
|
749
|
+
return "healthy"
|
|
750
|
+
|
|
751
|
+
def _get_keepalive_status(self) -> Optional[Dict[str, Any]]:
|
|
752
|
+
"""Get keepalive manager status."""
|
|
753
|
+
if not self._p2p_coordinator:
|
|
754
|
+
return None
|
|
755
|
+
|
|
756
|
+
if hasattr(self._p2p_coordinator, 'keepalive_manager') and self._p2p_coordinator.keepalive_manager:
|
|
757
|
+
return self._p2p_coordinator.keepalive_manager.get_health_status()
|
|
758
|
+
|
|
759
|
+
return None
|
|
760
|
+
|
|
761
|
+
def _get_swim_status(self) -> Optional[Dict[str, Any]]:
|
|
762
|
+
"""Get SWIM protocol status."""
|
|
763
|
+
if not self._p2p_coordinator or not self._p2p_coordinator.swim_manager:
|
|
764
|
+
return None
|
|
765
|
+
|
|
766
|
+
return self._p2p_coordinator.swim_manager.get_status()
|
|
767
|
+
|
|
768
|
+
def _get_capability_map(self) -> Dict[str, List[str]]:
|
|
769
|
+
"""Get the capability to agent_id mapping."""
|
|
770
|
+
if not self._p2p_coordinator:
|
|
771
|
+
return {}
|
|
772
|
+
|
|
773
|
+
# Convert defaultdict to regular dict for serialization
|
|
774
|
+
return dict(self._p2p_coordinator._capability_map)
|
|
775
|
+
|
|
618
776
|
def __repr__(self) -> str:
|
|
619
777
|
"""String representation of mesh."""
|
|
620
778
|
return (
|
|
@@ -28,10 +28,10 @@ import sys
|
|
|
28
28
|
|
|
29
29
|
sys.path.insert(0, '.')
|
|
30
30
|
|
|
31
|
-
from jarviscore.profiles import
|
|
31
|
+
from jarviscore.profiles import CustomAgent
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
class StandaloneProcessor(
|
|
34
|
+
class StandaloneProcessor(CustomAgent):
|
|
35
35
|
"""
|
|
36
36
|
Example standalone agent that joins mesh independently.
|
|
37
37
|
|
|
@@ -143,7 +143,7 @@ async def main():
|
|
|
143
143
|
print("Listening for peer requests...")
|
|
144
144
|
print("Press Ctrl+C to stop.\n")
|
|
145
145
|
|
|
146
|
-
# Run agent (
|
|
146
|
+
# Run agent (CustomAgent's run() handles the message loop)
|
|
147
147
|
try:
|
|
148
148
|
await agent.run()
|
|
149
149
|
except asyncio.CancelledError:
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom Profile Example: Using @jarvis_agent Decorator
|
|
3
|
+
|
|
4
|
+
This example shows how to use the @jarvis_agent decorator to convert
|
|
5
|
+
any Python class into a JarvisCore agent without modifying the class.
|
|
6
|
+
|
|
7
|
+
Use Case: You have existing Python classes/agents and want JarvisCore
|
|
8
|
+
to handle orchestration (data handoff, dependencies, shared memory).
|
|
9
|
+
"""
|
|
10
|
+
import asyncio
|
|
11
|
+
from jarviscore import Mesh, jarvis_agent, JarvisContext
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Example 1: Simple decorator (no context needed)
|
|
15
|
+
@jarvis_agent(role="processor", capabilities=["data_processing"])
|
|
16
|
+
class DataProcessor:
|
|
17
|
+
"""Simple data processor - doubles input values."""
|
|
18
|
+
|
|
19
|
+
def run(self, data):
|
|
20
|
+
"""Process data by doubling values."""
|
|
21
|
+
if isinstance(data, list):
|
|
22
|
+
return {"processed": [x * 2 for x in data]}
|
|
23
|
+
return {"processed": data * 2}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Example 2: Decorator with context access
|
|
27
|
+
@jarvis_agent(role="aggregator", capabilities=["aggregation"])
|
|
28
|
+
class Aggregator:
|
|
29
|
+
"""Aggregates results from previous steps using JarvisContext."""
|
|
30
|
+
|
|
31
|
+
def run(self, task, ctx: JarvisContext):
|
|
32
|
+
"""
|
|
33
|
+
Access previous step results via ctx.previous().
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
task: The task description
|
|
37
|
+
ctx: JarvisContext with memory and dependency access
|
|
38
|
+
"""
|
|
39
|
+
# Get output from a specific previous step
|
|
40
|
+
processed = ctx.previous("step1")
|
|
41
|
+
|
|
42
|
+
if processed:
|
|
43
|
+
data = processed.get("processed", [])
|
|
44
|
+
return {
|
|
45
|
+
"sum": sum(data) if isinstance(data, list) else data,
|
|
46
|
+
"count": len(data) if isinstance(data, list) else 1,
|
|
47
|
+
"source_step": "step1"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {"error": "No previous data found"}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Example 3: Decorator with custom execute method
|
|
54
|
+
@jarvis_agent(role="validator", capabilities=["validation"], execute_method="validate")
|
|
55
|
+
class DataValidator:
|
|
56
|
+
"""Validates data using a custom method name."""
|
|
57
|
+
|
|
58
|
+
def validate(self, data):
|
|
59
|
+
"""Custom execute method - validates input data."""
|
|
60
|
+
if isinstance(data, list):
|
|
61
|
+
return {
|
|
62
|
+
"valid": all(isinstance(x, (int, float)) for x in data),
|
|
63
|
+
"count": len(data),
|
|
64
|
+
"type": "list"
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
"valid": isinstance(data, (int, float)),
|
|
68
|
+
"type": type(data).__name__
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def main():
|
|
73
|
+
"""Run a multi-step workflow with custom profile agents."""
|
|
74
|
+
print("=" * 60)
|
|
75
|
+
print(" Custom Profile Example: @jarvis_agent Decorator")
|
|
76
|
+
print("=" * 60)
|
|
77
|
+
|
|
78
|
+
# Create mesh in autonomous mode
|
|
79
|
+
mesh = Mesh(mode="autonomous")
|
|
80
|
+
|
|
81
|
+
# Add our decorated agents
|
|
82
|
+
mesh.add(DataProcessor)
|
|
83
|
+
mesh.add(Aggregator)
|
|
84
|
+
mesh.add(DataValidator)
|
|
85
|
+
|
|
86
|
+
# Start the mesh
|
|
87
|
+
await mesh.start()
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
# Execute a multi-step workflow
|
|
91
|
+
print("\nExecuting workflow with 3 steps...\n")
|
|
92
|
+
|
|
93
|
+
results = await mesh.workflow("custom-profile-demo", [
|
|
94
|
+
{
|
|
95
|
+
"id": "step1",
|
|
96
|
+
"agent": "processor",
|
|
97
|
+
"task": "Process input data",
|
|
98
|
+
"params": {"data": [1, 2, 3, 4, 5]}
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"id": "step2",
|
|
102
|
+
"agent": "aggregator",
|
|
103
|
+
"task": "Aggregate processed results",
|
|
104
|
+
"depends_on": ["step1"] # Wait for step1
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"id": "step3",
|
|
108
|
+
"agent": "validator",
|
|
109
|
+
"task": "Validate original data",
|
|
110
|
+
"params": {"data": [1, 2, 3, 4, 5]}
|
|
111
|
+
}
|
|
112
|
+
])
|
|
113
|
+
|
|
114
|
+
# Print results
|
|
115
|
+
print("Results:")
|
|
116
|
+
print("-" * 40)
|
|
117
|
+
|
|
118
|
+
for i, result in enumerate(results):
|
|
119
|
+
step_name = ["Processor", "Aggregator", "Validator"][i]
|
|
120
|
+
print(f"\n{step_name} (step{i+1}):")
|
|
121
|
+
print(f" Status: {result.get('status')}")
|
|
122
|
+
print(f" Output: {result.get('output')}")
|
|
123
|
+
|
|
124
|
+
print("\n" + "=" * 60)
|
|
125
|
+
print(" Workflow completed successfully!")
|
|
126
|
+
print("=" * 60)
|
|
127
|
+
|
|
128
|
+
finally:
|
|
129
|
+
# Stop the mesh
|
|
130
|
+
await mesh.stop()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom Profile Example: Using wrap() Function
|
|
3
|
+
|
|
4
|
+
This example shows how to use the wrap() function to convert
|
|
5
|
+
an existing instance into a JarvisCore agent.
|
|
6
|
+
|
|
7
|
+
Use Case: You have an already-instantiated object (like a LangChain
|
|
8
|
+
agent, CrewAI agent, or any configured instance) and want to use it
|
|
9
|
+
with JarvisCore orchestration.
|
|
10
|
+
"""
|
|
11
|
+
import asyncio
|
|
12
|
+
from jarviscore import Mesh, wrap, JarvisContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Simulate an existing "LangChain-like" agent
|
|
16
|
+
class ExternalLLMAgent:
|
|
17
|
+
"""
|
|
18
|
+
Simulates an external LLM agent (like LangChain).
|
|
19
|
+
In real usage, this would be your actual LangChain/CrewAI agent.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, model_name: str, temperature: float = 0.7):
|
|
23
|
+
self.model_name = model_name
|
|
24
|
+
self.temperature = temperature
|
|
25
|
+
print(f" Initialized ExternalLLMAgent with {model_name}")
|
|
26
|
+
|
|
27
|
+
def invoke(self, query: str) -> dict:
|
|
28
|
+
"""LangChain-style invoke method."""
|
|
29
|
+
# Simulate LLM response
|
|
30
|
+
return {
|
|
31
|
+
"answer": f"Response to '{query}' from {self.model_name}",
|
|
32
|
+
"model": self.model_name,
|
|
33
|
+
"tokens_used": len(query.split()) * 10
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Simulate a data processing service
|
|
38
|
+
class DataService:
|
|
39
|
+
"""Simulates an external data processing service."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, api_url: str):
|
|
42
|
+
self.api_url = api_url
|
|
43
|
+
print(f" Initialized DataService with {api_url}")
|
|
44
|
+
|
|
45
|
+
def run(self, data):
|
|
46
|
+
"""Process data through the service."""
|
|
47
|
+
if isinstance(data, list):
|
|
48
|
+
return {
|
|
49
|
+
"transformed": [x ** 2 for x in data],
|
|
50
|
+
"source": self.api_url
|
|
51
|
+
}
|
|
52
|
+
return {"transformed": data ** 2, "source": self.api_url}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Simulate an agent that needs context
|
|
56
|
+
class ContextAwareProcessor:
|
|
57
|
+
"""Agent that uses JarvisContext to access previous results."""
|
|
58
|
+
|
|
59
|
+
def run(self, task, ctx: JarvisContext):
|
|
60
|
+
"""Process with context access."""
|
|
61
|
+
# Get all previous results
|
|
62
|
+
all_previous = ctx.all_previous()
|
|
63
|
+
|
|
64
|
+
summary = {
|
|
65
|
+
"task": task,
|
|
66
|
+
"previous_steps": list(all_previous.keys()),
|
|
67
|
+
"combined_data": {}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for step_id, output in all_previous.items():
|
|
71
|
+
if isinstance(output, dict):
|
|
72
|
+
summary["combined_data"][step_id] = output
|
|
73
|
+
|
|
74
|
+
return summary
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def main():
|
|
78
|
+
"""Demonstrate wrapping existing instances."""
|
|
79
|
+
print("=" * 60)
|
|
80
|
+
print(" Custom Profile Example: wrap() Function")
|
|
81
|
+
print("=" * 60)
|
|
82
|
+
|
|
83
|
+
# Create instances of "external" agents
|
|
84
|
+
print("\nCreating external agent instances...")
|
|
85
|
+
llm_agent = ExternalLLMAgent(model_name="gpt-4-turbo", temperature=0.3)
|
|
86
|
+
data_service = DataService(api_url="https://api.example.com/process")
|
|
87
|
+
context_processor = ContextAwareProcessor()
|
|
88
|
+
|
|
89
|
+
# Wrap them for JarvisCore
|
|
90
|
+
print("\nWrapping instances for JarvisCore...")
|
|
91
|
+
|
|
92
|
+
wrapped_llm = wrap(
|
|
93
|
+
llm_agent,
|
|
94
|
+
role="llm_assistant",
|
|
95
|
+
capabilities=["chat", "qa"],
|
|
96
|
+
execute_method="invoke" # LangChain uses "invoke"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
wrapped_data = wrap(
|
|
100
|
+
data_service,
|
|
101
|
+
role="data_processor",
|
|
102
|
+
capabilities=["data_processing", "transformation"]
|
|
103
|
+
# execute_method auto-detected as "run"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
wrapped_context = wrap(
|
|
107
|
+
context_processor,
|
|
108
|
+
role="context_aggregator",
|
|
109
|
+
capabilities=["aggregation", "summary"]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Create mesh and add wrapped agents
|
|
113
|
+
mesh = Mesh(mode="autonomous")
|
|
114
|
+
mesh.add(wrapped_llm)
|
|
115
|
+
mesh.add(wrapped_data)
|
|
116
|
+
mesh.add(wrapped_context)
|
|
117
|
+
|
|
118
|
+
await mesh.start()
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
print("\nExecuting workflow with wrapped agents...\n")
|
|
122
|
+
|
|
123
|
+
results = await mesh.workflow("wrap-demo", [
|
|
124
|
+
{
|
|
125
|
+
"id": "llm_step",
|
|
126
|
+
"agent": "llm_assistant",
|
|
127
|
+
"task": "What is the capital of France?",
|
|
128
|
+
"params": {"query": "What is the capital of France?"}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"id": "data_step",
|
|
132
|
+
"agent": "data_processor",
|
|
133
|
+
"task": "Transform numbers",
|
|
134
|
+
"params": {"data": [1, 2, 3, 4, 5]}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"id": "summary_step",
|
|
138
|
+
"agent": "context_aggregator",
|
|
139
|
+
"task": "Summarize all results",
|
|
140
|
+
"depends_on": ["llm_step", "data_step"]
|
|
141
|
+
}
|
|
142
|
+
])
|
|
143
|
+
|
|
144
|
+
# Print results
|
|
145
|
+
print("Results:")
|
|
146
|
+
print("-" * 40)
|
|
147
|
+
|
|
148
|
+
step_names = ["LLM Assistant", "Data Processor", "Context Aggregator"]
|
|
149
|
+
for i, result in enumerate(results):
|
|
150
|
+
print(f"\n{step_names[i]}:")
|
|
151
|
+
print(f" Status: {result.get('status')}")
|
|
152
|
+
output = result.get('output', {})
|
|
153
|
+
if isinstance(output, dict):
|
|
154
|
+
for key, value in output.items():
|
|
155
|
+
print(f" {key}: {value}")
|
|
156
|
+
else:
|
|
157
|
+
print(f" Output: {output}")
|
|
158
|
+
|
|
159
|
+
print("\n" + "=" * 60)
|
|
160
|
+
print(" Workflow with wrapped instances completed!")
|
|
161
|
+
print("=" * 60)
|
|
162
|
+
|
|
163
|
+
finally:
|
|
164
|
+
await mesh.stop()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == "__main__":
|
|
168
|
+
asyncio.run(main())
|
|
@@ -1,42 +1,47 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
CustomAgent + Cognitive Discovery Example
|
|
3
3
|
|
|
4
|
-
Demonstrates
|
|
4
|
+
Demonstrates v0.3.0 and v0.3.2 features:
|
|
5
5
|
|
|
6
|
-
1.
|
|
6
|
+
1. CustomAgent - Handler-based P2P agents (no run() loop needed)
|
|
7
7
|
- on_peer_request() handles incoming requests
|
|
8
8
|
- on_peer_notify() handles broadcast notifications
|
|
9
9
|
|
|
10
|
-
2. Cognitive Discovery - Dynamic peer awareness for LLMs
|
|
10
|
+
2. Cognitive Discovery (v0.3.0) - Dynamic peer awareness for LLMs
|
|
11
11
|
- get_cognitive_context() generates LLM-ready peer descriptions
|
|
12
12
|
- No hardcoded agent names in prompts
|
|
13
13
|
- LLM autonomously decides when to delegate
|
|
14
14
|
|
|
15
|
+
3. Session Context (v0.3.2) - Request tracking with metadata
|
|
16
|
+
- Pass context={mission_id, request_id} with peer requests
|
|
17
|
+
- Track requests across agent boundaries for debugging/tracing
|
|
18
|
+
|
|
15
19
|
Usage:
|
|
16
|
-
python examples/
|
|
20
|
+
python examples/customagent_cognitive_discovery_example.py
|
|
17
21
|
|
|
18
22
|
Prerequisites:
|
|
19
23
|
- .env file with CLAUDE_API_KEY (or other LLM provider)
|
|
20
24
|
"""
|
|
21
25
|
import asyncio
|
|
22
26
|
import sys
|
|
27
|
+
import uuid
|
|
23
28
|
from pathlib import Path
|
|
24
29
|
|
|
25
30
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
26
31
|
|
|
27
32
|
from jarviscore import Mesh
|
|
28
|
-
from jarviscore.profiles import
|
|
33
|
+
from jarviscore.profiles import CustomAgent
|
|
29
34
|
|
|
30
35
|
|
|
31
36
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
32
37
|
# SPECIALIST AGENT - Responds to requests from other agents
|
|
33
38
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
34
39
|
|
|
35
|
-
class AnalystAgent(
|
|
40
|
+
class AnalystAgent(CustomAgent):
|
|
36
41
|
"""
|
|
37
42
|
Specialist agent that handles analysis requests.
|
|
38
43
|
|
|
39
|
-
Uses
|
|
44
|
+
Uses CustomAgent profile - just implement handlers, no run() loop needed.
|
|
40
45
|
"""
|
|
41
46
|
role = "analyst"
|
|
42
47
|
capabilities = ["data_analysis", "statistics", "insights"]
|
|
@@ -45,13 +50,21 @@ class AnalystAgent(ListenerAgent):
|
|
|
45
50
|
async def on_peer_request(self, msg):
|
|
46
51
|
"""Handle incoming analysis requests."""
|
|
47
52
|
query = msg.data.get("question", msg.data.get("query", ""))
|
|
48
|
-
|
|
53
|
+
|
|
54
|
+
# v0.3.2: Access session context for request tracking
|
|
55
|
+
context = msg.context or {}
|
|
56
|
+
mission_id = context.get("mission_id", "unknown")
|
|
57
|
+
request_id = context.get("request_id", "unknown")
|
|
58
|
+
|
|
59
|
+
print(f"\n[Analyst] Received request (mission={mission_id[:8]}..., req={request_id[:8]}...)")
|
|
60
|
+
print(f"[Analyst] Query: {query[:50]}...")
|
|
49
61
|
|
|
50
62
|
# Simulate analysis (in real usage, this would use an LLM)
|
|
51
63
|
result = {
|
|
52
64
|
"analysis": f"Analysis of '{query}': The data shows positive trends.",
|
|
53
65
|
"confidence": 0.85,
|
|
54
|
-
"insights": ["Trend is upward", "Growth rate: 15%", "Recommendation: Continue"]
|
|
66
|
+
"insights": ["Trend is upward", "Growth rate: 15%", "Recommendation: Continue"],
|
|
67
|
+
"context": {"mission_id": mission_id, "request_id": request_id} # Echo back for tracing
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
print(f"[Analyst] Sending response with {len(result['insights'])} insights")
|
|
@@ -62,7 +75,7 @@ class AnalystAgent(ListenerAgent):
|
|
|
62
75
|
# COORDINATOR AGENT - Uses LLM with cognitive discovery
|
|
63
76
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
64
77
|
|
|
65
|
-
class CoordinatorAgent(
|
|
78
|
+
class CoordinatorAgent(CustomAgent):
|
|
66
79
|
"""
|
|
67
80
|
Coordinator agent that uses LLM with dynamic peer discovery.
|
|
68
81
|
|
|
@@ -78,6 +91,9 @@ class CoordinatorAgent(ListenerAgent):
|
|
|
78
91
|
async def setup(self):
|
|
79
92
|
await super().setup()
|
|
80
93
|
self.llm = self._create_llm_client()
|
|
94
|
+
# v0.3.2: Track missions for context propagation
|
|
95
|
+
self.mission_id = str(uuid.uuid4())
|
|
96
|
+
self.request_counter = 0
|
|
81
97
|
|
|
82
98
|
def _create_llm_client(self):
|
|
83
99
|
"""Create LLM client with fallback to mock."""
|
|
@@ -167,10 +183,22 @@ Never try to do analysis yourself - always delegate to the analyst."""
|
|
|
167
183
|
# Mock: simulate LLM deciding to delegate
|
|
168
184
|
if any(word in user_query.lower() for word in ["analyze", "analysis", "statistics", "data"]):
|
|
169
185
|
print("[Coordinator] Mock LLM decides to delegate to analyst")
|
|
186
|
+
|
|
187
|
+
# v0.3.2: Generate request context for tracking
|
|
188
|
+
self.request_counter += 1
|
|
189
|
+
request_context = {
|
|
190
|
+
"mission_id": self.mission_id,
|
|
191
|
+
"request_id": str(uuid.uuid4()),
|
|
192
|
+
"request_num": self.request_counter,
|
|
193
|
+
"source": "coordinator"
|
|
194
|
+
}
|
|
195
|
+
print(f"[Coordinator] Sending with context: mission={self.mission_id[:8]}...")
|
|
196
|
+
|
|
170
197
|
response = await self.peers.request(
|
|
171
198
|
"analyst",
|
|
172
199
|
{"question": user_query},
|
|
173
|
-
timeout=30
|
|
200
|
+
timeout=30,
|
|
201
|
+
context=request_context # v0.3.2: Pass context
|
|
174
202
|
)
|
|
175
203
|
return f"Based on the analyst's findings: {response.get('analysis', 'No response')}"
|
|
176
204
|
return f"I can help with: {user_query}"
|
|
@@ -261,12 +289,24 @@ Never try to do analysis yourself - always delegate to the analyst."""
|
|
|
261
289
|
role = args.get("role", "")
|
|
262
290
|
question = args.get("question", "")
|
|
263
291
|
|
|
292
|
+
# v0.3.2: Generate request context for tracking
|
|
293
|
+
self.request_counter += 1
|
|
294
|
+
request_context = {
|
|
295
|
+
"mission_id": self.mission_id,
|
|
296
|
+
"request_id": str(uuid.uuid4()),
|
|
297
|
+
"request_num": self.request_counter,
|
|
298
|
+
"source": "coordinator",
|
|
299
|
+
"tool": "ask_peer"
|
|
300
|
+
}
|
|
301
|
+
|
|
264
302
|
print(f"[Coordinator] Asking {role}: {question[:50]}...")
|
|
303
|
+
print(f"[Coordinator] Context: mission={self.mission_id[:8]}..., req_num={self.request_counter}")
|
|
265
304
|
|
|
266
305
|
response = await self.peers.request(
|
|
267
306
|
role,
|
|
268
307
|
{"question": question},
|
|
269
|
-
timeout=30
|
|
308
|
+
timeout=30,
|
|
309
|
+
context=request_context # v0.3.2: Pass context
|
|
270
310
|
)
|
|
271
311
|
|
|
272
312
|
return response
|
|
@@ -284,7 +324,8 @@ Never try to do analysis yourself - always delegate to the analyst."""
|
|
|
284
324
|
|
|
285
325
|
async def main():
|
|
286
326
|
print("=" * 60)
|
|
287
|
-
print("
|
|
327
|
+
print("CustomAgent + Cognitive Discovery + Session Context")
|
|
328
|
+
print("Features: v0.3.0 Cognitive Discovery, v0.3.2 Session Context")
|
|
288
329
|
print("=" * 60)
|
|
289
330
|
|
|
290
331
|
# Create mesh with both agents
|