agentrust-py 0.0.1a1__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,235 @@
1
+ """
2
+ AutoGen adapter — AgentTrust governance for Microsoft AutoGen agents.
3
+
4
+ Architecture doc (two-pager): "Connects to … AutoGen agents."
5
+
6
+ Supports both AutoGen v0.2 (pyautogen) and AutoGen v0.4 (autogen-agentchat):
7
+
8
+ Pattern 1 — AgentTrustReplyHook (v0.2 + v0.4):
9
+ Attach to any ConversableAgent / AssistantAgent to validate every reply
10
+ before it is sent.
11
+
12
+ Pattern 2 — AgentTrustGroupChatMonitor (v0.2 GroupChat):
13
+ Wraps a GroupChat's speaker-selection to validate messages between agents.
14
+
15
+ Usage::
16
+
17
+ from autogen import AssistantAgent
18
+ from agentrust_sdk.adapters.autogen import AgentTrustReplyHook
19
+
20
+ hook = AgentTrustReplyHook(agent_id="research-team")
21
+ assistant = AssistantAgent("researcher", llm_config={...})
22
+ hook.attach(assistant)
23
+
24
+ Requires Team tier.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import logging
29
+ import time
30
+ from typing import Any, Callable
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ def _check_tier(api_key: str | None = None) -> None:
36
+ from agentrust_sdk.auth import resolve_key
37
+ from agentrust_sdk.tiers import Capability, is_allowed
38
+ info = resolve_key(api_key)
39
+ if not is_allowed(Capability.CREWAI_ADAPTER, info.tier):
40
+ raise RuntimeError("[AgentTrust] AutoGen adapter requires Team tier or higher.")
41
+
42
+
43
+ def _validate_sync(
44
+ agent_id: str,
45
+ base_url: str,
46
+ api_key: str | None,
47
+ user: str,
48
+ input_text: str,
49
+ output: dict,
50
+ framework: str,
51
+ block_on_block: bool,
52
+ ) -> None:
53
+ from agentrust_sdk.client import AgentTrustClient
54
+ from agentrust_sdk.decorator import _check_decision, BlockedError
55
+
56
+ try:
57
+ with AgentTrustClient(base_url=base_url, api_key=api_key) as client:
58
+ resp = client.validate(
59
+ agent_id=agent_id,
60
+ user=user,
61
+ input=input_text,
62
+ output=output,
63
+ framework=framework,
64
+ )
65
+ _check_decision(resp, block_on_block, False)
66
+ except BlockedError:
67
+ raise
68
+ except Exception as exc:
69
+ logger.warning("[AgentTrust] AutoGen validate failed (non-fatal): %s", exc)
70
+
71
+
72
+ class AgentTrustReplyHook:
73
+ """
74
+ Reply hook that validates every AutoGen agent reply via AgentTrust.
75
+
76
+ Compatible with AutoGen v0.2 (pyautogen) reply_func mechanism and
77
+ AutoGen v0.4 (autogen-agentchat) message hook pattern.
78
+
79
+ Usage (v0.2)::
80
+
81
+ hook = AgentTrustReplyHook(agent_id="my-autogen")
82
+ assistant = AssistantAgent("researcher", llm_config={...})
83
+ hook.attach(assistant)
84
+
85
+ Usage (v0.4)::
86
+
87
+ hook = AgentTrustReplyHook(agent_id="my-autogen")
88
+ agent = AssistantAgent("researcher", model_client=..., tools=[...])
89
+ hook.attach_v4(agent)
90
+ """
91
+
92
+ def __init__(
93
+ self,
94
+ agent_id: str,
95
+ *,
96
+ base_url: str = "http://localhost:8000",
97
+ api_key: str | None = None,
98
+ user: str = "autogen",
99
+ block_on_block: bool = True,
100
+ framework: str = "AutoGen",
101
+ ) -> None:
102
+ _check_tier(api_key)
103
+ self._agent_id = agent_id
104
+ self._base_url = base_url
105
+ self._api_key = api_key
106
+ self._user = user
107
+ self._block_on_block = block_on_block
108
+ self._framework = framework
109
+
110
+ # ------------------------------------------------------------------
111
+ # AutoGen v0.2
112
+ # ------------------------------------------------------------------
113
+
114
+ def _reply_func(
115
+ self,
116
+ recipient: Any,
117
+ messages: list[dict] | None = None,
118
+ sender: Any = None,
119
+ config: Any = None,
120
+ ) -> tuple[bool, str | None]:
121
+ """AutoGen v0.2 reply function — registered via register_reply()."""
122
+ if not messages:
123
+ return False, None
124
+
125
+ last = messages[-1]
126
+ content = last.get("content", "") or ""
127
+ role = last.get("role", "assistant")
128
+
129
+ if role not in ("assistant", "user"):
130
+ return False, None
131
+
132
+ t0 = time.perf_counter()
133
+ output = {"content": content[:4000], "role": role}
134
+ _validate_sync(
135
+ self._agent_id, self._base_url, self._api_key,
136
+ self._user, content[:500], output, self._framework, self._block_on_block,
137
+ )
138
+ logger.debug(
139
+ "[AgentTrust] AutoGen reply validated in %.1f ms", (time.perf_counter() - t0) * 1000
140
+ )
141
+ return False, None # False = don't override reply; None = no replacement content
142
+
143
+ def attach(self, agent: Any) -> None:
144
+ """Attach to an AutoGen v0.2 ConversableAgent / AssistantAgent."""
145
+ if not hasattr(agent, "register_reply"):
146
+ raise TypeError(
147
+ f"Expected a ConversableAgent with register_reply(), got {type(agent).__name__}"
148
+ )
149
+ agent.register_reply(
150
+ trigger=[],
151
+ reply_func=self._reply_func,
152
+ position=0,
153
+ name="agentrust_reply_hook",
154
+ )
155
+ logger.info(
156
+ "[AgentTrust] AutoGen reply hook attached to agent '%s'", getattr(agent, "name", "?")
157
+ )
158
+
159
+ # ------------------------------------------------------------------
160
+ # AutoGen v0.4
161
+ # ------------------------------------------------------------------
162
+
163
+ def attach_v4(self, agent: Any) -> None:
164
+ """Attach to an AutoGen v0.4 BaseChatAgent via on_messages hook."""
165
+ original_on_messages = agent.on_messages
166
+
167
+ async def _wrapped_on_messages(messages: Any, cancellation_token: Any = None) -> Any:
168
+ result = await original_on_messages(messages, cancellation_token)
169
+ # result is a Response object with a .chat_message attribute
170
+ content = ""
171
+ if hasattr(result, "chat_message") and result.chat_message:
172
+ content = getattr(result.chat_message, "content", "") or ""
173
+ output = {"content": content[:4000]}
174
+ _validate_sync(
175
+ self._agent_id, self._base_url, self._api_key,
176
+ self._user, content[:500], output, self._framework, self._block_on_block,
177
+ )
178
+ return result
179
+
180
+ agent.on_messages = _wrapped_on_messages
181
+ logger.info(
182
+ "[AgentTrust] AutoGen v0.4 hook attached to agent '%s'",
183
+ getattr(agent, "name", "?"),
184
+ )
185
+
186
+
187
+ class AgentTrustGroupChatMonitor:
188
+ """
189
+ Monitors AutoGen GroupChat messages and validates each speaker's reply.
190
+
191
+ Usage::
192
+
193
+ from autogen import GroupChat, GroupChatManager
194
+ from agentrust_sdk.adapters.autogen import AgentTrustGroupChatMonitor
195
+
196
+ monitor = AgentTrustGroupChatMonitor(agent_id="planning-crew")
197
+ groupchat = GroupChat(agents=[...], messages=[])
198
+ manager = GroupChatManager(groupchat=groupchat)
199
+ monitor.attach(groupchat)
200
+ """
201
+
202
+ def __init__(
203
+ self,
204
+ agent_id: str,
205
+ *,
206
+ base_url: str = "http://localhost:8000",
207
+ api_key: str | None = None,
208
+ user: str = "autogen-group",
209
+ block_on_block: bool = False, # Default False — group chats tolerate review
210
+ framework: str = "AutoGen-GroupChat",
211
+ ) -> None:
212
+ _check_tier(api_key)
213
+ self._agent_id = agent_id
214
+ self._base_url = base_url
215
+ self._api_key = api_key
216
+ self._user = user
217
+ self._block_on_block = block_on_block
218
+ self._framework = framework
219
+
220
+ def attach(self, groupchat: Any) -> None:
221
+ """Patch groupchat.append to intercept every new message."""
222
+ original_append = groupchat.append
223
+
224
+ def _governed_append(message: dict, speaker: Any) -> None:
225
+ content = message.get("content", "") or ""
226
+ _validate_sync(
227
+ self._agent_id, self._base_url, self._api_key,
228
+ self._user, content[:500],
229
+ {"content": content[:4000], "speaker": getattr(speaker, "name", str(speaker))},
230
+ self._framework, self._block_on_block,
231
+ )
232
+ original_append(message, speaker)
233
+
234
+ groupchat.append = _governed_append
235
+ logger.info("[AgentTrust] AutoGen GroupChatMonitor attached")
@@ -0,0 +1,225 @@
1
+ """
2
+ Claude Agents adapter — AgentTrust governance for Anthropic Claude tool-use agents.
3
+
4
+ Architecture doc (two-pager): "Connects to … Claude Agents."
5
+
6
+ Intercepts Anthropic API tool-use responses and validates each tool result
7
+ before the agent continues. Works with both:
8
+
9
+ - Direct anthropic.Anthropic() client loops
10
+ - Claude's tool_use content blocks in messages API responses
11
+
12
+ Usage::
13
+
14
+ from anthropic import Anthropic
15
+ from agentrust_sdk.adapters.claude_agents import ClaudeAgentGuard
16
+
17
+ client = Anthropic()
18
+ guard = ClaudeAgentGuard(agent_id="invoice-agent")
19
+
20
+ # Wrap the anthropic client — all tool_use responses are validated
21
+ governed_client = guard.wrap_client(client)
22
+
23
+ # Or wrap a single agentic loop function
24
+ @guard.wrap_loop
25
+ def run_agent(query: str) -> str:
26
+ ...
27
+
28
+ Requires Developer tier.
29
+ """
30
+ from __future__ import annotations
31
+
32
+ import functools
33
+ import logging
34
+ import time
35
+ from typing import Any
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ def _check_tier(api_key: str | None = None) -> None:
41
+ from agentrust_sdk.auth import resolve_key
42
+ from agentrust_sdk.tiers import Capability, is_allowed
43
+ info = resolve_key(api_key)
44
+ # Reuse CREWAI_ADAPTER capability gate (Team tier) until a dedicated capability exists
45
+ if not is_allowed(Capability.CREWAI_ADAPTER, info.tier):
46
+ raise RuntimeError("[AgentTrust] Claude Agents adapter requires Team tier or higher.")
47
+
48
+
49
+ def _extract_tool_calls(response: Any) -> list[dict]:
50
+ """Extract tool_use blocks from an Anthropic Message response."""
51
+ tool_calls = []
52
+ content = getattr(response, "content", []) or []
53
+ for block in content:
54
+ btype = getattr(block, "type", None)
55
+ if btype == "tool_use":
56
+ tool_calls.append({
57
+ "name": getattr(block, "name", ""),
58
+ "args": dict(getattr(block, "input", {}) or {}),
59
+ "id": getattr(block, "id", ""),
60
+ })
61
+ return tool_calls
62
+
63
+
64
+ def _extract_text(response: Any) -> str:
65
+ content = getattr(response, "content", []) or []
66
+ for block in content:
67
+ if getattr(block, "type", None) == "text":
68
+ return getattr(block, "text", "")
69
+ return ""
70
+
71
+
72
+ def _validate_response(
73
+ agent_id: str,
74
+ base_url: str,
75
+ api_key: str | None,
76
+ user: str,
77
+ input_text: str,
78
+ response: Any,
79
+ framework: str,
80
+ block_on_block: bool,
81
+ ) -> None:
82
+ from agentrust_sdk.client import AgentTrustClient
83
+ from agentrust_sdk.decorator import _check_decision, BlockedError
84
+
85
+ tool_calls = _extract_tool_calls(response)
86
+ output_text = _extract_text(response)
87
+ stop_reason = getattr(response, "stop_reason", "")
88
+ usage = getattr(response, "usage", None)
89
+ tokens = 0
90
+ if usage:
91
+ tokens = getattr(usage, "input_tokens", 0) + getattr(usage, "output_tokens", 0)
92
+
93
+ output: dict = {
94
+ "text": output_text[:2000],
95
+ "stop_reason": stop_reason,
96
+ "tool_calls": tool_calls,
97
+ }
98
+
99
+ try:
100
+ with AgentTrustClient(base_url=base_url, api_key=api_key) as client:
101
+ resp = client.validate(
102
+ agent_id=agent_id,
103
+ user=user,
104
+ input=input_text[:1000],
105
+ output=output,
106
+ framework=framework,
107
+ tools_called=tool_calls,
108
+ tokens=tokens,
109
+ )
110
+ _check_decision(resp, block_on_block, False)
111
+ except BlockedError:
112
+ raise
113
+ except Exception as exc:
114
+ logger.warning("[AgentTrust] Claude agent validate failed (non-fatal): %s", exc)
115
+
116
+
117
+ class ClaudeAgentGuard:
118
+ """
119
+ Wraps Anthropic client messages.create() to validate every response.
120
+
121
+ Usage::
122
+
123
+ from anthropic import Anthropic
124
+ from agentrust_sdk.adapters.claude_agents import ClaudeAgentGuard
125
+
126
+ guard = ClaudeAgentGuard(agent_id="claims-agent")
127
+ client = Anthropic()
128
+ governed = guard.wrap_client(client)
129
+
130
+ # Now use `governed` instead of `client`:
131
+ response = governed.messages.create(
132
+ model="claude-opus-4-8",
133
+ max_tokens=1024,
134
+ tools=[...],
135
+ messages=[{"role": "user", "content": query}],
136
+ )
137
+ """
138
+
139
+ def __init__(
140
+ self,
141
+ agent_id: str,
142
+ *,
143
+ base_url: str = "http://localhost:8000",
144
+ api_key: str | None = None,
145
+ user: str = "claude-agent",
146
+ block_on_block: bool = True,
147
+ framework: str = "Claude-Agents",
148
+ ) -> None:
149
+ _check_tier(api_key)
150
+ self._agent_id = agent_id
151
+ self._base_url = base_url
152
+ self._api_key = api_key
153
+ self._user = user
154
+ self._block_on_block = block_on_block
155
+ self._framework = framework
156
+
157
+ def wrap_client(self, client: Any) -> Any:
158
+ """Return a proxy around *client* that validates every messages.create() call."""
159
+ guard = self
160
+
161
+ class _MessagesProxy:
162
+ def __init__(self, original: Any) -> None:
163
+ self._original = original
164
+
165
+ def create(self, *args: Any, **kwargs: Any) -> Any:
166
+ t0 = time.perf_counter()
167
+ response = self._original.create(*args, **kwargs)
168
+ latency_ms = (time.perf_counter() - t0) * 1000
169
+
170
+ messages = kwargs.get("messages", []) or []
171
+ last_user = next(
172
+ (m.get("content", "") for m in reversed(messages) if m.get("role") == "user"),
173
+ "",
174
+ )
175
+ input_text = last_user if isinstance(last_user, str) else str(last_user)[:500]
176
+
177
+ _validate_response(
178
+ guard._agent_id, guard._base_url, guard._api_key,
179
+ guard._user, input_text, response,
180
+ guard._framework, guard._block_on_block,
181
+ )
182
+ return response
183
+
184
+ class _ClientProxy:
185
+ def __init__(self, orig: Any) -> None:
186
+ self.messages = _MessagesProxy(orig.messages)
187
+ self._orig = orig
188
+
189
+ def __getattr__(self, name: str) -> Any:
190
+ return getattr(self._orig, name)
191
+
192
+ return _ClientProxy(client)
193
+
194
+ def wrap_loop(self, fn: Any) -> Any:
195
+ """Decorator for an agentic loop function — validates the final return value."""
196
+ guard = self
197
+
198
+ @functools.wraps(fn)
199
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
200
+ t0 = time.perf_counter()
201
+ result = fn(*args, **kwargs)
202
+ latency_ms = (time.perf_counter() - t0) * 1000
203
+ output = result if isinstance(result, dict) else {"result": str(result)[:2000]}
204
+ input_text = (args[0] if args else "") or ""
205
+
206
+ from agentrust_sdk.client import AgentTrustClient
207
+ from agentrust_sdk.decorator import _check_decision, BlockedError
208
+ try:
209
+ with AgentTrustClient(base_url=guard._base_url, api_key=guard._api_key) as client:
210
+ resp = client.validate(
211
+ agent_id=guard._agent_id,
212
+ user=guard._user,
213
+ input=str(input_text)[:500],
214
+ output=output,
215
+ framework=guard._framework,
216
+ latency_ms=latency_ms,
217
+ )
218
+ _check_decision(resp, guard._block_on_block, False)
219
+ except BlockedError:
220
+ raise
221
+ except Exception as exc:
222
+ logger.warning("[AgentTrust] Claude loop validate failed (non-fatal): %s", exc)
223
+ return result
224
+
225
+ return wrapper
@@ -0,0 +1,104 @@
1
+ """
2
+ CrewAI adapter — AgentTrust callback for task execution.
3
+
4
+ Requires Team tier. Gated at construction time.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from typing import Any, Callable
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def _check_adapter_tier(api_key: str | None = None) -> None:
15
+ import os
16
+ env = os.environ.get("AGENTRUST_ENV", "production").lower()
17
+ if env in ("development", "dev", "demo", "test") or not api_key:
18
+ return
19
+ if not api_key.startswith("at_"):
20
+ return
21
+ from agentrust_sdk.auth import resolve_key
22
+ from agentrust_sdk.tiers import Capability, is_allowed, UPGRADE_MESSAGES
23
+ info = resolve_key(api_key)
24
+ if not is_allowed(Capability.CREWAI_ADAPTER, info.tier):
25
+ msg = UPGRADE_MESSAGES.get(Capability.CREWAI_ADAPTER, "CrewAI adapter requires Team tier.")
26
+ raise RuntimeError(f"[AgentTrust] {msg}")
27
+
28
+
29
+ class AgentTrustCallback:
30
+ """
31
+ CrewAI task callback that validates output via AgentTrust.
32
+
33
+ Requires Team tier ($149/mo).
34
+
35
+ Usage::
36
+
37
+ from crewai import Task
38
+ from agentrust_sdk.adapters.crewai import AgentTrustCallback
39
+
40
+ cb = AgentTrustCallback(agent_id="research-crew")
41
+ task = Task(description="...", callback=cb.on_task_complete)
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ agent_id: str,
47
+ *,
48
+ base_url: str = "http://localhost:8000",
49
+ api_key: str | None = None,
50
+ user: str = "crewai",
51
+ block_on_block: bool = True,
52
+ block_on_review: bool = False,
53
+ framework: str = "CrewAI",
54
+ ) -> None:
55
+ _check_adapter_tier(api_key)
56
+ self._agent_id = agent_id
57
+ self._base_url = base_url
58
+ self._api_key = api_key
59
+ self._user = user
60
+ self._block_on_block = block_on_block
61
+ self._block_on_review = block_on_review
62
+ self._framework = framework
63
+ self._last_result: Any = None
64
+
65
+ def on_task_complete(self, output: Any) -> Any:
66
+ from agentrust_sdk.client import AgentTrustClient
67
+ from agentrust_sdk.decorator import BlockedError, _check_decision
68
+
69
+ raw = output
70
+ if hasattr(output, "raw"):
71
+ raw = output.raw
72
+ payload = raw if isinstance(raw, dict) else {"result": str(raw)}
73
+
74
+ try:
75
+ with AgentTrustClient(base_url=self._base_url, api_key=self._api_key) as client:
76
+ resp = client.validate(
77
+ agent_id=self._agent_id,
78
+ user=self._user,
79
+ input="crewai-task",
80
+ output=payload,
81
+ framework=self._framework,
82
+ )
83
+ _check_decision(resp, self._block_on_block, self._block_on_review)
84
+ self._last_result = resp
85
+ except BlockedError:
86
+ raise
87
+ except Exception as exc:
88
+ logger.warning("[AgentTrust] CrewAI callback failed (non-fatal): %s", exc)
89
+
90
+ return output
91
+
92
+ def task_callback(self, fn: Callable) -> Callable:
93
+ import functools
94
+
95
+ @functools.wraps(fn)
96
+ def wrapper(output: Any) -> Any:
97
+ self.on_task_complete(output)
98
+ return fn(output)
99
+
100
+ return wrapper
101
+
102
+ @property
103
+ def last_result(self) -> Any:
104
+ return self._last_result
@@ -0,0 +1,115 @@
1
+ """
2
+ LangGraph adapter — AgentTrust validation node.
3
+
4
+ Requires Team tier. Gated at import-time for clear error messaging.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from typing import Any
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def _check_adapter_tier(api_key: str | None = None) -> None:
15
+ import os
16
+ env = os.environ.get("AGENTRUST_ENV", "production").lower()
17
+ if env in ("development", "dev", "demo", "test") or not api_key:
18
+ return
19
+ if not api_key.startswith("at_"):
20
+ return
21
+ from agentrust_sdk.auth import resolve_key
22
+ from agentrust_sdk.tiers import Capability, is_allowed, UPGRADE_MESSAGES
23
+ info = resolve_key(api_key)
24
+ if not is_allowed(Capability.LANGGRAPH_ADAPTER, info.tier):
25
+ msg = UPGRADE_MESSAGES.get(Capability.LANGGRAPH_ADAPTER, "LangGraph adapter requires Team tier.")
26
+ raise RuntimeError(f"[AgentTrust] {msg}")
27
+
28
+
29
+ class AgentTrustNode:
30
+ """
31
+ Callable LangGraph node that validates state via AgentTrust.
32
+
33
+ Requires Team tier ($149/mo).
34
+
35
+ State keys:
36
+ output_key → dict output to validate (default: "output")
37
+ input_key → user's request text (default: "input")
38
+ user_key → user identity string (default: "user")
39
+
40
+ Injects ``_agentrust_result`` into the returned state.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ agent_id: str,
46
+ *,
47
+ base_url: str = "http://localhost:8000",
48
+ api_key: str | None = None,
49
+ output_key: str = "output",
50
+ input_key: str = "input",
51
+ user_key: str = "user",
52
+ block_on_block: bool = True,
53
+ block_on_review: bool = False,
54
+ framework: str = "LangGraph",
55
+ ) -> None:
56
+ _check_adapter_tier(api_key)
57
+ self._agent_id = agent_id
58
+ self._base_url = base_url
59
+ self._api_key = api_key
60
+ self._output_key = output_key
61
+ self._input_key = input_key
62
+ self._user_key = user_key
63
+ self._block_on_block = block_on_block
64
+ self._block_on_review = block_on_review
65
+ self._framework = framework
66
+
67
+ def __call__(self, state: dict[str, Any]) -> dict[str, Any]:
68
+ from agentrust_sdk.client import AgentTrustClient
69
+ from agentrust_sdk.decorator import BlockedError, _check_decision
70
+
71
+ output = state.get(self._output_key, {})
72
+ if not isinstance(output, dict):
73
+ output = {"result": str(output)}
74
+
75
+ try:
76
+ with AgentTrustClient(base_url=self._base_url, api_key=self._api_key) as client:
77
+ resp = client.validate(
78
+ agent_id=self._agent_id,
79
+ user=str(state.get(self._user_key, "unknown")),
80
+ input=str(state.get(self._input_key, "")),
81
+ output=output,
82
+ framework=self._framework,
83
+ )
84
+ _check_decision(resp, self._block_on_block, self._block_on_review)
85
+ return {**state, "_agentrust_result": resp.model_dump()}
86
+ except BlockedError:
87
+ raise
88
+ except Exception as exc:
89
+ logger.warning("[AgentTrust] LangGraph node failed (non-fatal): %s", exc)
90
+ return state
91
+
92
+ async def ainvoke(self, state: dict[str, Any]) -> dict[str, Any]:
93
+ from agentrust_sdk.client import AsyncAgentTrustClient
94
+ from agentrust_sdk.decorator import BlockedError, _check_decision
95
+
96
+ output = state.get(self._output_key, {})
97
+ if not isinstance(output, dict):
98
+ output = {"result": str(output)}
99
+
100
+ try:
101
+ async with AsyncAgentTrustClient(base_url=self._base_url, api_key=self._api_key) as client:
102
+ resp = await client.validate(
103
+ agent_id=self._agent_id,
104
+ user=str(state.get(self._user_key, "unknown")),
105
+ input=str(state.get(self._input_key, "")),
106
+ output=output,
107
+ framework=self._framework,
108
+ )
109
+ _check_decision(resp, self._block_on_block, self._block_on_review)
110
+ return {**state, "_agentrust_result": resp.model_dump()}
111
+ except BlockedError:
112
+ raise
113
+ except Exception as exc:
114
+ logger.warning("[AgentTrust] LangGraph async node failed (non-fatal): %s", exc)
115
+ return state