Conversation
Observability integrations sometimes need to set a trace-level attribute that is only known late in a call, after additional_span_attributes has already been applied at conversation start. Attaching a call recording is the motivating case: the audio only exists once the call ends, and some backends treat a persisted trace as immutable, so the reference has to be written while the conversation span is still open. The observer already owned the span but only exposed turn-level contexts, so integrations had to reach for _conversation_span. Add a read-only property.
There was a problem hiding this comment.
Pull request overview
Adds a small, targeted public accessor on TurnTraceObserver so integrations can safely reach the live OpenTelemetry “conversation” span while it’s still open, enabling late-arriving trace attributes (e.g., call recording references) without relying on private attributes.
Changes:
- Added a read-only
conversation_spanproperty toTurnTraceObserverthat exposes the currently-open conversation span (orNonebefore start / after end). - Added a unit test covering the new accessor and validating that attributes set through it are exported.
- Added a changelog fragment documenting the new API surface.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/pipecat/utils/tracing/turn_trace_observer.py |
Exposes the open conversation span via a new conversation_span property with clarifying docstring. |
tests/test_turn_trace_observer.py |
Adds test_conversation_span_accessor to verify None before start, mutability while open, and None after end. |
changelog/5272.added.md |
Documents the new public accessor and its intended usage window. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report✅ All modified and coverable lines are covered by tests.
🚀 New features to boost your workflow:
|
|
Closing. The consumer this was resurrected for (the Langfuse recording uploader) now lives in core (#5285), where it reads the observer's conversation span as same-package internals, and in practice that path barely runs: turn 1 starts with the conversation, so trace ids resolve from retained turn contexts. What remains is the secondary use case (setting conversation-span attributes learned late in a call), which has no concrete consumer today. Happy to resubmit if one shows up. |
What
Adds a read-only
conversation_spanproperty toTurnTraceObserver.Why
additional_span_attributesis applied once, when the conversation span is created on thefirst
StartFrame. That covers attributes you know up front, but not ones you only learnlate in a call, and the observer exposes
get_current_turn_context()andget_turn_context()for turn spans while offering nothing for the conversation. Integrations that want the
conversation span end up reading
_conversation_spandirectly.Scope note, please read before reviewing
This started as a prerequisite for attaching call recordings to Langfuse traces, where I
expected to have to write a media reference onto the conversation span before it closed. That
turned out to be unnecessary: Langfuse links media by trace and observation id and renders a
player from the link alone, so nothing has to be written into the span and no span has to be
open. The downstream example (pipecat-ai/pipecat-examples#240) now uses only public API,
turn_tracking_observerandget_turn_context(), and does not need this property.Update: it has a concrete consumer again. The Langfuse recording example resolves the
trace id by walking retained turn contexts, falling back to
get_current_turn_context(),and finally to the conversation span, where it has to reach for the private
_conversation_spanon today's releases. Its_resolve_trace_idalready prefers thisproperty when present, and once this ships that whole fallback chain collapses to reading
conversation_span.get_span_context().trace_id. That uploader is now moving into coreitself (#5285), where its trace-id resolution has the same fallback and would simplify the
same way.
Notes
Noneboth before the conversation starts and after it ends, socallers cannot accidentally write to a closed span.
the easy mistake to make here.
Test plan
test_conversation_span_accessorintests/test_turn_trace_observer.py: asserts theproperty is
Nonebefore the conversation exists, that an attribute set through it lands onthe exported span, and that it returns to
Noneafterend_conversation_tracing().uv run pytest tests/test_turn_trace_observer.py→ 11 passed.