agentkeeper-runtime-sdk 0.1.0b1__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,45 @@
1
+ from .anthropic import (
2
+ wrap_anthropic_client,
3
+ wrap_anthropic_runnable_tool,
4
+ wrap_anthropic_tool,
5
+ )
6
+ from .bedrock import wrap_bedrock_runtime_client
7
+ from .azure_openai import wrap_azure_openai_client
8
+ from .claude_managed_agents import wrap_claude_managed_agents_client
9
+ from .client import (
10
+ AgentKeeperRuntimeClient,
11
+ TrackResult,
12
+ create_agentkeeper_runtime_client,
13
+ find_raw_payload_keys,
14
+ )
15
+ from .langchain import (
16
+ create_langchain_callback_handler,
17
+ create_langgraph_callback_handler,
18
+ wrap_langchain_tool,
19
+ wrap_langgraph_tool,
20
+ )
21
+ from .openai_agents import (
22
+ wrap_openai_agents_run,
23
+ wrap_openai_agents_sdk,
24
+ wrap_openai_agents_tool,
25
+ )
26
+
27
+ __all__ = [
28
+ "AgentKeeperRuntimeClient",
29
+ "TrackResult",
30
+ "create_agentkeeper_runtime_client",
31
+ "find_raw_payload_keys",
32
+ "create_langchain_callback_handler",
33
+ "create_langgraph_callback_handler",
34
+ "wrap_langchain_tool",
35
+ "wrap_langgraph_tool",
36
+ "wrap_bedrock_runtime_client",
37
+ "wrap_azure_openai_client",
38
+ "wrap_claude_managed_agents_client",
39
+ "wrap_openai_agents_run",
40
+ "wrap_openai_agents_sdk",
41
+ "wrap_openai_agents_tool",
42
+ "wrap_anthropic_client",
43
+ "wrap_anthropic_runnable_tool",
44
+ "wrap_anthropic_tool",
45
+ ]
@@ -0,0 +1,441 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from typing import Any, Callable, Iterator, Mapping, Optional
5
+
6
+ from .client import (
7
+ AgentKeeperRuntimeClient,
8
+ ToolPolicyDecision,
9
+ compact,
10
+ create_agentkeeper_runtime_client,
11
+ get_value,
12
+ number_value,
13
+ object_keys,
14
+ safe_error_name,
15
+ string_value,
16
+ telemetry_metadata,
17
+ )
18
+
19
+ _RUNNABLE_TOOL_WRAPPED = "_agentkeeper_anthropic_runnable_tool_wrapped"
20
+
21
+
22
+ def _client_from(value: AgentKeeperRuntimeClient | Mapping[str, Any]) -> AgentKeeperRuntimeClient:
23
+ if isinstance(value, AgentKeeperRuntimeClient):
24
+ return value
25
+ return create_agentkeeper_runtime_client(**dict(value))
26
+
27
+
28
+ def _base_event(options: Mapping[str, Any]) -> dict[str, Any]:
29
+ return {
30
+ "runtime_integration": "anthropic_sdk",
31
+ "runtime_service": options.get("runtime_service") or options.get("runtimeService"),
32
+ "runtime_environment": options.get("runtime_environment") or options.get("runtimeEnvironment"),
33
+ **dict(options.get("event") or {}),
34
+ }
35
+
36
+
37
+ def _request_metadata(request: Mapping[str, Any], operation: str, options: Mapping[str, Any]) -> dict[str, Any]:
38
+ ids = telemetry_metadata(
39
+ runtime_service=options.get("runtime_service") or options.get("runtimeService"),
40
+ runtime_environment=options.get("runtime_environment") or options.get("runtimeEnvironment"),
41
+ run_id=options.get("run_id") or options.get("runId"),
42
+ trace_id=options.get("trace_id") or options.get("traceId"),
43
+ span_id=options.get("span_id") or options.get("spanId"),
44
+ user_id=options.get("user_id") or options.get("userId"),
45
+ convo_id=options.get("convo_id") or options.get("convoId"),
46
+ )
47
+ tools = request.get("tools")
48
+ tool_choice = request.get("tool_choice") if isinstance(request.get("tool_choice"), Mapping) else {}
49
+ return compact({
50
+ **ids,
51
+ "operation": operation,
52
+ "model": string_value(request.get("model")),
53
+ "max_tokens": number_value(request.get("max_tokens")),
54
+ "message_count": len(request.get("messages")) if isinstance(request.get("messages"), list) else None,
55
+ "tool_count": len(tools) if isinstance(tools, list) else None,
56
+ "tool_names": [
57
+ name for name in (string_value(get_value(tool, "name")) for tool in tools or []) if name
58
+ ][:50] or None,
59
+ "tool_choice_type": string_value(tool_choice.get("type")) if isinstance(tool_choice, Mapping) else None,
60
+ "tool_choice_name": string_value(tool_choice.get("name")) if isinstance(tool_choice, Mapping) else None,
61
+ "stream": request.get("stream") is True or operation.endswith(".stream"),
62
+ "has_system": bool(request.get("system")),
63
+ })
64
+
65
+
66
+ def _usage_fields(response: Any) -> dict[str, Any]:
67
+ usage = get_value(response, "usage") or {}
68
+ return compact({
69
+ "token_input": number_value(get_value(usage, "input_tokens")) or number_value(get_value(usage, "inputTokens")),
70
+ "token_output": number_value(get_value(usage, "output_tokens")) or number_value(get_value(usage, "outputTokens")),
71
+ })
72
+
73
+
74
+ def _model_name(request: Mapping[str, Any], response: Any = None) -> Optional[str]:
75
+ return string_value(get_value(response, "model")) or string_value(request.get("model"))
76
+
77
+
78
+ def _response_metadata(response: Any, metadata: Mapping[str, Any]) -> dict[str, Any]:
79
+ content = get_value(response, "content")
80
+ if not isinstance(content, list):
81
+ content = []
82
+ tool_names = []
83
+ for block in content:
84
+ if string_value(get_value(block, "type")) == "tool_use":
85
+ name = string_value(get_value(block, "name"))
86
+ if name:
87
+ tool_names.append(name)
88
+ return compact({
89
+ **dict(metadata),
90
+ "response_id": string_value(get_value(response, "id")),
91
+ "role": string_value(get_value(response, "role")),
92
+ "stop_reason": string_value(get_value(response, "stop_reason") or get_value(response, "stopReason")),
93
+ "stop_sequence_present": get_value(response, "stop_sequence") is not None,
94
+ "content_block_count": len(content) or None,
95
+ "tool_use_count": len(tool_names) or None,
96
+ "tool_use_names": tool_names[:50] or None,
97
+ "stream_event_count": number_value(get_value(response, "event_count")),
98
+ })
99
+
100
+
101
+ class _StreamProxy:
102
+ def __init__(
103
+ self,
104
+ stream: Any,
105
+ track_once: Callable[[Any, Optional[BaseException]], None],
106
+ model: Optional[str],
107
+ ) -> None:
108
+ self._stream = stream
109
+ self._track_once = track_once
110
+ self._model = model
111
+ self._tracked = False
112
+
113
+ def _track(self, response: Any, error: Optional[BaseException] = None) -> None:
114
+ if self._tracked:
115
+ return
116
+ self._tracked = True
117
+ self._track_once(response, error)
118
+
119
+ def __getattr__(self, name: str) -> Any:
120
+ value = getattr(self._stream, name)
121
+ if name in {
122
+ "get_final_message",
123
+ "final_message",
124
+ "finalMessage",
125
+ "done",
126
+ "run_until_done",
127
+ "runUntilDone",
128
+ } and callable(value):
129
+ def wrapped(*args: Any, **kwargs: Any) -> Any:
130
+ try:
131
+ result = value(*args, **kwargs)
132
+ except BaseException as error:
133
+ self._track({"model": self._model}, error)
134
+ raise
135
+ if inspect.isawaitable(result):
136
+ async def wait() -> Any:
137
+ try:
138
+ resolved = await result
139
+ except BaseException as error:
140
+ self._track({"model": self._model}, error)
141
+ raise
142
+ self._track(resolved)
143
+ return resolved
144
+
145
+ return wait()
146
+ self._track(result)
147
+ return result
148
+
149
+ return wrapped
150
+ return value
151
+
152
+ def __iter__(self) -> Iterator[Any]:
153
+ event_count = 0
154
+ try:
155
+ for event in self._stream:
156
+ event_count += 1
157
+ yield event
158
+ except BaseException as error:
159
+ self._track({"model": self._model, "event_count": event_count}, error)
160
+ raise
161
+ self._track({"model": self._model, "event_count": event_count})
162
+
163
+ def __enter__(self) -> Any:
164
+ entered = self._stream.__enter__()
165
+ if entered is self._stream:
166
+ return self
167
+ self._stream = entered
168
+ return self
169
+
170
+ def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> Any:
171
+ return self._stream.__exit__(exc_type, exc, tb)
172
+
173
+ async def __aenter__(self) -> Any:
174
+ entered = await self._stream.__aenter__()
175
+ if entered is not self._stream:
176
+ self._stream = entered
177
+ return self
178
+
179
+ async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> Any:
180
+ return await self._stream.__aexit__(exc_type, exc, tb)
181
+
182
+
183
+ def _request_from_args(args: tuple[Any, ...], kwargs: Mapping[str, Any]) -> dict[str, Any]:
184
+ if args and isinstance(args[0], Mapping):
185
+ return {**dict(args[0]), **dict(kwargs)}
186
+ return dict(kwargs)
187
+
188
+
189
+ def _prepare_tool_runner_request(request: dict[str, Any], ak: AgentKeeperRuntimeClient, options: Mapping[str, Any]) -> dict[str, Any]:
190
+ tools = request.get("tools")
191
+ if not isinstance(tools, list):
192
+ return request
193
+ return {
194
+ **request,
195
+ "tools": [
196
+ wrap_anthropic_runnable_tool(tool, ak, **options)
197
+ if (isinstance(tool, Mapping) and callable(tool.get("run"))) or callable(getattr(tool, "run", None))
198
+ else tool
199
+ for tool in tools
200
+ ],
201
+ }
202
+
203
+
204
+ class _AnthropicResourceProxy:
205
+ def __init__(self, resource: Any, ak: AgentKeeperRuntimeClient, options: Mapping[str, Any], prefix: str = "messages") -> None:
206
+ self._resource = resource
207
+ self._ak = ak
208
+ self._options = dict(options)
209
+ self._prefix = prefix
210
+
211
+ def __getattr__(self, name: str) -> Any:
212
+ value = getattr(self._resource, name)
213
+ if name in {"create", "stream", "tool_runner", "toolRunner"} and callable(value):
214
+ return self._wrap_method(name, value)
215
+ return value
216
+
217
+ def _track_start(self, request: Mapping[str, Any], operation: str, metadata: Mapping[str, Any]) -> None:
218
+ self._ak.track({
219
+ **_base_event(self._options),
220
+ "event_kind": "model_call",
221
+ "capability": "model_only",
222
+ "model_provider": "anthropic",
223
+ "model_name": _model_name(request),
224
+ "run_id": metadata.get("run_id"),
225
+ "trace_id": metadata.get("trace_id"),
226
+ "span_id": metadata.get("span_id"),
227
+ "verdict": "observed",
228
+ "evidence_summary": f"Anthropic SDK {operation} observed",
229
+ "metadata": metadata,
230
+ })
231
+
232
+ def _track_completion(
233
+ self,
234
+ request: Mapping[str, Any],
235
+ operation: str,
236
+ metadata: Mapping[str, Any],
237
+ response: Any,
238
+ error: Optional[BaseException] = None,
239
+ ) -> None:
240
+ self._ak.track({
241
+ **_base_event(self._options),
242
+ "event_kind": "model_call",
243
+ "capability": "post_run",
244
+ "model_provider": "anthropic",
245
+ "model_name": _model_name(request, response),
246
+ **_usage_fields(response),
247
+ "run_id": metadata.get("run_id"),
248
+ "trace_id": metadata.get("trace_id"),
249
+ "span_id": metadata.get("span_id"),
250
+ "verdict": "observed" if error else "passed",
251
+ "severity": "medium" if error else "low",
252
+ "evidence_summary": (
253
+ f"Anthropic SDK {operation} errored: {safe_error_name(error)}"
254
+ if error
255
+ else f"Anthropic SDK {operation} completed"
256
+ ),
257
+ "metadata": _response_metadata(response, metadata),
258
+ })
259
+
260
+ def _wrap_method(self, name: str, method: Callable[..., Any]) -> Callable[..., Any]:
261
+ operation = f"{self._prefix}.{name}"
262
+
263
+ def wrapped(*args: Any, **kwargs: Any) -> Any:
264
+ request = _request_from_args(args, kwargs)
265
+ if name in {"tool_runner", "toolRunner"}:
266
+ request = _prepare_tool_runner_request(request, self._ak, self._options)
267
+ args = (request,) if args and isinstance(args[0], Mapping) else args
268
+ kwargs = request if not args else kwargs
269
+ metadata = _request_metadata(request, operation, self._options)
270
+ self._track_start(request, operation, metadata)
271
+ tracked = False
272
+
273
+ def track_once(response: Any, error: Optional[BaseException] = None) -> None:
274
+ nonlocal tracked
275
+ if tracked:
276
+ return
277
+ tracked = True
278
+ self._track_completion(request, operation, metadata, response, error)
279
+
280
+ try:
281
+ result = method(*args, **kwargs)
282
+ except BaseException as error:
283
+ track_once({}, error)
284
+ raise
285
+
286
+ if inspect.isawaitable(result):
287
+ async def wait() -> Any:
288
+ try:
289
+ resolved = await result
290
+ except BaseException as error:
291
+ track_once({}, error)
292
+ raise
293
+ if name in {"stream", "tool_runner", "toolRunner"}:
294
+ return _StreamProxy(resolved, track_once, _model_name(request))
295
+ track_once(resolved)
296
+ return resolved
297
+
298
+ return wait()
299
+
300
+ if name == "stream" or request.get("stream") is True or name in {"tool_runner", "toolRunner"}:
301
+ return _StreamProxy(result, track_once, _model_name(request))
302
+
303
+ track_once(result)
304
+ return result
305
+
306
+ return wrapped
307
+
308
+
309
+ class _AnthropicClientProxy:
310
+ def __init__(self, client: Any, ak: AgentKeeperRuntimeClient, options: Mapping[str, Any]) -> None:
311
+ self._client = client
312
+ self._ak = ak
313
+ self._options = dict(options)
314
+
315
+ def __getattr__(self, name: str) -> Any:
316
+ value = getattr(self._client, name)
317
+ if name == "messages" and value is not None:
318
+ return _AnthropicResourceProxy(value, self._ak, self._options, "messages")
319
+ if name == "beta" and value is not None:
320
+ return _BetaProxy(value, self._ak, self._options)
321
+ return value
322
+
323
+
324
+ class _BetaProxy:
325
+ def __init__(self, beta: Any, ak: AgentKeeperRuntimeClient, options: Mapping[str, Any]) -> None:
326
+ self._beta = beta
327
+ self._ak = ak
328
+ self._options = dict(options)
329
+
330
+ def __getattr__(self, name: str) -> Any:
331
+ value = getattr(self._beta, name)
332
+ if name == "messages" and value is not None:
333
+ return _AnthropicResourceProxy(value, self._ak, self._options, "beta.messages")
334
+ return value
335
+
336
+
337
+ def wrap_anthropic_client(
338
+ anthropic_client: Any,
339
+ client_or_options: AgentKeeperRuntimeClient | Mapping[str, Any],
340
+ **options: Any,
341
+ ) -> Any:
342
+ return _AnthropicClientProxy(anthropic_client, _client_from(client_or_options), options)
343
+
344
+
345
+ def _tool_name(tool: Any, explicit: Optional[str]) -> str:
346
+ return (
347
+ explicit
348
+ or string_value(get_value(tool, "name"))
349
+ or string_value(get_value(tool, "type"))
350
+ or string_value(getattr(tool.__class__, "__name__", None))
351
+ or "anthropic_sdk_tool"
352
+ )
353
+
354
+
355
+ class _RunnableToolProxy:
356
+ def __init__(self, tool: Any, run: Callable[..., Any]) -> None:
357
+ self._tool = tool
358
+ self.run = run
359
+
360
+ def __getattr__(self, name: str) -> Any:
361
+ return getattr(self._tool, name)
362
+
363
+
364
+ def wrap_anthropic_runnable_tool(
365
+ tool: Any,
366
+ client_or_options: AgentKeeperRuntimeClient | Mapping[str, Any],
367
+ *,
368
+ tool_name: Optional[str] = None,
369
+ evaluate: Optional[Callable[[Any], ToolPolicyDecision | Any]] = None,
370
+ evaluate_tool: Optional[Callable[[str, Any], ToolPolicyDecision | Any]] = None,
371
+ **options: Any,
372
+ ) -> Any:
373
+ if isinstance(tool, Mapping) and tool.get(_RUNNABLE_TOOL_WRAPPED):
374
+ return tool
375
+ if getattr(tool, _RUNNABLE_TOOL_WRAPPED, False):
376
+ return tool
377
+
378
+ ak = _client_from(client_or_options)
379
+ name = _tool_name(tool, tool_name)
380
+ event = {
381
+ **_base_event(options),
382
+ "runtime_integration": "anthropic_sdk",
383
+ "tool_type": "anthropic_sdk",
384
+ "metadata": {"tool_runner": True, **dict(options.get("metadata") or {})},
385
+ }
386
+
387
+ def evaluator(tool_args: Any) -> ToolPolicyDecision | Any:
388
+ if evaluate:
389
+ return evaluate(tool_args)
390
+ if evaluate_tool:
391
+ return evaluate_tool(name, tool_args)
392
+ return {"verdict": "passed"}
393
+
394
+ def original_run(*args: Any, **kwargs: Any) -> Any:
395
+ run = tool.get("run") if isinstance(tool, Mapping) else getattr(tool, "run")
396
+ return run(*args, **kwargs)
397
+
398
+ def wrapped_run(*args: Any, **kwargs: Any) -> Any:
399
+ tool_args = args[0] if args else dict(kwargs)
400
+ return ak.run_guarded_tool(name, tool_args, lambda: original_run(*args, **kwargs), evaluate=evaluator, event=event)
401
+
402
+ if isinstance(tool, Mapping):
403
+ return {**dict(tool), "run": wrapped_run, _RUNNABLE_TOOL_WRAPPED: True}
404
+ wrapped = _RunnableToolProxy(tool, wrapped_run)
405
+ setattr(wrapped, _RUNNABLE_TOOL_WRAPPED, True)
406
+ return wrapped
407
+
408
+
409
+ def wrap_anthropic_tool(
410
+ tool_name: str,
411
+ execute: Callable[[Any], Any],
412
+ client_or_options: AgentKeeperRuntimeClient | Mapping[str, Any],
413
+ *,
414
+ evaluate: Optional[Callable[[Any], ToolPolicyDecision | Any]] = None,
415
+ evaluate_tool: Optional[Callable[[str, Any], ToolPolicyDecision | Any]] = None,
416
+ **options: Any,
417
+ ) -> Callable[[Any], Any]:
418
+ ak = _client_from(client_or_options)
419
+
420
+ def evaluator(tool_args: Any) -> ToolPolicyDecision | Any:
421
+ if evaluate:
422
+ return evaluate(tool_args)
423
+ if evaluate_tool:
424
+ return evaluate_tool(tool_name, tool_args)
425
+ return {"verdict": "passed"}
426
+
427
+ def wrapped(tool_args: Any) -> Any:
428
+ return ak.run_guarded_tool(
429
+ tool_name,
430
+ tool_args,
431
+ lambda: execute(tool_args),
432
+ evaluate=evaluator,
433
+ event={
434
+ **_base_event(options),
435
+ "runtime_integration": "anthropic_sdk",
436
+ "tool_type": "anthropic_sdk",
437
+ },
438
+ )
439
+
440
+ wrapped.__name__ = getattr(execute, "__name__", tool_name)
441
+ return wrapped