agentguard-llm 0.1.0__tar.gz
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.
- agentguard_llm-0.1.0/LICENSE +21 -0
- agentguard_llm-0.1.0/PKG-INFO +370 -0
- agentguard_llm-0.1.0/README.md +336 -0
- agentguard_llm-0.1.0/agentguard/__init__.py +39 -0
- agentguard_llm-0.1.0/agentguard/agent_wrapper.py +133 -0
- agentguard_llm-0.1.0/agentguard/circuit_breaker.py +80 -0
- agentguard_llm-0.1.0/agentguard/exceptions.py +14 -0
- agentguard_llm-0.1.0/agentguard/failure_classifier.py +62 -0
- agentguard_llm-0.1.0/agentguard/idempotency.py +53 -0
- agentguard_llm-0.1.0/agentguard/retry.py +54 -0
- agentguard_llm-0.1.0/agentguard_llm.egg-info/PKG-INFO +370 -0
- agentguard_llm-0.1.0/agentguard_llm.egg-info/SOURCES.txt +15 -0
- agentguard_llm-0.1.0/agentguard_llm.egg-info/dependency_links.txt +1 -0
- agentguard_llm-0.1.0/agentguard_llm.egg-info/top_level.txt +1 -0
- agentguard_llm-0.1.0/setup.cfg +4 -0
- agentguard_llm-0.1.0/setup.py +36 -0
- agentguard_llm-0.1.0/tests/test_agentguard.py +97 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AgentGuard Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentguard-llm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Production-grade fault tolerance for AI agents — circuit breakers, LLM-aware retry, idempotency, and loop detection
|
|
5
|
+
Home-page: https://github.com/agentguard-ai/agentguard
|
|
6
|
+
Author: AgentGuard Contributors
|
|
7
|
+
Project-URL: Bug Reports, https://github.com/agentguard-ai/agentguard/issues
|
|
8
|
+
Project-URL: Source, https://github.com/agentguard-ai/agentguard
|
|
9
|
+
Keywords: ai agents,llm,fault tolerance,circuit breaker,retry,idempotency,agent reliability,ai production,langchain,autogen,crewai,agent failure,llm retry
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Dynamic: author
|
|
25
|
+
Dynamic: classifier
|
|
26
|
+
Dynamic: description
|
|
27
|
+
Dynamic: description-content-type
|
|
28
|
+
Dynamic: home-page
|
|
29
|
+
Dynamic: keywords
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
Dynamic: project-url
|
|
32
|
+
Dynamic: requires-python
|
|
33
|
+
Dynamic: summary
|
|
34
|
+
|
|
35
|
+
# agentguard — Production-Grade Fault Tolerance for AI Agents
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/agentguard/)
|
|
38
|
+
[](https://pypi.org/project/agentguard/)
|
|
39
|
+
[](https://opensource.org/licenses/MIT)
|
|
40
|
+
|
|
41
|
+
**agentguard** is a production-ready Python library that adds circuit breakers, LLM-aware retry logic, idempotency, loop detection, and timeout enforcement to any AI agent or LLM pipeline.
|
|
42
|
+
|
|
43
|
+
> AI agents fail at 91%+ rates in production. agentguard stops that.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## The Problem
|
|
48
|
+
|
|
49
|
+
AI agents built with LangChain, AutoGen, CrewAI, or custom LLM pipelines fail catastrophically in production due to:
|
|
50
|
+
|
|
51
|
+
- **Infinite loops** — agents repeat the same tool calls indefinitely
|
|
52
|
+
- **Silent failures** — LLM errors swallowed without retry or alerting
|
|
53
|
+
- **Duplicate actions** — the same expensive LLM call fires multiple times
|
|
54
|
+
- **Rate limit crashes** — no intelligent backoff for 429/503 errors
|
|
55
|
+
- **Token limit blindness** — agents don't know when to stop or summarize
|
|
56
|
+
- **No circuit breaking** — one bad model call cascades into total failure
|
|
57
|
+
|
|
58
|
+
Existing tools like `tenacity`, LangGraph, and CrewAI are not LLM-aware and don't address agent-specific failure modes.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Features
|
|
63
|
+
|
|
64
|
+
- **Circuit Breaker** — Automatically opens after N failures, protecting downstream LLM APIs from cascading overload
|
|
65
|
+
- **LLM-Aware Retry** — Classifies errors (rate limit, token limit, provider outage, hallucinated tool call) and applies appropriate backoff
|
|
66
|
+
- **Idempotency** — Caches results by key to prevent duplicate expensive LLM executions
|
|
67
|
+
- **Loop Detection** — Detects and halts infinite agent action loops before they run up your API bill
|
|
68
|
+
- **Timeout Enforcement** — Hard timeouts on any agent step, with clean error propagation
|
|
69
|
+
- **Zero Dependencies** — Pure Python standard library only; works with any LLM framework
|
|
70
|
+
- **Full Observability** — Built-in stats, logging at every layer, structured error types
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Installation
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install agentguard
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Quick Start
|
|
83
|
+
|
|
84
|
+
### GuardedAgent — Full Protection in One Wrapper
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from agentguard import GuardedAgent
|
|
88
|
+
|
|
89
|
+
agent = GuardedAgent(
|
|
90
|
+
name="my_llm_agent",
|
|
91
|
+
max_retries=3,
|
|
92
|
+
circuit_threshold=5,
|
|
93
|
+
timeout=30.0,
|
|
94
|
+
loop_detection=True,
|
|
95
|
+
max_repeated_actions=3,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def call_llm(prompt: str) -> str:
|
|
99
|
+
# Your actual LLM call here (OpenAI, Anthropic, etc.)
|
|
100
|
+
return f"Response to: {prompt}"
|
|
101
|
+
|
|
102
|
+
result = agent.run(call_llm, "What is the capital of France?", action_label="llm_call")
|
|
103
|
+
print(result)
|
|
104
|
+
print(agent.get_stats())
|
|
105
|
+
# {'name': 'my_llm_agent', 'total_calls': 1, 'total_failures': 0, 'circuit': {...}}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Circuit Breaker — Standalone
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from agentguard import CircuitBreaker
|
|
112
|
+
|
|
113
|
+
cb = CircuitBreaker(failure_threshold=3, recovery_timeout=60.0, name="openai")
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
response = cb.call(call_llm, "Hello")
|
|
117
|
+
except Exception as e:
|
|
118
|
+
print(f"Protected from cascading failure: {e}")
|
|
119
|
+
|
|
120
|
+
print(cb.get_stats())
|
|
121
|
+
# {'name': 'openai', 'state': 'closed', 'failure_count': 0, ...}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### LLM-Aware Retry — Decorator Style
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from agentguard import llm_retry
|
|
128
|
+
|
|
129
|
+
@llm_retry(max_attempts=3)
|
|
130
|
+
def my_agent_step(query: str) -> str:
|
|
131
|
+
# Automatically retries on rate limits (429), provider outages (503)
|
|
132
|
+
# Stops immediately on token limit errors (non-retryable)
|
|
133
|
+
return call_llm(query)
|
|
134
|
+
|
|
135
|
+
result = my_agent_step("Summarize this document")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### LLM-Aware Retry — Programmatic
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from agentguard import LLMRetry, FailureClassifier
|
|
142
|
+
|
|
143
|
+
retry = LLMRetry(max_attempts=5, on_retry=lambda attempt, ftype, err: print(f"Retry {attempt}: {ftype}"))
|
|
144
|
+
result = retry.execute(call_llm, "Hello")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Idempotency — Prevent Duplicate Executions
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from agentguard import IdempotentAgent
|
|
151
|
+
|
|
152
|
+
agent = IdempotentAgent(ttl=3600.0)
|
|
153
|
+
|
|
154
|
+
# First call executes the function
|
|
155
|
+
result1 = agent.run(call_llm, "Summarize report", idempotency_key="report-summary-v1")
|
|
156
|
+
|
|
157
|
+
# Second call with same key returns cached result instantly
|
|
158
|
+
result2 = agent.run(call_llm, "Summarize report", idempotency_key="report-summary-v1")
|
|
159
|
+
|
|
160
|
+
assert result1 == result2 # True — function only ran once
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Failure Classifier — Understand What Went Wrong
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from agentguard import FailureClassifier, FailureType
|
|
167
|
+
|
|
168
|
+
fc = FailureClassifier()
|
|
169
|
+
|
|
170
|
+
err = Exception("429 Too Many Requests: rate limit exceeded")
|
|
171
|
+
ftype = fc.classify(err)
|
|
172
|
+
# FailureType.RATE_LIMIT
|
|
173
|
+
|
|
174
|
+
print(fc.is_retryable(ftype)) # True
|
|
175
|
+
print(fc.get_retry_delay(ftype, attempt=1)) # 10.0 seconds
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Architecture
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
agentguard/
|
|
184
|
+
├── agent_wrapper.py # GuardedAgent — orchestrates all protections
|
|
185
|
+
├── circuit_breaker.py # CircuitBreaker — CLOSED/OPEN/HALF_OPEN state machine
|
|
186
|
+
├── retry.py # LLMRetry + llm_retry decorator — intelligent backoff
|
|
187
|
+
├── idempotency.py # IdempotentAgent + IdempotencyStore — result caching
|
|
188
|
+
├── failure_classifier.py # FailureClassifier — LLM error pattern recognition
|
|
189
|
+
└── exceptions.py # Typed exception hierarchy
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Data Flow
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
Agent call
|
|
196
|
+
│
|
|
197
|
+
├─► Loop Detector (infinite loop check)
|
|
198
|
+
│
|
|
199
|
+
├─► Circuit Breaker (OPEN? → reject immediately)
|
|
200
|
+
│
|
|
201
|
+
├─► Idempotency Store (seen this key? → return cached)
|
|
202
|
+
│
|
|
203
|
+
├─► LLM Retry (classify failure → smart backoff → retry)
|
|
204
|
+
│
|
|
205
|
+
└─► Timeout Thread (hard deadline enforcement)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Failure Classification Logic
|
|
209
|
+
|
|
210
|
+
| Error Pattern | Classified As | Retryable | Backoff |
|
|
211
|
+
|---|---|---|---|
|
|
212
|
+
| `429`, `rate limit`, `quota exceeded` | RATE_LIMIT | Yes | Exponential (5s base, max 60s) |
|
|
213
|
+
| `503`, `service unavailable`, `overloaded` | PROVIDER_OUTAGE | Yes | Exponential (10s base, max 120s) |
|
|
214
|
+
| `context length`, `token limit` | TOKEN_LIMIT | No | — |
|
|
215
|
+
| `tool not found`, `invalid tool` | HALLUCINATED_TOOL_CALL | No | — |
|
|
216
|
+
| anything else | UNKNOWN | Yes | Exponential (1s base, max 30s) |
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## API Reference
|
|
221
|
+
|
|
222
|
+
### `GuardedAgent`
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
GuardedAgent(
|
|
226
|
+
name: str = "agent",
|
|
227
|
+
max_retries: int = 3,
|
|
228
|
+
circuit_threshold: int = 5,
|
|
229
|
+
circuit_recovery: float = 60.0,
|
|
230
|
+
timeout: Optional[float] = None,
|
|
231
|
+
loop_detection: bool = True,
|
|
232
|
+
max_repeated_actions: int = 3,
|
|
233
|
+
idempotency_ttl: float = 3600.0,
|
|
234
|
+
enable_idempotency: bool = True,
|
|
235
|
+
)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Methods:**
|
|
239
|
+
- `run(func, *args, action_label=None, idempotency_key=None, **kwargs)` — Execute with all protections
|
|
240
|
+
- `get_stats()` — Returns dict with call counts, failure counts, circuit state
|
|
241
|
+
- `reset_loop_detector()` — Clear the loop detection history
|
|
242
|
+
|
|
243
|
+
### `CircuitBreaker`
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
CircuitBreaker(
|
|
247
|
+
failure_threshold: int = 5,
|
|
248
|
+
recovery_timeout: float = 60.0,
|
|
249
|
+
half_open_max_calls: int = 1,
|
|
250
|
+
name: str = "default"
|
|
251
|
+
)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Methods:**
|
|
255
|
+
- `call(func, *args, **kwargs)` — Protected function call
|
|
256
|
+
- `get_stats()` — Returns state, failure count, last failure time
|
|
257
|
+
|
|
258
|
+
### `LLMRetry`
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
LLMRetry(
|
|
262
|
+
max_attempts: int = 3,
|
|
263
|
+
classifier: Optional[FailureClassifier] = None,
|
|
264
|
+
on_retry: Optional[Callable] = None,
|
|
265
|
+
)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Methods:**
|
|
269
|
+
- `execute(func, *args, **kwargs)` — Execute with retry logic
|
|
270
|
+
|
|
271
|
+
### `llm_retry` decorator
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
@llm_retry(max_attempts=3, classifier=None)
|
|
275
|
+
def my_func(): ...
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### `IdempotentAgent`
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
IdempotentAgent(store=None, ttl=3600.0)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Methods:**
|
|
285
|
+
- `run(func, *args, idempotency_key=None, **kwargs)` — Execute with deduplication
|
|
286
|
+
|
|
287
|
+
### `FailureClassifier`
|
|
288
|
+
|
|
289
|
+
**Methods:**
|
|
290
|
+
- `classify(error: Exception) -> FailureType`
|
|
291
|
+
- `is_retryable(failure_type: FailureType) -> bool`
|
|
292
|
+
- `get_retry_delay(failure_type: FailureType, attempt: int) -> float`
|
|
293
|
+
|
|
294
|
+
### Exceptions
|
|
295
|
+
|
|
296
|
+
| Exception | When Raised |
|
|
297
|
+
|---|---|
|
|
298
|
+
| `AgentGuardError` | Base class for all agentguard errors |
|
|
299
|
+
| `CircuitOpenError` | Circuit breaker is OPEN, call rejected |
|
|
300
|
+
| `MaxRetriesExceededError` | All retry attempts exhausted |
|
|
301
|
+
| `IdempotencyError` | Idempotency store conflict |
|
|
302
|
+
| `AgentTimeoutError` | Agent exceeded timeout limit |
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Real-World Integration Examples
|
|
307
|
+
|
|
308
|
+
### With OpenAI
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
import openai
|
|
312
|
+
from agentguard import GuardedAgent
|
|
313
|
+
|
|
314
|
+
agent = GuardedAgent(name="openai-agent", max_retries=3, timeout=30.0)
|
|
315
|
+
|
|
316
|
+
def gpt_call(prompt: str) -> str:
|
|
317
|
+
response = openai.chat.completions.create(
|
|
318
|
+
model="gpt-4",
|
|
319
|
+
messages=[{"role": "user", "content": prompt}]
|
|
320
|
+
)
|
|
321
|
+
return response.choices[0].message.content
|
|
322
|
+
|
|
323
|
+
result = agent.run(gpt_call, "Explain quantum computing", action_label="gpt_call")
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### With LangChain
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
from agentguard import llm_retry
|
|
330
|
+
|
|
331
|
+
@llm_retry(max_attempts=3)
|
|
332
|
+
def run_chain(input_text: str):
|
|
333
|
+
return my_langchain_chain.invoke({"input": input_text})
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### With CrewAI / AutoGen
|
|
337
|
+
|
|
338
|
+
```python
|
|
339
|
+
from agentguard import GuardedAgent
|
|
340
|
+
|
|
341
|
+
guard = GuardedAgent(name="crew-agent", loop_detection=True, max_repeated_actions=5)
|
|
342
|
+
|
|
343
|
+
# Wrap any crew task execution
|
|
344
|
+
result = guard.run(crew.kickoff, action_label="crew_task")
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Contributing
|
|
350
|
+
|
|
351
|
+
Contributions are welcome. Please open an issue first to discuss what you would like to change.
|
|
352
|
+
|
|
353
|
+
1. Fork the repository
|
|
354
|
+
2. Create your feature branch (`git checkout -b feature/your-feature`)
|
|
355
|
+
3. Run tests: `pytest tests/ -v`
|
|
356
|
+
4. Submit a pull request
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## License
|
|
361
|
+
|
|
362
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Links
|
|
367
|
+
|
|
368
|
+
- **PyPI**: https://pypi.org/project/agentguard/
|
|
369
|
+
- **GitHub**: https://github.com/agentguard-ai/agentguard
|
|
370
|
+
- **Issues**: https://github.com/agentguard-ai/agentguard/issues
|