Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:44:17 +08:00

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)) == []