agent-tool-resilience 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.
@@ -0,0 +1,217 @@
1
+ """
2
+ Result validation for tool outputs.
3
+ """
4
+
5
+ import json
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, Callable, Optional, Sequence, Union
8
+
9
+
10
+ class ValidationError(Exception):
11
+ """Raised when tool output fails validation."""
12
+
13
+ def __init__(self, message: str, result: Any, errors: list[str]):
14
+ super().__init__(message)
15
+ self.result = result
16
+ self.errors = errors
17
+
18
+
19
+ @dataclass
20
+ class ResultValidator:
21
+ """
22
+ Validates tool outputs against schemas and conditions.
23
+
24
+ Attributes:
25
+ schema: JSON Schema dict for validation (requires jsonschema package)
26
+ conditions: List of predicate functions that must all return True
27
+ required_keys: Keys that must be present in dict results
28
+ type_check: Expected type for the result
29
+ on_invalid: Callback when validation fails
30
+ """
31
+ schema: Optional[dict] = None
32
+ conditions: Sequence[Callable[[Any], bool]] = field(default_factory=list)
33
+ required_keys: Sequence[str] = field(default_factory=list)
34
+ type_check: Optional[type] = None
35
+ on_invalid: Optional[Callable[[Any, list[str]], None]] = None
36
+
37
+ def validate(self, result: Any) -> Any:
38
+ """
39
+ Validate a result, raising ValidationError if invalid.
40
+
41
+ Args:
42
+ result: The result to validate
43
+
44
+ Returns:
45
+ The result if valid
46
+
47
+ Raises:
48
+ ValidationError: If validation fails
49
+ """
50
+ errors: list[str] = []
51
+
52
+ # Type check
53
+ if self.type_check is not None:
54
+ if not isinstance(result, self.type_check):
55
+ errors.append(
56
+ f"Expected type {self.type_check.__name__}, "
57
+ f"got {type(result).__name__}"
58
+ )
59
+
60
+ # Required keys check
61
+ if self.required_keys and isinstance(result, dict):
62
+ missing = [k for k in self.required_keys if k not in result]
63
+ if missing:
64
+ errors.append(f"Missing required keys: {missing}")
65
+
66
+ # JSON Schema validation
67
+ if self.schema is not None:
68
+ schema_errors = self._validate_schema(result)
69
+ errors.extend(schema_errors)
70
+
71
+ # Custom condition checks
72
+ for i, condition in enumerate(self.conditions):
73
+ try:
74
+ if not condition(result):
75
+ errors.append(f"Condition {i} failed")
76
+ except Exception as e:
77
+ errors.append(f"Condition {i} raised exception: {e}")
78
+
79
+ if errors:
80
+ if self.on_invalid:
81
+ self.on_invalid(result, errors)
82
+ raise ValidationError(
83
+ f"Validation failed with {len(errors)} error(s)",
84
+ result=result,
85
+ errors=errors
86
+ )
87
+
88
+ return result
89
+
90
+ def _validate_schema(self, result: Any) -> list[str]:
91
+ """Validate against JSON Schema."""
92
+ errors = []
93
+
94
+ try:
95
+ import jsonschema
96
+
97
+ validator = jsonschema.Draft7Validator(self.schema)
98
+ for error in validator.iter_errors(result):
99
+ path = ".".join(str(p) for p in error.path) or "root"
100
+ errors.append(f"Schema error at {path}: {error.message}")
101
+ except ImportError:
102
+ # Fall back to basic validation if jsonschema not installed
103
+ errors.extend(self._basic_schema_validate(result, self.schema))
104
+
105
+ return errors
106
+
107
+ def _basic_schema_validate(
108
+ self,
109
+ result: Any,
110
+ schema: dict,
111
+ path: str = "root"
112
+ ) -> list[str]:
113
+ """Basic schema validation without jsonschema package."""
114
+ errors = []
115
+
116
+ # Type validation
117
+ schema_type = schema.get("type")
118
+ if schema_type:
119
+ type_map = {
120
+ "string": str,
121
+ "number": (int, float),
122
+ "integer": int,
123
+ "boolean": bool,
124
+ "array": list,
125
+ "object": dict,
126
+ "null": type(None),
127
+ }
128
+ expected = type_map.get(schema_type)
129
+ if expected and not isinstance(result, expected):
130
+ errors.append(
131
+ f"Schema error at {path}: expected {schema_type}, "
132
+ f"got {type(result).__name__}"
133
+ )
134
+
135
+ # Required fields for objects
136
+ if isinstance(result, dict):
137
+ required = schema.get("required", [])
138
+ for key in required:
139
+ if key not in result:
140
+ errors.append(
141
+ f"Schema error at {path}: missing required key '{key}'"
142
+ )
143
+
144
+ # Validate properties
145
+ properties = schema.get("properties", {})
146
+ for key, prop_schema in properties.items():
147
+ if key in result:
148
+ errors.extend(
149
+ self._basic_schema_validate(
150
+ result[key],
151
+ prop_schema,
152
+ f"{path}.{key}"
153
+ )
154
+ )
155
+
156
+ # Validate array items
157
+ if isinstance(result, list):
158
+ items_schema = schema.get("items")
159
+ if items_schema:
160
+ for i, item in enumerate(result):
161
+ errors.extend(
162
+ self._basic_schema_validate(
163
+ item,
164
+ items_schema,
165
+ f"{path}[{i}]"
166
+ )
167
+ )
168
+
169
+ return errors
170
+
171
+ def is_valid(self, result: Any) -> bool:
172
+ """Check if a result is valid without raising an exception."""
173
+ try:
174
+ self.validate(result)
175
+ return True
176
+ except ValidationError:
177
+ return False
178
+
179
+
180
+ # Common validators
181
+ def not_none() -> ResultValidator:
182
+ """Validate that result is not None."""
183
+ return ResultValidator(
184
+ conditions=[lambda r: r is not None]
185
+ )
186
+
187
+
188
+ def not_empty() -> ResultValidator:
189
+ """Validate that result is not empty (for strings, lists, dicts)."""
190
+ return ResultValidator(
191
+ conditions=[lambda r: bool(r)]
192
+ )
193
+
194
+
195
+ def has_keys(*keys: str) -> ResultValidator:
196
+ """Validate that result dict has specific keys."""
197
+ return ResultValidator(required_keys=list(keys))
198
+
199
+
200
+ def matches_type(expected_type: type) -> ResultValidator:
201
+ """Validate that result is of a specific type."""
202
+ return ResultValidator(type_check=expected_type)
203
+
204
+
205
+ def in_range(
206
+ min_val: Optional[Union[int, float]] = None,
207
+ max_val: Optional[Union[int, float]] = None
208
+ ) -> ResultValidator:
209
+ """Validate that numeric result is within a range."""
210
+ conditions = []
211
+
212
+ if min_val is not None:
213
+ conditions.append(lambda r: r >= min_val)
214
+ if max_val is not None:
215
+ conditions.append(lambda r: r <= max_val)
216
+
217
+ return ResultValidator(conditions=conditions)
@@ -0,0 +1,184 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-tool-resilience
3
+ Version: 0.1.0
4
+ Summary: Production-grade resilience for AI agent tool calls: retries, fallbacks, circuit breakers, and validation
5
+ Author-email: Korah Stone <korahcomm@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/KorahStone/agent-tool-resilience
8
+ Project-URL: Documentation, https://github.com/KorahStone/agent-tool-resilience#readme
9
+ Project-URL: Repository, https://github.com/KorahStone/agent-tool-resilience.git
10
+ Project-URL: Issues, https://github.com/KorahStone/agent-tool-resilience/issues
11
+ Keywords: ai,agents,llm,resilience,retry,circuit-breaker,fallback,tools,langchain,openai
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ Provides-Extra: jsonschema
25
+ Requires-Dist: jsonschema>=4.0.0; extra == "jsonschema"
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
28
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
29
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
30
+ Provides-Extra: all
31
+ Requires-Dist: agent-tool-resilience[dev,jsonschema]; extra == "all"
32
+
33
+ # agent-tool-resilience
34
+
35
+ A Python library for making AI agent tool calls resilient, with smart retries, fallbacks, circuit breakers, and result validation.
36
+
37
+ ## Why?
38
+
39
+ AI agents fail silently when tools break. This library provides production-grade resilience patterns:
40
+
41
+ - **Smart Retries**: Exponential backoff with jitter, respects rate limits
42
+ - **Fallbacks**: Graceful degradation when primary tools fail
43
+ - **Circuit Breakers**: Prevent cascade failures by stopping calls to broken services
44
+ - **Result Validation**: Ensure tool outputs meet expected schemas/conditions
45
+ - **Observability**: Full visibility into what happened during tool execution
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ pip install agent-tool-resilience
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ ```python
56
+ from agent_tool_resilience import ResilientTool, RetryPolicy, CircuitBreaker
57
+
58
+ # Wrap any tool with resilience
59
+ @ResilientTool(
60
+ retry=RetryPolicy(max_attempts=3, backoff="exponential"),
61
+ circuit_breaker=CircuitBreaker(failure_threshold=5, reset_timeout=60),
62
+ fallback=lambda *args, **kwargs: {"error": "service unavailable", "cached": True}
63
+ )
64
+ def call_weather_api(location: str) -> dict:
65
+ return requests.get(f"https://api.weather.com/{location}").json()
66
+
67
+ # Use it normally - resilience is automatic
68
+ result = call_weather_api("NYC")
69
+ ```
70
+
71
+ ## Features
72
+
73
+ ### Retry Policies
74
+
75
+ ```python
76
+ from agent_tool_resilience import RetryPolicy
77
+
78
+ # Exponential backoff with jitter
79
+ policy = RetryPolicy(
80
+ max_attempts=5,
81
+ backoff="exponential",
82
+ base_delay=1.0,
83
+ max_delay=60.0,
84
+ jitter=True,
85
+ retry_on=[TimeoutError, ConnectionError, RateLimitError]
86
+ )
87
+ ```
88
+
89
+ ### Circuit Breakers
90
+
91
+ Prevent cascade failures by temporarily stopping calls to failing services:
92
+
93
+ ```python
94
+ from agent_tool_resilience import CircuitBreaker
95
+
96
+ breaker = CircuitBreaker(
97
+ failure_threshold=5, # Open after 5 failures
98
+ success_threshold=2, # Close after 2 successes
99
+ reset_timeout=60, # Try again after 60 seconds
100
+ half_open_max_calls=3 # Limited calls in half-open state
101
+ )
102
+ ```
103
+
104
+ ### Fallback Strategies
105
+
106
+ ```python
107
+ from agent_tool_resilience import FallbackChain
108
+
109
+ fallbacks = FallbackChain([
110
+ lambda loc: call_backup_weather_api(loc),
111
+ lambda loc: get_cached_weather(loc),
112
+ lambda loc: {"status": "unavailable", "location": loc}
113
+ ])
114
+ ```
115
+
116
+ ### Result Validation
117
+
118
+ ```python
119
+ from agent_tool_resilience import ResultValidator
120
+
121
+ validator = ResultValidator(
122
+ schema={"type": "object", "required": ["temperature", "humidity"]},
123
+ conditions=[
124
+ lambda r: r.get("temperature") is not None,
125
+ lambda r: -100 < r.get("temperature", 0) < 150
126
+ ]
127
+ )
128
+ ```
129
+
130
+ ### Observability
131
+
132
+ ```python
133
+ from agent_tool_resilience import ToolExecutionTracer
134
+
135
+ tracer = ToolExecutionTracer()
136
+
137
+ @ResilientTool(tracer=tracer)
138
+ def my_tool():
139
+ ...
140
+
141
+ # After execution
142
+ print(tracer.get_execution_log())
143
+ # [
144
+ # {"tool": "my_tool", "attempt": 1, "status": "failed", "error": "timeout", "duration_ms": 5023},
145
+ # {"tool": "my_tool", "attempt": 2, "status": "success", "duration_ms": 234}
146
+ # ]
147
+ ```
148
+
149
+ ### Rate Limit Awareness
150
+
151
+ ```python
152
+ from agent_tool_resilience import RateLimitHandler
153
+
154
+ handler = RateLimitHandler(
155
+ requests_per_minute=60,
156
+ respect_retry_after=True,
157
+ auto_throttle=True
158
+ )
159
+ ```
160
+
161
+ ## Integration with Agent Frameworks
162
+
163
+ Works with any Python agent framework:
164
+
165
+ ```python
166
+ # LangChain
167
+ from langchain.tools import Tool
168
+ from agent_tool_resilience import ResilientTool
169
+
170
+ @ResilientTool(retry=RetryPolicy(max_attempts=3))
171
+ def search(query: str) -> str:
172
+ ...
173
+
174
+ langchain_tool = Tool(name="search", func=search, description="Search the web")
175
+
176
+ # OpenAI Function Calling
177
+ @ResilientTool(circuit_breaker=CircuitBreaker(failure_threshold=3))
178
+ def get_stock_price(symbol: str) -> dict:
179
+ ...
180
+ ```
181
+
182
+ ## License
183
+
184
+ MIT
@@ -0,0 +1,12 @@
1
+ agent_tool_resilience/__init__.py,sha256=7rXh6-kxCQlZrFnAT26GquZ4_eLg7o6TSkGK_gLXeL4,950
2
+ agent_tool_resilience/circuit_breaker.py,sha256=ejC1L9qmfRpaB2AUnrJ2fvjip_llrkxYrdU3kyCROKs,8566
3
+ agent_tool_resilience/fallback.py,sha256=QSsYIwyS6UZOUzU2ISnWqydlx9Jp9YEdfrvFWzPurzw,5915
4
+ agent_tool_resilience/rate_limit.py,sha256=w5Ak05Of5CC2uKXr74IeP6CXYLkiOOsXcCxQxqKKVkc,8787
5
+ agent_tool_resilience/resilient_tool.py,sha256=bUTNMSoqViA8jqT9sjf8ttD8RtnA2fnFKPJtftyFREU,13773
6
+ agent_tool_resilience/retry.py,sha256=_b_KVmjWS5-p2UPhhj206Da1mZeN1vF3RkOge8DJbE0,6774
7
+ agent_tool_resilience/tracer.py,sha256=YRQi9naD66oRbrP8KxT7D-H82dslwH0XVoOQ59CsxFQ,10299
8
+ agent_tool_resilience/validator.py,sha256=-3SeWs5z89-v9QJ_O66u4pGBylRsIOrsXI94YB4Q3dU,7078
9
+ agent_tool_resilience-0.1.0.dist-info/METADATA,sha256=ntSPXS-hzw7t5ZLe8ua_w0_TXgaAiNlclDjQdojnokw,5344
10
+ agent_tool_resilience-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
11
+ agent_tool_resilience-0.1.0.dist-info/top_level.txt,sha256=2iwiEMf33mMYIhEGThg6xhl-k69BGa_8SKMSh95umOM,22
12
+ agent_tool_resilience-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ agent_tool_resilience