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/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath"
version = "2.10.78"
version = "2.10.79"
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
34 changes: 31 additions & 3 deletions packages/uipath/src/uipath/eval/models/evaluation_set.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Evaluation set models."""

import re
from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field, field_validator
from pydantic.alias_generators import to_camel

from ..mocks._types import (
Expand All @@ -15,6 +16,21 @@
LegacyConversationalEvalOutput,
)

_GUID_RE = re.compile(
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
)


def normalize_eval_id(value: str) -> str:
"""Canonicalize a GUID id to lowercase; leave non-GUID ids unchanged.

GUIDs are case-insensitive, but downstream correlation (selection,
span/cache keying) compares ids as plain strings, so a mixed-case id
must be normalized at ingestion to stay matchable.
"""
return value.lower() if isinstance(value, str) and _GUID_RE.match(value) else value
Comment on lines +25 to +32


class EvaluatorReference(BaseModel):
"""Reference to an evaluator with optional weight.
Expand Down Expand Up @@ -96,6 +112,12 @@ class EvaluationItem(BaseModel):
alias="inputMockingStrategy",
)

@field_validator("id")
@classmethod
def _normalize_id(cls, value: str) -> str:
"""Normalize GUID ids to canonical lowercase."""
return normalize_eval_id(value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize override keys when lowercasing eval IDs

When an eval set contains an uppercase GUID and the caller provides --input-overrides keyed by that ID copied from the JSON, this validator changes eval_item.id to lowercase while the override maps are left untouched. The runtime later calls apply_input_overrides(..., eval_id=eval_item.id) and the legacy conversational migration does input_overrides.get(evaluation.id, {}), so those uppercase-keyed overrides are silently skipped in the same scenario this patch targets. Please canonicalize the override keys at ingestion/lookup as well.

Useful? React with 👍 / 👎.



class LegacyEvaluationItem(BaseModel):
"""Individual evaluation item within an evaluation set."""
Expand Down Expand Up @@ -130,6 +152,12 @@ class LegacyEvaluationItem(BaseModel):
default=None, alias="conversationalExpectedOutput"
)

@field_validator("id")
@classmethod
def _normalize_id(cls, value: str) -> str:
"""Normalize GUID ids to canonical lowercase."""
return normalize_eval_id(value)


class EvaluationSet(BaseModel):
"""Complete evaluation set model."""
Expand All @@ -153,7 +181,7 @@ class EvaluationSet(BaseModel):
def extract_selected_evals(self, eval_ids) -> None:
"""Filter evaluations to only include those with specified IDs."""
selected_evals: list[EvaluationItem] = []
remaining_ids = set(eval_ids)
remaining_ids = {normalize_eval_id(eval_id) for eval_id in eval_ids}
for evaluation in self.evaluations:
if evaluation.id in remaining_ids:
selected_evals.append(evaluation)
Expand Down Expand Up @@ -187,7 +215,7 @@ class LegacyEvaluationSet(BaseModel):
def extract_selected_evals(self, eval_ids) -> None:
"""Filter evaluations to only include those with specified IDs."""
selected_evals: list[LegacyEvaluationItem] = []
remaining_ids = set(eval_ids)
remaining_ids = {normalize_eval_id(eval_id) for eval_id in eval_ids}
for evaluation in self.evaluations:
if evaluation.id in remaining_ids:
selected_evals.append(evaluation)
Expand Down
69 changes: 69 additions & 0 deletions packages/uipath/tests/cli/eval/test_eval_id_casing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Tests for case-insensitive eval id handling (PC-4688).

Eval sets exported by some tools emit uppercase GUID ids. The backend
canonicalizes GUIDs to lowercase, so any case-sensitive correlation on the
runtime side (selection, span/cache keying) silently fails to match. These
tests pin the fix: GUID ids are normalized to lowercase at ingestion and
selection is casing-agnostic.
"""

from typing import Any

from uipath.eval.models.evaluation_set import (
EvaluationItem,
EvaluationSet,
LegacyEvaluationItem,
)

UPPER_GUID = "B063907C-76AB-4B0A-88A3-EC0FB40698B8"
LOWER_GUID = "b063907c-76ab-4b0a-88a3-ec0fb40698b8"


def _make_item(eval_id: str) -> dict[str, Any]:
return {
"id": eval_id,
"name": "item",
"inputs": {"x": 1},
"evaluationCriterias": {},
}


def test_evaluation_item_normalizes_uppercase_guid_id():
"""An uppercase GUID id is stored in canonical lowercase form."""
item = EvaluationItem.model_validate(_make_item(UPPER_GUID))
assert item.id == LOWER_GUID


def test_legacy_evaluation_item_normalizes_uppercase_guid_id():
"""LegacyEvaluationItem also normalizes uppercase GUID ids."""
item = LegacyEvaluationItem.model_validate(
{
"id": UPPER_GUID,
"name": "item",
"inputs": {"x": 1},
"expectedOutput": {},
"evalSetId": "set-1",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z",
}
)
assert item.id == LOWER_GUID


def test_non_guid_id_is_left_unchanged():
"""Non-GUID ids (e.g. slugs) keep their original value and casing."""
item = EvaluationItem.model_validate(_make_item("Test-Eval-1"))
assert item.id == "Test-Eval-1"


def test_extract_selected_evals_matches_regardless_of_caller_casing():
"""Selecting by an uppercase GUID matches a normalized stored id."""
eval_set = EvaluationSet.model_validate(
{
"id": "set-1",
"name": "set",
"evaluations": [_make_item(LOWER_GUID), _make_item("other-id")],
}
)
eval_set.extract_selected_evals([UPPER_GUID])
assert [e.id for e in eval_set.evaluations] == [LOWER_GUID]
4 changes: 2 additions & 2 deletions packages/uipath/uv.lock

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

Loading