From e5aaa4c4ebc6eda8d297140587cfaadd95498afb Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Fri, 20 Mar 2026 13:12:40 -0400 Subject: [PATCH] fix: read system_instruction from _settings instead of removed attribute Replace adapter-based extraction in traced_llm with direct reads from _settings.system_instruction (priority) and context messages (fallback). The old approach had three bugs: signature mismatch with Anthropic adapter, key name inconsistency, and unnecessary overhead from full message/tools conversion. Also deduplicate the system instruction in spans -- it was appearing as both "system" and "param.system_instruction". --- changelog/3449.fixed.md | 2 +- .../utils/tracing/service_decorators.py | 42 ++++++++++++------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/changelog/3449.fixed.md b/changelog/3449.fixed.md index a8663e004..7cf01c0cb 100644 --- a/changelog/3449.fixed.md +++ b/changelog/3449.fixed.md @@ -1 +1 @@ -- Fixed telemetry record correct system_instruction in span for LLM services +- Fixed stale `system_instruction` in LLM tracing spans by reading from `_settings.system_instruction` instead of the removed `_system_instruction` attribute. diff --git a/src/pipecat/utils/tracing/service_decorators.py b/src/pipecat/utils/tracing/service_decorators.py index 0b63989ea..2b189feb5 100644 --- a/src/pipecat/utils/tracing/service_decorators.py +++ b/src/pipecat/utils/tracing/service_decorators.py @@ -502,35 +502,45 @@ def traced_llm(func: Optional[Callable] = None, *, name: Optional[str] = None) - # Handle system message for different services system_message = None - # Handle system message for different services if isinstance(context, LLMContext): - # Universal LLMContext - use adapter to convert and get system message - if hasattr(self, "get_llm_adapter"): - adapter = self.get_llm_adapter() - try: - # Get LLM invocation params which includes system_instruction - params = adapter.get_llm_invocation_params(context) + # settings.system_instruction takes priority (matches service behavior) + if hasattr(self, "_settings") and getattr( + self._settings, "system_instruction", None + ): + system_message = self._settings.system_instruction + else: + # Fall back to extracting from context messages + ctx_messages = context.get_messages() + if ctx_messages: + first = ctx_messages[0] if ( - isinstance(params, dict) - and "system_instruction" in params + isinstance(first, dict) + and first.get("role") == "system" ): - system_message = params["system_instruction"] - except Exception as e: - logging.debug( - f"Could not extract system instruction from adapter: {e}" - ) + content = first.get("content") + if isinstance(content, str): + system_message = content + elif isinstance(content, list): + system_message = " ".join( + part.get("text", "") + for part in content + if isinstance(part, dict) + and part.get("type") == "text" + ) elif hasattr(context, "system"): system_message = context.system elif hasattr(context, "system_message"): system_message = context.system_message - elif hasattr(self, "_system_instruction"): - system_message = self._system_instruction # Use given_fields() defensively in case a service doesn't # initialize all settings. params = {} if hasattr(self, "_settings"): for key, value in self._settings.given_fields().items(): + # system_instruction is already captured as the + # "system" span attribute above. + if key == "system_instruction": + continue if isinstance(value, (int, float, bool, str)): params[key] = value elif value is None: