agentrust-py 0.0.7__tar.gz → 0.0.8__tar.gz
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_py-0.0.7 → agentrust_py-0.0.8}/PKG-INFO +3 -1
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/__init__.py +3 -1
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/adapters/autogen.py +135 -1
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/config.py +1 -1
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/pyproject.toml +3 -1
- agentrust_py-0.0.8/tests/adapters/test_ag2_quickstart_example.py +145 -0
- agentrust_py-0.0.8/tests/adapters/test_autogen_quickstart_example.py +138 -0
- agentrust_py-0.0.8/tests/adapters/test_autogen_v04_governed_tool.py +243 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/test_callback_url.py +1 -1
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/.gitignore +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/CHANGELOG.md +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/LICENSE +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/README.md +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust/__init__.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/_evaluations.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/_real_validator.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/adapters/__init__.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/adapters/_core.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/adapters/claude_agents.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/adapters/crewai.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/adapters/langgraph.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/adapters/mcp.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/adapters/openai_agents.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/audit/__init__.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/audit/logger.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/auth.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/auto.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/autoload.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/classifier/__init__.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/classifier/engine.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/classifier/profiles.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/classifier/system_prompt.md +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/cli.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/cli_certify.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/client.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/decorator.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/embedded.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/golden_suite.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/governance/__init__.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/governance/hitl.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/governance/kernel.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/governance/kill_switch.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/governance/modes.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/hooks.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/manifest.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/models.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/notifications/__init__.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/notifications/discord.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/notifications/dispatcher.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/notifications/slack.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/policy/__init__.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/policy/backends/__init__.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/policy/backends/native.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/policy/backends/opa.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/policy/engine.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/policy/loader.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/py.typed +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/queue_replay.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/tiers.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/tool_tracking.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/version_negotiation.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/agentrust_sdk/webhooks.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/policy/base.yaml +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/policy/default-policy.json +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/policy/financial.yaml +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/policy/medical.yaml +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/policy/profiles/advisory.json +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/policy/profiles/balanced.json +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/policy/profiles/strict.json +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/__init__.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/_cfg.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/__init__.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/_ag2.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/_crews.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/_graphs.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/test_ag2_governed_function.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/test_ag2_reply_hook.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/test_ag2_research_team_example.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/test_crewai_baseline.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/test_crewai_failure_modes.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/test_crewai_governed_tools.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/test_crewai_holds_and_chain.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/test_crewai_quickstart_example.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/test_crewai_task_checks.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/test_langgraph_baseline.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/test_langgraph_failure_modes.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/test_langgraph_governed_tools.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/test_langgraph_holds_and_chain.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/test_langgraph_node.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/test_langgraph_quickstart_example.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/conformance/__init__.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/conformance/bindings.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/conformance/test_c1_c10.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/conftest.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/test_adapter_tier_gate.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/test_adapters.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/test_async_advanced.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/test_chaos.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/test_cli_certify.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/test_client.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/test_client_adapter_contract.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/test_config.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/test_decorator.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/test_embedded.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/test_embedded_adapter_contract.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/test_golden_suite.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/test_governor.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/test_hooks.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/test_manifest.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/test_policy_packaging.py +0 -0
- {agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/test_webhooks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentrust-py
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.8
|
|
4
4
|
Summary: AgentTrust Edge — Full Governance Platform. Policy Engine + MAD Classifier + HITL + Kill Switch.
|
|
5
5
|
Project-URL: Homepage, https://agentrust.io
|
|
6
6
|
Project-URL: Documentation, https://docs.agentrust.io
|
|
@@ -25,6 +25,8 @@ Requires-Dist: pydantic<3.0.0,>=2.0.0
|
|
|
25
25
|
Requires-Dist: pyyaml<7.0,>=6.0.3
|
|
26
26
|
Provides-Extra: ag2
|
|
27
27
|
Requires-Dist: ag2<1.0,>=0.9; extra == 'ag2'
|
|
28
|
+
Provides-Extra: autogen
|
|
29
|
+
Requires-Dist: autogen-agentchat<1.0,>=0.4; extra == 'autogen'
|
|
28
30
|
Provides-Extra: autoload
|
|
29
31
|
Provides-Extra: crewai
|
|
30
32
|
Requires-Dist: crewai<2.0,>=1.0; extra == 'crewai'
|
|
@@ -43,6 +43,8 @@ from .adapters.autogen import (
|
|
|
43
43
|
AgentTrustReplyHook,
|
|
44
44
|
governed_function,
|
|
45
45
|
governed_functions,
|
|
46
|
+
governed_tool as governed_autogen_tool,
|
|
47
|
+
governed_tools as governed_autogen_tools,
|
|
46
48
|
)
|
|
47
49
|
from .adapters.claude_agents import ClaudeAgentGuard
|
|
48
50
|
from .adapters.openai_agents import agent_guard as openai_agent_guard, OpenAIAgentsGuard, SwarmGuard
|
|
@@ -154,4 +156,4 @@ __all__ = [
|
|
|
154
156
|
"validate",
|
|
155
157
|
]
|
|
156
158
|
|
|
157
|
-
__version__ = "0.0.
|
|
159
|
+
__version__ = "0.0.8"
|
|
@@ -10,6 +10,10 @@ Pattern 0 — governed_function (AG2 / pyautogen 0.2 tools):
|
|
|
10
10
|
held for approval when needed, run, post-checked and audited. A refusal
|
|
11
11
|
is returned as the tool result, so the chat keeps going.
|
|
12
12
|
|
|
13
|
+
Pattern 0b — governed_tool (AutoGen 0.4+ / autogen-agentchat tools):
|
|
14
|
+
Wrap each FunctionTool (or plain function) before giving it to an
|
|
15
|
+
AssistantAgent. Same checks as governed_function.
|
|
16
|
+
|
|
13
17
|
Pattern 1 — AgentTrustReplyHook (v0.2 + v0.4):
|
|
14
18
|
Attach to any ConversableAgent / AssistantAgent to validate every reply
|
|
15
19
|
before it is sent.
|
|
@@ -42,6 +46,7 @@ from ._core import Allowed, Governor, OnBlock, Refused, check_adapter_tier
|
|
|
42
46
|
logger = logging.getLogger(__name__)
|
|
43
47
|
|
|
44
48
|
_FRAMEWORK = "ag2"
|
|
49
|
+
_FRAMEWORK_V04 = "autogen"
|
|
45
50
|
|
|
46
51
|
|
|
47
52
|
def governed_function(
|
|
@@ -132,6 +137,102 @@ def governed_functions(
|
|
|
132
137
|
return [governed_function(fn, agent_id=agent_id, governor=gov) for fn in fns]
|
|
133
138
|
|
|
134
139
|
|
|
140
|
+
def _governor(
|
|
141
|
+
agent_id: str, framework: str, on_block: OnBlock, review_timeout_s: float,
|
|
142
|
+
user: str, base_url: str | None, api_key: str | None,
|
|
143
|
+
) -> Governor:
|
|
144
|
+
return Governor(
|
|
145
|
+
agent_id, framework=framework, on_block=on_block, review_timeout_s=review_timeout_s,
|
|
146
|
+
user=user, base_url=base_url, api_key=api_key,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def governed_tool(
|
|
151
|
+
tool: Any,
|
|
152
|
+
*,
|
|
153
|
+
agent_id: str,
|
|
154
|
+
on_block: OnBlock = "refuse",
|
|
155
|
+
review_timeout_s: float = 300.0,
|
|
156
|
+
user: str = "autogen",
|
|
157
|
+
base_url: str | None = None,
|
|
158
|
+
api_key: str | None = None,
|
|
159
|
+
governor: Governor | None = None,
|
|
160
|
+
) -> Any:
|
|
161
|
+
"""Wrap an AutoGen 0.4+ tool so AgentTrust governs every call.
|
|
162
|
+
|
|
163
|
+
Accepts a ``BaseTool`` such as ``FunctionTool``, or a plain function (its
|
|
164
|
+
docstring becomes the description). The result has the same name,
|
|
165
|
+
description and argument schema, so the model sees the same tool. Pass it
|
|
166
|
+
in ``AssistantAgent(tools=[...])``. A refused call reaches the agent as the
|
|
167
|
+
tool result.
|
|
168
|
+
"""
|
|
169
|
+
check_adapter_tier(Capability.AUTOGEN_ADAPTER, api_key)
|
|
170
|
+
gov = governor or _governor(
|
|
171
|
+
agent_id, _FRAMEWORK_V04, on_block, review_timeout_s, user, base_url, api_key,
|
|
172
|
+
)
|
|
173
|
+
return _governed_tool_class().wrap(_as_tool(tool), gov)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def governed_tools(
|
|
177
|
+
tools: Sequence[Any],
|
|
178
|
+
*,
|
|
179
|
+
agent_id: str,
|
|
180
|
+
on_block: OnBlock = "refuse",
|
|
181
|
+
review_timeout_s: float = 300.0,
|
|
182
|
+
user: str = "autogen",
|
|
183
|
+
base_url: str | None = None,
|
|
184
|
+
api_key: str | None = None,
|
|
185
|
+
governor: Governor | None = None,
|
|
186
|
+
) -> list[Any]:
|
|
187
|
+
"""Wrap each AutoGen 0.4+ tool with :func:`governed_tool`, sharing one Governor."""
|
|
188
|
+
check_adapter_tier(Capability.AUTOGEN_ADAPTER, api_key)
|
|
189
|
+
gov = governor or _governor(
|
|
190
|
+
agent_id, _FRAMEWORK_V04, on_block, review_timeout_s, user, base_url, api_key,
|
|
191
|
+
)
|
|
192
|
+
return [governed_tool(t, agent_id=agent_id, governor=gov) for t in tools]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _as_tool(tool: Any) -> Any:
|
|
196
|
+
from autogen_core.tools import BaseTool, FunctionTool
|
|
197
|
+
|
|
198
|
+
if isinstance(tool, BaseTool):
|
|
199
|
+
return tool
|
|
200
|
+
return FunctionTool(tool, description=inspect.getdoc(tool) or tool.__name__)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@functools.lru_cache(maxsize=None)
|
|
204
|
+
def _governed_tool_class() -> type:
|
|
205
|
+
"""Build GovernedTool on first use, so autogen-core stays optional."""
|
|
206
|
+
from autogen_core import CancellationToken
|
|
207
|
+
from autogen_core.tools import BaseTool
|
|
208
|
+
|
|
209
|
+
class GovernedTool(BaseTool):
|
|
210
|
+
"""A tool whose every call goes through an AgentTrust Governor."""
|
|
211
|
+
|
|
212
|
+
def __init__(self, inner: Any, governor: Governor) -> None:
|
|
213
|
+
super().__init__(
|
|
214
|
+
inner.args_type(), inner.return_type(), inner.name, inner.description,
|
|
215
|
+
strict=getattr(inner, "_strict", False),
|
|
216
|
+
)
|
|
217
|
+
self._inner = inner
|
|
218
|
+
self._governor = governor
|
|
219
|
+
|
|
220
|
+
@classmethod
|
|
221
|
+
def wrap(cls, inner: Any, governor: Governor) -> "GovernedTool":
|
|
222
|
+
return cls(inner, governor)
|
|
223
|
+
|
|
224
|
+
async def run(self, args: Any, cancellation_token: CancellationToken) -> Any:
|
|
225
|
+
params = args.model_dump()
|
|
226
|
+
outcome = await self._governor.aguard_tool(
|
|
227
|
+
self.name, params, lambda: self._inner.run(args, cancellation_token),
|
|
228
|
+
input=_describe_call(self.name, params),
|
|
229
|
+
)
|
|
230
|
+
# The refusal text is the tool result the agent reads next.
|
|
231
|
+
return outcome.message if isinstance(outcome, Refused) else outcome.result
|
|
232
|
+
|
|
233
|
+
return GovernedTool
|
|
234
|
+
|
|
235
|
+
|
|
135
236
|
def _describe_call(name: str, args: dict[str, Any]) -> str:
|
|
136
237
|
"""The proposed call as the pre-check's request input."""
|
|
137
238
|
return f"tool call {name}({', '.join(f'{k}={v!r}' for k, v in args.items())})"
|
|
@@ -169,6 +270,39 @@ def _validate_sync(
|
|
|
169
270
|
logger.warning("[AgentTrust] AutoGen validate failed (non-fatal): %s", exc)
|
|
170
271
|
|
|
171
272
|
|
|
273
|
+
async def _avalidate(
|
|
274
|
+
agent_id: str,
|
|
275
|
+
base_url: str,
|
|
276
|
+
api_key: str | None,
|
|
277
|
+
user: str,
|
|
278
|
+
input_text: str,
|
|
279
|
+
output: dict,
|
|
280
|
+
framework: str,
|
|
281
|
+
block_on_block: bool,
|
|
282
|
+
) -> None:
|
|
283
|
+
"""Async counterpart of :func:`_validate_sync`, safe to await in an event loop."""
|
|
284
|
+
from agentrust_sdk.client import AsyncAgentTrustClient, GatewayUnavailableError
|
|
285
|
+
from agentrust_sdk.config import SDK_CONFIG
|
|
286
|
+
from agentrust_sdk.decorator import _check_decision, BlockedError
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
async with AsyncAgentTrustClient(base_url=base_url, api_key=api_key) as client:
|
|
290
|
+
resp = await client.validate(
|
|
291
|
+
agent_id=agent_id,
|
|
292
|
+
user=user,
|
|
293
|
+
input=input_text,
|
|
294
|
+
output=output,
|
|
295
|
+
framework=framework,
|
|
296
|
+
)
|
|
297
|
+
_check_decision(resp, block_on_block, False)
|
|
298
|
+
except (BlockedError, GatewayUnavailableError):
|
|
299
|
+
raise
|
|
300
|
+
except Exception as exc:
|
|
301
|
+
if SDK_CONFIG.failure_mode == "closed":
|
|
302
|
+
raise
|
|
303
|
+
logger.warning("[AgentTrust] AutoGen validate failed (non-fatal): %s", exc)
|
|
304
|
+
|
|
305
|
+
|
|
172
306
|
class AgentTrustReplyHook:
|
|
173
307
|
"""
|
|
174
308
|
Reply hook that validates every AutoGen agent reply via AgentTrust.
|
|
@@ -259,7 +393,7 @@ class AgentTrustReplyHook:
|
|
|
259
393
|
if hasattr(result, "chat_message") and result.chat_message:
|
|
260
394
|
content = getattr(result.chat_message, "content", "") or ""
|
|
261
395
|
output = {"content": content[:4000]}
|
|
262
|
-
|
|
396
|
+
await _avalidate(
|
|
263
397
|
self._agent_id, self._base_url, self._api_key,
|
|
264
398
|
self._user, content[:500], output, self._framework, self._block_on_block,
|
|
265
399
|
)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentrust-py"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.8"
|
|
8
8
|
description = "AgentTrust Edge — Full Governance Platform. Policy Engine + MAD Classifier + HITL + Kill Switch."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "Apache-2.0" }
|
|
@@ -43,6 +43,8 @@ langgraph = ["langgraph>=0.2.0,<2.0.0"]
|
|
|
43
43
|
crewai = ["crewai>=1.0,<2.0"]
|
|
44
44
|
# AG2 0.x keeps the ConversableAgent API (module `autogen`); 1.x is a new API
|
|
45
45
|
ag2 = ["ag2>=0.9,<1.0"]
|
|
46
|
+
# Microsoft AutoGen 0.4+ (module autogen_agentchat)
|
|
47
|
+
autogen = ["autogen-agentchat>=0.4,<1.0"]
|
|
46
48
|
openai = ["openai>=2.41.1,<3.0.0"]
|
|
47
49
|
langchain = ["langchain>=0.2.0,<1.0.0"]
|
|
48
50
|
# Retry/backoff (recommended for production)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""examples/ag2_quickstart.py: the calculator quickstart under governed_functions.
|
|
2
|
+
|
|
3
|
+
The example's own agents run with a scripted model against an embedded
|
|
4
|
+
gateway that has the example's manifest (add and multiply declared, divide
|
|
5
|
+
not).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import importlib.util
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
pytest.importorskip("autogen")
|
|
16
|
+
pytestmark = pytest.mark.framework
|
|
17
|
+
|
|
18
|
+
from tests.adapters import _ag2
|
|
19
|
+
from tests.adapters._ag2 import final, tool_call
|
|
20
|
+
|
|
21
|
+
EXAMPLE = Path(__file__).resolve().parents[3] / "examples" / "ag2_quickstart.py"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def quickstart():
|
|
26
|
+
if not EXAMPLE.exists():
|
|
27
|
+
pytest.skip("examples/ is not part of this checkout")
|
|
28
|
+
spec = importlib.util.spec_from_file_location("ag2_quickstart", EXAMPLE)
|
|
29
|
+
module = importlib.util.module_from_spec(spec)
|
|
30
|
+
spec.loader.exec_module(module)
|
|
31
|
+
return module
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def restore_model(monkeypatch):
|
|
36
|
+
"""--scripted patches AG2's model call for good; undo that after the test."""
|
|
37
|
+
from autogen.oai.client import OpenAIWrapper
|
|
38
|
+
|
|
39
|
+
monkeypatch.setattr(OpenAIWrapper, "create", OpenAIWrapper.create)
|
|
40
|
+
monkeypatch.setattr(
|
|
41
|
+
OpenAIWrapper, "extract_text_or_completion_object", OpenAIWrapper.extract_text_or_completion_object,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.fixture
|
|
46
|
+
def gateway(embedded_gw, quickstart):
|
|
47
|
+
return embedded_gw(manifests={quickstart.AGENT_ID: quickstart.MANIFEST})
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _ledger(gateway):
|
|
51
|
+
"""The gateway's decisions for the example's agent, oldest first: phase, tool, decision, envelope, parent."""
|
|
52
|
+
import httpx
|
|
53
|
+
|
|
54
|
+
resp = httpx.get(f"{gateway.url}/v1/audit/executions", params={"limit": 100},
|
|
55
|
+
headers={"Authorization": f"Bearer {gateway._token}"})
|
|
56
|
+
rows = sorted(resp.json()["items"], key=lambda r: r["id"])
|
|
57
|
+
return [[r["phase"], json.loads(r["request_json"] or "{}").get("metadata", {}).get("action", "-"),
|
|
58
|
+
r["decision"], r["envelope_id"][:8], (r["parent_envelope_id"] or "-")[:8]] for r in rows]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _ask(quickstart, request):
|
|
62
|
+
return quickstart.ask(request, quickstart.llm_config(scripted=True))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_declared_tool_runs_and_both_checks_are_recorded(quickstart, gateway, monkeypatch):
|
|
66
|
+
model = _ag2.install(monkeypatch, tool_call("add", {"a": 3, "b": 4}), final("3 + 4 = 7. DONE"))
|
|
67
|
+
|
|
68
|
+
assert "3 + 4 = 7" in _ask(quickstart, "Add 3 and 4.")
|
|
69
|
+
|
|
70
|
+
assert any('"7"' in p or "7" in p.split("tool")[-1] for p in model.prompts[1:])
|
|
71
|
+
pre, post = _ledger(gateway)
|
|
72
|
+
assert pre[:3] == ["pre_check", "add", "approve"]
|
|
73
|
+
assert post[:3] == ["post_check", "add", "approve"]
|
|
74
|
+
assert post[4] == pre[3] # the post-check row points at its pre-check
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_undeclared_tool_is_refused_before_it_runs(quickstart, gateway, monkeypatch):
|
|
78
|
+
ran = []
|
|
79
|
+
|
|
80
|
+
def divide(a: int, b: int) -> float:
|
|
81
|
+
"""Divide `a` by `b`."""
|
|
82
|
+
ran.append((a, b))
|
|
83
|
+
return a / b
|
|
84
|
+
|
|
85
|
+
from agentrust_sdk.adapters.autogen import governed_function
|
|
86
|
+
|
|
87
|
+
monkeypatch.setattr(quickstart, "tools", [governed_function(divide, agent_id=quickstart.AGENT_ID)])
|
|
88
|
+
model = _ag2.install(monkeypatch, tool_call("divide", {"a": 10, "b": 2}), final("I can't divide. DONE"))
|
|
89
|
+
|
|
90
|
+
assert "I can't divide." in _ask(quickstart, "Divide 10 by 2.") # the chat still finished
|
|
91
|
+
|
|
92
|
+
assert ran == []
|
|
93
|
+
refusal = [p for p in model.prompts if "Blocked by AgentTrust" in p]
|
|
94
|
+
assert refusal and "'divide' is not in this agent's declared_tools" in refusal[-1]
|
|
95
|
+
[pre] = _ledger(gateway)
|
|
96
|
+
assert pre[:3] == ["pre_check", "divide", "block"]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_agentrust_off_lets_every_tool_run(quickstart, monkeypatch):
|
|
100
|
+
monkeypatch.setenv("AGENTRUST_ENABLED", "false")
|
|
101
|
+
model = _ag2.install(monkeypatch, tool_call("divide", {"a": 10, "b": 2}), final("10 / 2 = 5. DONE"))
|
|
102
|
+
|
|
103
|
+
assert "10 / 2 = 5" in _ask(quickstart, "Divide 10 by 2.")
|
|
104
|
+
|
|
105
|
+
assert any("5.0" in p for p in model.prompts[1:])
|
|
106
|
+
assert not any("Blocked by AgentTrust" in p for p in model.prompts)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_scripted_flag_runs_the_whole_quickstart_with_no_model(
|
|
110
|
+
quickstart, gateway, restore_model, capsys, monkeypatch,
|
|
111
|
+
):
|
|
112
|
+
# --console talks to the gateway the fixture started (embed_gateway keeps one per process).
|
|
113
|
+
monkeypatch.setenv("AGENTRUST_AGENT_ID", quickstart.AGENT_ID)
|
|
114
|
+
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
115
|
+
|
|
116
|
+
assert quickstart.main(["--console", "--scripted"]) == 0
|
|
117
|
+
|
|
118
|
+
out = capsys.readouterr().out
|
|
119
|
+
assert "model scripted" in out
|
|
120
|
+
assert "Result: 3 + 4 = 7." in out
|
|
121
|
+
assert "Result: That is everything you asked for." in out
|
|
122
|
+
assert [r[:3] for r in _ledger(gateway)] == [
|
|
123
|
+
["pre_check", "add", "approve"], ["post_check", "add", "approve"], ["pre_check", "divide", "block"],
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_without_agentrust_flag_runs_with_agentrust_off(quickstart, restore_model, capsys, monkeypatch):
|
|
128
|
+
monkeypatch.setenv("AGENTRUST_ENABLED", "true") # main() turns it off; monkeypatch restores it
|
|
129
|
+
|
|
130
|
+
assert quickstart.main(["--without-agentrust", "--scripted"]) == 0
|
|
131
|
+
|
|
132
|
+
out = capsys.readouterr().out
|
|
133
|
+
assert "AgentTrust: off" in out
|
|
134
|
+
assert "Blocked by AgentTrust" not in out
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_console_flag_names_the_missing_settings(quickstart, monkeypatch, capsys):
|
|
138
|
+
for name in ("AGENTRUST_GATEWAY_URL", "AGENTRUST_KEY", "AGENTRUST_AGENT_ID"):
|
|
139
|
+
monkeypatch.delenv(name, raising=False)
|
|
140
|
+
|
|
141
|
+
assert quickstart.main(["--console"]) == 2
|
|
142
|
+
|
|
143
|
+
err = capsys.readouterr().err
|
|
144
|
+
assert "AGENTRUST_GATEWAY_URL, AGENTRUST_KEY, AGENTRUST_AGENT_ID" in err
|
|
145
|
+
assert "--local" in err
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""examples/autogen_quickstart.py: the calculator quickstart under governed_tools (AutoGen 0.4+).
|
|
2
|
+
|
|
3
|
+
The example's own agent runs with a scripted model against an embedded
|
|
4
|
+
gateway that has the example's manifest (add and multiply declared, divide
|
|
5
|
+
not).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import importlib.util
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
pytest.importorskip("autogen_agentchat")
|
|
17
|
+
pytest.importorskip("autogen_ext")
|
|
18
|
+
pytestmark = pytest.mark.framework
|
|
19
|
+
|
|
20
|
+
EXAMPLE = Path(__file__).resolve().parents[3] / "examples" / "autogen_quickstart.py"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def quickstart():
|
|
25
|
+
if not EXAMPLE.exists():
|
|
26
|
+
pytest.skip("examples/ is not part of this checkout")
|
|
27
|
+
spec = importlib.util.spec_from_file_location("autogen_quickstart", EXAMPLE)
|
|
28
|
+
module = importlib.util.module_from_spec(spec)
|
|
29
|
+
spec.loader.exec_module(module)
|
|
30
|
+
return module
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def gateway(embedded_gw, quickstart):
|
|
35
|
+
return embedded_gw(manifests={quickstart.AGENT_ID: quickstart.MANIFEST})
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _ledger(gateway):
|
|
39
|
+
"""The gateway's decisions for the example's agent, oldest first: phase, tool, decision, envelope, parent."""
|
|
40
|
+
import httpx
|
|
41
|
+
|
|
42
|
+
resp = httpx.get(f"{gateway.url}/v1/audit/executions", params={"limit": 100},
|
|
43
|
+
headers={"Authorization": f"Bearer {gateway._token}"})
|
|
44
|
+
rows = sorted(resp.json()["items"], key=lambda r: r["id"])
|
|
45
|
+
return [[r["phase"], json.loads(r["request_json"] or "{}").get("metadata", {}).get("action", "-"),
|
|
46
|
+
r["decision"], r["envelope_id"][:8], (r["parent_envelope_id"] or "-")[:8]] for r in rows]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _run(quickstart, request):
|
|
50
|
+
agent = quickstart.build_agent(quickstart.scripted_client())
|
|
51
|
+
return asyncio.run(agent.run(task=request))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _tool_results(result):
|
|
55
|
+
return [r.content for m in result.messages if m.type == "ToolCallExecutionEvent" for r in m.content]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_declared_tool_runs_and_both_checks_are_recorded(quickstart, gateway):
|
|
59
|
+
result = _run(quickstart, "Add 3 and 4.")
|
|
60
|
+
|
|
61
|
+
assert "3 + 4 = 7" in result.messages[-1].content
|
|
62
|
+
assert _tool_results(result) == ["7"]
|
|
63
|
+
pre, post = _ledger(gateway)
|
|
64
|
+
assert pre[:3] == ["pre_check", "add", "approve"]
|
|
65
|
+
assert post[:3] == ["post_check", "add", "approve"]
|
|
66
|
+
assert post[4] == pre[3] # the post-check row points at its pre-check
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_undeclared_tool_is_refused_before_it_runs(quickstart, gateway, monkeypatch):
|
|
70
|
+
ran = []
|
|
71
|
+
inner = quickstart.tools[2]._inner # divide, behind the AgentTrust wrapper
|
|
72
|
+
original = inner.run
|
|
73
|
+
|
|
74
|
+
async def spy(args, token):
|
|
75
|
+
ran.append(args)
|
|
76
|
+
return await original(args, token)
|
|
77
|
+
|
|
78
|
+
monkeypatch.setattr(inner, "run", spy)
|
|
79
|
+
client = quickstart.scripted_client()
|
|
80
|
+
asyncio.run(quickstart.build_agent(client).run(task="Add 3 and 4.")) # consume the first script pair
|
|
81
|
+
ledger_before = len(_ledger(gateway))
|
|
82
|
+
|
|
83
|
+
result = asyncio.run(quickstart.build_agent(client).run(task="Divide 10 by 2."))
|
|
84
|
+
|
|
85
|
+
assert ran == []
|
|
86
|
+
assert "That is everything you asked for." in result.messages[-1].content # the run still finished
|
|
87
|
+
[refusal] = _tool_results(result)
|
|
88
|
+
assert "Blocked by AgentTrust" in refusal and "'divide' is not in this agent's declared_tools" in refusal
|
|
89
|
+
assert _ledger(gateway)[ledger_before][:3] == ["pre_check", "divide", "block"]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_agentrust_off_lets_every_tool_run(quickstart, monkeypatch):
|
|
93
|
+
monkeypatch.setenv("AGENTRUST_ENABLED", "false")
|
|
94
|
+
client = quickstart.scripted_client()
|
|
95
|
+
asyncio.run(quickstart.build_agent(client).run(task="Add 3 and 4."))
|
|
96
|
+
|
|
97
|
+
result = asyncio.run(quickstart.build_agent(client).run(task="Divide 10 by 2."))
|
|
98
|
+
|
|
99
|
+
assert _tool_results(result) == ["5.0"]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_scripted_flag_runs_the_whole_quickstart_with_no_model(
|
|
103
|
+
quickstart, gateway, capsys, monkeypatch,
|
|
104
|
+
):
|
|
105
|
+
# --console talks to the gateway the fixture started (embed_gateway keeps one per process).
|
|
106
|
+
monkeypatch.setenv("AGENTRUST_AGENT_ID", quickstart.AGENT_ID)
|
|
107
|
+
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
108
|
+
|
|
109
|
+
assert quickstart.main(["--console", "--scripted"]) == 0
|
|
110
|
+
|
|
111
|
+
out = capsys.readouterr().out
|
|
112
|
+
assert "model scripted" in out
|
|
113
|
+
assert "Result: 3 + 4 = 7." in out
|
|
114
|
+
assert "Result: That is everything you asked for." in out
|
|
115
|
+
assert [r[:3] for r in _ledger(gateway)] == [
|
|
116
|
+
["pre_check", "add", "approve"], ["post_check", "add", "approve"], ["pre_check", "divide", "block"],
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_without_agentrust_flag_runs_with_agentrust_off(quickstart, capsys, monkeypatch):
|
|
121
|
+
monkeypatch.setenv("AGENTRUST_ENABLED", "true") # main() turns it off; monkeypatch restores it
|
|
122
|
+
|
|
123
|
+
assert quickstart.main(["--without-agentrust", "--scripted"]) == 0
|
|
124
|
+
|
|
125
|
+
out = capsys.readouterr().out
|
|
126
|
+
assert "AgentTrust: off" in out
|
|
127
|
+
assert "divide returned: 5.0" in out
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_console_flag_names_the_missing_settings(quickstart, monkeypatch, capsys):
|
|
131
|
+
for name in ("AGENTRUST_GATEWAY_URL", "AGENTRUST_KEY", "AGENTRUST_AGENT_ID"):
|
|
132
|
+
monkeypatch.delenv(name, raising=False)
|
|
133
|
+
|
|
134
|
+
assert quickstart.main(["--console"]) == 2
|
|
135
|
+
|
|
136
|
+
err = capsys.readouterr().err
|
|
137
|
+
assert "AGENTRUST_GATEWAY_URL, AGENTRUST_KEY, AGENTRUST_AGENT_ID" in err
|
|
138
|
+
assert "--local" in err
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""AutoGen 0.4+ (autogen-agentchat): governed_tool and the async reply hook."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
pytest.importorskip("autogen_agentchat")
|
|
10
|
+
pytest.importorskip("autogen_ext")
|
|
11
|
+
pytestmark = pytest.mark.framework
|
|
12
|
+
|
|
13
|
+
from autogen_agentchat.agents import AssistantAgent
|
|
14
|
+
from autogen_core import CancellationToken, FunctionCall
|
|
15
|
+
from autogen_core.models import CreateResult, RequestUsage
|
|
16
|
+
from autogen_core.tools import FunctionTool
|
|
17
|
+
from autogen_ext.models.replay import ReplayChatCompletionClient
|
|
18
|
+
|
|
19
|
+
from agentrust_sdk.adapters.autogen import AgentTrustReplyHook, governed_tool, governed_tools
|
|
20
|
+
from tests.test_governor import FakeAsyncClient, _gov, _resp
|
|
21
|
+
|
|
22
|
+
AGENT = "autogen-v04-agent"
|
|
23
|
+
TOKEN = CancellationToken()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _refund_tool(calls):
|
|
27
|
+
async def refund(amount: float, reason: str = "") -> str:
|
|
28
|
+
"""Refund an amount to the customer."""
|
|
29
|
+
calls.append(amount)
|
|
30
|
+
return f"refunded {amount}"
|
|
31
|
+
|
|
32
|
+
return FunctionTool(refund, description="Refund an amount to the customer.")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _call(name, args):
|
|
36
|
+
import json
|
|
37
|
+
|
|
38
|
+
return CreateResult(
|
|
39
|
+
finish_reason="function_calls",
|
|
40
|
+
content=[FunctionCall(id="c1", name=name, arguments=json.dumps(args))],
|
|
41
|
+
usage=RequestUsage(prompt_tokens=0, completion_tokens=0),
|
|
42
|
+
cached=False,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Unit: a real Governor on a scripted async client
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
def test_wrapped_tool_keeps_name_description_and_schema():
|
|
51
|
+
inner = _refund_tool([])
|
|
52
|
+
wrapped = governed_tool(inner, agent_id="a", governor=_gov(FakeAsyncClient()))
|
|
53
|
+
|
|
54
|
+
assert wrapped.name == "refund"
|
|
55
|
+
assert wrapped.description == inner.description
|
|
56
|
+
assert wrapped.schema == inner.schema
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_allowed_call_is_pre_checked_runs_and_is_post_checked():
|
|
60
|
+
calls, client = [], FakeAsyncClient()
|
|
61
|
+
wrapped = governed_tool(_refund_tool(calls), agent_id="a", governor=_gov(client))
|
|
62
|
+
|
|
63
|
+
result = asyncio.run(wrapped.run_json({"amount": 5, "reason": "late"}, TOKEN))
|
|
64
|
+
|
|
65
|
+
assert result == "refunded 5.0" and calls == [5.0]
|
|
66
|
+
[pre] = client.pre_calls
|
|
67
|
+
assert (pre["action"], pre["params"]) == ("refund", {"amount": 5.0, "reason": "late"})
|
|
68
|
+
assert len(client.post_calls) == 1
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_blocked_call_does_not_run_and_returns_the_refusal_text():
|
|
72
|
+
calls = []
|
|
73
|
+
client = FakeAsyncClient(pre=[_resp("block", reason="over the limit")])
|
|
74
|
+
wrapped = governed_tool(_refund_tool(calls), agent_id="a", governor=_gov(client))
|
|
75
|
+
|
|
76
|
+
result = asyncio.run(wrapped.run_json({"amount": 9999}, TOKEN))
|
|
77
|
+
|
|
78
|
+
assert calls == [] and "over the limit" in result
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_plain_function_is_accepted_and_keeps_its_docstring_as_description():
|
|
82
|
+
calls = []
|
|
83
|
+
|
|
84
|
+
async def lookup(order_id: str) -> str:
|
|
85
|
+
"""Look up an order."""
|
|
86
|
+
calls.append(order_id)
|
|
87
|
+
return f"order {order_id}"
|
|
88
|
+
|
|
89
|
+
[wrapped] = governed_tools([lookup], agent_id="a", governor=_gov(FakeAsyncClient()))
|
|
90
|
+
|
|
91
|
+
assert wrapped.description == "Look up an order."
|
|
92
|
+
assert asyncio.run(wrapped.run_json({"order_id": "42"}, TOKEN)) == "order 42"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_sync_function_tool_runs_without_blocking_the_loop():
|
|
96
|
+
calls = []
|
|
97
|
+
|
|
98
|
+
def ping(host: str) -> str:
|
|
99
|
+
"""Ping a host."""
|
|
100
|
+
calls.append(host)
|
|
101
|
+
return f"pong {host}"
|
|
102
|
+
|
|
103
|
+
[wrapped] = governed_tools([ping], agent_id="a", governor=_gov(FakeAsyncClient()))
|
|
104
|
+
|
|
105
|
+
assert asyncio.run(wrapped.run_json({"host": "h"}, TOKEN)) == "pong h"
|
|
106
|
+
assert calls == ["h"]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_kill_switch_off_runs_plainly(monkeypatch):
|
|
110
|
+
monkeypatch.setenv("AGENTRUST_ENABLED", "false")
|
|
111
|
+
calls, client = [], FakeAsyncClient()
|
|
112
|
+
wrapped = governed_tool(_refund_tool(calls), agent_id="a", governor=_gov(client))
|
|
113
|
+
|
|
114
|
+
assert asyncio.run(wrapped.run_json({"amount": 1}, TOKEN)) == "refunded 1.0"
|
|
115
|
+
assert client.pre_calls == []
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_gateway_down_in_closed_mode_refuses():
|
|
119
|
+
from agentrust_sdk.client import GatewayUnavailableError
|
|
120
|
+
|
|
121
|
+
calls = []
|
|
122
|
+
client = FakeAsyncClient(pre_error=GatewayUnavailableError("down"))
|
|
123
|
+
wrapped = governed_tool(_refund_tool(calls), agent_id="a", governor=_gov(client))
|
|
124
|
+
|
|
125
|
+
assert "gateway unavailable" in asyncio.run(wrapped.run_json({"amount": 1}, TOKEN))
|
|
126
|
+
assert calls == []
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
# Real agent on an embedded gateway
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
def _ledger(gw):
|
|
134
|
+
rows = httpx.get(f"{gw.url}/v1/audit/executions", params={"limit": 100},
|
|
135
|
+
headers={"Authorization": f"Bearer {gw._token}"}).json()["items"]
|
|
136
|
+
return sorted(rows, key=lambda r: r["id"])
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _agent(tool, *replies):
|
|
140
|
+
model = ReplayChatCompletionClient(list(replies), model_info={
|
|
141
|
+
"vision": False, "function_calling": True, "json_output": False,
|
|
142
|
+
"family": "unknown", "structured_output": False,
|
|
143
|
+
})
|
|
144
|
+
return AssistantAgent("assistant", model_client=model, tools=[tool], reflect_on_tool_use=True)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_agent_runs_a_declared_tool_with_pre_and_post_check(embedded_gw):
|
|
148
|
+
gw = embedded_gw(manifests={AGENT: {"declared_tools": [{"name": "refund"}]}})
|
|
149
|
+
calls = []
|
|
150
|
+
agent = _agent(governed_tool(_refund_tool(calls), agent_id=AGENT),
|
|
151
|
+
_call("refund", {"amount": 5}), "All done.")
|
|
152
|
+
|
|
153
|
+
asyncio.run(agent.run(task="refund 5"))
|
|
154
|
+
|
|
155
|
+
assert calls == [5.0]
|
|
156
|
+
assert [(r["phase"], r["decision"]) for r in _ledger(gw)] == [
|
|
157
|
+
("pre_check", "approve"), ("post_check", "approve"),
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_agent_gets_a_refusal_for_an_undeclared_tool_and_still_finishes(embedded_gw):
|
|
162
|
+
gw = embedded_gw(manifests={AGENT: {"declared_tools": [{"name": "calculator"}]}})
|
|
163
|
+
calls = []
|
|
164
|
+
agent = _agent(governed_tool(_refund_tool(calls), agent_id=AGENT),
|
|
165
|
+
_call("refund", {"amount": 5}), "Could not refund.")
|
|
166
|
+
|
|
167
|
+
result = asyncio.run(agent.run(task="refund 5"))
|
|
168
|
+
|
|
169
|
+
assert calls == []
|
|
170
|
+
assert "Could not refund." in result.messages[-1].content
|
|
171
|
+
first = _ledger(gw)[0]
|
|
172
|
+
assert (first["phase"], first["decision"]) == ("pre_check", "block")
|
|
173
|
+
assert any("declared_tools" in str(m.content) for m in result.messages)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
# attach_v4: async validate, fails closed
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
class _AsyncClient:
|
|
181
|
+
outcome = "approve"
|
|
182
|
+
error: Exception | None = None
|
|
183
|
+
calls: list[dict] = []
|
|
184
|
+
|
|
185
|
+
def __init__(self, *a, **kw):
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
async def __aenter__(self):
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
async def __aexit__(self, *a):
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
async def validate(self, **kw):
|
|
195
|
+
type(self).calls.append(kw)
|
|
196
|
+
if type(self).error:
|
|
197
|
+
raise type(self).error
|
|
198
|
+
return _resp(type(self).outcome, reason="not allowed")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@pytest.fixture
|
|
202
|
+
def aclient(monkeypatch):
|
|
203
|
+
_AsyncClient.outcome, _AsyncClient.error, _AsyncClient.calls = "approve", None, []
|
|
204
|
+
monkeypatch.setattr("agentrust_sdk.client.AsyncAgentTrustClient", _AsyncClient)
|
|
205
|
+
return _AsyncClient
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _plain_agent(*replies):
|
|
209
|
+
model = ReplayChatCompletionClient(list(replies))
|
|
210
|
+
return AssistantAgent("assistant", model_client=model)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_attach_v4_validates_the_reply_with_the_async_client(aclient):
|
|
214
|
+
agent = _plain_agent("hello there")
|
|
215
|
+
AgentTrustReplyHook(agent_id="a").attach_v4(agent)
|
|
216
|
+
|
|
217
|
+
asyncio.run(agent.run(task="hi"))
|
|
218
|
+
|
|
219
|
+
[call] = aclient.calls
|
|
220
|
+
assert call["output"]["content"] == "hello there"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_attach_v4_raises_when_the_reply_is_blocked(aclient):
|
|
224
|
+
from agentrust_sdk.decorator import BlockedError
|
|
225
|
+
|
|
226
|
+
aclient.outcome = "block"
|
|
227
|
+
agent = _plain_agent("leak it")
|
|
228
|
+
AgentTrustReplyHook(agent_id="a").attach_v4(agent)
|
|
229
|
+
|
|
230
|
+
with pytest.raises(BlockedError):
|
|
231
|
+
asyncio.run(agent.run(task="hi"))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_attach_v4_fails_closed_when_the_gateway_is_down(aclient, monkeypatch):
|
|
235
|
+
from agentrust_sdk.client import GatewayUnavailableError
|
|
236
|
+
|
|
237
|
+
monkeypatch.setenv("AGENTRUST_FAILURE_MODE", "closed")
|
|
238
|
+
aclient.error = GatewayUnavailableError("down")
|
|
239
|
+
agent = _plain_agent("hello")
|
|
240
|
+
AgentTrustReplyHook(agent_id="a").attach_v4(agent)
|
|
241
|
+
|
|
242
|
+
with pytest.raises(GatewayUnavailableError):
|
|
243
|
+
asyncio.run(agent.run(task="hi"))
|
|
@@ -152,7 +152,7 @@ class TestAnswerFields:
|
|
|
152
152
|
# Release hygiene: one version everywhere, and the right install name
|
|
153
153
|
# ---------------------------------------------------------------------------
|
|
154
154
|
REPO = Path(__file__).resolve().parents[2]
|
|
155
|
-
RELEASE = "0.0.
|
|
155
|
+
RELEASE = "0.0.8"
|
|
156
156
|
|
|
157
157
|
|
|
158
158
|
class TestReleaseHygiene:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentrust_py-0.0.7 → agentrust_py-0.0.8}/tests/adapters/test_langgraph_quickstart_example.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|