Skip to content

Commit 9f4a4f1

Browse files
aditik0303claude
andcommitted
refactor(governance): migrate LlamaIndex adapter to factory-evaluator
Core PR #1761 removed BaseAdapter from uipath-core. Migrate to the factory-evaluator pattern (matching #899): - governance/adapter.py -> event_handler.py: replace the BaseAdapter subclass (name/can_handle/attach/detach) with module-level install_governance() that registers the GovernanceEventHandler on the root instrumentation dispatcher; keep the handler + callbacks. File named for its seam (the event handler), like LangChain's callbacks.py. - runtime/factory.py: new_runtime reads `evaluator` from kwargs and calls install_governance. - 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_event_handler.py): drop can_handle/ attach/detach; cover install_governance + factory wiring. ruff + mypy clean; 17 governance tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a4ce202 commit 9f4a4f1

5 files changed

Lines changed: 129 additions & 142 deletions

File tree

packages/uipath-llamaindex/pyproject.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,6 @@ register = "uipath_llamaindex.middlewares:register_middleware"
4545
[project.entry-points."uipath.runtime.factories"]
4646
llamaindex = "uipath_llamaindex.runtime:register_runtime_factory"
4747

48-
[project.entry-points."uipath.governance.adapters"]
49-
llamaindex = "uipath_llamaindex.governance:register_governance_adapter"
50-
5148
[project.urls]
5249
Homepage = "https://uipath.com"
5350
Repository = "https://github.com/UiPath/uipath-integrations-python/"
Lines changed: 10 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,20 @@
11
"""Governance integration for ``uipath-llamaindex``.
22
3-
Registers :class:`LlamaIndexAdapter` with the adapter registry in
4-
``uipath.core.adapters`` so the governance host can attach the
5-
LlamaIndex-specific governance (BEFORE_MODEL, AFTER_MODEL, TOOL_CALL) when it
6-
sees a LlamaIndex workflow/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` — registers a :class:`GovernanceEventHandler`
4+
on the LlamaIndex root instrumentation dispatcher, which governs LLM/tool events
5+
(BEFORE_MODEL, AFTER_MODEL, TOOL_CALL). Wired into a run by passing an
6+
``evaluator`` to :class:`UiPathLlamaIndexRuntimeFactory`; the factory calls
7+
:func:`install_governance`.
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 GovernanceEventHandler, LlamaIndexAdapter
24-
25-
logger = logging.getLogger(__name__)
26-
27-
_registered: bool = False
28-
29-
30-
def register_governance_adapter() -> None:
31-
"""Register :class:`LlamaIndexAdapter` 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 == "LlamaIndex" for a in registry.get_all()):
40-
_registered = True
41-
return
42-
registry.register(LlamaIndexAdapter())
43-
_registered = True
44-
logger.debug("Registered uipath-llamaindex governance adapter")
45-
46-
15+
from .event_handler import GovernanceEventHandler, install_governance
4716

4817
__all__ = [
4918
"GovernanceEventHandler",
50-
"LlamaIndexAdapter",
51-
"register_governance_adapter",
19+
"install_governance",
5220
]

packages/uipath-llamaindex/src/uipath_llamaindex/governance/adapter.py renamed to packages/uipath-llamaindex/src/uipath_llamaindex/governance/event_handler.py

Lines changed: 35 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
"""LlamaIndex adapter for UiPath governance.
1+
"""LlamaIndex governance event handler for UiPath.
22
33
Provides governance for LlamaIndex agents/workflows. Unlike the ADK / OpenAI /
4-
Agent-Framework adapters — which install per-agent callbacks or middleware —
4+
Agent-Framework integrations — which install per-agent callbacks or middleware —
55
LlamaIndex routes everything (LLM calls, tool calls) through its global
66
**instrumentation dispatcher** (the same mechanism the package already uses for
77
OpenInference tracing). So this adapter governs by registering a
@@ -13,9 +13,9 @@
1313
- ``AgentToolCallEvent`` → TOOL_CALL (tool name + arguments)
1414
1515
The dispatcher is process-global, so registration is process-wide — which fits
16-
the coded-agent model (one workflow per process). :meth:`attach` therefore
17-
returns the ``agent`` unchanged (nothing is mutated on it); the wiring lives on
18-
the dispatcher. :meth:`detach` removes the handler.
16+
the coded-agent model (one workflow per process). :func:`install_governance`
17+
therefore returns the ``agent`` unchanged (nothing is mutated on it); the wiring
18+
lives on the dispatcher.
1919
2020
LlamaIndex does **not** emit a tool-*end* instrumentation event, so AFTER_TOOL
2121
is not wired here; a tool's result is governed at the next ``LLMChatStartEvent``
@@ -25,9 +25,11 @@
2525
Chain-level boundaries (BEFORE_AGENT / AFTER_AGENT) are owned by the
2626
governance host and are intentionally not fired here.
2727
28-
Contracts and the evaluator protocol come from ``uipath-core``; this package
29-
contributes only the LlamaIndex-specific implementation and registers it with
30-
the adapter registry via the ``uipath.governance.adapters`` entry point.
28+
The evaluator protocol comes from ``uipath-core``; this package contributes
29+
only the LlamaIndex-specific wiring. Governance is installed by the runtime
30+
factory: passing an ``evaluator`` to ``new_runtime`` calls
31+
:func:`install_governance`, which registers the handler on the dispatcher. No
32+
adapter registry, no entry point, no import-time side effects.
3133
3234
Audit emission and enforcement (raising :class:`GovernanceBlockException` on
3335
DENY) are owned by the evaluator. The handler only extracts payloads and calls
@@ -54,7 +56,7 @@
5456
LLMChatStartEvent,
5557
)
5658
from pydantic import PrivateAttr
57-
from uipath.core.adapters import BaseAdapter, EvaluatorProtocol
59+
from uipath.core.adapters import EvaluatorProtocol
5860
from uipath.core.governance.exceptions import GovernanceBlockException
5961

6062
logger = logging.getLogger(__name__)
@@ -64,57 +66,31 @@
6466
_BEFORE_MODEL_TEXT_CAP = 64000
6567

6668

67-
class LlamaIndexAdapter(BaseAdapter):
68-
"""Adapter for the LlamaIndex framework.
69+
def install_governance(
70+
agent: Any,
71+
evaluator: EvaluatorProtocol,
72+
*,
73+
agent_name: str,
74+
session_id: str,
75+
) -> Any:
76+
"""Register the governance event handler on the root dispatcher.
6977
70-
Detects LlamaIndex workflows/agents and governs them by registering a
71-
:class:`GovernanceEventHandler` on the root instrumentation dispatcher.
72-
"""
73-
74-
@property
75-
def name(self) -> str:
76-
return "LlamaIndex"
77-
78-
def can_handle(self, agent: Any) -> bool:
79-
"""Return True only for a LlamaIndex ``Workflow`` (incl. agent workflows)."""
80-
try:
81-
from workflows import Workflow
82-
except ImportError:
83-
return False
84-
return isinstance(agent, Workflow)
78+
Returns the ``agent`` unchanged — LlamaIndex governance is wired on the
79+
process-global instrumentation dispatcher, not on the agent object.
80+
Idempotent: a second call is a no-op while a handler is already registered.
8581
86-
def attach(
87-
self,
88-
agent: Any,
89-
agent_id: str,
90-
session_id: str,
91-
evaluator: EvaluatorProtocol,
92-
) -> Any:
93-
"""Register the governance event handler on the root dispatcher.
94-
95-
Returns the ``agent`` unchanged — LlamaIndex governance is wired on the
96-
process-global dispatcher, not on the agent object. Idempotent: a
97-
second attach is a no-op while a handler is already registered.
98-
"""
99-
dispatcher = get_dispatcher()
100-
if any(isinstance(h, GovernanceEventHandler) for h in dispatcher.event_handlers):
101-
return agent # idempotent — already governed
102-
callbacks = GovernanceCallbacks(
103-
evaluator=evaluator, agent_name=agent_id, session_id=session_id
104-
)
105-
dispatcher.add_event_handler(GovernanceEventHandler(callbacks=callbacks))
106-
logger.debug("Registered governance event handler on LlamaIndex dispatcher")
107-
return agent
108-
109-
def detach(self, governed: Any) -> Any:
110-
"""Remove the governance event handler from the root dispatcher."""
111-
dispatcher = get_dispatcher()
112-
dispatcher.event_handlers = [
113-
h
114-
for h in dispatcher.event_handlers
115-
if not isinstance(h, GovernanceEventHandler)
116-
]
117-
return governed
82+
Called by :class:`UiPathLlamaIndexRuntimeFactory` when an ``evaluator``
83+
is supplied to ``new_runtime``.
84+
"""
85+
dispatcher = get_dispatcher()
86+
if any(isinstance(h, GovernanceEventHandler) for h in dispatcher.event_handlers):
87+
return agent # idempotent — already governed
88+
callbacks = GovernanceCallbacks(
89+
evaluator=evaluator, agent_name=agent_name, session_id=session_id
90+
)
91+
dispatcher.add_event_handler(GovernanceEventHandler(callbacks=callbacks))
92+
logger.debug("Registered governance event handler on LlamaIndex dispatcher")
93+
return agent
11894

11995

12096
class GovernanceEventHandler(BaseEventHandler):
@@ -279,5 +255,5 @@ def _coerce_args(arguments: Any) -> Dict[str, Any]:
279255
__all__: List[str] = [
280256
"GovernanceCallbacks",
281257
"GovernanceEventHandler",
282-
"LlamaIndexAdapter",
258+
"install_governance",
283259
]

packages/uipath-llamaindex/src/uipath_llamaindex/runtime/factory.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
LlamaIndexInstrumentor,
88
get_current_span,
99
)
10+
from uipath.core.adapters import EvaluatorProtocol
1011
from uipath.core.tracing import UiPathSpanUtils, UiPathTraceManager
1112
from uipath.platform.resume_triggers import UiPathResumeTriggerHandler
1213
from uipath.runtime import (
@@ -19,6 +20,7 @@
1920
from uipath.runtime.errors import UiPathErrorCategory
2021
from workflows import Workflow
2122

23+
from uipath_llamaindex.governance import install_governance
2224
from uipath_llamaindex.runtime._telemetry import (
2325
ToolCallAttributeNormalizer,
2426
)
@@ -233,6 +235,7 @@ async def _create_runtime_instance(
233235
workflow: Workflow,
234236
runtime_id: str,
235237
entrypoint: str,
238+
evaluator: EvaluatorProtocol | None = None,
236239
) -> UiPathRuntimeProtocol:
237240
"""
238241
Create a runtime instance from a workflow.
@@ -241,10 +244,19 @@ async def _create_runtime_instance(
241244
workflow: The workflow
242245
runtime_id: Unique identifier for the runtime instance
243246
entrypoint: Workflow entrypoint name
247+
evaluator: When supplied, governance is installed on the
248+
instrumentation dispatcher via :func:`install_governance`.
244249
245250
Returns:
246251
Configured runtime instance
247252
"""
253+
if evaluator is not None:
254+
install_governance(
255+
workflow,
256+
evaluator,
257+
agent_name=entrypoint,
258+
session_id=runtime_id,
259+
)
248260

249261
storage = await self._get_storage()
250262

@@ -274,6 +286,9 @@ async def new_runtime(
274286
Args:
275287
entrypoint: Workflow name from llama_index.json
276288
runtime_id: Unique identifier for the runtime instance
289+
**kwargs: Forwarded factory kwargs. Recognized: ``evaluator``
290+
(``EvaluatorProtocol``) — when present, governance is installed
291+
on the dispatcher via :func:`install_governance`.
277292
278293
Returns:
279294
Configured runtime instance with workflow
@@ -284,6 +299,7 @@ async def new_runtime(
284299
workflow=workflow,
285300
runtime_id=runtime_id,
286301
entrypoint=entrypoint,
302+
evaluator=kwargs.get("evaluator"),
287303
)
288304

289305
async def dispose(self) -> None:

0 commit comments

Comments
 (0)