9201ef759e
Harness Compat / harness compat (push) Failing after 0s
CI / test on 3.12 (standard) (push) Has been cancelled
CI / test on 3.13 (standard) (push) Has been cancelled
CI / test on 3.14 (standard) (push) Has been cancelled
CI / test on 3.10 (all-extras) (push) Has been cancelled
CI / test on 3.11 (all-extras) (push) Has been cancelled
CI / test on 3.12 (all-extras) (push) Has been cancelled
CI / test on 3.14 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.10 (pydantic-evals) (push) Has been cancelled
CI / test on 3.11 (pydantic-evals) (push) Has been cancelled
CI / test on 3.12 (pydantic-evals) (push) Has been cancelled
CI / deploy-docs-preview (push) Has been cancelled
CI / build release artifacts (push) Has been cancelled
CI / publish to PyPI (push) Has been cancelled
CI / Send tweet (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / mypy (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / test on 3.10 (standard) (push) Has been cancelled
CI / test on 3.11 (standard) (push) Has been cancelled
CI / test on 3.13 (all-extras) (push) Has been cancelled
CI / test on 3.14 (all-extras) (push) Has been cancelled
CI / test on 3.10 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.11 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.12 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.13 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.13 (pydantic-evals) (push) Has been cancelled
CI / test on 3.14 (pydantic-evals) (push) Has been cancelled
CI / test on 3.10 (lowest-versions) (push) Has been cancelled
CI / test on 3.11 (lowest-versions) (push) Has been cancelled
CI / test on 3.12 (lowest-versions) (push) Has been cancelled
CI / test on 3.13 (lowest-versions) (push) Has been cancelled
CI / test on 3.14 (lowest-versions) (push) Has been cancelled
CI / test examples on 3.11 (push) Has been cancelled
CI / test examples on 3.12 (push) Has been cancelled
CI / test examples on 3.13 (push) Has been cancelled
CI / test examples on 3.14 (push) Has been cancelled
CI / coverage (push) Has been cancelled
CI / check (push) Has been cancelled
CI / deploy-docs (push) Has been cancelled
167 lines
6.5 KiB
Python
167 lines
6.5 KiB
Python
from __future__ import annotations as _annotations
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from ..conftest import try_import
|
|
|
|
with try_import() as imports_successful:
|
|
from pydantic_ai._spec import _SerializedNamedSpec # pyright: ignore[reportPrivateUsage]
|
|
from pydantic_evals.evaluators.common import HasMatchingSpan
|
|
from pydantic_evals.evaluators.spec import EvaluatorSpec
|
|
from pydantic_evals.otel.span_tree import SpanQuery
|
|
|
|
pytestmark = [pytest.mark.skipif(not imports_successful(), reason='pydantic-evals not installed'), pytest.mark.anyio]
|
|
|
|
|
|
def test_evaluator_spec_basic():
|
|
"""Test basic EvaluatorSpec functionality."""
|
|
# Test with no arguments
|
|
spec = EvaluatorSpec(name='TestEvaluator', arguments=None)
|
|
assert spec.name == 'TestEvaluator'
|
|
assert spec.arguments is None
|
|
assert spec.args == ()
|
|
assert spec.kwargs == {}
|
|
|
|
# Test with single positional argument
|
|
spec = EvaluatorSpec(name='TestEvaluator', arguments=('value',))
|
|
assert spec.name == 'TestEvaluator'
|
|
assert spec.arguments == ('value',)
|
|
assert spec.args == ('value',)
|
|
assert spec.kwargs == {}
|
|
|
|
# Test with keyword arguments
|
|
spec = EvaluatorSpec(name='TestEvaluator', arguments={'key': 'value'})
|
|
assert spec.name == 'TestEvaluator'
|
|
assert spec.arguments == {'key': 'value'}
|
|
assert spec.args == ()
|
|
assert spec.kwargs == {'key': 'value'}
|
|
|
|
|
|
def test_evaluator_spec_deserialization():
|
|
"""Test EvaluatorSpec deserialization."""
|
|
# Test string form
|
|
spec = EvaluatorSpec.model_validate('TestEvaluator')
|
|
assert spec.name == 'TestEvaluator'
|
|
assert spec.arguments is None
|
|
|
|
# Test single argument form
|
|
spec = EvaluatorSpec.model_validate({'TestEvaluator': 'value'})
|
|
assert spec.name == 'TestEvaluator'
|
|
assert spec.arguments == ('value',)
|
|
|
|
# Test kwargs form
|
|
spec = EvaluatorSpec.model_validate({'TestEvaluator': {'key': 'value'}})
|
|
assert spec.name == 'TestEvaluator'
|
|
assert spec.arguments == {'key': 'value'}
|
|
|
|
# Test invalid form
|
|
with pytest.raises(ValidationError):
|
|
EvaluatorSpec.model_validate({'TestEvaluator1': 'value', 'TestEvaluator2': 'value'})
|
|
|
|
|
|
def test_evaluator_spec_serialization():
|
|
"""Test EvaluatorSpec serialization."""
|
|
# Test no arguments
|
|
spec = EvaluatorSpec(name='TestEvaluator', arguments=None)
|
|
assert spec.model_dump(context={'use_short_form': True}) == 'TestEvaluator'
|
|
|
|
# Test single argument
|
|
spec = EvaluatorSpec(name='TestEvaluator', arguments=('value',))
|
|
assert spec.model_dump(context={'use_short_form': True}) == {'TestEvaluator': 'value'}
|
|
|
|
# Test kwargs
|
|
spec = EvaluatorSpec(name='TestEvaluator', arguments={'key': 'value'})
|
|
assert spec.model_dump(context={'use_short_form': True}) == {'TestEvaluator': {'key': 'value'}}
|
|
|
|
# Test without short form
|
|
spec = EvaluatorSpec(name='TestEvaluator', arguments=None)
|
|
assert spec.model_dump() == {'name': 'TestEvaluator', 'arguments': None}
|
|
|
|
|
|
def test_serialized_named_spec():
|
|
"""Test _SerializedNamedSpec functionality."""
|
|
# Test string form
|
|
spec = _SerializedNamedSpec.model_validate('TestEvaluator')
|
|
|
|
assert spec.to_named_spec().name == 'TestEvaluator'
|
|
assert spec.to_named_spec().arguments is None
|
|
|
|
# Test single argument form
|
|
spec = _SerializedNamedSpec.model_validate({'TestEvaluator': 'value'})
|
|
assert spec.to_named_spec().name == 'TestEvaluator'
|
|
assert spec.to_named_spec().arguments == ('value',)
|
|
|
|
# Test kwargs form
|
|
spec = _SerializedNamedSpec.model_validate({'TestEvaluator': {'key': 'value'}})
|
|
assert spec.to_named_spec().name == 'TestEvaluator'
|
|
assert spec.to_named_spec().arguments == {'key': 'value'}
|
|
|
|
# Test invalid form
|
|
with pytest.raises(ValueError) as exc_info:
|
|
_SerializedNamedSpec.model_validate({'TestEvaluator1': 'value', 'TestEvaluator2': 'value'})
|
|
assert 'Expected a single key' in str(exc_info.value)
|
|
|
|
# Test conversion to NamedSpec (EvaluatorSpec)
|
|
spec = _SerializedNamedSpec.model_validate('TestEvaluator')
|
|
named_spec = spec.to_named_spec()
|
|
assert isinstance(named_spec, EvaluatorSpec)
|
|
assert named_spec.name == 'TestEvaluator'
|
|
assert named_spec.arguments is None
|
|
|
|
|
|
def test_evaluator_spec_with_non_string_keys():
|
|
"""Test EvaluatorSpec with non-string keys in arguments."""
|
|
# Test with non-string keys in dict
|
|
spec = _SerializedNamedSpec.model_validate({'TestEvaluator': {1: 'value', 2: 'value2'}})
|
|
assert spec.to_named_spec().name == 'TestEvaluator'
|
|
assert spec.to_named_spec().arguments == (
|
|
{1: 'value', 2: 'value2'},
|
|
) # Should be treated as a single positional argument
|
|
|
|
# Test with mixed keys
|
|
spec = _SerializedNamedSpec.model_validate({'TestEvaluator': {'key': 'value', 1: 'value2'}})
|
|
assert spec.to_named_spec().name == 'TestEvaluator'
|
|
assert spec.to_named_spec().arguments == (
|
|
{'key': 'value', 1: 'value2'},
|
|
) # Should be treated as a single positional argument
|
|
|
|
|
|
def test_has_matching_span_round_trip():
|
|
"""Test that HasMatchingSpan with SpanQuery survives serialization round-trip.
|
|
|
|
SpanQuery is a TypedDict that serializes to a dict with all-string keys.
|
|
Without special handling, the compact tuple form (arguments=(SpanQuery(...),))
|
|
would serialize as {HasMatchingSpan: {key: value}} which the deserializer
|
|
interprets as kwargs instead of a single positional arg.
|
|
"""
|
|
query = SpanQuery(some_descendant_has=SpanQuery(has_attributes={'gen_ai.tool.name': 'calculator'}))
|
|
evaluator = HasMatchingSpan(query=query)
|
|
|
|
# Serialize to spec
|
|
spec = evaluator.as_spec()
|
|
assert spec.name == 'HasMatchingSpan'
|
|
# The arguments should use the kwargs form (not compact tuple) since
|
|
# SpanQuery is a dict with all-string keys
|
|
assert isinstance(spec.arguments, dict)
|
|
assert 'query' in spec.arguments
|
|
|
|
# Serialize to short form (as it would appear in YAML)
|
|
short_form = spec.model_dump(context={'use_short_form': True})
|
|
assert isinstance(short_form, dict)
|
|
assert 'HasMatchingSpan' in short_form
|
|
serialized_args = short_form['HasMatchingSpan']
|
|
assert isinstance(serialized_args, dict)
|
|
assert 'query' in serialized_args
|
|
|
|
# Deserialize back
|
|
deserialized_spec = EvaluatorSpec.model_validate(short_form)
|
|
assert deserialized_spec.name == 'HasMatchingSpan'
|
|
assert deserialized_spec.kwargs == {
|
|
'query': {'some_descendant_has': {'has_attributes': {'gen_ai.tool.name': 'calculator'}}}
|
|
}
|
|
|
|
# Reconstruct the evaluator from the deserialized spec
|
|
reconstructed = HasMatchingSpan(**deserialized_spec.kwargs)
|
|
assert reconstructed.query == query
|