Skip to content
Open
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
5 changes: 5 additions & 0 deletions .github/workflows/lint-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ jobs:
working-directory: packages/uipath-platform
run: uv run ruff format --check .

- name: Check import contracts
if: steps.check.outputs.skip != 'true'
working-directory: packages/uipath-platform
run: uv run lint-imports

lint-uipath:
name: Lint uipath
needs: detect-changed-packages
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ wheels/

.cache/*
.cache
.import_linter_cache/
site


Expand Down
1 change: 1 addition & 0 deletions packages/uipath-platform/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pytest tests/services/test_assets_service.py # Single test file
ruff check . # Lint
ruff format --check . # Format check
mypy src tests # Type check
uv run lint-imports # Import-graph layering contract
```

No justfile exists for this package — run commands directly.
Expand Down
22 changes: 21 additions & 1 deletion packages/uipath-platform/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-platform"
version = "0.1.91"
version = "0.2.0"
description = "HTTP client library for programmatic access to UiPath Platform"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down Expand Up @@ -47,6 +47,7 @@ dev = [
"pytest-cov>=4.1.0",
"pytest-mock>=3.11.1",
"pre-commit>=4.1.0",
"import-linter>=2.0",
]

[tool.hatch.build.targets.wheel]
Expand Down Expand Up @@ -126,6 +127,25 @@ exclude_lines = [
"@(abc\\.)?abstractmethod",
]

[tool.importlinter]
root_package = "uipath.platform"
exclude_type_checking_imports = true

[[tool.importlinter.contracts]]
id = "platform-layers"
name = "uipath-platform layered architecture"
type = "layers"
containers = ["uipath.platform"]
exhaustive = true
layers = [
"_uipath : resume_triggers",
"agenthub | automation_ops | automation_tracker | connections | context_grounding | entities | external_applications | governance | guardrails | memory | pii_detection | portal | resource_catalog | semantic_proxy",
"orchestrator",
"action_center | attachments | chat | documents | identity",
"common",
"constants | errors",
]

[tool.uv]
exclude-newer = "2 days"

Expand Down
34 changes: 10 additions & 24 deletions packages/uipath-platform/src/uipath/platform/_uipath.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from functools import cached_property
from typing import Optional

from pydantic import ValidationError

from uipath.platform.automation_tracker import AutomationTrackerService

from .action_center import TasksService
Expand All @@ -12,15 +10,13 @@
from .chat import ConversationsService, UiPathLlmChatService, UiPathOpenAIService
from .common import (
ApiClient,
UiPathApiConfig,
UiPathExecutionContext,
)
from .common.auth import resolve_config_from_env
from .common.auth import build_api_config, resolve_config_from_env
from .connections import ConnectionsService
from .context_grounding import ContextGroundingService
from .documents import DocumentsService
from .entities import EntitiesService
from .errors import BaseUrlMissingError, SecretMissingError
from .external_applications import ExternalApplicationService
from .governance import GovernanceService
from .guardrails import GuardrailsService
Expand Down Expand Up @@ -60,25 +56,15 @@ def __init__(
scope: Optional[str] = None,
debug: bool = False,
) -> None:
try:
if _has_valid_client_credentials(client_id, client_secret):
assert client_id and client_secret
service = ExternalApplicationService(base_url)
token_data = service.get_token_data(client_id, client_secret, scope)
base_url, secret = service._base_url, token_data.access_token
else:
base_url, secret = resolve_config_from_env(base_url, secret)

self._config = UiPathApiConfig(
base_url=base_url,
secret=secret,
)
except ValidationError as e:
for error in e.errors():
if error["loc"][0] == "base_url":
raise BaseUrlMissingError() from e
elif error["loc"][0] == "secret":
raise SecretMissingError() from e
if _has_valid_client_credentials(client_id, client_secret):
assert client_id and client_secret
service = ExternalApplicationService(base_url)
token_data = service.get_token_data(client_id, client_secret, scope)
base_url, secret = service._base_url, token_data.access_token
else:
base_url, secret = resolve_config_from_env(base_url, secret)

self._config = build_api_config(base_url, secret)
self._execution_context = UiPathExecutionContext()

@property
Expand Down
54 changes: 0 additions & 54 deletions packages/uipath-platform/src/uipath/platform/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,34 +33,6 @@
from ._user_agent import user_agent_value
from .auth import TokenData
from .dynamic_schema import jsonschema_to_pydantic
from .interrupt_models import (
CreateBatchTransform,
CreateDeepRag,
CreateDeepRagRaw,
CreateEphemeralIndex,
CreateEphemeralIndexRaw,
CreateEscalation,
CreateTask,
DocumentExtraction,
DocumentExtractionValidation,
InvokeProcess,
InvokeProcessRaw,
InvokeSystemAgent,
WaitBatchTransform,
WaitDeepRag,
WaitDeepRagRaw,
WaitDocumentExtraction,
WaitDocumentExtractionValidation,
WaitEphemeralIndex,
WaitEphemeralIndexRaw,
WaitEscalation,
WaitIntegrationEvent,
WaitJob,
WaitJobRaw,
WaitSystemAgent,
WaitTask,
WaitUntil,
)
from .paging import PagedResult

__all__ = [
Expand All @@ -72,33 +44,7 @@
"FolderContext",
"TokenData",
"UiPathConfig",
"CreateTask",
"CreateEscalation",
"WaitEscalation",
"InvokeProcess",
"InvokeProcessRaw",
"WaitTask",
"WaitJob",
"WaitJobRaw",
"PagedResult",
"CreateDeepRag",
"CreateDeepRagRaw",
"WaitDeepRag",
"WaitDeepRagRaw",
"CreateBatchTransform",
"WaitBatchTransform",
"DocumentExtraction",
"WaitDocumentExtraction",
"InvokeSystemAgent",
"WaitSystemAgent",
"CreateEphemeralIndex",
"CreateEphemeralIndexRaw",
"WaitEphemeralIndex",
"WaitEphemeralIndexRaw",
"DocumentExtractionValidation",
"WaitDocumentExtractionValidation",
"WaitIntegrationEvent",
"WaitUntil",
"RequestSpec",
"Endpoint",
"UiPathUrl",
Expand Down
26 changes: 25 additions & 1 deletion packages/uipath-platform/src/uipath/platform/common/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
from os import environ as env
from typing import Optional

from pydantic import BaseModel
from pydantic import BaseModel, ValidationError

from uipath.platform.constants import (
ENV_BASE_URL,
ENV_UIPATH_ACCESS_TOKEN,
ENV_UNATTENDED_USER_ACCESS_TOKEN,
)
from uipath.platform.errors import BaseUrlMissingError, SecretMissingError

from ._config import UiPathApiConfig


class TokenData(BaseModel):
Expand All @@ -35,3 +38,24 @@ def resolve_config_from_env(
or env.get(ENV_UIPATH_ACCESS_TOKEN)
)
return base_url_value, secret_value


def build_api_config(
base_url: Optional[str],
secret: Optional[str],
) -> UiPathApiConfig:
"""Build a validated API config, translating missing fields into typed errors.

Raises:
BaseUrlMissingError: If ``base_url`` is missing.
SecretMissingError: If ``secret`` is missing.
"""
try:
return UiPathApiConfig(base_url=base_url, secret=secret) # type: ignore[arg-type]
except ValidationError as e:
for error in e.errors():
if error["loc"][0] == "base_url":
raise BaseUrlMissingError() from e
elif error["loc"][0] == "secret":
raise SecretMissingError() from e
raise
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Default env-driven guardrail evaluator."""

from typing import Any

from uipath.core.guardrails import GuardrailValidationResult

from ..common import UiPathExecutionContext
from ..common.auth import build_api_config, resolve_config_from_env
from ._guardrails_service import GuardrailsService
from .guardrails import BuiltInValidatorGuardrail


class DefaultGuardrailEvaluator:
"""Evaluates guardrails via a ``GuardrailsService`` configured from env vars.

Raises:
BaseUrlMissingError: If the base URL is not configured.
SecretMissingError: If the access token is not configured.
"""

def __init__(self) -> None:
base_url, secret = resolve_config_from_env(None, None)
self._service = GuardrailsService(
build_api_config(base_url, secret), UiPathExecutionContext()
)

def evaluate_guardrail(
self,
input_data: str | dict[str, Any],
guardrail: BuiltInValidatorGuardrail,
) -> GuardrailValidationResult:
"""Validate input data using the provided guardrail."""
return self._service.evaluate_guardrail(input_data, guardrail)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Internal protocol for guardrail evaluation capabilities."""

from typing import Any, Protocol

from uipath.core.guardrails import GuardrailValidationResult

from .guardrails import BuiltInValidatorGuardrail


class GuardrailEvaluator(Protocol):
"""Capability to evaluate a built-in guardrail against input data."""

def evaluate_guardrail(
self,
input_data: str | dict[str, Any],
guardrail: BuiltInValidatorGuardrail,
) -> GuardrailValidationResult:
"""Validate input data using the provided guardrail."""
...
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from uipath.core.guardrails import GuardrailValidationResult

from uipath.platform.guardrails._default_evaluator import DefaultGuardrailEvaluator
from uipath.platform.guardrails._evaluator import GuardrailEvaluator
from uipath.platform.guardrails.guardrails import BuiltInValidatorGuardrail

from .._enums import GuardrailExecutionStage
Expand Down Expand Up @@ -78,6 +80,8 @@ def get_built_in_guardrail(self, name, description, enabled_for_evals):
)
"""

_evaluator: GuardrailEvaluator | None = None

@abstractmethod
def get_built_in_guardrail(
self,
Expand Down Expand Up @@ -109,15 +113,13 @@ def run(
) -> GuardrailValidationResult:
"""Evaluate via the UiPath Guardrails API.

Lazily initialises the ``UiPath`` client on the first call and reuses
it for all subsequent invocations.
Lazily initialises the guardrail evaluator on the first call and
reuses it for all subsequent invocations.
"""
built_in = self.get_built_in_guardrail(name, description, enabled_for_evals)
if not hasattr(self, "_uipath"):
from uipath.platform import UiPath

self._uipath: Any = UiPath()
return self._uipath.guardrails.evaluate_guardrail(data, built_in)
if self._evaluator is None:
self._evaluator = DefaultGuardrailEvaluator()
return self._evaluator.evaluate_guardrail(data, built_in)


class CustomGuardrailValidator(GuardrailValidatorBase, ABC):
Expand Down
Loading
Loading