66"""
77
88import re
9- from typing import Any , Dict , Generator , List , Literal , Optional , TypedDict , Union , cast
9+ from typing import (
10+ Any ,
11+ Dict ,
12+ Generator ,
13+ List ,
14+ Literal ,
15+ Mapping ,
16+ Optional ,
17+ Tuple ,
18+ TypedDict ,
19+ Union ,
20+ cast ,
21+ )
1022
1123from opentelemetry import (
1224 baggage ,
2941from langfuse ._client .attributes import LangfuseOtelSpanAttributes
3042from langfuse ._client .constants import LANGFUSE_SDK_EXPERIMENT_ENVIRONMENT
3143from langfuse .logger import langfuse_logger
44+ from langfuse .model import PromptClient
3245
3346PropagatedKeys = Literal [
3447 "user_id" ,
3851 "tags" ,
3952 "trace_name" ,
4053 "environment" ,
54+ "prompt_name" ,
55+ "prompt_version" ,
4156]
4257
4358InternalPropagatedKeys = Literal [
5873 "tags" ,
5974 "trace_name" ,
6075 "environment" ,
76+ "prompt_name" ,
77+ "prompt_version" ,
6178 "experiment_id" ,
6279 "experiment_name" ,
6380 "experiment_metadata" ,
@@ -103,6 +120,7 @@ def propagate_attributes(
103120 tags : Optional [List [str ]] = None ,
104121 trace_name : Optional [str ] = None ,
105122 environment : Optional [str ] = None ,
123+ prompt : Optional [Union [PromptClient , Mapping [str , Any ]]] = None ,
106124 as_baggage : bool = False ,
107125) -> _AgnosticContextManager [Any ]:
108126 """Propagate trace-level attributes to all spans created within this context.
@@ -139,6 +157,17 @@ def propagate_attributes(
139157 tags: List of tags to categorize the group of observations
140158 trace_name: Name to assign to the trace. Must be US-ASCII string, ≤200 characters.
141159 Use this to set a consistent trace name for all spans created within this context.
160+ prompt: Langfuse prompt to link to generations created within this context.
161+ Accepts a `PromptClient` returned by `langfuse.get_prompt(...)` or any
162+ object/dict exposing `name` (string) and `version` (integer) — e.g.
163+ `{"name": "my-prompt", "version": 3}`. This is the recommended way to
164+ link prompts to generations emitted by auto-instrumentation libraries
165+ (e.g. LiteLLM's `langfuse_otel`, OpenAI Agents SDK, OpenInference)
166+ where you don't create the generation via the Langfuse SDK yourself.
167+ The prompt link is only applied to generation-type observations by the
168+ Langfuse backend. Fallback prompts are never linked. An explicit
169+ `prompt` passed to `start_observation` / `update_current_generation`
170+ takes precedence over the propagated one.
142171 environment: Langfuse environment to assign to spans created in this context.
143172 Must be a lowercase alphanumeric string with optional hyphens or underscores,
144173 must be ≤40 characters, and must not start with "langfuse". This maps to
@@ -184,6 +213,24 @@ def propagate_attributes(
184213 ...
185214 ```
186215
216+ Prompt linking with auto-instrumented libraries:
217+
218+ ```python
219+ from langfuse import Langfuse
220+
221+ langfuse = Langfuse()
222+ prompt = langfuse.get_prompt("my-prompt")
223+
224+ with langfuse.propagate_attributes(prompt=prompt):
225+ # Generations emitted by auto-instrumentation (LiteLLM langfuse_otel,
226+ # OpenAI Agents SDK, OpenInference, ...) within this context are
227+ # linked to the prompt version.
228+ completion = litellm.completion(
229+ model="gpt-4o",
230+ messages=prompt.compile(topic="chickens"),
231+ )
232+ ```
233+
187234 Late propagation (anti-pattern):
188235
189236 ```python
@@ -244,6 +291,7 @@ def propagate_attributes(
244291 tags = tags ,
245292 trace_name = trace_name ,
246293 environment = environment ,
294+ prompt = prompt ,
247295 as_baggage = as_baggage ,
248296 )
249297
@@ -258,6 +306,7 @@ def _propagate_attributes(
258306 tags : Optional [List [str ]] = None ,
259307 trace_name : Optional [str ] = None ,
260308 environment : Optional [str ] = None ,
309+ prompt : Optional [Union [PromptClient , Mapping [str , Any ]]] = None ,
261310 as_baggage : bool = False ,
262311 experiment : Optional [PropagatedExperimentAttributes ] = None ,
263312) -> Generator [Any , Any , Any ]:
@@ -273,6 +322,25 @@ def _propagate_attributes(
273322 "environment" : environment ,
274323 }
275324
325+ prompt_info = _extract_propagated_prompt (prompt ) if prompt is not None else None
326+ if prompt_info is not None :
327+ prompt_name , prompt_version = prompt_info
328+
329+ context = _set_propagated_attribute (
330+ key = "prompt_name" ,
331+ value = prompt_name ,
332+ context = context ,
333+ span = current_span ,
334+ as_baggage = as_baggage ,
335+ )
336+ context = _set_propagated_attribute (
337+ key = "prompt_version" ,
338+ value = prompt_version ,
339+ context = context ,
340+ span = current_span ,
341+ as_baggage = as_baggage ,
342+ )
343+
276344 propagated_metadata_attributes : Dict [str , Optional [Dict [str , Any ]]] = {
277345 "metadata" : metadata ,
278346 }
@@ -336,10 +404,51 @@ def _propagate_attributes(
336404 _detach_context_token_safely (token )
337405
338406
407+ def _extract_propagated_prompt (
408+ prompt : Union [PromptClient , Mapping [str , Any ]],
409+ ) -> Optional [Tuple [str , int ]]:
410+ """Extract and validate (name, version) from a prompt-like value.
411+
412+ Accepts a PromptClient or any mapping/object exposing `name` and `version`.
413+ Returns None (with a warning) if the value is invalid or a fallback prompt.
414+ """
415+ if isinstance (prompt , Mapping ):
416+ name = prompt .get ("name" )
417+ version = prompt .get ("version" )
418+ is_fallback = bool (prompt .get ("is_fallback" , False ))
419+ else :
420+ name = getattr (prompt , "name" , None )
421+ version = getattr (prompt , "version" , None )
422+ is_fallback = bool (getattr (prompt , "is_fallback" , False ))
423+
424+ if is_fallback :
425+ langfuse_logger .debug (
426+ "Propagated prompt is a fallback prompt. Skipping prompt linking."
427+ )
428+ return None
429+
430+ if not isinstance (name , str ) or not name :
431+ langfuse_logger .warning (
432+ "Propagated 'prompt' has no valid 'name' (non-empty string required). Dropping prompt link."
433+ )
434+ return None
435+
436+ if isinstance (version , str ) and version .isdigit ():
437+ version = int (version )
438+
439+ if not isinstance (version , int ) or isinstance (version , bool ):
440+ langfuse_logger .warning (
441+ "Propagated 'prompt' has no valid 'version' (integer required). Dropping prompt link."
442+ )
443+ return None
444+
445+ return name , version
446+
447+
339448def _get_propagated_attributes_from_context (
340449 context : otel_context_api .Context ,
341- ) -> Dict [str , Union [str , List [str ]]]:
342- propagated_attributes : Dict [str , Union [str , List [str ]]] = {}
450+ ) -> Dict [str , Union [str , int , List [str ]]]:
451+ propagated_attributes : Dict [str , Union [str , int , List [str ]]] = {}
343452
344453 # Handle baggage
345454 baggage_entries = baggage .get_all (context = context )
@@ -362,6 +471,14 @@ def _get_propagated_attributes_from_context(
362471 propagated_attributes [span_key ] = validated_environment
363472 continue
364473
474+ if (
475+ span_key == LangfuseOtelSpanAttributes .OBSERVATION_PROMPT_VERSION
476+ and isinstance (baggage_value , str )
477+ and baggage_value .isdigit ()
478+ ):
479+ propagated_attributes [span_key ] = int (baggage_value )
480+ continue
481+
365482 propagated_attributes [span_key ] = (
366483 baggage_value
367484 if isinstance (baggage_value , (str , list ))
@@ -398,7 +515,7 @@ def _get_propagated_attributes_from_context(
398515 span_key = _get_propagated_span_key (key )
399516
400517 propagated_attributes [span_key ] = (
401- value if isinstance (value , (str , list )) else str (value )
518+ value if isinstance (value , (str , int , list )) else str (value )
402519 )
403520
404521 if (
@@ -415,7 +532,7 @@ def _get_propagated_attributes_from_context(
415532def _set_propagated_attribute (
416533 * ,
417534 key : str ,
418- value : Union [str , List [str ], Dict [str , str ]],
535+ value : Union [str , int , List [str ], Dict [str , str ]],
419536 context : otel_context_api .Context ,
420537 span : otel_trace_api .Span ,
421538 as_baggage : bool ,
@@ -472,7 +589,9 @@ def _set_propagated_attribute(
472589 )
473590 else :
474591 context = otel_baggage_api .set_baggage (
475- name = baggage_key , value = value , context = context
592+ name = baggage_key ,
593+ value = str (value ) if isinstance (value , int ) else value ,
594+ context = context ,
476595 )
477596
478597 return context
@@ -622,6 +741,8 @@ def _get_propagated_span_key(key: str) -> str:
622741 "trace_name" : LangfuseOtelSpanAttributes .TRACE_NAME ,
623742 "environment" : LangfuseOtelSpanAttributes .ENVIRONMENT ,
624743 "metadata" : LangfuseOtelSpanAttributes .TRACE_METADATA ,
744+ "prompt_name" : LangfuseOtelSpanAttributes .OBSERVATION_PROMPT_NAME ,
745+ "prompt_version" : LangfuseOtelSpanAttributes .OBSERVATION_PROMPT_VERSION ,
625746 "experiment_id" : LangfuseOtelSpanAttributes .EXPERIMENT_ID ,
626747 "experiment_name" : LangfuseOtelSpanAttributes .EXPERIMENT_NAME ,
627748 "experiment_metadata" : LangfuseOtelSpanAttributes .EXPERIMENT_METADATA ,
0 commit comments