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.
- agentrust/__init__.py +72 -0
- agentrust_py-0.0.1a1.dist-info/METADATA +193 -0
- agentrust_py-0.0.1a1.dist-info/RECORD +29 -0
- agentrust_py-0.0.1a1.dist-info/WHEEL +4 -0
- agentrust_py-0.0.1a1.dist-info/entry_points.txt +2 -0
- agentrust_py-0.0.1a1.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 +104 -0
- agentrust_sdk/adapters/langgraph.py +115 -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 +792 -0
- agentrust_sdk/config.py +205 -0
- agentrust_sdk/decorator.py +288 -0
- agentrust_sdk/embedded.py +438 -0
- agentrust_sdk/hooks.py +461 -0
- agentrust_sdk/models.py +87 -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
|
@@ -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
|