Skip to content
Open
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
86 changes: 86 additions & 0 deletions python/frameworks/openai/tests/test_span_io_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""
Regression test for issue #151: multi-turn conversation input was being
dropped from the span. _process_input_data set INPUT_VALUE three times
on the same span, and the last two writes overwrote the first, correct
one, leaving only the first message's text on the span.
"""

import json

import pytest
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
InMemorySpanExporter,
)

from fi_instrumentation.fi_types import SpanAttributes
from traceai_openai._span_io_handler import _process_input_data
from traceai_openai._with_span import _WithSpan


@pytest.fixture
def recorder():
exporter = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))
tracer = provider.get_tracer(__name__)
return tracer, exporter


def _get_input_value(exporter):
spans = exporter.get_finished_spans()
assert len(spans) == 1
return spans[0].attributes.get(SpanAttributes.INPUT_VALUE)


def test_multi_message_input_is_not_dropped(recorder):
tracer, exporter = recorder
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is the capital of France?"},
{"role": "assistant", "content": "Paris."},
{"role": "user", "content": "And Germany?"},
]

with tracer.start_as_current_span("openai.chat") as span:
_process_input_data(messages, _WithSpan(span))

input_value = _get_input_value(exporter)
assert input_value is not None

parsed = json.loads(input_value)
assert len(parsed) == len(messages), (
"Expected all messages to survive in INPUT_VALUE, but only "
f"{len(parsed)} of {len(messages)} were present."
)
assert [m["content"] for m in parsed] == [m["content"] for m in messages]


def test_multimodal_message_list_preserves_all_messages(recorder):
tracer, exporter = recorder
messages = [
{"role": "user", "content": "Describe this image."},
{
"role": "user",
"content": [
{"type": "text", "text": "What is in this picture?"},
{
"type": "image_url",
"image_url": {"url": "https://example.com/cat.png"},
},
],
},
]

with tracer.start_as_current_span("openai.chat") as span:
_process_input_data(messages, _WithSpan(span))

input_value = _get_input_value(exporter)
parsed = json.loads(input_value)
assert len(parsed) == 2

spans = exporter.get_finished_spans()
images_value = spans[0].attributes.get(SpanAttributes.INPUT_IMAGES)
assert images_value is not None
assert json.loads(images_value) == ["https://example.com/cat.png"]
17 changes: 3 additions & 14 deletions python/frameworks/openai/traceai_openai/_span_io_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ def _process_input_data(input_data: Any, span: _WithSpan) -> None:
if isinstance(input_data, list):
input_content = []
input_images = []
eval_input = []
for msg in input_data:
if isinstance(msg, dict):
msg_content = msg.get("content", "")
Expand All @@ -29,18 +28,11 @@ def _process_input_data(input_data: Any, span: _WithSpan) -> None:
if isinstance(item, dict):
if item.get("type") == "text":
filtered_content.append(item)
eval_input.append(item.get("text", ""))
elif item.get("type") == "image_url":
url_data = item.get("image_url", {})
url = url_data.get("url")
if url:
input_images.append(url)
image_index = len(input_images) - 1
eval_input.append(
"{{"
+ f"{SpanAttributes.INPUT_IMAGES}.{image_index}"
+ "}}"
)
if filtered_content:
msg_dict = msg.copy()
msg_dict["content"] = filtered_content
Expand All @@ -49,18 +41,15 @@ def _process_input_data(input_data: Any, span: _WithSpan) -> None:
continue
else:
input_content.append(msg)
eval_input.append(msg_content)
# INPUT_VALUE holds the full multi-message conversation as JSON.
# Do not overwrite it later with a single-message string; that was
# the cause of multi-turn context being dropped from the span (#151).
if input_content:
input_value = json.dumps(input_content, ensure_ascii=False)
span.set_attribute(SpanAttributes.INPUT_VALUE, input_value)
if input_images:
images_value = json.dumps(input_images, ensure_ascii=False)
span.set_attribute(SpanAttributes.INPUT_IMAGES, images_value)
if eval_input:
eval_input_str = " \n ".join(map(str, eval_input))
span.set_attribute(SpanAttributes.INPUT_VALUE, eval_input_str)
if eval_input and len(eval_input) > 0:
span.set_attribute(SpanAttributes.INPUT_VALUE, eval_input[0])
else:
try:
input_str = json.dumps(input_data, ensure_ascii=False).strip()
Expand Down