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.
Files changed (42) hide show
  1. agentfield/__init__.py +66 -0
  2. agentfield/agent.py +3569 -0
  3. agentfield/agent_ai.py +1125 -0
  4. agentfield/agent_cli.py +386 -0
  5. agentfield/agent_field_handler.py +494 -0
  6. agentfield/agent_mcp.py +534 -0
  7. agentfield/agent_registry.py +29 -0
  8. agentfield/agent_server.py +1185 -0
  9. agentfield/agent_utils.py +269 -0
  10. agentfield/agent_workflow.py +323 -0
  11. agentfield/async_config.py +278 -0
  12. agentfield/async_execution_manager.py +1227 -0
  13. agentfield/client.py +1447 -0
  14. agentfield/connection_manager.py +280 -0
  15. agentfield/decorators.py +527 -0
  16. agentfield/did_manager.py +337 -0
  17. agentfield/dynamic_skills.py +304 -0
  18. agentfield/execution_context.py +255 -0
  19. agentfield/execution_state.py +453 -0
  20. agentfield/http_connection_manager.py +429 -0
  21. agentfield/litellm_adapters.py +140 -0
  22. agentfield/logger.py +249 -0
  23. agentfield/mcp_client.py +204 -0
  24. agentfield/mcp_manager.py +340 -0
  25. agentfield/mcp_stdio_bridge.py +550 -0
  26. agentfield/memory.py +723 -0
  27. agentfield/memory_events.py +489 -0
  28. agentfield/multimodal.py +173 -0
  29. agentfield/multimodal_response.py +403 -0
  30. agentfield/pydantic_utils.py +227 -0
  31. agentfield/rate_limiter.py +280 -0
  32. agentfield/result_cache.py +441 -0
  33. agentfield/router.py +190 -0
  34. agentfield/status.py +70 -0
  35. agentfield/types.py +710 -0
  36. agentfield/utils.py +26 -0
  37. agentfield/vc_generator.py +464 -0
  38. agentfield/vision.py +198 -0
  39. agentfield-0.1.22rc2.dist-info/METADATA +102 -0
  40. agentfield-0.1.22rc2.dist-info/RECORD +42 -0
  41. agentfield-0.1.22rc2.dist-info/WHEEL +5 -0
  42. 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
+ )