Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/uipath-core/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-core"
version = "0.5.17"
version = "0.6.0"
description = "UiPath Core abstractions"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
35 changes: 35 additions & 0 deletions packages/uipath-core/src/uipath/core/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Generic adapter contracts for framework integrations.

This package holds only the abstract contracts — concrete adapter
implementations live in framework-specific plugin packages (e.g.
``uipath-langchain``, ``uipath-openai``) that target the framework they
integrate with. Plugin packages register their concrete adapters with
the global :class:`AdapterRegistry` via the
``uipath.governance.adapters`` entry-point group.

Public surface:

- :class:`BaseAdapter` – abstract base every adapter inherits from.
- :class:`GovernedAgentBase` – proxy base for governed agent wrappers.
- :class:`EvaluatorProtocol` – structural protocol the adapter expects
from any policy evaluator.
- :class:`AdapterRegistry` – ordered list of adapters that resolves
the first match for a given agent.
"""

from .base import BaseAdapter, GovernedAgentBase
from .evaluator import EvaluatorProtocol
from .registry import (
AdapterRegistry,
get_adapter_registry,
reset_adapter_registry,
)

__all__ = [
"BaseAdapter",
"GovernedAgentBase",
"EvaluatorProtocol",
"AdapterRegistry",
"get_adapter_registry",
"reset_adapter_registry",
]
107 changes: 107 additions & 0 deletions packages/uipath-core/src/uipath/core/adapters/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Base adapter contracts for framework-specific integrations.

An adapter's job:

1. Detect whether it can handle a given agent object.
2. Attach hooks to that agent (framework-specific).
3. Publish events to a policy evaluator when those hooks fire.

The evaluator subscribes to events and runs policy checks; it never
knows or cares which adapter fired the event.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Any
from uuid import uuid4

from .evaluator import EvaluatorProtocol


class BaseAdapter(ABC):
"""Base class for framework-specific governance adapters."""

#: Set to True on a catch-all adapter that should always sort last in
#: the registry. The registry uses this flag (not the class name) to
#: keep the fallback in last position when new adapters register.
is_fallback: bool = False

@property
def name(self) -> str:
"""Return adapter name for logging."""
return self.__class__.__name__

@abstractmethod
def can_handle(self, agent: Any) -> bool:
"""Return True if this adapter knows how to hook into this agent type."""

@abstractmethod
def attach(
self,
agent: Any,
agent_id: str,
session_id: str,
evaluator: EvaluatorProtocol,
) -> Any:
"""Attach governance hooks to the agent.

Args:
agent: The agent to govern.
agent_id: Unique identifier for the agent.
session_id: Session identifier for tracing.
evaluator: Policy evaluator implementing
:class:`EvaluatorProtocol`.

Returns:
A governed proxy (or the original agent with hooks installed).
"""

def detach(self, governed: Any) -> Any:
"""Detach governance and return the original agent.

Default implementation uses the public :attr:`GovernedAgentBase.unwrapped`
contract; non-proxy adapters that return the original agent from
:meth:`attach` get back ``governed`` unchanged.
"""
return getattr(governed, "unwrapped", governed)

def _generate_trace_id(self) -> str:
"""Generate a trace ID for governance events."""
return str(uuid4())


class GovernedAgentBase:
"""Base class for governed agent proxies.

Provides common functionality for all governed agents:

- Stores reference to original agent
- Forwards unknown attributes to original agent
- Tracks governance metadata
"""

def __init__(
self,
agent: Any,
adapter: BaseAdapter,
agent_id: str,
session_id: str,
evaluator: EvaluatorProtocol,
) -> None:
"""Initialize with the wrapped agent and governance metadata."""
self._agent = agent
self._adapter = adapter
self._agent_id = agent_id
self._session_id = session_id
self._evaluator = evaluator
self._trace_id = adapter._generate_trace_id()

@property
def unwrapped(self) -> Any:
"""Get the original unwrapped agent."""
return self._agent

def __getattr__(self, name: str) -> Any:
"""Forward attribute access to the original agent."""
return getattr(self._agent, name)
100 changes: 100 additions & 0 deletions packages/uipath-core/src/uipath/core/adapters/evaluator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Structural contract for the policy evaluator an adapter talks to.

Framework adapters call into a policy evaluator at each lifecycle hook.
The concrete evaluator (e.g.
:class:`uipath.core.governance.native.evaluator.GovernanceEvaluator`)
lives elsewhere; adapters depend only on this structural protocol so
they can be swapped against alternative evaluators (Microsoft AGT,
composite, …) without code change.

``EvaluatorProtocol`` is a :class:`typing.Protocol` so any class whose
methods match the signatures below satisfies the contract without
inheritance.
"""

from __future__ import annotations

from typing import Any, Protocol, runtime_checkable


@runtime_checkable
class EvaluatorProtocol(Protocol):
"""Structural protocol an adapter expects from a policy evaluator.

Return types are intentionally :class:`typing.Any`: the concrete
audit record shape lives in the plugin package that owns the
evaluator and the policy model. Adapters in that package cast the
return value back to the concrete type they know.
"""

def evaluate_before_agent(
self,
agent_input: str,
agent_name: str,
runtime_id: str,
trace_id: str,
model_name: str = "",
**kwargs: Any,
) -> Any:
"""Evaluate BEFORE_AGENT rules."""
...

def evaluate_after_agent(
self,
agent_output: str,
agent_name: str,
runtime_id: str,
trace_id: str,
**kwargs: Any,
) -> Any:
"""Evaluate AFTER_AGENT rules."""
...

def evaluate_before_model(
self,
model_input: str,
agent_name: str,
runtime_id: str,
trace_id: str,
messages: list[dict[str, Any]] | None = None,
model_name: str = "",
**kwargs: Any,
) -> Any:
"""Evaluate BEFORE_MODEL rules."""
...

def evaluate_after_model(
self,
model_output: str,
agent_name: str,
runtime_id: str,
trace_id: str,
**kwargs: Any,
) -> Any:
"""Evaluate AFTER_MODEL rules."""
...

def evaluate_tool_call(
self,
tool_name: str,
tool_args: dict[str, Any],
agent_name: str,
runtime_id: str,
trace_id: str,
session_state: dict[str, Any] | None = None,
**kwargs: Any,
) -> Any:
"""Evaluate TOOL_CALL rules."""
...

def evaluate_after_tool(
self,
tool_name: str,
tool_result: str,
agent_name: str,
runtime_id: str,
trace_id: str,
**kwargs: Any,
) -> Any:
"""Evaluate AFTER_TOOL rules."""
...
Loading
Loading