Skip to content

Commit 4a1489b

Browse files
authored
feat[PLT-105095]: changes for governance (#1700)
1 parent 9a5d2bb commit 4a1489b

19 files changed

Lines changed: 1704 additions & 7 deletions

File tree

packages/uipath-core/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-core"
3-
version = "0.5.17"
3+
version = "0.5.18"
44
description = "UiPath Core abstractions"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Generic adapter contracts for framework integrations.
2+
3+
This package holds only the abstract contracts — concrete adapter
4+
implementations live in framework-specific plugin packages (e.g.
5+
``uipath-langchain``, ``uipath-openai``) that target the framework they
6+
integrate with. Plugin packages register their concrete adapters with
7+
the global :class:`AdapterRegistry` via the
8+
``uipath.governance.adapters`` entry-point group.
9+
10+
Public surface:
11+
12+
- :class:`BaseAdapter` – abstract base every adapter inherits from.
13+
- :class:`GovernedAgentBase` – proxy base for governed agent wrappers.
14+
- :class:`EvaluatorProtocol` – structural protocol the adapter expects
15+
from any policy evaluator.
16+
- :class:`AdapterRegistry` – ordered list of adapters that resolves
17+
the first match for a given agent.
18+
"""
19+
20+
from .base import BaseAdapter, GovernedAgentBase
21+
from .evaluator import EvaluatorProtocol
22+
from .registry import (
23+
AdapterRegistry,
24+
get_adapter_registry,
25+
reset_adapter_registry,
26+
)
27+
28+
__all__ = [
29+
"BaseAdapter",
30+
"GovernedAgentBase",
31+
"EvaluatorProtocol",
32+
"AdapterRegistry",
33+
"get_adapter_registry",
34+
"reset_adapter_registry",
35+
]
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Base adapter contracts for framework-specific integrations.
2+
3+
An adapter's job:
4+
5+
1. Detect whether it can handle a given agent object.
6+
2. Attach hooks to that agent (framework-specific).
7+
3. Publish events to a policy evaluator when those hooks fire.
8+
9+
The evaluator subscribes to events and runs policy checks; it never
10+
knows or cares which adapter fired the event.
11+
"""
12+
13+
from __future__ import annotations
14+
15+
from abc import ABC, abstractmethod
16+
from typing import Any
17+
from uuid import uuid4
18+
19+
from .evaluator import EvaluatorProtocol
20+
21+
22+
class BaseAdapter(ABC):
23+
"""Base class for framework-specific governance adapters."""
24+
25+
#: Higher value = more specific = inserted earlier in the registry.
26+
#: Plugin authors should set this above ``0`` on adapters that target
27+
#: a narrower agent type than another already-registered adapter, so
28+
#: the specific one wins ``can_handle`` resolution regardless of the
29+
#: order in which plugins happen to be imported. Among adapters with
30+
#: the same priority, registration order is preserved (stable).
31+
priority: int = 0
32+
33+
#: Set to True on a catch-all adapter that should always sort last in
34+
#: the registry. The registry uses this flag (not the class name or
35+
#: :attr:`priority`) to keep the fallback in last position when new
36+
#: adapters register.
37+
is_fallback: bool = False
38+
39+
@property
40+
def name(self) -> str:
41+
"""Return adapter name for logging."""
42+
return self.__class__.__name__
43+
44+
@abstractmethod
45+
def can_handle(self, agent: Any) -> bool:
46+
"""Return True if this adapter knows how to hook into this agent type."""
47+
48+
@abstractmethod
49+
def attach(
50+
self,
51+
agent: Any,
52+
agent_id: str,
53+
session_id: str,
54+
evaluator: EvaluatorProtocol,
55+
) -> Any:
56+
"""Attach governance hooks to the agent.
57+
58+
Args:
59+
agent: The agent to govern.
60+
agent_id: Unique identifier for the agent.
61+
session_id: Session identifier for tracing.
62+
evaluator: Policy evaluator implementing
63+
:class:`EvaluatorProtocol`.
64+
65+
Returns:
66+
A governed proxy (or the original agent with hooks installed).
67+
"""
68+
69+
def detach(self, governed: Any) -> Any:
70+
"""Detach governance and return the original agent.
71+
72+
Default implementation uses the public :attr:`GovernedAgentBase.unwrapped`
73+
contract; non-proxy adapters that return the original agent from
74+
:meth:`attach` get back ``governed`` unchanged.
75+
"""
76+
return getattr(governed, "unwrapped", governed)
77+
78+
def _generate_trace_id(self) -> str:
79+
"""Generate a trace ID for governance events."""
80+
return str(uuid4())
81+
82+
83+
class GovernedAgentBase:
84+
"""Base class for governed agent proxies.
85+
86+
Provides common functionality for all governed agents:
87+
88+
- Stores reference to original agent
89+
- Forwards unknown attributes to original agent
90+
- Tracks governance metadata
91+
"""
92+
93+
def __init__(
94+
self,
95+
agent: Any,
96+
adapter: BaseAdapter,
97+
agent_id: str,
98+
session_id: str,
99+
evaluator: EvaluatorProtocol,
100+
) -> None:
101+
"""Initialize with the wrapped agent and governance metadata."""
102+
self._agent = agent
103+
self._adapter = adapter
104+
self._agent_id = agent_id
105+
self._session_id = session_id
106+
self._evaluator = evaluator
107+
self._trace_id = adapter._generate_trace_id()
108+
109+
@property
110+
def unwrapped(self) -> Any:
111+
"""Get the original unwrapped agent."""
112+
return self._agent
113+
114+
def __getattr__(self, name: str) -> Any:
115+
"""Forward attribute access to the original agent."""
116+
return getattr(self._agent, name)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Structural contract for the policy evaluator an adapter talks to.
2+
3+
Framework adapters call into a policy evaluator at each lifecycle hook.
4+
Concrete evaluator implementations (the native runtime evaluator, a
5+
Microsoft AGT bridge, a composite, …) live in packages outside
6+
``uipath-core`` — adapters depend only on this structural protocol so
7+
they can be swapped against any of them without code change.
8+
9+
``EvaluatorProtocol`` is a :class:`typing.Protocol` so any class whose
10+
methods match the signatures below satisfies the contract without
11+
inheritance.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
from typing import Any, Protocol, runtime_checkable
17+
18+
19+
@runtime_checkable
20+
class EvaluatorProtocol(Protocol):
21+
"""Structural protocol an adapter expects from a policy evaluator.
22+
23+
Return types are intentionally :class:`typing.Any`: the concrete
24+
audit record shape lives in the plugin package that owns the
25+
evaluator and the policy model. Adapters in that package cast the
26+
return value back to the concrete type they know.
27+
"""
28+
29+
def evaluate_before_agent(
30+
self,
31+
agent_input: str,
32+
agent_name: str,
33+
runtime_id: str,
34+
trace_id: str,
35+
model_name: str = "",
36+
**kwargs: Any,
37+
) -> Any:
38+
"""Evaluate BEFORE_AGENT rules."""
39+
...
40+
41+
def evaluate_after_agent(
42+
self,
43+
agent_output: str,
44+
agent_name: str,
45+
runtime_id: str,
46+
trace_id: str,
47+
**kwargs: Any,
48+
) -> Any:
49+
"""Evaluate AFTER_AGENT rules."""
50+
...
51+
52+
def evaluate_before_model(
53+
self,
54+
model_input: str,
55+
agent_name: str,
56+
runtime_id: str,
57+
trace_id: str,
58+
messages: list[dict[str, Any]] | None = None,
59+
model_name: str = "",
60+
**kwargs: Any,
61+
) -> Any:
62+
"""Evaluate BEFORE_MODEL rules."""
63+
...
64+
65+
def evaluate_after_model(
66+
self,
67+
model_output: str,
68+
agent_name: str,
69+
runtime_id: str,
70+
trace_id: str,
71+
**kwargs: Any,
72+
) -> Any:
73+
"""Evaluate AFTER_MODEL rules."""
74+
...
75+
76+
def evaluate_tool_call(
77+
self,
78+
tool_name: str,
79+
tool_args: dict[str, Any],
80+
agent_name: str,
81+
runtime_id: str,
82+
trace_id: str,
83+
session_state: dict[str, Any] | None = None,
84+
**kwargs: Any,
85+
) -> Any:
86+
"""Evaluate TOOL_CALL rules."""
87+
...
88+
89+
def evaluate_after_tool(
90+
self,
91+
tool_name: str,
92+
tool_result: str,
93+
agent_name: str,
94+
runtime_id: str,
95+
trace_id: str,
96+
**kwargs: Any,
97+
) -> Any:
98+
"""Evaluate AFTER_TOOL rules."""
99+
...

0 commit comments

Comments
 (0)