agentrust-py 0.0.3__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.
- agentrust/__init__.py +72 -0
- agentrust_py-0.0.3.dist-info/METADATA +193 -0
- agentrust_py-0.0.3.dist-info/RECORD +29 -0
- agentrust_py-0.0.3.dist-info/WHEEL +4 -0
- agentrust_py-0.0.3.dist-info/entry_points.txt +2 -0
- agentrust_py-0.0.3.dist-info/licenses/LICENSE +177 -0
- agentrust_sdk/__init__.py +124 -0
- agentrust_sdk/adapters/__init__.py +1 -0
- agentrust_sdk/adapters/autogen.py +235 -0
- agentrust_sdk/adapters/claude_agents.py +225 -0
- agentrust_sdk/adapters/crewai.py +98 -0
- agentrust_sdk/adapters/langgraph.py +109 -0
- agentrust_sdk/adapters/mcp.py +193 -0
- agentrust_sdk/adapters/openai_agents.py +263 -0
- agentrust_sdk/auth.py +192 -0
- agentrust_sdk/auto.py +397 -0
- agentrust_sdk/autoload.py +95 -0
- agentrust_sdk/cli.py +736 -0
- agentrust_sdk/client.py +790 -0
- agentrust_sdk/config.py +192 -0
- agentrust_sdk/decorator.py +276 -0
- agentrust_sdk/embedded.py +428 -0
- agentrust_sdk/hooks.py +461 -0
- agentrust_sdk/models.py +81 -0
- agentrust_sdk/py.typed +0 -0
- agentrust_sdk/queue_replay.py +204 -0
- agentrust_sdk/tiers.py +180 -0
- agentrust_sdk/version_negotiation.py +290 -0
- agentrust_sdk/webhooks.py +782 -0
agentrust_sdk/auto.py
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentTrust transparent auto-instrumentation.
|
|
3
|
+
|
|
4
|
+
Drop-in patching for OpenAI, LangChain, and Anthropic SDKs — no changes
|
|
5
|
+
required in user code.
|
|
6
|
+
|
|
7
|
+
Quick start::
|
|
8
|
+
|
|
9
|
+
import agentrust_sdk.auto as at_auto
|
|
10
|
+
at_auto.install(agent_id="my-agent", api_key="at-...")
|
|
11
|
+
|
|
12
|
+
Or use the convenience alias::
|
|
13
|
+
|
|
14
|
+
from agentrust_sdk.auto import auto_install
|
|
15
|
+
auto_install(agent_id="my-agent")
|
|
16
|
+
|
|
17
|
+
After calling ``install()`` every call to:
|
|
18
|
+
|
|
19
|
+
* ``openai.OpenAI().chat.completions.create`` / ``AsyncOpenAI`` variant
|
|
20
|
+
* ``anthropic.Anthropic().messages.create`` / ``AsyncAnthropic`` variant
|
|
21
|
+
* Any LangChain LLM / chain / tool invocation (via ``BaseCallbackHandler``)
|
|
22
|
+
|
|
23
|
+
…will be transparently wrapped with ``AgentTrustClient.validate()``.
|
|
24
|
+
|
|
25
|
+
The original return value is **always** preserved. Validation errors are
|
|
26
|
+
logged and swallowed (or raise, depending on ``failure_mode``).
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import functools
|
|
31
|
+
import logging
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
# Module-level set that tracks which patches have been applied so that
|
|
37
|
+
# calling install() multiple times is a no-op.
|
|
38
|
+
_INSTALLED: set[str] = set()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Helpers
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
def _make_client(agent_id: str, api_key: str | None, gateway_url: str | None, failure_mode: str):
|
|
46
|
+
"""Lazily import and construct an AgentTrustClient."""
|
|
47
|
+
from .client import AgentTrustClient # local import to avoid circular deps
|
|
48
|
+
|
|
49
|
+
kwargs: dict[str, Any] = {"failure_mode": failure_mode}
|
|
50
|
+
if api_key:
|
|
51
|
+
kwargs["api_key"] = api_key
|
|
52
|
+
if gateway_url:
|
|
53
|
+
kwargs["gateway_url"] = gateway_url
|
|
54
|
+
return AgentTrustClient(**kwargs), agent_id
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _safe_validate(client, agent_id: str, user: str, input_text: str, output_text: str) -> None:
|
|
58
|
+
"""Call validate() and swallow any exception so the caller is unaffected."""
|
|
59
|
+
try:
|
|
60
|
+
client.validate(
|
|
61
|
+
agent_id=agent_id,
|
|
62
|
+
user=user,
|
|
63
|
+
input=input_text,
|
|
64
|
+
output=output_text,
|
|
65
|
+
)
|
|
66
|
+
except Exception as exc: # noqa: BLE001
|
|
67
|
+
logger.debug("AgentTrust validation error (non-fatal): %s", exc)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def _safe_validate_async(client, agent_id: str, user: str, input_text: str, output_text: str) -> None:
|
|
71
|
+
"""Async version of _safe_validate."""
|
|
72
|
+
try:
|
|
73
|
+
await client.validate_async(
|
|
74
|
+
agent_id=agent_id,
|
|
75
|
+
user=user,
|
|
76
|
+
input=input_text,
|
|
77
|
+
output=output_text,
|
|
78
|
+
)
|
|
79
|
+
except Exception as exc: # noqa: BLE001
|
|
80
|
+
logger.debug("AgentTrust async validation error (non-fatal): %s", exc)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _extract_openai_prompt(kwargs: dict[str, Any]) -> str:
|
|
84
|
+
"""Best-effort extraction of a human-readable prompt from OpenAI messages."""
|
|
85
|
+
messages = kwargs.get("messages", [])
|
|
86
|
+
if not messages:
|
|
87
|
+
return ""
|
|
88
|
+
parts = []
|
|
89
|
+
for m in messages:
|
|
90
|
+
role = m.get("role", "")
|
|
91
|
+
content = m.get("content", "")
|
|
92
|
+
if isinstance(content, list):
|
|
93
|
+
# vision / multi-part content
|
|
94
|
+
content = " ".join(
|
|
95
|
+
p.get("text", "") for p in content if isinstance(p, dict) and p.get("type") == "text"
|
|
96
|
+
)
|
|
97
|
+
parts.append(f"{role}: {content}")
|
|
98
|
+
return "\n".join(parts)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _extract_openai_response(response: Any) -> str:
|
|
102
|
+
"""Extract text from an OpenAI ChatCompletion response object."""
|
|
103
|
+
try:
|
|
104
|
+
return response.choices[0].message.content or ""
|
|
105
|
+
except Exception:
|
|
106
|
+
return str(response)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# Patch 1 — OpenAI SDK (openai >= 1.0)
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def _patch_openai(client_at, agent_id: str) -> None:
|
|
114
|
+
"""Wrap openai.OpenAI and openai.AsyncOpenAI completions.create."""
|
|
115
|
+
if "openai" in _INSTALLED:
|
|
116
|
+
return
|
|
117
|
+
try:
|
|
118
|
+
import openai # noqa: PLC0415
|
|
119
|
+
except ImportError:
|
|
120
|
+
logger.debug("openai not installed — skipping AgentTrust OpenAI patch")
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
# --- sync ---
|
|
124
|
+
_orig_sync_create = openai.resources.chat.completions.Completions.create
|
|
125
|
+
|
|
126
|
+
@functools.wraps(_orig_sync_create)
|
|
127
|
+
def _sync_create(self, *args: Any, **kwargs: Any) -> Any:
|
|
128
|
+
result = _orig_sync_create(self, *args, **kwargs)
|
|
129
|
+
prompt = _extract_openai_prompt(kwargs)
|
|
130
|
+
output = _extract_openai_response(result)
|
|
131
|
+
_safe_validate(client_at, agent_id, user="openai-sync", input_text=prompt, output_text=output)
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
openai.resources.chat.completions.Completions.create = _sync_create # type: ignore[method-assign]
|
|
135
|
+
|
|
136
|
+
# --- async ---
|
|
137
|
+
try:
|
|
138
|
+
_orig_async_create = openai.resources.chat.completions.AsyncCompletions.create
|
|
139
|
+
|
|
140
|
+
@functools.wraps(_orig_async_create)
|
|
141
|
+
async def _async_create(self, *args: Any, **kwargs: Any) -> Any:
|
|
142
|
+
result = await _orig_async_create(self, *args, **kwargs)
|
|
143
|
+
prompt = _extract_openai_prompt(kwargs)
|
|
144
|
+
output = _extract_openai_response(result)
|
|
145
|
+
await _safe_validate_async(client_at, agent_id, user="openai-async", input_text=prompt, output_text=output)
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
openai.resources.chat.completions.AsyncCompletions.create = _async_create # type: ignore[method-assign]
|
|
149
|
+
except AttributeError:
|
|
150
|
+
logger.debug("AsyncCompletions not found in openai — async patch skipped")
|
|
151
|
+
|
|
152
|
+
_INSTALLED.add("openai")
|
|
153
|
+
logger.info("AgentTrust: OpenAI patch installed")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# Patch 2 — LangChain (langchain >= 0.1)
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
class AgentTrustCallbackHandler:
|
|
161
|
+
"""
|
|
162
|
+
LangChain BaseCallbackHandler that validates LLM / chain / tool outputs
|
|
163
|
+
via AgentTrust.
|
|
164
|
+
|
|
165
|
+
This class inherits from ``langchain_core.callbacks.BaseCallbackHandler``
|
|
166
|
+
only when LangChain is available. When LangChain is absent the class is
|
|
167
|
+
defined as a plain Python class so that the module can still be imported.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
# Filled in by _patch_langchain() after the real base class is resolved.
|
|
171
|
+
_client_at: Any = None
|
|
172
|
+
_agent_id: str = "auto"
|
|
173
|
+
|
|
174
|
+
def on_llm_end(self, response: Any, **kwargs: Any) -> None: # type: ignore[override]
|
|
175
|
+
try:
|
|
176
|
+
# LLMResult stores generations as a list-of-list of Generation objects
|
|
177
|
+
for gen_list in getattr(response, "generations", []):
|
|
178
|
+
for gen in gen_list:
|
|
179
|
+
text = getattr(gen, "text", "") or ""
|
|
180
|
+
_safe_validate(
|
|
181
|
+
self._client_at,
|
|
182
|
+
self._agent_id,
|
|
183
|
+
user="langchain-llm",
|
|
184
|
+
input_text="",
|
|
185
|
+
output_text=text,
|
|
186
|
+
)
|
|
187
|
+
except Exception as exc: # noqa: BLE001
|
|
188
|
+
logger.debug("AgentTrust LangChain on_llm_end error: %s", exc)
|
|
189
|
+
|
|
190
|
+
def on_chain_end(self, outputs: Any, **kwargs: Any) -> None: # type: ignore[override]
|
|
191
|
+
try:
|
|
192
|
+
output_text = str(outputs) if outputs is not None else ""
|
|
193
|
+
_safe_validate(
|
|
194
|
+
self._client_at,
|
|
195
|
+
self._agent_id,
|
|
196
|
+
user="langchain-chain",
|
|
197
|
+
input_text="",
|
|
198
|
+
output_text=output_text,
|
|
199
|
+
)
|
|
200
|
+
except Exception as exc: # noqa: BLE001
|
|
201
|
+
logger.debug("AgentTrust LangChain on_chain_end error: %s", exc)
|
|
202
|
+
|
|
203
|
+
def on_tool_end(self, output: Any, **kwargs: Any) -> None: # type: ignore[override]
|
|
204
|
+
try:
|
|
205
|
+
output_text = str(output) if output is not None else ""
|
|
206
|
+
_safe_validate(
|
|
207
|
+
self._client_at,
|
|
208
|
+
self._agent_id,
|
|
209
|
+
user="langchain-tool",
|
|
210
|
+
input_text="",
|
|
211
|
+
output_text=output_text,
|
|
212
|
+
)
|
|
213
|
+
except Exception as exc: # noqa: BLE001
|
|
214
|
+
logger.debug("AgentTrust LangChain on_tool_end error: %s", exc)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _patch_langchain(client_at, agent_id: str) -> None:
|
|
218
|
+
"""Register AgentTrustCallbackHandler as a global LangChain callback."""
|
|
219
|
+
if "langchain" in _INSTALLED:
|
|
220
|
+
return
|
|
221
|
+
try:
|
|
222
|
+
from langchain_core.callbacks import BaseCallbackHandler # noqa: PLC0415
|
|
223
|
+
except ImportError:
|
|
224
|
+
try:
|
|
225
|
+
from langchain.callbacks import BaseCallbackHandler # type: ignore[no-redef] # noqa: PLC0415
|
|
226
|
+
except ImportError:
|
|
227
|
+
logger.debug("langchain not installed — skipping AgentTrust LangChain patch")
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
# Re-define the handler as a proper subclass now that we have the base.
|
|
231
|
+
class _Handler(BaseCallbackHandler): # type: ignore[misc]
|
|
232
|
+
def on_llm_end(self, response: Any, **kwargs: Any) -> None:
|
|
233
|
+
AgentTrustCallbackHandler.on_llm_end(self, response, **kwargs)
|
|
234
|
+
|
|
235
|
+
def on_chain_end(self, outputs: Any, **kwargs: Any) -> None:
|
|
236
|
+
AgentTrustCallbackHandler.on_chain_end(self, outputs, **kwargs)
|
|
237
|
+
|
|
238
|
+
def on_tool_end(self, output: Any, **kwargs: Any) -> None:
|
|
239
|
+
AgentTrustCallbackHandler.on_tool_end(self, output, **kwargs)
|
|
240
|
+
|
|
241
|
+
handler = _Handler()
|
|
242
|
+
handler._client_at = client_at # type: ignore[attr-defined]
|
|
243
|
+
handler._agent_id = agent_id # type: ignore[attr-defined]
|
|
244
|
+
|
|
245
|
+
# Expose the concrete subclass so users can import it if needed.
|
|
246
|
+
AgentTrustCallbackHandler.__bases__ = (BaseCallbackHandler,) # type: ignore[misc]
|
|
247
|
+
|
|
248
|
+
# Try to register globally via the callback manager.
|
|
249
|
+
_registered = False
|
|
250
|
+
try:
|
|
251
|
+
from langchain.callbacks import set_handler # noqa: PLC0415
|
|
252
|
+
set_handler(handler)
|
|
253
|
+
_registered = True
|
|
254
|
+
except (ImportError, AttributeError):
|
|
255
|
+
pass
|
|
256
|
+
|
|
257
|
+
if not _registered:
|
|
258
|
+
try:
|
|
259
|
+
from langchain_core.callbacks import get_callback_manager # noqa: PLC0415
|
|
260
|
+
get_callback_manager().add_handler(handler)
|
|
261
|
+
_registered = True
|
|
262
|
+
except (ImportError, AttributeError):
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
if not _registered:
|
|
266
|
+
logger.debug(
|
|
267
|
+
"AgentTrust: could not find a global LangChain callback registry; "
|
|
268
|
+
"add AgentTrustCallbackHandler() manually to your chain's callbacks."
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
_INSTALLED.add("langchain")
|
|
272
|
+
logger.info("AgentTrust: LangChain patch installed (global registration=%s)", _registered)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# ---------------------------------------------------------------------------
|
|
276
|
+
# Patch 3 — Anthropic SDK (anthropic >= 0.20)
|
|
277
|
+
# ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
def _extract_anthropic_prompt(kwargs: dict[str, Any]) -> str:
|
|
280
|
+
"""Best-effort extraction of prompt text from anthropic.messages.create kwargs."""
|
|
281
|
+
messages = kwargs.get("messages", [])
|
|
282
|
+
parts = []
|
|
283
|
+
for m in messages:
|
|
284
|
+
role = m.get("role", "")
|
|
285
|
+
content = m.get("content", "")
|
|
286
|
+
if isinstance(content, list):
|
|
287
|
+
content = " ".join(
|
|
288
|
+
block.get("text", "") for block in content
|
|
289
|
+
if isinstance(block, dict) and block.get("type") == "text"
|
|
290
|
+
)
|
|
291
|
+
parts.append(f"{role}: {content}")
|
|
292
|
+
system = kwargs.get("system", "")
|
|
293
|
+
if system:
|
|
294
|
+
parts.insert(0, f"system: {system}")
|
|
295
|
+
return "\n".join(parts)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _extract_anthropic_response(response: Any) -> str:
|
|
299
|
+
"""Extract text from an anthropic Message response."""
|
|
300
|
+
try:
|
|
301
|
+
parts = []
|
|
302
|
+
for block in response.content:
|
|
303
|
+
if hasattr(block, "text"):
|
|
304
|
+
parts.append(block.text)
|
|
305
|
+
return "\n".join(parts)
|
|
306
|
+
except Exception:
|
|
307
|
+
return str(response)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _patch_anthropic(client_at, agent_id: str) -> None:
|
|
311
|
+
"""Wrap anthropic.Anthropic().messages.create and AsyncAnthropic variant."""
|
|
312
|
+
if "anthropic" in _INSTALLED:
|
|
313
|
+
return
|
|
314
|
+
try:
|
|
315
|
+
import anthropic # noqa: PLC0415
|
|
316
|
+
except ImportError:
|
|
317
|
+
logger.debug("anthropic not installed — skipping AgentTrust Anthropic patch")
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
# --- sync ---
|
|
321
|
+
try:
|
|
322
|
+
_orig_sync = anthropic.resources.messages.Messages.create
|
|
323
|
+
|
|
324
|
+
@functools.wraps(_orig_sync)
|
|
325
|
+
def _sync_create(self, *args: Any, **kwargs: Any) -> Any:
|
|
326
|
+
result = _orig_sync(self, *args, **kwargs)
|
|
327
|
+
prompt = _extract_anthropic_prompt(kwargs)
|
|
328
|
+
output = _extract_anthropic_response(result)
|
|
329
|
+
_safe_validate(client_at, agent_id, user="anthropic-sync", input_text=prompt, output_text=output)
|
|
330
|
+
return result
|
|
331
|
+
|
|
332
|
+
anthropic.resources.messages.Messages.create = _sync_create # type: ignore[method-assign]
|
|
333
|
+
except AttributeError:
|
|
334
|
+
logger.debug("anthropic.resources.messages.Messages not found — sync patch skipped")
|
|
335
|
+
|
|
336
|
+
# --- async ---
|
|
337
|
+
try:
|
|
338
|
+
_orig_async = anthropic.resources.messages.AsyncMessages.create
|
|
339
|
+
|
|
340
|
+
@functools.wraps(_orig_async)
|
|
341
|
+
async def _async_create(self, *args: Any, **kwargs: Any) -> Any:
|
|
342
|
+
result = await _orig_async(self, *args, **kwargs)
|
|
343
|
+
prompt = _extract_anthropic_prompt(kwargs)
|
|
344
|
+
output = _extract_anthropic_response(result)
|
|
345
|
+
await _safe_validate_async(client_at, agent_id, user="anthropic-async", input_text=prompt, output_text=output)
|
|
346
|
+
return result
|
|
347
|
+
|
|
348
|
+
anthropic.resources.messages.AsyncMessages.create = _async_create # type: ignore[method-assign]
|
|
349
|
+
except AttributeError:
|
|
350
|
+
logger.debug("anthropic.resources.messages.AsyncMessages not found — async patch skipped")
|
|
351
|
+
|
|
352
|
+
_INSTALLED.add("anthropic")
|
|
353
|
+
logger.info("AgentTrust: Anthropic patch installed")
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# ---------------------------------------------------------------------------
|
|
357
|
+
# Public API
|
|
358
|
+
# ---------------------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
def install(
|
|
361
|
+
agent_id: str = "auto",
|
|
362
|
+
api_key: str | None = None,
|
|
363
|
+
gateway_url: str | None = None,
|
|
364
|
+
failure_mode: str = "open",
|
|
365
|
+
) -> None:
|
|
366
|
+
"""Install all available AgentTrust auto-instrumentation patches.
|
|
367
|
+
|
|
368
|
+
This function is **idempotent** — calling it more than once has no effect.
|
|
369
|
+
|
|
370
|
+
Parameters
|
|
371
|
+
----------
|
|
372
|
+
agent_id:
|
|
373
|
+
The agent identifier reported to AgentTrust for every validation call.
|
|
374
|
+
Defaults to ``"auto"``.
|
|
375
|
+
api_key:
|
|
376
|
+
AgentTrust API key. When *None* the key is resolved from the
|
|
377
|
+
environment variable ``AGENTRUST_API_KEY`` or ``~/.agentrust/config.yaml``.
|
|
378
|
+
gateway_url:
|
|
379
|
+
Override the AgentTrust gateway URL (useful for on-premise deployments).
|
|
380
|
+
failure_mode:
|
|
381
|
+
One of ``"open"`` (default — allow on error), ``"closed"`` (deny on
|
|
382
|
+
error), or ``"queue"`` (buffer calls for later retry).
|
|
383
|
+
"""
|
|
384
|
+
if _INSTALLED == {"openai", "langchain", "anthropic"}:
|
|
385
|
+
# All known patches already applied — fast-path exit.
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
client_at, resolved_agent_id = _make_client(agent_id, api_key, gateway_url, failure_mode)
|
|
389
|
+
|
|
390
|
+
_patch_openai(client_at, resolved_agent_id)
|
|
391
|
+
_patch_langchain(client_at, resolved_agent_id)
|
|
392
|
+
_patch_anthropic(client_at, resolved_agent_id)
|
|
393
|
+
|
|
394
|
+
logger.info("AgentTrust auto-instrumentation complete. Installed: %s", _INSTALLED)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
auto_install = install # alias for convenience
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentTrust autoload bootstrap — zero-code instrumentation entry point.
|
|
3
|
+
|
|
4
|
+
Three ways to activate without editing application code
|
|
5
|
+
-------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
**1. PYTHONSTARTUP (interactive / script startup):**
|
|
8
|
+
|
|
9
|
+
export PYTHONSTARTUP=/path/to/agentrust_autoload.py
|
|
10
|
+
# or point to this module:
|
|
11
|
+
export PYTHONSTARTUP=$(python -c "import agentrust_sdk.autoload; print(agentrust_sdk.autoload.__file__)")
|
|
12
|
+
|
|
13
|
+
**2. sitecustomize / usercustomize:**
|
|
14
|
+
|
|
15
|
+
# /path/to/site-packages/sitecustomize.py (system) or usercustomize.py (user)
|
|
16
|
+
import agentrust_sdk.autoload # noqa: F401
|
|
17
|
+
|
|
18
|
+
**3. PYTHONPATH + sitecustomize trick:**
|
|
19
|
+
|
|
20
|
+
# Create a sitecustomize.py in a directory and prepend to PYTHONPATH:
|
|
21
|
+
echo "import agentrust_sdk.autoload" > /tmp/agentrust_site/sitecustomize.py
|
|
22
|
+
export PYTHONPATH=/tmp/agentrust_site:$PYTHONPATH
|
|
23
|
+
|
|
24
|
+
**4. Framework entry point (gunicorn/uvicorn pre-exec):**
|
|
25
|
+
|
|
26
|
+
gunicorn myapp:app --preload --worker-class uvicorn.workers.UvicornWorker \\
|
|
27
|
+
--config agentrust_sdk.autoload.gunicorn_conf
|
|
28
|
+
|
|
29
|
+
**5. `[autoload]` setuptools extra:**
|
|
30
|
+
|
|
31
|
+
pip install agentrust-sdk[autoload]
|
|
32
|
+
|
|
33
|
+
Then in your app's own `sitecustomize.py` or startup:
|
|
34
|
+
import agentrust_sdk.autoload
|
|
35
|
+
|
|
36
|
+
Environment controls
|
|
37
|
+
--------------------
|
|
38
|
+
``AGENTRUST_ENABLED=false`` → skip all instrumentation
|
|
39
|
+
``AGENTRUST_AUTO_INSTRUMENT=false`` → skip hook patching
|
|
40
|
+
``AGENTRUST_AUTOLOAD_LOG_LEVEL=DEBUG`` → verbose bootstrap logging
|
|
41
|
+
``AGENTRUST_AUTOLOAD_EMBED=true`` → also start embedded gateway
|
|
42
|
+
``AGENTRUST_AUTOLOAD_AGENT_ID=<string>`` → agent_id label for auto patches
|
|
43
|
+
"""
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import logging
|
|
47
|
+
import os
|
|
48
|
+
|
|
49
|
+
_log_level = os.environ.get("AGENTRUST_AUTOLOAD_LOG_LEVEL", "INFO").upper()
|
|
50
|
+
logging.basicConfig(level=getattr(logging, _log_level, logging.INFO))
|
|
51
|
+
logger = logging.getLogger("agentrust.autoload")
|
|
52
|
+
|
|
53
|
+
# Respect kill-switch — do nothing if disabled
|
|
54
|
+
if os.environ.get("AGENTRUST_ENABLED", "true").lower() in ("0", "false", "no"):
|
|
55
|
+
logger.debug("[AgentTrust autoload] AGENTRUST_ENABLED=false — skipping bootstrap")
|
|
56
|
+
else:
|
|
57
|
+
try:
|
|
58
|
+
from agentrust_sdk.hooks import auto_instrument
|
|
59
|
+
from agentrust_sdk.config import SDK_CONFIG
|
|
60
|
+
|
|
61
|
+
_agent_id = os.environ.get("AGENTRUST_AUTOLOAD_AGENT_ID", "autoload")
|
|
62
|
+
|
|
63
|
+
patched = auto_instrument(agent_id=_agent_id)
|
|
64
|
+
if patched:
|
|
65
|
+
logger.info("[AgentTrust autoload] Patched frameworks: %s", ", ".join(patched))
|
|
66
|
+
else:
|
|
67
|
+
logger.debug("[AgentTrust autoload] No frameworks to patch (none installed or AGENTRUST_AUTO_INSTRUMENT=false)")
|
|
68
|
+
|
|
69
|
+
# Optionally start embedded gateway (SQLite, no external deps)
|
|
70
|
+
if os.environ.get("AGENTRUST_AUTOLOAD_EMBED", "false").lower() in ("1", "true", "yes"):
|
|
71
|
+
from agentrust_sdk.embedded import embed_gateway
|
|
72
|
+
gw = embed_gateway()
|
|
73
|
+
logger.info("[AgentTrust autoload] Embedded gateway started on %s", SDK_CONFIG.gateway_url)
|
|
74
|
+
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
# Bootstrap must never crash the host application
|
|
77
|
+
logger.warning("[AgentTrust autoload] Bootstrap error (non-fatal): %s", exc)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
# gunicorn config helper
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
def _post_fork(server, worker): # noqa: ANN001
|
|
85
|
+
"""gunicorn post_fork hook — re-apply patches after fork."""
|
|
86
|
+
try:
|
|
87
|
+
from agentrust_sdk.hooks import auto_instrument
|
|
88
|
+
auto_instrument(agent_id=os.environ.get("AGENTRUST_AUTOLOAD_AGENT_ID", "autoload"))
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
import logging as _log
|
|
91
|
+
_log.getLogger("agentrust.autoload").warning("post_fork patch error: %s", exc)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# gunicorn config module surface (used via --config agentrust_sdk.autoload)
|
|
95
|
+
post_fork = _post_fork
|