agentrust-py 0.0.6__tar.gz → 0.0.7__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.6 → agentrust_py-0.0.7}/PKG-INFO +3 -1
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/__init__.py +9 -2
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/adapters/autogen.py +129 -41
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/config.py +1 -1
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/decorator.py +12 -1
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/manifest.py +1 -1
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/pyproject.toml +3 -1
- agentrust_py-0.0.7/tests/adapters/_ag2.py +57 -0
- agentrust_py-0.0.7/tests/adapters/test_ag2_governed_function.py +232 -0
- agentrust_py-0.0.7/tests/adapters/test_ag2_reply_hook.py +157 -0
- agentrust_py-0.0.7/tests/adapters/test_ag2_research_team_example.py +122 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_callback_url.py +1 -1
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/.gitignore +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/CHANGELOG.md +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/LICENSE +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/README.md +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/_evaluations.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/_real_validator.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/adapters/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/adapters/_core.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/adapters/claude_agents.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/adapters/crewai.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/adapters/langgraph.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/adapters/mcp.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/adapters/openai_agents.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/audit/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/audit/logger.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/auth.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/auto.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/autoload.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/classifier/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/classifier/engine.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/classifier/profiles.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/classifier/system_prompt.md +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/cli.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/cli_certify.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/client.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/embedded.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/golden_suite.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/governance/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/governance/hitl.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/governance/kernel.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/governance/kill_switch.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/governance/modes.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/hooks.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/models.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/notifications/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/notifications/discord.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/notifications/dispatcher.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/notifications/slack.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/policy/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/policy/backends/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/policy/backends/native.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/policy/backends/opa.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/policy/engine.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/policy/loader.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/py.typed +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/queue_replay.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/tiers.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/tool_tracking.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/version_negotiation.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/agentrust_sdk/webhooks.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/policy/base.yaml +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/policy/default-policy.json +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/policy/financial.yaml +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/policy/medical.yaml +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/policy/profiles/advisory.json +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/policy/profiles/balanced.json +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/policy/profiles/strict.json +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/_cfg.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/_crews.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/_graphs.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_crewai_baseline.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_crewai_failure_modes.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_crewai_governed_tools.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_crewai_holds_and_chain.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_crewai_quickstart_example.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_crewai_task_checks.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_langgraph_baseline.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_langgraph_failure_modes.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_langgraph_governed_tools.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_langgraph_holds_and_chain.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_langgraph_node.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/adapters/test_langgraph_quickstart_example.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/conformance/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/conformance/bindings.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/conformance/test_c1_c10.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/conftest.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_adapter_tier_gate.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_adapters.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_async_advanced.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_chaos.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_cli_certify.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_client.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_client_adapter_contract.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_config.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_decorator.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_embedded.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_embedded_adapter_contract.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_golden_suite.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_governor.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_hooks.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_manifest.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/tests/test_policy_packaging.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.7}/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.7
|
|
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
|
|
@@ -23,6 +23,8 @@ Requires-Python: >=3.10
|
|
|
23
23
|
Requires-Dist: httpx<1.0.0,>=0.27.0
|
|
24
24
|
Requires-Dist: pydantic<3.0.0,>=2.0.0
|
|
25
25
|
Requires-Dist: pyyaml<7.0,>=6.0.3
|
|
26
|
+
Provides-Extra: ag2
|
|
27
|
+
Requires-Dist: ag2<1.0,>=0.9; extra == 'ag2'
|
|
26
28
|
Provides-Extra: autoload
|
|
27
29
|
Provides-Extra: crewai
|
|
28
30
|
Requires-Dist: crewai<2.0,>=1.0; extra == 'crewai'
|
|
@@ -38,7 +38,12 @@ from .client import (
|
|
|
38
38
|
from .config import SDK_CONFIG
|
|
39
39
|
from .hooks import auto_instrument, auto_wrap
|
|
40
40
|
from .adapters.mcp import mcp_tool_guard, MCPServerGuard
|
|
41
|
-
from .adapters.autogen import
|
|
41
|
+
from .adapters.autogen import (
|
|
42
|
+
AgentTrustGroupChatMonitor,
|
|
43
|
+
AgentTrustReplyHook,
|
|
44
|
+
governed_function,
|
|
45
|
+
governed_functions,
|
|
46
|
+
)
|
|
42
47
|
from .adapters.claude_agents import ClaudeAgentGuard
|
|
43
48
|
from .adapters.openai_agents import agent_guard as openai_agent_guard, OpenAIAgentsGuard, SwarmGuard
|
|
44
49
|
from .adapters.crewai import AgentTrustCallback
|
|
@@ -87,6 +92,8 @@ from .webhooks import (
|
|
|
87
92
|
__all__ = [
|
|
88
93
|
# Primary API
|
|
89
94
|
"harness",
|
|
95
|
+
"governed_function",
|
|
96
|
+
"governed_functions",
|
|
90
97
|
# Embedded gateway (zero-dep plug-and-play)
|
|
91
98
|
"embed_gateway",
|
|
92
99
|
"EmbeddedGateway",
|
|
@@ -147,4 +154,4 @@ __all__ = [
|
|
|
147
154
|
"validate",
|
|
148
155
|
]
|
|
149
156
|
|
|
150
|
-
__version__ = "0.0.
|
|
157
|
+
__version__ = "0.0.7"
|
|
@@ -5,6 +5,11 @@ Architecture doc (two-pager): "Connects to … AutoGen agents."
|
|
|
5
5
|
|
|
6
6
|
Supports both AutoGen v0.2 (pyautogen) and AutoGen v0.4 (autogen-agentchat):
|
|
7
7
|
|
|
8
|
+
Pattern 0 — governed_function (AG2 / pyautogen 0.2 tools):
|
|
9
|
+
Wrap each tool before register_function so every call is pre-checked,
|
|
10
|
+
held for approval when needed, run, post-checked and audited. A refusal
|
|
11
|
+
is returned as the tool result, so the chat keeps going.
|
|
12
|
+
|
|
8
13
|
Pattern 1 — AgentTrustReplyHook (v0.2 + v0.4):
|
|
9
14
|
Attach to any ConversableAgent / AssistantAgent to validate every reply
|
|
10
15
|
before it is sent.
|
|
@@ -25,19 +30,111 @@ Requires Team tier.
|
|
|
25
30
|
"""
|
|
26
31
|
from __future__ import annotations
|
|
27
32
|
|
|
33
|
+
import functools
|
|
34
|
+
import inspect
|
|
28
35
|
import logging
|
|
29
36
|
import time
|
|
30
|
-
from typing import Any, Callable
|
|
37
|
+
from typing import Any, Callable, Sequence
|
|
38
|
+
|
|
39
|
+
from ..tiers import Capability
|
|
40
|
+
from ._core import Allowed, Governor, OnBlock, Refused, check_adapter_tier
|
|
31
41
|
|
|
32
42
|
logger = logging.getLogger(__name__)
|
|
33
43
|
|
|
44
|
+
_FRAMEWORK = "ag2"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def governed_function(
|
|
48
|
+
fn: Callable[..., Any],
|
|
49
|
+
*,
|
|
50
|
+
agent_id: str,
|
|
51
|
+
name: str | None = None,
|
|
52
|
+
on_block: OnBlock = "refuse",
|
|
53
|
+
review_timeout_s: float = 300.0,
|
|
54
|
+
user: str = "ag2",
|
|
55
|
+
base_url: str | None = None,
|
|
56
|
+
api_key: str | None = None,
|
|
57
|
+
governor: Governor | None = None,
|
|
58
|
+
) -> Callable[..., Any]:
|
|
59
|
+
"""Wrap an AG2 tool function so AgentTrust governs every call.
|
|
60
|
+
|
|
61
|
+
The result keeps the name, docstring and signature of *fn*, so AG2 builds
|
|
62
|
+
the same tool schema from it. Pass it to ``register_function`` or
|
|
63
|
+
``register_for_llm``. Async functions stay async.
|
|
64
|
+
"""
|
|
65
|
+
check_adapter_tier(Capability.AUTOGEN_ADAPTER, api_key)
|
|
66
|
+
gov = governor or Governor(
|
|
67
|
+
agent_id,
|
|
68
|
+
framework=_FRAMEWORK,
|
|
69
|
+
on_block=on_block,
|
|
70
|
+
review_timeout_s=review_timeout_s,
|
|
71
|
+
user=user,
|
|
72
|
+
base_url=base_url,
|
|
73
|
+
api_key=api_key,
|
|
74
|
+
)
|
|
75
|
+
tool_name = name or fn.__name__
|
|
76
|
+
signature = inspect.signature(fn)
|
|
77
|
+
|
|
78
|
+
def call_args(args: tuple, kwargs: dict) -> dict[str, Any]:
|
|
79
|
+
bound = signature.bind(*args, **kwargs)
|
|
80
|
+
bound.apply_defaults()
|
|
81
|
+
return dict(bound.arguments)
|
|
82
|
+
|
|
83
|
+
def unwrap(outcome: Allowed | Refused) -> Any:
|
|
84
|
+
# The refusal text is the tool result the agent reads next.
|
|
85
|
+
return outcome.message if isinstance(outcome, Refused) else outcome.result
|
|
86
|
+
|
|
87
|
+
if inspect.iscoroutinefunction(fn):
|
|
88
|
+
|
|
89
|
+
@functools.wraps(fn)
|
|
90
|
+
async def governed(*args: Any, **kwargs: Any) -> Any:
|
|
91
|
+
params = call_args(args, kwargs)
|
|
92
|
+
outcome = await gov.aguard_tool(
|
|
93
|
+
tool_name, params, lambda: fn(*args, **kwargs), input=_describe_call(tool_name, params),
|
|
94
|
+
)
|
|
95
|
+
return unwrap(outcome)
|
|
96
|
+
|
|
97
|
+
else:
|
|
34
98
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
99
|
+
@functools.wraps(fn)
|
|
100
|
+
def governed(*args: Any, **kwargs: Any) -> Any:
|
|
101
|
+
params = call_args(args, kwargs)
|
|
102
|
+
outcome = gov.guard_tool(
|
|
103
|
+
tool_name, params, lambda: fn(*args, **kwargs), input=_describe_call(tool_name, params),
|
|
104
|
+
)
|
|
105
|
+
return unwrap(outcome)
|
|
106
|
+
|
|
107
|
+
return governed
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def governed_functions(
|
|
111
|
+
fns: Sequence[Callable[..., Any]],
|
|
112
|
+
*,
|
|
113
|
+
agent_id: str,
|
|
114
|
+
on_block: OnBlock = "refuse",
|
|
115
|
+
review_timeout_s: float = 300.0,
|
|
116
|
+
user: str = "ag2",
|
|
117
|
+
base_url: str | None = None,
|
|
118
|
+
api_key: str | None = None,
|
|
119
|
+
governor: Governor | None = None,
|
|
120
|
+
) -> list[Callable[..., Any]]:
|
|
121
|
+
"""Wrap each function with :func:`governed_function`, sharing one Governor."""
|
|
122
|
+
check_adapter_tier(Capability.AUTOGEN_ADAPTER, api_key)
|
|
123
|
+
gov = governor or Governor(
|
|
124
|
+
agent_id,
|
|
125
|
+
framework=_FRAMEWORK,
|
|
126
|
+
on_block=on_block,
|
|
127
|
+
review_timeout_s=review_timeout_s,
|
|
128
|
+
user=user,
|
|
129
|
+
base_url=base_url,
|
|
130
|
+
api_key=api_key,
|
|
131
|
+
)
|
|
132
|
+
return [governed_function(fn, agent_id=agent_id, governor=gov) for fn in fns]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _describe_call(name: str, args: dict[str, Any]) -> str:
|
|
136
|
+
"""The proposed call as the pre-check's request input."""
|
|
137
|
+
return f"tool call {name}({', '.join(f'{k}={v!r}' for k, v in args.items())})"
|
|
41
138
|
|
|
42
139
|
|
|
43
140
|
def _validate_sync(
|
|
@@ -50,7 +147,8 @@ def _validate_sync(
|
|
|
50
147
|
framework: str,
|
|
51
148
|
block_on_block: bool,
|
|
52
149
|
) -> None:
|
|
53
|
-
from agentrust_sdk.client import AgentTrustClient
|
|
150
|
+
from agentrust_sdk.client import AgentTrustClient, GatewayUnavailableError
|
|
151
|
+
from agentrust_sdk.config import SDK_CONFIG
|
|
54
152
|
from agentrust_sdk.decorator import _check_decision, BlockedError
|
|
55
153
|
|
|
56
154
|
try:
|
|
@@ -63,9 +161,11 @@ def _validate_sync(
|
|
|
63
161
|
framework=framework,
|
|
64
162
|
)
|
|
65
163
|
_check_decision(resp, block_on_block, False)
|
|
66
|
-
except BlockedError:
|
|
164
|
+
except (BlockedError, GatewayUnavailableError):
|
|
67
165
|
raise
|
|
68
166
|
except Exception as exc:
|
|
167
|
+
if SDK_CONFIG.failure_mode == "closed":
|
|
168
|
+
raise
|
|
69
169
|
logger.warning("[AgentTrust] AutoGen validate failed (non-fatal): %s", exc)
|
|
70
170
|
|
|
71
171
|
|
|
@@ -99,7 +199,7 @@ class AgentTrustReplyHook:
|
|
|
99
199
|
block_on_block: bool = True,
|
|
100
200
|
framework: str = "AutoGen",
|
|
101
201
|
) -> None:
|
|
102
|
-
|
|
202
|
+
check_adapter_tier(Capability.AUTOGEN_ADAPTER, api_key)
|
|
103
203
|
self._agent_id = agent_id
|
|
104
204
|
self._base_url = base_url
|
|
105
205
|
self._api_key = api_key
|
|
@@ -111,47 +211,35 @@ class AgentTrustReplyHook:
|
|
|
111
211
|
# AutoGen v0.2
|
|
112
212
|
# ------------------------------------------------------------------
|
|
113
213
|
|
|
114
|
-
def
|
|
115
|
-
self,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
214
|
+
def _check_outgoing(
|
|
215
|
+
self, sender: Any = None, message: Any = None, recipient: Any = None, silent: bool = False,
|
|
216
|
+
) -> Any:
|
|
217
|
+
"""AG2 ``process_message_before_send`` hook: check the agent's own reply."""
|
|
218
|
+
content = message.get("content") if isinstance(message, dict) else message
|
|
219
|
+
if not isinstance(content, str) or not content:
|
|
220
|
+
return message
|
|
131
221
|
|
|
132
222
|
t0 = time.perf_counter()
|
|
133
|
-
output = {"content": content[:4000], "role": role}
|
|
134
223
|
_validate_sync(
|
|
135
|
-
self._agent_id, self._base_url, self._api_key,
|
|
136
|
-
|
|
224
|
+
self._agent_id, self._base_url, self._api_key, self._user,
|
|
225
|
+
content[:500], {"content": content[:4000], "role": "assistant"},
|
|
226
|
+
self._framework, self._block_on_block,
|
|
137
227
|
)
|
|
138
228
|
logger.debug(
|
|
139
229
|
"[AgentTrust] AutoGen reply validated in %.1f ms", (time.perf_counter() - t0) * 1000
|
|
140
230
|
)
|
|
141
|
-
return
|
|
231
|
+
return message
|
|
142
232
|
|
|
143
233
|
def attach(self, agent: Any) -> None:
|
|
144
|
-
"""Attach to an AutoGen v0.2 ConversableAgent
|
|
145
|
-
|
|
234
|
+
"""Attach to an AG2 / AutoGen v0.2 ConversableAgent.
|
|
235
|
+
|
|
236
|
+
Checks every message the agent sends, before it leaves the agent.
|
|
237
|
+
"""
|
|
238
|
+
if not hasattr(agent, "register_hook"):
|
|
146
239
|
raise TypeError(
|
|
147
|
-
f"Expected a ConversableAgent with
|
|
240
|
+
f"Expected a ConversableAgent with register_hook(), got {type(agent).__name__}"
|
|
148
241
|
)
|
|
149
|
-
agent.
|
|
150
|
-
trigger=[],
|
|
151
|
-
reply_func=self._reply_func,
|
|
152
|
-
position=0,
|
|
153
|
-
name="agentrust_reply_hook",
|
|
154
|
-
)
|
|
242
|
+
agent.register_hook("process_message_before_send", self._check_outgoing)
|
|
155
243
|
logger.info(
|
|
156
244
|
"[AgentTrust] AutoGen reply hook attached to agent '%s'", getattr(agent, "name", "?")
|
|
157
245
|
)
|
|
@@ -209,7 +297,7 @@ class AgentTrustGroupChatMonitor:
|
|
|
209
297
|
block_on_block: bool = False, # Default False — group chats tolerate review
|
|
210
298
|
framework: str = "AutoGen",
|
|
211
299
|
) -> None:
|
|
212
|
-
|
|
300
|
+
check_adapter_tier(Capability.AUTOGEN_ADAPTER, api_key)
|
|
213
301
|
self._agent_id = agent_id
|
|
214
302
|
self._base_url = base_url
|
|
215
303
|
self._api_key = api_key
|
|
@@ -329,6 +329,17 @@ def _check_decision(resp: Any, block_on_block: bool, block_on_review: bool) -> N
|
|
|
329
329
|
raise BlockedError(outcome, resp.decision.reason, resp.envelope_id)
|
|
330
330
|
|
|
331
331
|
|
|
332
|
+
def _ag2_installed() -> bool:
|
|
333
|
+
"""AG2 installs the ``autogen`` module under the distribution name ``ag2``."""
|
|
334
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
version("ag2")
|
|
338
|
+
except PackageNotFoundError:
|
|
339
|
+
return False
|
|
340
|
+
return True
|
|
341
|
+
|
|
342
|
+
|
|
332
343
|
def _detect_framework() -> str:
|
|
333
344
|
"""Best-effort framework detection from installed packages."""
|
|
334
345
|
try:
|
|
@@ -341,7 +352,7 @@ def _detect_framework() -> str:
|
|
|
341
352
|
("llama_index", "LlamaIndex"),
|
|
342
353
|
]:
|
|
343
354
|
if importlib.util.find_spec(name):
|
|
344
|
-
return label
|
|
355
|
+
return "AG2" if name == "autogen" and _ag2_installed() else label
|
|
345
356
|
except Exception:
|
|
346
357
|
pass
|
|
347
358
|
return "REST"
|
|
@@ -50,7 +50,7 @@ class CertifyManifest(BaseModel):
|
|
|
50
50
|
display_name: str = ""
|
|
51
51
|
framework: str = Field(
|
|
52
52
|
default="rest",
|
|
53
|
-
pattern=r"^(langchain|langgraph|crewai|autogen|openai-agents|mcp|rest)$",
|
|
53
|
+
pattern=r"^(langchain|langgraph|crewai|autogen|ag2|openai-agents|mcp|rest)$",
|
|
54
54
|
)
|
|
55
55
|
language: str = Field(default="python", pattern=r"^(python|typescript|rest)$")
|
|
56
56
|
model: ModelDeclaration = Field(default_factory=ModelDeclaration)
|
|
@@ -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.7"
|
|
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" }
|
|
@@ -41,6 +41,8 @@ llm = [
|
|
|
41
41
|
# Framework adapters (optional, tier-gated)
|
|
42
42
|
langgraph = ["langgraph>=0.2.0,<2.0.0"]
|
|
43
43
|
crewai = ["crewai>=1.0,<2.0"]
|
|
44
|
+
# AG2 0.x keeps the ConversableAgent API (module `autogen`); 1.x is a new API
|
|
45
|
+
ag2 = ["ag2>=0.9,<1.0"]
|
|
44
46
|
openai = ["openai>=2.41.1,<3.0.0"]
|
|
45
47
|
langchain = ["langchain>=0.2.0,<1.0.0"]
|
|
46
48
|
# Retry/backoff (recommended for production)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""A scripted chat model for real AG2 agents, shared by the AG2 binding tests."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from types import SimpleNamespace as NS
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def tool_call(name: str, args: dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
|
10
|
+
"""The reply in which the agent calls a tool."""
|
|
11
|
+
return name, args
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def final(text: str = "done DONE") -> str:
|
|
15
|
+
return text
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ScriptedModel:
|
|
19
|
+
"""Stands in for ``OpenAIWrapper.create``: replays *replies* in order."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, *replies):
|
|
22
|
+
self.replies = list(replies)
|
|
23
|
+
self.prompts: list[str] = []
|
|
24
|
+
|
|
25
|
+
def create(self, wrapper, **params):
|
|
26
|
+
self.prompts.append(json.dumps(params.get("messages", []), default=str))
|
|
27
|
+
reply = self.replies.pop(0)
|
|
28
|
+
if isinstance(reply, str):
|
|
29
|
+
message = {"role": "assistant", "content": reply}
|
|
30
|
+
msg = NS(content=reply, tool_calls=None, function_call=None, role="assistant")
|
|
31
|
+
else:
|
|
32
|
+
name, args = reply
|
|
33
|
+
call = {"id": "call-1", "type": "function",
|
|
34
|
+
"function": {"name": name, "arguments": json.dumps(args)}}
|
|
35
|
+
message = {"role": "assistant", "content": None, "tool_calls": [call]}
|
|
36
|
+
msg = NS(content=None, tool_calls=[call], function_call=None, role="assistant")
|
|
37
|
+
msg.model_dump = lambda: message
|
|
38
|
+
return NS(
|
|
39
|
+
choices=[NS(message=msg, finish_reason="stop")], model="scripted", cost=0, id="r",
|
|
40
|
+
usage=NS(prompt_tokens=0, completion_tokens=0, total_tokens=0),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def extract(response):
|
|
45
|
+
msg = response.choices[0].message
|
|
46
|
+
return [msg.model_dump()] if msg.tool_calls else [msg.content]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def install(monkeypatch, *replies) -> ScriptedModel:
|
|
50
|
+
from autogen.oai.client import OpenAIWrapper
|
|
51
|
+
|
|
52
|
+
model = ScriptedModel(*replies)
|
|
53
|
+
monkeypatch.setattr(OpenAIWrapper, "create", lambda self, **kw: model.create(self, **kw))
|
|
54
|
+
monkeypatch.setattr(
|
|
55
|
+
OpenAIWrapper, "extract_text_or_completion_object", classmethod(lambda cls, r: model.extract(r)),
|
|
56
|
+
)
|
|
57
|
+
return model
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""governed_function: every AG2 tool call is pre-checked before it runs."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import inspect
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
pytest.importorskip("autogen")
|
|
12
|
+
|
|
13
|
+
from autogen import ConversableAgent, register_function
|
|
14
|
+
|
|
15
|
+
from agentrust_sdk.adapters.autogen import governed_function, governed_functions
|
|
16
|
+
from tests.test_governor import FakeAsyncClient, FakeClient, _gov, _resp
|
|
17
|
+
|
|
18
|
+
AGENT = "ag2-agent"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _refund(calls):
|
|
22
|
+
def refund(amount: float, reason: str = "") -> str:
|
|
23
|
+
"""Refund an amount to the customer."""
|
|
24
|
+
calls.append(amount)
|
|
25
|
+
return f"refunded {amount}"
|
|
26
|
+
|
|
27
|
+
return refund
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Unit: a real Governor on a scripted client
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
def test_wrapper_keeps_name_docstring_and_signature():
|
|
35
|
+
inner = _refund([])
|
|
36
|
+
wrapped = governed_function(inner, agent_id="a", governor=_gov(FakeClient()))
|
|
37
|
+
|
|
38
|
+
assert wrapped.__name__ == "refund"
|
|
39
|
+
assert wrapped.__doc__ == inner.__doc__
|
|
40
|
+
assert inspect.signature(wrapped) == inspect.signature(inner)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_allowed_call_is_pre_checked_runs_and_is_post_checked():
|
|
44
|
+
calls, client = [], FakeClient()
|
|
45
|
+
wrapped = governed_function(_refund(calls), agent_id="a", governor=_gov(client))
|
|
46
|
+
|
|
47
|
+
assert wrapped(5, reason="late") == "refunded 5"
|
|
48
|
+
|
|
49
|
+
assert calls == [5]
|
|
50
|
+
[pre] = client.pre_calls
|
|
51
|
+
assert (pre["action"], pre["params"]) == ("refund", {"amount": 5, "reason": "late"})
|
|
52
|
+
assert pre["input"] == "tool call refund(amount=5, reason='late')"
|
|
53
|
+
assert len(client.post_calls) == 1
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_defaults_are_part_of_the_checked_arguments():
|
|
57
|
+
client = FakeClient()
|
|
58
|
+
governed_function(_refund([]), agent_id="a", governor=_gov(client))(amount=1.5)
|
|
59
|
+
|
|
60
|
+
assert client.pre_calls[0]["params"] == {"amount": 1.5, "reason": ""}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_blocked_call_does_not_run_and_returns_the_refusal_text():
|
|
64
|
+
calls = []
|
|
65
|
+
client = FakeClient(pre=[_resp("block", reason="over the limit")])
|
|
66
|
+
wrapped = governed_function(_refund(calls), agent_id="a", governor=_gov(client))
|
|
67
|
+
|
|
68
|
+
result = wrapped(9999)
|
|
69
|
+
|
|
70
|
+
assert calls == []
|
|
71
|
+
assert "over the limit" in result
|
|
72
|
+
assert client.post_calls == []
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_held_call_runs_once_a_reviewer_approves():
|
|
76
|
+
calls = []
|
|
77
|
+
client = FakeClient(
|
|
78
|
+
pre=[_resp("escalate", review_id="r1")], resolutions=["pending", "approved"],
|
|
79
|
+
)
|
|
80
|
+
wrapped = governed_function(_refund(calls), agent_id="a", governor=_gov(client))
|
|
81
|
+
|
|
82
|
+
assert wrapped(5) == "refunded 5"
|
|
83
|
+
assert calls == [5]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_held_call_is_refused_when_the_review_times_out():
|
|
87
|
+
calls = []
|
|
88
|
+
client = FakeClient(pre=[_resp("escalate", review_id="r1")])
|
|
89
|
+
wrapped = governed_function(
|
|
90
|
+
_refund(calls), agent_id="a", governor=_gov(client, review_timeout_s=10),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
result = wrapped(5)
|
|
94
|
+
|
|
95
|
+
assert calls == []
|
|
96
|
+
assert isinstance(result, str) and result
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_async_function_stays_async_and_is_governed():
|
|
100
|
+
calls, client = [], FakeAsyncClient()
|
|
101
|
+
|
|
102
|
+
async def lookup(order_id: str) -> str:
|
|
103
|
+
"""Look up an order."""
|
|
104
|
+
calls.append(order_id)
|
|
105
|
+
return f"order {order_id}"
|
|
106
|
+
|
|
107
|
+
wrapped = governed_function(lookup, agent_id="a", governor=_gov(client))
|
|
108
|
+
|
|
109
|
+
assert inspect.iscoroutinefunction(wrapped)
|
|
110
|
+
assert asyncio.run(wrapped("42")) == "order 42"
|
|
111
|
+
assert calls == ["42"] and len(client.pre_calls) == 1
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_blocked_async_call_does_not_run():
|
|
115
|
+
calls = []
|
|
116
|
+
|
|
117
|
+
async def lookup(order_id: str) -> str:
|
|
118
|
+
"""Look up an order."""
|
|
119
|
+
calls.append(order_id)
|
|
120
|
+
return "x"
|
|
121
|
+
|
|
122
|
+
client = FakeAsyncClient(pre=[_resp("block", reason="nope")])
|
|
123
|
+
wrapped = governed_function(lookup, agent_id="a", governor=_gov(client))
|
|
124
|
+
|
|
125
|
+
assert "nope" in asyncio.run(wrapped("42"))
|
|
126
|
+
assert calls == []
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_function_without_arguments():
|
|
130
|
+
client = FakeClient()
|
|
131
|
+
|
|
132
|
+
def ping() -> str:
|
|
133
|
+
"""Ping."""
|
|
134
|
+
return "pong"
|
|
135
|
+
|
|
136
|
+
assert governed_function(ping, agent_id="a", governor=_gov(client))() == "pong"
|
|
137
|
+
assert client.pre_calls[0]["params"] == {}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_exception_from_the_tool_propagates_and_skips_the_post_check():
|
|
141
|
+
client = FakeClient()
|
|
142
|
+
|
|
143
|
+
def boom() -> str:
|
|
144
|
+
"""Fail."""
|
|
145
|
+
raise ValueError("bad")
|
|
146
|
+
|
|
147
|
+
with pytest.raises(ValueError):
|
|
148
|
+
governed_function(boom, agent_id="a", governor=_gov(client))()
|
|
149
|
+
assert client.post_calls == []
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_kill_switch_off_runs_the_function_plainly(monkeypatch):
|
|
153
|
+
monkeypatch.setenv("AGENTRUST_ENABLED", "false")
|
|
154
|
+
calls, client = [], FakeClient()
|
|
155
|
+
wrapped = governed_function(_refund(calls), agent_id="a", governor=_gov(client))
|
|
156
|
+
|
|
157
|
+
assert wrapped(5) == "refunded 5"
|
|
158
|
+
assert client.pre_calls == []
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_governed_functions_share_one_governor():
|
|
162
|
+
client = FakeClient()
|
|
163
|
+
a, b = governed_functions([_refund([]), _refund([])], agent_id="a", governor=_gov(client))
|
|
164
|
+
|
|
165
|
+
assert a.__name__ == b.__name__ == "refund"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_gateway_down_in_closed_mode_refuses(monkeypatch):
|
|
169
|
+
from agentrust_sdk.client import GatewayUnavailableError
|
|
170
|
+
|
|
171
|
+
calls = []
|
|
172
|
+
client = FakeClient(pre_error=GatewayUnavailableError("down"))
|
|
173
|
+
wrapped = governed_function(_refund(calls), agent_id="a", governor=_gov(client))
|
|
174
|
+
|
|
175
|
+
assert "gateway unavailable" in wrapped(5)
|
|
176
|
+
assert calls == []
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Real AG2: the tool schema is unchanged and registration works
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
def _schema(fn):
|
|
184
|
+
from autogen import LLMConfig
|
|
185
|
+
|
|
186
|
+
cfg = LLMConfig({"model": "m", "api_key": "k", "api_type": "openai"})
|
|
187
|
+
caller = ConversableAgent("caller", llm_config=cfg)
|
|
188
|
+
executor = ConversableAgent("executor", llm_config=False)
|
|
189
|
+
register_function(fn, caller=caller, executor=executor, name=fn.__name__, description="d")
|
|
190
|
+
return caller.llm_config.tools if hasattr(caller.llm_config, "tools") else caller.llm_config["tools"]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_ag2_builds_the_same_tool_schema_from_the_wrapper():
|
|
194
|
+
inner = _refund([])
|
|
195
|
+
wrapped = governed_function(inner, agent_id="a", governor=_gov(FakeClient()))
|
|
196
|
+
|
|
197
|
+
assert _schema(wrapped) == _schema(inner)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
# Embedded gateway: what lands in the ledger
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
def _ledger(gw):
|
|
205
|
+
rows = httpx.get(f"{gw.url}/v1/audit/executions", params={"limit": 100},
|
|
206
|
+
headers={"Authorization": f"Bearer {gw._token}"}).json()["items"]
|
|
207
|
+
return sorted(rows, key=lambda r: r["id"])
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_declared_tool_leaves_pre_and_post_check_rows(embedded_gw):
|
|
211
|
+
gw = embedded_gw(manifests={AGENT: {"declared_tools": [{"name": "refund"}]}})
|
|
212
|
+
calls = []
|
|
213
|
+
|
|
214
|
+
result = governed_function(_refund(calls), agent_id=AGENT)(5)
|
|
215
|
+
|
|
216
|
+
assert result == "refunded 5" and calls == [5]
|
|
217
|
+
assert [(r["phase"], r["decision"]) for r in _ledger(gw)] == [
|
|
218
|
+
("pre_check", "approve"), ("post_check", "approve"),
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_undeclared_tool_is_refused_before_it_runs(embedded_gw):
|
|
223
|
+
gw = embedded_gw(manifests={AGENT: {"declared_tools": [{"name": "calculator"}]}})
|
|
224
|
+
calls = []
|
|
225
|
+
|
|
226
|
+
result = governed_function(_refund(calls), agent_id=AGENT)(5)
|
|
227
|
+
|
|
228
|
+
assert calls == []
|
|
229
|
+
assert "is not in this agent's declared_tools" in result
|
|
230
|
+
first = _ledger(gw)[0]
|
|
231
|
+
assert (first["phase"], first["decision"]) == ("pre_check", "block")
|
|
232
|
+
assert json.loads(first["request_json"])["metadata"]["action"] == "refund"
|