agentrust-py 0.0.6__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.6 → agentrust_py-0.0.8}/PKG-INFO +5 -1
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/__init__.py +11 -2
- agentrust_py-0.0.8/agentrust_sdk/adapters/autogen.py +457 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/config.py +1 -1
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/decorator.py +12 -1
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/manifest.py +1 -1
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/pyproject.toml +5 -1
- agentrust_py-0.0.8/tests/adapters/_ag2.py +57 -0
- agentrust_py-0.0.8/tests/adapters/test_ag2_governed_function.py +232 -0
- agentrust_py-0.0.8/tests/adapters/test_ag2_quickstart_example.py +145 -0
- agentrust_py-0.0.8/tests/adapters/test_ag2_reply_hook.py +157 -0
- agentrust_py-0.0.8/tests/adapters/test_ag2_research_team_example.py +122 -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.6 → agentrust_py-0.0.8}/tests/test_callback_url.py +1 -1
- agentrust_py-0.0.6/agentrust_sdk/adapters/autogen.py +0 -235
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/.gitignore +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/CHANGELOG.md +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/LICENSE +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/README.md +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/_evaluations.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/_real_validator.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/adapters/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/adapters/_core.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/adapters/claude_agents.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/adapters/crewai.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/adapters/langgraph.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/adapters/mcp.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/adapters/openai_agents.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/audit/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/audit/logger.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/auth.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/auto.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/autoload.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/classifier/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/classifier/engine.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/classifier/profiles.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/classifier/system_prompt.md +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/cli.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/cli_certify.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/client.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/embedded.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/golden_suite.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/governance/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/governance/hitl.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/governance/kernel.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/governance/kill_switch.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/governance/modes.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/hooks.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/models.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/notifications/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/notifications/discord.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/notifications/dispatcher.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/notifications/slack.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/policy/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/policy/backends/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/policy/backends/native.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/policy/backends/opa.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/policy/engine.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/policy/loader.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/py.typed +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/queue_replay.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/tiers.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/tool_tracking.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/version_negotiation.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/agentrust_sdk/webhooks.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/policy/base.yaml +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/policy/default-policy.json +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/policy/financial.yaml +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/policy/medical.yaml +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/policy/profiles/advisory.json +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/policy/profiles/balanced.json +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/policy/profiles/strict.json +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/_cfg.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/adapters/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/adapters/_crews.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/adapters/_graphs.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/adapters/test_crewai_baseline.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/adapters/test_crewai_failure_modes.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/adapters/test_crewai_governed_tools.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/adapters/test_crewai_holds_and_chain.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/adapters/test_crewai_quickstart_example.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/adapters/test_crewai_task_checks.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/adapters/test_langgraph_baseline.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/adapters/test_langgraph_failure_modes.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/adapters/test_langgraph_governed_tools.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/adapters/test_langgraph_holds_and_chain.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/adapters/test_langgraph_node.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/adapters/test_langgraph_quickstart_example.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/conformance/__init__.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/conformance/bindings.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/conformance/test_c1_c10.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/conftest.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/test_adapter_tier_gate.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/test_adapters.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/test_async_advanced.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/test_chaos.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/test_cli_certify.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/test_client.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/test_client_adapter_contract.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/test_config.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/test_decorator.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/test_embedded.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/test_embedded_adapter_contract.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/test_golden_suite.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/test_governor.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/test_hooks.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/test_manifest.py +0 -0
- {agentrust_py-0.0.6 → agentrust_py-0.0.8}/tests/test_policy_packaging.py +0 -0
- {agentrust_py-0.0.6 → 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
|
|
@@ -23,6 +23,10 @@ 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'
|
|
28
|
+
Provides-Extra: autogen
|
|
29
|
+
Requires-Dist: autogen-agentchat<1.0,>=0.4; extra == 'autogen'
|
|
26
30
|
Provides-Extra: autoload
|
|
27
31
|
Provides-Extra: crewai
|
|
28
32
|
Requires-Dist: crewai<2.0,>=1.0; extra == 'crewai'
|
|
@@ -38,7 +38,14 @@ 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
|
+
governed_tool as governed_autogen_tool,
|
|
47
|
+
governed_tools as governed_autogen_tools,
|
|
48
|
+
)
|
|
42
49
|
from .adapters.claude_agents import ClaudeAgentGuard
|
|
43
50
|
from .adapters.openai_agents import agent_guard as openai_agent_guard, OpenAIAgentsGuard, SwarmGuard
|
|
44
51
|
from .adapters.crewai import AgentTrustCallback
|
|
@@ -87,6 +94,8 @@ from .webhooks import (
|
|
|
87
94
|
__all__ = [
|
|
88
95
|
# Primary API
|
|
89
96
|
"harness",
|
|
97
|
+
"governed_function",
|
|
98
|
+
"governed_functions",
|
|
90
99
|
# Embedded gateway (zero-dep plug-and-play)
|
|
91
100
|
"embed_gateway",
|
|
92
101
|
"EmbeddedGateway",
|
|
@@ -147,4 +156,4 @@ __all__ = [
|
|
|
147
156
|
"validate",
|
|
148
157
|
]
|
|
149
158
|
|
|
150
|
-
__version__ = "0.0.
|
|
159
|
+
__version__ = "0.0.8"
|
|
@@ -0,0 +1,457 @@
|
|
|
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 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
|
+
|
|
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
|
+
|
|
17
|
+
Pattern 1 — AgentTrustReplyHook (v0.2 + v0.4):
|
|
18
|
+
Attach to any ConversableAgent / AssistantAgent to validate every reply
|
|
19
|
+
before it is sent.
|
|
20
|
+
|
|
21
|
+
Pattern 2 — AgentTrustGroupChatMonitor (v0.2 GroupChat):
|
|
22
|
+
Wraps a GroupChat's speaker-selection to validate messages between agents.
|
|
23
|
+
|
|
24
|
+
Usage::
|
|
25
|
+
|
|
26
|
+
from autogen import AssistantAgent
|
|
27
|
+
from agentrust_sdk.adapters.autogen import AgentTrustReplyHook
|
|
28
|
+
|
|
29
|
+
hook = AgentTrustReplyHook(agent_id="research-team")
|
|
30
|
+
assistant = AssistantAgent("researcher", llm_config={...})
|
|
31
|
+
hook.attach(assistant)
|
|
32
|
+
|
|
33
|
+
Requires Team tier.
|
|
34
|
+
"""
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import functools
|
|
38
|
+
import inspect
|
|
39
|
+
import logging
|
|
40
|
+
import time
|
|
41
|
+
from typing import Any, Callable, Sequence
|
|
42
|
+
|
|
43
|
+
from ..tiers import Capability
|
|
44
|
+
from ._core import Allowed, Governor, OnBlock, Refused, check_adapter_tier
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
_FRAMEWORK = "ag2"
|
|
49
|
+
_FRAMEWORK_V04 = "autogen"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def governed_function(
|
|
53
|
+
fn: Callable[..., Any],
|
|
54
|
+
*,
|
|
55
|
+
agent_id: str,
|
|
56
|
+
name: str | None = None,
|
|
57
|
+
on_block: OnBlock = "refuse",
|
|
58
|
+
review_timeout_s: float = 300.0,
|
|
59
|
+
user: str = "ag2",
|
|
60
|
+
base_url: str | None = None,
|
|
61
|
+
api_key: str | None = None,
|
|
62
|
+
governor: Governor | None = None,
|
|
63
|
+
) -> Callable[..., Any]:
|
|
64
|
+
"""Wrap an AG2 tool function so AgentTrust governs every call.
|
|
65
|
+
|
|
66
|
+
The result keeps the name, docstring and signature of *fn*, so AG2 builds
|
|
67
|
+
the same tool schema from it. Pass it to ``register_function`` or
|
|
68
|
+
``register_for_llm``. Async functions stay async.
|
|
69
|
+
"""
|
|
70
|
+
check_adapter_tier(Capability.AUTOGEN_ADAPTER, api_key)
|
|
71
|
+
gov = governor or Governor(
|
|
72
|
+
agent_id,
|
|
73
|
+
framework=_FRAMEWORK,
|
|
74
|
+
on_block=on_block,
|
|
75
|
+
review_timeout_s=review_timeout_s,
|
|
76
|
+
user=user,
|
|
77
|
+
base_url=base_url,
|
|
78
|
+
api_key=api_key,
|
|
79
|
+
)
|
|
80
|
+
tool_name = name or fn.__name__
|
|
81
|
+
signature = inspect.signature(fn)
|
|
82
|
+
|
|
83
|
+
def call_args(args: tuple, kwargs: dict) -> dict[str, Any]:
|
|
84
|
+
bound = signature.bind(*args, **kwargs)
|
|
85
|
+
bound.apply_defaults()
|
|
86
|
+
return dict(bound.arguments)
|
|
87
|
+
|
|
88
|
+
def unwrap(outcome: Allowed | Refused) -> Any:
|
|
89
|
+
# The refusal text is the tool result the agent reads next.
|
|
90
|
+
return outcome.message if isinstance(outcome, Refused) else outcome.result
|
|
91
|
+
|
|
92
|
+
if inspect.iscoroutinefunction(fn):
|
|
93
|
+
|
|
94
|
+
@functools.wraps(fn)
|
|
95
|
+
async def governed(*args: Any, **kwargs: Any) -> Any:
|
|
96
|
+
params = call_args(args, kwargs)
|
|
97
|
+
outcome = await gov.aguard_tool(
|
|
98
|
+
tool_name, params, lambda: fn(*args, **kwargs), input=_describe_call(tool_name, params),
|
|
99
|
+
)
|
|
100
|
+
return unwrap(outcome)
|
|
101
|
+
|
|
102
|
+
else:
|
|
103
|
+
|
|
104
|
+
@functools.wraps(fn)
|
|
105
|
+
def governed(*args: Any, **kwargs: Any) -> Any:
|
|
106
|
+
params = call_args(args, kwargs)
|
|
107
|
+
outcome = gov.guard_tool(
|
|
108
|
+
tool_name, params, lambda: fn(*args, **kwargs), input=_describe_call(tool_name, params),
|
|
109
|
+
)
|
|
110
|
+
return unwrap(outcome)
|
|
111
|
+
|
|
112
|
+
return governed
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def governed_functions(
|
|
116
|
+
fns: Sequence[Callable[..., Any]],
|
|
117
|
+
*,
|
|
118
|
+
agent_id: str,
|
|
119
|
+
on_block: OnBlock = "refuse",
|
|
120
|
+
review_timeout_s: float = 300.0,
|
|
121
|
+
user: str = "ag2",
|
|
122
|
+
base_url: str | None = None,
|
|
123
|
+
api_key: str | None = None,
|
|
124
|
+
governor: Governor | None = None,
|
|
125
|
+
) -> list[Callable[..., Any]]:
|
|
126
|
+
"""Wrap each function with :func:`governed_function`, sharing one Governor."""
|
|
127
|
+
check_adapter_tier(Capability.AUTOGEN_ADAPTER, api_key)
|
|
128
|
+
gov = governor or Governor(
|
|
129
|
+
agent_id,
|
|
130
|
+
framework=_FRAMEWORK,
|
|
131
|
+
on_block=on_block,
|
|
132
|
+
review_timeout_s=review_timeout_s,
|
|
133
|
+
user=user,
|
|
134
|
+
base_url=base_url,
|
|
135
|
+
api_key=api_key,
|
|
136
|
+
)
|
|
137
|
+
return [governed_function(fn, agent_id=agent_id, governor=gov) for fn in fns]
|
|
138
|
+
|
|
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
|
+
|
|
236
|
+
def _describe_call(name: str, args: dict[str, Any]) -> str:
|
|
237
|
+
"""The proposed call as the pre-check's request input."""
|
|
238
|
+
return f"tool call {name}({', '.join(f'{k}={v!r}' for k, v in args.items())})"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _validate_sync(
|
|
242
|
+
agent_id: str,
|
|
243
|
+
base_url: str,
|
|
244
|
+
api_key: str | None,
|
|
245
|
+
user: str,
|
|
246
|
+
input_text: str,
|
|
247
|
+
output: dict,
|
|
248
|
+
framework: str,
|
|
249
|
+
block_on_block: bool,
|
|
250
|
+
) -> None:
|
|
251
|
+
from agentrust_sdk.client import AgentTrustClient, GatewayUnavailableError
|
|
252
|
+
from agentrust_sdk.config import SDK_CONFIG
|
|
253
|
+
from agentrust_sdk.decorator import _check_decision, BlockedError
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
with AgentTrustClient(base_url=base_url, api_key=api_key) as client:
|
|
257
|
+
resp = client.validate(
|
|
258
|
+
agent_id=agent_id,
|
|
259
|
+
user=user,
|
|
260
|
+
input=input_text,
|
|
261
|
+
output=output,
|
|
262
|
+
framework=framework,
|
|
263
|
+
)
|
|
264
|
+
_check_decision(resp, block_on_block, False)
|
|
265
|
+
except (BlockedError, GatewayUnavailableError):
|
|
266
|
+
raise
|
|
267
|
+
except Exception as exc:
|
|
268
|
+
if SDK_CONFIG.failure_mode == "closed":
|
|
269
|
+
raise
|
|
270
|
+
logger.warning("[AgentTrust] AutoGen validate failed (non-fatal): %s", exc)
|
|
271
|
+
|
|
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
|
+
|
|
306
|
+
class AgentTrustReplyHook:
|
|
307
|
+
"""
|
|
308
|
+
Reply hook that validates every AutoGen agent reply via AgentTrust.
|
|
309
|
+
|
|
310
|
+
Compatible with AutoGen v0.2 (pyautogen) reply_func mechanism and
|
|
311
|
+
AutoGen v0.4 (autogen-agentchat) message hook pattern.
|
|
312
|
+
|
|
313
|
+
Usage (v0.2)::
|
|
314
|
+
|
|
315
|
+
hook = AgentTrustReplyHook(agent_id="my-autogen")
|
|
316
|
+
assistant = AssistantAgent("researcher", llm_config={...})
|
|
317
|
+
hook.attach(assistant)
|
|
318
|
+
|
|
319
|
+
Usage (v0.4)::
|
|
320
|
+
|
|
321
|
+
hook = AgentTrustReplyHook(agent_id="my-autogen")
|
|
322
|
+
agent = AssistantAgent("researcher", model_client=..., tools=[...])
|
|
323
|
+
hook.attach_v4(agent)
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
def __init__(
|
|
327
|
+
self,
|
|
328
|
+
agent_id: str,
|
|
329
|
+
*,
|
|
330
|
+
base_url: str = "http://localhost:8000",
|
|
331
|
+
api_key: str | None = None,
|
|
332
|
+
user: str = "autogen",
|
|
333
|
+
block_on_block: bool = True,
|
|
334
|
+
framework: str = "AutoGen",
|
|
335
|
+
) -> None:
|
|
336
|
+
check_adapter_tier(Capability.AUTOGEN_ADAPTER, api_key)
|
|
337
|
+
self._agent_id = agent_id
|
|
338
|
+
self._base_url = base_url
|
|
339
|
+
self._api_key = api_key
|
|
340
|
+
self._user = user
|
|
341
|
+
self._block_on_block = block_on_block
|
|
342
|
+
self._framework = framework
|
|
343
|
+
|
|
344
|
+
# ------------------------------------------------------------------
|
|
345
|
+
# AutoGen v0.2
|
|
346
|
+
# ------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
def _check_outgoing(
|
|
349
|
+
self, sender: Any = None, message: Any = None, recipient: Any = None, silent: bool = False,
|
|
350
|
+
) -> Any:
|
|
351
|
+
"""AG2 ``process_message_before_send`` hook: check the agent's own reply."""
|
|
352
|
+
content = message.get("content") if isinstance(message, dict) else message
|
|
353
|
+
if not isinstance(content, str) or not content:
|
|
354
|
+
return message
|
|
355
|
+
|
|
356
|
+
t0 = time.perf_counter()
|
|
357
|
+
_validate_sync(
|
|
358
|
+
self._agent_id, self._base_url, self._api_key, self._user,
|
|
359
|
+
content[:500], {"content": content[:4000], "role": "assistant"},
|
|
360
|
+
self._framework, self._block_on_block,
|
|
361
|
+
)
|
|
362
|
+
logger.debug(
|
|
363
|
+
"[AgentTrust] AutoGen reply validated in %.1f ms", (time.perf_counter() - t0) * 1000
|
|
364
|
+
)
|
|
365
|
+
return message
|
|
366
|
+
|
|
367
|
+
def attach(self, agent: Any) -> None:
|
|
368
|
+
"""Attach to an AG2 / AutoGen v0.2 ConversableAgent.
|
|
369
|
+
|
|
370
|
+
Checks every message the agent sends, before it leaves the agent.
|
|
371
|
+
"""
|
|
372
|
+
if not hasattr(agent, "register_hook"):
|
|
373
|
+
raise TypeError(
|
|
374
|
+
f"Expected a ConversableAgent with register_hook(), got {type(agent).__name__}"
|
|
375
|
+
)
|
|
376
|
+
agent.register_hook("process_message_before_send", self._check_outgoing)
|
|
377
|
+
logger.info(
|
|
378
|
+
"[AgentTrust] AutoGen reply hook attached to agent '%s'", getattr(agent, "name", "?")
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# ------------------------------------------------------------------
|
|
382
|
+
# AutoGen v0.4
|
|
383
|
+
# ------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
def attach_v4(self, agent: Any) -> None:
|
|
386
|
+
"""Attach to an AutoGen v0.4 BaseChatAgent via on_messages hook."""
|
|
387
|
+
original_on_messages = agent.on_messages
|
|
388
|
+
|
|
389
|
+
async def _wrapped_on_messages(messages: Any, cancellation_token: Any = None) -> Any:
|
|
390
|
+
result = await original_on_messages(messages, cancellation_token)
|
|
391
|
+
# result is a Response object with a .chat_message attribute
|
|
392
|
+
content = ""
|
|
393
|
+
if hasattr(result, "chat_message") and result.chat_message:
|
|
394
|
+
content = getattr(result.chat_message, "content", "") or ""
|
|
395
|
+
output = {"content": content[:4000]}
|
|
396
|
+
await _avalidate(
|
|
397
|
+
self._agent_id, self._base_url, self._api_key,
|
|
398
|
+
self._user, content[:500], output, self._framework, self._block_on_block,
|
|
399
|
+
)
|
|
400
|
+
return result
|
|
401
|
+
|
|
402
|
+
agent.on_messages = _wrapped_on_messages
|
|
403
|
+
logger.info(
|
|
404
|
+
"[AgentTrust] AutoGen v0.4 hook attached to agent '%s'",
|
|
405
|
+
getattr(agent, "name", "?"),
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class AgentTrustGroupChatMonitor:
|
|
410
|
+
"""
|
|
411
|
+
Monitors AutoGen GroupChat messages and validates each speaker's reply.
|
|
412
|
+
|
|
413
|
+
Usage::
|
|
414
|
+
|
|
415
|
+
from autogen import GroupChat, GroupChatManager
|
|
416
|
+
from agentrust_sdk.adapters.autogen import AgentTrustGroupChatMonitor
|
|
417
|
+
|
|
418
|
+
monitor = AgentTrustGroupChatMonitor(agent_id="planning-crew")
|
|
419
|
+
groupchat = GroupChat(agents=[...], messages=[])
|
|
420
|
+
manager = GroupChatManager(groupchat=groupchat)
|
|
421
|
+
monitor.attach(groupchat)
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
def __init__(
|
|
425
|
+
self,
|
|
426
|
+
agent_id: str,
|
|
427
|
+
*,
|
|
428
|
+
base_url: str = "http://localhost:8000",
|
|
429
|
+
api_key: str | None = None,
|
|
430
|
+
user: str = "autogen-group",
|
|
431
|
+
block_on_block: bool = False, # Default False — group chats tolerate review
|
|
432
|
+
framework: str = "AutoGen",
|
|
433
|
+
) -> None:
|
|
434
|
+
check_adapter_tier(Capability.AUTOGEN_ADAPTER, api_key)
|
|
435
|
+
self._agent_id = agent_id
|
|
436
|
+
self._base_url = base_url
|
|
437
|
+
self._api_key = api_key
|
|
438
|
+
self._user = user
|
|
439
|
+
self._block_on_block = block_on_block
|
|
440
|
+
self._framework = framework
|
|
441
|
+
|
|
442
|
+
def attach(self, groupchat: Any) -> None:
|
|
443
|
+
"""Patch groupchat.append to intercept every new message."""
|
|
444
|
+
original_append = groupchat.append
|
|
445
|
+
|
|
446
|
+
def _governed_append(message: dict, speaker: Any) -> None:
|
|
447
|
+
content = message.get("content", "") or ""
|
|
448
|
+
_validate_sync(
|
|
449
|
+
self._agent_id, self._base_url, self._api_key,
|
|
450
|
+
self._user, content[:500],
|
|
451
|
+
{"content": content[:4000], "speaker": getattr(speaker, "name", str(speaker))},
|
|
452
|
+
self._framework, self._block_on_block,
|
|
453
|
+
)
|
|
454
|
+
original_append(message, speaker)
|
|
455
|
+
|
|
456
|
+
groupchat.append = _governed_append
|
|
457
|
+
logger.info("[AgentTrust] AutoGen GroupChatMonitor attached")
|
|
@@ -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.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" }
|
|
@@ -41,6 +41,10 @@ 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"]
|
|
46
|
+
# Microsoft AutoGen 0.4+ (module autogen_agentchat)
|
|
47
|
+
autogen = ["autogen-agentchat>=0.4,<1.0"]
|
|
44
48
|
openai = ["openai>=2.41.1,<3.0.0"]
|
|
45
49
|
langchain = ["langchain>=0.2.0,<1.0.0"]
|
|
46
50
|
# 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
|