agentfences 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.
- agentfences-0.1.0.dist-info/METADATA +101 -0
- agentfences-0.1.0.dist-info/RECORD +8 -0
- agentfences-0.1.0.dist-info/WHEEL +5 -0
- agentfences-0.1.0.dist-info/top_level.txt +1 -0
- fences/__init__.py +7 -0
- fences/client.py +60 -0
- fences/core.py +267 -0
- fences/exceptions.py +38 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentfences
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Runtime governance for AI agents — budget limits, loop protection, token limits, audit trail
|
|
5
|
+
Home-page: https://github.com/bhaviksardar/fences
|
|
6
|
+
Author: Bhavik Sardar
|
|
7
|
+
Keywords: ai,agents,governance,llm,budget,safety,token
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: requests>=2.25.0
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: classifier
|
|
17
|
+
Dynamic: description
|
|
18
|
+
Dynamic: description-content-type
|
|
19
|
+
Dynamic: home-page
|
|
20
|
+
Dynamic: keywords
|
|
21
|
+
Dynamic: requires-dist
|
|
22
|
+
Dynamic: requires-python
|
|
23
|
+
Dynamic: summary
|
|
24
|
+
|
|
25
|
+
# Fences
|
|
26
|
+
|
|
27
|
+
Runtime governance for AI agents. Give any agent budget limits, loop protection, and a decision audit trail in three lines of code.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import fences
|
|
31
|
+
fences.init(local_only=True)
|
|
32
|
+
|
|
33
|
+
@fences.governed(budget_usd=0.50, max_iterations=20, max_duration_ms=60_000)
|
|
34
|
+
async def run_agent(query: str):
|
|
35
|
+
response = call_llm(query)
|
|
36
|
+
fences.log_decision(reasoning="searching for sources", action="web_search")
|
|
37
|
+
await fences.checkpoint(cost_delta_usd=compute_cost(response))
|
|
38
|
+
return response
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If the agent exceeds its budget, loops past the iteration limit, or runs too long, `checkpoint()` raises and execution stops immediately.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install fences
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quickstart — no backend needed
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import fences
|
|
53
|
+
from fences import governed, checkpoint, log_decision
|
|
54
|
+
from fences import BudgetExceeded, IterationLimitReached
|
|
55
|
+
|
|
56
|
+
fences.init(local_only=True)
|
|
57
|
+
|
|
58
|
+
@governed(budget_usd=0.10, max_iterations=10)
|
|
59
|
+
async def my_agent(query: str):
|
|
60
|
+
for step in range(100):
|
|
61
|
+
log_decision(reasoning=f"step {step}: searching", action="search")
|
|
62
|
+
await checkpoint(cost_delta_usd=0.01)
|
|
63
|
+
return "done"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## What gets enforced
|
|
67
|
+
|
|
68
|
+
| Limit | Parameter | Raises |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| Spend | `budget_usd` | `BudgetExceeded` |
|
|
71
|
+
| Iterations | `max_iterations` | `IterationLimitReached` |
|
|
72
|
+
| Duration | `max_duration_ms` | `TimeLimitReached` |
|
|
73
|
+
|
|
74
|
+
## Handling breaches
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from fences import BudgetExceeded, IterationLimitReached, TimeLimitReached
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
result = await my_agent("research this topic")
|
|
81
|
+
except BudgetExceeded as e:
|
|
82
|
+
print(f"Stopped: spent ${e.spent_usd:.4f} of ${e.budget_usd:.4f}")
|
|
83
|
+
except IterationLimitReached as e:
|
|
84
|
+
print(f"Stopped: {e.iterations} iterations reached")
|
|
85
|
+
except TimeLimitReached as e:
|
|
86
|
+
print(f"Stopped: ran for {e.duration_ms}ms")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Cloud mode
|
|
90
|
+
|
|
91
|
+
Connect to a Fences backend for persistent audit trails, a dashboard, and server-authoritative enforcement across distributed agents.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
fences.init(api_key="fc_...", endpoint="https://your-fences-instance.com")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Everything else stays the same — same decorator, same `checkpoint()` calls.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
fences/__init__.py,sha256=qQg57Xvqjr3Kni_MIpHrXj74gUR-cq6KoxbvfAuOJok,376
|
|
2
|
+
fences/client.py,sha256=eR9xjN42xeOPlJwp1x6XYLy4dV_SIGKQyuCVv38Ewbg,2242
|
|
3
|
+
fences/core.py,sha256=Jk09NrxLLPr_LfXkK2_aOMLtrliBNyjgcGcK5gJd5eI,8077
|
|
4
|
+
fences/exceptions.py,sha256=5nn8VgqYso7hxODr9Eebrp8HGbw_aUb1JfKBZdpz7Qo,1249
|
|
5
|
+
agentfences-0.1.0.dist-info/METADATA,sha256=jag8Y93jLhsZaBj4kg68xEf_Vm_Bb0lrW5D32rC_iFw,2941
|
|
6
|
+
agentfences-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
7
|
+
agentfences-0.1.0.dist-info/top_level.txt,sha256=uwq9QgEQVJftVRH4_d3gzxYkOSWL0Vb_D4CImXVrPCI,7
|
|
8
|
+
agentfences-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fences
|
fences/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .core import init, governed, checkpoint, log_decision, get_active_run
|
|
2
|
+
from .exceptions import FencesError, BudgetExceeded, IterationLimitReached, TimeLimitReached, TokenLimitReached
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"init", "governed", "checkpoint", "log_decision", "get_active_run",
|
|
6
|
+
"FencesError", "BudgetExceeded", "IterationLimitReached", "TimeLimitReached", "TokenLimitReached",
|
|
7
|
+
]
|
fences/client.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class GovClient:
|
|
6
|
+
def __init__(self, api_key: str, endpoint: str, timeout: float = 3.0):
|
|
7
|
+
self.api_key = api_key
|
|
8
|
+
self.endpoint = endpoint.rstrip("/")
|
|
9
|
+
self.timeout = timeout
|
|
10
|
+
|
|
11
|
+
def start_run(self, run_id: str, agent_name: str, budget_usd: float, max_iterations: int, max_duration_ms: int, max_tokens: int) -> dict:
|
|
12
|
+
return self._post("/api/runs/start", {
|
|
13
|
+
"run_id": run_id,
|
|
14
|
+
"agent_name": agent_name,
|
|
15
|
+
"budget_usd": budget_usd,
|
|
16
|
+
"max_iterations": max_iterations,
|
|
17
|
+
"max_duration_ms": max_duration_ms,
|
|
18
|
+
"max_tokens": max_tokens,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
def checkpoint(self, run_id: str, cost_delta_usd: float, iterations: int, duration_ms: int, tokens_used: int) -> dict:
|
|
22
|
+
return self._post(f"/api/runs/{run_id}/checkpoint", {
|
|
23
|
+
"cost_delta_usd": cost_delta_usd,
|
|
24
|
+
"iterations": iterations,
|
|
25
|
+
"duration_ms": duration_ms,
|
|
26
|
+
"tokens_used": tokens_used,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
def log_decision(self, run_id: str, iteration: int, reasoning: str, action: Optional[str]) -> dict:
|
|
30
|
+
try:
|
|
31
|
+
return self._post(f"/api/runs/{run_id}/decisions", {
|
|
32
|
+
"iteration": iteration,
|
|
33
|
+
"reasoning": reasoning,
|
|
34
|
+
"action": action,
|
|
35
|
+
})
|
|
36
|
+
except Exception:
|
|
37
|
+
return {}
|
|
38
|
+
|
|
39
|
+
def end_run(self, run_id: str, status: str, error: Optional[str] = None) -> dict:
|
|
40
|
+
return self._post(f"/api/runs/{run_id}/end", {
|
|
41
|
+
"status": status,
|
|
42
|
+
"error": error,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
def _post(self, path: str, payload: dict) -> dict:
|
|
46
|
+
try:
|
|
47
|
+
resp = requests.post(
|
|
48
|
+
f"{self.endpoint}{path}",
|
|
49
|
+
json=payload,
|
|
50
|
+
headers={"X-API-Key": self.api_key},
|
|
51
|
+
timeout=self.timeout,
|
|
52
|
+
)
|
|
53
|
+
if resp.status_code in (401, 403):
|
|
54
|
+
raise PermissionError(f"Fences API key rejected: {resp.text}")
|
|
55
|
+
resp.raise_for_status()
|
|
56
|
+
return resp.json()
|
|
57
|
+
except PermissionError:
|
|
58
|
+
raise
|
|
59
|
+
except requests.RequestException as e:
|
|
60
|
+
return {"ok": True, "network_error": str(e)}
|
fences/core.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import uuid
|
|
3
|
+
import asyncio
|
|
4
|
+
import functools
|
|
5
|
+
import threading
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Optional, Callable
|
|
8
|
+
|
|
9
|
+
from .exceptions import BudgetExceeded, IterationLimitReached, TimeLimitReached, TokenLimitReached
|
|
10
|
+
from .client import GovClient
|
|
11
|
+
|
|
12
|
+
_local = threading.local()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class RunState:
|
|
17
|
+
run_id: str
|
|
18
|
+
agent_name: str
|
|
19
|
+
budget_usd: float
|
|
20
|
+
max_iterations: int
|
|
21
|
+
max_duration_ms: int
|
|
22
|
+
max_tokens: int
|
|
23
|
+
cost_usd: float = 0.0
|
|
24
|
+
iterations: int = 0
|
|
25
|
+
tokens_used: int = 0
|
|
26
|
+
started_at: float = field(default_factory=time.time)
|
|
27
|
+
decisions: list = field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def duration_ms(self) -> int:
|
|
31
|
+
return int((time.time() - self.started_at) * 1000)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_active_run() -> Optional[RunState]:
|
|
35
|
+
return getattr(_local, "run", None)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _set_active_run(run: Optional[RunState]):
|
|
39
|
+
_local.run = run
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_client: Optional[GovClient] = None
|
|
43
|
+
_local_only: bool = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def init(
|
|
47
|
+
api_key: Optional[str] = None,
|
|
48
|
+
endpoint: str = "http://localhost:8000",
|
|
49
|
+
local_only: bool = False,
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
Initialize Fences. Call once at startup before using @governed.
|
|
53
|
+
|
|
54
|
+
Local mode — no backend required:
|
|
55
|
+
fences.init(local_only=True)
|
|
56
|
+
|
|
57
|
+
Cloud mode — connects to a Fences backend:
|
|
58
|
+
fences.init(api_key="fc_...", endpoint="https://...")
|
|
59
|
+
"""
|
|
60
|
+
global _client, _local_only
|
|
61
|
+
_local_only = local_only
|
|
62
|
+
|
|
63
|
+
if local_only:
|
|
64
|
+
_client = None
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
if not api_key:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
"api_key is required unless local_only=True. "
|
|
70
|
+
"Use fences.init(local_only=True) for local usage."
|
|
71
|
+
)
|
|
72
|
+
_client = GovClient(api_key=api_key, endpoint=endpoint)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _get_client() -> Optional[GovClient]:
|
|
76
|
+
return _client
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _require_init():
|
|
80
|
+
if not _local_only and _client is None:
|
|
81
|
+
raise RuntimeError(
|
|
82
|
+
"Call fences.init() before using Fences. "
|
|
83
|
+
"For local usage: fences.init(local_only=True)"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def governed(
|
|
88
|
+
budget_usd: float,
|
|
89
|
+
max_iterations: int = 100,
|
|
90
|
+
max_duration_ms: int = 300_000,
|
|
91
|
+
max_tokens: int = 0,
|
|
92
|
+
):
|
|
93
|
+
"""
|
|
94
|
+
Decorator that applies governance policy to an agent function.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
budget_usd: Maximum spend allowed for this run in USD.
|
|
98
|
+
max_iterations: Maximum number of checkpoint() calls allowed.
|
|
99
|
+
max_duration_ms: Maximum wall-clock duration in milliseconds.
|
|
100
|
+
max_tokens: Maximum total tokens (input + output) allowed. 0 = no limit.
|
|
101
|
+
|
|
102
|
+
Raises BudgetExceeded, IterationLimitReached, TimeLimitReached, or
|
|
103
|
+
TokenLimitReached when limits are crossed.
|
|
104
|
+
|
|
105
|
+
Usage:
|
|
106
|
+
@fences.governed(budget_usd=0.50, max_iterations=20, max_tokens=50000)
|
|
107
|
+
async def run_agent(query: str):
|
|
108
|
+
response = call_llm(query)
|
|
109
|
+
await fences.checkpoint(
|
|
110
|
+
cost_delta_usd=0.02,
|
|
111
|
+
tokens_used=response.usage.total_tokens,
|
|
112
|
+
)
|
|
113
|
+
return response
|
|
114
|
+
"""
|
|
115
|
+
def decorator(func: Callable) -> Callable:
|
|
116
|
+
is_async = asyncio.iscoroutinefunction(func)
|
|
117
|
+
|
|
118
|
+
@functools.wraps(func)
|
|
119
|
+
async def async_wrapper(*args, **kwargs):
|
|
120
|
+
_require_init()
|
|
121
|
+
run = _start_run(func.__name__, budget_usd, max_iterations, max_duration_ms, max_tokens)
|
|
122
|
+
_set_active_run(run)
|
|
123
|
+
try:
|
|
124
|
+
result = await func(*args, **kwargs)
|
|
125
|
+
_end_run(run, status="success")
|
|
126
|
+
return result
|
|
127
|
+
except (BudgetExceeded, IterationLimitReached, TimeLimitReached, TokenLimitReached):
|
|
128
|
+
_end_run(run, status="breached")
|
|
129
|
+
raise
|
|
130
|
+
except Exception as e:
|
|
131
|
+
_end_run(run, status="error", error=str(e))
|
|
132
|
+
raise
|
|
133
|
+
finally:
|
|
134
|
+
_set_active_run(None)
|
|
135
|
+
|
|
136
|
+
@functools.wraps(func)
|
|
137
|
+
def sync_wrapper(*args, **kwargs):
|
|
138
|
+
_require_init()
|
|
139
|
+
run = _start_run(func.__name__, budget_usd, max_iterations, max_duration_ms, max_tokens)
|
|
140
|
+
_set_active_run(run)
|
|
141
|
+
try:
|
|
142
|
+
result = func(*args, **kwargs)
|
|
143
|
+
_end_run(run, status="success")
|
|
144
|
+
return result
|
|
145
|
+
except (BudgetExceeded, IterationLimitReached, TimeLimitReached, TokenLimitReached):
|
|
146
|
+
_end_run(run, status="breached")
|
|
147
|
+
raise
|
|
148
|
+
except Exception as e:
|
|
149
|
+
_end_run(run, status="error", error=str(e))
|
|
150
|
+
raise
|
|
151
|
+
finally:
|
|
152
|
+
_set_active_run(None)
|
|
153
|
+
|
|
154
|
+
return async_wrapper if is_async else sync_wrapper
|
|
155
|
+
|
|
156
|
+
return decorator
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _start_run(agent_name, budget_usd, max_iterations, max_duration_ms, max_tokens) -> RunState:
|
|
160
|
+
run_id = str(uuid.uuid4())
|
|
161
|
+
run = RunState(
|
|
162
|
+
run_id=run_id,
|
|
163
|
+
agent_name=agent_name,
|
|
164
|
+
budget_usd=budget_usd,
|
|
165
|
+
max_iterations=max_iterations,
|
|
166
|
+
max_duration_ms=max_duration_ms,
|
|
167
|
+
max_tokens=max_tokens,
|
|
168
|
+
)
|
|
169
|
+
client = _get_client()
|
|
170
|
+
if client:
|
|
171
|
+
client.start_run(run_id, agent_name, budget_usd, max_iterations, max_duration_ms, max_tokens)
|
|
172
|
+
return run
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _end_run(run: RunState, status: str, error: Optional[str] = None):
|
|
176
|
+
client = _get_client()
|
|
177
|
+
if client:
|
|
178
|
+
client.end_run(run.run_id, status=status, error=error)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
async def checkpoint(cost_delta_usd: float = 0.0, tokens_used: int = 0):
|
|
182
|
+
"""
|
|
183
|
+
Report spend and token usage, then check all governance limits.
|
|
184
|
+
|
|
185
|
+
Call this after each LLM call in your agent loop.
|
|
186
|
+
Raises BudgetExceeded, IterationLimitReached, TimeLimitReached,
|
|
187
|
+
or TokenLimitReached if any limit is crossed.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
cost_delta_usd: Amount spent since the last checkpoint call.
|
|
191
|
+
tokens_used: Total tokens (input + output) consumed by this step.
|
|
192
|
+
"""
|
|
193
|
+
run = get_active_run()
|
|
194
|
+
if run is None:
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
run.cost_usd += cost_delta_usd
|
|
198
|
+
run.tokens_used += tokens_used
|
|
199
|
+
run.iterations += 1
|
|
200
|
+
|
|
201
|
+
# Local checks — instant, no network
|
|
202
|
+
if run.cost_usd >= run.budget_usd:
|
|
203
|
+
raise BudgetExceeded(run.cost_usd, run.budget_usd)
|
|
204
|
+
|
|
205
|
+
if run.iterations >= run.max_iterations:
|
|
206
|
+
raise IterationLimitReached(run.iterations, run.max_iterations)
|
|
207
|
+
|
|
208
|
+
if run.duration_ms >= run.max_duration_ms:
|
|
209
|
+
raise TimeLimitReached(run.duration_ms, run.max_duration_ms)
|
|
210
|
+
|
|
211
|
+
if run.max_tokens > 0 and run.tokens_used >= run.max_tokens:
|
|
212
|
+
raise TokenLimitReached(run.tokens_used, run.max_tokens)
|
|
213
|
+
|
|
214
|
+
# Server check — cloud mode only
|
|
215
|
+
client = _get_client()
|
|
216
|
+
if client is None:
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
result = client.checkpoint(
|
|
220
|
+
run.run_id, cost_delta_usd, run.iterations, run.duration_ms, run.tokens_used
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if result.get("ok", True):
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
breach = result.get("breach")
|
|
227
|
+
if breach == "budget_exceeded":
|
|
228
|
+
raise BudgetExceeded(result.get("spent_usd", run.cost_usd), result.get("budget_usd", run.budget_usd))
|
|
229
|
+
elif breach == "iteration_limit":
|
|
230
|
+
raise IterationLimitReached(run.iterations, run.max_iterations)
|
|
231
|
+
elif breach == "time_limit":
|
|
232
|
+
raise TimeLimitReached(run.duration_ms, run.max_duration_ms)
|
|
233
|
+
elif breach == "token_limit":
|
|
234
|
+
raise TokenLimitReached(run.tokens_used, run.max_tokens)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def log_decision(reasoning: str, action: Optional[str] = None):
|
|
238
|
+
"""
|
|
239
|
+
Record the agent's reasoning at this step.
|
|
240
|
+
|
|
241
|
+
Call this before any significant action to build an audit trail
|
|
242
|
+
of why the agent did what it did.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
reasoning: Why the agent is taking this action.
|
|
246
|
+
action: Short label for the action being taken (e.g. "web_search").
|
|
247
|
+
"""
|
|
248
|
+
run = get_active_run()
|
|
249
|
+
if run is None:
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
entry = {
|
|
253
|
+
"timestamp": time.time(),
|
|
254
|
+
"iteration": run.iterations,
|
|
255
|
+
"reasoning": reasoning,
|
|
256
|
+
"action": action,
|
|
257
|
+
}
|
|
258
|
+
run.decisions.append(entry)
|
|
259
|
+
|
|
260
|
+
client = _get_client()
|
|
261
|
+
if client:
|
|
262
|
+
client.log_decision(
|
|
263
|
+
run_id=run.run_id,
|
|
264
|
+
iteration=run.iterations,
|
|
265
|
+
reasoning=reasoning,
|
|
266
|
+
action=action,
|
|
267
|
+
)
|
fences/exceptions.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class FencesError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BudgetExceeded(FencesError):
|
|
6
|
+
def __init__(self, spent_usd: float, budget_usd: float):
|
|
7
|
+
self.spent_usd = spent_usd
|
|
8
|
+
self.budget_usd = budget_usd
|
|
9
|
+
super().__init__(
|
|
10
|
+
f"Run exceeded budget: spent ${spent_usd:.4f} of ${budget_usd:.4f} limit"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class IterationLimitReached(FencesError):
|
|
15
|
+
def __init__(self, iterations: int, max_iterations: int):
|
|
16
|
+
self.iterations = iterations
|
|
17
|
+
self.max_iterations = max_iterations
|
|
18
|
+
super().__init__(
|
|
19
|
+
f"Run exceeded iteration limit: {iterations} of {max_iterations} allowed"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TimeLimitReached(FencesError):
|
|
24
|
+
def __init__(self, duration_ms: int, max_duration_ms: int):
|
|
25
|
+
self.duration_ms = duration_ms
|
|
26
|
+
self.max_duration_ms = max_duration_ms
|
|
27
|
+
super().__init__(
|
|
28
|
+
f"Run exceeded time limit: {duration_ms}ms of {max_duration_ms}ms allowed"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TokenLimitReached(FencesError):
|
|
33
|
+
def __init__(self, tokens_used: int, max_tokens: int):
|
|
34
|
+
self.tokens_used = tokens_used
|
|
35
|
+
self.max_tokens = max_tokens
|
|
36
|
+
super().__init__(
|
|
37
|
+
f"Run exceeded token limit: {tokens_used} of {max_tokens} tokens allowed"
|
|
38
|
+
)
|