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.2.35"
version = "0.2.36"
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 @@ -20,6 +20,9 @@ class UiPathEndpoints(Enum):
AH_VENDOR_COMPLETION_ENDPOINT = (
"agenthub_/llm/raw/vendor/{vendor}/model/{model}/completions"
)
AH_VENDOR_DECISION_ENDPOINT = (
"agenthub_/llm/raw/vendor/{vendor}/model/{model}/decisions"
)
AH_CAPABILITIES_ENDPOINT = "agenthub_/llm/api/capabilities"
AH_DISCOVERY_ENDPOINT = "agenthub_/llm/api/discovery"

Expand All @@ -29,6 +32,9 @@ class UiPathEndpoints(Enum):
OR_VENDOR_COMPLETION_ENDPOINT = (
"orchestrator_/llm/raw/vendor/{vendor}/model/{model}/completions"
)
OR_VENDOR_DECISION_ENDPOINT = (
"orchestrator_/llm/raw/vendor/{vendor}/model/{model}/decisions"
)
OR_CAPABILITIES_ENDPOINT = "orchestrator_/llm/api/capabilities"
OR_DISCOVERY_ENDPOINT = "orchestrator_/llm/api/discovery"

Expand Down Expand Up @@ -61,6 +67,7 @@ class EndpointManager:
get_normalized_endpoint(): Returns the appropriate normalized completion endpoint.
get_embeddings_endpoint(): Returns the appropriate embeddings endpoint.
get_vendor_endpoint(): Returns the appropriate vendor completion endpoint.
get_vendor_decisions_endpoint(): Returns the appropriate vendor decisions endpoint.
All endpoint methods automatically select the best available endpoint using the fallback order,
unless overridden by the UIPATH_LLM_SERVICE environment variable.
""" # noqa: D205
Expand Down Expand Up @@ -210,3 +217,11 @@ def get_vendor_endpoint(cls) -> str:
UiPathEndpoints.AH_VENDOR_COMPLETION_ENDPOINT,
UiPathEndpoints.OR_VENDOR_COMPLETION_ENDPOINT,
)

@classmethod
def get_vendor_decisions_endpoint(cls) -> str:
"""Get the vendor decisions endpoint (typed classification decisions)."""
return cls._select_endpoint(
UiPathEndpoints.AH_VENDOR_DECISION_ENDPOINT,
UiPathEndpoints.OR_VENDOR_DECISION_ENDPOINT,
)
32 changes: 32 additions & 0 deletions packages/uipath-platform/tests/common/test_endpoints_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import pytest

from uipath.platform.common._endpoints_manager import EndpointManager


@pytest.mark.parametrize(
("service", "expected"),
[
("agenthub", "agenthub_/llm/raw/vendor/{vendor}/model/{model}/decisions"),
(
"orchestrator",
"orchestrator_/llm/raw/vendor/{vendor}/model/{model}/decisions",
),
],
)
def test_vendor_decisions_endpoint_follows_the_llm_service(
monkeypatch: pytest.MonkeyPatch, service: str, expected: str
) -> None:
monkeypatch.setenv("UIPATH_LLM_SERVICE", service)

assert EndpointManager.get_vendor_decisions_endpoint() == expected


def test_vendor_completion_endpoint_is_unchanged(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("UIPATH_LLM_SERVICE", "agenthub")

assert (
EndpointManager.get_vendor_endpoint()
== "agenthub_/llm/raw/vendor/{vendor}/model/{model}/completions"
)
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.

2 changes: 1 addition & 1 deletion packages/uipath/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath"
version = "2.14.36"
version = "2.14.37"
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"
Expand Down
160 changes: 122 additions & 38 deletions packages/uipath/src/uipath/agent/models/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class AgentInternalToolType(str, CaseInsensitiveEnum):
BATCH_TRANSFORM = "batch-transform"
HTTP_REQUEST = "http-request"
CREATE_FILE = "create-file"
JEV_CLASSIFIER = "jev-classifier"
CLASSIFIER = "classifier"


class AgentEscalationRecipientType(str, CaseInsensitiveEnum):
Expand Down Expand Up @@ -1092,13 +1092,13 @@ class AgentInternalCreateFileToolProperties(BaseResourceProperties):
)


class AgentInternalJevClassifierToolProperties(BaseResourceProperties):
"""Agent internal Jev classifier tool properties model."""
class AgentInternalClassifierToolProperties(BaseResourceProperties):
"""Agent internal classifier tool properties model."""

tool_type: Literal[AgentInternalToolType.JEV_CLASSIFIER] = Field(
alias="toolType", default=AgentInternalToolType.JEV_CLASSIFIER, frozen=True
tool_type: Literal[AgentInternalToolType.CLASSIFIER] = Field(
alias="toolType", default=AgentInternalToolType.CLASSIFIER, frozen=True
)
settings: AgentInternalJevClassifierSettings = Field(..., alias="settings")
settings: AgentInternalClassifierSettings = Field(..., alias="settings")


AgentInternalToolProperties = Annotated[
Expand All @@ -1108,7 +1108,7 @@ class AgentInternalJevClassifierToolProperties(BaseResourceProperties):
AgentInternalBatchTransformToolProperties,
AgentInternalHttpRequestToolProperties,
AgentInternalCreateFileToolProperties,
AgentInternalJevClassifierToolProperties,
AgentInternalClassifierToolProperties,
],
Field(discriminator="tool_type"),
_case_insensitive_enum_validator("tool_type", AgentInternalToolType, "toolType"),
Expand Down Expand Up @@ -1144,38 +1144,31 @@ class AgentInternalBatchTransformSettings(BaseCfg):
)


JEV_NAME_PATTERN = r"^[A-Za-z_][A-Za-z0-9_]{0,63}$"
# Question names are the keys of the answers in the tool output, for every provider.
CLASSIFIER_QUESTION_NAME_PATTERN = r"^[A-Za-z_][A-Za-z0-9_]{0,63}$"


class JevQuestionType(str, CaseInsensitiveEnum):
"""Jev question type enumeration (matches TypeSafe's question types)."""
class ClassifierProvider(str, CaseInsensitiveEnum):
"""The classification API a classifier tool calls, implied by its model."""

CHOICE = "choice"
SCORE = "score"
NOUL = "noul"
TYPESAFE = "typesafe" # TypeSafe AI's Jev models
OPENAI = "openai" # OpenAI's Decisions API


class JevChoiceOption(BaseCfg):
"""An option of a Jev choice question."""
class BaseClassifierQuestion(BaseCfg):
"""Common fields of a classifier question.

name: str = Field(..., min_length=1)
description: Optional[str] = Field(None)


class BaseJevQuestion(BaseCfg):
"""Common fields of a Jev question.

A question carries only the fields of its type: ``options`` belong to choice
questions, ``levels`` to score questions and ``criteria`` to noul questions.
A question carries only the fields of its provider and type: ``foreign_fields``
lists the fields of the other types (and of the other provider) it rejects.
"""

foreign_fields: ClassVar[tuple[str, ...]] = ()

name: str = Field(..., pattern=JEV_NAME_PATTERN)
name: str = Field(..., pattern=CLASSIFIER_QUESTION_NAME_PATTERN)
instructions: str = Field(..., min_length=1)

@model_validator(mode="after")
def _only_own_fields(self) -> "BaseJevQuestion":
def _only_own_fields(self) -> "BaseClassifierQuestion":
extra = self.model_extra or {}
present = [f for f in self.foreign_fields if extra.get(f) is not None]
if present:
Expand All @@ -1185,10 +1178,28 @@ def _only_own_fields(self) -> "BaseJevQuestion":
return self


class JevChoiceQuestion(BaseJevQuestion):
# --- Jev (TypeSafe) questions ---


class JevQuestionType(str, CaseInsensitiveEnum):
"""Jev question type enumeration (matches TypeSafe's question types)."""

CHOICE = "choice"
SCORE = "score"
NOUL = "noul"


class JevChoiceOption(BaseCfg):
"""An option of a Jev choice question."""

name: str = Field(..., min_length=1)
description: Optional[str] = Field(None)


class JevChoiceQuestion(BaseClassifierQuestion):
"""Jev question selecting one of a set of options."""

foreign_fields: ClassVar[tuple[str, ...]] = ("levels", "criteria")
foreign_fields: ClassVar[tuple[str, ...]] = ("levels", "criteria", "choices")

type: Literal[JevQuestionType.CHOICE] = JevQuestionType.CHOICE
options: List[JevChoiceOption] = Field(..., min_length=2, max_length=255)
Expand All @@ -1201,10 +1212,10 @@ def _unique_option_names(self) -> "JevChoiceQuestion":
return self


class JevScoreQuestion(BaseJevQuestion):
class JevScoreQuestion(BaseClassifierQuestion):
"""Jev question placing the input on an ordered scale of levels."""

foreign_fields: ClassVar[tuple[str, ...]] = ("options", "criteria")
foreign_fields: ClassVar[tuple[str, ...]] = ("options", "criteria", "choices")

type: Literal[JevQuestionType.SCORE] = JevQuestionType.SCORE
levels: List[Annotated[str, Field(min_length=1)]] = Field(
Expand All @@ -1221,10 +1232,10 @@ class JevNoulCriteria(BaseModel):
false: Optional[str] = Field(None)


class JevNoulQuestion(BaseJevQuestion):
class JevNoulQuestion(BaseClassifierQuestion):
"""Jev yes/no question answered with a probability."""

foreign_fields: ClassVar[tuple[str, ...]] = ("options", "levels")
foreign_fields: ClassVar[tuple[str, ...]] = ("options", "levels", "choices")

type: Literal[JevQuestionType.NOUL] = JevQuestionType.NOUL
criteria: Optional[JevNoulCriteria] = Field(None)
Expand All @@ -1237,15 +1248,88 @@ class JevNoulQuestion(BaseJevQuestion):
]


class AgentInternalJevClassifierSettings(BaseCfg):
"""Agent internal Jev classifier tool settings model.
# --- OpenAI Decisions questions (matches the Decisions API request) ---


class DecisionsQuestionType(str, CaseInsensitiveEnum):
"""OpenAI Decisions question type enumeration."""

CHOICE = "choice"
SCORE = "score"
PREDICATE = "predicate"


class DecisionsChoice(BaseCfg):
"""A choice of a Decisions choice question."""

value: str = Field(..., min_length=1)
description: Optional[str] = Field(None)


class DecisionsScoreLevel(BaseCfg):
"""A level of a Decisions score question."""

label: str = Field(..., min_length=1)
description: Optional[str] = Field(None)


class DecisionsChoiceQuestion(BaseClassifierQuestion):
"""Decisions question selecting one of a set of choices."""

foreign_fields: ClassVar[tuple[str, ...]] = ("levels", "options", "criteria")

type: Literal[DecisionsQuestionType.CHOICE] = DecisionsQuestionType.CHOICE
choices: List[DecisionsChoice] = Field(..., min_length=2)

@model_validator(mode="after")
def _unique_choice_values(self) -> "DecisionsChoiceQuestion":
values = [choice.value for choice in self.choices]
if len(values) != len(set(values)):
raise ValueError(f"Question '{self.name}' has duplicate choice values")
return self


class DecisionsScoreQuestion(BaseClassifierQuestion):
"""Decisions question placing the input on levels ordered lowest to highest."""

foreign_fields: ClassVar[tuple[str, ...]] = ("choices", "options", "criteria")

type: Literal[DecisionsQuestionType.SCORE] = DecisionsQuestionType.SCORE
levels: List[DecisionsScoreLevel] = Field(..., min_length=2)


class DecisionsPredicateQuestion(BaseClassifierQuestion):
"""Decisions question estimating the probability that a condition is true."""

foreign_fields: ClassVar[tuple[str, ...]] = (
"choices",
"levels",
"options",
"criteria",
)

type: Literal[DecisionsQuestionType.PREDICATE] = DecisionsQuestionType.PREDICATE


DecisionsQuestion = Annotated[
Union[DecisionsChoiceQuestion, DecisionsScoreQuestion, DecisionsPredicateQuestion],
Field(discriminator="type"),
_case_insensitive_enum_validator("type", DecisionsQuestionType),
]

ClassifierQuestion = Union[JevQuestion, DecisionsQuestion]


class AgentInternalClassifierSettings(BaseCfg):
"""Agent internal classifier tool settings model.

Only the configuration that is never a call argument: the ``state`` and
``questions`` the tool sends to Jev are declared in the resource's
``inputSchema`` and supplied through its ``argumentProperties``. The model
is required: there is no default Jev model.
Only the configuration that is never a call argument: the input and the
``questions`` the tool sends are declared in the resource's ``inputSchema``
and supplied through its ``argumentProperties``, in the shapes of the
provider. The provider and the model are required: there is no default.
"""

provider: ClassifierProvider = Field(...)
model: str = Field(..., min_length=1)


Expand Down
Loading
Loading