85742ab165
Deploy Documentation / deploy (push) Has been cancelled
CPU Test / Test (Utilities, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, latest, Python 3.13) (push) Has been cancelled
Dashboard / Chromatic (push) Has been cancelled
CPU Test / Lint - fast (push) Has been cancelled
CPU Test / Lint - next (push) Has been cancelled
CPU Test / Lint - slow (push) Has been cancelled
CPU Test / Lint - JavaScript (push) Has been cancelled
CPU Test / Build documentation (push) Has been cancelled
CPU Test / Test (AgentOps, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Others, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Store, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Weave, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (AgentOps, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (LLM proxy, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Others, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Store, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Utilities, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (JavaScript) (push) Has been cancelled
244 lines
8.1 KiB
Python
244 lines
8.1 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
import importlib
|
|
import json
|
|
from dataclasses import dataclass
|
|
from typing import Any, Dict, Optional, cast
|
|
|
|
import pytest
|
|
|
|
reward_module = importlib.import_module("agentlightning.emitter.reward")
|
|
from agentlightning.emitter.reward import emit_reward, get_rewards_from_span
|
|
from agentlightning.reward import find_final_reward, find_reward_spans, get_reward_value, is_reward_span
|
|
from agentlightning.semconv import AGL_ANNOTATION, LightningSpanAttributes, RewardPydanticModel
|
|
from agentlightning.types import SpanLike
|
|
from agentlightning.utils.otel import make_link_attributes, make_tag_attributes
|
|
|
|
|
|
@dataclass
|
|
class FakeSpan:
|
|
name: str
|
|
attributes: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
@dataclass
|
|
class AttributeSpan:
|
|
attributes: Optional[Dict[str, Any]]
|
|
|
|
|
|
def make_span(name: str, attributes: Optional[Dict[str, Any]] = None) -> SpanLike:
|
|
return cast(SpanLike, FakeSpan(name=name, attributes=attributes))
|
|
|
|
|
|
def _capture_emit_annotation(monkeypatch: pytest.MonkeyPatch) -> tuple[Dict[str, Any], object]:
|
|
captured: Dict[str, Any] = {}
|
|
sentinel = object()
|
|
|
|
def fake_emit_annotation(payload: Dict[str, Any], *, propagate: bool) -> object:
|
|
captured["payload"] = payload
|
|
captured["propagate"] = propagate
|
|
return sentinel
|
|
|
|
monkeypatch.setattr(reward_module, "emit_annotation", fake_emit_annotation)
|
|
return captured, sentinel
|
|
|
|
|
|
def test_emit_reward_example_scalar(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
captured, sentinel = _capture_emit_annotation(monkeypatch)
|
|
|
|
result = emit_reward(1.0)
|
|
|
|
assert result is sentinel
|
|
assert captured["propagate"] is True
|
|
dimensions = captured["payload"][LightningSpanAttributes.REWARD.value]
|
|
assert dimensions == [{"name": "primary", "value": 1.0}]
|
|
|
|
|
|
def test_emit_reward_example_multi_dimensional(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
captured, _ = _capture_emit_annotation(monkeypatch)
|
|
|
|
emit_reward({"task_completion": 1.0, "efficiency": 0.8}, primary_key="task_completion")
|
|
|
|
dimensions = captured["payload"][LightningSpanAttributes.REWARD.value]
|
|
assert dimensions == [
|
|
{"name": "task_completion", "value": 1.0},
|
|
{"name": "efficiency", "value": 0.8},
|
|
]
|
|
|
|
|
|
def test_emit_reward_example_with_links(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
captured, _ = _capture_emit_annotation(monkeypatch)
|
|
|
|
link_attrs = make_link_attributes({"gen_ai.response.id": "response-123", "span_id": "abcd-efgh"})
|
|
emit_reward(0.5, attributes=link_attrs)
|
|
|
|
payload = captured["payload"]
|
|
assert payload[f"{LightningSpanAttributes.LINK.value}.0.key_match"] == "gen_ai.response.id"
|
|
assert payload[f"{LightningSpanAttributes.LINK.value}.0.value_match"] == "response-123"
|
|
reward_dimensions = payload[LightningSpanAttributes.REWARD.value]
|
|
assert reward_dimensions == [{"name": "primary", "value": 0.5}]
|
|
|
|
|
|
def test_emit_reward_example_with_tags(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
captured, _ = _capture_emit_annotation(monkeypatch)
|
|
|
|
tag_attrs = make_tag_attributes(["fast", "reliable"])
|
|
emit_reward(0.7, attributes=tag_attrs)
|
|
|
|
payload = captured["payload"]
|
|
assert payload[f"{LightningSpanAttributes.TAG.value}.0"] == "fast"
|
|
assert payload[f"{LightningSpanAttributes.TAG.value}.1"] == "reliable"
|
|
reward_dimensions = payload[LightningSpanAttributes.REWARD.value]
|
|
assert reward_dimensions == [{"name": "primary", "value": 0.7}]
|
|
|
|
|
|
def test_get_reward_value_from_agentops_dict() -> None:
|
|
span = make_span(
|
|
name="any",
|
|
attributes={
|
|
"agentops.task.output": {"type": "reward", "value": 3.5},
|
|
},
|
|
)
|
|
|
|
assert get_reward_value(span) == 3.5
|
|
|
|
|
|
def test_get_reward_value_from_agentops_json_string() -> None:
|
|
payload = json.dumps({"type": "reward", "value": 1.25})
|
|
span = make_span(name="any", attributes={"agentops.entity.output": payload})
|
|
|
|
assert get_reward_value(span) == 1.25
|
|
|
|
|
|
def test_get_reward_value_from_reward_span_attributes() -> None:
|
|
span = make_span(
|
|
name=AGL_ANNOTATION,
|
|
attributes={"reward": 0.75},
|
|
)
|
|
|
|
assert get_reward_value(span) == 0.75
|
|
|
|
|
|
def test_get_reward_value_returns_none_when_not_reward() -> None:
|
|
span = make_span(name="any", attributes={"agentops.task.output": {"foo": "bar"}})
|
|
|
|
assert get_reward_value(span) is None
|
|
|
|
|
|
def test_is_reward_span_matches_reward_value() -> None:
|
|
span = make_span(
|
|
name="whatever",
|
|
attributes={"agentops.task.output": {"type": "reward", "value": 4.2}},
|
|
)
|
|
|
|
assert is_reward_span(span) is True
|
|
|
|
|
|
def test_is_reward_span_false_when_no_reward() -> None:
|
|
span = make_span(name="absent", attributes={"agentops.entity.output": {"value": 1}})
|
|
|
|
assert is_reward_span(span) is False
|
|
|
|
|
|
def test_find_reward_spans_filters_correctly() -> None:
|
|
reward_span = make_span(
|
|
name=AGL_ANNOTATION,
|
|
attributes={"reward": 2.0},
|
|
)
|
|
non_reward_span = make_span(name="other", attributes={})
|
|
|
|
spans = find_reward_spans([non_reward_span, reward_span, non_reward_span])
|
|
|
|
assert spans == [reward_span]
|
|
|
|
|
|
def test_find_final_reward_returns_last_reward_value() -> None:
|
|
spans = [
|
|
make_span(name="first", attributes={}),
|
|
make_span(name=AGL_ANNOTATION, attributes={"reward": 1.0}),
|
|
make_span(name="agentops", attributes={"agentops.task.output": {"type": "reward", "value": 5.5}}),
|
|
]
|
|
|
|
assert find_final_reward(spans) == 5.5
|
|
|
|
|
|
def test_find_final_reward_returns_none_when_no_reward() -> None:
|
|
spans = [
|
|
make_span(name="first", attributes={}),
|
|
make_span(name="second", attributes={"agentops.task.output": {"foo": "bar"}}),
|
|
]
|
|
|
|
assert find_final_reward(spans) is None
|
|
|
|
|
|
def test_emit_reward_scalar_converts_to_primary_dimension(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
captured: Dict[str, Any] = {}
|
|
sentinel_span = object()
|
|
|
|
def fake_emit_annotation(payload: Dict[str, Any], *, propagate: bool) -> object:
|
|
captured["payload"] = payload
|
|
captured["propagate"] = propagate
|
|
return sentinel_span
|
|
|
|
monkeypatch.setattr(reward_module, "emit_annotation", fake_emit_annotation)
|
|
|
|
result = emit_reward(2, attributes={"extra": "value"}, propagate=False)
|
|
|
|
assert result is sentinel_span
|
|
assert captured["propagate"] is False
|
|
rewards = captured["payload"][LightningSpanAttributes.REWARD.value]
|
|
assert rewards == [{"name": "primary", "value": 2.0}]
|
|
assert captured["payload"]["extra"] == "value"
|
|
|
|
|
|
def test_emit_reward_dict_requires_primary_key(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
captured: Dict[str, Any] = {}
|
|
|
|
def fake_emit_annotation(payload: Dict[str, Any], *, propagate: bool) -> Dict[str, Any]:
|
|
captured["payload"] = payload
|
|
return payload
|
|
|
|
monkeypatch.setattr(reward_module, "emit_annotation", fake_emit_annotation)
|
|
|
|
emit_reward({"score": 0.8, "other": 0.2}, primary_key="score")
|
|
|
|
rewards = captured["payload"][LightningSpanAttributes.REWARD.value]
|
|
assert [dim["name"] for dim in rewards] == ["score", "other"]
|
|
|
|
with pytest.raises(ValueError):
|
|
emit_reward({"score": 0.8}, primary_key=None)
|
|
|
|
with pytest.raises(ValueError):
|
|
emit_reward({"score": 0.8}, primary_key="missing")
|
|
|
|
with pytest.raises(ValueError):
|
|
emit_reward({"score": "bad"}, primary_key="score")
|
|
|
|
|
|
def test_emit_reward_rejects_non_numeric() -> None:
|
|
with pytest.raises(TypeError):
|
|
emit_reward("bad") # type: ignore[arg-type]
|
|
|
|
|
|
def test_get_rewards_from_span_roundtrip() -> None:
|
|
attributes = {
|
|
f"{LightningSpanAttributes.REWARD.value}.0.name": "primary",
|
|
f"{LightningSpanAttributes.REWARD.value}.0.value": 1.0,
|
|
f"{LightningSpanAttributes.REWARD.value}.1.name": "aux",
|
|
f"{LightningSpanAttributes.REWARD.value}.1.value": 0.25,
|
|
}
|
|
span = AttributeSpan(attributes=attributes)
|
|
|
|
rewards = get_rewards_from_span(cast(SpanLike, span))
|
|
|
|
assert rewards == [
|
|
RewardPydanticModel(name="primary", value=1.0),
|
|
RewardPydanticModel(name="aux", value=0.25),
|
|
]
|
|
|
|
|
|
def test_get_rewards_from_span_returns_empty_when_missing() -> None:
|
|
span = AttributeSpan(attributes={})
|
|
|
|
assert get_rewards_from_span(cast(SpanLike, span)) == []
|