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
198 lines
5.9 KiB
Python
198 lines
5.9 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
from dataclasses import dataclass
|
|
from typing import Any, Dict, Optional, cast
|
|
|
|
import pytest
|
|
|
|
from agentlightning.emitter import emit_object
|
|
from agentlightning.emitter import object as object_module
|
|
from agentlightning.emitter.object import encode_object, get_object_value
|
|
from agentlightning.semconv import AGL_OBJECT, LightningSpanAttributes
|
|
from agentlightning.types.tracer import SpanLike
|
|
|
|
|
|
@dataclass
|
|
class FakeSpan:
|
|
attributes: Optional[Dict[str, Any]]
|
|
|
|
|
|
class DummySpan:
|
|
def __enter__(self) -> "DummySpan":
|
|
return self
|
|
|
|
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> bool:
|
|
return False
|
|
|
|
|
|
class DummyTracer:
|
|
def __init__(self, span: DummySpan) -> None:
|
|
self._span = span
|
|
self.last_name: Optional[str] = None
|
|
self.last_attributes: Optional[Dict[str, Any]] = None
|
|
|
|
def create_span(self, name: str, attributes: Optional[Dict[str, Any]] = None, **kwargs: Any) -> DummySpan:
|
|
self.last_name = name
|
|
self.last_attributes = attributes or {}
|
|
return self._span
|
|
|
|
|
|
def _stub_tracer(monkeypatch: pytest.MonkeyPatch, span: DummySpan) -> DummyTracer:
|
|
tracer = DummyTracer(span)
|
|
|
|
def fake_get_tracer(*_: Any, **__: Any) -> DummyTracer:
|
|
return tracer
|
|
|
|
monkeypatch.setattr(object_module, "get_active_tracer", fake_get_tracer)
|
|
return tracer
|
|
|
|
|
|
def test_encode_object_for_primitives() -> None:
|
|
encoded = encode_object("hello")
|
|
|
|
assert encoded == {
|
|
LightningSpanAttributes.OBJECT_TYPE.value: "str",
|
|
LightningSpanAttributes.OBJECT_LITERAL.value: "hello",
|
|
}
|
|
|
|
|
|
def test_encode_object_for_bytes() -> None:
|
|
payload = b"binary"
|
|
encoded = encode_object(payload)
|
|
expected_literal = base64.b64encode(payload).decode("utf-8")
|
|
|
|
assert encoded == {
|
|
LightningSpanAttributes.OBJECT_TYPE.value: "bytes",
|
|
LightningSpanAttributes.OBJECT_LITERAL.value: expected_literal,
|
|
}
|
|
|
|
|
|
def test_encode_object_for_dict_serializes_json() -> None:
|
|
payload = {"key": 1, "nested": [1, 2]}
|
|
encoded = encode_object(payload)
|
|
|
|
assert encoded[LightningSpanAttributes.OBJECT_TYPE.value] == "dict"
|
|
assert json.loads(encoded[LightningSpanAttributes.OBJECT_JSON.value]) == payload
|
|
|
|
|
|
def test_encode_object_raises_for_unserializable() -> None:
|
|
with pytest.raises(RuntimeError):
|
|
encode_object(object())
|
|
|
|
|
|
def test_get_object_value_from_json_attribute() -> None:
|
|
payload = {"answer": 42}
|
|
span = FakeSpan(
|
|
attributes={
|
|
LightningSpanAttributes.OBJECT_JSON.value: json.dumps(payload),
|
|
LightningSpanAttributes.OBJECT_TYPE.value: "dict",
|
|
}
|
|
)
|
|
|
|
assert get_object_value(cast(SpanLike, span)) == payload
|
|
|
|
|
|
def test_get_object_value_from_literal_types() -> None:
|
|
span = FakeSpan(
|
|
attributes={
|
|
LightningSpanAttributes.OBJECT_LITERAL.value: "123",
|
|
LightningSpanAttributes.OBJECT_TYPE.value: "int",
|
|
}
|
|
)
|
|
assert get_object_value(cast(SpanLike, span)) == 123
|
|
|
|
span = FakeSpan(
|
|
attributes={
|
|
LightningSpanAttributes.OBJECT_LITERAL.value: "3.14",
|
|
LightningSpanAttributes.OBJECT_TYPE.value: "float",
|
|
}
|
|
)
|
|
assert get_object_value(cast(SpanLike, span)) == pytest.approx(3.14) # type: ignore
|
|
|
|
span = FakeSpan(
|
|
attributes={
|
|
LightningSpanAttributes.OBJECT_LITERAL.value: "true",
|
|
LightningSpanAttributes.OBJECT_TYPE.value: "bool",
|
|
}
|
|
)
|
|
assert get_object_value(cast(SpanLike, span)) is True
|
|
|
|
literal = base64.b64encode(b"hi").decode("utf-8")
|
|
span = FakeSpan(
|
|
attributes={
|
|
LightningSpanAttributes.OBJECT_LITERAL.value: literal,
|
|
LightningSpanAttributes.OBJECT_TYPE.value: "bytes",
|
|
}
|
|
)
|
|
assert get_object_value(cast(SpanLike, span)) == b"hi"
|
|
|
|
|
|
def test_get_object_value_returns_none_when_missing() -> None:
|
|
span = FakeSpan(attributes={})
|
|
|
|
assert get_object_value(cast(SpanLike, span)) is None
|
|
|
|
|
|
def test_get_object_value_raises_for_invalid_json() -> None:
|
|
span = FakeSpan(
|
|
attributes={
|
|
LightningSpanAttributes.OBJECT_JSON.value: "not json",
|
|
}
|
|
)
|
|
|
|
with pytest.raises(RuntimeError):
|
|
get_object_value(cast(SpanLike, span))
|
|
|
|
|
|
def test_emit_object_serializes_payload(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
span = DummySpan()
|
|
tracer = _stub_tracer(monkeypatch, span)
|
|
|
|
payload = {"foo": "bar", "baz": [1, 2, 3]}
|
|
emit_object(payload)
|
|
|
|
assert tracer.last_name == AGL_OBJECT
|
|
assert tracer.last_attributes is not None
|
|
assert json.loads(tracer.last_attributes[LightningSpanAttributes.OBJECT_JSON.value]) == payload
|
|
|
|
|
|
def test_emit_object_requires_json_serializable() -> None:
|
|
with pytest.raises(RuntimeError):
|
|
emit_object(object()) # type: ignore[arg-type]
|
|
|
|
|
|
def test_get_object_value_raises_for_unknown_literal_type() -> None:
|
|
span = FakeSpan(
|
|
attributes={
|
|
LightningSpanAttributes.OBJECT_LITERAL.value: "value",
|
|
LightningSpanAttributes.OBJECT_TYPE.value: "complex",
|
|
}
|
|
)
|
|
|
|
with pytest.raises(RuntimeError):
|
|
get_object_value(cast(SpanLike, span))
|
|
|
|
|
|
def test_emit_object_flattens_attributes(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
span = DummySpan()
|
|
tracer = _stub_tracer(monkeypatch, span)
|
|
|
|
emit_object({"foo": "bar"}, attributes={"meta": {"tag": "foo"}, "labels": ["x", "y"]})
|
|
|
|
assert tracer.last_attributes is not None
|
|
assert tracer.last_attributes["meta.tag"] == "foo"
|
|
assert tracer.last_attributes["labels"] == ["x", "y"]
|
|
|
|
|
|
def test_emit_object_propagate_false(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
def fail_get_active_tracer() -> DummyTracer:
|
|
raise AssertionError("Should not resolve tracer when propagate=False")
|
|
|
|
monkeypatch.setattr(object_module, "get_active_tracer", fail_get_active_tracer)
|
|
|
|
emit_object({"foo": "bar"}, propagate=False)
|