1- """Google ADK adapter for UiPath governance .
1+ """Google ADK governance callbacks for UiPath.
22
33Provides 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
66agents are executed by a ``Runner`` that holds its **own** reference to
77the 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.
1819Returning a proxy here would also break ADK's own ``isinstance(agent,
1920LlmAgent)`` checks in output-schema / graph resolution, since ``LlmAgent``
2021is a Pydantic model.
2324*not* fired from here — they are owned by the governance host. Firing them
2425here 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
3133Audit emission and enforcement (raising :class:`GovernanceBlockException`
3234on DENY) are owned by the evaluator itself. Each callback only extracts
4345from typing import Any , Dict , List
4446from uuid import uuid4
4547
46- from uipath .core .adapters import BaseAdapter , EvaluatorProtocol
48+ from uipath .core .adapters import EvaluatorProtocol
4749from uipath .core .governance .exceptions import GovernanceBlockException
4850
4951logger = 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-
10998def _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
198166class GovernanceCallbacks :
0 commit comments