code-puppy 0.0.126__py3-none-any.whl → 0.0.128__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.
- code_puppy/__init__.py +1 -0
- code_puppy/agent.py +65 -69
- code_puppy/agents/agent_code_puppy.py +0 -3
- code_puppy/agents/runtime_manager.py +212 -0
- code_puppy/command_line/command_handler.py +56 -25
- code_puppy/command_line/mcp_commands.py +1298 -0
- code_puppy/command_line/meta_command_handler.py +3 -2
- code_puppy/command_line/model_picker_completion.py +21 -8
- code_puppy/main.py +52 -157
- code_puppy/mcp/__init__.py +23 -0
- code_puppy/mcp/async_lifecycle.py +237 -0
- code_puppy/mcp/circuit_breaker.py +218 -0
- code_puppy/mcp/config_wizard.py +437 -0
- code_puppy/mcp/dashboard.py +291 -0
- code_puppy/mcp/error_isolation.py +360 -0
- code_puppy/mcp/examples/retry_example.py +208 -0
- code_puppy/mcp/health_monitor.py +549 -0
- code_puppy/mcp/managed_server.py +346 -0
- code_puppy/mcp/manager.py +701 -0
- code_puppy/mcp/registry.py +412 -0
- code_puppy/mcp/retry_manager.py +321 -0
- code_puppy/mcp/server_registry_catalog.py +751 -0
- code_puppy/mcp/status_tracker.py +355 -0
- code_puppy/messaging/spinner/textual_spinner.py +6 -2
- code_puppy/model_factory.py +19 -4
- code_puppy/models.json +22 -4
- code_puppy/tui/app.py +19 -27
- code_puppy/tui/tests/test_agent_command.py +22 -15
- {code_puppy-0.0.126.data → code_puppy-0.0.128.data}/data/code_puppy/models.json +22 -4
- {code_puppy-0.0.126.dist-info → code_puppy-0.0.128.dist-info}/METADATA +2 -3
- {code_puppy-0.0.126.dist-info → code_puppy-0.0.128.dist-info}/RECORD +34 -18
- {code_puppy-0.0.126.dist-info → code_puppy-0.0.128.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.126.dist-info → code_puppy-0.0.128.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.126.dist-info → code_puppy-0.0.128.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Example usage of RetryManager with MCP server operations.
|
|
4
|
+
|
|
5
|
+
This demonstrates how the RetryManager can be integrated with MCP server calls
|
|
6
|
+
to handle transient failures gracefully with intelligent backoff strategies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import random
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
# Add project root to path
|
|
16
|
+
project_root = Path(__file__).parents[3]
|
|
17
|
+
sys.path.insert(0, str(project_root))
|
|
18
|
+
|
|
19
|
+
from code_puppy.mcp.retry_manager import get_retry_manager, retry_mcp_call
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MockMCPServer:
|
|
23
|
+
"""Mock MCP server for demonstration purposes."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, failure_rate: float = 0.3):
|
|
26
|
+
"""
|
|
27
|
+
Initialize the mock server.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
failure_rate: Probability of failure (0.0 to 1.0)
|
|
31
|
+
"""
|
|
32
|
+
self.failure_rate = failure_rate
|
|
33
|
+
self.call_count = 0
|
|
34
|
+
|
|
35
|
+
async def list_tools(self) -> list:
|
|
36
|
+
"""Simulate listing available tools."""
|
|
37
|
+
self.call_count += 1
|
|
38
|
+
|
|
39
|
+
# Simulate random failures
|
|
40
|
+
if random.random() < self.failure_rate:
|
|
41
|
+
raise ConnectionError(f"Simulated connection failure (call #{self.call_count})")
|
|
42
|
+
|
|
43
|
+
return [
|
|
44
|
+
{"name": "read_file", "description": "Read a file"},
|
|
45
|
+
{"name": "write_file", "description": "Write a file"},
|
|
46
|
+
{"name": "list_directory", "description": "List directory contents"}
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
async def call_tool(self, name: str, args: dict) -> Any:
|
|
50
|
+
"""Simulate calling a tool."""
|
|
51
|
+
self.call_count += 1
|
|
52
|
+
|
|
53
|
+
# Simulate random failures
|
|
54
|
+
if random.random() < self.failure_rate:
|
|
55
|
+
if random.random() < 0.5:
|
|
56
|
+
raise ConnectionError(f"Connection failed for {name}")
|
|
57
|
+
else:
|
|
58
|
+
# Simulate a 500 error
|
|
59
|
+
import httpx
|
|
60
|
+
from unittest.mock import Mock
|
|
61
|
+
response = Mock()
|
|
62
|
+
response.status_code = 500
|
|
63
|
+
raise httpx.HTTPStatusError("Server Error", request=Mock(), response=response)
|
|
64
|
+
|
|
65
|
+
return f"Tool '{name}' executed with args: {args}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def demonstrate_basic_retry():
|
|
69
|
+
"""Demonstrate basic retry functionality."""
|
|
70
|
+
print("=== Basic Retry Demonstration ===")
|
|
71
|
+
|
|
72
|
+
retry_manager = get_retry_manager()
|
|
73
|
+
server = MockMCPServer(failure_rate=0.5) # 50% failure rate
|
|
74
|
+
|
|
75
|
+
async def list_tools_call():
|
|
76
|
+
return await server.list_tools()
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
result = await retry_manager.retry_with_backoff(
|
|
80
|
+
func=list_tools_call,
|
|
81
|
+
max_attempts=3,
|
|
82
|
+
strategy="exponential",
|
|
83
|
+
server_id="demo-server"
|
|
84
|
+
)
|
|
85
|
+
print(f"✅ Success: Retrieved {len(result)} tools")
|
|
86
|
+
print(f"Server call count: {server.call_count}")
|
|
87
|
+
except Exception as e:
|
|
88
|
+
print(f"❌ Failed after retries: {e}")
|
|
89
|
+
|
|
90
|
+
# Check retry stats
|
|
91
|
+
stats = await retry_manager.get_retry_stats("demo-server")
|
|
92
|
+
print(f"Retry stats: total={stats.total_retries}, successful={stats.successful_retries}")
|
|
93
|
+
print()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def demonstrate_different_strategies():
|
|
97
|
+
"""Demonstrate different backoff strategies."""
|
|
98
|
+
print("=== Backoff Strategies Demonstration ===")
|
|
99
|
+
|
|
100
|
+
strategies = ["fixed", "linear", "exponential", "exponential_jitter"]
|
|
101
|
+
|
|
102
|
+
for strategy in strategies:
|
|
103
|
+
print(f"\n{strategy.upper()} strategy:")
|
|
104
|
+
server = MockMCPServer(failure_rate=0.7) # High failure rate
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
start_time = asyncio.get_event_loop().time()
|
|
108
|
+
|
|
109
|
+
result = await retry_mcp_call(
|
|
110
|
+
func=lambda: server.call_tool("read_file", {"path": "/example.txt"}),
|
|
111
|
+
server_id=f"server-{strategy}",
|
|
112
|
+
max_attempts=3,
|
|
113
|
+
strategy=strategy
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
end_time = asyncio.get_event_loop().time()
|
|
117
|
+
print(f" ✅ Success: {result}")
|
|
118
|
+
print(f" Time taken: {end_time - start_time:.2f}s")
|
|
119
|
+
print(f" Call count: {server.call_count}")
|
|
120
|
+
except Exception as e:
|
|
121
|
+
end_time = asyncio.get_event_loop().time()
|
|
122
|
+
print(f" ❌ Failed: {e}")
|
|
123
|
+
print(f" Time taken: {end_time - start_time:.2f}s")
|
|
124
|
+
print(f" Call count: {server.call_count}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async def demonstrate_concurrent_retries():
|
|
128
|
+
"""Demonstrate concurrent retry operations."""
|
|
129
|
+
print("\n=== Concurrent Retries Demonstration ===")
|
|
130
|
+
|
|
131
|
+
retry_manager = get_retry_manager()
|
|
132
|
+
|
|
133
|
+
# Create multiple servers with different failure rates
|
|
134
|
+
servers = [
|
|
135
|
+
("reliable-server", MockMCPServer(failure_rate=0.1)),
|
|
136
|
+
("unreliable-server", MockMCPServer(failure_rate=0.8)),
|
|
137
|
+
("moderate-server", MockMCPServer(failure_rate=0.4))
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
async def make_call(server_name: str, server: MockMCPServer):
|
|
141
|
+
"""Make a call with retry handling."""
|
|
142
|
+
try:
|
|
143
|
+
result = await retry_manager.retry_with_backoff(
|
|
144
|
+
func=lambda: server.list_tools(),
|
|
145
|
+
max_attempts=3,
|
|
146
|
+
strategy="exponential_jitter",
|
|
147
|
+
server_id=server_name
|
|
148
|
+
)
|
|
149
|
+
return f"{server_name}: Success (calls: {server.call_count})"
|
|
150
|
+
except Exception as e:
|
|
151
|
+
return f"{server_name}: Failed - {e} (calls: {server.call_count})"
|
|
152
|
+
|
|
153
|
+
# Run concurrent calls
|
|
154
|
+
tasks = [make_call(name, server) for name, server in servers]
|
|
155
|
+
results = await asyncio.gather(*tasks)
|
|
156
|
+
|
|
157
|
+
print("Concurrent results:")
|
|
158
|
+
for result in results:
|
|
159
|
+
print(f" {result}")
|
|
160
|
+
|
|
161
|
+
# Show overall stats
|
|
162
|
+
print("\nOverall retry statistics:")
|
|
163
|
+
all_stats = await retry_manager.get_all_stats()
|
|
164
|
+
for server_id, stats in all_stats.items():
|
|
165
|
+
success_rate = (stats.successful_retries / max(stats.total_retries, 1)) * 100
|
|
166
|
+
print(f" {server_id}: {stats.total_retries} retries, {success_rate:.1f}% success rate")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def demonstrate_error_classification():
|
|
170
|
+
"""Demonstrate error classification for retry decisions."""
|
|
171
|
+
print("\n=== Error Classification Demonstration ===")
|
|
172
|
+
|
|
173
|
+
retry_manager = get_retry_manager()
|
|
174
|
+
|
|
175
|
+
# Test different error types
|
|
176
|
+
test_errors = [
|
|
177
|
+
ConnectionError("Network connection failed"),
|
|
178
|
+
asyncio.TimeoutError("Request timeout"),
|
|
179
|
+
ValueError("JSON decode error: invalid format"),
|
|
180
|
+
ValueError("Schema validation failed"),
|
|
181
|
+
Exception("Authentication failed"),
|
|
182
|
+
Exception("Permission denied")
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
print("Error retry decisions:")
|
|
186
|
+
for error in test_errors:
|
|
187
|
+
should_retry = retry_manager.should_retry(error)
|
|
188
|
+
status = "✅ RETRY" if should_retry else "❌ NO RETRY"
|
|
189
|
+
print(f" {type(error).__name__}: {error} → {status}")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
async def main():
|
|
193
|
+
"""Run all demonstrations."""
|
|
194
|
+
print("RetryManager Example Demonstrations")
|
|
195
|
+
print("=" * 50)
|
|
196
|
+
|
|
197
|
+
await demonstrate_basic_retry()
|
|
198
|
+
await demonstrate_different_strategies()
|
|
199
|
+
await demonstrate_concurrent_retries()
|
|
200
|
+
await demonstrate_error_classification()
|
|
201
|
+
|
|
202
|
+
print("\n🎉 All demonstrations completed!")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
if __name__ == "__main__":
|
|
206
|
+
# Set a seed for reproducible results in the demo
|
|
207
|
+
random.seed(42)
|
|
208
|
+
asyncio.run(main())
|