From 49088481a37219f59091552b235558e85103dcf6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 08:20:10 +0000 Subject: [PATCH 1/2] feat(agent): unify the jev classifier into a classifier tool with typesafe and openai providers The `jev-classifier` internal tool type becomes `classifier`. Its settings hold the model and its provider (`typesafe` for Jev, `openai` for the Decisions API), both required. The question models gain the OpenAI Decisions shapes (choice with choices, score with labeled levels, predicate) next to the Jev ones, and each question rejects the fields of the other types and of the other provider. Breaking for the unreleased Jev tool: `jev-classifier` no longer parses. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01F6qDf7FQSgba7JrYH4j6jw --- packages/uipath/pyproject.toml | 4 +- .../uipath/src/uipath/agent/models/agent.py | 160 ++++++++++++---- ...sifier_tool.py => test_classifier_tool.py} | 181 ++++++++++++++---- packages/uipath/uv.lock | 4 +- 4 files changed, 267 insertions(+), 82 deletions(-) rename packages/uipath/tests/agent/models/{test_jev_classifier_tool.py => test_classifier_tool.py} (56%) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index c8ae36006..03591f19e 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "uipath" -version = "2.14.37" +version = "2.14.38" 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.33, <0.6.0", "uipath-runtime>=0.13.5, <0.14.0", - "uipath-platform>=0.2.34, <0.3.0", + "uipath-platform>=0.2.37, <0.3.0", "uipath-ipc>=2.5.1, <2.6.0", "click>=8.3.3, <9.0.0", "httpx>=0.28.1", diff --git a/packages/uipath/src/uipath/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index fab429836..4f0ce3691 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -131,7 +131,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): @@ -1115,13 +1115,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[ @@ -1131,7 +1131,7 @@ class AgentInternalJevClassifierToolProperties(BaseResourceProperties): AgentInternalBatchTransformToolProperties, AgentInternalHttpRequestToolProperties, AgentInternalCreateFileToolProperties, - AgentInternalJevClassifierToolProperties, + AgentInternalClassifierToolProperties, ], Field(discriminator="tool_type"), _case_insensitive_enum_validator("tool_type", AgentInternalToolType, "toolType"), @@ -1167,38 +1167,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: @@ -1208,10 +1201,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) @@ -1224,10 +1235,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( @@ -1244,10 +1255,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) @@ -1260,15 +1271,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) diff --git a/packages/uipath/tests/agent/models/test_jev_classifier_tool.py b/packages/uipath/tests/agent/models/test_classifier_tool.py similarity index 56% rename from packages/uipath/tests/agent/models/test_jev_classifier_tool.py rename to packages/uipath/tests/agent/models/test_classifier_tool.py index c5e0f0927..3263a69e2 100644 --- a/packages/uipath/tests/agent/models/test_jev_classifier_tool.py +++ b/packages/uipath/tests/agent/models/test_classifier_tool.py @@ -4,13 +4,18 @@ from pydantic import TypeAdapter, ValidationError from uipath.agent.models.agent import ( - AgentInternalJevClassifierSettings, - AgentInternalJevClassifierToolProperties, + AgentInternalClassifierSettings, + AgentInternalClassifierToolProperties, AgentInternalToolResourceConfig, AgentInternalToolType, AgentToolArgumentArgumentProperties, AgentToolObjectBuilderArgumentProperties, AgentToolStaticArgumentProperties, + ClassifierProvider, + DecisionsChoiceQuestion, + DecisionsPredicateQuestion, + DecisionsQuestion, + DecisionsScoreQuestion, JevChoiceQuestion, JevNoulCriteria, JevNoulQuestion, @@ -23,7 +28,7 @@ def _resource(**settings: Any) -> dict[str, Any]: return { "$resourceType": "tool", "id": "3f1c2a3e-0000-0000-0000-000000000001", - "name": "Jev Classifier", + "name": "Classifier", "description": "Classify support tickets", "type": "Internal", "referenceKey": None, @@ -39,7 +44,7 @@ def _resource(**settings: Any) -> dict[str, Any]: "outputSchema": {"type": "object", "properties": {}}, "settings": {}, "argumentProperties": {}, - "properties": {"toolType": "jev-classifier", "settings": settings}, + "properties": {"toolType": "classifier", "settings": settings}, } @@ -62,6 +67,34 @@ def _resource(**settings: Any) -> dict[str, Any]: _QUESTION: TypeAdapter[JevQuestion] = TypeAdapter(JevQuestion) +DECISIONS_CHOICE = { + "name": "department", + "type": "choice", + "instructions": "Which department should handle this complaint?", + "choices": [ + {"value": "billing", "description": "Payments, invoices, and refunds."}, + {"value": "technical", "description": None}, + ], +} +DECISIONS_SCORE = { + "name": "frustration", + "type": "score", + "instructions": "How frustrated the customer appears", + "levels": [ + {"label": "Calm", "description": "No frustration"}, + {"label": "Very angry"}, + ], +} +DECISIONS_PREDICATE = { + "name": "is_urgent", + "type": "predicate", + "instructions": "The customer needs an answer today.", +} + +_DECISIONS_QUESTION: TypeAdapter[DecisionsQuestion] = TypeAdapter(DecisionsQuestion) + +JEV = {"provider": "typesafe", "model": "jev-1.13.0"} + def _validate(data: dict[str, Any]) -> AgentInternalToolResourceConfig: return AgentInternalToolResourceConfig.model_validate(data) @@ -69,57 +102,64 @@ def _validate(data: dict[str, Any]) -> AgentInternalToolResourceConfig: def _settings( resource: AgentInternalToolResourceConfig, -) -> AgentInternalJevClassifierSettings: - assert isinstance(resource.properties, AgentInternalJevClassifierToolProperties) +) -> AgentInternalClassifierSettings: + assert isinstance(resource.properties, AgentInternalClassifierToolProperties) return resource.properties.settings -def test_settings_hold_only_the_model() -> None: - resource = _validate(_resource(model="jev-1.13.0")) +@pytest.mark.parametrize( + ("settings", "provider"), + [ + pytest.param(JEV, ClassifierProvider.TYPESAFE, id="typesafe"), + pytest.param( + {"provider": "openai", "model": "gpt-6-luna"}, + ClassifierProvider.OPENAI, + id="openai", + ), + ], +) +def test_settings_hold_the_provider_and_the_model( + settings: dict[str, Any], provider: ClassifierProvider +) -> None: + resource = _validate(_resource(**settings)) - assert isinstance(resource.properties, AgentInternalJevClassifierToolProperties) - assert resource.properties.tool_type == AgentInternalToolType.JEV_CLASSIFIER - assert _settings(resource).model == "jev-1.13.0" - assert _settings(resource).model_dump() == {"model": "jev-1.13.0"} + assert isinstance(resource.properties, AgentInternalClassifierToolProperties) + assert resource.properties.tool_type == AgentInternalToolType.CLASSIFIER + assert _settings(resource).provider is provider + assert _settings(resource).model == settings["model"] @pytest.mark.parametrize( "settings", - [pytest.param({}, id="missing"), pytest.param({"model": ""}, id="empty")], + [ + pytest.param({"provider": "typesafe"}, id="missing-model"), + pytest.param({"provider": "typesafe", "model": ""}, id="empty-model"), + pytest.param({"model": "jev-1.13.0"}, id="missing-provider"), + pytest.param({"provider": "anthropic", "model": "x"}, id="unknown-provider"), + ], ) -def test_model_is_required(settings: dict[str, Any]) -> None: - # No default Jev model: a tool without one fails to load. - with pytest.raises(ValidationError, match="model"): +def test_provider_and_model_are_required(settings: dict[str, Any]) -> None: + # No default provider or model: a tool without them fails to load. + with pytest.raises(ValidationError): _validate(_resource(**settings)) -def test_tool_type_is_case_insensitive() -> None: - data = _resource(model="jev-1.13.0") - data["properties"]["toolType"] = "Jev-Classifier" +def test_tool_type_and_provider_are_case_insensitive() -> None: + data = _resource(provider="OpenAI", model="gpt-6-luna") + data["properties"]["toolType"] = "Classifier" - assert isinstance( - _validate(data).properties, AgentInternalJevClassifierToolProperties - ) + resource = _validate(data) + assert isinstance(resource.properties, AgentInternalClassifierToolProperties) + assert _settings(resource).provider is ClassifierProvider.OPENAI -def test_legacy_state_and_questions_settings_are_ignored_extras() -> None: - # BaseCfg allows extra fields: settings written before questions and state - # moved to inputSchema still parse, but nothing reads or validates them. - resource = _validate( - _resource( - model="jev-1.13.0", - state={"type": "number"}, - questions=[{"type": "freeform"}], - ) - ) - settings = _settings(resource) - assert settings.model == "jev-1.13.0" - assert not hasattr(AgentInternalJevClassifierSettings, "questions") - assert settings.model_extra == { - "state": {"type": "number"}, - "questions": [{"type": "freeform"}], - } +def test_the_jev_classifier_tool_type_is_gone() -> None: + data = _resource(**JEV) + data["properties"]["toolType"] = "jev-classifier" + + with pytest.raises(ValidationError): + _validate(data) def test_parses_all_question_types() -> None: @@ -177,6 +217,8 @@ def test_noul_criteria_serialise_with_true_false_keys() -> None: @pytest.mark.parametrize( "question", [ + pytest.param({**CHOICE, "choices": [{"value": "a"}]}, id="choices-on-choice"), + pytest.param({**NOUL, "type": "predicate"}, id="decisions-type"), pytest.param({**NOUL, "name": "has space"}, id="invalid-name"), pytest.param({**NOUL, "name": "1st"}, id="name-starts-with-digit"), pytest.param({**NOUL, "name": "a" * 65}, id="name-too-long"), @@ -217,7 +259,7 @@ def test_rejects_invalid_question(question: dict[str, Any]) -> None: def test_parses_object_builder_argument_properties() -> None: - raw = _resource(model="jev-1.13.0") + raw = _resource(**JEV) raw["argumentProperties"] = { "$['questions']": {"variant": "ObjectBuilder"}, "$['questions']['department']": {"variant": "objectBuilder"}, @@ -248,3 +290,62 @@ def test_parses_object_builder_argument_properties() -> None: props["$['questions']['is_urgent']['instructions']"], AgentToolStaticArgumentProperties, ) + + +def test_parses_all_decisions_question_types() -> None: + choice = _DECISIONS_QUESTION.validate_python(DECISIONS_CHOICE) + score = _DECISIONS_QUESTION.validate_python(DECISIONS_SCORE) + predicate = _DECISIONS_QUESTION.validate_python( + {**DECISIONS_PREDICATE, "type": "PREDICATE"} + ) + + assert isinstance(choice, DecisionsChoiceQuestion) + assert [c.value for c in choice.choices] == ["billing", "technical"] + assert isinstance(score, DecisionsScoreQuestion) + assert [level.label for level in score.levels] == ["Calm", "Very angry"] + assert score.levels[1].description is None + assert isinstance(predicate, DecisionsPredicateQuestion) + + +@pytest.mark.parametrize( + "question", + [ + pytest.param({**DECISIONS_PREDICATE, "name": "has space"}, id="invalid-name"), + pytest.param({**DECISIONS_PREDICATE, "instructions": ""}, id="empty-instr"), + pytest.param( + {**DECISIONS_CHOICE, "choices": DECISIONS_CHOICE["choices"][:1]}, + id="one-choice", + ), + pytest.param( + {**DECISIONS_CHOICE, "choices": [{"value": "a"}, {"value": "a"}]}, + id="duplicate-choices", + ), + pytest.param( + {**DECISIONS_CHOICE, "choices": [{"value": "a"}, {"value": ""}]}, + id="empty-choice", + ), + pytest.param( + {**DECISIONS_SCORE, "levels": [{"label": "only"}]}, id="one-level" + ), + pytest.param({**DECISIONS_SCORE, "levels": ["Low", "High"]}, id="jev-levels"), + pytest.param( + {**DECISIONS_SCORE, "levels": [{"label": "a"}, {}]}, id="no-label" + ), + pytest.param({**CHOICE, "type": "choice"}, id="jev-options"), + pytest.param({**NOUL, "type": "noul"}, id="jev-type"), + pytest.param( + {**DECISIONS_PREDICATE, "criteria": {"true": "Yes"}}, id="jev-criteria" + ), + pytest.param( + {**DECISIONS_PREDICATE, "choices": DECISIONS_CHOICE["choices"]}, + id="choices-on-predicate", + ), + pytest.param( + {**DECISIONS_CHOICE, "levels": DECISIONS_SCORE["levels"]}, + id="levels-on-choice", + ), + ], +) +def test_rejects_invalid_decisions_question(question: dict[str, Any]) -> None: + with pytest.raises(ValidationError): + _DECISIONS_QUESTION.validate_python(question) diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index cede61da3..1a9dc47af 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2599,7 +2599,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.14.37" +version = "2.14.38" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2762,7 +2762,7 @@ wheels = [ [[package]] name = "uipath-platform" -version = "0.2.36" +version = "0.2.37" source = { editable = "../uipath-platform" } dependencies = [ { name = "anyio" }, From 95e9f90265c8100c3ed46730f40926ba671c9301 Mon Sep 17 00:00:00 2001 From: Mihalache Marius Date: Fri, 9 Oct 2026 16:50:52 +0300 Subject: [PATCH 2/2] feat(platform): add the LLM vendor decisions endpoint The LLM Gateway serves typed classification decisions (OpenAI's Decisions API, TypeSafe's Jev) on a raw vendor route next to /completions. AgentHub proxies it as llm/raw/vendor/{vendor}/model/{model}/decisions, and EndpointManager.get_vendor_decisions_endpoint() returns that route. There is no Orchestrator variant: the orchestrator_/llm routes are no longer used, and a comment on UiPathEndpoints now says so. Co-Authored-By: Claude Opus 5.5 --- packages/uipath-platform/pyproject.toml | 2 +- .../platform/common/_endpoints_manager.py | 21 +++++++++++++-- .../tests/common/test_endpoints_manager.py | 26 +++++++++++++++++++ packages/uipath-platform/uv.lock | 2 +- 4 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 packages/uipath-platform/tests/common/test_endpoints_manager.py diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 4699bfeeb..df2ba2732 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.2.36" +version = "0.2.37" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/common/_endpoints_manager.py b/packages/uipath-platform/src/uipath/platform/common/_endpoints_manager.py index 8e3b436ac..9dcbb407b 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_endpoints_manager.py +++ b/packages/uipath-platform/src/uipath/platform/common/_endpoints_manager.py @@ -20,9 +20,16 @@ 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" + # Deprecated: the orchestrator_/llm routes are no longer used. They were a temporary + # fallback for when AgentHub is unavailable and remain only for existing callers. + # New LLM endpoints are AgentHub-only: add an AH_ constant and return it directly, + # never an orchestrator_ variant or a _select_endpoint() pair. OR_NORMALIZED_COMPLETION_ENDPOINT = "orchestrator_/llm/api/chat/completions" OR_PASSTHROUGH_COMPLETION_ENDPOINT = "orchestrator_/llm/openai/deployments/{model}/chat/completions?api-version={api_version}" OR_EMBEDDING_ENDPOINT = "orchestrator_/llm/openai/deployments/{model}/embeddings?api-version={api_version}" @@ -61,8 +68,10 @@ 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. - All endpoint methods automatically select the best available endpoint using the fallback order, - unless overridden by the UIPATH_LLM_SERVICE environment variable. + get_vendor_decisions_endpoint(): Returns the AgentHub vendor decisions endpoint. + All endpoint methods except get_vendor_decisions_endpoint() automatically select the best + available endpoint using the fallback order, unless overridden by the UIPATH_LLM_SERVICE + environment variable. """ # noqa: D205 _base_url = UiPathConfig.base_url or "" @@ -210,3 +219,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). + + AgentHub only: the orchestrator_/llm routes are no longer used (see UiPathEndpoints). + """ + return UiPathEndpoints.AH_VENDOR_DECISION_ENDPOINT.value diff --git a/packages/uipath-platform/tests/common/test_endpoints_manager.py b/packages/uipath-platform/tests/common/test_endpoints_manager.py new file mode 100644 index 000000000..75ac6a88e --- /dev/null +++ b/packages/uipath-platform/tests/common/test_endpoints_manager.py @@ -0,0 +1,26 @@ +import pytest + +from uipath.platform.common._endpoints_manager import EndpointManager + + +@pytest.mark.parametrize("service", ["", "agenthub", "orchestrator"]) +def test_vendor_decisions_endpoint_is_always_agenthub( + monkeypatch: pytest.MonkeyPatch, service: str +) -> None: + monkeypatch.setenv("UIPATH_LLM_SERVICE", service) + + assert ( + EndpointManager.get_vendor_decisions_endpoint() + == "agenthub_/llm/raw/vendor/{vendor}/model/{model}/decisions" + ) + + +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" + ) diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 99070bbf4..9ada6c4ac 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.2.36" +version = "0.2.37" source = { editable = "." } dependencies = [ { name = "anyio" },