jarviscore-framework 0.1.0__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/calculator_agent_example.py +77 -0
- examples/multi_agent_workflow.py +132 -0
- examples/research_agent_example.py +76 -0
- jarviscore/__init__.py +54 -0
- jarviscore/cli/__init__.py +7 -0
- jarviscore/cli/__main__.py +33 -0
- jarviscore/cli/check.py +404 -0
- jarviscore/cli/smoketest.py +371 -0
- jarviscore/config/__init__.py +7 -0
- jarviscore/config/settings.py +128 -0
- jarviscore/core/__init__.py +7 -0
- jarviscore/core/agent.py +163 -0
- jarviscore/core/mesh.py +463 -0
- jarviscore/core/profile.py +64 -0
- jarviscore/docs/API_REFERENCE.md +932 -0
- jarviscore/docs/CONFIGURATION.md +753 -0
- jarviscore/docs/GETTING_STARTED.md +600 -0
- jarviscore/docs/TROUBLESHOOTING.md +424 -0
- jarviscore/docs/USER_GUIDE.md +983 -0
- jarviscore/execution/__init__.py +94 -0
- jarviscore/execution/code_registry.py +298 -0
- jarviscore/execution/generator.py +268 -0
- jarviscore/execution/llm.py +430 -0
- jarviscore/execution/repair.py +283 -0
- jarviscore/execution/result_handler.py +332 -0
- jarviscore/execution/sandbox.py +555 -0
- jarviscore/execution/search.py +281 -0
- jarviscore/orchestration/__init__.py +18 -0
- jarviscore/orchestration/claimer.py +101 -0
- jarviscore/orchestration/dependency.py +143 -0
- jarviscore/orchestration/engine.py +292 -0
- jarviscore/orchestration/status.py +96 -0
- jarviscore/p2p/__init__.py +23 -0
- jarviscore/p2p/broadcaster.py +353 -0
- jarviscore/p2p/coordinator.py +364 -0
- jarviscore/p2p/keepalive.py +361 -0
- jarviscore/p2p/swim_manager.py +290 -0
- jarviscore/profiles/__init__.py +6 -0
- jarviscore/profiles/autoagent.py +264 -0
- jarviscore/profiles/customagent.py +137 -0
- jarviscore_framework-0.1.0.dist-info/METADATA +136 -0
- jarviscore_framework-0.1.0.dist-info/RECORD +55 -0
- jarviscore_framework-0.1.0.dist-info/WHEEL +5 -0
- jarviscore_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
- jarviscore_framework-0.1.0.dist-info/top_level.txt +3 -0
- tests/conftest.py +44 -0
- tests/test_agent.py +165 -0
- tests/test_autoagent.py +140 -0
- tests/test_autoagent_day4.py +186 -0
- tests/test_customagent.py +248 -0
- tests/test_integration.py +293 -0
- tests/test_llm_fallback.py +185 -0
- tests/test_mesh.py +356 -0
- tests/test_p2p_integration.py +375 -0
- tests/test_remote_sandbox.py +116 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test LLM Provider Fallback Chain
|
|
3
|
+
|
|
4
|
+
Tests the fallback order: Claude → Azure → Gemini → vLLM
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import os
|
|
9
|
+
from jarviscore.execution.llm import UnifiedLLMClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_provider_detection():
|
|
13
|
+
"""Test that all configured providers are detected."""
|
|
14
|
+
print("\n" + "="*70)
|
|
15
|
+
print("Testing LLM Provider Detection")
|
|
16
|
+
print("="*70 + "\n")
|
|
17
|
+
|
|
18
|
+
llm = UnifiedLLMClient()
|
|
19
|
+
|
|
20
|
+
print(f"Detected providers: {[p.value for p in llm.provider_order]}")
|
|
21
|
+
print(f"\nProvider status:")
|
|
22
|
+
print(f" ✓ Claude: {'Available' if llm.claude_client else 'Not available'}")
|
|
23
|
+
print(f" ✓ Azure: {'Available' if llm.azure_client else 'Not available'}")
|
|
24
|
+
print(f" ✓ Gemini: {'Available' if llm.gemini_client else 'Not available'}")
|
|
25
|
+
print(f" ✓ vLLM: {'Available' if llm.vllm_endpoint else 'Not available'}")
|
|
26
|
+
|
|
27
|
+
# Verify order is correct
|
|
28
|
+
expected_order = ['claude', 'azure', 'gemini'] # vLLM not configured by default
|
|
29
|
+
actual_order = [p.value for p in llm.provider_order]
|
|
30
|
+
|
|
31
|
+
print(f"\nFallback order:")
|
|
32
|
+
for i, provider in enumerate(actual_order, 1):
|
|
33
|
+
print(f" {i}. {provider}")
|
|
34
|
+
|
|
35
|
+
assert len(actual_order) > 0, "No providers detected!"
|
|
36
|
+
print("\n✅ Provider detection test passed!")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def test_claude_primary():
|
|
40
|
+
"""Test that Claude is used when available."""
|
|
41
|
+
print("\n" + "="*70)
|
|
42
|
+
print("Testing Claude (Primary Provider)")
|
|
43
|
+
print("="*70 + "\n")
|
|
44
|
+
|
|
45
|
+
llm = UnifiedLLMClient()
|
|
46
|
+
|
|
47
|
+
if not llm.claude_client:
|
|
48
|
+
print("⚠️ Claude not available, skipping test")
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
result = await llm.generate(
|
|
53
|
+
prompt="Say 'OK' only",
|
|
54
|
+
temperature=0.0,
|
|
55
|
+
max_tokens=10
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
print(f"Provider used: {result.get('provider', 'unknown')}")
|
|
59
|
+
print(f"Response: {result.get('content', '')[:50]}")
|
|
60
|
+
assert result.get('provider') == 'claude'
|
|
61
|
+
print("\n✅ Claude test passed!")
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print(f"\n❌ Claude test failed: {e}")
|
|
65
|
+
raise
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def test_azure_fallback():
|
|
69
|
+
"""Test Azure fallback when Claude is unavailable."""
|
|
70
|
+
print("\n" + "="*70)
|
|
71
|
+
print("Testing Azure (Fallback #1)")
|
|
72
|
+
print("="*70 + "\n")
|
|
73
|
+
|
|
74
|
+
# Create LLM client with Azure only (pass config directly)
|
|
75
|
+
from jarviscore.config.settings import settings
|
|
76
|
+
|
|
77
|
+
azure_config = {
|
|
78
|
+
'claude_api_key': None, # Disable Claude
|
|
79
|
+
'anthropic_api_key': None,
|
|
80
|
+
'azure_api_key': settings.azure_api_key,
|
|
81
|
+
'azure_endpoint': settings.azure_endpoint,
|
|
82
|
+
'azure_deployment': settings.azure_deployment,
|
|
83
|
+
'azure_api_version': settings.azure_api_version,
|
|
84
|
+
'gemini_api_key': None, # Disable Gemini
|
|
85
|
+
'llm_endpoint': None, # Disable vLLM
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
llm = UnifiedLLMClient(config=azure_config)
|
|
90
|
+
|
|
91
|
+
if not llm.azure_client:
|
|
92
|
+
print("⚠️ Azure not available, skipping test")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
result = await llm.generate(
|
|
96
|
+
prompt="Say 'OK' only",
|
|
97
|
+
temperature=0.0,
|
|
98
|
+
max_tokens=10
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
print(f"Provider used: {result.get('provider', 'unknown')}")
|
|
102
|
+
print(f"Response: {result.get('content', '')[:50]}")
|
|
103
|
+
assert result.get('provider') == 'azure'
|
|
104
|
+
print("\n✅ Azure fallback test passed!")
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print(f"\n❌ Azure fallback test failed: {e}")
|
|
108
|
+
raise
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def test_gemini_fallback():
|
|
112
|
+
"""Test Gemini fallback when Claude and Azure are unavailable."""
|
|
113
|
+
print("\n" + "="*70)
|
|
114
|
+
print("Testing Gemini (Fallback #2)")
|
|
115
|
+
print("="*70 + "\n")
|
|
116
|
+
|
|
117
|
+
# Create LLM client with Gemini only (pass config directly)
|
|
118
|
+
from jarviscore.config.settings import settings
|
|
119
|
+
|
|
120
|
+
gemini_config = {
|
|
121
|
+
'claude_api_key': None, # Disable Claude
|
|
122
|
+
'anthropic_api_key': None,
|
|
123
|
+
'azure_api_key': None, # Disable Azure
|
|
124
|
+
'azure_endpoint': None,
|
|
125
|
+
'gemini_api_key': settings.gemini_api_key,
|
|
126
|
+
'gemini_model': settings.gemini_model,
|
|
127
|
+
'llm_endpoint': None, # Disable vLLM
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
llm = UnifiedLLMClient(config=gemini_config)
|
|
132
|
+
|
|
133
|
+
if not llm.gemini_client:
|
|
134
|
+
print("⚠️ Gemini not available, skipping test")
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
result = await llm.generate(
|
|
138
|
+
prompt="Say 'OK' only",
|
|
139
|
+
temperature=0.0,
|
|
140
|
+
max_tokens=10
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
print(f"Provider used: {result.get('provider', 'unknown')}")
|
|
144
|
+
print(f"Response: {result.get('content', '')[:50]}")
|
|
145
|
+
assert result.get('provider') == 'gemini'
|
|
146
|
+
print("\n✅ Gemini fallback test passed!")
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
print(f"\n⚠️ Gemini fallback test skipped (quota/rate limit): {e}")
|
|
150
|
+
# Gemini often has quota limits, so we don't fail the test
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async def run_all_tests():
|
|
154
|
+
"""Run all fallback tests."""
|
|
155
|
+
print("\n" + "="*70)
|
|
156
|
+
print("JarvisCore LLM Fallback Chain Tests")
|
|
157
|
+
print("Testing: Claude → Azure → Gemini → vLLM")
|
|
158
|
+
print("="*70)
|
|
159
|
+
|
|
160
|
+
# Test 1: Provider detection
|
|
161
|
+
test_provider_detection()
|
|
162
|
+
|
|
163
|
+
# Test 2: Claude (primary)
|
|
164
|
+
await test_claude_primary()
|
|
165
|
+
|
|
166
|
+
# Test 3: Azure (fallback #1)
|
|
167
|
+
await test_azure_fallback()
|
|
168
|
+
|
|
169
|
+
# Test 4: Gemini (fallback #2)
|
|
170
|
+
await test_gemini_fallback()
|
|
171
|
+
|
|
172
|
+
print("\n" + "="*70)
|
|
173
|
+
print("Summary")
|
|
174
|
+
print("="*70)
|
|
175
|
+
print("\n✅ All fallback tests completed successfully!")
|
|
176
|
+
print("\nFallback chain verified:")
|
|
177
|
+
print(" 1. Claude (primary) - ✅ Working")
|
|
178
|
+
print(" 2. Azure (fallback) - ✅ Working")
|
|
179
|
+
print(" 3. Gemini (fallback) - ✅ Working (quota limits may apply)")
|
|
180
|
+
print(" 4. vLLM (local) - ⚠️ Configure LLM_ENDPOINT to test")
|
|
181
|
+
print()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == '__main__':
|
|
185
|
+
asyncio.run(run_all_tests())
|
tests/test_mesh.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for Mesh orchestrator.
|
|
3
|
+
"""
|
|
4
|
+
import pytest
|
|
5
|
+
from jarviscore import Mesh, MeshMode, Agent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Test agents
|
|
9
|
+
class TestAgent1(Agent):
|
|
10
|
+
"""Test agent with role 'agent1'."""
|
|
11
|
+
role = "agent1"
|
|
12
|
+
capabilities = ["capability1", "shared_capability"]
|
|
13
|
+
|
|
14
|
+
async def execute_task(self, task):
|
|
15
|
+
return {"status": "success", "output": f"Result from {self.role}"}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestAgent2(Agent):
|
|
19
|
+
"""Test agent with role 'agent2'."""
|
|
20
|
+
role = "agent2"
|
|
21
|
+
capabilities = ["capability2", "shared_capability"]
|
|
22
|
+
|
|
23
|
+
async def execute_task(self, task):
|
|
24
|
+
return {"status": "success", "output": f"Result from {self.role}"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestMeshInitialization:
|
|
28
|
+
"""Test mesh initialization."""
|
|
29
|
+
|
|
30
|
+
def test_mesh_creation_default_mode(self):
|
|
31
|
+
"""Test creating mesh with default mode."""
|
|
32
|
+
mesh = Mesh()
|
|
33
|
+
|
|
34
|
+
assert mesh.mode == MeshMode.AUTONOMOUS
|
|
35
|
+
assert mesh.config == {}
|
|
36
|
+
assert mesh.agents == []
|
|
37
|
+
assert mesh._started is False
|
|
38
|
+
|
|
39
|
+
def test_mesh_creation_autonomous_mode(self):
|
|
40
|
+
"""Test creating mesh in autonomous mode."""
|
|
41
|
+
mesh = Mesh(mode="autonomous")
|
|
42
|
+
|
|
43
|
+
assert mesh.mode == MeshMode.AUTONOMOUS
|
|
44
|
+
|
|
45
|
+
def test_mesh_creation_distributed_mode(self):
|
|
46
|
+
"""Test creating mesh in distributed mode."""
|
|
47
|
+
mesh = Mesh(mode="distributed")
|
|
48
|
+
|
|
49
|
+
assert mesh.mode == MeshMode.DISTRIBUTED
|
|
50
|
+
|
|
51
|
+
def test_mesh_creation_invalid_mode(self):
|
|
52
|
+
"""Test that invalid mode raises ValueError."""
|
|
53
|
+
with pytest.raises(ValueError) as exc_info:
|
|
54
|
+
Mesh(mode="invalid_mode")
|
|
55
|
+
|
|
56
|
+
assert "Invalid mode" in str(exc_info.value)
|
|
57
|
+
|
|
58
|
+
def test_mesh_creation_with_config(self):
|
|
59
|
+
"""Test creating mesh with configuration."""
|
|
60
|
+
config = {
|
|
61
|
+
"p2p_enabled": True,
|
|
62
|
+
"state_backend": "redis",
|
|
63
|
+
"max_parallel": 10
|
|
64
|
+
}
|
|
65
|
+
mesh = Mesh(config=config)
|
|
66
|
+
|
|
67
|
+
assert mesh.config == config
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestMeshAgentRegistration:
|
|
71
|
+
"""Test agent registration with mesh."""
|
|
72
|
+
|
|
73
|
+
def test_add_single_agent(self):
|
|
74
|
+
"""Test adding a single agent to mesh."""
|
|
75
|
+
mesh = Mesh()
|
|
76
|
+
agent = mesh.add(TestAgent1)
|
|
77
|
+
|
|
78
|
+
assert len(mesh.agents) == 1
|
|
79
|
+
assert agent.role == "agent1"
|
|
80
|
+
assert agent._mesh is mesh
|
|
81
|
+
assert mesh.get_agent("agent1") == agent
|
|
82
|
+
|
|
83
|
+
def test_add_multiple_agents(self):
|
|
84
|
+
"""Test adding multiple agents to mesh."""
|
|
85
|
+
mesh = Mesh()
|
|
86
|
+
agent1 = mesh.add(TestAgent1)
|
|
87
|
+
agent2 = mesh.add(TestAgent2)
|
|
88
|
+
|
|
89
|
+
assert len(mesh.agents) == 2
|
|
90
|
+
assert mesh.get_agent("agent1") == agent1
|
|
91
|
+
assert mesh.get_agent("agent2") == agent2
|
|
92
|
+
|
|
93
|
+
def test_add_agent_with_custom_id(self):
|
|
94
|
+
"""Test adding agent with custom ID."""
|
|
95
|
+
mesh = Mesh()
|
|
96
|
+
agent = mesh.add(TestAgent1, agent_id="custom-123")
|
|
97
|
+
|
|
98
|
+
assert agent.agent_id == "custom-123"
|
|
99
|
+
assert mesh.get_agent("agent1") == agent
|
|
100
|
+
|
|
101
|
+
def test_add_duplicate_role_fails(self):
|
|
102
|
+
"""Test that adding agent with duplicate role raises ValueError."""
|
|
103
|
+
mesh = Mesh()
|
|
104
|
+
mesh.add(TestAgent1)
|
|
105
|
+
|
|
106
|
+
with pytest.raises(ValueError) as exc_info:
|
|
107
|
+
mesh.add(TestAgent1)
|
|
108
|
+
|
|
109
|
+
assert "already registered" in str(exc_info.value)
|
|
110
|
+
|
|
111
|
+
def test_add_invalid_agent_class_fails(self):
|
|
112
|
+
"""Test that adding non-Agent class raises TypeError."""
|
|
113
|
+
mesh = Mesh()
|
|
114
|
+
|
|
115
|
+
class NotAnAgent:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
with pytest.raises(TypeError) as exc_info:
|
|
119
|
+
mesh.add(NotAnAgent)
|
|
120
|
+
|
|
121
|
+
assert "must inherit from Agent" in str(exc_info.value)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class TestMeshCapabilityIndex:
|
|
125
|
+
"""Test mesh capability indexing."""
|
|
126
|
+
|
|
127
|
+
def test_get_agents_by_capability_single(self):
|
|
128
|
+
"""Test getting agents by capability (single match)."""
|
|
129
|
+
mesh = Mesh()
|
|
130
|
+
agent1 = mesh.add(TestAgent1)
|
|
131
|
+
|
|
132
|
+
agents = mesh.get_agents_by_capability("capability1")
|
|
133
|
+
|
|
134
|
+
assert len(agents) == 1
|
|
135
|
+
assert agents[0] == agent1
|
|
136
|
+
|
|
137
|
+
def test_get_agents_by_capability_multiple(self):
|
|
138
|
+
"""Test getting agents by capability (multiple matches)."""
|
|
139
|
+
mesh = Mesh()
|
|
140
|
+
agent1 = mesh.add(TestAgent1)
|
|
141
|
+
agent2 = mesh.add(TestAgent2)
|
|
142
|
+
|
|
143
|
+
agents = mesh.get_agents_by_capability("shared_capability")
|
|
144
|
+
|
|
145
|
+
assert len(agents) == 2
|
|
146
|
+
assert agent1 in agents
|
|
147
|
+
assert agent2 in agents
|
|
148
|
+
|
|
149
|
+
def test_get_agents_by_capability_none(self):
|
|
150
|
+
"""Test getting agents by capability (no matches)."""
|
|
151
|
+
mesh = Mesh()
|
|
152
|
+
mesh.add(TestAgent1)
|
|
153
|
+
|
|
154
|
+
agents = mesh.get_agents_by_capability("nonexistent")
|
|
155
|
+
|
|
156
|
+
assert agents == []
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class TestMeshLifecycle:
|
|
160
|
+
"""Test mesh start/stop lifecycle."""
|
|
161
|
+
|
|
162
|
+
@pytest.mark.asyncio
|
|
163
|
+
async def test_start_mesh_calls_agent_setup(self):
|
|
164
|
+
"""Test that start() calls setup() on all agents."""
|
|
165
|
+
mesh = Mesh()
|
|
166
|
+
|
|
167
|
+
# Track setup calls
|
|
168
|
+
setup_called = []
|
|
169
|
+
|
|
170
|
+
class TrackingAgent(Agent):
|
|
171
|
+
role = "tracker"
|
|
172
|
+
capabilities = ["tracking"]
|
|
173
|
+
|
|
174
|
+
async def execute_task(self, task):
|
|
175
|
+
return {"status": "success"}
|
|
176
|
+
|
|
177
|
+
async def setup(self):
|
|
178
|
+
await super().setup()
|
|
179
|
+
setup_called.append(self.agent_id)
|
|
180
|
+
|
|
181
|
+
agent1 = mesh.add(TrackingAgent, agent_id="tracker-1")
|
|
182
|
+
agent2 = mesh.add(TrackingAgent, agent_id="tracker-2")
|
|
183
|
+
|
|
184
|
+
await mesh.start()
|
|
185
|
+
|
|
186
|
+
assert agent1.agent_id in setup_called
|
|
187
|
+
assert agent2.agent_id in setup_called
|
|
188
|
+
assert mesh._started is True
|
|
189
|
+
|
|
190
|
+
@pytest.mark.asyncio
|
|
191
|
+
async def test_start_mesh_without_agents_fails(self):
|
|
192
|
+
"""Test that start() fails if no agents registered."""
|
|
193
|
+
mesh = Mesh()
|
|
194
|
+
|
|
195
|
+
with pytest.raises(RuntimeError) as exc_info:
|
|
196
|
+
await mesh.start()
|
|
197
|
+
|
|
198
|
+
assert "No agents registered" in str(exc_info.value)
|
|
199
|
+
|
|
200
|
+
@pytest.mark.asyncio
|
|
201
|
+
async def test_start_mesh_twice_fails(self):
|
|
202
|
+
"""Test that start() fails if mesh already started."""
|
|
203
|
+
mesh = Mesh()
|
|
204
|
+
mesh.add(TestAgent1)
|
|
205
|
+
|
|
206
|
+
await mesh.start()
|
|
207
|
+
|
|
208
|
+
with pytest.raises(RuntimeError) as exc_info:
|
|
209
|
+
await mesh.start()
|
|
210
|
+
|
|
211
|
+
assert "already started" in str(exc_info.value)
|
|
212
|
+
|
|
213
|
+
@pytest.mark.asyncio
|
|
214
|
+
async def test_stop_mesh_calls_agent_teardown(self):
|
|
215
|
+
"""Test that stop() calls teardown() on all agents."""
|
|
216
|
+
mesh = Mesh()
|
|
217
|
+
|
|
218
|
+
# Track teardown calls
|
|
219
|
+
teardown_called = []
|
|
220
|
+
|
|
221
|
+
class TrackingAgent(Agent):
|
|
222
|
+
role = "tracker"
|
|
223
|
+
capabilities = ["tracking"]
|
|
224
|
+
|
|
225
|
+
async def execute_task(self, task):
|
|
226
|
+
return {"status": "success"}
|
|
227
|
+
|
|
228
|
+
async def teardown(self):
|
|
229
|
+
await super().teardown()
|
|
230
|
+
teardown_called.append(self.agent_id)
|
|
231
|
+
|
|
232
|
+
agent1 = mesh.add(TrackingAgent, agent_id="tracker-1")
|
|
233
|
+
agent2 = mesh.add(TrackingAgent, agent_id="tracker-2")
|
|
234
|
+
|
|
235
|
+
await mesh.start()
|
|
236
|
+
await mesh.stop()
|
|
237
|
+
|
|
238
|
+
assert agent1.agent_id in teardown_called
|
|
239
|
+
assert agent2.agent_id in teardown_called
|
|
240
|
+
assert mesh._started is False
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class TestMeshWorkflow:
|
|
244
|
+
"""Test mesh workflow execution (autonomous mode)."""
|
|
245
|
+
|
|
246
|
+
@pytest.mark.asyncio
|
|
247
|
+
async def test_workflow_execution_single_step(self):
|
|
248
|
+
"""Test executing single-step workflow."""
|
|
249
|
+
mesh = Mesh(mode="autonomous")
|
|
250
|
+
mesh.add(TestAgent1)
|
|
251
|
+
|
|
252
|
+
await mesh.start()
|
|
253
|
+
|
|
254
|
+
results = await mesh.workflow("test-workflow", [
|
|
255
|
+
{"agent": "agent1", "task": "Do something"}
|
|
256
|
+
])
|
|
257
|
+
|
|
258
|
+
assert len(results) == 1
|
|
259
|
+
assert results[0]["status"] == "success"
|
|
260
|
+
assert "agent1" in results[0]["agent"]
|
|
261
|
+
|
|
262
|
+
@pytest.mark.asyncio
|
|
263
|
+
async def test_workflow_execution_multiple_steps(self):
|
|
264
|
+
"""Test executing multi-step workflow."""
|
|
265
|
+
mesh = Mesh(mode="autonomous")
|
|
266
|
+
mesh.add(TestAgent1)
|
|
267
|
+
mesh.add(TestAgent2)
|
|
268
|
+
|
|
269
|
+
await mesh.start()
|
|
270
|
+
|
|
271
|
+
results = await mesh.workflow("test-workflow", [
|
|
272
|
+
{"agent": "agent1", "task": "Step 1"},
|
|
273
|
+
{"agent": "agent2", "task": "Step 2"}
|
|
274
|
+
])
|
|
275
|
+
|
|
276
|
+
assert len(results) == 2
|
|
277
|
+
assert all(r["status"] == "success" for r in results)
|
|
278
|
+
|
|
279
|
+
@pytest.mark.asyncio
|
|
280
|
+
async def test_workflow_with_capability_routing(self):
|
|
281
|
+
"""Test workflow routing by capability."""
|
|
282
|
+
mesh = Mesh(mode="autonomous")
|
|
283
|
+
mesh.add(TestAgent1)
|
|
284
|
+
|
|
285
|
+
await mesh.start()
|
|
286
|
+
|
|
287
|
+
results = await mesh.workflow("test-workflow", [
|
|
288
|
+
{"agent": "capability1", "task": "Use capability"}
|
|
289
|
+
])
|
|
290
|
+
|
|
291
|
+
assert len(results) == 1
|
|
292
|
+
assert results[0]["status"] == "success"
|
|
293
|
+
|
|
294
|
+
@pytest.mark.asyncio
|
|
295
|
+
async def test_workflow_agent_not_found(self):
|
|
296
|
+
"""Test workflow with missing agent."""
|
|
297
|
+
mesh = Mesh(mode="autonomous")
|
|
298
|
+
mesh.add(TestAgent1)
|
|
299
|
+
|
|
300
|
+
await mesh.start()
|
|
301
|
+
|
|
302
|
+
results = await mesh.workflow("test-workflow", [
|
|
303
|
+
{"agent": "nonexistent", "task": "Should fail"}
|
|
304
|
+
])
|
|
305
|
+
|
|
306
|
+
assert len(results) == 1
|
|
307
|
+
assert results[0]["status"] == "failure"
|
|
308
|
+
assert "No agent found" in results[0]["error"]
|
|
309
|
+
|
|
310
|
+
@pytest.mark.asyncio
|
|
311
|
+
async def test_workflow_not_started_fails(self):
|
|
312
|
+
"""Test that workflow() fails if mesh not started."""
|
|
313
|
+
mesh = Mesh(mode="autonomous")
|
|
314
|
+
mesh.add(TestAgent1)
|
|
315
|
+
|
|
316
|
+
with pytest.raises(RuntimeError) as exc_info:
|
|
317
|
+
await mesh.workflow("test-workflow", [
|
|
318
|
+
{"agent": "agent1", "task": "Should fail"}
|
|
319
|
+
])
|
|
320
|
+
|
|
321
|
+
assert "not started" in str(exc_info.value)
|
|
322
|
+
|
|
323
|
+
@pytest.mark.asyncio
|
|
324
|
+
async def test_workflow_distributed_mode_fails(self):
|
|
325
|
+
"""Test that workflow() fails in distributed mode."""
|
|
326
|
+
# Use unique port to avoid conflicts with P2P tests
|
|
327
|
+
mesh = Mesh(mode="distributed", config={'bind_port': 7999})
|
|
328
|
+
mesh.add(TestAgent1)
|
|
329
|
+
|
|
330
|
+
await mesh.start()
|
|
331
|
+
|
|
332
|
+
with pytest.raises(RuntimeError) as exc_info:
|
|
333
|
+
await mesh.workflow("test-workflow", [
|
|
334
|
+
{"agent": "agent1", "task": "Should fail"}
|
|
335
|
+
])
|
|
336
|
+
|
|
337
|
+
assert "only available in autonomous mode" in str(exc_info.value)
|
|
338
|
+
|
|
339
|
+
await mesh.stop()
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class TestMeshRepresentation:
|
|
343
|
+
"""Test mesh string representation."""
|
|
344
|
+
|
|
345
|
+
def test_mesh_repr(self):
|
|
346
|
+
"""Test mesh __repr__."""
|
|
347
|
+
mesh = Mesh(mode="autonomous")
|
|
348
|
+
mesh.add(TestAgent1)
|
|
349
|
+
mesh.add(TestAgent2)
|
|
350
|
+
|
|
351
|
+
repr_str = repr(mesh)
|
|
352
|
+
|
|
353
|
+
assert "Mesh" in repr_str
|
|
354
|
+
assert "autonomous" in repr_str
|
|
355
|
+
assert "agents=2" in repr_str
|
|
356
|
+
assert "started=False" in repr_str
|