agentfield 0.1.22rc2__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.
- agentfield/__init__.py +66 -0
- agentfield/agent.py +3569 -0
- agentfield/agent_ai.py +1125 -0
- agentfield/agent_cli.py +386 -0
- agentfield/agent_field_handler.py +494 -0
- agentfield/agent_mcp.py +534 -0
- agentfield/agent_registry.py +29 -0
- agentfield/agent_server.py +1185 -0
- agentfield/agent_utils.py +269 -0
- agentfield/agent_workflow.py +323 -0
- agentfield/async_config.py +278 -0
- agentfield/async_execution_manager.py +1227 -0
- agentfield/client.py +1447 -0
- agentfield/connection_manager.py +280 -0
- agentfield/decorators.py +527 -0
- agentfield/did_manager.py +337 -0
- agentfield/dynamic_skills.py +304 -0
- agentfield/execution_context.py +255 -0
- agentfield/execution_state.py +453 -0
- agentfield/http_connection_manager.py +429 -0
- agentfield/litellm_adapters.py +140 -0
- agentfield/logger.py +249 -0
- agentfield/mcp_client.py +204 -0
- agentfield/mcp_manager.py +340 -0
- agentfield/mcp_stdio_bridge.py +550 -0
- agentfield/memory.py +723 -0
- agentfield/memory_events.py +489 -0
- agentfield/multimodal.py +173 -0
- agentfield/multimodal_response.py +403 -0
- agentfield/pydantic_utils.py +227 -0
- agentfield/rate_limiter.py +280 -0
- agentfield/result_cache.py +441 -0
- agentfield/router.py +190 -0
- agentfield/status.py +70 -0
- agentfield/types.py +710 -0
- agentfield/utils.py +26 -0
- agentfield/vc_generator.py +464 -0
- agentfield/vision.py +198 -0
- agentfield-0.1.22rc2.dist-info/METADATA +102 -0
- agentfield-0.1.22rc2.dist-info/RECORD +42 -0
- agentfield-0.1.22rc2.dist-info/WHEEL +5 -0
- agentfield-0.1.22rc2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import hashlib
|
|
3
|
+
import os
|
|
4
|
+
import random
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
from agentfield.logger import log_debug
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RateLimitError(Exception):
|
|
11
|
+
"""Custom exception for rate limit errors"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, message: str, retry_after: Optional[float] = None):
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
self.retry_after = retry_after
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StatelessRateLimiter:
|
|
19
|
+
"""
|
|
20
|
+
Stateless rate limiter with adaptive exponential backoff.
|
|
21
|
+
|
|
22
|
+
Designed to work across hundreds of containers without coordination.
|
|
23
|
+
Uses container-specific jitter to naturally distribute load.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
max_retries: int = 20,
|
|
29
|
+
base_delay: float = 1.0,
|
|
30
|
+
max_delay: float = 300.0,
|
|
31
|
+
jitter_factor: float = 0.25,
|
|
32
|
+
circuit_breaker_threshold: int = 10,
|
|
33
|
+
circuit_breaker_timeout: int = 300,
|
|
34
|
+
):
|
|
35
|
+
self.max_retries = max_retries
|
|
36
|
+
self.base_delay = base_delay
|
|
37
|
+
self.max_delay = max_delay
|
|
38
|
+
self.jitter_factor = jitter_factor
|
|
39
|
+
self.circuit_breaker_threshold = circuit_breaker_threshold
|
|
40
|
+
self.circuit_breaker_timeout = circuit_breaker_timeout
|
|
41
|
+
|
|
42
|
+
# Container-specific seed for consistent but distributed jitter
|
|
43
|
+
self._container_seed = self._get_container_seed()
|
|
44
|
+
|
|
45
|
+
# Circuit breaker state (per-instance)
|
|
46
|
+
self._consecutive_failures = 0
|
|
47
|
+
self._circuit_open_time = None
|
|
48
|
+
|
|
49
|
+
def _get_container_seed(self) -> int:
|
|
50
|
+
"""Generate a container-specific seed for consistent jitter distribution"""
|
|
51
|
+
# Use hostname, process ID, and other container-specific identifiers
|
|
52
|
+
identifier = f"{os.getenv('HOSTNAME', 'localhost')}-{os.getpid()}"
|
|
53
|
+
return int(hashlib.md5(identifier.encode()).hexdigest()[:8], 16)
|
|
54
|
+
|
|
55
|
+
def _is_rate_limit_error(self, error: Exception) -> bool:
|
|
56
|
+
"""
|
|
57
|
+
Universal rate limit error detection for any LiteLLM provider.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
error: Exception to check
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
bool: True if this is a rate limit error
|
|
64
|
+
"""
|
|
65
|
+
# Check for specific LiteLLM rate limit error
|
|
66
|
+
if hasattr(error, "__class__") and "RateLimitError" in str(error.__class__):
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
# Check HTTP status codes
|
|
70
|
+
if hasattr(error, "response"):
|
|
71
|
+
if hasattr(error.response, "status_code"):
|
|
72
|
+
if error.response.status_code in [429, 503]:
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
# Check for HTTP status in error attributes
|
|
76
|
+
if hasattr(error, "status_code"):
|
|
77
|
+
if error.status_code in [429, 503]:
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
# Check error message for rate limit keywords
|
|
81
|
+
error_message = str(error).lower()
|
|
82
|
+
rate_limit_keywords = [
|
|
83
|
+
"rate limit",
|
|
84
|
+
"rate-limit",
|
|
85
|
+
"rate_limit",
|
|
86
|
+
"too many requests",
|
|
87
|
+
"quota exceeded",
|
|
88
|
+
"temporarily rate-limited",
|
|
89
|
+
"rate limited",
|
|
90
|
+
"requests per",
|
|
91
|
+
"rpm exceeded",
|
|
92
|
+
"tpm exceeded",
|
|
93
|
+
"usage limit",
|
|
94
|
+
"throttled",
|
|
95
|
+
"throttling",
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
return any(keyword in error_message for keyword in rate_limit_keywords)
|
|
99
|
+
|
|
100
|
+
def _extract_retry_after(self, error: Exception) -> Optional[float]:
|
|
101
|
+
"""
|
|
102
|
+
Extract retry-after value from error if available.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
error: Exception that may contain retry-after information
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Optional[float]: Retry-after seconds if found
|
|
109
|
+
"""
|
|
110
|
+
# Check for Retry-After header in HTTP response
|
|
111
|
+
if hasattr(error, "response") and hasattr(error.response, "headers"):
|
|
112
|
+
retry_after = error.response.headers.get("Retry-After")
|
|
113
|
+
if retry_after:
|
|
114
|
+
try:
|
|
115
|
+
return float(retry_after)
|
|
116
|
+
except ValueError:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
# Check for retry_after in error attributes
|
|
120
|
+
if hasattr(error, "retry_after"):
|
|
121
|
+
try:
|
|
122
|
+
return float(error.retry_after)
|
|
123
|
+
except (ValueError, TypeError):
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
def _calculate_backoff_delay(
|
|
129
|
+
self, attempt: int, retry_after: Optional[float] = None
|
|
130
|
+
) -> float:
|
|
131
|
+
"""
|
|
132
|
+
Calculate backoff delay with exponential backoff and jitter.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
attempt: Current attempt number (0-based)
|
|
136
|
+
retry_after: Server-suggested retry delay
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
float: Delay in seconds
|
|
140
|
+
"""
|
|
141
|
+
# Use server-suggested delay if available and reasonable
|
|
142
|
+
if retry_after and retry_after <= self.max_delay:
|
|
143
|
+
base_delay = retry_after
|
|
144
|
+
else:
|
|
145
|
+
# Exponential backoff: base_delay * (2 ^ attempt)
|
|
146
|
+
base_delay = min(self.base_delay * (2**attempt), self.max_delay)
|
|
147
|
+
|
|
148
|
+
# Add container-specific jitter to distribute load
|
|
149
|
+
# Use container seed to ensure consistent but distributed jitter
|
|
150
|
+
random.seed(self._container_seed + attempt)
|
|
151
|
+
jitter_range = base_delay * self.jitter_factor
|
|
152
|
+
jitter = random.uniform(-jitter_range, jitter_range)
|
|
153
|
+
|
|
154
|
+
# Ensure minimum delay and apply jitter
|
|
155
|
+
delay = max(0.1, base_delay + jitter)
|
|
156
|
+
|
|
157
|
+
log_debug(
|
|
158
|
+
f"Rate limit backoff: attempt={attempt}, base_delay={base_delay:.2f}s, jitter={jitter:.2f}s, total_delay={delay:.2f}s"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return delay
|
|
162
|
+
|
|
163
|
+
def _check_circuit_breaker(self) -> bool:
|
|
164
|
+
"""
|
|
165
|
+
Check if circuit breaker is open.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
bool: True if circuit is open (should not retry)
|
|
169
|
+
"""
|
|
170
|
+
if self._circuit_open_time is None:
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
# Check if circuit breaker timeout has passed
|
|
174
|
+
if time.time() - self._circuit_open_time > self.circuit_breaker_timeout:
|
|
175
|
+
log_debug("Circuit breaker timeout passed, attempting to close circuit")
|
|
176
|
+
self._circuit_open_time = None
|
|
177
|
+
self._consecutive_failures = 0
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
def _update_circuit_breaker(self, success: bool):
|
|
183
|
+
"""
|
|
184
|
+
Update circuit breaker state based on operation result.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
success: Whether the operation succeeded
|
|
188
|
+
"""
|
|
189
|
+
if success:
|
|
190
|
+
# Reset on success
|
|
191
|
+
self._consecutive_failures = 0
|
|
192
|
+
if self._circuit_open_time:
|
|
193
|
+
log_debug("Circuit breaker closed after successful request")
|
|
194
|
+
self._circuit_open_time = None
|
|
195
|
+
else:
|
|
196
|
+
# Increment failures
|
|
197
|
+
self._consecutive_failures += 1
|
|
198
|
+
|
|
199
|
+
# Open circuit if threshold reached
|
|
200
|
+
if (
|
|
201
|
+
self._consecutive_failures >= self.circuit_breaker_threshold
|
|
202
|
+
and self._circuit_open_time is None
|
|
203
|
+
):
|
|
204
|
+
self._circuit_open_time = time.time()
|
|
205
|
+
log_debug(
|
|
206
|
+
f"Circuit breaker opened after {self._consecutive_failures} consecutive failures"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
async def execute_with_retry(self, func, *args, **kwargs) -> Any:
|
|
210
|
+
"""
|
|
211
|
+
Execute a function with rate limit retry logic.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
func: Async function to execute
|
|
215
|
+
*args: Positional arguments for func
|
|
216
|
+
**kwargs: Keyword arguments for func
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Any: Result of successful function execution
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
RateLimitError: If max retries exceeded or circuit breaker is open
|
|
223
|
+
Exception: Original exception if not rate limit related
|
|
224
|
+
"""
|
|
225
|
+
# Check circuit breaker
|
|
226
|
+
if self._check_circuit_breaker():
|
|
227
|
+
raise RateLimitError(
|
|
228
|
+
f"Circuit breaker is open. Too many consecutive rate limit failures. "
|
|
229
|
+
f"Will retry after {self.circuit_breaker_timeout} seconds."
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
last_error = None
|
|
233
|
+
|
|
234
|
+
for attempt in range(self.max_retries + 1):
|
|
235
|
+
try:
|
|
236
|
+
# Execute the function
|
|
237
|
+
result = await func(*args, **kwargs)
|
|
238
|
+
|
|
239
|
+
# Success - update circuit breaker and return
|
|
240
|
+
self._update_circuit_breaker(success=True)
|
|
241
|
+
|
|
242
|
+
if attempt > 0:
|
|
243
|
+
log_debug(f"Rate limit retry succeeded on attempt {attempt + 1}")
|
|
244
|
+
|
|
245
|
+
return result
|
|
246
|
+
|
|
247
|
+
except Exception as error:
|
|
248
|
+
last_error = error
|
|
249
|
+
|
|
250
|
+
# Check if this is a rate limit error
|
|
251
|
+
if not self._is_rate_limit_error(error):
|
|
252
|
+
# Not a rate limit error - re-raise immediately
|
|
253
|
+
raise error
|
|
254
|
+
|
|
255
|
+
# Update circuit breaker for rate limit failure
|
|
256
|
+
self._update_circuit_breaker(success=False)
|
|
257
|
+
|
|
258
|
+
# Check if we've exceeded max retries
|
|
259
|
+
if attempt >= self.max_retries:
|
|
260
|
+
log_debug(f"Rate limit max retries ({self.max_retries}) exceeded")
|
|
261
|
+
break
|
|
262
|
+
|
|
263
|
+
# Extract retry-after if available
|
|
264
|
+
retry_after = self._extract_retry_after(error)
|
|
265
|
+
|
|
266
|
+
# Calculate backoff delay
|
|
267
|
+
delay = self._calculate_backoff_delay(attempt, retry_after)
|
|
268
|
+
|
|
269
|
+
log_debug(
|
|
270
|
+
f"Rate limit detected on attempt {attempt + 1}, retrying in {delay:.2f}s. Error: {str(error)[:100]}"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Wait before retry
|
|
274
|
+
await asyncio.sleep(delay)
|
|
275
|
+
|
|
276
|
+
# All retries exhausted
|
|
277
|
+
raise RateLimitError(
|
|
278
|
+
f"Rate limit retries exhausted after {self.max_retries} attempts. "
|
|
279
|
+
f"Last error: {str(last_error)}"
|
|
280
|
+
)
|