Skip to content

Commit bb00412

Browse files
mjnoviceclaude
andauthored
fix(eval): reject out-of-range scores from LLM judge tool calls (#1819)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 9965d6e commit bb00412

4 files changed

Lines changed: 111 additions & 3 deletions

File tree

packages/uipath/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath"
3-
version = "2.13.13"
3+
version = "2.13.14"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

packages/uipath/src/uipath/eval/evaluators/legacy_llm_helpers.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Helper functions for legacy LLM evaluators using function calling."""
22

33
import logging
4+
import math
45
from typing import Any
56

67
from uipath.platform.chat.llm_gateway import (
@@ -89,9 +90,32 @@ def extract_tool_call_response(response: Any, model: str) -> LLMResponse:
8990
logger.debug(f"Arguments: {arguments}")
9091
raise ValueError(error_msg)
9192

92-
score = float(arguments["score"])
93+
try:
94+
score = float(arguments["score"])
95+
except (ValueError, TypeError) as e:
96+
error_msg = (
97+
f"Non-numeric score {arguments['score']!r} in tool call arguments "
98+
f"from model {model}: expected a number between 0 and 100"
99+
)
100+
logger.error(f"❌ {error_msg}")
101+
logger.debug(f"Arguments: {arguments}")
102+
raise ValueError(error_msg) from e
103+
93104
justification = str(arguments["justification"])
94105

106+
# Models occasionally emit corrupted numeric tool arguments despite the
107+
# 0-100 range stated in the tool schema (e.g. gemini-2.5-flash returning
108+
# 989898 or 950). Unvalidated, such a value poisons every run-level
109+
# aggregate downstream, so reject it and let the evaluation surface as
110+
# an error instead of recording a fabricated score.
111+
if not math.isfinite(score) or score < 0.0 or score > 100.0:
112+
error_msg = (
113+
f"Invalid score {score!r} in tool call arguments from model {model}: "
114+
f"expected a number between 0 and 100"
115+
)
116+
logger.error(f"❌ {error_msg}")
117+
raise ValueError(error_msg)
118+
95119
logger.debug(
96120
f"✅ Extracted score: {score}, justification length: {len(justification)} chars"
97121
)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""Tests for legacy LLM helper functions (submit_evaluation tool-call parsing)."""
2+
3+
from types import SimpleNamespace
4+
from typing import Any
5+
6+
import pytest
7+
8+
from uipath.eval.evaluators.legacy_llm_helpers import extract_tool_call_response
9+
10+
11+
def _make_response(arguments: dict[str, Any]) -> Any:
12+
"""Build a minimal chat-completions response carrying a submit_evaluation tool call."""
13+
tool_call = SimpleNamespace(arguments=arguments)
14+
message = SimpleNamespace(tool_calls=[tool_call])
15+
choice = SimpleNamespace(message=message)
16+
return SimpleNamespace(choices=[choice])
17+
18+
19+
class TestExtractToolCallResponse:
20+
"""Test extract_tool_call_response score validation."""
21+
22+
def test_valid_score_passes_through(self) -> None:
23+
response = _make_response({"score": 88, "justification": "ok"})
24+
25+
result = extract_tool_call_response(response, "gemini-2.5-flash")
26+
27+
assert result.score == 88.0
28+
assert result.justification == "ok"
29+
30+
def test_out_of_range_score_is_rejected(self) -> None:
31+
# Real payload observed in production: gemini-2.5-flash returned
32+
# score=989898 in its submit_evaluation tool call while the justification
33+
# said the outputs "match perfectly". Unvalidated, this single value blew
34+
# a 64-item run-level average up to 15559.13%. The evaluation must surface
35+
# as an error rather than record a fabricated score.
36+
response = _make_response(
37+
{"score": 989898, "justification": "matches perfectly"}
38+
)
39+
40+
with pytest.raises(ValueError, match="Invalid score 989898"):
41+
extract_tool_call_response(response, "gemini-2.5-flash")
42+
43+
def test_out_of_range_950_is_rejected(self) -> None:
44+
# Second production occurrence from the same eval run: score=950
45+
# (the model most likely intended 95).
46+
response = _make_response({"score": 950, "justification": "equivalent"})
47+
48+
with pytest.raises(ValueError, match="Invalid score 950"):
49+
extract_tool_call_response(response, "gemini-2.5-flash")
50+
51+
def test_negative_score_is_rejected(self) -> None:
52+
response = _make_response({"score": -5, "justification": "bad"})
53+
54+
with pytest.raises(ValueError, match="Invalid score -5"):
55+
extract_tool_call_response(response, "gpt-4o")
56+
57+
@pytest.mark.parametrize("boundary", [0, 100])
58+
def test_boundary_scores_accepted(self, boundary: int) -> None:
59+
response = _make_response({"score": boundary, "justification": "j"})
60+
61+
result = extract_tool_call_response(response, "m")
62+
63+
assert result.score == float(boundary)
64+
assert result.justification == "j"
65+
66+
@pytest.mark.parametrize("value", [float("nan"), float("inf"), float("-inf")])
67+
def test_non_finite_score_is_rejected(self, value: float) -> None:
68+
response = _make_response({"score": value, "justification": "j"})
69+
70+
with pytest.raises(ValueError, match="Invalid score"):
71+
extract_tool_call_response(response, "m")
72+
73+
def test_missing_score_raises(self) -> None:
74+
response = _make_response({"justification": "j"})
75+
76+
with pytest.raises(ValueError, match="Missing 'score'"):
77+
extract_tool_call_response(response, "m")
78+
79+
@pytest.mark.parametrize("value", ["not-a-number", None, [95]])
80+
def test_non_numeric_score_is_rejected(self, value: Any) -> None:
81+
response = _make_response({"score": value, "justification": "j"})
82+
83+
with pytest.raises(ValueError, match="Non-numeric score"):
84+
extract_tool_call_response(response, "m")

packages/uipath/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)