onceonly-sdk 2.0.2__py3-none-any.whl → 3.0.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.
- onceonly/__init__.py +26 -1
- onceonly/_http.py +26 -4
- onceonly/_util.py +3 -1
- onceonly/ai.py +378 -31
- onceonly/ai_models.py +27 -0
- onceonly/client.py +77 -4
- onceonly/decorators.py +87 -1
- onceonly/governance.py +471 -0
- onceonly/models.py +58 -7
- onceonly/version.py +1 -1
- onceonly_sdk-3.0.0.dist-info/METADATA +1031 -0
- onceonly_sdk-3.0.0.dist-info/RECORD +18 -0
- {onceonly_sdk-2.0.2.dist-info → onceonly_sdk-3.0.0.dist-info}/WHEEL +1 -1
- onceonly_sdk-2.0.2.dist-info/METADATA +0 -216
- onceonly_sdk-2.0.2.dist-info/RECORD +0 -17
- {onceonly_sdk-2.0.2.dist-info → onceonly_sdk-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {onceonly_sdk-2.0.2.dist-info → onceonly_sdk-3.0.0.dist-info}/top_level.txt +0 -0
onceonly/client.py
CHANGED
|
@@ -16,6 +16,7 @@ from ._http import (
|
|
|
16
16
|
)
|
|
17
17
|
from .exceptions import ApiError, UnauthorizedError, OverLimitError, RateLimitError, ValidationError
|
|
18
18
|
from .ai import AiClient
|
|
19
|
+
from .governance import GovernanceClient
|
|
19
20
|
from .version import __version__
|
|
20
21
|
from ._util import to_metadata_dict, MetadataLike
|
|
21
22
|
|
|
@@ -88,6 +89,14 @@ class OnceOnly:
|
|
|
88
89
|
retry_max_backoff=self._retry_max_backoff,
|
|
89
90
|
)
|
|
90
91
|
|
|
92
|
+
self.gov = GovernanceClient(
|
|
93
|
+
self._sync_client,
|
|
94
|
+
self._get_async_client,
|
|
95
|
+
max_retries_429=self._max_retries_429,
|
|
96
|
+
retry_backoff=self._retry_backoff,
|
|
97
|
+
retry_max_backoff=self._retry_max_backoff,
|
|
98
|
+
)
|
|
99
|
+
|
|
91
100
|
# ---------- Public API ----------
|
|
92
101
|
|
|
93
102
|
def check_lock(
|
|
@@ -150,10 +159,10 @@ class OnceOnly:
|
|
|
150
159
|
raise
|
|
151
160
|
|
|
152
161
|
# thin wrapper for agent UX
|
|
153
|
-
def ai_run_and_wait(self, key: str, **kwargs):
|
|
162
|
+
def ai_run_and_wait(self, key: Optional[str] = None, **kwargs):
|
|
154
163
|
return self.ai.run_and_wait(key=key, **kwargs)
|
|
155
164
|
|
|
156
|
-
async def ai_run_and_wait_async(self, key: str, **kwargs):
|
|
165
|
+
async def ai_run_and_wait_async(self, key: Optional[str] = None, **kwargs):
|
|
157
166
|
return await self.ai.run_and_wait_async(key=key, **kwargs)
|
|
158
167
|
|
|
159
168
|
def me(self) -> Dict[str, Any]:
|
|
@@ -194,6 +203,63 @@ class OnceOnly:
|
|
|
194
203
|
)
|
|
195
204
|
return parse_json_or_raise(resp)
|
|
196
205
|
|
|
206
|
+
def usage_all(self) -> Dict[str, Any]:
|
|
207
|
+
resp = request_with_retries_sync(
|
|
208
|
+
lambda: self._sync_client.get("/usage/all"),
|
|
209
|
+
max_retries=self._max_retries_429,
|
|
210
|
+
base_backoff=self._retry_backoff,
|
|
211
|
+
max_backoff=self._retry_max_backoff,
|
|
212
|
+
)
|
|
213
|
+
return parse_json_or_raise(resp)
|
|
214
|
+
|
|
215
|
+
async def usage_all_async(self) -> Dict[str, Any]:
|
|
216
|
+
client = await self._get_async_client()
|
|
217
|
+
resp = await request_with_retries_async(
|
|
218
|
+
lambda: client.get("/usage/all"),
|
|
219
|
+
max_retries=self._max_retries_429,
|
|
220
|
+
base_backoff=self._retry_backoff,
|
|
221
|
+
max_backoff=self._retry_max_backoff,
|
|
222
|
+
)
|
|
223
|
+
return parse_json_or_raise(resp)
|
|
224
|
+
|
|
225
|
+
def events(self, limit: int = 50) -> Any:
|
|
226
|
+
resp = request_with_retries_sync(
|
|
227
|
+
lambda: self._sync_client.get("/events", params={"limit": int(limit)}),
|
|
228
|
+
max_retries=self._max_retries_429,
|
|
229
|
+
base_backoff=self._retry_backoff,
|
|
230
|
+
max_backoff=self._retry_max_backoff,
|
|
231
|
+
)
|
|
232
|
+
return parse_json_or_raise(resp)
|
|
233
|
+
|
|
234
|
+
async def events_async(self, limit: int = 50) -> Any:
|
|
235
|
+
client = await self._get_async_client()
|
|
236
|
+
resp = await request_with_retries_async(
|
|
237
|
+
lambda: client.get("/events", params={"limit": int(limit)}),
|
|
238
|
+
max_retries=self._max_retries_429,
|
|
239
|
+
base_backoff=self._retry_backoff,
|
|
240
|
+
max_backoff=self._retry_max_backoff,
|
|
241
|
+
)
|
|
242
|
+
return parse_json_or_raise(resp)
|
|
243
|
+
|
|
244
|
+
def metrics(self, from_day: str, to_day: str) -> Any:
|
|
245
|
+
resp = request_with_retries_sync(
|
|
246
|
+
lambda: self._sync_client.get("/metrics", params={"from_day": from_day, "to_day": to_day}),
|
|
247
|
+
max_retries=self._max_retries_429,
|
|
248
|
+
base_backoff=self._retry_backoff,
|
|
249
|
+
max_backoff=self._retry_max_backoff,
|
|
250
|
+
)
|
|
251
|
+
return parse_json_or_raise(resp)
|
|
252
|
+
|
|
253
|
+
async def metrics_async(self, from_day: str, to_day: str) -> Any:
|
|
254
|
+
client = await self._get_async_client()
|
|
255
|
+
resp = await request_with_retries_async(
|
|
256
|
+
lambda: client.get("/metrics", params={"from_day": from_day, "to_day": to_day}),
|
|
257
|
+
max_retries=self._max_retries_429,
|
|
258
|
+
base_backoff=self._retry_backoff,
|
|
259
|
+
max_backoff=self._retry_max_backoff,
|
|
260
|
+
)
|
|
261
|
+
return parse_json_or_raise(resp)
|
|
262
|
+
|
|
197
263
|
def close(self) -> None:
|
|
198
264
|
if self._own_sync:
|
|
199
265
|
self._sync_client.close()
|
|
@@ -321,8 +387,15 @@ class OnceOnly:
|
|
|
321
387
|
status = str(data.get("status") or "").strip().lower()
|
|
322
388
|
success = data.get("success")
|
|
323
389
|
|
|
324
|
-
|
|
325
|
-
|
|
390
|
+
if status in ("locked", "duplicate"):
|
|
391
|
+
locked = status == "locked"
|
|
392
|
+
duplicate = status == "duplicate"
|
|
393
|
+
elif oo_status in ("locked", "duplicate"):
|
|
394
|
+
locked = oo_status == "locked"
|
|
395
|
+
duplicate = oo_status == "duplicate"
|
|
396
|
+
else:
|
|
397
|
+
locked = bool(success)
|
|
398
|
+
duplicate = not bool(success)
|
|
326
399
|
|
|
327
400
|
raw = data if isinstance(data, dict) else {}
|
|
328
401
|
md = to_metadata_dict(fallback_meta)
|
onceonly/decorators.py
CHANGED
|
@@ -5,13 +5,17 @@ import inspect
|
|
|
5
5
|
import json
|
|
6
6
|
import hashlib
|
|
7
7
|
import logging
|
|
8
|
-
from typing import Any, Callable, Optional, TypeVar
|
|
8
|
+
from typing import Any, Callable, Optional, TypeVar, Awaitable, Union
|
|
9
9
|
|
|
10
10
|
from .client import OnceOnly
|
|
11
|
+
from ._util import MetadataLike
|
|
12
|
+
from .ai_models import AiResult
|
|
11
13
|
|
|
12
14
|
logger = logging.getLogger("onceonly")
|
|
13
15
|
|
|
14
16
|
T = TypeVar("T")
|
|
17
|
+
KeyFn = Callable[..., str]
|
|
18
|
+
MetaFn = Callable[..., Optional[MetadataLike]]
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
def _truncate(s: str, max_len: int = 2048) -> str:
|
|
@@ -172,3 +176,85 @@ def idempotent(
|
|
|
172
176
|
return sync_wrapper # type: ignore[return-value]
|
|
173
177
|
|
|
174
178
|
return decorator
|
|
179
|
+
|
|
180
|
+
def idempotent_ai(
|
|
181
|
+
client: OnceOnly,
|
|
182
|
+
*,
|
|
183
|
+
key: Optional[str] = None,
|
|
184
|
+
key_fn: Optional[KeyFn] = None,
|
|
185
|
+
ttl: int = 300,
|
|
186
|
+
metadata: Optional[MetadataLike] = None,
|
|
187
|
+
metadata_fn: Optional[MetaFn] = None,
|
|
188
|
+
extend_every: float = 30.0,
|
|
189
|
+
wait_on_conflict: bool = True,
|
|
190
|
+
timeout: float = 60.0,
|
|
191
|
+
poll_min: float = 0.5,
|
|
192
|
+
poll_max: float = 5.0,
|
|
193
|
+
error_code: str = "fn_error",
|
|
194
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Union[AiResult, Awaitable[AiResult]]]]:
|
|
195
|
+
"""
|
|
196
|
+
AI Lease decorator (exactly-once local execution).
|
|
197
|
+
|
|
198
|
+
Provide either:
|
|
199
|
+
- key="fixed:key"
|
|
200
|
+
- key_fn(*args, **kwargs) -> str
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
if key is None and key_fn is None:
|
|
204
|
+
raise ValueError("idempotent_ai requires either key=... or key_fn=...")
|
|
205
|
+
|
|
206
|
+
def _make_key(args: tuple, kwargs: dict) -> str:
|
|
207
|
+
if key is not None:
|
|
208
|
+
return str(key)
|
|
209
|
+
assert key_fn is not None
|
|
210
|
+
return str(key_fn(*args, **kwargs))
|
|
211
|
+
|
|
212
|
+
def _make_meta(args: tuple, kwargs: dict) -> Optional[MetadataLike]:
|
|
213
|
+
if metadata_fn is not None:
|
|
214
|
+
return metadata_fn(*args, **kwargs)
|
|
215
|
+
return metadata
|
|
216
|
+
|
|
217
|
+
def decorator(fn: Callable[..., Any]):
|
|
218
|
+
if inspect.iscoroutinefunction(fn):
|
|
219
|
+
|
|
220
|
+
@functools.wraps(fn)
|
|
221
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> AiResult:
|
|
222
|
+
k = _make_key(args, kwargs)
|
|
223
|
+
md = _make_meta(args, kwargs)
|
|
224
|
+
|
|
225
|
+
return await client.ai.run_fn_async(
|
|
226
|
+
key=k,
|
|
227
|
+
fn=lambda: fn(*args, **kwargs), # returns awaitable; run_fn_async awaits it
|
|
228
|
+
ttl=int(ttl),
|
|
229
|
+
metadata=md,
|
|
230
|
+
extend_every=float(extend_every),
|
|
231
|
+
wait_on_conflict=bool(wait_on_conflict),
|
|
232
|
+
timeout=float(timeout),
|
|
233
|
+
poll_min=float(poll_min),
|
|
234
|
+
poll_max=float(poll_max),
|
|
235
|
+
error_code=str(error_code),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return async_wrapper
|
|
239
|
+
|
|
240
|
+
@functools.wraps(fn)
|
|
241
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> AiResult:
|
|
242
|
+
k = _make_key(args, kwargs)
|
|
243
|
+
md = _make_meta(args, kwargs)
|
|
244
|
+
|
|
245
|
+
return client.ai.run_fn(
|
|
246
|
+
key=k,
|
|
247
|
+
fn=lambda: fn(*args, **kwargs),
|
|
248
|
+
ttl=int(ttl),
|
|
249
|
+
metadata=md,
|
|
250
|
+
extend_every=float(extend_every),
|
|
251
|
+
wait_on_conflict=bool(wait_on_conflict),
|
|
252
|
+
timeout=float(timeout),
|
|
253
|
+
poll_min=float(poll_min),
|
|
254
|
+
poll_max=float(poll_max),
|
|
255
|
+
error_code=str(error_code),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return sync_wrapper
|
|
259
|
+
|
|
260
|
+
return decorator
|
onceonly/governance.py
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional, Literal
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from ._http import parse_json_or_raise, request_with_retries_sync, request_with_retries_async
|
|
8
|
+
from .models import Policy, AgentStatus, AgentLogItem, AgentMetrics
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GovernanceClient:
|
|
12
|
+
"""
|
|
13
|
+
Agent Governance API:
|
|
14
|
+
- Policies (limits/permissions)
|
|
15
|
+
- Kill switch (enable/disable)
|
|
16
|
+
- Audit logs + metrics
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
sync_client: httpx.Client,
|
|
22
|
+
async_client_getter: Callable[[], Awaitable[httpx.AsyncClient]],
|
|
23
|
+
*,
|
|
24
|
+
max_retries_429: int = 0,
|
|
25
|
+
retry_backoff: float = 0.5,
|
|
26
|
+
retry_max_backoff: float = 5.0,
|
|
27
|
+
):
|
|
28
|
+
self._c = sync_client
|
|
29
|
+
self._get_ac = async_client_getter
|
|
30
|
+
self._max_retries_429 = int(max_retries_429)
|
|
31
|
+
self._retry_backoff = float(retry_backoff)
|
|
32
|
+
self._retry_max_backoff = float(retry_max_backoff)
|
|
33
|
+
|
|
34
|
+
# -------- Policies --------
|
|
35
|
+
|
|
36
|
+
def _policy_from_response(
|
|
37
|
+
self,
|
|
38
|
+
j: Any,
|
|
39
|
+
*,
|
|
40
|
+
fallback_agent_id: Optional[str] = None,
|
|
41
|
+
fallback_policy: Optional[Dict[str, Any]] = None,
|
|
42
|
+
) -> Policy:
|
|
43
|
+
if isinstance(j, dict):
|
|
44
|
+
agent_id = str(j.get("agent_id") or fallback_agent_id or "")
|
|
45
|
+
pol = j.get("policy") if isinstance(j.get("policy"), dict) else (fallback_policy or j)
|
|
46
|
+
else:
|
|
47
|
+
agent_id = str(fallback_agent_id or "")
|
|
48
|
+
pol = fallback_policy or {}
|
|
49
|
+
|
|
50
|
+
if not isinstance(pol, dict):
|
|
51
|
+
pol = {}
|
|
52
|
+
|
|
53
|
+
return Policy(
|
|
54
|
+
agent_id=agent_id,
|
|
55
|
+
policy=pol,
|
|
56
|
+
max_actions_per_hour=pol.get("max_actions_per_hour"),
|
|
57
|
+
max_spend_usd_per_day=pol.get("max_spend_usd_per_day"),
|
|
58
|
+
allowed_tools=pol.get("allowed_tools"),
|
|
59
|
+
blocked_tools=pol.get("blocked_tools"),
|
|
60
|
+
max_calls_per_tool=pol.get("max_calls_per_tool"),
|
|
61
|
+
pricing_rules=pol.get("pricing_rules"),
|
|
62
|
+
raw=j if isinstance(j, dict) else None,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def _extract_list(value: Any) -> List[Any]:
|
|
67
|
+
if isinstance(value, list):
|
|
68
|
+
return value
|
|
69
|
+
if isinstance(value, dict):
|
|
70
|
+
items = value.get("items")
|
|
71
|
+
if isinstance(items, list):
|
|
72
|
+
return items
|
|
73
|
+
data = value.get("data")
|
|
74
|
+
if isinstance(data, list):
|
|
75
|
+
return data
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
def upsert_policy(self, policy: Dict[str, Any], *, agent_id: Optional[str] = None) -> Policy:
|
|
79
|
+
agent_id = agent_id or str(policy.get("agent_id") or "")
|
|
80
|
+
if not agent_id:
|
|
81
|
+
raise ValueError("upsert_policy requires agent_id")
|
|
82
|
+
|
|
83
|
+
payload = dict(policy)
|
|
84
|
+
payload["agent_id"] = agent_id
|
|
85
|
+
|
|
86
|
+
data = request_with_retries_sync(
|
|
87
|
+
lambda: self._c.post(f"/policies/{agent_id}", json=payload),
|
|
88
|
+
max_retries=self._max_retries_429,
|
|
89
|
+
base_backoff=self._retry_backoff,
|
|
90
|
+
max_backoff=self._retry_max_backoff,
|
|
91
|
+
)
|
|
92
|
+
j = parse_json_or_raise(data)
|
|
93
|
+
return self._policy_from_response(j, fallback_agent_id=agent_id, fallback_policy=payload)
|
|
94
|
+
|
|
95
|
+
async def upsert_policy_async(self, policy: Dict[str, Any], *, agent_id: Optional[str] = None) -> Policy:
|
|
96
|
+
agent_id = agent_id or str(policy.get("agent_id") or "")
|
|
97
|
+
if not agent_id:
|
|
98
|
+
raise ValueError("upsert_policy_async requires agent_id")
|
|
99
|
+
|
|
100
|
+
payload = dict(policy)
|
|
101
|
+
payload["agent_id"] = agent_id
|
|
102
|
+
|
|
103
|
+
ac = await self._get_ac()
|
|
104
|
+
resp = await request_with_retries_async(
|
|
105
|
+
lambda: ac.post(f"/policies/{agent_id}", json=payload),
|
|
106
|
+
max_retries=self._max_retries_429,
|
|
107
|
+
base_backoff=self._retry_backoff,
|
|
108
|
+
max_backoff=self._retry_max_backoff,
|
|
109
|
+
)
|
|
110
|
+
j = parse_json_or_raise(resp)
|
|
111
|
+
return self._policy_from_response(j, fallback_agent_id=agent_id, fallback_policy=payload)
|
|
112
|
+
|
|
113
|
+
def policy_from_template(self, agent_id: str, template: str, overrides: Optional[Dict[str, Any]] = None) -> Policy:
|
|
114
|
+
payload = {"agent_id": agent_id, "template": template, "overrides": overrides or {}}
|
|
115
|
+
data = request_with_retries_sync(
|
|
116
|
+
lambda: self._c.post(f"/policies/{agent_id}/from-template", json=payload),
|
|
117
|
+
max_retries=self._max_retries_429,
|
|
118
|
+
base_backoff=self._retry_backoff,
|
|
119
|
+
max_backoff=self._retry_max_backoff,
|
|
120
|
+
)
|
|
121
|
+
j = parse_json_or_raise(data)
|
|
122
|
+
return self._policy_from_response(j, fallback_agent_id=agent_id, fallback_policy=payload)
|
|
123
|
+
|
|
124
|
+
async def policy_from_template_async(
|
|
125
|
+
self, agent_id: str, template: str, overrides: Optional[Dict[str, Any]] = None
|
|
126
|
+
) -> Policy:
|
|
127
|
+
payload = {"agent_id": agent_id, "template": template, "overrides": overrides or {}}
|
|
128
|
+
ac = await self._get_ac()
|
|
129
|
+
resp = await request_with_retries_async(
|
|
130
|
+
lambda: ac.post(f"/policies/{agent_id}/from-template", json=payload),
|
|
131
|
+
max_retries=self._max_retries_429,
|
|
132
|
+
base_backoff=self._retry_backoff,
|
|
133
|
+
max_backoff=self._retry_max_backoff,
|
|
134
|
+
)
|
|
135
|
+
j = parse_json_or_raise(resp)
|
|
136
|
+
return self._policy_from_response(j, fallback_agent_id=agent_id, fallback_policy=payload)
|
|
137
|
+
|
|
138
|
+
def list_policies(self) -> List[Policy]:
|
|
139
|
+
data = request_with_retries_sync(
|
|
140
|
+
lambda: self._c.get("/policies"),
|
|
141
|
+
max_retries=self._max_retries_429,
|
|
142
|
+
base_backoff=self._retry_backoff,
|
|
143
|
+
max_backoff=self._retry_max_backoff,
|
|
144
|
+
)
|
|
145
|
+
j = parse_json_or_raise(data)
|
|
146
|
+
items = self._extract_list(j)
|
|
147
|
+
out: List[Policy] = []
|
|
148
|
+
for it in items or []:
|
|
149
|
+
out.append(self._policy_from_response(it))
|
|
150
|
+
return out
|
|
151
|
+
|
|
152
|
+
async def list_policies_async(self) -> List[Policy]:
|
|
153
|
+
ac = await self._get_ac()
|
|
154
|
+
resp = await request_with_retries_async(
|
|
155
|
+
lambda: ac.get("/policies"),
|
|
156
|
+
max_retries=self._max_retries_429,
|
|
157
|
+
base_backoff=self._retry_backoff,
|
|
158
|
+
max_backoff=self._retry_max_backoff,
|
|
159
|
+
)
|
|
160
|
+
j = parse_json_or_raise(resp)
|
|
161
|
+
items = self._extract_list(j)
|
|
162
|
+
out: List[Policy] = []
|
|
163
|
+
for it in items or []:
|
|
164
|
+
out.append(self._policy_from_response(it))
|
|
165
|
+
return out
|
|
166
|
+
|
|
167
|
+
def get_policy(self, agent_id: str) -> Policy:
|
|
168
|
+
data = request_with_retries_sync(
|
|
169
|
+
lambda: self._c.get(f"/policies/{agent_id}"),
|
|
170
|
+
max_retries=self._max_retries_429,
|
|
171
|
+
base_backoff=self._retry_backoff,
|
|
172
|
+
max_backoff=self._retry_max_backoff,
|
|
173
|
+
)
|
|
174
|
+
j = parse_json_or_raise(data)
|
|
175
|
+
return self._policy_from_response(j, fallback_agent_id=agent_id)
|
|
176
|
+
|
|
177
|
+
async def get_policy_async(self, agent_id: str) -> Policy:
|
|
178
|
+
ac = await self._get_ac()
|
|
179
|
+
resp = await request_with_retries_async(
|
|
180
|
+
lambda: ac.get(f"/policies/{agent_id}"),
|
|
181
|
+
max_retries=self._max_retries_429,
|
|
182
|
+
base_backoff=self._retry_backoff,
|
|
183
|
+
max_backoff=self._retry_max_backoff,
|
|
184
|
+
)
|
|
185
|
+
j = parse_json_or_raise(resp)
|
|
186
|
+
return self._policy_from_response(j, fallback_agent_id=agent_id)
|
|
187
|
+
|
|
188
|
+
# -------- Tools Registry --------
|
|
189
|
+
|
|
190
|
+
def create_tool(self, tool: Dict[str, Any]) -> Dict[str, Any]:
|
|
191
|
+
data = request_with_retries_sync(
|
|
192
|
+
lambda: self._c.post("/tools", json=tool),
|
|
193
|
+
max_retries=self._max_retries_429,
|
|
194
|
+
base_backoff=self._retry_backoff,
|
|
195
|
+
max_backoff=self._retry_max_backoff,
|
|
196
|
+
)
|
|
197
|
+
return parse_json_or_raise(data)
|
|
198
|
+
|
|
199
|
+
async def create_tool_async(self, tool: Dict[str, Any]) -> Dict[str, Any]:
|
|
200
|
+
ac = await self._get_ac()
|
|
201
|
+
resp = await request_with_retries_async(
|
|
202
|
+
lambda: ac.post("/tools", json=tool),
|
|
203
|
+
max_retries=self._max_retries_429,
|
|
204
|
+
base_backoff=self._retry_backoff,
|
|
205
|
+
max_backoff=self._retry_max_backoff,
|
|
206
|
+
)
|
|
207
|
+
return parse_json_or_raise(resp)
|
|
208
|
+
|
|
209
|
+
def list_tools(self, scope_id: str = "global") -> List[Dict[str, Any]]:
|
|
210
|
+
data = request_with_retries_sync(
|
|
211
|
+
lambda: self._c.get("/tools", params={"scope_id": scope_id}),
|
|
212
|
+
max_retries=self._max_retries_429,
|
|
213
|
+
base_backoff=self._retry_backoff,
|
|
214
|
+
max_backoff=self._retry_max_backoff,
|
|
215
|
+
)
|
|
216
|
+
j = parse_json_or_raise(data)
|
|
217
|
+
return self._extract_list(j)
|
|
218
|
+
|
|
219
|
+
async def list_tools_async(self, scope_id: str = "global") -> List[Dict[str, Any]]:
|
|
220
|
+
ac = await self._get_ac()
|
|
221
|
+
resp = await request_with_retries_async(
|
|
222
|
+
lambda: ac.get("/tools", params={"scope_id": scope_id}),
|
|
223
|
+
max_retries=self._max_retries_429,
|
|
224
|
+
base_backoff=self._retry_backoff,
|
|
225
|
+
max_backoff=self._retry_max_backoff,
|
|
226
|
+
)
|
|
227
|
+
j = parse_json_or_raise(resp)
|
|
228
|
+
return self._extract_list(j)
|
|
229
|
+
|
|
230
|
+
def get_tool(self, name: str, scope_id: str = "global") -> Dict[str, Any]:
|
|
231
|
+
data = request_with_retries_sync(
|
|
232
|
+
lambda: self._c.get(f"/tools/{name}", params={"scope_id": scope_id}),
|
|
233
|
+
max_retries=self._max_retries_429,
|
|
234
|
+
base_backoff=self._retry_backoff,
|
|
235
|
+
max_backoff=self._retry_max_backoff,
|
|
236
|
+
)
|
|
237
|
+
return parse_json_or_raise(data)
|
|
238
|
+
|
|
239
|
+
async def get_tool_async(self, name: str, scope_id: str = "global") -> Dict[str, Any]:
|
|
240
|
+
ac = await self._get_ac()
|
|
241
|
+
resp = await request_with_retries_async(
|
|
242
|
+
lambda: ac.get(f"/tools/{name}", params={"scope_id": scope_id}),
|
|
243
|
+
max_retries=self._max_retries_429,
|
|
244
|
+
base_backoff=self._retry_backoff,
|
|
245
|
+
max_backoff=self._retry_max_backoff,
|
|
246
|
+
)
|
|
247
|
+
return parse_json_or_raise(resp)
|
|
248
|
+
|
|
249
|
+
def toggle_tool(self, name: str, *, enabled: bool, scope_id: str = "global") -> Dict[str, Any]:
|
|
250
|
+
data = request_with_retries_sync(
|
|
251
|
+
lambda: self._c.post(
|
|
252
|
+
f"/tools/{name}/toggle",
|
|
253
|
+
params={"scope_id": scope_id},
|
|
254
|
+
json={"enabled": bool(enabled)},
|
|
255
|
+
),
|
|
256
|
+
max_retries=self._max_retries_429,
|
|
257
|
+
base_backoff=self._retry_backoff,
|
|
258
|
+
max_backoff=self._retry_max_backoff,
|
|
259
|
+
)
|
|
260
|
+
return parse_json_or_raise(data)
|
|
261
|
+
|
|
262
|
+
async def toggle_tool_async(self, name: str, *, enabled: bool, scope_id: str = "global") -> Dict[str, Any]:
|
|
263
|
+
ac = await self._get_ac()
|
|
264
|
+
resp = await request_with_retries_async(
|
|
265
|
+
lambda: ac.post(
|
|
266
|
+
f"/tools/{name}/toggle",
|
|
267
|
+
params={"scope_id": scope_id},
|
|
268
|
+
json={"enabled": bool(enabled)},
|
|
269
|
+
),
|
|
270
|
+
max_retries=self._max_retries_429,
|
|
271
|
+
base_backoff=self._retry_backoff,
|
|
272
|
+
max_backoff=self._retry_max_backoff,
|
|
273
|
+
)
|
|
274
|
+
return parse_json_or_raise(resp)
|
|
275
|
+
|
|
276
|
+
def delete_tool(self, name: str, scope_id: str = "global") -> Dict[str, Any]:
|
|
277
|
+
data = request_with_retries_sync(
|
|
278
|
+
lambda: self._c.delete(f"/tools/{name}", params={"scope_id": scope_id}),
|
|
279
|
+
max_retries=self._max_retries_429,
|
|
280
|
+
base_backoff=self._retry_backoff,
|
|
281
|
+
max_backoff=self._retry_max_backoff,
|
|
282
|
+
)
|
|
283
|
+
return parse_json_or_raise(data)
|
|
284
|
+
|
|
285
|
+
async def delete_tool_async(self, name: str, scope_id: str = "global") -> Dict[str, Any]:
|
|
286
|
+
ac = await self._get_ac()
|
|
287
|
+
resp = await request_with_retries_async(
|
|
288
|
+
lambda: ac.delete(f"/tools/{name}", params={"scope_id": scope_id}),
|
|
289
|
+
max_retries=self._max_retries_429,
|
|
290
|
+
base_backoff=self._retry_backoff,
|
|
291
|
+
max_backoff=self._retry_max_backoff,
|
|
292
|
+
)
|
|
293
|
+
return parse_json_or_raise(resp)
|
|
294
|
+
|
|
295
|
+
# -------- Kill switch --------
|
|
296
|
+
|
|
297
|
+
def disable_agent(self, agent_id: str, reason: str = "") -> AgentStatus:
|
|
298
|
+
payload = {"reason": reason} if reason else {}
|
|
299
|
+
data = request_with_retries_sync(
|
|
300
|
+
lambda: self._c.post(f"/agents/{agent_id}/disable", json=payload),
|
|
301
|
+
max_retries=self._max_retries_429,
|
|
302
|
+
base_backoff=self._retry_backoff,
|
|
303
|
+
max_backoff=self._retry_max_backoff,
|
|
304
|
+
)
|
|
305
|
+
j = parse_json_or_raise(data)
|
|
306
|
+
return AgentStatus(
|
|
307
|
+
agent_id=str(j.get("agent_id") or agent_id),
|
|
308
|
+
is_enabled=bool(j.get("is_enabled", j.get("enabled"))),
|
|
309
|
+
disabled_reason=j.get("disabled_reason"),
|
|
310
|
+
disabled_at=j.get("disabled_at"),
|
|
311
|
+
raw=j,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
async def disable_agent_async(self, agent_id: str, reason: str = "") -> AgentStatus:
|
|
315
|
+
payload = {"reason": reason} if reason else {}
|
|
316
|
+
ac = await self._get_ac()
|
|
317
|
+
resp = await request_with_retries_async(
|
|
318
|
+
lambda: ac.post(f"/agents/{agent_id}/disable", json=payload),
|
|
319
|
+
max_retries=self._max_retries_429,
|
|
320
|
+
base_backoff=self._retry_backoff,
|
|
321
|
+
max_backoff=self._retry_max_backoff,
|
|
322
|
+
)
|
|
323
|
+
j = parse_json_or_raise(resp)
|
|
324
|
+
return AgentStatus(
|
|
325
|
+
agent_id=str(j.get("agent_id") or agent_id),
|
|
326
|
+
is_enabled=bool(j.get("is_enabled", j.get("enabled"))),
|
|
327
|
+
disabled_reason=j.get("disabled_reason"),
|
|
328
|
+
disabled_at=j.get("disabled_at"),
|
|
329
|
+
raw=j,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
def enable_agent(self, agent_id: str, reason: str = "") -> AgentStatus:
|
|
333
|
+
payload = {"reason": reason} if reason else {}
|
|
334
|
+
data = request_with_retries_sync(
|
|
335
|
+
lambda: self._c.post(f"/agents/{agent_id}/enable", json=payload),
|
|
336
|
+
max_retries=self._max_retries_429,
|
|
337
|
+
base_backoff=self._retry_backoff,
|
|
338
|
+
max_backoff=self._retry_max_backoff,
|
|
339
|
+
)
|
|
340
|
+
j = parse_json_or_raise(data)
|
|
341
|
+
return AgentStatus(
|
|
342
|
+
agent_id=str(j.get("agent_id") or agent_id),
|
|
343
|
+
is_enabled=bool(j.get("is_enabled", j.get("enabled"))),
|
|
344
|
+
disabled_reason=j.get("disabled_reason"),
|
|
345
|
+
disabled_at=j.get("disabled_at"),
|
|
346
|
+
raw=j,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
async def enable_agent_async(self, agent_id: str, reason: str = "") -> AgentStatus:
|
|
350
|
+
payload = {"reason": reason} if reason else {}
|
|
351
|
+
ac = await self._get_ac()
|
|
352
|
+
resp = await request_with_retries_async(
|
|
353
|
+
lambda: ac.post(f"/agents/{agent_id}/enable", json=payload),
|
|
354
|
+
max_retries=self._max_retries_429,
|
|
355
|
+
base_backoff=self._retry_backoff,
|
|
356
|
+
max_backoff=self._retry_max_backoff,
|
|
357
|
+
)
|
|
358
|
+
j = parse_json_or_raise(resp)
|
|
359
|
+
return AgentStatus(
|
|
360
|
+
agent_id=str(j.get("agent_id") or agent_id),
|
|
361
|
+
is_enabled=bool(j.get("is_enabled", j.get("enabled"))),
|
|
362
|
+
disabled_reason=j.get("disabled_reason"),
|
|
363
|
+
disabled_at=j.get("disabled_at"),
|
|
364
|
+
raw=j,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# -------- Audit logs + metrics --------
|
|
368
|
+
|
|
369
|
+
def agent_logs(self, agent_id: str, limit: int = 100) -> List[AgentLogItem]:
|
|
370
|
+
data = request_with_retries_sync(
|
|
371
|
+
lambda: self._c.get(f"/agents/{agent_id}/logs", params={"limit": int(limit)}),
|
|
372
|
+
max_retries=self._max_retries_429,
|
|
373
|
+
base_backoff=self._retry_backoff,
|
|
374
|
+
max_backoff=self._retry_max_backoff,
|
|
375
|
+
)
|
|
376
|
+
j = parse_json_or_raise(data)
|
|
377
|
+
items = self._extract_list(j)
|
|
378
|
+
out: List[AgentLogItem] = []
|
|
379
|
+
for it in items or []:
|
|
380
|
+
if not isinstance(it, dict):
|
|
381
|
+
continue
|
|
382
|
+
allowed = bool(it.get("allowed", True))
|
|
383
|
+
decision = str(it.get("decision") or ("blocked" if not allowed else "allowed"))
|
|
384
|
+
policy_reason = it.get("policy_reason") or it.get("reason")
|
|
385
|
+
out.append(
|
|
386
|
+
AgentLogItem(
|
|
387
|
+
ts=it.get("ts"),
|
|
388
|
+
agent_id=str(it.get("agent_id") or agent_id),
|
|
389
|
+
tool=it.get("tool"),
|
|
390
|
+
allowed=allowed,
|
|
391
|
+
decision=decision,
|
|
392
|
+
policy_reason=policy_reason,
|
|
393
|
+
reason=str(it.get("reason") or policy_reason or ""),
|
|
394
|
+
args_hash=it.get("args_hash"),
|
|
395
|
+
risk_level=it.get("risk_level"),
|
|
396
|
+
spend_usd=float(it.get("spend_usd") or 0),
|
|
397
|
+
raw=it,
|
|
398
|
+
)
|
|
399
|
+
)
|
|
400
|
+
return out
|
|
401
|
+
|
|
402
|
+
async def agent_logs_async(self, agent_id: str, limit: int = 100) -> List[AgentLogItem]:
|
|
403
|
+
ac = await self._get_ac()
|
|
404
|
+
resp = await request_with_retries_async(
|
|
405
|
+
lambda: ac.get(f"/agents/{agent_id}/logs", params={"limit": int(limit)}),
|
|
406
|
+
max_retries=self._max_retries_429,
|
|
407
|
+
base_backoff=self._retry_backoff,
|
|
408
|
+
max_backoff=self._retry_max_backoff,
|
|
409
|
+
)
|
|
410
|
+
j = parse_json_or_raise(resp)
|
|
411
|
+
items = self._extract_list(j)
|
|
412
|
+
out: List[AgentLogItem] = []
|
|
413
|
+
for it in items or []:
|
|
414
|
+
if not isinstance(it, dict):
|
|
415
|
+
continue
|
|
416
|
+
allowed = bool(it.get("allowed", True))
|
|
417
|
+
decision = str(it.get("decision") or ("blocked" if not allowed else "allowed"))
|
|
418
|
+
policy_reason = it.get("policy_reason") or it.get("reason")
|
|
419
|
+
out.append(
|
|
420
|
+
AgentLogItem(
|
|
421
|
+
ts=it.get("ts"),
|
|
422
|
+
agent_id=str(it.get("agent_id") or agent_id),
|
|
423
|
+
tool=it.get("tool"),
|
|
424
|
+
allowed=allowed,
|
|
425
|
+
decision=decision,
|
|
426
|
+
policy_reason=policy_reason,
|
|
427
|
+
reason=str(it.get("reason") or policy_reason or ""),
|
|
428
|
+
args_hash=it.get("args_hash"),
|
|
429
|
+
risk_level=it.get("risk_level"),
|
|
430
|
+
spend_usd=float(it.get("spend_usd") or 0),
|
|
431
|
+
raw=it,
|
|
432
|
+
)
|
|
433
|
+
)
|
|
434
|
+
return out
|
|
435
|
+
|
|
436
|
+
def agent_metrics(self, agent_id: str, period: Literal["hour", "day", "week"] = "day") -> AgentMetrics:
|
|
437
|
+
data = request_with_retries_sync(
|
|
438
|
+
lambda: self._c.get(f"/agents/{agent_id}/metrics", params={"period": period}),
|
|
439
|
+
max_retries=self._max_retries_429,
|
|
440
|
+
base_backoff=self._retry_backoff,
|
|
441
|
+
max_backoff=self._retry_max_backoff,
|
|
442
|
+
)
|
|
443
|
+
j = parse_json_or_raise(data)
|
|
444
|
+
return AgentMetrics(
|
|
445
|
+
agent_id=str(j.get("agent_id") or agent_id),
|
|
446
|
+
period=str(j.get("period") or period), # type: ignore[arg-type]
|
|
447
|
+
total_actions=int(j.get("total_actions") or 0),
|
|
448
|
+
blocked_actions=int(j.get("blocked_actions") or 0),
|
|
449
|
+
total_spend_usd=float(j.get("total_spend_usd") or 0),
|
|
450
|
+
top_tools=list(j.get("top_tools") or []),
|
|
451
|
+
raw=j if isinstance(j, dict) else None,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
async def agent_metrics_async(self, agent_id: str, period: Literal["hour", "day", "week"] = "day") -> AgentMetrics:
|
|
455
|
+
ac = await self._get_ac()
|
|
456
|
+
resp = await request_with_retries_async(
|
|
457
|
+
lambda: ac.get(f"/agents/{agent_id}/metrics", params={"period": period}),
|
|
458
|
+
max_retries=self._max_retries_429,
|
|
459
|
+
base_backoff=self._retry_backoff,
|
|
460
|
+
max_backoff=self._retry_max_backoff,
|
|
461
|
+
)
|
|
462
|
+
j = parse_json_or_raise(resp)
|
|
463
|
+
return AgentMetrics(
|
|
464
|
+
agent_id=str(j.get("agent_id") or agent_id),
|
|
465
|
+
period=str(j.get("period") or period), # type: ignore[arg-type]
|
|
466
|
+
total_actions=int(j.get("total_actions") or 0),
|
|
467
|
+
blocked_actions=int(j.get("blocked_actions") or 0),
|
|
468
|
+
total_spend_usd=float(j.get("total_spend_usd") or 0),
|
|
469
|
+
top_tools=list(j.get("top_tools") or []),
|
|
470
|
+
raw=j if isinstance(j, dict) else None,
|
|
471
|
+
)
|