|
| 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") |
0 commit comments