85742ab165
CPU Test / Lint - next (push) Waiting to run
Dashboard / Chromatic (push) Waiting to run
CPU Test / Lint - fast (push) Waiting to run
CPU Test / Build documentation (push) Waiting to run
CPU Test / Test (Store, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Utilities, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Weave, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (AgentOps, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (LLM proxy, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Others, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Store, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Utilities, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Weave, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (AgentOps, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (LLM proxy, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Others, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Store, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Utilities, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Weave, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (AgentOps, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (LLM proxy, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Others, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Store, latest, Python 3.13) (push) Waiting to run
CPU Test / Lint - slow (push) Waiting to run
CPU Test / Lint - JavaScript (push) Waiting to run
CPU Test / Test (AgentOps, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (LLM proxy, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Others, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Utilities, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Weave, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (JavaScript) (push) Waiting to run
Deploy Documentation / deploy (push) Has been cancelled
342 lines
9.7 KiB
Python
342 lines
9.7 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
import json
|
|
from typing import Any, Dict, List
|
|
|
|
from agentlightning.adapter import LlmProxyTraceToTriplet
|
|
from agentlightning.types import Span
|
|
|
|
|
|
def _mk_span(
|
|
*,
|
|
span_id: str,
|
|
name: str,
|
|
seq: int,
|
|
start: int,
|
|
end: int,
|
|
attrs: Dict[str, Any],
|
|
parent_id: str | None = None,
|
|
) -> Span:
|
|
return Span.from_attributes(
|
|
rollout_id="rollout-X",
|
|
attempt_id="attempt-Y",
|
|
sequence_id=seq,
|
|
trace_id="trace-Z",
|
|
span_id=span_id,
|
|
parent_id=parent_id,
|
|
name=name,
|
|
attributes=attrs,
|
|
start_time=start,
|
|
end_time=end,
|
|
)
|
|
|
|
|
|
def _raw_attrs_with_tokens(
|
|
prompt_ids: List[int], resp_ids: List[int], *, response_id: str, model: str = "my/own-model"
|
|
):
|
|
"""
|
|
Build attributes for a 'raw_gen_ai_request' span mirroring the sample.
|
|
Token id fields are stringified lists, as in the provided trace dump.
|
|
"""
|
|
return {
|
|
"llm.hosted_vllm.prompt_token_ids": str(prompt_ids),
|
|
# vLLM sometimes sends response_token_ids as List[List[int]]
|
|
"llm.hosted_vllm.response_token_ids": str([resp_ids]),
|
|
"llm.hosted_vllm.choices": str([{"token_ids": resp_ids}]),
|
|
"llm.hosted_vllm.id": response_id,
|
|
"llm.hosted_vllm.model": model,
|
|
}
|
|
|
|
|
|
def _agentops_reward_attrs(value: float):
|
|
# As in the sample: agentops.task.output is often a JSON-encoded dict
|
|
return {
|
|
"agentops.task.output": json.dumps({"type": "reward", "value": value}),
|
|
"agentops.span.kind": "task",
|
|
"operation.name": "agentops_reward_operation",
|
|
}
|
|
|
|
|
|
def test_sequence_matching_assigns_reward_to_latest_prior_llm():
|
|
"""
|
|
Grounded in the provided sample:
|
|
- One raw_gen_ai_request at sequence 1
|
|
- One raw_gen_ai_request at sequence 4
|
|
- A reward emitted at sequence 6
|
|
First-occurrence by sequence should assign the reward to the seq=4 LLM call.
|
|
"""
|
|
# Use short token lists for readability, but structure matches the sample.
|
|
p1 = [151644, 8948, 198]
|
|
r1 = [151657, 198, 4913]
|
|
|
|
p2 = [151644, 872, 198, 151657, 198]
|
|
r2 = [15, 13, 15, 22, 24, 3245]
|
|
|
|
spans = [
|
|
_mk_span(
|
|
span_id="s-raw-1",
|
|
name="raw_gen_ai_request",
|
|
seq=1,
|
|
start=1000,
|
|
end=1010,
|
|
attrs=_raw_attrs_with_tokens(p1, r1, response_id="chatcmpl-AAA"),
|
|
),
|
|
_mk_span(
|
|
span_id="s-raw-4",
|
|
name="raw_gen_ai_request",
|
|
seq=4,
|
|
start=2000,
|
|
end=2020,
|
|
attrs=_raw_attrs_with_tokens(p2, r2, response_id="chatcmpl-BBB"),
|
|
),
|
|
_mk_span(
|
|
span_id="s-reward-6",
|
|
name="agentops_reward_operation.task",
|
|
seq=6,
|
|
start=3000,
|
|
end=3001,
|
|
attrs=_agentops_reward_attrs(0.0),
|
|
),
|
|
]
|
|
|
|
adapter = LlmProxyTraceToTriplet()
|
|
trips = adapter.adapt(spans)
|
|
|
|
# Two LLM calls → two triplets
|
|
assert len(trips) == 2
|
|
|
|
# Ordered by LLM sequence only
|
|
t1, t2 = trips
|
|
|
|
assert t1.prompt["token_ids"] == p1
|
|
assert t1.response["token_ids"] == r1
|
|
# Reward appears at seq 6, so the *latest* prior LLM (seq 4) gets it, not seq 1.
|
|
assert t1.reward is None
|
|
|
|
assert t2.prompt["token_ids"] == p2
|
|
assert t2.response["token_ids"] == r2
|
|
assert t2.reward == 0.0
|
|
|
|
|
|
def test_deduplicates_same_response_id_from_raw_spans():
|
|
"""
|
|
If two raw_gen_ai_request spans share the same response id,
|
|
only the first should be kept as an LLM call.
|
|
"""
|
|
ids = [1, 2, 3]
|
|
spans = [
|
|
_mk_span(
|
|
span_id="dup-a",
|
|
name="raw_gen_ai_request",
|
|
seq=1,
|
|
start=100,
|
|
end=101,
|
|
attrs=_raw_attrs_with_tokens(ids, ids, response_id="chatcmpl-DUP"),
|
|
),
|
|
_mk_span(
|
|
span_id="dup-b",
|
|
name="raw_gen_ai_request",
|
|
seq=2,
|
|
start=110,
|
|
end=111,
|
|
attrs=_raw_attrs_with_tokens(ids, ids, response_id="chatcmpl-DUP"),
|
|
),
|
|
_mk_span(
|
|
span_id="reward-3",
|
|
name="agentops_reward_operation.task",
|
|
seq=3,
|
|
start=200,
|
|
end=201,
|
|
attrs=_agentops_reward_attrs(1.0),
|
|
),
|
|
]
|
|
|
|
adapter = LlmProxyTraceToTriplet()
|
|
trips = adapter.adapt(spans)
|
|
|
|
assert len(trips) == 1
|
|
assert trips[0].prompt["token_ids"] == ids
|
|
assert trips[0].response["token_ids"] == ids
|
|
# Reward at seq 3 should attach to the only LLM call (seq 1).
|
|
assert trips[0].reward == 1.0
|
|
|
|
|
|
def test_ignores_litellm_request_without_token_ids():
|
|
"""
|
|
litellm_request spans often carry usage and prompt/response *text*,
|
|
but not token id arrays. Adapter should ignore these unless token ids exist.
|
|
"""
|
|
spans = [
|
|
_mk_span(
|
|
span_id="litellm-no-tids",
|
|
name="litellm_request",
|
|
seq=1,
|
|
start=10,
|
|
end=20,
|
|
attrs={
|
|
"gen_ai.response.id": "chatcmpl-XYZ",
|
|
# no prompt_token_ids / response_token_ids here
|
|
},
|
|
),
|
|
_mk_span(
|
|
span_id="reward-2",
|
|
name="agentops_reward_operation.task",
|
|
seq=2,
|
|
start=30,
|
|
end=31,
|
|
attrs=_agentops_reward_attrs(0.5),
|
|
),
|
|
]
|
|
|
|
adapter = LlmProxyTraceToTriplet()
|
|
trips = adapter.adapt(spans)
|
|
|
|
# No token ids → no triplets
|
|
assert trips == []
|
|
|
|
|
|
def test_reward_none():
|
|
prompt_ids = [1, 2, 3]
|
|
resp_ids = [4, 5, 6]
|
|
spans = [
|
|
_mk_span(
|
|
span_id="s-raw-1",
|
|
name="raw_gen_ai_request",
|
|
seq=1,
|
|
start=100,
|
|
end=101,
|
|
attrs=_raw_attrs_with_tokens(prompt_ids, resp_ids, response_id="chatcmpl-BUG"),
|
|
),
|
|
]
|
|
|
|
adapter = LlmProxyTraceToTriplet()
|
|
|
|
triplets = adapter.adapt(spans)
|
|
assert len(triplets) == 1
|
|
assert triplets[0].prompt["token_ids"] == prompt_ids
|
|
assert triplets[0].response["token_ids"] == resp_ids
|
|
assert triplets[0].reward is None
|
|
assert triplets[0].metadata["response_id"] == "chatcmpl-BUG"
|
|
|
|
|
|
def test_rewards_before_or_equal_sequence_are_skipped():
|
|
"""
|
|
Rewards that appear before an LLM call (or share its sequence id) should be ignored.
|
|
Only the first reward strictly after the LLM call should apply.
|
|
"""
|
|
prompt_ids = [9, 9, 9]
|
|
resp_ids = [8, 8, 8]
|
|
spans = [
|
|
_mk_span(
|
|
span_id="reward-early",
|
|
name="agentops_reward_operation.task",
|
|
seq=1,
|
|
start=10,
|
|
end=11,
|
|
attrs=_agentops_reward_attrs(1.0),
|
|
),
|
|
_mk_span(
|
|
span_id="llm-call",
|
|
name="raw_gen_ai_request",
|
|
seq=2,
|
|
start=20,
|
|
end=21,
|
|
attrs=_raw_attrs_with_tokens(prompt_ids, resp_ids, response_id="chatcmpl-SKIP"),
|
|
),
|
|
_mk_span(
|
|
span_id="reward-same-seq",
|
|
name="agentops_reward_operation.task",
|
|
seq=2,
|
|
start=22,
|
|
end=23,
|
|
attrs=_agentops_reward_attrs(2.0),
|
|
),
|
|
_mk_span(
|
|
span_id="reward-late",
|
|
name="agentops_reward_operation.task",
|
|
seq=3,
|
|
start=30,
|
|
end=31,
|
|
attrs=_agentops_reward_attrs(3.5),
|
|
),
|
|
]
|
|
|
|
adapter = LlmProxyTraceToTriplet()
|
|
triplets = adapter.adapt(spans)
|
|
|
|
assert len(triplets) == 1
|
|
triplet = triplets[0]
|
|
assert triplet.prompt["token_ids"] == prompt_ids
|
|
assert triplet.response["token_ids"] == resp_ids
|
|
# Only the reward after the LLM call should attach.
|
|
assert triplet.reward == 3.5
|
|
|
|
|
|
def test_multiple_rewards_attach_to_latest_unmatched_llm_calls():
|
|
"""
|
|
Rewards should attach to the most recent unmatched LLM call whose sequence id is smaller.
|
|
Later rewards backfill older unmatched LLM calls.
|
|
"""
|
|
p1, r1 = [1, 1], [2, 2]
|
|
p2, r2 = [3, 3], [4, 4]
|
|
p3, r3 = [5, 5], [6, 6]
|
|
spans = [
|
|
_mk_span(
|
|
span_id="llm-1",
|
|
name="raw_gen_ai_request",
|
|
seq=2,
|
|
start=100,
|
|
end=110,
|
|
attrs=_raw_attrs_with_tokens(p1, r1, response_id="chatcmpl-A"),
|
|
),
|
|
_mk_span(
|
|
span_id="llm-2",
|
|
name="raw_gen_ai_request",
|
|
seq=4,
|
|
start=120,
|
|
end=130,
|
|
attrs=_raw_attrs_with_tokens(p2, r2, response_id="chatcmpl-B"),
|
|
),
|
|
_mk_span(
|
|
span_id="llm-3",
|
|
name="raw_gen_ai_request",
|
|
seq=6,
|
|
start=140,
|
|
end=150,
|
|
attrs=_raw_attrs_with_tokens(p3, r3, response_id="chatcmpl-C"),
|
|
),
|
|
_mk_span(
|
|
span_id="reward-1",
|
|
name="agentops_reward_operation.task",
|
|
seq=5,
|
|
start=200,
|
|
end=201,
|
|
attrs=_agentops_reward_attrs(0.1),
|
|
),
|
|
_mk_span(
|
|
span_id="reward-2",
|
|
name="agentops_reward_operation.task",
|
|
seq=7,
|
|
start=210,
|
|
end=211,
|
|
attrs=_agentops_reward_attrs(0.2),
|
|
),
|
|
_mk_span(
|
|
span_id="reward-3",
|
|
name="agentops_reward_operation.task",
|
|
seq=8,
|
|
start=220,
|
|
end=221,
|
|
attrs=_agentops_reward_attrs(0.3),
|
|
),
|
|
]
|
|
|
|
adapter = LlmProxyTraceToTriplet()
|
|
triplets = adapter.adapt(spans)
|
|
|
|
assert len(triplets) == 3
|
|
# Triplets are emitted in LLM sequence order.
|
|
assert triplets[0].reward == 0.3 # backfilled by the last reward
|
|
assert triplets[1].reward == 0.1 # first reward targets the latest prior call (seq=4)
|
|
assert triplets[2].reward == 0.2 # second reward picks up the remaining unmatched call (seq=6)
|