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
Original file line number Diff line number Diff line change
Expand Up @@ -957,9 +957,12 @@ def _start_realtime_span(self, span_data, parent_context, span_name, operation):
def _end_generation_span(self, otel_span, span_data, trace_content):
"""Handle on_span_end logic for generation/response spans."""
input_data = getattr(span_data, "input", [])
response = getattr(span_data, "response", None)
if trace_content and response and getattr(response, "instructions", None):
existing = input_data if isinstance(input_data, list) else []
input_data = [{"role": "system", "content": response.instructions}] + existing
_extract_prompt_attributes(otel_span, input_data, trace_content)

response = getattr(span_data, "response", None)
tools = getattr(span_data, "tools", None) or (
getattr(response, "tools", None) if response else None
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,13 @@ def test_agent_spans(exporter, test_agent):
assert response_span.attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] == "openai"

# Test input messages (JSON array with parts-based schema)
# index 0 is the system message (agent instructions), index 1 is the user message
input_messages = json.loads(response_span.attributes[GenAIAttributes.GEN_AI_INPUT_MESSAGES])
assert input_messages[0]["role"] == "user"
assert "parts" in input_messages[0], "Input messages must use parts-based schema"
assert input_messages[0]["parts"][0]["type"] == "text"
assert input_messages[0]["parts"][0]["content"] == "What is AI?"
assert input_messages[0]["role"] == "system"
user_message = next(m for m in input_messages if m["role"] == "user")
assert "parts" in user_message, "Input messages must use parts-based schema"
assert user_message["parts"][0]["type"] == "text"
assert user_message["parts"][0]["content"] == "What is AI?"

# Test usage tokens
assert response_span.attributes[GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS] is not None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,136 @@ def test_no_tool_definitions_when_content_gated(self, tracer_and_exporter, proce

otel_span.end()

def test_instructions_prepended_as_system_message(self, tracer_and_exporter, processor):
"""response.instructions must appear as the first message with role=system."""
tracer, _ = tracer_and_exporter
otel_span = tracer.start_span("test-gen")

response = MagicMock()
response.instructions = "You are a helpful assistant."
response.tools = None
response.output = []
response.model = None
response.id = None
response.temperature = None
response.max_output_tokens = None
response.top_p = None
response.frequency_penalty = None
response.finish_reason = None
response.usage = None

span_data = MagicMock()
span_data.input = [{"role": "user", "content": "Hello"}]
span_data.response = response
span_data.tools = None

processor._end_generation_span(otel_span, span_data, trace_content=True)

raw = otel_span.attributes.get(GenAIAttributes.GEN_AI_INPUT_MESSAGES)
assert raw is not None
messages = json.loads(raw)
assert messages[0]["role"] == "system"
assert messages[0]["parts"][0]["content"] == "You are a helpful assistant."
assert messages[1]["role"] == "user"

otel_span.end()

def test_instructions_not_prepended_when_content_gated(self, tracer_and_exporter, processor):
"""response.instructions must NOT appear when trace_content=False."""
tracer, _ = tracer_and_exporter
otel_span = tracer.start_span("test-gen")

response = MagicMock()
response.instructions = "You are a helpful assistant."
response.tools = None
response.output = []
response.model = None
response.id = None
response.temperature = None
response.max_output_tokens = None
response.top_p = None
response.frequency_penalty = None
response.finish_reason = None
response.usage = None

span_data = MagicMock()
span_data.input = [{"role": "user", "content": "Hello"}]
span_data.response = response
span_data.tools = None

processor._end_generation_span(otel_span, span_data, trace_content=False)

assert GenAIAttributes.GEN_AI_INPUT_MESSAGES not in otel_span.attributes

otel_span.end()

def test_instructions_only_with_empty_input(self, tracer_and_exporter, processor):
"""Empty input + instructions → single system message in output."""
tracer, _ = tracer_and_exporter
otel_span = tracer.start_span("test-gen")

response = MagicMock()
response.instructions = "You are a helpful assistant."
response.tools = None
response.output = []
response.model = None
response.id = None
response.temperature = None
response.max_output_tokens = None
response.top_p = None
response.frequency_penalty = None
response.finish_reason = None
response.usage = None

span_data = MagicMock()
span_data.input = []
span_data.response = response
span_data.tools = None

processor._end_generation_span(otel_span, span_data, trace_content=True)

raw = otel_span.attributes.get(GenAIAttributes.GEN_AI_INPUT_MESSAGES)
assert raw is not None
messages = json.loads(raw)
assert len(messages) == 1
assert messages[0]["role"] == "system"
assert messages[0]["parts"][0]["content"] == "You are a helpful assistant."

otel_span.end()

def test_empty_instructions_skipped(self, tracer_and_exporter, processor):
"""instructions == "" is falsy and must be skipped — no system message prepended."""
tracer, _ = tracer_and_exporter
otel_span = tracer.start_span("test-gen")

response = MagicMock()
response.instructions = ""
response.tools = None
response.output = []
response.model = None
response.id = None
response.temperature = None
response.max_output_tokens = None
response.top_p = None
response.frequency_penalty = None
response.finish_reason = None
response.usage = None

span_data = MagicMock()
span_data.input = [{"role": "user", "content": "Hello"}]
span_data.response = response
span_data.tools = None

processor._end_generation_span(otel_span, span_data, trace_content=True)

raw = otel_span.attributes.get(GenAIAttributes.GEN_AI_INPUT_MESSAGES)
assert raw is not None
messages = json.loads(raw)
assert len(messages) == 1
assert messages[0]["role"] == "user"

otel_span.end()

Comment thread
coderabbitai[bot] marked this conversation as resolved.
def test_extracts_response_attributes(self, tracer_and_exporter, processor):
"""Must extract response model, id, etc."""
tracer, exporter = tracer_and_exporter
Expand Down
Loading