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
2 changes: 1 addition & 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.73"
version = "0.1.74"
description = "HTTP client library for programmatic access to UiPath Platform"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"""

import logging
import re
from typing import Any, Dict, List, Optional, Type

from httpx import Response
Expand All @@ -24,6 +25,8 @@
from ..common._bindings import _resource_overwrites
from ..common._config import UiPathApiConfig
from ..common._execution_context import UiPathExecutionContext
from ..common._folder_context import header_folder
from ..common._models import Endpoint, RequestSpec
from ..orchestrator._folder_service import FolderService
from ._entity_data_service import EntityDataService, FileContent
from ._entity_resolution import (
Expand Down Expand Up @@ -58,6 +61,12 @@

logger = logging.getLogger(__name__)

# Ontology name contract (QueryEngine OntologyController): lowercase, starts
# with a letter, max 64 chars. The name becomes a URL path segment.
_ONTOLOGY_NAME_RE = re.compile(r"^[a-z][a-z0-9-]{0,63}$")
# Allowed ontology component file types (also URL path segments).
_ONTOLOGY_FILE_TYPES = frozenset({"owl", "r2rml", "shacl", "summary", "context"})


class EntitiesService(BaseService):
"""Service for managing UiPath Data Service entities.
Expand Down Expand Up @@ -1100,6 +1109,68 @@ async def delete_record_async(self, entity_key: str, record_id: str) -> None:
"""
await self._data.delete_record_async(entity_key, record_id)

async def get_ontology_file_async(
self,
Comment on lines +1112 to +1113
ontology_name: str,
file_type: str = "owl",
folder_key: Optional[str] = None,
) -> Dict[str, Any]:
Comment thread
sankalp-uipath marked this conversation as resolved.
"""Fetch one file of an ontology from Data Fabric.

!!! warning "Preview Feature"
This method is currently experimental. Behavior and parameters are
subject to change in future versions.

Ontologies are served by the same QueryEngine service as entity SQL
queries, under ``datafabric_/api/ontologies``. The JSON wrapper is
requested so the result is notation-agnostic — the ``owl`` file content
may be Turtle or OWL Functional Notation.

Args:
ontology_name: Ontology name. Validated against the QE name contract.
file_type: One of owl, r2rml, shacl, summary, context.
folder_key: Folder the ontology lives in.

Returns:
Dict[str, Any]: The file record (e.g. ``content``, ``mediaType``).

Raises:
ValueError: If the ontology name or file type is invalid.
"""
self._validate_ontology_name(ontology_name)
self._validate_file_type(file_type)
spec = self._ontology_file_spec(ontology_name, file_type)
headers = {"Accept": "application/json", **header_folder(folder_key, None)}
response = await self.request_async(spec.method, spec.endpoint, headers=headers)
return response.json()
Comment thread
sankalp-uipath marked this conversation as resolved.

@staticmethod
def _validate_ontology_name(ontology_name: str) -> None:
"""Validate the ontology name before it becomes a URL path segment."""
if not _ONTOLOGY_NAME_RE.match(ontology_name or ""):
raise ValueError(
f"Invalid ontology name {ontology_name!r}. "
"Must match ^[a-z][a-z0-9-]{0,63}$."
)

@staticmethod
def _validate_file_type(file_type: str) -> None:
"""Validate the file type before it becomes a URL path segment."""
if file_type not in _ONTOLOGY_FILE_TYPES:
allowed = ", ".join(sorted(_ONTOLOGY_FILE_TYPES))
raise ValueError(
f"Invalid ontology file type {file_type!r}. One of: {allowed}."
)

@staticmethod
def _ontology_file_spec(ontology_name: str, file_type: str) -> RequestSpec:
return RequestSpec(
method="GET",
endpoint=Endpoint(
f"datafabric_/api/ontologies/{ontology_name}/files/{file_type}"
),
)

@traced(name="entity_record_insert_batch", run_type="uipath")
def insert_records(
self,
Expand Down
75 changes: 75 additions & 0 deletions packages/uipath-platform/tests/services/test_entities_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2647,3 +2647,78 @@ def test_5xx_with_batch_shape_still_propagates(
entity_key=str(entity_key),
records=[{"name": "x"}],
)


class TestGetOntologyFileAsync:
"""Tests for EntitiesService.get_ontology_file_async."""

@pytest.mark.anyio
async def test_builds_endpoint_and_folder_header(
self, service: EntitiesService
) -> None:
response = MagicMock()
response.json.return_value = {"content": "OWL", "mediaType": "text/plain"}
service.request_async = AsyncMock(return_value=response) # type: ignore[method-assign]

result = await service.get_ontology_file_async(
"library", "owl", folder_key="folder-1"
)

assert result == {"content": "OWL", "mediaType": "text/plain"}
service.request_async.assert_called_once()
call = service.request_async.call_args
method, endpoint = call.args[0], call.args[1]
headers = call.kwargs["headers"]
assert method == "GET"
assert str(endpoint) == "/datafabric_/api/ontologies/library/files/owl"
assert headers["Accept"] == "application/json"
assert headers["x-uipath-folderkey"] == "folder-1"

@pytest.mark.anyio
async def test_no_folder_header_when_folder_key_none(
self, service: EntitiesService
) -> None:
response = MagicMock()
response.json.return_value = {"content": "OWL", "mediaType": "text/plain"}
service.request_async = AsyncMock(return_value=response) # type: ignore[method-assign]

await service.get_ontology_file_async("library")

headers = service.request_async.call_args.kwargs["headers"]
assert "x-uipath-folderkey" not in headers

@pytest.mark.anyio
@pytest.mark.parametrize(
"file_type", ["owl", "r2rml", "shacl", "summary", "context"]
)
async def test_accepts_allowed_file_types(
self, service: EntitiesService, file_type: str
) -> None:
response = MagicMock()
response.json.return_value = {"content": "x"}
service.request_async = AsyncMock(return_value=response) # type: ignore[method-assign]

await service.get_ontology_file_async("library", file_type)

endpoint = service.request_async.call_args.args[1]
assert str(endpoint) == f"/datafabric_/api/ontologies/library/files/{file_type}"

@pytest.mark.anyio
async def test_rejects_invalid_ontology_name(
self, service: EntitiesService
) -> None:
service.request_async = AsyncMock() # type: ignore[method-assign]

with pytest.raises(ValueError, match="Invalid ontology name"):
await service.get_ontology_file_async("Bad_Name") # uppercase + underscore

service.request_async.assert_not_called()

@pytest.mark.anyio
async def test_rejects_invalid_file_type(self, service: EntitiesService) -> None:
service.request_async = AsyncMock() # type: ignore[method-assign]

with pytest.raises(ValueError, match="Invalid ontology file type"):
await service.get_ontology_file_async("library", "exe")

service.request_async.assert_not_called()
2 changes: 1 addition & 1 deletion packages/uipath-platform/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/uipath/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[project]
name = "uipath"
version = "2.11.9"
version = "2.11.10"
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
dependencies = [
"uipath-core>=0.5.21, <0.6.0",
"uipath-runtime>=0.11.0, <0.12.0",
"uipath-platform>=0.1.73, <0.2.0",
"uipath-platform>=0.1.74, <0.2.0",
"click>=8.3.1",
"httpx>=0.28.1",
"pyjwt>=2.10.1",
Expand Down
35 changes: 34 additions & 1 deletion packages/uipath/src/uipath/agent/models/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class AgentResourceType(str, CaseInsensitiveEnum):
ESCALATION = "escalation"
MCP = "mcp"
A2A = "a2a"
ONTOLOGY = "ontology"
UNKNOWN = "unknown" # fallback branch discriminator


Expand Down Expand Up @@ -341,6 +342,7 @@ class BaseAgentResourceConfig(BaseCfg):
AgentResourceType.ESCALATION,
AgentResourceType.MCP,
AgentResourceType.A2A,
AgentResourceType.ONTOLOGY,
AgentResourceType.UNKNOWN,
] = Field(alias="$resourceType")

Expand Down Expand Up @@ -427,6 +429,27 @@ class AgentContextSettings(BaseCfg):
)


class AgentOntologyResourceConfig(BaseAgentResourceConfig):
"""A Data Fabric ontology as a standalone, first-class agent resource.

Promoted from a nested context field to its own ``resources[]`` entry so a
single ontology can be defined once and referenced by one or more Data
Fabric contexts (see ``AgentContextResourceConfig.ontology_refs``). Each
ontology carries its own ``folderId`` so it resolves from its own folder,
independent of the entities (which may also span several folders).

``name`` (inherited) is the ontology name used both to reference this
resource from a context and to fetch it from the QueryEngine ontology API.
"""

resource_type: Literal[AgentResourceType.ONTOLOGY] = Field(
alias="$resourceType", default=AgentResourceType.ONTOLOGY, frozen=True
)
id: Optional[str] = Field(None, alias="id")
ontology_key: Optional[str] = Field(None, alias="referenceKey")
folder_key: str = Field(..., alias="folderId")


class AgentContextResourceConfig(BaseAgentResourceConfig):
"""Agent context resource configuration model."""

Expand All @@ -440,6 +463,15 @@ class AgentContextResourceConfig(BaseAgentResourceConfig):
None, description="Context settings"
)
entity_set: Optional[List[DataFabricEntityItem]] = Field(None, alias="entitySet")
ontology_refs: Optional[List[str]] = Field(
None,
alias="ontologyRefs",
description=(
"Names of standalone ontology resources "
"(AgentOntologyResourceConfig) this context is grounded by. "
"Resolved against the agent's resources at runtime."
),
)
Comment on lines 465 to +474
argument_properties: Dict[str, AgentToolArgumentProperties] = Field(
{}, alias="argumentProperties"
)
Expand Down Expand Up @@ -1145,6 +1177,7 @@ class AgentUnknownToolResourceConfig(BaseAgentToolResourceConfig):
EscalationResourceConfig, # nested discrim on 'escalation_type'
AgentMcpResourceConfig,
AgentA2aResourceConfig,
AgentOntologyResourceConfig,
AgentUnknownResourceConfig, # when parent sets resource_type="Unknown"
],
Field(discriminator="resource_type"),
Expand Down Expand Up @@ -1498,7 +1531,7 @@ def _normalize_guardrails(v: Dict[str, Any]) -> None:

@staticmethod
def _normalize_resources(v: Dict[str, Any]) -> None:
KNOWN_RES = {"tool", "context", "escalation", "mcp", "a2a"}
KNOWN_RES = {"tool", "context", "escalation", "mcp", "a2a", "ontology"}
TOOL_MAP = {
"agent": "Agent",
"process": "Process",
Expand Down
Loading
Loading