Skip to content

Commit d2f4342

Browse files
authored
Add workflow content capture to SpanEmitter and corresponding tests (#19)
1 parent eec047f commit d2f4342

File tree

2 files changed

+50
-1
lines changed

2 files changed

+50
-1
lines changed

util/opentelemetry-util-genai/src/opentelemetry/util/genai/emitters/span.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,8 @@ def _start_workflow(self, workflow: Workflow) -> None:
388388
self._attach_span(workflow, span, cm)
389389

390390
# Set workflow attributes
391+
# TODO: Align to enum when semconvs is updated.
392+
span.set_attribute(GenAI.GEN_AI_OPERATION_NAME, "invoke_workflow")
391393
span.set_attribute(GEN_AI_WORKFLOW_NAME, workflow.name)
392394
if workflow.workflow_type:
393395
span.set_attribute(GEN_AI_WORKFLOW_TYPE, workflow.workflow_type)

util/opentelemetry-util-genai/tests/test_span_metric_event_generator.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1+
import json
2+
13
import pytest
24

35
from opentelemetry.sdk.trace import TracerProvider
6+
from opentelemetry.semconv._incubating.attributes import (
7+
gen_ai_attributes as GenAI,
8+
)
9+
from opentelemetry.util.genai.attributes import (
10+
GEN_AI_INPUT_MESSAGES,
11+
GEN_AI_OUTPUT_MESSAGES,
12+
)
413
from opentelemetry.util.genai.emitters.composite import CompositeEmitter
514
from opentelemetry.util.genai.emitters.content_events import (
615
ContentEventsEmitter,
@@ -12,6 +21,7 @@
1221
LLMInvocation,
1322
OutputMessage,
1423
Text,
24+
Workflow,
1525
)
1626

1727

@@ -75,7 +85,7 @@ class _RecordingEvaluationEmitter:
7585
role = "evaluation"
7686

7787
def __init__(self) -> None:
78-
self.call_log = []
88+
self.call_log: list[tuple[str, object]] = []
7989

8090
def on_evaluation_results(self, results, obj=None):
8191
self.call_log.append(("results", list(results)))
@@ -167,3 +177,40 @@ def test_span_emitter_filters_non_gen_ai_attributes():
167177
assert "traceloop.association.properties.ls_temperature" not in attrs
168178
assert all(not key.startswith("traceloop.") for key in attrs.keys())
169179
assert any(key.startswith("gen_ai.") for key in attrs)
180+
181+
182+
def test_span_emitter_workflow_captures_content():
183+
provider = TracerProvider()
184+
tracer = provider.get_tracer(__name__)
185+
emitter = SpanEmitter(tracer=tracer, capture_content=True)
186+
187+
workflow = Workflow(
188+
name="trip_planner",
189+
workflow_type="sequential",
190+
initial_input="Plan a trip to Rome",
191+
final_output="Here is your itinerary",
192+
)
193+
194+
emitter.on_start(workflow)
195+
emitter.on_end(workflow)
196+
197+
span = workflow.span
198+
assert span is not None
199+
attrs = getattr(span, "attributes", None) or getattr(
200+
span, "_attributes", {}
201+
)
202+
203+
operation_value = attrs.get(GenAI.GEN_AI_OPERATION_NAME)
204+
assert operation_value == "invoke_workflow"
205+
206+
input_messages_raw = attrs.get(GEN_AI_INPUT_MESSAGES)
207+
assert input_messages_raw is not None
208+
input_messages = json.loads(input_messages_raw)
209+
assert input_messages[0]["parts"][0]["content"] == "Plan a trip to Rome"
210+
211+
output_messages_raw = attrs.get(GEN_AI_OUTPUT_MESSAGES)
212+
assert output_messages_raw is not None
213+
output_messages = json.loads(output_messages_raw)
214+
assert (
215+
output_messages[0]["parts"][0]["content"] == "Here is your itinerary"
216+
)

0 commit comments

Comments
 (0)