Skip to content

Commit e0c7d86

Browse files
aditik0303claude
andcommitted
refactor(governance): migrate OpenAI Agents adapter to factory-evaluator
Core PR #1761 dropped BaseAdapter + AdapterRegistry from uipath-core (only EvaluatorProtocol remains), so the entry-point/adapter model no longer compiles against current core. Migrate to the factory-evaluator pattern used by the LangChain adapter (#899): - governance/adapter.py: replace the BaseAdapter subclass (name/ can_handle/attach/detach) with a module-level install_governance(); keep GovernanceAgentHooks + the handoff-graph walk + chaining. - runtime/factory.py: new_runtime reads `evaluator` from kwargs and calls install_governance on the resolved agent. - governance/__init__.py: drop register_governance_adapter + the registry import; expose install_governance. No import-time side effects. - pyproject.toml: remove the uipath.governance.adapters entry point. - tests: drop can_handle/attach/detach tests; cover install_governance + factory wiring (evaluator present installs, absent skips). ruff + mypy clean; 24 governance tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent fd72ec5 commit e0c7d86

5 files changed

Lines changed: 120 additions & 165 deletions

File tree

packages/uipath-openai-agents/pyproject.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ register = "uipath_openai_agents.middlewares:register_middleware"
3131
[project.entry-points."uipath.runtime.factories"]
3232
openai-agents = "uipath_openai_agents.runtime:register_runtime_factory"
3333

34-
[project.entry-points."uipath.governance.adapters"]
35-
openai-agents = "uipath_openai_agents.governance:register_governance_adapter"
36-
3734
[project.urls]
3835
Homepage = "https://uipath.com"
3936
Repository = "https://github.com/UiPath/uipath-integrations-python"
Lines changed: 10 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,20 @@
11
"""Governance integration for ``uipath-openai-agents``.
22
3-
Registers :class:`OpenAIAgentsAdapter` with the adapter registry in
4-
``uipath.core.adapters`` so the governance host can attach the
5-
OpenAI-Agents-specific inner hooks (BEFORE_MODEL, AFTER_MODEL, TOOL_CALL,
6-
AFTER_TOOL) when it sees an OpenAI Agents agent.
7-
8-
Registration is **idempotent**: calling :func:`register_governance_adapter`
9-
twice is a no-op on the second call.
10-
11-
Wiring: the package exposes :func:`register_governance_adapter` as an entry
12-
point under ``uipath.governance.adapters``. The governance adapter discovery
13-
path calls it to register the adapter. Importing this module does not, by
14-
itself, mutate the global registry.
3+
Exposes :func:`install_governance` — installs the OpenAI-Agents-specific inner
4+
hooks (BEFORE_MODEL, AFTER_MODEL, TOOL_CALL, AFTER_TOOL) onto an agent's native
5+
``hooks`` slot. Wired into a run by passing an ``evaluator`` to
6+
:class:`UiPathOpenAIAgentRuntimeFactory`; the factory calls
7+
:func:`install_governance` on the resolved agent.
8+
9+
Importing this module has no side effects: no adapter is registered, no global
10+
state is mutated.
1511
"""
1612

1713
from __future__ import annotations
1814

19-
import logging
20-
21-
from uipath.core.adapters import get_adapter_registry
22-
23-
from .adapter import GovernanceAgentHooks, OpenAIAgentsAdapter
24-
25-
logger = logging.getLogger(__name__)
26-
27-
_registered: bool = False
28-
29-
30-
def register_governance_adapter() -> None:
31-
"""Register :class:`OpenAIAgentsAdapter` with the global registry.
32-
33-
Idempotent — safe to call multiple times.
34-
"""
35-
global _registered
36-
if _registered:
37-
return
38-
registry = get_adapter_registry()
39-
if any(a.name == "OpenAIAgents" for a in registry.get_all()):
40-
_registered = True
41-
return
42-
registry.register(OpenAIAgentsAdapter())
43-
_registered = True
44-
logger.debug("Registered uipath-openai-agents governance adapter")
45-
15+
from .adapter import GovernanceAgentHooks, install_governance
4616

4717
__all__ = [
4818
"GovernanceAgentHooks",
49-
"OpenAIAgentsAdapter",
50-
"register_governance_adapter",
19+
"install_governance",
5120
]

packages/uipath-openai-agents/src/uipath_openai_agents/governance/adapter.py

Lines changed: 43 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@
3030
boundary evaluation. (The SDK's per-agent ``on_start`` / ``on_end`` are
3131
pass-through-only here for that reason.)
3232
33-
Contracts and the evaluator protocol come from ``uipath-core``; this package
34-
contributes only the OpenAI-Agents-specific implementation and registers it
35-
with the adapter registry via the ``uipath.governance.adapters`` entry point.
33+
The evaluator protocol comes from ``uipath-core``; this package contributes
34+
only the OpenAI-Agents-specific wiring. Governance is installed by the runtime
35+
factory: passing an ``evaluator`` to
36+
:class:`UiPathOpenAIAgentRuntimeFactory.new_runtime` calls
37+
:func:`install_governance` on the resolved agent. No adapter registry, no
38+
entry point, no import-time side effects.
3639
3740
Audit emission and enforcement (raising :class:`GovernanceBlockException` on
3841
DENY) are owned by the evaluator itself. Each hook only extracts the relevant
@@ -50,7 +53,7 @@
5053
from uuid import uuid4
5154

5255
from agents import Agent, AgentHooks
53-
from uipath.core.adapters import BaseAdapter, EvaluatorProtocol
56+
from uipath.core.adapters import EvaluatorProtocol
5457
from uipath.core.governance.exceptions import GovernanceBlockException
5558

5659
logger = logging.getLogger(__name__)
@@ -62,73 +65,46 @@
6265
# full prompt — see :func:`_latest_input_text`.
6366
_BEFORE_MODEL_TEXT_CAP = 64000
6467

65-
# Marks an agent we have already governed so a double ``attach`` is a no-op and
66-
# ``detach`` can restore the hooks slot to whatever was there before.
67-
_PREV_HOOKS_ATTR = "_uipath_governance_prev_hooks"
6868

69+
def install_governance(
70+
agent: Agent,
71+
evaluator: EvaluatorProtocol,
72+
*,
73+
agent_name: str,
74+
session_id: str,
75+
) -> Agent:
76+
"""Install governance hooks on the agent graph (mutated in place).
6977
70-
class OpenAIAgentsAdapter(BaseAdapter):
71-
"""Adapter for the OpenAI Agents SDK.
78+
Walks every agent reachable through ``handoffs`` and installs a
79+
:class:`GovernanceAgentHooks` on each one's ``hooks`` slot, chaining to any
80+
pre-existing hooks. Returns the original ``agent`` — the ``Runner`` already
81+
holds this reference, so in-place mutation is what wires governance into
82+
execution. Idempotent: an already-governed agent is left untouched.
7283
73-
Detects ``agents.Agent`` instances and installs governance hooks on every
74-
agent reachable through the ``handoffs`` graph.
84+
Called by :class:`UiPathOpenAIAgentRuntimeFactory` when an ``evaluator``
85+
is supplied to ``new_runtime``.
7586
"""
76-
77-
@property
78-
def name(self) -> str:
79-
return "OpenAIAgents"
80-
81-
def can_handle(self, agent: Any) -> bool:
82-
"""Return True only for an OpenAI Agents ``Agent``."""
83-
return isinstance(agent, Agent)
84-
85-
def attach(
86-
self,
87-
agent: Any,
88-
agent_id: str,
89-
session_id: str,
90-
evaluator: EvaluatorProtocol,
91-
) -> Any:
92-
"""Install governance hooks on the agent graph (mutated in place).
93-
94-
Returns the original ``agent`` — the ``Runner`` already holds this
95-
reference, so in-place mutation is what actually wires governance into
96-
execution. A wrapping proxy would not reach the ``Runner`` and would
97-
break the SDK's ``isinstance(agent, Agent)`` checks.
98-
"""
99-
agents = _iter_agents(agent)
100-
installed = 0
101-
for node in agents:
102-
if isinstance(getattr(node, "hooks", None), GovernanceAgentHooks):
103-
continue # idempotent — already governed
104-
prev = getattr(node, "hooks", None)
105-
hooks = GovernanceAgentHooks(
106-
evaluator=evaluator,
107-
agent_name=agent_id,
108-
session_id=session_id,
109-
inner=prev,
110-
)
111-
# Remember what was there so detach can restore it.
112-
setattr(node, _PREV_HOOKS_ATTR, prev)
113-
node.hooks = hooks
114-
installed += 1
115-
if not agents:
116-
logger.warning(
117-
"OpenAIAgentsAdapter found no Agent in %s — deep hooks will not fire",
118-
type(agent).__name__,
119-
)
120-
else:
121-
logger.debug("Installed governance hooks on %d OpenAI agent(s)", installed)
122-
return agent
123-
124-
def detach(self, governed: Any) -> Any:
125-
"""Restore each agent's original ``hooks`` slot and return the graph."""
126-
for node in _iter_agents(governed):
127-
if isinstance(getattr(node, "hooks", None), GovernanceAgentHooks):
128-
node.hooks = getattr(node, _PREV_HOOKS_ATTR, None)
129-
if hasattr(node, _PREV_HOOKS_ATTR):
130-
delattr(node, _PREV_HOOKS_ATTR)
131-
return governed
87+
agents = _iter_agents(agent)
88+
installed = 0
89+
for node in agents:
90+
if isinstance(getattr(node, "hooks", None), GovernanceAgentHooks):
91+
continue # idempotent — already governed
92+
prev = getattr(node, "hooks", None)
93+
node.hooks = GovernanceAgentHooks(
94+
evaluator=evaluator,
95+
agent_name=agent_name,
96+
session_id=session_id,
97+
inner=prev,
98+
)
99+
installed += 1
100+
if not agents:
101+
logger.warning(
102+
"install_governance found no Agent in %s — deep hooks will not fire",
103+
type(agent).__name__,
104+
)
105+
else:
106+
logger.debug("Installed governance hooks on %d OpenAI agent(s)", installed)
107+
return agent
132108

133109

134110
def _iter_agents(root: Any) -> List[Any]:

packages/uipath-openai-agents/src/uipath_openai_agents/runtime/factory.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from agents import Agent
77
from openinference.instrumentation.openai_agents import OpenAIAgentsInstrumentor
8+
from uipath.core.adapters import EvaluatorProtocol
89
from uipath.runtime import (
910
UiPathRuntimeContext,
1011
UiPathRuntimeFactorySettings,
@@ -13,6 +14,7 @@
1314
)
1415
from uipath.runtime.errors import UiPathErrorCategory
1516

17+
from uipath_openai_agents.governance import install_governance
1618
from uipath_openai_agents.runtime.agent import OpenAiAgentLoader
1719
from uipath_openai_agents.runtime.config import OpenAiAgentsConfig
1820
from uipath_openai_agents.runtime.errors import (
@@ -201,6 +203,7 @@ async def _create_runtime_instance(
201203
agent: Agent,
202204
runtime_id: str,
203205
entrypoint: str,
206+
evaluator: EvaluatorProtocol | None = None,
204207
) -> UiPathRuntimeProtocol:
205208
"""
206209
Create a runtime instance from an agent.
@@ -209,10 +212,20 @@ async def _create_runtime_instance(
209212
agent: The OpenAI Agent
210213
runtime_id: Unique identifier for the runtime instance
211214
entrypoint: Agent entrypoint name
215+
evaluator: When supplied, governance hooks are installed on the
216+
agent graph in place via :func:`install_governance`.
212217
213218
Returns:
214219
Configured runtime instance
215220
"""
221+
if evaluator is not None:
222+
install_governance(
223+
agent,
224+
evaluator,
225+
agent_name=entrypoint,
226+
session_id=runtime_id,
227+
)
228+
216229
return UiPathOpenAIAgentRuntime(
217230
agent=agent,
218231
runtime_id=runtime_id,
@@ -228,7 +241,9 @@ async def new_runtime(
228241
Args:
229242
entrypoint: Agent name from openai_agents.json
230243
runtime_id: Unique identifier for the runtime instance
231-
**kwargs: Additional keyword arguments (unused)
244+
**kwargs: Forwarded factory kwargs. Recognized: ``evaluator``
245+
(``EvaluatorProtocol``) — when present, governance hooks are
246+
installed on the agent via :func:`install_governance`.
232247
233248
Returns:
234249
Configured runtime instance with agent
@@ -239,6 +254,7 @@ async def new_runtime(
239254
agent=agent,
240255
runtime_id=runtime_id,
241256
entrypoint=entrypoint,
257+
evaluator=kwargs.get("evaluator"),
242258
)
243259

244260
async def dispose(self) -> None:

0 commit comments

Comments
 (0)