Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/agentevals/trace_attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,6 @@
GENAI_ATTRIBUTE_ALIASES: dict[str, list[str]] = {
OTEL_GENAI_PROVIDER_NAME: [OTEL_GENAI_SYSTEM],
}

# agentevals custom attributes (repository-specific, outside OTel semconv)
AGENTEVALS_SESSION_ID = "agentevals.session_id"
35 changes: 20 additions & 15 deletions src/agentevals/utils/log_enrichment.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from collections import defaultdict

from ..trace_attrs import (
OTEL_GENAI_AGENT_NAME,
AGENTEVALS_SESSION_ID,
OTEL_GENAI_INPUT_MESSAGES,
OTEL_GENAI_OUTPUT_MESSAGES,
)
Expand All @@ -28,7 +28,7 @@ def enrich_spans_with_logs(spans: list[dict], logs: list[dict], session_id: str
Args:
spans: List of OTLP span dictionaries
logs: List of GenAI log event dictionaries
session_id: Optional session ID to add as agent.name attribute
session_id: Optional session ID to add as the agentevals.session_id attribute

Returns:
List of enriched span dictionaries with message attributes added
Expand Down Expand Up @@ -88,6 +88,22 @@ def _extract_messages_from_logs(
return input_messages, output_messages


def _append_session_id(attrs: list[dict], session_id: str | None) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One q: what reads agentevals.session.id downstream? I only see it written, never read. There might be benefits for OTLP export consumers, just want to confirm it's intentional and not dead now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is write-only inside runtime code. I found no downstream internal reader in the repo beyond being appended during enrichment. So this looks intentional as exported metadata for external OTLP consumers rather than an in-repo dependency today.

"""Append the session ID attribute to *attrs* in place, if present.

Written to the custom ``agentevals.session_id`` attribute rather than
``gen_ai.agent.name`` so real agent names (from the SDK or OTLP) are
never overwritten.
"""
if session_id:
attrs.append(
{
"key": AGENTEVALS_SESSION_ID,
"value": {"stringValue": session_id},
}
)


def _inject_messages(
span: dict,
input_messages: list[dict],
Expand All @@ -113,13 +129,7 @@ def _inject_messages(
"value": {"stringValue": json.dumps(output_messages)},
}
)
if session_id:
attrs.append(
{
"key": OTEL_GENAI_AGENT_NAME,
"value": {"stringValue": session_id},
}
)
_append_session_id(attrs, session_id)

return span_copy

Expand Down Expand Up @@ -148,12 +158,7 @@ def _enrich_per_span(
span_copy = span.copy()
if session_id:
attrs = list(span_copy.get("attributes", []))
attrs.append(
{
"key": OTEL_GENAI_AGENT_NAME,
"value": {"stringValue": session_id},
}
)
_append_session_id(attrs, session_id)
span_copy["attributes"] = attrs
enriched.append(span_copy)

Expand Down
25 changes: 21 additions & 4 deletions tests/test_log_enrichment.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ def test_session_id_injected(self):
logs = [_make_log("gen_ai.user.message", {"content": "hi"})]
result = enrich_spans_with_logs(spans, logs, session_id="my-session")

agent_name = _get_injected_attr(result[0], "gen_ai.agent.name", parse_json=False)
assert agent_name == "my-session"
session_attr = _get_injected_attr(result[0], "agentevals.session_id", parse_json=False)
assert session_attr == "my-session"

def test_choice_extracts_nested_content(self):
spans = [_make_span()]
Expand Down Expand Up @@ -139,6 +139,23 @@ def test_logs_matched_to_correct_span(self):
msgs_s2 = _get_injected_attr(result[1], "gen_ai.input.messages")
assert msgs_s2 == [{"role": "user", "content": "question 2"}]

def test_agent_name_preserved_with_session_id(self):
"""Regression: real gen_ai.agent.name attr must survive enrichment,
session_id lands in its own attr, doesn't clobber it."""
span = _make_span(
"s1",
attrs=[{"key": "gen_ai.agent.name", "value": {"stringValue": "roll_die_agent"}}],
)
logs = [_make_log("gen_ai.user.message", {"content": "hi"}, span_id="s1")]
result = enrich_spans_with_logs([span], logs, session_id="sess-123")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this catches the regression. Let's do it via flatten_otlp_attributes(result[0]["attributes"])["gen_ai.agent.name"] (or assert there's exactly one gen_ai.agent.name attr). The agentevals.session_id assertion is fine!


agent_name_attrs = [a for a in result[0]["attributes"] if a["key"] == "gen_ai.agent.name"]
assert len(agent_name_attrs) == 1, f"expected exactly one gen_ai.agent.name attr, found {len(agent_name_attrs)}"
assert agent_name_attrs[0]["value"]["stringValue"] == "roll_die_agent"

session_attr = _get_injected_attr(result[0], "agentevals.session_id", parse_json=False)
assert session_attr == "sess-123"

def test_span_without_logs_not_enriched(self):
spans = [_make_span("s1"), _make_span("s2")]
logs = [
Expand All @@ -156,8 +173,8 @@ def test_session_id_on_all_spans(self):
]
result = enrich_spans_with_logs(spans, logs, session_id="test")

assert _get_injected_attr(result[0], "gen_ai.agent.name", parse_json=False) == "test"
assert _get_injected_attr(result[1], "gen_ai.agent.name", parse_json=False) == "test"
assert _get_injected_attr(result[0], "agentevals.session_id", parse_json=False) == "test"
assert _get_injected_attr(result[1], "agentevals.session_id", parse_json=False) == "test"

def test_tool_calls_in_assistant_message(self):
spans = [_make_span("s1")]
Expand Down