Skip to content

Commit 39948ab

Browse files
aditik0303claude
andcommitted
refactor(governance): migrate Google ADK adapter to factory-evaluator
Core PR #1761 removed BaseAdapter from uipath-core. Migrate to the factory-evaluator pattern (matching #899): - governance/adapter.py -> callbacks.py: replace the BaseAdapter subclass (name/can_handle/attach/detach) with module-level install_governance() that installs governance on each LlmAgent's native *_callback slots (walking sub_agents); keep GovernanceCallbacks + the callback helpers; drop the detach-only _remove_callbacks. File named for its seam (callbacks), like LangChain's callbacks.py. - runtime/factory.py: new_runtime reads `evaluator` from kwargs and calls install_governance before building the Runner. - governance/__init__.py: drop register_governance_adapter + registry import; expose install_governance. No import-time side effects. - pyproject.toml: remove the uipath.governance.adapters entry point. - tests (test_adapter.py -> test_callbacks.py): drop can_handle/attach/ detach; cover install_governance + factory wiring. ruff + mypy clean; 21 governance tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e0a0cf1 commit 39948ab

6 files changed

Lines changed: 156 additions & 179 deletions

File tree

packages/uipath-google-adk/pyproject.toml

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

34-
[project.entry-points."uipath.governance.adapters"]
35-
google-adk = "uipath_google_adk.governance:register_governance_adapter"
36-
3734
[project.urls]
3835
Homepage = "https://uipath.com"
3936
Repository = "https://github.com/UiPath/uipath-integrations-python"
Lines changed: 11 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,20 @@
11
"""Governance integration for ``uipath-google-adk``.
22
3-
Registers :class:`GoogleADKAdapter` with the adapter registry in
4-
``uipath.core.adapters`` so the governance host can attach the ADK-specific
5-
inner hooks (BEFORE_MODEL, AFTER_MODEL, TOOL_CALL, AFTER_TOOL) when it sees a
6-
Google ADK 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 governance callbacks
4+
(BEFORE_MODEL, AFTER_MODEL, TOOL_CALL, AFTER_TOOL) on every ``LlmAgent`` in an
5+
ADK agent tree's native ``*_callback`` slots. Wired into a run by passing an
6+
``evaluator`` to :class:`UiPathGoogleADKRuntimeFactory`; 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 GoogleADKAdapter, GovernanceCallbacks
24-
25-
logger = logging.getLogger(__name__)
26-
27-
_registered: bool = False
28-
29-
30-
def register_governance_adapter() -> None:
31-
"""Register :class:`GoogleADKAdapter` 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 == "GoogleADK" for a in registry.get_all()):
40-
_registered = True
41-
return
42-
registry.register(GoogleADKAdapter())
43-
_registered = True
44-
logger.debug("Registered uipath-google-adk governance adapter")
45-
15+
from .callbacks import GovernanceCallbacks, install_governance
4616

4717
__all__ = [
48-
"GoogleADKAdapter",
4918
"GovernanceCallbacks",
50-
"register_governance_adapter",
51-
]
19+
"install_governance",
20+
]

packages/uipath-google-adk/src/uipath_google_adk/governance/adapter.py renamed to packages/uipath-google-adk/src/uipath_google_adk/governance/callbacks.py

Lines changed: 50 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
"""Google ADK adapter for UiPath governance.
1+
"""Google ADK governance callbacks for UiPath.
22
33
Provides governance for Google ADK agents (``google.adk.agents.LlmAgent``
4-
and any ``BaseAgent`` tree containing them). Unlike the LangChain adapter
4+
and any ``BaseAgent`` tree containing them). Unlike the LangChain integration
55
— which wraps a ``Runnable`` and intercepts ``invoke`` / ``ainvoke`` — ADK
66
agents are executed by a ``Runner`` that holds its **own** reference to
77
the agent object. Replacing ``runtime.agent`` with a proxy would never
8-
reach the ``Runner``. So this adapter installs governance directly onto
9-
each ``LlmAgent``'s native callback attributes, mutating them in place:
8+
reach the ``Runner``. So :func:`install_governance` installs governance
9+
directly onto each ``LlmAgent``'s native callback attributes, mutating them
10+
in place:
1011
1112
- ``before_model_callback`` → BEFORE_MODEL
1213
- ``after_model_callback`` → AFTER_MODEL
1314
- ``before_tool_callback`` → TOOL_CALL
1415
- ``after_tool_callback`` → AFTER_TOOL
1516
16-
Because the mutation is in place, :meth:`GoogleADKAdapter.attach` returns
17-
the **original agent** (hooks installed) rather than a wrapping proxy.
17+
Because the mutation is in place, :func:`install_governance` returns the
18+
**original agent** (hooks installed) rather than a wrapping proxy.
1819
Returning a proxy here would also break ADK's own ``isinstance(agent,
1920
LlmAgent)`` checks in output-schema / graph resolution, since ``LlmAgent``
2021
is a Pydantic model.
@@ -23,10 +24,11 @@
2324
*not* fired from here — they are owned by the governance host. Firing them
2425
here too would duplicate every boundary evaluation.
2526
26-
Contracts and the evaluator protocol come from ``uipath-core``; this
27-
package contributes only the ADK-specific implementation and registers it
28-
with the adapter registry via the ``uipath.governance.adapters`` entry
29-
point.
27+
The evaluator protocol comes from ``uipath-core``; this package contributes
28+
only the ADK-specific wiring. Governance is installed by the runtime
29+
factory: passing an ``evaluator`` to ``new_runtime`` calls
30+
:func:`install_governance` on the resolved agent. No adapter registry, no
31+
entry point, no import-time side effects.
3032
3133
Audit emission and enforcement (raising :class:`GovernanceBlockException`
3234
on DENY) are owned by the evaluator itself. Each callback only extracts
@@ -43,7 +45,7 @@
4345
from typing import Any, Dict, List
4446
from uuid import uuid4
4547

46-
from uipath.core.adapters import BaseAdapter, EvaluatorProtocol
48+
from uipath.core.adapters import EvaluatorProtocol
4749
from uipath.core.governance.exceptions import GovernanceBlockException
4850

4951
logger = logging.getLogger(__name__)
@@ -93,19 +95,6 @@ def _install_callback(agent: Any, attr: str, fn: Any) -> None:
9395
setattr(agent, attr, [fn, *handlers])
9496

9597

96-
def _remove_callbacks(agent: Any) -> None:
97-
"""Strip this adapter's governance callbacks from every managed slot."""
98-
for attr in (_MODEL_BEFORE, _MODEL_AFTER, _TOOL_BEFORE, _TOOL_AFTER):
99-
existing = getattr(agent, attr, None)
100-
if existing is None:
101-
continue
102-
if isinstance(existing, list):
103-
kept = [h for h in existing if not _is_governance_callable(h)]
104-
setattr(agent, attr, kept or None)
105-
elif _is_governance_callable(existing):
106-
setattr(agent, attr, None)
107-
108-
10998
def _iter_llm_agents(root: Any) -> List[Any]:
11099
"""Return every ``LlmAgent``-shaped node in the ``sub_agents`` tree.
111100
@@ -132,67 +121,46 @@ def _iter_llm_agents(root: Any) -> List[Any]:
132121
return found
133122

134123

135-
class GoogleADKAdapter(BaseAdapter):
136-
"""Adapter for the Google ADK framework.
137-
138-
Detects ``google.adk`` agents and installs governance callbacks on
139-
every ``LlmAgent`` reachable through the ``sub_agents`` tree.
140-
"""
124+
def install_governance(
125+
agent: Any,
126+
evaluator: EvaluatorProtocol,
127+
*,
128+
agent_name: str,
129+
session_id: str,
130+
) -> Any:
131+
"""Install governance callbacks on the agent tree (mutated in place).
141132
142-
@property
143-
def name(self) -> str:
144-
return "GoogleADK"
145-
146-
def can_handle(self, agent: Any) -> bool:
147-
"""Return True only for a Google ADK ``BaseAgent`` (incl. LlmAgent trees)."""
148-
try:
149-
from google.adk.agents import BaseAgent
150-
except ImportError:
151-
return False
152-
return isinstance(agent, BaseAgent)
153-
154-
def attach(
155-
self,
156-
agent: Any,
157-
agent_id: str,
158-
session_id: str,
159-
evaluator: EvaluatorProtocol,
160-
) -> Any:
161-
"""Install governance callbacks on the agent (mutated in place).
133+
Walks every ``LlmAgent`` reachable through ``sub_agents`` and prepends
134+
governance to each model/tool callback slot, preserving existing handlers.
135+
Returns the original ``agent`` — the ``Runner`` already holds this
136+
reference, so in-place mutation is what wires governance into execution.
137+
Idempotent: a slot that already carries a governance callback is skipped.
162138
163-
Returns the original ``agent`` — the ``Runner`` already holds this
164-
reference, so in-place mutation is what actually wires governance
165-
into execution. A wrapping proxy would not reach the ``Runner``
166-
and would break ADK's ``isinstance(agent, LlmAgent)`` checks.
167-
"""
168-
callbacks = GovernanceCallbacks(
169-
evaluator=evaluator,
170-
agent_name=agent_id,
171-
session_id=session_id,
139+
Called by :class:`UiPathGoogleADKRuntimeFactory` when an ``evaluator``
140+
is supplied to ``new_runtime``.
141+
"""
142+
callbacks = GovernanceCallbacks(
143+
evaluator=evaluator,
144+
agent_name=agent_name,
145+
session_id=session_id,
146+
)
147+
llm_agents = _iter_llm_agents(agent)
148+
for node in llm_agents:
149+
_install_callback(node, _MODEL_BEFORE, callbacks.before_model)
150+
_install_callback(node, _MODEL_AFTER, callbacks.after_model)
151+
_install_callback(node, _TOOL_BEFORE, callbacks.before_tool)
152+
_install_callback(node, _TOOL_AFTER, callbacks.after_tool)
153+
if not llm_agents:
154+
logger.warning(
155+
"install_governance found no LlmAgent in %s — deep hooks will not fire",
156+
type(agent).__name__,
172157
)
173-
llm_agents = _iter_llm_agents(agent)
174-
for node in llm_agents:
175-
_install_callback(node, _MODEL_BEFORE, callbacks.before_model)
176-
_install_callback(node, _MODEL_AFTER, callbacks.after_model)
177-
_install_callback(node, _TOOL_BEFORE, callbacks.before_tool)
178-
_install_callback(node, _TOOL_AFTER, callbacks.after_tool)
179-
if not llm_agents:
180-
logger.warning(
181-
"GoogleADKAdapter found no LlmAgent in %s — deep hooks will not fire",
182-
type(agent).__name__,
183-
)
184-
else:
185-
logger.debug(
186-
"Installed governance callbacks on %d ADK LlmAgent(s)",
187-
len(llm_agents),
188-
)
189-
return agent
190-
191-
def detach(self, governed: Any) -> Any:
192-
"""Remove governance callbacks from the agent tree and return it."""
193-
for node in _iter_llm_agents(governed):
194-
_remove_callbacks(node)
195-
return governed
158+
else:
159+
logger.debug(
160+
"Installed governance callbacks on %d ADK LlmAgent(s)",
161+
len(llm_agents),
162+
)
163+
return agent
196164

197165

198166
class GovernanceCallbacks:

packages/uipath-google-adk/src/uipath_google_adk/runtime/factory.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from google.adk.runners import Runner
99
from google.adk.sessions.sqlite_session_service import SqliteSessionService
1010
from openinference.instrumentation.google_adk import GoogleADKInstrumentor
11+
from uipath.core.adapters import EvaluatorProtocol
1112
from uipath.runtime import (
1213
UiPathRuntimeContext,
1314
UiPathRuntimeFactorySettings,
@@ -16,6 +17,7 @@
1617
)
1718
from uipath.runtime.errors import UiPathErrorCategory
1819

20+
from uipath_google_adk.governance import install_governance
1921
from uipath_google_adk.runtime.config import GoogleADKConfig
2022
from uipath_google_adk.runtime.errors import (
2123
UiPathGoogleADKErrorCode,
@@ -209,6 +211,7 @@ async def _create_runtime_instance(
209211
agent: BaseAgent,
210212
runtime_id: str,
211213
entrypoint: str,
214+
evaluator: EvaluatorProtocol | None = None,
212215
) -> UiPathRuntimeProtocol:
213216
"""
214217
Create a runtime instance from an agent.
@@ -217,7 +220,19 @@ async def _create_runtime_instance(
217220
retrieves or creates a session for the given runtime_id.
218221
Sessions persist across calls, enabling multi-turn conversations
219222
where only the current user message is sent each time.
223+
224+
When ``evaluator`` is supplied, governance callbacks are installed on
225+
the agent tree in place via :func:`install_governance` before the
226+
``Runner`` is created.
220227
"""
228+
if evaluator is not None:
229+
install_governance(
230+
agent,
231+
evaluator,
232+
agent_name=entrypoint,
233+
session_id=runtime_id,
234+
)
235+
221236
session_service = await self._get_session_service()
222237
runner = Runner(
223238
agent=agent,
@@ -256,7 +271,9 @@ async def new_runtime(
256271
Args:
257272
entrypoint: Agent name from google_adk.json
258273
runtime_id: Unique identifier for the runtime instance
259-
**kwargs: Additional keyword arguments (unused)
274+
**kwargs: Forwarded factory kwargs. Recognized: ``evaluator``
275+
(``EvaluatorProtocol``) — when present, governance callbacks
276+
are installed on the agent via :func:`install_governance`.
260277
261278
Returns:
262279
Configured runtime instance with agent
@@ -267,6 +284,7 @@ async def new_runtime(
267284
agent=agent,
268285
runtime_id=runtime_id,
269286
entrypoint=entrypoint,
287+
evaluator=kwargs.get("evaluator"),
270288
)
271289

272290
async def dispose(self) -> None:

0 commit comments

Comments
 (0)