From d4dea30407a6f3f1a5e733d51bdb536d38c5423b Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Mar 2026 10:31:25 -0400 Subject: [PATCH 01/23] Centralize system message handling in adapters; add developer message support Two goals: 1. Centralize system_instruction vs context system message resolution into the LLM adapters. This eliminates duplication between in-pipeline and out-of-band (run_inference) code paths across ~16 locations in service llm.py files. 2. Add support for "developer" role messages in conversation context, which is facilitated by the above centralization. Shared helpers on BaseLLMAdapter: - _extract_initial_system_or_developer: extracts/converts messages[0] based on role and whether system_instruction is provided - _resolve_system_instruction: warns on conflicts between system_instruction and context system messages, returns the effective instruction Developer message handling (new): - Non-OpenAI adapters: an initial "developer" message is promoted to the system instruction when no system_instruction is provided; otherwise it is converted to "user". Subsequent "developer" messages are always converted to "user". No conflict warning is emitted for developer messages (unlike "system" messages). - OpenAI adapter: "developer" messages pass through in conversation history without triggering conflict warnings. - OpenAI Responses adapter: "developer" messages are kept as "developer" role (same as "system", which is also converted to "developer" for the Responses API). Other behavior changes: - Gemini: "initial" system message detection now checks messages[0] only (previously searched anywhere in the list) - Bedrock: a lone system message is now converted to "user" instead of being extracted to an empty message list (matches existing Anthropic behavior) --- src/pipecat/adapters/base_llm_adapter.py | 129 +++- .../adapters/services/anthropic_adapter.py | 69 +- .../adapters/services/bedrock_adapter.py | 59 +- .../adapters/services/gemini_adapter.py | 114 ++-- .../adapters/services/open_ai_adapter.py | 24 +- .../services/open_ai_responses_adapter.py | 31 +- .../adapters/services/perplexity_adapter.py | 10 +- src/pipecat/services/anthropic/llm.py | 25 +- src/pipecat/services/aws/llm.py | 27 +- src/pipecat/services/cerebras/llm.py | 11 - src/pipecat/services/fireworks/llm.py | 11 - src/pipecat/services/google/llm.py | 29 +- src/pipecat/services/mistral/llm.py | 11 - src/pipecat/services/openai/base_llm.py | 27 +- src/pipecat/services/perplexity/llm.py | 11 - src/pipecat/services/sambanova/llm.py | 11 - tests/test_adapter_system_instruction.py | 587 ++++++++++++++++++ tests/test_get_llm_invocation_params.py | 24 +- tests/test_openai_responses_adapter.py | 34 +- tests/test_run_inference.py | 50 +- 20 files changed, 995 insertions(+), 299 deletions(-) create mode 100644 tests/test_adapter_system_instruction.py diff --git a/src/pipecat/adapters/base_llm_adapter.py b/src/pipecat/adapters/base_llm_adapter.py index 95082a5d6..672004e7b 100644 --- a/src/pipecat/adapters/base_llm_adapter.py +++ b/src/pipecat/adapters/base_llm_adapter.py @@ -11,7 +11,7 @@ adapters that handle tool format conversion and standardization. """ from abc import ABC, abstractmethod -from typing import Any, Dict, Generic, List, TypeVar +from typing import Any, Dict, Generic, List, Optional, Tuple, TypeVar from loguru import logger @@ -39,10 +39,16 @@ class BaseLLMAdapter(ABC, Generic[TLLMInvocationParams]): - Converting standardized tools schema to provider-specific tool formats. - Extracting messages from the LLM context for the purposes of logging about the specific provider. + - Resolving conflicts between ``system_instruction`` and initial + system/developer messages in the conversation context. Subclasses must implement provider-specific conversion logic. """ + def __init__(self): + """Initialize the adapter.""" + self._warned_system_instruction = False + @property @abstractmethod def id_for_llm_specific_messages(self) -> str: @@ -129,4 +135,123 @@ class BaseLLMAdapter(ABC, Generic[TLLMInvocationParams]): # Fallback to return the same tools in case they are not in a standard format return tools - # TODO: we can move the logic to also handle the Messages here + def _extract_initial_system_or_developer( + self, + messages: list, + *, + system_instruction: Optional[str], + ) -> Tuple[Optional[str], Optional[str]]: + """Extract an initial system/developer message from messages, if appropriate. + + Checks ``messages[0]``. Behavior: + + - ``"system"`` role: always extract (pop from messages). + - ``"developer"`` role **without** ``system_instruction``: extract (pop). + - ``"developer"`` role **with** ``system_instruction``: don't extract; + convert to ``"user"`` in-place. + - Any other role: no-op. + + If extracting would leave the messages list empty (``len(messages) == 1``), + the message is converted to ``"user"`` role instead of being extracted. + This prevents sending an empty conversation history to providers that + require at least one non-system message (e.g. Anthropic, Bedrock). + + Args: + messages: Provider-formatted message list (mutated in-place). + system_instruction: The system instruction from service settings or + ``run_inference``, used to decide whether to extract a + ``"developer"`` message. + + Returns: + ``(extracted_content, original_role)`` where *original_role* is + ``"system"`` or ``"developer"``, or ``(None, None)`` if nothing + was extracted. + """ + if not messages: + return None, None + + role = messages[0].get("role") + if role not in ("system", "developer"): + return None, None + + # "developer" + system_instruction present → keep in messages as "user" + if role == "developer" and system_instruction: + messages[0]["role"] = "user" + return None, None + + # Would extracting empty the list? Convert to "user" instead. + if len(messages) == 1: + messages[0]["role"] = "user" + return None, None + + # Extract + content = messages[0].get("content", "") + if isinstance(content, list): + # Join text parts for providers that expect a string system instruction + content = " ".join( + part.get("text", "") for part in content if part.get("type") == "text" + ) + messages.pop(0) + return content, role + + def _resolve_system_instruction( + self, + initial_context_message: Optional[str], + initial_context_message_role: Optional[str], + system_instruction: Optional[str], + *, + discard_context_system: bool, + ) -> Optional[str]: + """Resolve conflict between ``system_instruction`` and an initial context message. + + Only warns when *initial_context_message_role* is ``"system"`` (not + ``"developer"``), since a developer message coexisting with + ``system_instruction`` is expected and handled elsewhere. + + Args: + initial_context_message: Content extracted from ``messages[0]`` + by :meth:`_extract_initial_system_or_developer`, or detected + inline (OpenAI adapters). + initial_context_message_role: ``"system"`` or ``"developer"`` — + the original role before extraction/detection. + system_instruction: From service settings or ``run_inference`` param. + discard_context_system: If ``True`` (non-OpenAI adapters), the + context system message is discarded when ``system_instruction`` + is also present. If ``False`` (OpenAI adapters), both are kept. + + Returns: + The effective system instruction to use, or ``None`` if the system + instruction is already represented in the messages (OpenAI path). + """ + both_present = initial_context_message and system_instruction + from_system_role = initial_context_message_role == "system" + + if both_present and from_system_role: + if not self._warned_system_instruction: + self._warned_system_instruction = True + if discard_context_system: + logger.warning( + "Both system_instruction and a system message in context are set." + " Using system_instruction." + ) + else: + logger.warning( + "Both system_instruction and an initial system message in context" + " are set, which may be unintended. Prefer system_instruction." + ) + + if system_instruction: + if discard_context_system: + return system_instruction + else: + # OpenAI path: caller prepends; return the instruction for prepending + return system_instruction + + if initial_context_message: + if discard_context_system: + return initial_context_message + else: + # Content is already in messages; nothing to prepend + return None + + return None diff --git a/src/pipecat/adapters/services/anthropic_adapter.py b/src/pipecat/adapters/services/anthropic_adapter.py index ecc87154c..dfd68375c 100644 --- a/src/pipecat/adapters/services/anthropic_adapter.py +++ b/src/pipecat/adapters/services/anthropic_adapter.py @@ -9,7 +9,7 @@ import copy import json from dataclasses import dataclass -from typing import Any, Dict, List, TypedDict +from typing import Any, Dict, List, Optional, TypedDict from anthropic import NOT_GIVEN, NotGiven from anthropic.types.message_param import MessageParam @@ -48,24 +48,37 @@ class AnthropicLLMAdapter(BaseLLMAdapter[AnthropicLLMInvocationParams]): return "anthropic" def get_llm_invocation_params( - self, context: LLMContext, enable_prompt_caching: bool + self, + context: LLMContext, + enable_prompt_caching: bool, + system_instruction: Optional[str] = None, ) -> AnthropicLLMInvocationParams: """Get Anthropic-specific LLM invocation parameters from a universal LLM context. Args: context: The LLM context containing messages, tools, etc. enable_prompt_caching: Whether prompt caching should be enabled. + system_instruction: Optional system instruction from service settings + or ``run_inference``. Returns: Dictionary of parameters for invoking Anthropic's LLM API. """ - messages = self._from_universal_context_messages(self.get_messages(context)) + converted = self._from_universal_context_messages( + self.get_messages(context), system_instruction=system_instruction + ) + system = self._resolve_system_instruction( + converted.system if converted.system is not NOT_GIVEN else None, + converted.system_role, + system_instruction, + discard_context_system=True, + ) return { - "system": messages.system, + "system": system if system is not None else NOT_GIVEN, "messages": ( - self._with_cache_control_markers(messages.messages) + self._with_cache_control_markers(converted.messages) if enable_prompt_caching - else messages.messages + else converted.messages ), # NOTE: LLMContext's tools are guaranteed to be a ToolsSchema (or NOT_GIVEN) "tools": self.from_standard_tools(context.tools) or [], @@ -105,35 +118,39 @@ class AnthropicLLMAdapter(BaseLLMAdapter[AnthropicLLMInvocationParams]): messages: List[MessageParam] system: str | NotGiven + system_role: Optional[str] = None # "system" or "developer" — origin of extracted system def _from_universal_context_messages( - self, universal_context_messages: List[LLMContextMessage] + self, + universal_context_messages: List[LLMContextMessage], + *, + system_instruction: Optional[str] = None, ) -> ConvertedMessages: system = NOT_GIVEN - messages = [] + system_role = None - # First, map messages using self._from_universal_context_message(m) + # Extract initial system/developer from universal messages BEFORE conversion, + # so the helper works with standard message format (not provider-specific). + remaining = list(universal_context_messages) + if remaining and not isinstance(remaining[0], LLMSpecificMessage): + extracted_content, extracted_role = self._extract_initial_system_or_developer( + remaining, system_instruction=system_instruction + ) + if extracted_content is not None: + system = extracted_content + system_role = extracted_role + + # Convert remaining messages to Anthropic format + messages = [] try: - messages = [self._from_universal_context_message(m) for m in universal_context_messages] + messages = [self._from_universal_context_message(m) for m in remaining] except Exception as e: logger.error(f"Error mapping messages: {e}") - # See if we should pull the system message out of our messages list. - if messages and messages[0]["role"] == "system": - if len(messages) == 1: - # If we have only have a system message in the list, all we can really do - # without introducing too much magic is change the role to "user". - messages[0]["role"] = "user" - else: - # If we have more than one message, we'll pull the system message out of the - # list. - system = messages[0]["content"] - messages.pop(0) - - # Convert any subsequent "system"-role messages to "user"-role - # messages, as Anthropic doesn't support system input messages. + # Convert any subsequent "system"/"developer"-role messages to "user"-role + # messages, as Anthropic doesn't support system or developer input messages. for message in messages: - if message["role"] == "system": + if message["role"] in ("system", "developer"): message["role"] = "user" # Merge consecutive messages with the same role. @@ -163,7 +180,7 @@ class AnthropicLLMAdapter(BaseLLMAdapter[AnthropicLLMInvocationParams]): elif isinstance(message["content"], list) and len(message["content"]) == 0: message["content"] = [{"type": "text", "text": "(empty)"}] - return self.ConvertedMessages(messages=messages, system=system) + return self.ConvertedMessages(messages=messages, system=system, system_role=system_role) def _from_universal_context_message(self, message: LLMContextMessage) -> MessageParam: if isinstance(message, LLMSpecificMessage): diff --git a/src/pipecat/adapters/services/bedrock_adapter.py b/src/pipecat/adapters/services/bedrock_adapter.py index d63c5cf0f..7df924880 100644 --- a/src/pipecat/adapters/services/bedrock_adapter.py +++ b/src/pipecat/adapters/services/bedrock_adapter.py @@ -47,19 +47,31 @@ class AWSBedrockLLMAdapter(BaseLLMAdapter[AWSBedrockLLMInvocationParams]): """Get the identifier used in LLMSpecificMessage instances for AWS Bedrock.""" return "aws" - def get_llm_invocation_params(self, context: LLMContext) -> AWSBedrockLLMInvocationParams: + def get_llm_invocation_params( + self, context: LLMContext, *, system_instruction: Optional[str] = None + ) -> AWSBedrockLLMInvocationParams: """Get AWS Bedrock-specific LLM invocation parameters from a universal LLM context. Args: context: The LLM context containing messages, tools, etc. + system_instruction: Optional system instruction from service settings + or ``run_inference``. Returns: Dictionary of parameters for invoking AWS Bedrock's LLM API. """ - messages = self._from_universal_context_messages(self.get_messages(context)) + converted = self._from_universal_context_messages( + self.get_messages(context), system_instruction=system_instruction + ) + effective_system = self._resolve_system_instruction( + converted.system, + converted.system_role, + system_instruction, + discard_context_system=True, + ) return { - "system": messages.system, - "messages": messages.messages, + "system": [{"text": effective_system}] if effective_system else None, + "messages": converted.messages, # NOTE: LLMContext's tools are guaranteed to be a ToolsSchema (or NOT_GIVEN) "tools": self.from_standard_tools(context.tools) or [], # To avoid refactoring in AWSBedrockLLMService, we just pass through tool_choice. @@ -96,32 +108,43 @@ class AWSBedrockLLMAdapter(BaseLLMAdapter[AWSBedrockLLMInvocationParams]): @dataclass class ConvertedMessages: - """Container for Anthropic-formatted messages converted from universal context.""" + """Container for Bedrock-formatted messages converted from universal context.""" messages: List[dict[str, Any]] system: Optional[str] + system_role: Optional[str] = None # "system" or "developer" — origin of extracted system def _from_universal_context_messages( - self, universal_context_messages: List[LLMContextMessage] + self, + universal_context_messages: List[LLMContextMessage], + *, + system_instruction: Optional[str] = None, ) -> ConvertedMessages: system = None - messages = [] + system_role = None - # First, map messages using self._from_universal_context_message(m) + # Extract initial system/developer from universal messages BEFORE conversion, + # so the helper works with standard message format (not provider-specific). + remaining = list(universal_context_messages) + if remaining and not isinstance(remaining[0], LLMSpecificMessage): + extracted_content, extracted_role = self._extract_initial_system_or_developer( + remaining, system_instruction=system_instruction + ) + if extracted_content is not None: + system = extracted_content + system_role = extracted_role + + # Convert remaining messages to Bedrock format + messages = [] try: - messages = [self._from_universal_context_message(m) for m in universal_context_messages] + messages = [self._from_universal_context_message(m) for m in remaining] except Exception as e: logger.error(f"Error mapping messages: {e}") - # See if we should pull the system message out of our messages list - if messages and messages[0]["role"] == "system": - system = messages[0]["content"] - messages.pop(0) - - # Convert any subsequent "system"-role messages to "user"-role - # messages, as AWS Bedrock doesn't support system input messages. + # Convert any subsequent "system"/"developer"-role messages to "user"-role + # messages, as AWS Bedrock doesn't support system or developer input messages. for message in messages: - if message["role"] == "system": + if message["role"] in ("system", "developer"): message["role"] = "user" # Merge consecutive messages with the same role. @@ -151,7 +174,7 @@ class AWSBedrockLLMAdapter(BaseLLMAdapter[AWSBedrockLLMInvocationParams]): elif isinstance(message["content"], list) and len(message["content"]) == 0: message["content"] = [{"type": "text", "text": "(empty)"}] - return self.ConvertedMessages(messages=messages, system=system) + return self.ConvertedMessages(messages=messages, system=system, system_role=system_role) def _from_universal_context_message(self, message: LLMContextMessage) -> dict[str, Any]: if isinstance(message, LLMSpecificMessage): diff --git a/src/pipecat/adapters/services/gemini_adapter.py b/src/pipecat/adapters/services/gemini_adapter.py index 4968c2719..d73ac388d 100644 --- a/src/pipecat/adapters/services/gemini_adapter.py +++ b/src/pipecat/adapters/services/gemini_adapter.py @@ -53,19 +53,31 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]): """Get the identifier used in LLMSpecificMessage instances for Google.""" return "google" - def get_llm_invocation_params(self, context: LLMContext) -> GeminiLLMInvocationParams: + def get_llm_invocation_params( + self, context: LLMContext, *, system_instruction: Optional[str] = None + ) -> GeminiLLMInvocationParams: """Get Gemini-specific LLM invocation parameters from a universal LLM context. Args: context: The LLM context containing messages, tools, etc. + system_instruction: Optional system instruction from service settings + or ``run_inference``. Returns: Dictionary of parameters for Gemini's API. """ - messages = self._from_universal_context_messages(self.get_messages(context)) + converted = self._from_universal_context_messages( + self.get_messages(context), system_instruction=system_instruction + ) + effective_system = self._resolve_system_instruction( + converted.system_instruction, + converted.system_instruction_role, + system_instruction, + discard_context_system=True, + ) return { - "system_instruction": messages.system_instruction, - "messages": messages.messages, + "system_instruction": effective_system, + "messages": converted.messages, # NOTE: LLMContext's tools are guaranteed to be a ToolsSchema (or NOT_GIVEN) "tools": self.from_standard_tools(context.tools), } @@ -164,57 +176,65 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]): messages: List[Content] system_instruction: Optional[str] = None + system_instruction_role: Optional[str] = None # "system" or "developer" @dataclass class MessageConversionResult: """Result of converting a single universal context message to Google format. - Either content (a Google Content object) or a system instruction string - is guaranteed to be set. - - Also returns a tool call ID to name mapping for any tool calls - discovered in the message. + Contains a Google Content object and a tool call ID to name mapping + for any tool calls discovered in the message. """ content: Optional[Content] = None - system_instruction: Optional[str] = None tool_call_id_to_name_mapping: Dict[str, str] = field(default_factory=dict) @dataclass class MessageConversionParams: """Parameters for converting a single universal context message to Google format.""" - already_have_system_instruction: bool tool_call_id_to_name_mapping: Dict[str, str] def _from_universal_context_messages( - self, universal_context_messages: List[LLMContextMessage] + self, + universal_context_messages: List[LLMContextMessage], + *, + system_instruction: Optional[str] = None, ) -> ConvertedMessages: """Restructures messages to ensure proper Google format and message ordering. This method handles conversion of OpenAI-formatted messages to Google format, - with special handling for function calls, function responses, and system messages. - System messages are added back to the context as user messages when needed. + with special handling for function calls, function responses, and system/developer + messages. - The final message order is preserved as: + Initial system/developer messages are extracted as the system instruction + (only from ``messages[0]``). Subsequent system/developer messages are + converted to user role. - 1. Function calls (from model) - 2. Function responses (from user) - 3. Text messages (converted from system messages) - - Note:: - - System messages are only added back when there are no regular text - messages in the context, ensuring proper conversation continuity - after function calls. + Args: + universal_context_messages: Messages from the LLM context. + system_instruction: Optional system instruction from service settings, + used to decide whether to extract an initial "developer" message. """ - system_instruction = None + # Extract initial system/developer message from universal messages before conversion. + # We work on a mutable copy so we can pop messages[0] if needed. + remaining_messages = list(universal_context_messages) + extracted_system = None + extracted_role = None + + # Extract initial system/developer from universal messages BEFORE conversion, + # so the helper works with standard message format. + if remaining_messages and not isinstance(remaining_messages[0], LLMSpecificMessage): + extracted_system, extracted_role = self._extract_initial_system_or_developer( + remaining_messages, system_instruction=system_instruction + ) + messages = [] tool_call_id_to_name_mapping = {} thought_signature_dicts = [] # Process each message, converting to Google format as needed - for message in universal_context_messages: + for message in remaining_messages: # We have a Google-specific message; this may either be a # thought-signature-containing message that we need to handle in a # special way, or a message already in Google format that we can @@ -237,16 +257,12 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]): result = self._from_standard_message( message, params=self.MessageConversionParams( - already_have_system_instruction=bool(system_instruction), tool_call_id_to_name_mapping=tool_call_id_to_name_mapping, ), ) - # Each result is either a Content or a system instruction if result.content: messages.append(result.content) - elif result.system_instruction: - system_instruction = result.system_instruction # Merge tool call ID to name mapping if result.tool_call_id_to_name_mapping: @@ -259,6 +275,7 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]): messages = self._merge_parallel_tool_calls_for_thinking(thought_signature_dicts, messages) # Check if we only have function-related messages (no regular text) + effective_system = extracted_system or system_instruction has_regular_messages = any( len(msg.parts) == 1 and getattr(msg.parts[0], "text", None) @@ -268,13 +285,17 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]): ) # Add system instruction back as a user message if we only have function messages - if system_instruction and not has_regular_messages: - messages.append(Content(role="user", parts=[Part(text=system_instruction)])) + if effective_system and not has_regular_messages: + messages.append(Content(role="user", parts=[Part(text=effective_system)])) # Remove any empty messages messages = [m for m in messages if m.parts] - return self.ConvertedMessages(messages=messages, system_instruction=system_instruction) + return self.ConvertedMessages( + messages=messages, + system_instruction=extracted_system, + system_instruction_role=extracted_role, + ) def _from_standard_message( self, message: LLMStandardMessage, *, params: MessageConversionParams @@ -282,17 +303,16 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]): """Convert standard universal context message to Google Content object. Handles conversion of text, images, and function calls to Google's - format. - System instructions are returned as a plain string. + format. System and developer messages at this stage (i.e. non-initial + ones, since the initial one is already extracted) are converted to + user role. Args: message: Message in standard universal context format. - already_have_system_instruction: Whether we already have a system instruction params: Parameters for conversion. Returns: - MessageConversionResult containing either a Content object or a - system instruction string. + MessageConversionResult containing a Content object. Examples: Standard text message:: @@ -333,20 +353,10 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]): role = message["role"] content = message.get("content", []) - if role == "system": - if params.already_have_system_instruction: - role = "user" # Convert system message to user role if we already have a system instruction - else: - system_instruction: str = None - if isinstance(content, str): - system_instruction = content - elif isinstance(content, list): - # If content is a list, we assume it's a list of text parts, per the standard - system_instruction = " ".join( - part["text"] for part in content if part.get("type") == "text" - ) - if system_instruction: - return self.MessageConversionResult(system_instruction=system_instruction) + # Convert non-initial system/developer messages to user role, + # as Gemini doesn't support these as input messages. + if role in ("system", "developer"): + role = "user" elif role == "assistant": role = "model" diff --git a/src/pipecat/adapters/services/open_ai_adapter.py b/src/pipecat/adapters/services/open_ai_adapter.py index f4b534f2c..9e8b449d0 100644 --- a/src/pipecat/adapters/services/open_ai_adapter.py +++ b/src/pipecat/adapters/services/open_ai_adapter.py @@ -7,7 +7,7 @@ """OpenAI LLM adapter for Pipecat.""" import copy -from typing import Any, Dict, List, TypedDict +from typing import Any, Dict, List, Optional, TypedDict from openai._types import NotGiven as OpenAINotGiven from openai.types.chat import ( @@ -51,17 +51,35 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]): """Get the identifier used in LLMSpecificMessage instances for OpenAI.""" return "openai" - def get_llm_invocation_params(self, context: LLMContext) -> OpenAILLMInvocationParams: + def get_llm_invocation_params( + self, context: LLMContext, *, system_instruction: Optional[str] = None + ) -> OpenAILLMInvocationParams: """Get OpenAI-specific LLM invocation parameters from a universal LLM context. Args: context: The LLM context containing messages, tools, etc. + system_instruction: Optional system instruction from service settings + or ``run_inference``. If provided, prepended as a system message. Returns: Dictionary of parameters for OpenAI's ChatCompletion API. """ + messages = self._from_universal_context_messages(self.get_messages(context)) + + if system_instruction: + # Detect initial system message for warning purposes (don't extract) + initial_role = messages[0].get("role") if messages else None + initial_content = messages[0].get("content", "") if initial_role == "system" else None + self._resolve_system_instruction( + initial_content, + initial_role if initial_role == "system" else None, + system_instruction, + discard_context_system=False, + ) + messages = [{"role": "system", "content": system_instruction}] + messages + return { - "messages": self._from_universal_context_messages(self.get_messages(context)), + "messages": messages, # NOTE; LLMContext's tools are guaranteed to be a ToolsSchema (or NOT_GIVEN) "tools": self.from_standard_tools(context.tools), "tool_choice": context.tool_choice, diff --git a/src/pipecat/adapters/services/open_ai_responses_adapter.py b/src/pipecat/adapters/services/open_ai_responses_adapter.py index 70627fe5d..89968a721 100644 --- a/src/pipecat/adapters/services/open_ai_responses_adapter.py +++ b/src/pipecat/adapters/services/open_ai_responses_adapter.py @@ -9,7 +9,6 @@ import copy from typing import Any, Dict, List, Optional, TypedDict -from loguru import logger from openai._types import NotGiven as OpenAINotGiven from openai.types.responses import FunctionToolParam, ResponseInputItemParam @@ -41,11 +40,6 @@ class OpenAIResponsesLLMAdapter(BaseLLMAdapter[OpenAIResponsesLLMInvocationParam - Extracting and sanitizing messages from the LLM context for logging """ - def __init__(self): - """Initialize the adapter.""" - super().__init__() - self._warned_system_instruction = False - @property def id_for_llm_specific_messages(self) -> str: """Get the identifier used in LLMSpecificMessage instances.""" @@ -67,6 +61,18 @@ class OpenAIResponsesLLMAdapter(BaseLLMAdapter[OpenAIResponsesLLMInvocationParam Dictionary of parameters for the Responses API. """ messages = self.get_messages(context) + + # Check for conflict: system_instruction + initial system message + if system_instruction and messages: + first_msg = messages[0] if not isinstance(messages[0], LLMSpecificMessage) else None + if first_msg and first_msg.get("role") == "system": + self._resolve_system_instruction( + first_msg.get("content", ""), + "system", + system_instruction, + discard_context_system=False, + ) + input_items = self._convert_messages_to_input(messages) params: OpenAIResponsesLLMInvocationParams = { @@ -158,24 +164,15 @@ class OpenAIResponsesLLMAdapter(BaseLLMAdapter[OpenAIResponsesLLMInvocationParam List of Responses API input items. """ result: List[ResponseInputItemParam] = [] - is_first = True for message in messages: if isinstance(message, LLMSpecificMessage): result.append(message.message) - is_first = False continue role = message.get("role") - if role == "system": - if is_first and not self._warned_system_instruction: - logger.warning( - "System messages in LLMContext are converted to 'developer' role for the " - "Responses API. Consider using settings.system_instruction instead, which " - "maps to the 'instructions' parameter." - ) - self._warned_system_instruction = True + if role in ("system", "developer"): content = message.get("content", "") if isinstance(content, list): content = self._convert_multimodal_content(content) @@ -218,8 +215,6 @@ class OpenAIResponsesLLMAdapter(BaseLLMAdapter[OpenAIResponsesLLMInvocationParam } ) - is_first = False - return result def _convert_multimodal_content(self, content: list) -> list: diff --git a/src/pipecat/adapters/services/perplexity_adapter.py b/src/pipecat/adapters/services/perplexity_adapter.py index a8fbe3c18..5359baddc 100644 --- a/src/pipecat/adapters/services/perplexity_adapter.py +++ b/src/pipecat/adapters/services/perplexity_adapter.py @@ -28,7 +28,7 @@ the messages are sent to Perplexity's API. """ import copy -from typing import List +from typing import List, Optional from openai.types.chat import ChatCompletionMessageParam @@ -49,17 +49,21 @@ class PerplexityLLMAdapter(OpenAILLMAdapter): ``build_chat_completion_params`` prepends ``system_instruction``. """ - def get_llm_invocation_params(self, context: LLMContext) -> OpenAILLMInvocationParams: + def get_llm_invocation_params( + self, context: LLMContext, *, system_instruction: Optional[str] = None + ) -> OpenAILLMInvocationParams: """Get OpenAI-compatible invocation parameters with Perplexity message fixes applied. Args: context: The LLM context containing messages, tools, etc. + system_instruction: Optional system instruction from service settings + or ``run_inference``. Forwarded to the parent adapter. Returns: Dictionary of parameters for Perplexity's ChatCompletion API, with messages transformed to satisfy Perplexity's constraints. """ - params = super().get_llm_invocation_params(context) + params = super().get_llm_invocation_params(context, system_instruction=system_instruction) params["messages"] = self._transform_messages(list(params["messages"])) return params diff --git a/src/pipecat/services/anthropic/llm.py b/src/pipecat/services/anthropic/llm.py index 2f375df82..f35d6226c 100644 --- a/src/pipecat/services/anthropic/llm.py +++ b/src/pipecat/services/anthropic/llm.py @@ -370,10 +370,13 @@ class AnthropicLLMService(LLMService): messages = [] system = NOT_GIVEN tools = [] + effective_instruction = system_instruction or self._settings.system_instruction if isinstance(context, LLMContext): adapter: AnthropicLLMAdapter = self.get_llm_adapter() invocation_params = adapter.get_llm_invocation_params( - context, enable_prompt_caching=self._settings.enable_prompt_caching + context, + enable_prompt_caching=self._settings.enable_prompt_caching, + system_instruction=effective_instruction, ) messages = invocation_params["messages"] system = invocation_params["system"] @@ -384,15 +387,6 @@ class AnthropicLLMService(LLMService): system = getattr(context, "system", NOT_GIVEN) tools = context.tools or [] - # Override system instruction if provided - if system_instruction is not None: - if system and system is not NOT_GIVEN: - logger.warning( - f"{self}: Both system_instruction and a system message in context are set." - " Using system_instruction." - ) - system = system_instruction - # Build params using the same method as streaming completions params = { "model": self._settings.model, @@ -460,15 +454,10 @@ class AnthropicLLMService(LLMService): if isinstance(context, LLMContext): adapter: AnthropicLLMAdapter = self.get_llm_adapter() params: AnthropicLLMInvocationParams = adapter.get_llm_invocation_params( - context, enable_prompt_caching=self._settings.enable_prompt_caching + context, + enable_prompt_caching=self._settings.enable_prompt_caching, + system_instruction=self._settings.system_instruction, ) - if self._settings.system_instruction: - if params["system"] is not NOT_GIVEN: - logger.warning( - f"{self}: Both system_instruction and a system message in context are" - " set. Using system_instruction." - ) - params["system"] = self._settings.system_instruction return params # Anthropic-specific context diff --git a/src/pipecat/services/aws/llm.py b/src/pipecat/services/aws/llm.py index 92049dffb..cda186167 100644 --- a/src/pipecat/services/aws/llm.py +++ b/src/pipecat/services/aws/llm.py @@ -942,25 +942,19 @@ class AWSBedrockLLMService(LLMService): """ messages = [] system = [] + effective_instruction = system_instruction or self._settings.system_instruction if isinstance(context, LLMContext): adapter: AWSBedrockLLMAdapter = self.get_llm_adapter() - params: AWSBedrockLLMInvocationParams = adapter.get_llm_invocation_params(context) + params: AWSBedrockLLMInvocationParams = adapter.get_llm_invocation_params( + context, system_instruction=effective_instruction + ) messages = params["messages"] - system = params["system"] # [{"text": "system message"}] + system = params["system"] # [{"text": "system message"}] or None else: context = AWSBedrockLLMContext.upgrade_to_bedrock(context) messages = context.messages system = getattr(context, "system", None) # [{"text": "system message"}] - # Override system instruction if provided - if system_instruction is not None: - if system: - logger.warning( - f"{self}: Both system_instruction and a system message in context are set." - " Using system_instruction." - ) - system = [{"text": system_instruction}] - # Prepare request parameters using the same method as streaming inference_config = self._build_inference_config() @@ -1086,14 +1080,9 @@ class AWSBedrockLLMService(LLMService): # Universal LLMContext if isinstance(context, LLMContext): adapter: AWSBedrockLLMAdapter = self.get_llm_adapter() - params: AWSBedrockLLMInvocationParams = adapter.get_llm_invocation_params(context) - if self._settings.system_instruction: - if params["system"]: - logger.warning( - f"{self}: Both system_instruction and a system message in context are" - " set. Using system_instruction." - ) - params["system"] = [{"text": self._settings.system_instruction}] + params: AWSBedrockLLMInvocationParams = adapter.get_llm_invocation_params( + context, system_instruction=self._settings.system_instruction + ) return params # AWS Bedrock-specific context diff --git a/src/pipecat/services/cerebras/llm.py b/src/pipecat/services/cerebras/llm.py index dfb62baf8..5be270ae1 100644 --- a/src/pipecat/services/cerebras/llm.py +++ b/src/pipecat/services/cerebras/llm.py @@ -114,15 +114,4 @@ class CerebrasLLMService(OpenAILLMService): params.update(self._settings.extra) - # Prepend system instruction if set - if self._settings.system_instruction: - messages = params.get("messages", []) - if messages and messages[0].get("role") == "system": - logger.warning( - f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended." - ) - params["messages"] = [ - {"role": "system", "content": self._settings.system_instruction} - ] + messages - return params diff --git a/src/pipecat/services/fireworks/llm.py b/src/pipecat/services/fireworks/llm.py index 5efa60793..7d2997987 100644 --- a/src/pipecat/services/fireworks/llm.py +++ b/src/pipecat/services/fireworks/llm.py @@ -115,15 +115,4 @@ class FireworksLLMService(OpenAILLMService): params.update(self._settings.extra) - # Prepend system instruction if set - if self._settings.system_instruction: - messages = params.get("messages", []) - if messages and messages[0].get("role") == "system": - logger.warning( - f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended." - ) - params["messages"] = [ - {"role": "system", "content": self._settings.system_instruction} - ] + messages - return params diff --git a/src/pipecat/services/google/llm.py b/src/pipecat/services/google/llm.py index 26ad46311..c8d54830e 100644 --- a/src/pipecat/services/google/llm.py +++ b/src/pipecat/services/google/llm.py @@ -904,9 +904,12 @@ class GoogleLLMService(LLMService): messages = [] system = [] tools = [] + effective_instruction = system_instruction or self._settings.system_instruction if isinstance(context, LLMContext): adapter = self.get_llm_adapter() - params: GeminiLLMInvocationParams = adapter.get_llm_invocation_params(context) + params: GeminiLLMInvocationParams = adapter.get_llm_invocation_params( + context, system_instruction=effective_instruction + ) messages = params["messages"] system = params["system_instruction"] tools = params["tools"] @@ -916,15 +919,6 @@ class GoogleLLMService(LLMService): system = getattr(context, "system_message", None) tools = context.tools or [] - # Override system instruction if provided - if system_instruction is not None: - if system: - logger.warning( - f"{self}: Both system_instruction and a system message in context are set." - " Using system_instruction." - ) - system = system_instruction - # Build generation config using the same method as streaming generation_params = self._build_generation_params( system_instruction=system, tools=tools if tools else None @@ -1015,15 +1009,8 @@ class GoogleLLMService(LLMService): ) -> AsyncIterator[GenerateContentResponse]: messages = params_from_context["messages"] - # Constructor/settings system instruction takes priority over context. - if self._settings.system_instruction and params_from_context["system_instruction"]: - logger.warning( - f"{self}: Both system_instruction and a system message in context are" - " set. Using system_instruction." - ) - system_instruction = ( - self._settings.system_instruction or params_from_context["system_instruction"] - ) + # The adapter already resolved system_instruction vs context system message. + system_instruction = params_from_context["system_instruction"] tools = [] if params_from_context["tools"]: @@ -1072,7 +1059,9 @@ class GoogleLLMService(LLMService): self, context: LLMContext ) -> AsyncIterator[GenerateContentResponse]: adapter = self.get_llm_adapter() - params: GeminiLLMInvocationParams = adapter.get_llm_invocation_params(context) + params: GeminiLLMInvocationParams = adapter.get_llm_invocation_params( + context, system_instruction=self._settings.system_instruction + ) logger.debug( f"{self}: Generating chat from universal context [{params['system_instruction']}] | {adapter.get_messages_for_logging(context)}" diff --git a/src/pipecat/services/mistral/llm.py b/src/pipecat/services/mistral/llm.py index 063dac3aa..a2edb4cb6 100644 --- a/src/pipecat/services/mistral/llm.py +++ b/src/pipecat/services/mistral/llm.py @@ -233,15 +233,4 @@ class MistralLLMService(OpenAILLMService): # Add any extra parameters params.update(self._settings.extra) - # Prepend system instruction if set - if self._settings.system_instruction: - messages = params.get("messages", []) - if messages and messages[0].get("role") == "system": - logger.warning( - f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended." - ) - params["messages"] = [ - {"role": "system", "content": self._settings.system_instruction} - ] + messages - return params diff --git a/src/pipecat/services/openai/base_llm.py b/src/pipecat/services/openai/base_llm.py index eb8ce3cc6..d2b19e2df 100644 --- a/src/pipecat/services/openai/base_llm.py +++ b/src/pipecat/services/openai/base_llm.py @@ -327,17 +327,6 @@ class BaseOpenAILLMService(LLMService): params.update(self._settings.extra) - # Prepend system instruction from constructor - if self._settings.system_instruction: - messages = params.get("messages", []) - if messages and messages[0].get("role") == "system": - logger.warning( - f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended." - ) - params["messages"] = [ - {"role": "system", "content": self._settings.system_instruction} - ] + messages - return params async def run_inference( @@ -358,10 +347,11 @@ class BaseOpenAILLMService(LLMService): Returns: The LLM's response as a string, or None if no response is generated. """ + effective_instruction = system_instruction or self._settings.system_instruction if isinstance(context, LLMContext): adapter = self.get_llm_adapter() invocation_params: OpenAILLMInvocationParams = adapter.get_llm_invocation_params( - context + context, system_instruction=effective_instruction ) else: invocation_params = OpenAILLMInvocationParams( @@ -375,15 +365,6 @@ class BaseOpenAILLMService(LLMService): params["stream"] = False params.pop("stream_options", None) - # Prepend system instruction if provided - if system_instruction is not None: - messages = params.get("messages", []) - if messages and messages[0].get("role") == "system": - logger.warning( - f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended." - ) - params["messages"] = [{"role": "system", "content": system_instruction}] + messages - # Override max_tokens if provided if max_tokens is not None: # Use max_completion_tokens for newer models, fallback to max_tokens @@ -439,7 +420,9 @@ class BaseOpenAILLMService(LLMService): f"{self}: Generating chat from universal context {adapter.get_messages_for_logging(context)}" ) - params: OpenAILLMInvocationParams = adapter.get_llm_invocation_params(context) + params: OpenAILLMInvocationParams = adapter.get_llm_invocation_params( + context, system_instruction=self._settings.system_instruction + ) chunks = await self.get_chat_completions(params) return chunks diff --git a/src/pipecat/services/perplexity/llm.py b/src/pipecat/services/perplexity/llm.py index 9ea323c5d..b4aedc3f7 100644 --- a/src/pipecat/services/perplexity/llm.py +++ b/src/pipecat/services/perplexity/llm.py @@ -121,17 +121,6 @@ class PerplexityLLMService(OpenAILLMService): if self._settings.max_tokens is not None: params["max_tokens"] = self._settings.max_tokens - # Prepend system instruction if set - if self._settings.system_instruction: - messages = params.get("messages", []) - if messages and messages[0].get("role") == "system": - logger.warning( - f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended." - ) - params["messages"] = [ - {"role": "system", "content": self._settings.system_instruction} - ] + messages - return params async def _process_context(self, context: OpenAILLMContext | LLMContext): diff --git a/src/pipecat/services/sambanova/llm.py b/src/pipecat/services/sambanova/llm.py index 710a22db2..35cf60886 100644 --- a/src/pipecat/services/sambanova/llm.py +++ b/src/pipecat/services/sambanova/llm.py @@ -131,17 +131,6 @@ class SambaNovaLLMService(OpenAILLMService): # type: ignore params.update(self._settings.extra) - # Prepend system instruction if set - if self._settings.system_instruction: - messages = params.get("messages", []) - if messages and messages[0].get("role") == "system": - logger.warning( - f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended." - ) - params["messages"] = [ - {"role": "system", "content": self._settings.system_instruction} - ] + messages - return params @traced_llm # type: ignore diff --git a/tests/test_adapter_system_instruction.py b/tests/test_adapter_system_instruction.py new file mode 100644 index 000000000..125702dd6 --- /dev/null +++ b/tests/test_adapter_system_instruction.py @@ -0,0 +1,587 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""Unit tests for system_instruction and developer message handling in LLM adapters. + +Tests cover: + +1. system_instruction only (no system/developer in context) +2. Initial "system" message only (no system_instruction) +3. Initial "developer" message only (no system_instruction) -> promoted to system instruction +4. Both system_instruction and initial "system" message -> warns +5. Both system_instruction and initial "developer" message -> does NOT warn; developer becomes "user" +6. Non-OpenAI adapters: subsequent "developer" messages converted to "user" +7. Non-OpenAI adapters: initial "system" discarded when system_instruction provided +8. Gemini: non-initial "system" message is converted to "user" (not extracted) +9. Single system-only message: converted to "user" instead of extracting (empty list prevention) +""" + +import unittest +from unittest.mock import patch + +from pipecat.adapters.services.open_ai_adapter import OpenAILLMAdapter +from pipecat.processors.aggregators.llm_context import LLMContext, LLMStandardMessage + + +class TestOpenAIAdapterSystemInstruction(unittest.TestCase): + """Tests for the OpenAI ChatCompletion adapter.""" + + def setUp(self): + self.adapter = OpenAILLMAdapter() + + def test_system_instruction_only(self): + """system_instruction alone is prepended as a system message.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context, system_instruction="Be helpful.") + + self.assertEqual(params["messages"][0]["role"], "system") + self.assertEqual(params["messages"][0]["content"], "Be helpful.") + self.assertEqual(params["messages"][1]["role"], "user") + + def test_initial_system_message_only(self): + """Initial system message without system_instruction passes through.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["messages"]), 2) + self.assertEqual(params["messages"][0]["role"], "system") + self.assertEqual(params["messages"][0]["content"], "You are helpful.") + + def test_both_system_instruction_and_system_message_warns(self): + """system_instruction + initial system message warns but allows both.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + params = self.adapter.get_llm_invocation_params( + context, system_instruction="Be concise." + ) + mock_logger.warning.assert_called_once() + warning_msg = mock_logger.warning.call_args[0][0] + self.assertIn("may be unintended", warning_msg) + + # Both are present: prepended system_instruction + original system message + self.assertEqual(params["messages"][0]["content"], "Be concise.") + self.assertEqual(params["messages"][1]["content"], "You are helpful.") + + def test_both_system_instruction_and_developer_message_no_warning(self): + """system_instruction + initial developer message does NOT warn.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + params = self.adapter.get_llm_invocation_params( + context, system_instruction="Be concise." + ) + mock_logger.warning.assert_not_called() + + # system_instruction prepended, developer message stays in messages + self.assertEqual(params["messages"][0]["content"], "Be concise.") + self.assertEqual(params["messages"][1]["role"], "developer") + + def test_warning_fires_only_once(self): + """Conflict warning fires only once per adapter instance.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + self.adapter.get_llm_invocation_params(context, system_instruction="Be concise.") + self.adapter.get_llm_invocation_params(context, system_instruction="Be concise.") + mock_logger.warning.assert_called_once() + + +class TestAnthropicAdapterSystemInstruction(unittest.TestCase): + """Tests for the Anthropic adapter.""" + + def setUp(self): + from pipecat.adapters.services.anthropic_adapter import AnthropicLLMAdapter + + self.adapter = AnthropicLLMAdapter() + + def test_system_instruction_only(self): + """system_instruction alone becomes the system parameter.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params( + context, enable_prompt_caching=False, system_instruction="Be helpful." + ) + + self.assertEqual(params["system"], "Be helpful.") + self.assertEqual(len(params["messages"]), 1) + self.assertEqual(params["messages"][0]["role"], "user") + + def test_initial_system_message_only(self): + """Initial system message is extracted as the system parameter.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context, enable_prompt_caching=False) + + self.assertEqual(params["system"], "You are helpful.") + self.assertEqual(len(params["messages"]), 1) + self.assertEqual(params["messages"][0]["role"], "user") + + def test_initial_developer_message_promoted(self): + """Initial developer message without system_instruction is promoted to system.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context, enable_prompt_caching=False) + + self.assertEqual(params["system"], "Extra context.") + self.assertEqual(len(params["messages"]), 1) + + def test_both_system_instruction_and_system_message_warns(self): + """system_instruction + initial system message warns and uses system_instruction.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + params = self.adapter.get_llm_invocation_params( + context, + enable_prompt_caching=False, + system_instruction="Be concise.", + ) + mock_logger.warning.assert_called_once() + warning_msg = mock_logger.warning.call_args[0][0] + self.assertIn("Using system_instruction", warning_msg) + + self.assertEqual(params["system"], "Be concise.") + + def test_both_system_instruction_and_developer_message_no_warning(self): + """system_instruction + initial developer message: no warning, developer becomes user.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "assistant", "content": "Hi"}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + params = self.adapter.get_llm_invocation_params( + context, + enable_prompt_caching=False, + system_instruction="Be concise.", + ) + mock_logger.warning.assert_not_called() + + self.assertEqual(params["system"], "Be concise.") + # Developer message should have been converted to "user" + self.assertEqual(params["messages"][0]["role"], "user") + self.assertEqual(params["messages"][0]["content"], "Extra context.") + + def test_subsequent_developer_messages_converted_to_user(self): + """Subsequent developer messages are converted to user role.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi"}, + {"role": "developer", "content": "More instructions"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context, enable_prompt_caching=False) + + # Developer message was converted to "user" + self.assertEqual(params["messages"][2]["role"], "user") + self.assertEqual(params["messages"][2]["content"], "More instructions") + + def test_initial_system_discarded_when_system_instruction_provided(self): + """Initial system message is discarded when system_instruction is provided.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "Old instruction."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger"): + params = self.adapter.get_llm_invocation_params( + context, + enable_prompt_caching=False, + system_instruction="New instruction.", + ) + + self.assertEqual(params["system"], "New instruction.") + # Only the user message should remain + self.assertEqual(len(params["messages"]), 1) + self.assertEqual(params["messages"][0]["role"], "user") + + def test_single_system_message_becomes_user(self): + """A lone system message is converted to user (not extracted) to prevent empty history.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context, enable_prompt_caching=False) + + from anthropic import NOT_GIVEN + + self.assertEqual(params["system"], NOT_GIVEN) + self.assertEqual(len(params["messages"]), 1) + self.assertEqual(params["messages"][0]["role"], "user") + + +class TestBedrockAdapterSystemInstruction(unittest.TestCase): + """Tests for the AWS Bedrock adapter.""" + + def setUp(self): + from pipecat.adapters.services.bedrock_adapter import AWSBedrockLLMAdapter + + self.adapter = AWSBedrockLLMAdapter() + + def test_system_instruction_only(self): + """system_instruction alone becomes the system parameter.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context, system_instruction="Be helpful.") + + self.assertEqual(params["system"], [{"text": "Be helpful."}]) + + def test_initial_system_message_only(self): + """Initial system message is extracted as the system parameter.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["system"], [{"text": "You are helpful."}]) + self.assertEqual(len(params["messages"]), 1) + + def test_initial_developer_message_promoted(self): + """Initial developer message without system_instruction is promoted.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["system"], [{"text": "Extra context."}]) + + def test_both_system_instruction_and_system_message_warns(self): + """system_instruction + initial system message warns and uses system_instruction.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + params = self.adapter.get_llm_invocation_params( + context, system_instruction="Be concise." + ) + mock_logger.warning.assert_called_once() + + self.assertEqual(params["system"], [{"text": "Be concise."}]) + + def test_both_system_instruction_and_developer_message_no_warning(self): + """system_instruction + initial developer message: no warning, developer becomes user.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + params = self.adapter.get_llm_invocation_params( + context, system_instruction="Be concise." + ) + mock_logger.warning.assert_not_called() + + self.assertEqual(params["system"], [{"text": "Be concise."}]) + self.assertEqual(params["messages"][0]["role"], "user") + + def test_subsequent_developer_messages_converted_to_user(self): + """Subsequent developer messages are converted to user role.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi"}, + {"role": "developer", "content": "More instructions"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["messages"][2]["role"], "user") + + def test_single_system_message_becomes_user(self): + """A lone system message is converted to user to prevent empty history.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertIsNone(params["system"]) + self.assertEqual(len(params["messages"]), 1) + self.assertEqual(params["messages"][0]["role"], "user") + + +class TestGeminiAdapterSystemInstruction(unittest.TestCase): + """Tests for the Gemini adapter.""" + + def setUp(self): + from pipecat.adapters.services.gemini_adapter import GeminiLLMAdapter + + self.adapter = GeminiLLMAdapter() + + def test_system_instruction_only(self): + """system_instruction alone becomes the system_instruction parameter.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context, system_instruction="Be helpful.") + + self.assertEqual(params["system_instruction"], "Be helpful.") + + def test_initial_system_message_only(self): + """Initial system message is extracted as system_instruction.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["system_instruction"], "You are helpful.") + self.assertEqual(len(params["messages"]), 1) + + def test_initial_developer_message_promoted(self): + """Initial developer message without system_instruction is promoted.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["system_instruction"], "Extra context.") + + def test_both_system_instruction_and_system_message_warns(self): + """system_instruction + initial system message warns and uses system_instruction.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + params = self.adapter.get_llm_invocation_params( + context, system_instruction="Be concise." + ) + mock_logger.warning.assert_called_once() + + self.assertEqual(params["system_instruction"], "Be concise.") + + def test_both_system_instruction_and_developer_message_no_warning(self): + """system_instruction + initial developer message: no warning, developer becomes user.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + params = self.adapter.get_llm_invocation_params( + context, system_instruction="Be concise." + ) + mock_logger.warning.assert_not_called() + + self.assertEqual(params["system_instruction"], "Be concise.") + + def test_non_initial_system_message_not_extracted(self): + """Non-initial system message is converted to user, not extracted as system instruction.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + {"role": "system", "content": "Late system message"}, + {"role": "user", "content": "How are you?"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + # No system instruction should be extracted from non-initial position + self.assertIsNone(params["system_instruction"]) + # The system message should have been converted to user role in the Gemini Content + # (we check that 3 messages are present, meaning no extraction happened) + self.assertEqual(len(params["messages"]), 3) + + def test_subsequent_developer_messages_converted_to_user(self): + """Subsequent developer messages are converted to user role.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + {"role": "developer", "content": "More instructions"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["messages"]), 2) + # Second message (developer) should be converted to user in Google format + self.assertEqual(params["messages"][1].role, "user") + + +class TestBaseLLMAdapterHelpers(unittest.TestCase): + """Tests for the shared helper methods on BaseLLMAdapter.""" + + def setUp(self): + # Use OpenAILLMAdapter as a concrete implementation for testing the base helpers + self.adapter = OpenAILLMAdapter() + + def test_extract_system_message(self): + """System message is extracted from messages[0].""" + messages = [ + {"role": "system", "content": "Be helpful."}, + {"role": "user", "content": "Hello"}, + ] + content, role = self.adapter._extract_initial_system_or_developer( + messages, system_instruction=None + ) + + self.assertEqual(content, "Be helpful.") + self.assertEqual(role, "system") + self.assertEqual(len(messages), 1) # popped + + def test_extract_developer_without_system_instruction(self): + """Developer message is extracted when no system_instruction.""" + messages = [ + {"role": "developer", "content": "Context."}, + {"role": "user", "content": "Hello"}, + ] + content, role = self.adapter._extract_initial_system_or_developer( + messages, system_instruction=None + ) + + self.assertEqual(content, "Context.") + self.assertEqual(role, "developer") + self.assertEqual(len(messages), 1) + + def test_developer_with_system_instruction_converts_to_user(self): + """Developer message with system_instruction is converted to user, not extracted.""" + messages = [ + {"role": "developer", "content": "Context."}, + {"role": "user", "content": "Hello"}, + ] + content, role = self.adapter._extract_initial_system_or_developer( + messages, system_instruction="Be helpful." + ) + + self.assertIsNone(content) + self.assertIsNone(role) + self.assertEqual(len(messages), 2) # not popped + self.assertEqual(messages[0]["role"], "user") # converted to user + + def test_single_system_message_becomes_user(self): + """Single system message is converted to user instead of extracting (empty prevention).""" + messages = [ + {"role": "system", "content": "Be helpful."}, + ] + content, role = self.adapter._extract_initial_system_or_developer( + messages, system_instruction=None + ) + + self.assertIsNone(content) + self.assertIsNone(role) + self.assertEqual(len(messages), 1) # not popped + self.assertEqual(messages[0]["role"], "user") + + def test_non_system_message_ignored(self): + """Non-system/developer first message is ignored.""" + messages = [ + {"role": "user", "content": "Hello"}, + ] + content, role = self.adapter._extract_initial_system_or_developer( + messages, system_instruction=None + ) + + self.assertIsNone(content) + self.assertIsNone(role) + self.assertEqual(len(messages), 1) + + def test_empty_messages(self): + """Empty messages list returns None.""" + messages = [] + content, role = self.adapter._extract_initial_system_or_developer( + messages, system_instruction=None + ) + + self.assertIsNone(content) + self.assertIsNone(role) + + def test_resolve_both_system_discard(self): + """Resolve with discard=True: system_instruction wins, warns.""" + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + result = self.adapter._resolve_system_instruction( + "from context", "system", "from settings", discard_context_system=True + ) + mock_logger.warning.assert_called_once() + + self.assertEqual(result, "from settings") + + def test_resolve_both_system_keep(self): + """Resolve with discard=False: warns but returns system_instruction.""" + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + result = self.adapter._resolve_system_instruction( + "from context", "system", "from settings", discard_context_system=False + ) + mock_logger.warning.assert_called_once() + + self.assertEqual(result, "from settings") + + def test_resolve_only_system_instruction(self): + """Only system_instruction: returns it, no warning.""" + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + result = self.adapter._resolve_system_instruction( + None, None, "from settings", discard_context_system=True + ) + mock_logger.warning.assert_not_called() + + self.assertEqual(result, "from settings") + + def test_resolve_only_context_system_discard(self): + """Only context system (discard=True): returns it.""" + result = self.adapter._resolve_system_instruction( + "from context", "system", None, discard_context_system=True + ) + + self.assertEqual(result, "from context") + + def test_resolve_only_context_system_keep(self): + """Only context system (discard=False): returns None (already in messages).""" + result = self.adapter._resolve_system_instruction( + "from context", "system", None, discard_context_system=False + ) + + self.assertIsNone(result) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_get_llm_invocation_params.py b/tests/test_get_llm_invocation_params.py index 9cfeb8933..279288661 100644 --- a/tests/test_get_llm_invocation_params.py +++ b/tests/test_get_llm_invocation_params.py @@ -424,10 +424,11 @@ class TestGeminiGetLLMInvocationParams(unittest.TestCase): context = LLMContext(messages=messages) params = self.adapter.get_llm_invocation_params(context) - # System instruction should be extracted - self.assertEqual(params["system_instruction"], "You are a helpful assistant.") + # When there's only one message, it's converted to user in-place (not extracted) + # so system_instruction is None + self.assertIsNone(params["system_instruction"]) - # But since there are no other messages, it should also be added back as a user message + # The system message should be converted to a user message self.assertEqual(len(params["messages"]), 1) self.assertEqual(params["messages"][0].role, "user") self.assertEqual(params["messages"][0].parts[0].text, "You are a helpful assistant.") @@ -973,7 +974,7 @@ class TestAWSBedrockGetLLMInvocationParams(unittest.TestCase): self.assertEqual(params["messages"][2]["content"][0]["text"], "Remember to be concise.") def test_single_system_message_handling(self): - """Test that a single system message is extracted as system parameter and no messages remain.""" + """Test that a single system message is converted to user role when no other messages exist.""" messages = [ {"role": "system", "content": "You are a helpful assistant."}, ] @@ -984,13 +985,16 @@ class TestAWSBedrockGetLLMInvocationParams(unittest.TestCase): # Get invocation params params = self.adapter.get_llm_invocation_params(context) - # System should be extracted (in AWS Bedrock format) - self.assertIsInstance(params["system"], list) - self.assertEqual(len(params["system"]), 1) - self.assertEqual(params["system"][0]["text"], "You are a helpful assistant.") + # When there's only one message, it's converted to user in-place (not extracted) + # so system is None + self.assertIsNone(params["system"]) - # No messages should remain after system extraction - self.assertEqual(len(params["messages"]), 0) + # Single system message should be converted to user role + self.assertEqual(len(params["messages"]), 1) + self.assertEqual(params["messages"][0]["role"], "user") + self.assertEqual( + params["messages"][0]["content"][0]["text"], "You are a helpful assistant." + ) class TestPerplexityGetLLMInvocationParams(unittest.TestCase): diff --git a/tests/test_openai_responses_adapter.py b/tests/test_openai_responses_adapter.py index 973c05c8c..9b0237baf 100644 --- a/tests/test_openai_responses_adapter.py +++ b/tests/test_openai_responses_adapter.py @@ -58,9 +58,8 @@ class TestOpenAIResponsesAdapter(unittest.TestCase): self.assertEqual(params["input"][0]["role"], "developer") self.assertEqual(params["input"][0]["content"], "You are helpful.") - def test_first_system_message_triggers_warning(self): - """First system message triggers a warning about using system_instruction.""" - # Use a fresh adapter so the warning hasn't been emitted yet + def test_system_message_without_system_instruction_no_warning(self): + """System message without system_instruction does not trigger a warning.""" adapter = OpenAIResponsesLLMAdapter() messages: list[LLMStandardMessage] = [ {"role": "system", "content": "You are helpful."}, @@ -68,8 +67,21 @@ class TestOpenAIResponsesAdapter(unittest.TestCase): ] context = LLMContext(messages=messages) - with patch("pipecat.adapters.services.open_ai_responses_adapter.logger") as mock_logger: + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: adapter.get_llm_invocation_params(context) + mock_logger.warning.assert_not_called() + + def test_system_message_with_system_instruction_triggers_warning(self): + """System message + system_instruction triggers a conflict warning.""" + adapter = OpenAIResponsesLLMAdapter() + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + adapter.get_llm_invocation_params(context, system_instruction="Be concise.") mock_logger.warning.assert_called_once() warning_msg = mock_logger.warning.call_args[0][0] self.assertIn("system_instruction", warning_msg) @@ -83,15 +95,15 @@ class TestOpenAIResponsesAdapter(unittest.TestCase): context = LLMContext(messages=messages) adapter = OpenAIResponsesLLMAdapter() - with patch("pipecat.adapters.services.open_ai_responses_adapter.logger") as mock_logger: - params = adapter.get_llm_invocation_params(context) + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + params = adapter.get_llm_invocation_params(context, system_instruction="Be helpful.") mock_logger.warning.assert_not_called() self.assertEqual(params["input"][1]["role"], "developer") self.assertEqual(params["input"][1]["content"], "New instruction") - def test_first_system_message_warning_fires_only_once(self): - """The first-system-message warning fires only once per adapter instance.""" + def test_conflict_warning_fires_only_once(self): + """The conflict warning fires only once per adapter instance.""" messages: list[LLMStandardMessage] = [ {"role": "system", "content": "You are helpful."}, {"role": "user", "content": "Hello"}, @@ -99,9 +111,9 @@ class TestOpenAIResponsesAdapter(unittest.TestCase): context = LLMContext(messages=messages) adapter = OpenAIResponsesLLMAdapter() - with patch("pipecat.adapters.services.open_ai_responses_adapter.logger") as mock_logger: - adapter.get_llm_invocation_params(context) - adapter.get_llm_invocation_params(context) + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + adapter.get_llm_invocation_params(context, system_instruction="Be concise.") + adapter.get_llm_invocation_params(context, system_instruction="Be concise.") # Warning should have been emitted exactly once, not twice mock_logger.warning.assert_called_once() diff --git a/tests/test_run_inference.py b/tests/test_run_inference.py index cef13fb27..884b1b922 100644 --- a/tests/test_run_inference.py +++ b/tests/test_run_inference.py @@ -60,7 +60,9 @@ async def test_openai_run_inference_with_llm_context(): # Verify assert result == "Hello! How can I help you today?" service.get_llm_adapter.assert_called_once() - mock_adapter.get_llm_invocation_params.assert_called_once_with(mock_context) + mock_adapter.get_llm_invocation_params.assert_called_once_with( + mock_context, system_instruction=None + ) service._client.chat.completions.create.assert_called_once_with( model="gpt-4", stream=False, @@ -187,7 +189,7 @@ async def test_anthropic_run_inference_with_llm_context(): assert result == "Hello! How can I help you today?" service.get_llm_adapter.assert_called_once() mock_adapter.get_llm_invocation_params.assert_called_once_with( - mock_context, enable_prompt_caching=False + mock_context, enable_prompt_caching=False, system_instruction=None ) service._client.beta.messages.create.assert_called_once_with( model="claude-3-sonnet-20240229", @@ -302,7 +304,9 @@ async def test_google_run_inference_with_llm_context(): # Verify assert result == "Hello! How can I help you today?" service.get_llm_adapter.assert_called_once() - mock_adapter.get_llm_invocation_params.assert_called_once_with(mock_context) + mock_adapter.get_llm_invocation_params.assert_called_once_with( + mock_context, system_instruction=None + ) service._client.aio.models.generate_content.assert_called_once() @@ -421,7 +425,9 @@ async def test_aws_bedrock_run_inference_with_llm_context(): # Verify assert result == "Hello! How can I help you today?" service.get_llm_adapter.assert_called_once() - mock_adapter.get_llm_invocation_params.assert_called_once_with(mock_context) + mock_adapter.get_llm_invocation_params.assert_called_once_with( + mock_context, system_instruction=None + ) # Verify the call includes configured parameters call_kwargs = mock_client.converse.call_args.kwargs @@ -543,15 +549,10 @@ async def test_openai_run_inference_system_instruction_overrides_context(): ) assert result == "Response" - call_kwargs = service._client.chat.completions.create.call_args.kwargs - messages = call_kwargs["messages"] - # system_instruction should be prepended as the first message - assert messages[0] == {"role": "system", "content": "New system instruction"} - # Original system message should still be present - assert messages[1] == {"role": "system", "content": "Original system message"} - # User message should still be present - assert messages[2] == {"role": "user", "content": "Hello"} - assert len(messages) == 3 + # Verify the adapter was called with the correct system_instruction + mock_adapter.get_llm_invocation_params.assert_called_once_with( + mock_context, system_instruction="New system instruction" + ) @pytest.mark.asyncio @@ -608,9 +609,12 @@ async def test_anthropic_run_inference_system_instruction_overrides_context(): result = await service.run_inference(mock_context, system_instruction="New system instruction") assert result == "Response" - call_kwargs = service._client.beta.messages.create.call_args.kwargs - assert call_kwargs["system"] == "New system instruction" - assert call_kwargs["messages"] == test_messages + # Verify the adapter was called with the correct system_instruction + mock_adapter.get_llm_invocation_params.assert_called_once_with( + mock_context, + enable_prompt_caching=False, + system_instruction="New system instruction", + ) @pytest.mark.asyncio @@ -665,9 +669,10 @@ async def test_google_run_inference_system_instruction_overrides_context(): result = await service.run_inference(mock_context, system_instruction="New system instruction") assert result == "Response" - call_kwargs = service._client.aio.models.generate_content.call_args.kwargs - config = call_kwargs["config"] - assert config.system_instruction == "New system instruction" + # Verify the adapter was called with the correct system_instruction + mock_adapter.get_llm_invocation_params.assert_called_once_with( + mock_context, system_instruction="New system instruction" + ) @pytest.mark.asyncio @@ -731,9 +736,10 @@ async def test_aws_bedrock_run_inference_system_instruction_overrides_context(): ) assert result == "Response" - call_kwargs = mock_client.converse.call_args.kwargs - assert call_kwargs["system"] == [{"text": "New system instruction"}] - assert call_kwargs["messages"] == test_messages + # Verify the adapter was called with the correct system_instruction + mock_adapter.get_llm_invocation_params.assert_called_once_with( + mock_context, system_instruction="New system instruction" + ) @pytest.mark.asyncio From bb7199d143d801a361347ceb63dbd8ae9bfcae38 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Mar 2026 10:33:14 -0400 Subject: [PATCH 02/23] Add changelog entries for #4089 --- changelog/4089.added.md | 1 + changelog/4089.changed.md | 1 + changelog/4089.fixed.md | 1 + 3 files changed, 3 insertions(+) create mode 100644 changelog/4089.added.md create mode 100644 changelog/4089.changed.md create mode 100644 changelog/4089.fixed.md diff --git a/changelog/4089.added.md b/changelog/4089.added.md new file mode 100644 index 000000000..5b31081a2 --- /dev/null +++ b/changelog/4089.added.md @@ -0,0 +1 @@ +- Added support for "developer" role messages in conversation context across all LLM adapters. For non-OpenAI services (Anthropic, Google, AWS Bedrock), an initial "developer" message is promoted to the system instruction when no `system_instruction` is configured; otherwise it is converted to a "user" message. For OpenAI services, "developer" messages pass through in conversation history. For the Responses API, they are kept as "developer" role (matching the existing "system" → "developer" conversion). diff --git a/changelog/4089.changed.md b/changelog/4089.changed.md new file mode 100644 index 000000000..f44a7b772 --- /dev/null +++ b/changelog/4089.changed.md @@ -0,0 +1 @@ +- ⚠️ `GeminiLLMAdapter` now only treats `messages[0]` as the initial system message, matching all other adapters. Previously it searched for the first "system" message anywhere in the conversation history. A "system" message appearing later in the list will now be converted to "user" instead of being extracted as the system instruction. diff --git a/changelog/4089.fixed.md b/changelog/4089.fixed.md new file mode 100644 index 000000000..03cadf4d2 --- /dev/null +++ b/changelog/4089.fixed.md @@ -0,0 +1 @@ +- Fixed `AWSBedrockLLMAdapter` sending an empty message list to the API when the only message in context was a system message. The lone system message is now converted to "user" role instead of being extracted, matching the existing Anthropic adapter behavior. From 45178972d7be9353ad4e9e093ec9e260d8242a58 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Mar 2026 14:00:56 -0400 Subject: [PATCH 03/23] Fix stale docstring in PerplexityLLMAdapter --- src/pipecat/adapters/services/perplexity_adapter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pipecat/adapters/services/perplexity_adapter.py b/src/pipecat/adapters/services/perplexity_adapter.py index 5359baddc..2055914e8 100644 --- a/src/pipecat/adapters/services/perplexity_adapter.py +++ b/src/pipecat/adapters/services/perplexity_adapter.py @@ -45,8 +45,8 @@ class PerplexityLLMAdapter(OpenAILLMAdapter): no non-initial system messages, last message must be user/tool). The transformations are applied in ``get_llm_invocation_params`` after the - parent adapter extracts messages from the LLM context, and before - ``build_chat_completion_params`` prepends ``system_instruction``. + parent adapter extracts messages from the LLM context (including any + ``system_instruction`` prepend). """ def get_llm_invocation_params( From e29a63e1ae2ed5a48783c664abf842ab2025900a Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Mar 2026 14:11:40 -0400 Subject: [PATCH 04/23] Improve _extract_initial_system_or_developer docstring clarity --- src/pipecat/adapters/base_llm_adapter.py | 32 +++++++++++++++--------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/pipecat/adapters/base_llm_adapter.py b/src/pipecat/adapters/base_llm_adapter.py index 672004e7b..197089669 100644 --- a/src/pipecat/adapters/base_llm_adapter.py +++ b/src/pipecat/adapters/base_llm_adapter.py @@ -141,25 +141,33 @@ class BaseLLMAdapter(ABC, Generic[TLLMInvocationParams]): *, system_instruction: Optional[str], ) -> Tuple[Optional[str], Optional[str]]: - """Extract an initial system/developer message from messages, if appropriate. + """Extract an initial system/developer message for use as a system instruction. + + Only useful for services that expect the system instruction as a + separate parameter, not inline in conversation history (today, all + non-OpenAI services). Checks ``messages[0]``. Behavior: - - ``"system"`` role: always extract (pop from messages). - - ``"developer"`` role **without** ``system_instruction``: extract (pop). - - ``"developer"`` role **with** ``system_instruction``: don't extract; - convert to ``"user"`` in-place. + - ``"system"`` role: assumed to be intended as the system instruction. + Extract (pop from messages). + - ``"developer"`` role **without** ``system_instruction``: also assumed + to be intended as the system instruction. Extract (pop). + - ``"developer"`` role **with** ``system_instruction``: assumed to be + intended as a conversation-history message (since a system instruction + is already provided). Don't extract; convert to ``"user"`` in-place. - Any other role: no-op. - If extracting would leave the messages list empty (``len(messages) == 1``), - the message is converted to ``"user"`` role instead of being extracted. - This prevents sending an empty conversation history to providers that - require at least one non-system message (e.g. Anthropic, Bedrock). + If extracting would leave the messages list empty + (``len(messages) == 1``), the message is converted to ``"user"`` role + instead of being extracted. This prevents sending an empty conversation + history to providers that require at least one non-system message + (e.g. Anthropic, Bedrock). Args: - messages: Provider-formatted message list (mutated in-place). - system_instruction: The system instruction from service settings or - ``run_inference``, used to decide whether to extract a + messages: Message list in standard format (mutated in-place). + system_instruction: The system instruction from service settings + or ``run_inference``, used to decide whether to extract a ``"developer"`` message. Returns: From 3bbec0a2c8bf0eb65a6c0a773f4c0b6e32455ba7 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Mar 2026 14:21:17 -0400 Subject: [PATCH 05/23] Broaden docstring: all non-OpenAI providers need non-empty messages --- src/pipecat/adapters/base_llm_adapter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pipecat/adapters/base_llm_adapter.py b/src/pipecat/adapters/base_llm_adapter.py index 197089669..aad8664e7 100644 --- a/src/pipecat/adapters/base_llm_adapter.py +++ b/src/pipecat/adapters/base_llm_adapter.py @@ -161,8 +161,7 @@ class BaseLLMAdapter(ABC, Generic[TLLMInvocationParams]): If extracting would leave the messages list empty (``len(messages) == 1``), the message is converted to ``"user"`` role instead of being extracted. This prevents sending an empty conversation - history to providers that require at least one non-system message - (e.g. Anthropic, Bedrock). + history to providers that require at least one non-system message. Args: messages: Message list in standard format (mutated in-place). From 7377d88cf5d6660f9b952043a5fc865b3023723a Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Mar 2026 14:36:23 -0400 Subject: [PATCH 06/23] Move system_instruction tests into test_get_llm_invocation_params.py --- tests/test_adapter_system_instruction.py | 587 ----------------------- tests/test_get_llm_invocation_params.py | 485 +++++++++++++++++++ 2 files changed, 485 insertions(+), 587 deletions(-) delete mode 100644 tests/test_adapter_system_instruction.py diff --git a/tests/test_adapter_system_instruction.py b/tests/test_adapter_system_instruction.py deleted file mode 100644 index 125702dd6..000000000 --- a/tests/test_adapter_system_instruction.py +++ /dev/null @@ -1,587 +0,0 @@ -# -# Copyright (c) 2024-2026, Daily -# -# SPDX-License-Identifier: BSD 2-Clause License -# - -"""Unit tests for system_instruction and developer message handling in LLM adapters. - -Tests cover: - -1. system_instruction only (no system/developer in context) -2. Initial "system" message only (no system_instruction) -3. Initial "developer" message only (no system_instruction) -> promoted to system instruction -4. Both system_instruction and initial "system" message -> warns -5. Both system_instruction and initial "developer" message -> does NOT warn; developer becomes "user" -6. Non-OpenAI adapters: subsequent "developer" messages converted to "user" -7. Non-OpenAI adapters: initial "system" discarded when system_instruction provided -8. Gemini: non-initial "system" message is converted to "user" (not extracted) -9. Single system-only message: converted to "user" instead of extracting (empty list prevention) -""" - -import unittest -from unittest.mock import patch - -from pipecat.adapters.services.open_ai_adapter import OpenAILLMAdapter -from pipecat.processors.aggregators.llm_context import LLMContext, LLMStandardMessage - - -class TestOpenAIAdapterSystemInstruction(unittest.TestCase): - """Tests for the OpenAI ChatCompletion adapter.""" - - def setUp(self): - self.adapter = OpenAILLMAdapter() - - def test_system_instruction_only(self): - """system_instruction alone is prepended as a system message.""" - messages: list[LLMStandardMessage] = [ - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context, system_instruction="Be helpful.") - - self.assertEqual(params["messages"][0]["role"], "system") - self.assertEqual(params["messages"][0]["content"], "Be helpful.") - self.assertEqual(params["messages"][1]["role"], "user") - - def test_initial_system_message_only(self): - """Initial system message without system_instruction passes through.""" - messages: list[LLMStandardMessage] = [ - {"role": "system", "content": "You are helpful."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) - - self.assertEqual(len(params["messages"]), 2) - self.assertEqual(params["messages"][0]["role"], "system") - self.assertEqual(params["messages"][0]["content"], "You are helpful.") - - def test_both_system_instruction_and_system_message_warns(self): - """system_instruction + initial system message warns but allows both.""" - messages: list[LLMStandardMessage] = [ - {"role": "system", "content": "You are helpful."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - - with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: - params = self.adapter.get_llm_invocation_params( - context, system_instruction="Be concise." - ) - mock_logger.warning.assert_called_once() - warning_msg = mock_logger.warning.call_args[0][0] - self.assertIn("may be unintended", warning_msg) - - # Both are present: prepended system_instruction + original system message - self.assertEqual(params["messages"][0]["content"], "Be concise.") - self.assertEqual(params["messages"][1]["content"], "You are helpful.") - - def test_both_system_instruction_and_developer_message_no_warning(self): - """system_instruction + initial developer message does NOT warn.""" - messages: list[LLMStandardMessage] = [ - {"role": "developer", "content": "Extra context."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - - with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: - params = self.adapter.get_llm_invocation_params( - context, system_instruction="Be concise." - ) - mock_logger.warning.assert_not_called() - - # system_instruction prepended, developer message stays in messages - self.assertEqual(params["messages"][0]["content"], "Be concise.") - self.assertEqual(params["messages"][1]["role"], "developer") - - def test_warning_fires_only_once(self): - """Conflict warning fires only once per adapter instance.""" - messages: list[LLMStandardMessage] = [ - {"role": "system", "content": "You are helpful."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - - with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: - self.adapter.get_llm_invocation_params(context, system_instruction="Be concise.") - self.adapter.get_llm_invocation_params(context, system_instruction="Be concise.") - mock_logger.warning.assert_called_once() - - -class TestAnthropicAdapterSystemInstruction(unittest.TestCase): - """Tests for the Anthropic adapter.""" - - def setUp(self): - from pipecat.adapters.services.anthropic_adapter import AnthropicLLMAdapter - - self.adapter = AnthropicLLMAdapter() - - def test_system_instruction_only(self): - """system_instruction alone becomes the system parameter.""" - messages: list[LLMStandardMessage] = [ - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params( - context, enable_prompt_caching=False, system_instruction="Be helpful." - ) - - self.assertEqual(params["system"], "Be helpful.") - self.assertEqual(len(params["messages"]), 1) - self.assertEqual(params["messages"][0]["role"], "user") - - def test_initial_system_message_only(self): - """Initial system message is extracted as the system parameter.""" - messages: list[LLMStandardMessage] = [ - {"role": "system", "content": "You are helpful."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context, enable_prompt_caching=False) - - self.assertEqual(params["system"], "You are helpful.") - self.assertEqual(len(params["messages"]), 1) - self.assertEqual(params["messages"][0]["role"], "user") - - def test_initial_developer_message_promoted(self): - """Initial developer message without system_instruction is promoted to system.""" - messages: list[LLMStandardMessage] = [ - {"role": "developer", "content": "Extra context."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context, enable_prompt_caching=False) - - self.assertEqual(params["system"], "Extra context.") - self.assertEqual(len(params["messages"]), 1) - - def test_both_system_instruction_and_system_message_warns(self): - """system_instruction + initial system message warns and uses system_instruction.""" - messages: list[LLMStandardMessage] = [ - {"role": "system", "content": "You are helpful."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - - with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: - params = self.adapter.get_llm_invocation_params( - context, - enable_prompt_caching=False, - system_instruction="Be concise.", - ) - mock_logger.warning.assert_called_once() - warning_msg = mock_logger.warning.call_args[0][0] - self.assertIn("Using system_instruction", warning_msg) - - self.assertEqual(params["system"], "Be concise.") - - def test_both_system_instruction_and_developer_message_no_warning(self): - """system_instruction + initial developer message: no warning, developer becomes user.""" - messages: list[LLMStandardMessage] = [ - {"role": "developer", "content": "Extra context."}, - {"role": "assistant", "content": "Hi"}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - - with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: - params = self.adapter.get_llm_invocation_params( - context, - enable_prompt_caching=False, - system_instruction="Be concise.", - ) - mock_logger.warning.assert_not_called() - - self.assertEqual(params["system"], "Be concise.") - # Developer message should have been converted to "user" - self.assertEqual(params["messages"][0]["role"], "user") - self.assertEqual(params["messages"][0]["content"], "Extra context.") - - def test_subsequent_developer_messages_converted_to_user(self): - """Subsequent developer messages are converted to user role.""" - messages: list[LLMStandardMessage] = [ - {"role": "user", "content": "Hello"}, - {"role": "assistant", "content": "Hi"}, - {"role": "developer", "content": "More instructions"}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context, enable_prompt_caching=False) - - # Developer message was converted to "user" - self.assertEqual(params["messages"][2]["role"], "user") - self.assertEqual(params["messages"][2]["content"], "More instructions") - - def test_initial_system_discarded_when_system_instruction_provided(self): - """Initial system message is discarded when system_instruction is provided.""" - messages: list[LLMStandardMessage] = [ - {"role": "system", "content": "Old instruction."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - - with patch("pipecat.adapters.base_llm_adapter.logger"): - params = self.adapter.get_llm_invocation_params( - context, - enable_prompt_caching=False, - system_instruction="New instruction.", - ) - - self.assertEqual(params["system"], "New instruction.") - # Only the user message should remain - self.assertEqual(len(params["messages"]), 1) - self.assertEqual(params["messages"][0]["role"], "user") - - def test_single_system_message_becomes_user(self): - """A lone system message is converted to user (not extracted) to prevent empty history.""" - messages: list[LLMStandardMessage] = [ - {"role": "system", "content": "You are helpful."}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context, enable_prompt_caching=False) - - from anthropic import NOT_GIVEN - - self.assertEqual(params["system"], NOT_GIVEN) - self.assertEqual(len(params["messages"]), 1) - self.assertEqual(params["messages"][0]["role"], "user") - - -class TestBedrockAdapterSystemInstruction(unittest.TestCase): - """Tests for the AWS Bedrock adapter.""" - - def setUp(self): - from pipecat.adapters.services.bedrock_adapter import AWSBedrockLLMAdapter - - self.adapter = AWSBedrockLLMAdapter() - - def test_system_instruction_only(self): - """system_instruction alone becomes the system parameter.""" - messages: list[LLMStandardMessage] = [ - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context, system_instruction="Be helpful.") - - self.assertEqual(params["system"], [{"text": "Be helpful."}]) - - def test_initial_system_message_only(self): - """Initial system message is extracted as the system parameter.""" - messages: list[LLMStandardMessage] = [ - {"role": "system", "content": "You are helpful."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) - - self.assertEqual(params["system"], [{"text": "You are helpful."}]) - self.assertEqual(len(params["messages"]), 1) - - def test_initial_developer_message_promoted(self): - """Initial developer message without system_instruction is promoted.""" - messages: list[LLMStandardMessage] = [ - {"role": "developer", "content": "Extra context."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) - - self.assertEqual(params["system"], [{"text": "Extra context."}]) - - def test_both_system_instruction_and_system_message_warns(self): - """system_instruction + initial system message warns and uses system_instruction.""" - messages: list[LLMStandardMessage] = [ - {"role": "system", "content": "You are helpful."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - - with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: - params = self.adapter.get_llm_invocation_params( - context, system_instruction="Be concise." - ) - mock_logger.warning.assert_called_once() - - self.assertEqual(params["system"], [{"text": "Be concise."}]) - - def test_both_system_instruction_and_developer_message_no_warning(self): - """system_instruction + initial developer message: no warning, developer becomes user.""" - messages: list[LLMStandardMessage] = [ - {"role": "developer", "content": "Extra context."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - - with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: - params = self.adapter.get_llm_invocation_params( - context, system_instruction="Be concise." - ) - mock_logger.warning.assert_not_called() - - self.assertEqual(params["system"], [{"text": "Be concise."}]) - self.assertEqual(params["messages"][0]["role"], "user") - - def test_subsequent_developer_messages_converted_to_user(self): - """Subsequent developer messages are converted to user role.""" - messages: list[LLMStandardMessage] = [ - {"role": "user", "content": "Hello"}, - {"role": "assistant", "content": "Hi"}, - {"role": "developer", "content": "More instructions"}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) - - self.assertEqual(params["messages"][2]["role"], "user") - - def test_single_system_message_becomes_user(self): - """A lone system message is converted to user to prevent empty history.""" - messages: list[LLMStandardMessage] = [ - {"role": "system", "content": "You are helpful."}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) - - self.assertIsNone(params["system"]) - self.assertEqual(len(params["messages"]), 1) - self.assertEqual(params["messages"][0]["role"], "user") - - -class TestGeminiAdapterSystemInstruction(unittest.TestCase): - """Tests for the Gemini adapter.""" - - def setUp(self): - from pipecat.adapters.services.gemini_adapter import GeminiLLMAdapter - - self.adapter = GeminiLLMAdapter() - - def test_system_instruction_only(self): - """system_instruction alone becomes the system_instruction parameter.""" - messages: list[LLMStandardMessage] = [ - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context, system_instruction="Be helpful.") - - self.assertEqual(params["system_instruction"], "Be helpful.") - - def test_initial_system_message_only(self): - """Initial system message is extracted as system_instruction.""" - messages: list[LLMStandardMessage] = [ - {"role": "system", "content": "You are helpful."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) - - self.assertEqual(params["system_instruction"], "You are helpful.") - self.assertEqual(len(params["messages"]), 1) - - def test_initial_developer_message_promoted(self): - """Initial developer message without system_instruction is promoted.""" - messages: list[LLMStandardMessage] = [ - {"role": "developer", "content": "Extra context."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) - - self.assertEqual(params["system_instruction"], "Extra context.") - - def test_both_system_instruction_and_system_message_warns(self): - """system_instruction + initial system message warns and uses system_instruction.""" - messages: list[LLMStandardMessage] = [ - {"role": "system", "content": "You are helpful."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - - with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: - params = self.adapter.get_llm_invocation_params( - context, system_instruction="Be concise." - ) - mock_logger.warning.assert_called_once() - - self.assertEqual(params["system_instruction"], "Be concise.") - - def test_both_system_instruction_and_developer_message_no_warning(self): - """system_instruction + initial developer message: no warning, developer becomes user.""" - messages: list[LLMStandardMessage] = [ - {"role": "developer", "content": "Extra context."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - - with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: - params = self.adapter.get_llm_invocation_params( - context, system_instruction="Be concise." - ) - mock_logger.warning.assert_not_called() - - self.assertEqual(params["system_instruction"], "Be concise.") - - def test_non_initial_system_message_not_extracted(self): - """Non-initial system message is converted to user, not extracted as system instruction.""" - messages: list[LLMStandardMessage] = [ - {"role": "user", "content": "Hello"}, - {"role": "system", "content": "Late system message"}, - {"role": "user", "content": "How are you?"}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) - - # No system instruction should be extracted from non-initial position - self.assertIsNone(params["system_instruction"]) - # The system message should have been converted to user role in the Gemini Content - # (we check that 3 messages are present, meaning no extraction happened) - self.assertEqual(len(params["messages"]), 3) - - def test_subsequent_developer_messages_converted_to_user(self): - """Subsequent developer messages are converted to user role.""" - messages: list[LLMStandardMessage] = [ - {"role": "user", "content": "Hello"}, - {"role": "developer", "content": "More instructions"}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) - - self.assertEqual(len(params["messages"]), 2) - # Second message (developer) should be converted to user in Google format - self.assertEqual(params["messages"][1].role, "user") - - -class TestBaseLLMAdapterHelpers(unittest.TestCase): - """Tests for the shared helper methods on BaseLLMAdapter.""" - - def setUp(self): - # Use OpenAILLMAdapter as a concrete implementation for testing the base helpers - self.adapter = OpenAILLMAdapter() - - def test_extract_system_message(self): - """System message is extracted from messages[0].""" - messages = [ - {"role": "system", "content": "Be helpful."}, - {"role": "user", "content": "Hello"}, - ] - content, role = self.adapter._extract_initial_system_or_developer( - messages, system_instruction=None - ) - - self.assertEqual(content, "Be helpful.") - self.assertEqual(role, "system") - self.assertEqual(len(messages), 1) # popped - - def test_extract_developer_without_system_instruction(self): - """Developer message is extracted when no system_instruction.""" - messages = [ - {"role": "developer", "content": "Context."}, - {"role": "user", "content": "Hello"}, - ] - content, role = self.adapter._extract_initial_system_or_developer( - messages, system_instruction=None - ) - - self.assertEqual(content, "Context.") - self.assertEqual(role, "developer") - self.assertEqual(len(messages), 1) - - def test_developer_with_system_instruction_converts_to_user(self): - """Developer message with system_instruction is converted to user, not extracted.""" - messages = [ - {"role": "developer", "content": "Context."}, - {"role": "user", "content": "Hello"}, - ] - content, role = self.adapter._extract_initial_system_or_developer( - messages, system_instruction="Be helpful." - ) - - self.assertIsNone(content) - self.assertIsNone(role) - self.assertEqual(len(messages), 2) # not popped - self.assertEqual(messages[0]["role"], "user") # converted to user - - def test_single_system_message_becomes_user(self): - """Single system message is converted to user instead of extracting (empty prevention).""" - messages = [ - {"role": "system", "content": "Be helpful."}, - ] - content, role = self.adapter._extract_initial_system_or_developer( - messages, system_instruction=None - ) - - self.assertIsNone(content) - self.assertIsNone(role) - self.assertEqual(len(messages), 1) # not popped - self.assertEqual(messages[0]["role"], "user") - - def test_non_system_message_ignored(self): - """Non-system/developer first message is ignored.""" - messages = [ - {"role": "user", "content": "Hello"}, - ] - content, role = self.adapter._extract_initial_system_or_developer( - messages, system_instruction=None - ) - - self.assertIsNone(content) - self.assertIsNone(role) - self.assertEqual(len(messages), 1) - - def test_empty_messages(self): - """Empty messages list returns None.""" - messages = [] - content, role = self.adapter._extract_initial_system_or_developer( - messages, system_instruction=None - ) - - self.assertIsNone(content) - self.assertIsNone(role) - - def test_resolve_both_system_discard(self): - """Resolve with discard=True: system_instruction wins, warns.""" - with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: - result = self.adapter._resolve_system_instruction( - "from context", "system", "from settings", discard_context_system=True - ) - mock_logger.warning.assert_called_once() - - self.assertEqual(result, "from settings") - - def test_resolve_both_system_keep(self): - """Resolve with discard=False: warns but returns system_instruction.""" - with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: - result = self.adapter._resolve_system_instruction( - "from context", "system", "from settings", discard_context_system=False - ) - mock_logger.warning.assert_called_once() - - self.assertEqual(result, "from settings") - - def test_resolve_only_system_instruction(self): - """Only system_instruction: returns it, no warning.""" - with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: - result = self.adapter._resolve_system_instruction( - None, None, "from settings", discard_context_system=True - ) - mock_logger.warning.assert_not_called() - - self.assertEqual(result, "from settings") - - def test_resolve_only_context_system_discard(self): - """Only context system (discard=True): returns it.""" - result = self.adapter._resolve_system_instruction( - "from context", "system", None, discard_context_system=True - ) - - self.assertEqual(result, "from context") - - def test_resolve_only_context_system_keep(self): - """Only context system (discard=False): returns None (already in messages).""" - result = self.adapter._resolve_system_instruction( - "from context", "system", None, discard_context_system=False - ) - - self.assertIsNone(result) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_get_llm_invocation_params.py b/tests/test_get_llm_invocation_params.py index 279288661..d88d6193d 100644 --- a/tests/test_get_llm_invocation_params.py +++ b/tests/test_get_llm_invocation_params.py @@ -14,6 +14,8 @@ For OpenAI adapter: 2. LLMSpecificMessage objects with llm='openai' are included and others are filtered out 3. Complex message structures (like multi-part content) are preserved 4. System instructions are preserved throughout messages at any position +5. system_instruction is prepended as a system message, with conflict warnings +6. Developer messages pass through without triggering warnings For Gemini adapter: 1. LLMStandardMessage objects are converted to Gemini Content format @@ -22,6 +24,8 @@ For Gemini adapter: 4. System messages are extracted as system_instruction (without duplication) 5. Single system instruction is converted to user message when no other messages exist 6. Multiple system instructions: first extracted, later ones converted to user messages +7. system_instruction overrides context system message, with conflict warnings +8. Developer messages are promoted to system instruction or converted to user For Anthropic adapter: 1. LLMStandardMessage objects are converted to Anthropic MessageParam format @@ -30,6 +34,8 @@ For Anthropic adapter: 4. System messages: first extracted as system parameter, later ones converted to user messages 5. Consecutive messages with same role are merged into multi-content-block messages 6. Empty text content is converted to "(empty)" +7. system_instruction overrides context system message, with conflict warnings +8. Developer messages are promoted to system instruction or converted to user For AWS Bedrock adapter: 1. LLMStandardMessage objects are converted to AWS Bedrock format @@ -38,9 +44,16 @@ For AWS Bedrock adapter: 4. System messages: first extracted as system parameter, later ones converted to user messages 5. Consecutive messages with same role are merged into multi-content-block messages 6. Empty text content is converted to "(empty)" +7. system_instruction overrides context system message, with conflict warnings +8. Developer messages are promoted to system instruction or converted to user + +For BaseLLMAdapter helpers: +1. _extract_initial_system_or_developer: system/developer extraction and conversion logic +2. _resolve_system_instruction: conflict resolution between context and settings """ import unittest +from unittest.mock import patch from google.genai.types import Content, Part @@ -214,6 +227,82 @@ class TestOpenAIGetLLMInvocationParams(unittest.TestCase): self.assertEqual(params["messages"][4]["role"], "user") self.assertEqual(params["messages"][6]["role"], "assistant") + def test_system_instruction_only(self): + """system_instruction alone is prepended as a system message.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context, system_instruction="Be helpful.") + + self.assertEqual(params["messages"][0]["role"], "system") + self.assertEqual(params["messages"][0]["content"], "Be helpful.") + self.assertEqual(params["messages"][1]["role"], "user") + + def test_initial_system_message_only(self): + """Initial system message without system_instruction passes through.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["messages"]), 2) + self.assertEqual(params["messages"][0]["role"], "system") + self.assertEqual(params["messages"][0]["content"], "You are helpful.") + + def test_both_system_instruction_and_system_message_warns(self): + """system_instruction + initial system message warns but allows both.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + params = self.adapter.get_llm_invocation_params( + context, system_instruction="Be concise." + ) + mock_logger.warning.assert_called_once() + warning_msg = mock_logger.warning.call_args[0][0] + self.assertIn("may be unintended", warning_msg) + + # Both are present: prepended system_instruction + original system message + self.assertEqual(params["messages"][0]["content"], "Be concise.") + self.assertEqual(params["messages"][1]["content"], "You are helpful.") + + def test_both_system_instruction_and_developer_message_no_warning(self): + """system_instruction + initial developer message does NOT warn.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + params = self.adapter.get_llm_invocation_params( + context, system_instruction="Be concise." + ) + mock_logger.warning.assert_not_called() + + # system_instruction prepended, developer message stays in messages + self.assertEqual(params["messages"][0]["content"], "Be concise.") + self.assertEqual(params["messages"][1]["role"], "developer") + + def test_warning_fires_only_once(self): + """Conflict warning fires only once per adapter instance.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + self.adapter.get_llm_invocation_params(context, system_instruction="Be concise.") + self.adapter.get_llm_invocation_params(context, system_instruction="Be concise.") + mock_logger.warning.assert_called_once() + class TestGeminiGetLLMInvocationParams(unittest.TestCase): def setUp(self) -> None: @@ -478,6 +567,100 @@ class TestGeminiGetLLMInvocationParams(unittest.TestCase): # Should have 2 model messages (converted from assistant) self.assertEqual(len(model_messages), 2) + def test_system_instruction_only(self): + """system_instruction alone becomes the system_instruction parameter.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context, system_instruction="Be helpful.") + + self.assertEqual(params["system_instruction"], "Be helpful.") + + def test_initial_system_message_only(self): + """Initial system message is extracted as system_instruction.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["system_instruction"], "You are helpful.") + self.assertEqual(len(params["messages"]), 1) + + def test_initial_developer_message_promoted(self): + """Initial developer message without system_instruction is promoted.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["system_instruction"], "Extra context.") + + def test_both_system_instruction_and_system_message_warns(self): + """system_instruction + initial system message warns and uses system_instruction.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + params = self.adapter.get_llm_invocation_params( + context, system_instruction="Be concise." + ) + mock_logger.warning.assert_called_once() + + self.assertEqual(params["system_instruction"], "Be concise.") + + def test_both_system_instruction_and_developer_message_no_warning(self): + """system_instruction + initial developer message: no warning, developer becomes user.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + params = self.adapter.get_llm_invocation_params( + context, system_instruction="Be concise." + ) + mock_logger.warning.assert_not_called() + + self.assertEqual(params["system_instruction"], "Be concise.") + + def test_non_initial_system_message_not_extracted(self): + """Non-initial system message is converted to user, not extracted as system instruction.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + {"role": "system", "content": "Late system message"}, + {"role": "user", "content": "How are you?"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + # No system instruction should be extracted from non-initial position + self.assertIsNone(params["system_instruction"]) + # The system message should have been converted to user role in the Gemini Content + # (we check that 3 messages are present, meaning no extraction happened) + self.assertEqual(len(params["messages"]), 3) + + def test_subsequent_developer_messages_converted_to_user(self): + """Subsequent developer messages are converted to user role.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + {"role": "developer", "content": "More instructions"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["messages"]), 2) + # Second message (developer) should be converted to user in Google format + self.assertEqual(params["messages"][1].role, "user") + class TestAnthropicGetLLMInvocationParams(unittest.TestCase): def setUp(self) -> None: @@ -737,6 +920,108 @@ class TestAnthropicGetLLMInvocationParams(unittest.TestCase): self.assertEqual(params["messages"][0]["role"], "user") self.assertEqual(params["messages"][0]["content"], "You are a helpful assistant.") + def test_system_instruction_only(self): + """system_instruction alone becomes the system parameter.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params( + context, enable_prompt_caching=False, system_instruction="Be helpful." + ) + + self.assertEqual(params["system"], "Be helpful.") + self.assertEqual(len(params["messages"]), 1) + self.assertEqual(params["messages"][0]["role"], "user") + + def test_initial_developer_message_promoted(self): + """Initial developer message without system_instruction is promoted to system.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context, enable_prompt_caching=False) + + self.assertEqual(params["system"], "Extra context.") + self.assertEqual(len(params["messages"]), 1) + + def test_both_system_instruction_and_system_message_warns(self): + """system_instruction + initial system message warns and uses system_instruction.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + params = self.adapter.get_llm_invocation_params( + context, + enable_prompt_caching=False, + system_instruction="Be concise.", + ) + mock_logger.warning.assert_called_once() + warning_msg = mock_logger.warning.call_args[0][0] + self.assertIn("Using system_instruction", warning_msg) + + self.assertEqual(params["system"], "Be concise.") + + def test_both_system_instruction_and_developer_message_no_warning(self): + """system_instruction + initial developer message: no warning, developer becomes user.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "assistant", "content": "Hi"}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + params = self.adapter.get_llm_invocation_params( + context, + enable_prompt_caching=False, + system_instruction="Be concise.", + ) + mock_logger.warning.assert_not_called() + + self.assertEqual(params["system"], "Be concise.") + # Developer message should have been converted to "user" + self.assertEqual(params["messages"][0]["role"], "user") + self.assertEqual(params["messages"][0]["content"], "Extra context.") + + def test_subsequent_developer_messages_converted_to_user(self): + """Subsequent developer messages are converted to user role.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi"}, + {"role": "developer", "content": "More instructions"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context, enable_prompt_caching=False) + + # Developer message was converted to "user" + self.assertEqual(params["messages"][2]["role"], "user") + self.assertEqual(params["messages"][2]["content"], "More instructions") + + def test_initial_system_discarded_when_system_instruction_provided(self): + """Initial system message is discarded when system_instruction is provided.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "Old instruction."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger"): + params = self.adapter.get_llm_invocation_params( + context, + enable_prompt_caching=False, + system_instruction="New instruction.", + ) + + self.assertEqual(params["system"], "New instruction.") + # Only the user message should remain + self.assertEqual(len(params["messages"]), 1) + self.assertEqual(params["messages"][0]["role"], "user") + class TestAWSBedrockGetLLMInvocationParams(unittest.TestCase): def setUp(self) -> None: @@ -996,6 +1281,72 @@ class TestAWSBedrockGetLLMInvocationParams(unittest.TestCase): params["messages"][0]["content"][0]["text"], "You are a helpful assistant." ) + def test_system_instruction_only(self): + """system_instruction alone becomes the system parameter.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context, system_instruction="Be helpful.") + + self.assertEqual(params["system"], [{"text": "Be helpful."}]) + + def test_initial_developer_message_promoted(self): + """Initial developer message without system_instruction is promoted.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["system"], [{"text": "Extra context."}]) + + def test_both_system_instruction_and_system_message_warns(self): + """system_instruction + initial system message warns and uses system_instruction.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + params = self.adapter.get_llm_invocation_params( + context, system_instruction="Be concise." + ) + mock_logger.warning.assert_called_once() + + self.assertEqual(params["system"], [{"text": "Be concise."}]) + + def test_both_system_instruction_and_developer_message_no_warning(self): + """system_instruction + initial developer message: no warning, developer becomes user.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + params = self.adapter.get_llm_invocation_params( + context, system_instruction="Be concise." + ) + mock_logger.warning.assert_not_called() + + self.assertEqual(params["system"], [{"text": "Be concise."}]) + self.assertEqual(params["messages"][0]["role"], "user") + + def test_subsequent_developer_messages_converted_to_user(self): + """Subsequent developer messages are converted to user role.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi"}, + {"role": "developer", "content": "More instructions"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["messages"][2]["role"], "user") + class TestPerplexityGetLLMInvocationParams(unittest.TestCase): def setUp(self) -> None: @@ -1214,5 +1565,139 @@ class TestPerplexityGetLLMInvocationParams(unittest.TestCase): self.assertEqual(params["messages"], []) +class TestBaseLLMAdapterHelpers(unittest.TestCase): + """Tests for the shared helper methods on BaseLLMAdapter.""" + + def setUp(self): + # Use OpenAILLMAdapter as a concrete implementation for testing the base helpers + self.adapter = OpenAILLMAdapter() + + def test_extract_system_message(self): + """System message is extracted from messages[0].""" + messages = [ + {"role": "system", "content": "Be helpful."}, + {"role": "user", "content": "Hello"}, + ] + content, role = self.adapter._extract_initial_system_or_developer( + messages, system_instruction=None + ) + + self.assertEqual(content, "Be helpful.") + self.assertEqual(role, "system") + self.assertEqual(len(messages), 1) # popped + + def test_extract_developer_without_system_instruction(self): + """Developer message is extracted when no system_instruction.""" + messages = [ + {"role": "developer", "content": "Context."}, + {"role": "user", "content": "Hello"}, + ] + content, role = self.adapter._extract_initial_system_or_developer( + messages, system_instruction=None + ) + + self.assertEqual(content, "Context.") + self.assertEqual(role, "developer") + self.assertEqual(len(messages), 1) + + def test_developer_with_system_instruction_converts_to_user(self): + """Developer message with system_instruction is converted to user, not extracted.""" + messages = [ + {"role": "developer", "content": "Context."}, + {"role": "user", "content": "Hello"}, + ] + content, role = self.adapter._extract_initial_system_or_developer( + messages, system_instruction="Be helpful." + ) + + self.assertIsNone(content) + self.assertIsNone(role) + self.assertEqual(len(messages), 2) # not popped + self.assertEqual(messages[0]["role"], "user") # converted to user + + def test_single_system_message_becomes_user(self): + """Single system message is converted to user instead of extracting (empty prevention).""" + messages = [ + {"role": "system", "content": "Be helpful."}, + ] + content, role = self.adapter._extract_initial_system_or_developer( + messages, system_instruction=None + ) + + self.assertIsNone(content) + self.assertIsNone(role) + self.assertEqual(len(messages), 1) # not popped + self.assertEqual(messages[0]["role"], "user") + + def test_non_system_message_ignored(self): + """Non-system/developer first message is ignored.""" + messages = [ + {"role": "user", "content": "Hello"}, + ] + content, role = self.adapter._extract_initial_system_or_developer( + messages, system_instruction=None + ) + + self.assertIsNone(content) + self.assertIsNone(role) + self.assertEqual(len(messages), 1) + + def test_empty_messages(self): + """Empty messages list returns None.""" + messages = [] + content, role = self.adapter._extract_initial_system_or_developer( + messages, system_instruction=None + ) + + self.assertIsNone(content) + self.assertIsNone(role) + + def test_resolve_both_system_discard(self): + """Resolve with discard=True: system_instruction wins, warns.""" + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + result = self.adapter._resolve_system_instruction( + "from context", "system", "from settings", discard_context_system=True + ) + mock_logger.warning.assert_called_once() + + self.assertEqual(result, "from settings") + + def test_resolve_both_system_keep(self): + """Resolve with discard=False: warns but returns system_instruction.""" + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + result = self.adapter._resolve_system_instruction( + "from context", "system", "from settings", discard_context_system=False + ) + mock_logger.warning.assert_called_once() + + self.assertEqual(result, "from settings") + + def test_resolve_only_system_instruction(self): + """Only system_instruction: returns it, no warning.""" + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + result = self.adapter._resolve_system_instruction( + None, None, "from settings", discard_context_system=True + ) + mock_logger.warning.assert_not_called() + + self.assertEqual(result, "from settings") + + def test_resolve_only_context_system_discard(self): + """Only context system (discard=True): returns it.""" + result = self.adapter._resolve_system_instruction( + "from context", "system", None, discard_context_system=True + ) + + self.assertEqual(result, "from context") + + def test_resolve_only_context_system_keep(self): + """Only context system (discard=False): returns None (already in messages).""" + result = self.adapter._resolve_system_instruction( + "from context", "system", None, discard_context_system=False + ) + + self.assertIsNone(result) + + if __name__ == "__main__": unittest.main() From 64ba013b68c5599f15141c9c4554d211875b110c Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Mar 2026 15:04:30 -0400 Subject: [PATCH 07/23] Move OpenAI Responses adapter tests into test_get_llm_invocation_params.py Consolidates all adapter get_llm_invocation_params tests in one file. Adds new tests for developer message handling in the Responses adapter. --- tests/test_get_llm_invocation_params.py | 373 ++++++++++++++++++++++++ tests/test_openai_responses_adapter.py | 361 ----------------------- 2 files changed, 373 insertions(+), 361 deletions(-) delete mode 100644 tests/test_openai_responses_adapter.py diff --git a/tests/test_get_llm_invocation_params.py b/tests/test_get_llm_invocation_params.py index d88d6193d..22e950db9 100644 --- a/tests/test_get_llm_invocation_params.py +++ b/tests/test_get_llm_invocation_params.py @@ -47,6 +47,16 @@ For AWS Bedrock adapter: 7. system_instruction overrides context system message, with conflict warnings 8. Developer messages are promoted to system instruction or converted to user +For OpenAI Responses adapter: +1. LLMContext messages are converted to Responses API input items +2. System and developer role messages are converted to developer role +3. Assistant tool_calls produce function_call input items +4. Tool messages produce function_call_output input items +5. Multimodal content conversion (text -> input_text, image_url -> input_image) +6. Tools schema flattening (nested function dict -> flat format) +7. system_instruction sets instructions (or becomes developer message if input is empty) +8. Developer messages pass through as developer role without triggering warnings + For BaseLLMAdapter helpers: 1. _extract_initial_system_or_developer: system/developer extraction and conversion logic 2. _resolve_system_instruction: conflict resolution between context and settings @@ -57,10 +67,13 @@ from unittest.mock import patch from google.genai.types import Content, Part +from pipecat.adapters.schemas.function_schema import FunctionSchema +from pipecat.adapters.schemas.tools_schema import ToolsSchema from pipecat.adapters.services.anthropic_adapter import AnthropicLLMAdapter from pipecat.adapters.services.bedrock_adapter import AWSBedrockLLMAdapter from pipecat.adapters.services.gemini_adapter import GeminiLLMAdapter from pipecat.adapters.services.open_ai_adapter import OpenAILLMAdapter +from pipecat.adapters.services.open_ai_responses_adapter import OpenAIResponsesLLMAdapter from pipecat.adapters.services.perplexity_adapter import PerplexityLLMAdapter from pipecat.processors.aggregators.llm_context import ( LLMContext, @@ -1565,6 +1578,366 @@ class TestPerplexityGetLLMInvocationParams(unittest.TestCase): self.assertEqual(params["messages"], []) +class TestOpenAIResponsesGetLLMInvocationParams(unittest.TestCase): + def setUp(self) -> None: + """Sets up a common adapter instance for all tests.""" + self.adapter = OpenAIResponsesLLMAdapter() + + def test_simple_user_assistant_messages(self): + """Simple user/assistant text messages are converted correctly.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there!"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["input"]), 2) + self.assertEqual(params["input"][0], {"role": "user", "content": "Hello"}) + self.assertEqual(params["input"][1], {"role": "assistant", "content": "Hi there!"}) + + def test_system_role_converted_to_developer(self): + """System role messages are converted to developer role.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["input"][0]["role"], "developer") + self.assertEqual(params["input"][0]["content"], "You are helpful.") + + def test_developer_role_kept_as_developer(self): + """Developer role messages are kept as developer role.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["input"][0]["role"], "developer") + self.assertEqual(params["input"][0]["content"], "Extra context.") + + def test_system_message_without_system_instruction_no_warning(self): + """System message without system_instruction does not trigger a warning.""" + adapter = OpenAIResponsesLLMAdapter() + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + adapter.get_llm_invocation_params(context) + mock_logger.warning.assert_not_called() + + def test_system_message_with_system_instruction_triggers_warning(self): + """System message + system_instruction triggers a conflict warning.""" + adapter = OpenAIResponsesLLMAdapter() + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + adapter.get_llm_invocation_params(context, system_instruction="Be concise.") + mock_logger.warning.assert_called_once() + warning_msg = mock_logger.warning.call_args[0][0] + self.assertIn("system_instruction", warning_msg) + + def test_developer_message_with_system_instruction_no_warning(self): + """Developer message + system_instruction does NOT trigger a warning.""" + adapter = OpenAIResponsesLLMAdapter() + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + params = adapter.get_llm_invocation_params(context, system_instruction="Be concise.") + mock_logger.warning.assert_not_called() + + # Developer message stays as developer, system_instruction becomes instructions + self.assertEqual(params["input"][0]["role"], "developer") + self.assertEqual(params["instructions"], "Be concise.") + + def test_non_initial_system_message_no_warning(self): + """Non-initial system messages are converted without a warning.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + {"role": "system", "content": "New instruction"}, + ] + context = LLMContext(messages=messages) + + adapter = OpenAIResponsesLLMAdapter() + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + params = adapter.get_llm_invocation_params(context, system_instruction="Be helpful.") + mock_logger.warning.assert_not_called() + + self.assertEqual(params["input"][1]["role"], "developer") + self.assertEqual(params["input"][1]["content"], "New instruction") + + def test_conflict_warning_fires_only_once(self): + """The conflict warning fires only once per adapter instance.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + adapter = OpenAIResponsesLLMAdapter() + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + adapter.get_llm_invocation_params(context, system_instruction="Be concise.") + adapter.get_llm_invocation_params(context, system_instruction="Be concise.") + mock_logger.warning.assert_called_once() + + def test_assistant_tool_calls_to_function_call(self): + """Assistant messages with tool_calls produce function_call input items.""" + messages = [ + { + "role": "assistant", + "tool_calls": [ + { + "id": "call_123", + "function": { + "name": "get_weather", + "arguments": '{"location": "SF"}', + }, + "type": "function", + } + ], + } + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["input"]), 1) + fc = params["input"][0] + self.assertEqual(fc["type"], "function_call") + self.assertEqual(fc["call_id"], "call_123") + self.assertEqual(fc["name"], "get_weather") + self.assertEqual(fc["arguments"], '{"location": "SF"}') + + def test_multiple_tool_calls(self): + """Multiple tool calls in one assistant message produce multiple function_call items.""" + messages = [ + { + "role": "assistant", + "tool_calls": [ + { + "id": "call_1", + "function": {"name": "get_weather", "arguments": '{"location": "SF"}'}, + "type": "function", + }, + { + "id": "call_2", + "function": { + "name": "get_restaurant", + "arguments": '{"location": "SF"}', + }, + "type": "function", + }, + ], + } + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["input"]), 2) + self.assertEqual(params["input"][0]["name"], "get_weather") + self.assertEqual(params["input"][1]["name"], "get_restaurant") + + def test_tool_message_to_function_call_output(self): + """Tool role messages produce function_call_output input items.""" + messages = [ + { + "role": "tool", + "content": '{"temperature": "72"}', + "tool_call_id": "call_123", + } + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["input"]), 1) + fco = params["input"][0] + self.assertEqual(fco["type"], "function_call_output") + self.assertEqual(fco["call_id"], "call_123") + self.assertEqual(fco["output"], '{"temperature": "72"}') + + def test_mixed_conversation(self): + """Mixed conversation with text + function calls converts correctly.""" + messages = [ + {"role": "user", "content": "What's the weather in SF?"}, + { + "role": "assistant", + "tool_calls": [ + { + "id": "call_abc", + "function": {"name": "get_weather", "arguments": '{"location": "SF"}'}, + "type": "function", + } + ], + }, + { + "role": "tool", + "content": '{"temp": "72"}', + "tool_call_id": "call_abc", + }, + {"role": "assistant", "content": "It's 72 degrees in SF."}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["input"]), 4) + self.assertEqual(params["input"][0]["role"], "user") + self.assertEqual(params["input"][1]["type"], "function_call") + self.assertEqual(params["input"][2]["type"], "function_call_output") + self.assertEqual(params["input"][3]["role"], "assistant") + + def test_multimodal_text_conversion(self): + """Multimodal text content parts are converted to input_text.""" + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "What's in this image?"}, + ], + } + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + content = params["input"][0]["content"] + self.assertEqual(len(content), 1) + self.assertEqual(content[0]["type"], "input_text") + self.assertEqual(content[0]["text"], "What's in this image?") + + def test_multimodal_image_conversion(self): + """Multimodal image_url content parts are converted to input_image.""" + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "Describe this:"}, + { + "type": "image_url", + "image_url": {"url": "data:image/jpeg;base64,abc123"}, + }, + ], + } + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + content = params["input"][0]["content"] + self.assertEqual(len(content), 2) + self.assertEqual(content[0]["type"], "input_text") + self.assertEqual(content[1]["type"], "input_image") + self.assertEqual(content[1]["image_url"], "data:image/jpeg;base64,abc123") + self.assertEqual(content[1]["detail"], "auto") + + def test_multimodal_image_with_detail(self): + """Image content parts preserve the detail setting when provided.""" + messages = [ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": {"url": "https://example.com/img.png", "detail": "high"}, + }, + ], + } + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + content = params["input"][0]["content"] + self.assertEqual(content[0]["detail"], "high") + + def test_tools_schema_flattening(self): + """Tools schema with nested function dict is flattened to Responses API format.""" + weather_fn = FunctionSchema( + name="get_weather", + description="Get the current weather", + properties={ + "location": {"type": "string", "description": "The city"}, + }, + required=["location"], + ) + tools = ToolsSchema(standard_tools=[weather_fn]) + context = LLMContext(tools=tools) + params = self.adapter.get_llm_invocation_params(context) + + tool_list = params["tools"] + self.assertEqual(len(tool_list), 1) + tool = tool_list[0] + self.assertEqual(tool["type"], "function") + self.assertEqual(tool["name"], "get_weather") + self.assertEqual(tool["description"], "Get the current weather") + self.assertIn("properties", tool["parameters"]) + + def test_empty_messages(self): + """Empty messages list produces empty input list.""" + context = LLMContext(messages=[]) + params = self.adapter.get_llm_invocation_params(context) + self.assertEqual(params["input"], []) + + def test_llm_specific_message_passthrough(self): + """LLMSpecificMessage with llm='openai_responses' passes through.""" + specific_msg = self.adapter.create_llm_specific_message( + {"type": "function_call", "call_id": "x", "name": "foo", "arguments": "{}"} + ) + messages = [ + {"role": "user", "content": "Hello"}, + specific_msg, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["input"]), 2) + self.assertEqual(params["input"][0]["role"], "user") + self.assertEqual(params["input"][1]["type"], "function_call") + + def test_id_for_llm_specific_messages(self): + """Adapter identifier is 'openai_responses'.""" + self.assertEqual(self.adapter.id_for_llm_specific_messages, "openai_responses") + + def test_system_instruction_with_messages_sets_instructions(self): + """When system_instruction is provided and input is non-empty, sets instructions.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context, system_instruction="Be helpful.") + + self.assertEqual(params["instructions"], "Be helpful.") + self.assertEqual(len(params["input"]), 1) + self.assertEqual(params["input"][0]["role"], "user") + + def test_system_instruction_with_empty_input_becomes_developer_message(self): + """When system_instruction is provided but input is empty, it becomes a developer message.""" + context = LLMContext(messages=[]) + params = self.adapter.get_llm_invocation_params(context, system_instruction="Be helpful.") + + self.assertNotIn("instructions", params) + self.assertEqual(len(params["input"]), 1) + self.assertEqual(params["input"][0]["role"], "developer") + self.assertEqual(params["input"][0]["content"], "Be helpful.") + + def test_no_system_instruction_omits_instructions(self): + """When no system_instruction is provided, instructions key is absent.""" + context = LLMContext(messages=[{"role": "user", "content": "Hi"}]) + params = self.adapter.get_llm_invocation_params(context) + + self.assertNotIn("instructions", params) + + class TestBaseLLMAdapterHelpers(unittest.TestCase): """Tests for the shared helper methods on BaseLLMAdapter.""" diff --git a/tests/test_openai_responses_adapter.py b/tests/test_openai_responses_adapter.py deleted file mode 100644 index 9b0237baf..000000000 --- a/tests/test_openai_responses_adapter.py +++ /dev/null @@ -1,361 +0,0 @@ -# -# Copyright (c) 2024-2026, Daily -# -# SPDX-License-Identifier: BSD 2-Clause License -# - -"""Unit tests for the OpenAI Responses API adapter. - -Tests the conversion from LLMContext messages to Responses API input items, including: - -1. Simple user/assistant text messages pass through (with correct role) -2. System role converted to developer role -3. First-message system role triggers a warning -4. Assistant messages with tool_calls produce function_call input items -5. Tool messages produce function_call_output input items -6. Mixed conversations with text + function calls convert correctly -7. Multimodal content conversion (text -> input_text, image_url -> input_image) -8. Tools schema flattening (nested function dict -> flat format) -9. Empty messages list -10. LLMSpecificMessage with llm="openai_responses" passes through -""" - -import unittest -from unittest.mock import patch - -from pipecat.adapters.schemas.function_schema import FunctionSchema -from pipecat.adapters.schemas.tools_schema import ToolsSchema -from pipecat.adapters.services.open_ai_responses_adapter import OpenAIResponsesLLMAdapter -from pipecat.processors.aggregators.llm_context import LLMContext, LLMStandardMessage - - -class TestOpenAIResponsesAdapter(unittest.TestCase): - def setUp(self): - self.adapter = OpenAIResponsesLLMAdapter() - - def test_simple_user_assistant_messages(self): - """Simple user/assistant text messages are converted correctly.""" - messages: list[LLMStandardMessage] = [ - {"role": "user", "content": "Hello"}, - {"role": "assistant", "content": "Hi there!"}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) - - self.assertEqual(len(params["input"]), 2) - self.assertEqual(params["input"][0], {"role": "user", "content": "Hello"}) - self.assertEqual(params["input"][1], {"role": "assistant", "content": "Hi there!"}) - - def test_system_role_converted_to_developer(self): - """System role messages are converted to developer role.""" - messages: list[LLMStandardMessage] = [ - {"role": "system", "content": "You are helpful."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) - - self.assertEqual(params["input"][0]["role"], "developer") - self.assertEqual(params["input"][0]["content"], "You are helpful.") - - def test_system_message_without_system_instruction_no_warning(self): - """System message without system_instruction does not trigger a warning.""" - adapter = OpenAIResponsesLLMAdapter() - messages: list[LLMStandardMessage] = [ - {"role": "system", "content": "You are helpful."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - - with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: - adapter.get_llm_invocation_params(context) - mock_logger.warning.assert_not_called() - - def test_system_message_with_system_instruction_triggers_warning(self): - """System message + system_instruction triggers a conflict warning.""" - adapter = OpenAIResponsesLLMAdapter() - messages: list[LLMStandardMessage] = [ - {"role": "system", "content": "You are helpful."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - - with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: - adapter.get_llm_invocation_params(context, system_instruction="Be concise.") - mock_logger.warning.assert_called_once() - warning_msg = mock_logger.warning.call_args[0][0] - self.assertIn("system_instruction", warning_msg) - - def test_non_initial_system_message_no_warning(self): - """Non-initial system messages are converted without a warning.""" - messages: list[LLMStandardMessage] = [ - {"role": "user", "content": "Hello"}, - {"role": "system", "content": "New instruction"}, - ] - context = LLMContext(messages=messages) - - adapter = OpenAIResponsesLLMAdapter() - with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: - params = adapter.get_llm_invocation_params(context, system_instruction="Be helpful.") - mock_logger.warning.assert_not_called() - - self.assertEqual(params["input"][1]["role"], "developer") - self.assertEqual(params["input"][1]["content"], "New instruction") - - def test_conflict_warning_fires_only_once(self): - """The conflict warning fires only once per adapter instance.""" - messages: list[LLMStandardMessage] = [ - {"role": "system", "content": "You are helpful."}, - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - - adapter = OpenAIResponsesLLMAdapter() - with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: - adapter.get_llm_invocation_params(context, system_instruction="Be concise.") - adapter.get_llm_invocation_params(context, system_instruction="Be concise.") - # Warning should have been emitted exactly once, not twice - mock_logger.warning.assert_called_once() - - def test_assistant_tool_calls_to_function_call(self): - """Assistant messages with tool_calls produce function_call input items.""" - messages = [ - { - "role": "assistant", - "tool_calls": [ - { - "id": "call_123", - "function": { - "name": "get_weather", - "arguments": '{"location": "SF"}', - }, - "type": "function", - } - ], - } - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) - - self.assertEqual(len(params["input"]), 1) - fc = params["input"][0] - self.assertEqual(fc["type"], "function_call") - self.assertEqual(fc["call_id"], "call_123") - self.assertEqual(fc["name"], "get_weather") - self.assertEqual(fc["arguments"], '{"location": "SF"}') - - def test_multiple_tool_calls(self): - """Multiple tool calls in one assistant message produce multiple function_call items.""" - messages = [ - { - "role": "assistant", - "tool_calls": [ - { - "id": "call_1", - "function": {"name": "get_weather", "arguments": '{"location": "SF"}'}, - "type": "function", - }, - { - "id": "call_2", - "function": {"name": "get_restaurant", "arguments": '{"location": "SF"}'}, - "type": "function", - }, - ], - } - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) - - self.assertEqual(len(params["input"]), 2) - self.assertEqual(params["input"][0]["name"], "get_weather") - self.assertEqual(params["input"][1]["name"], "get_restaurant") - - def test_tool_message_to_function_call_output(self): - """Tool role messages produce function_call_output input items.""" - messages = [ - { - "role": "tool", - "content": '{"temperature": "72"}', - "tool_call_id": "call_123", - } - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) - - self.assertEqual(len(params["input"]), 1) - fco = params["input"][0] - self.assertEqual(fco["type"], "function_call_output") - self.assertEqual(fco["call_id"], "call_123") - self.assertEqual(fco["output"], '{"temperature": "72"}') - - def test_mixed_conversation(self): - """Mixed conversation with text + function calls converts correctly.""" - messages = [ - {"role": "user", "content": "What's the weather in SF?"}, - { - "role": "assistant", - "tool_calls": [ - { - "id": "call_abc", - "function": {"name": "get_weather", "arguments": '{"location": "SF"}'}, - "type": "function", - } - ], - }, - { - "role": "tool", - "content": '{"temp": "72"}', - "tool_call_id": "call_abc", - }, - {"role": "assistant", "content": "It's 72 degrees in SF."}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) - - self.assertEqual(len(params["input"]), 4) - self.assertEqual(params["input"][0]["role"], "user") - self.assertEqual(params["input"][1]["type"], "function_call") - self.assertEqual(params["input"][2]["type"], "function_call_output") - self.assertEqual(params["input"][3]["role"], "assistant") - - def test_multimodal_text_conversion(self): - """Multimodal text content parts are converted to input_text.""" - messages = [ - { - "role": "user", - "content": [ - {"type": "text", "text": "What's in this image?"}, - ], - } - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) - - content = params["input"][0]["content"] - self.assertEqual(len(content), 1) - self.assertEqual(content[0]["type"], "input_text") - self.assertEqual(content[0]["text"], "What's in this image?") - - def test_multimodal_image_conversion(self): - """Multimodal image_url content parts are converted to input_image.""" - messages = [ - { - "role": "user", - "content": [ - {"type": "text", "text": "Describe this:"}, - { - "type": "image_url", - "image_url": {"url": "data:image/jpeg;base64,abc123"}, - }, - ], - } - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) - - content = params["input"][0]["content"] - self.assertEqual(len(content), 2) - self.assertEqual(content[0]["type"], "input_text") - self.assertEqual(content[1]["type"], "input_image") - self.assertEqual(content[1]["image_url"], "data:image/jpeg;base64,abc123") - self.assertEqual(content[1]["detail"], "auto") - - def test_multimodal_image_with_detail(self): - """Image content parts preserve the detail setting when provided.""" - messages = [ - { - "role": "user", - "content": [ - { - "type": "image_url", - "image_url": {"url": "https://example.com/img.png", "detail": "high"}, - }, - ], - } - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) - - content = params["input"][0]["content"] - self.assertEqual(content[0]["detail"], "high") - - def test_tools_schema_flattening(self): - """Tools schema with nested function dict is flattened to Responses API format.""" - weather_fn = FunctionSchema( - name="get_weather", - description="Get the current weather", - properties={ - "location": {"type": "string", "description": "The city"}, - }, - required=["location"], - ) - tools = ToolsSchema(standard_tools=[weather_fn]) - context = LLMContext(tools=tools) - params = self.adapter.get_llm_invocation_params(context) - - tool_list = params["tools"] - self.assertEqual(len(tool_list), 1) - tool = tool_list[0] - self.assertEqual(tool["type"], "function") - self.assertEqual(tool["name"], "get_weather") - self.assertEqual(tool["description"], "Get the current weather") - self.assertIn("properties", tool["parameters"]) - - def test_empty_messages(self): - """Empty messages list produces empty input list.""" - context = LLMContext(messages=[]) - params = self.adapter.get_llm_invocation_params(context) - self.assertEqual(params["input"], []) - - def test_llm_specific_message_passthrough(self): - """LLMSpecificMessage with llm='openai_responses' passes through.""" - specific_msg = self.adapter.create_llm_specific_message( - {"type": "function_call", "call_id": "x", "name": "foo", "arguments": "{}"} - ) - messages = [ - {"role": "user", "content": "Hello"}, - specific_msg, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) - - self.assertEqual(len(params["input"]), 2) - self.assertEqual(params["input"][0]["role"], "user") - self.assertEqual(params["input"][1]["type"], "function_call") - - def test_id_for_llm_specific_messages(self): - """Adapter identifier is 'openai_responses'.""" - self.assertEqual(self.adapter.id_for_llm_specific_messages, "openai_responses") - - def test_system_instruction_with_messages_sets_instructions(self): - """When system_instruction is provided and input is non-empty, sets instructions.""" - messages: list[LLMStandardMessage] = [ - {"role": "user", "content": "Hello"}, - ] - context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context, system_instruction="Be helpful.") - - self.assertEqual(params["instructions"], "Be helpful.") - self.assertEqual(len(params["input"]), 1) - self.assertEqual(params["input"][0]["role"], "user") - - def test_system_instruction_with_empty_input_becomes_developer_message(self): - """When system_instruction is provided but input is empty, it becomes a developer message.""" - context = LLMContext(messages=[]) - params = self.adapter.get_llm_invocation_params(context, system_instruction="Be helpful.") - - self.assertNotIn("instructions", params) - self.assertEqual(len(params["input"]), 1) - self.assertEqual(params["input"][0]["role"], "developer") - self.assertEqual(params["input"][0]["content"], "Be helpful.") - - def test_no_system_instruction_omits_instructions(self): - """When no system_instruction is provided, instructions key is absent.""" - context = LLMContext(messages=[{"role": "user", "content": "Hi"}]) - params = self.adapter.get_llm_invocation_params(context) - - self.assertNotIn("instructions", params) - - -if __name__ == "__main__": - unittest.main() From a0393b9af6df3e600d26d0669f1fe28208015c86 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Mar 2026 15:25:39 -0400 Subject: [PATCH 08/23] Fix: warn on system_instruction conflict even with single system message When the only message in context was a system message, _extract_initial_system_or_developer would convert it to "user" (to prevent empty history) without warning about the conflict with system_instruction. Now warns inline before converting, with a message explaining both the conflict and the user-role conversion. --- src/pipecat/adapters/base_llm_adapter.py | 9 +++++++++ tests/test_get_llm_invocation_params.py | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/pipecat/adapters/base_llm_adapter.py b/src/pipecat/adapters/base_llm_adapter.py index aad8664e7..6242d373e 100644 --- a/src/pipecat/adapters/base_llm_adapter.py +++ b/src/pipecat/adapters/base_llm_adapter.py @@ -188,6 +188,15 @@ class BaseLLMAdapter(ABC, Generic[TLLMInvocationParams]): # Would extracting empty the list? Convert to "user" instead. if len(messages) == 1: + if role == "system" and system_instruction: + if not self._warned_system_instruction: + self._warned_system_instruction = True + logger.warning( + "Both system_instruction and a system message in context are set." + " Using system_instruction. The system message in context is being" + " converted to a user message to avoid sending an empty conversation" + " history." + ) messages[0]["role"] = "user" return None, None diff --git a/tests/test_get_llm_invocation_params.py b/tests/test_get_llm_invocation_params.py index 22e950db9..b695cde59 100644 --- a/tests/test_get_llm_invocation_params.py +++ b/tests/test_get_llm_invocation_params.py @@ -2002,6 +2002,23 @@ class TestBaseLLMAdapterHelpers(unittest.TestCase): self.assertEqual(len(messages), 1) # not popped self.assertEqual(messages[0]["role"], "user") + def test_single_system_message_with_system_instruction_warns(self): + """Single system message + system_instruction still warns even though content isn't extracted.""" + messages = [ + {"role": "system", "content": "Be helpful."}, + ] + + adapter = OpenAILLMAdapter() + with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: + content, role = adapter._extract_initial_system_or_developer( + messages, system_instruction="Be concise." + ) + mock_logger.warning.assert_called_once() + + self.assertIsNone(content) + self.assertIsNone(role) + self.assertEqual(messages[0]["role"], "user") + def test_non_system_message_ignored(self): """Non-system/developer first message is ignored.""" messages = [ From 2135557689751a98010ceed17b0f4f17e24162c2 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Mar 2026 15:58:34 -0400 Subject: [PATCH 09/23] Simplify: don't promote developer messages to system instruction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Developer messages are now always converted to "user" in non-OpenAI adapters, never promoted to the system instruction. This removes an inconsistency where adding an unrelated message to context would change whether a developer message got promoted. Simplifications: - Rename _extract_initial_system_or_developer → _extract_initial_system - Return Optional[str] instead of Tuple (role is always "system") - Drop initial_context_message_role from _resolve_system_instruction - Drop system_role fields from all ConvertedMessages dataclasses --- src/pipecat/adapters/base_llm_adapter.py | 90 +++++--------- .../adapters/services/anthropic_adapter.py | 14 +-- .../adapters/services/bedrock_adapter.py | 14 +-- .../adapters/services/gemini_adapter.py | 8 +- .../adapters/services/open_ai_adapter.py | 8 +- .../services/open_ai_responses_adapter.py | 1 - tests/test_get_llm_invocation_params.py | 112 ++++++++---------- 7 files changed, 98 insertions(+), 149 deletions(-) diff --git a/src/pipecat/adapters/base_llm_adapter.py b/src/pipecat/adapters/base_llm_adapter.py index 6242d373e..aeef313b0 100644 --- a/src/pipecat/adapters/base_llm_adapter.py +++ b/src/pipecat/adapters/base_llm_adapter.py @@ -11,7 +11,7 @@ adapters that handle tool format conversion and standardization. """ from abc import ABC, abstractmethod -from typing import Any, Dict, Generic, List, Optional, Tuple, TypeVar +from typing import Any, Dict, Generic, List, Optional, TypeVar from loguru import logger @@ -135,60 +135,46 @@ class BaseLLMAdapter(ABC, Generic[TLLMInvocationParams]): # Fallback to return the same tools in case they are not in a standard format return tools - def _extract_initial_system_or_developer( + def _extract_initial_system( self, messages: list, *, - system_instruction: Optional[str], - ) -> Tuple[Optional[str], Optional[str]]: - """Extract an initial system/developer message for use as a system instruction. + system_instruction: Optional[str] = None, + ) -> Optional[str]: + """Extract an initial ``"system"`` message for use as a system instruction. Only useful for services that expect the system instruction as a separate parameter, not inline in conversation history (today, all - non-OpenAI services). + non-OpenAI services). Does not extract ``"developer"`` messages — + those are converted to ``"user"`` by the adapter's subsequent message + loop, like any other non-system role the provider doesn't support. - Checks ``messages[0]``. Behavior: - - - ``"system"`` role: assumed to be intended as the system instruction. - Extract (pop from messages). - - ``"developer"`` role **without** ``system_instruction``: also assumed - to be intended as the system instruction. Extract (pop). - - ``"developer"`` role **with** ``system_instruction``: assumed to be - intended as a conversation-history message (since a system instruction - is already provided). Don't extract; convert to ``"user"`` in-place. - - Any other role: no-op. - - If extracting would leave the messages list empty - (``len(messages) == 1``), the message is converted to ``"user"`` role - instead of being extracted. This prevents sending an empty conversation - history to providers that require at least one non-system message. + Checks ``messages[0]``. If the role is ``"system"``, pops and returns + its content. If extracting would leave the messages list empty + (``len(messages) == 1``), the message is converted to ``"user"`` + role instead of being extracted, to prevent sending an empty + conversation history to providers that require at least one + non-system message. Args: messages: Message list in standard format (mutated in-place). system_instruction: The system instruction from service settings - or ``run_inference``, used to decide whether to extract a - ``"developer"`` message. + or ``run_inference``. Only used to decide whether to warn + about a conflict in the single-message case. Returns: - ``(extracted_content, original_role)`` where *original_role* is - ``"system"`` or ``"developer"``, or ``(None, None)`` if nothing + The extracted system message content, or ``None`` if nothing was extracted. """ if not messages: - return None, None + return None - role = messages[0].get("role") - if role not in ("system", "developer"): - return None, None - - # "developer" + system_instruction present → keep in messages as "user" - if role == "developer" and system_instruction: - messages[0]["role"] = "user" - return None, None + if messages[0].get("role") != "system": + return None # Would extracting empty the list? Convert to "user" instead. if len(messages) == 1: - if role == "system" and system_instruction: + if system_instruction: if not self._warned_system_instruction: self._warned_system_instruction = True logger.warning( @@ -198,7 +184,7 @@ class BaseLLMAdapter(ABC, Generic[TLLMInvocationParams]): " history." ) messages[0]["role"] = "user" - return None, None + return None # Extract content = messages[0].get("content", "") @@ -208,28 +194,21 @@ class BaseLLMAdapter(ABC, Generic[TLLMInvocationParams]): part.get("text", "") for part in content if part.get("type") == "text" ) messages.pop(0) - return content, role + return content def _resolve_system_instruction( self, - initial_context_message: Optional[str], - initial_context_message_role: Optional[str], + system_from_context: Optional[str], system_instruction: Optional[str], *, discard_context_system: bool, ) -> Optional[str]: - """Resolve conflict between ``system_instruction`` and an initial context message. - - Only warns when *initial_context_message_role* is ``"system"`` (not - ``"developer"``), since a developer message coexisting with - ``system_instruction`` is expected and handled elsewhere. + """Resolve conflict between ``system_instruction`` and an extracted context system message. Args: - initial_context_message: Content extracted from ``messages[0]`` - by :meth:`_extract_initial_system_or_developer`, or detected + system_from_context: Content extracted from an initial ``"system"`` + message by :meth:`_extract_initial_system`, or detected inline (OpenAI adapters). - initial_context_message_role: ``"system"`` or ``"developer"`` — - the original role before extraction/detection. system_instruction: From service settings or ``run_inference`` param. discard_context_system: If ``True`` (non-OpenAI adapters), the context system message is discarded when ``system_instruction`` @@ -239,10 +218,7 @@ class BaseLLMAdapter(ABC, Generic[TLLMInvocationParams]): The effective system instruction to use, or ``None`` if the system instruction is already represented in the messages (OpenAI path). """ - both_present = initial_context_message and system_instruction - from_system_role = initial_context_message_role == "system" - - if both_present and from_system_role: + if system_from_context and system_instruction: if not self._warned_system_instruction: self._warned_system_instruction = True if discard_context_system: @@ -257,15 +233,11 @@ class BaseLLMAdapter(ABC, Generic[TLLMInvocationParams]): ) if system_instruction: - if discard_context_system: - return system_instruction - else: - # OpenAI path: caller prepends; return the instruction for prepending - return system_instruction + return system_instruction - if initial_context_message: + if system_from_context: if discard_context_system: - return initial_context_message + return system_from_context else: # Content is already in messages; nothing to prepend return None diff --git a/src/pipecat/adapters/services/anthropic_adapter.py b/src/pipecat/adapters/services/anthropic_adapter.py index dfd68375c..9617dadeb 100644 --- a/src/pipecat/adapters/services/anthropic_adapter.py +++ b/src/pipecat/adapters/services/anthropic_adapter.py @@ -69,7 +69,6 @@ class AnthropicLLMAdapter(BaseLLMAdapter[AnthropicLLMInvocationParams]): ) system = self._resolve_system_instruction( converted.system if converted.system is not NOT_GIVEN else None, - converted.system_role, system_instruction, discard_context_system=True, ) @@ -118,7 +117,6 @@ class AnthropicLLMAdapter(BaseLLMAdapter[AnthropicLLMInvocationParams]): messages: List[MessageParam] system: str | NotGiven - system_role: Optional[str] = None # "system" or "developer" — origin of extracted system def _from_universal_context_messages( self, @@ -127,18 +125,16 @@ class AnthropicLLMAdapter(BaseLLMAdapter[AnthropicLLMInvocationParams]): system_instruction: Optional[str] = None, ) -> ConvertedMessages: system = NOT_GIVEN - system_role = None - # Extract initial system/developer from universal messages BEFORE conversion, + # Extract initial system message from universal messages BEFORE conversion, # so the helper works with standard message format (not provider-specific). remaining = list(universal_context_messages) if remaining and not isinstance(remaining[0], LLMSpecificMessage): - extracted_content, extracted_role = self._extract_initial_system_or_developer( + extracted = self._extract_initial_system( remaining, system_instruction=system_instruction ) - if extracted_content is not None: - system = extracted_content - system_role = extracted_role + if extracted is not None: + system = extracted # Convert remaining messages to Anthropic format messages = [] @@ -180,7 +176,7 @@ class AnthropicLLMAdapter(BaseLLMAdapter[AnthropicLLMInvocationParams]): elif isinstance(message["content"], list) and len(message["content"]) == 0: message["content"] = [{"type": "text", "text": "(empty)"}] - return self.ConvertedMessages(messages=messages, system=system, system_role=system_role) + return self.ConvertedMessages(messages=messages, system=system) def _from_universal_context_message(self, message: LLMContextMessage) -> MessageParam: if isinstance(message, LLMSpecificMessage): diff --git a/src/pipecat/adapters/services/bedrock_adapter.py b/src/pipecat/adapters/services/bedrock_adapter.py index 7df924880..3150e6458 100644 --- a/src/pipecat/adapters/services/bedrock_adapter.py +++ b/src/pipecat/adapters/services/bedrock_adapter.py @@ -65,7 +65,6 @@ class AWSBedrockLLMAdapter(BaseLLMAdapter[AWSBedrockLLMInvocationParams]): ) effective_system = self._resolve_system_instruction( converted.system, - converted.system_role, system_instruction, discard_context_system=True, ) @@ -112,7 +111,6 @@ class AWSBedrockLLMAdapter(BaseLLMAdapter[AWSBedrockLLMInvocationParams]): messages: List[dict[str, Any]] system: Optional[str] - system_role: Optional[str] = None # "system" or "developer" — origin of extracted system def _from_universal_context_messages( self, @@ -121,18 +119,12 @@ class AWSBedrockLLMAdapter(BaseLLMAdapter[AWSBedrockLLMInvocationParams]): system_instruction: Optional[str] = None, ) -> ConvertedMessages: system = None - system_role = None - # Extract initial system/developer from universal messages BEFORE conversion, + # Extract initial system message from universal messages BEFORE conversion, # so the helper works with standard message format (not provider-specific). remaining = list(universal_context_messages) if remaining and not isinstance(remaining[0], LLMSpecificMessage): - extracted_content, extracted_role = self._extract_initial_system_or_developer( - remaining, system_instruction=system_instruction - ) - if extracted_content is not None: - system = extracted_content - system_role = extracted_role + system = self._extract_initial_system(remaining, system_instruction=system_instruction) # Convert remaining messages to Bedrock format messages = [] @@ -174,7 +166,7 @@ class AWSBedrockLLMAdapter(BaseLLMAdapter[AWSBedrockLLMInvocationParams]): elif isinstance(message["content"], list) and len(message["content"]) == 0: message["content"] = [{"type": "text", "text": "(empty)"}] - return self.ConvertedMessages(messages=messages, system=system, system_role=system_role) + return self.ConvertedMessages(messages=messages, system=system) def _from_universal_context_message(self, message: LLMContextMessage) -> dict[str, Any]: if isinstance(message, LLMSpecificMessage): diff --git a/src/pipecat/adapters/services/gemini_adapter.py b/src/pipecat/adapters/services/gemini_adapter.py index d73ac388d..565d0d0b8 100644 --- a/src/pipecat/adapters/services/gemini_adapter.py +++ b/src/pipecat/adapters/services/gemini_adapter.py @@ -71,7 +71,6 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]): ) effective_system = self._resolve_system_instruction( converted.system_instruction, - converted.system_instruction_role, system_instruction, discard_context_system=True, ) @@ -176,7 +175,6 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]): messages: List[Content] system_instruction: Optional[str] = None - system_instruction_role: Optional[str] = None # "system" or "developer" @dataclass class MessageConversionResult: @@ -220,12 +218,11 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]): # We work on a mutable copy so we can pop messages[0] if needed. remaining_messages = list(universal_context_messages) extracted_system = None - extracted_role = None - # Extract initial system/developer from universal messages BEFORE conversion, + # Extract initial system message from universal messages BEFORE conversion, # so the helper works with standard message format. if remaining_messages and not isinstance(remaining_messages[0], LLMSpecificMessage): - extracted_system, extracted_role = self._extract_initial_system_or_developer( + extracted_system = self._extract_initial_system( remaining_messages, system_instruction=system_instruction ) @@ -294,7 +291,6 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]): return self.ConvertedMessages( messages=messages, system_instruction=extracted_system, - system_instruction_role=extracted_role, ) def _from_standard_message( diff --git a/src/pipecat/adapters/services/open_ai_adapter.py b/src/pipecat/adapters/services/open_ai_adapter.py index 9e8b449d0..f544f2c29 100644 --- a/src/pipecat/adapters/services/open_ai_adapter.py +++ b/src/pipecat/adapters/services/open_ai_adapter.py @@ -68,11 +68,13 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]): if system_instruction: # Detect initial system message for warning purposes (don't extract) - initial_role = messages[0].get("role") if messages else None - initial_content = messages[0].get("content", "") if initial_role == "system" else None + initial_content = ( + messages[0].get("content", "") + if messages and messages[0].get("role") == "system" + else None + ) self._resolve_system_instruction( initial_content, - initial_role if initial_role == "system" else None, system_instruction, discard_context_system=False, ) diff --git a/src/pipecat/adapters/services/open_ai_responses_adapter.py b/src/pipecat/adapters/services/open_ai_responses_adapter.py index 89968a721..57398a9f6 100644 --- a/src/pipecat/adapters/services/open_ai_responses_adapter.py +++ b/src/pipecat/adapters/services/open_ai_responses_adapter.py @@ -68,7 +68,6 @@ class OpenAIResponsesLLMAdapter(BaseLLMAdapter[OpenAIResponsesLLMInvocationParam if first_msg and first_msg.get("role") == "system": self._resolve_system_instruction( first_msg.get("content", ""), - "system", system_instruction, discard_context_system=False, ) diff --git a/tests/test_get_llm_invocation_params.py b/tests/test_get_llm_invocation_params.py index b695cde59..524d409c5 100644 --- a/tests/test_get_llm_invocation_params.py +++ b/tests/test_get_llm_invocation_params.py @@ -25,7 +25,7 @@ For Gemini adapter: 5. Single system instruction is converted to user message when no other messages exist 6. Multiple system instructions: first extracted, later ones converted to user messages 7. system_instruction overrides context system message, with conflict warnings -8. Developer messages are promoted to system instruction or converted to user +8. Developer messages are converted to user For Anthropic adapter: 1. LLMStandardMessage objects are converted to Anthropic MessageParam format @@ -35,7 +35,7 @@ For Anthropic adapter: 5. Consecutive messages with same role are merged into multi-content-block messages 6. Empty text content is converted to "(empty)" 7. system_instruction overrides context system message, with conflict warnings -8. Developer messages are promoted to system instruction or converted to user +8. Developer messages are converted to user For AWS Bedrock adapter: 1. LLMStandardMessage objects are converted to AWS Bedrock format @@ -45,7 +45,7 @@ For AWS Bedrock adapter: 5. Consecutive messages with same role are merged into multi-content-block messages 6. Empty text content is converted to "(empty)" 7. system_instruction overrides context system message, with conflict warnings -8. Developer messages are promoted to system instruction or converted to user +8. Developer messages are converted to user For OpenAI Responses adapter: 1. LLMContext messages are converted to Responses API input items @@ -58,7 +58,7 @@ For OpenAI Responses adapter: 8. Developer messages pass through as developer role without triggering warnings For BaseLLMAdapter helpers: -1. _extract_initial_system_or_developer: system/developer extraction and conversion logic +1. _extract_initial_system: system extraction and conversion logic 2. _resolve_system_instruction: conflict resolution between context and settings """ @@ -602,8 +602,8 @@ class TestGeminiGetLLMInvocationParams(unittest.TestCase): self.assertEqual(params["system_instruction"], "You are helpful.") self.assertEqual(len(params["messages"]), 1) - def test_initial_developer_message_promoted(self): - """Initial developer message without system_instruction is promoted.""" + def test_initial_developer_message_becomes_user(self): + """Initial developer message without system_instruction becomes user, not system_instruction.""" messages: list[LLMStandardMessage] = [ {"role": "developer", "content": "Extra context."}, {"role": "user", "content": "Hello"}, @@ -611,7 +611,10 @@ class TestGeminiGetLLMInvocationParams(unittest.TestCase): context = LLMContext(messages=messages) params = self.adapter.get_llm_invocation_params(context) - self.assertEqual(params["system_instruction"], "Extra context.") + self.assertIsNone(params["system_instruction"]) + self.assertEqual(len(params["messages"]), 2) + self.assertEqual(params["messages"][0].role, "user") + self.assertEqual(params["messages"][0].parts[0].text, "Extra context.") def test_both_system_instruction_and_system_message_warns(self): """system_instruction + initial system message warns and uses system_instruction.""" @@ -947,17 +950,22 @@ class TestAnthropicGetLLMInvocationParams(unittest.TestCase): self.assertEqual(len(params["messages"]), 1) self.assertEqual(params["messages"][0]["role"], "user") - def test_initial_developer_message_promoted(self): - """Initial developer message without system_instruction is promoted to system.""" + def test_initial_developer_message_becomes_user(self): + """Initial developer message without system_instruction becomes user, not system.""" + from anthropic import NOT_GIVEN + messages: list[LLMStandardMessage] = [ {"role": "developer", "content": "Extra context."}, + {"role": "assistant", "content": "OK"}, {"role": "user", "content": "Hello"}, ] context = LLMContext(messages=messages) params = self.adapter.get_llm_invocation_params(context, enable_prompt_caching=False) - self.assertEqual(params["system"], "Extra context.") - self.assertEqual(len(params["messages"]), 1) + self.assertEqual(params["system"], NOT_GIVEN) + self.assertEqual(len(params["messages"]), 3) + self.assertEqual(params["messages"][0]["role"], "user") + self.assertEqual(params["messages"][0]["content"], "Extra context.") def test_both_system_instruction_and_system_message_warns(self): """system_instruction + initial system message warns and uses system_instruction.""" @@ -1304,16 +1312,20 @@ class TestAWSBedrockGetLLMInvocationParams(unittest.TestCase): self.assertEqual(params["system"], [{"text": "Be helpful."}]) - def test_initial_developer_message_promoted(self): - """Initial developer message without system_instruction is promoted.""" + def test_initial_developer_message_becomes_user(self): + """Initial developer message without system_instruction becomes user, not system.""" messages: list[LLMStandardMessage] = [ {"role": "developer", "content": "Extra context."}, + {"role": "assistant", "content": "OK"}, {"role": "user", "content": "Hello"}, ] context = LLMContext(messages=messages) params = self.adapter.get_llm_invocation_params(context) - self.assertEqual(params["system"], [{"text": "Extra context."}]) + self.assertIsNone(params["system"]) + self.assertEqual(len(params["messages"]), 3) + self.assertEqual(params["messages"][0]["role"], "user") + self.assertEqual(params["messages"][0]["content"][0]["text"], "Extra context.") def test_both_system_instruction_and_system_message_warns(self): """system_instruction + initial system message warns and uses system_instruction.""" @@ -1951,54 +1963,43 @@ class TestBaseLLMAdapterHelpers(unittest.TestCase): {"role": "system", "content": "Be helpful."}, {"role": "user", "content": "Hello"}, ] - content, role = self.adapter._extract_initial_system_or_developer( - messages, system_instruction=None - ) + content = self.adapter._extract_initial_system(messages, system_instruction=None) self.assertEqual(content, "Be helpful.") - self.assertEqual(role, "system") self.assertEqual(len(messages), 1) # popped - def test_extract_developer_without_system_instruction(self): - """Developer message is extracted when no system_instruction.""" + def test_extract_developer_not_extracted(self): + """Developer message is not extracted by _extract_initial_system.""" messages = [ {"role": "developer", "content": "Context."}, {"role": "user", "content": "Hello"}, ] - content, role = self.adapter._extract_initial_system_or_developer( - messages, system_instruction=None - ) - - self.assertEqual(content, "Context.") - self.assertEqual(role, "developer") - self.assertEqual(len(messages), 1) - - def test_developer_with_system_instruction_converts_to_user(self): - """Developer message with system_instruction is converted to user, not extracted.""" - messages = [ - {"role": "developer", "content": "Context."}, - {"role": "user", "content": "Hello"}, - ] - content, role = self.adapter._extract_initial_system_or_developer( - messages, system_instruction="Be helpful." - ) + content = self.adapter._extract_initial_system(messages, system_instruction=None) self.assertIsNone(content) - self.assertIsNone(role) self.assertEqual(len(messages), 2) # not popped - self.assertEqual(messages[0]["role"], "user") # converted to user + self.assertEqual(messages[0]["role"], "developer") # unchanged + + def test_developer_with_system_instruction_not_extracted(self): + """Developer message with system_instruction is not handled by _extract_initial_system.""" + messages = [ + {"role": "developer", "content": "Context."}, + {"role": "user", "content": "Hello"}, + ] + content = self.adapter._extract_initial_system(messages, system_instruction="Be helpful.") + + self.assertIsNone(content) + self.assertEqual(len(messages), 2) # not popped + self.assertEqual(messages[0]["role"], "developer") # unchanged by helper def test_single_system_message_becomes_user(self): """Single system message is converted to user instead of extracting (empty prevention).""" messages = [ {"role": "system", "content": "Be helpful."}, ] - content, role = self.adapter._extract_initial_system_or_developer( - messages, system_instruction=None - ) + content = self.adapter._extract_initial_system(messages, system_instruction=None) self.assertIsNone(content) - self.assertIsNone(role) self.assertEqual(len(messages), 1) # not popped self.assertEqual(messages[0]["role"], "user") @@ -2010,13 +2011,10 @@ class TestBaseLLMAdapterHelpers(unittest.TestCase): adapter = OpenAILLMAdapter() with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: - content, role = adapter._extract_initial_system_or_developer( - messages, system_instruction="Be concise." - ) + content = adapter._extract_initial_system(messages, system_instruction="Be concise.") mock_logger.warning.assert_called_once() self.assertIsNone(content) - self.assertIsNone(role) self.assertEqual(messages[0]["role"], "user") def test_non_system_message_ignored(self): @@ -2024,29 +2022,23 @@ class TestBaseLLMAdapterHelpers(unittest.TestCase): messages = [ {"role": "user", "content": "Hello"}, ] - content, role = self.adapter._extract_initial_system_or_developer( - messages, system_instruction=None - ) + content = self.adapter._extract_initial_system(messages, system_instruction=None) self.assertIsNone(content) - self.assertIsNone(role) self.assertEqual(len(messages), 1) def test_empty_messages(self): """Empty messages list returns None.""" messages = [] - content, role = self.adapter._extract_initial_system_or_developer( - messages, system_instruction=None - ) + content = self.adapter._extract_initial_system(messages, system_instruction=None) self.assertIsNone(content) - self.assertIsNone(role) def test_resolve_both_system_discard(self): """Resolve with discard=True: system_instruction wins, warns.""" with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: result = self.adapter._resolve_system_instruction( - "from context", "system", "from settings", discard_context_system=True + "from context", "from settings", discard_context_system=True ) mock_logger.warning.assert_called_once() @@ -2056,7 +2048,7 @@ class TestBaseLLMAdapterHelpers(unittest.TestCase): """Resolve with discard=False: warns but returns system_instruction.""" with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: result = self.adapter._resolve_system_instruction( - "from context", "system", "from settings", discard_context_system=False + "from context", "from settings", discard_context_system=False ) mock_logger.warning.assert_called_once() @@ -2066,7 +2058,7 @@ class TestBaseLLMAdapterHelpers(unittest.TestCase): """Only system_instruction: returns it, no warning.""" with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: result = self.adapter._resolve_system_instruction( - None, None, "from settings", discard_context_system=True + None, "from settings", discard_context_system=True ) mock_logger.warning.assert_not_called() @@ -2075,7 +2067,7 @@ class TestBaseLLMAdapterHelpers(unittest.TestCase): def test_resolve_only_context_system_discard(self): """Only context system (discard=True): returns it.""" result = self.adapter._resolve_system_instruction( - "from context", "system", None, discard_context_system=True + "from context", None, discard_context_system=True ) self.assertEqual(result, "from context") @@ -2083,7 +2075,7 @@ class TestBaseLLMAdapterHelpers(unittest.TestCase): def test_resolve_only_context_system_keep(self): """Only context system (discard=False): returns None (already in messages).""" result = self.adapter._resolve_system_instruction( - "from context", "system", None, discard_context_system=False + "from context", None, discard_context_system=False ) self.assertIsNone(result) From e0bc9c73c643d20619a1de005b9732caa1ff7271 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Mar 2026 16:00:56 -0400 Subject: [PATCH 10/23] Add Anthropic interruptible example (07e) and register in release evals --- .../07e-interruptible-anthropic.py | 123 ++++++++++++++++++ scripts/evals/run-release-evals.py | 1 + 2 files changed, 124 insertions(+) create mode 100644 examples/foundational/07e-interruptible-anthropic.py diff --git a/examples/foundational/07e-interruptible-anthropic.py b/examples/foundational/07e-interruptible-anthropic.py new file mode 100644 index 000000000..8b4428999 --- /dev/null +++ b/examples/foundational/07e-interruptible-anthropic.py @@ -0,0 +1,123 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import os + +from dotenv import load_dotenv +from loguru import logger + +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.frames.frames import LLMRunFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) +from pipecat.runner.types import RunnerArguments +from pipecat.runner.utils import create_transport +from pipecat.services.anthropic.llm import AnthropicLLMService +from pipecat.services.cartesia.tts import CartesiaTTSService +from pipecat.services.deepgram.stt import DeepgramSTTService +from pipecat.transports.base_transport import BaseTransport, TransportParams +from pipecat.transports.daily.transport import DailyParams +from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams + +load_dotenv(override=True) + +# We use lambdas to defer transport parameter creation until the transport +# type is selected at runtime. +transport_params = { + "daily": lambda: DailyParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "twilio": lambda: FastAPIWebsocketParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "webrtc": lambda: TransportParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), +} + + +async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): + logger.info(f"Starting bot") + + stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY")) + + tts = CartesiaTTSService( + api_key=os.getenv("CARTESIA_API_KEY"), + settings=CartesiaTTSService.Settings( + voice="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady + ), + ) + + llm = AnthropicLLMService( + api_key=os.getenv("ANTHROPIC_API_KEY"), + settings=AnthropicLLMService.Settings( + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", + ), + ) + + context = LLMContext() + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) + + pipeline = Pipeline( + [ + transport.input(), # Transport user input + stt, + user_aggregator, # User responses + llm, # LLM + tts, # TTS + transport.output(), # Transport bot output + assistant_aggregator, # Assistant spoken responses + ] + ) + + task = PipelineTask( + pipeline, + params=PipelineParams( + enable_metrics=True, + enable_usage_metrics=True, + ), + idle_timeout_secs=runner_args.pipeline_idle_timeout_secs, + ) + + @transport.event_handler("on_client_connected") + async def on_client_connected(transport, client): + logger.info(f"Client connected") + # Kick off the conversation. + context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + await task.queue_frames([LLMRunFrame()]) + + @transport.event_handler("on_client_disconnected") + async def on_client_disconnected(transport, client): + logger.info(f"Client disconnected") + await task.cancel() + + runner = PipelineRunner(handle_sigint=runner_args.handle_sigint) + + await runner.run(task) + + +async def bot(runner_args: RunnerArguments): + """Main bot entry point compatible with Pipecat Cloud.""" + transport = await create_transport(runner_args, transport_params) + await run_bot(transport, runner_args) + + +if __name__ == "__main__": + from pipecat.runner.run import main + + main() diff --git a/scripts/evals/run-release-evals.py b/scripts/evals/run-release-evals.py index de4e7c1b7..4d113844e 100644 --- a/scripts/evals/run-release-evals.py +++ b/scripts/evals/run-release-evals.py @@ -108,6 +108,7 @@ TESTS_07 = [ ("07c-interruptible-deepgram-http.py", EVAL_SIMPLE_MATH), ("07d-interruptible-elevenlabs.py", EVAL_SIMPLE_MATH), ("07d-interruptible-elevenlabs-http.py", EVAL_SIMPLE_MATH), + ("07e-interruptible-anthropic.py", EVAL_SIMPLE_MATH), ("07f-interruptible-azure.py", EVAL_SIMPLE_MATH), ("07f-interruptible-azure-http.py", EVAL_SIMPLE_MATH), ("07g-interruptible-openai.py", EVAL_SIMPLE_MATH), From 2bb36b5b66755d8578041617b14ee19420267acb Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Mar 2026 16:06:39 -0400 Subject: [PATCH 11/23] Update changelog for developer message simplification --- changelog/4089.added.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/4089.added.md b/changelog/4089.added.md index 5b31081a2..b1ec724f5 100644 --- a/changelog/4089.added.md +++ b/changelog/4089.added.md @@ -1 +1 @@ -- Added support for "developer" role messages in conversation context across all LLM adapters. For non-OpenAI services (Anthropic, Google, AWS Bedrock), an initial "developer" message is promoted to the system instruction when no `system_instruction` is configured; otherwise it is converted to a "user" message. For OpenAI services, "developer" messages pass through in conversation history. For the Responses API, they are kept as "developer" role (matching the existing "system" → "developer" conversion). +- Added support for "developer" role messages in conversation context across all LLM adapters. For non-OpenAI services (Anthropic, Google, AWS Bedrock), "developer" messages are converted to "user" messages (use `system_instruction` to set the system instruction). For OpenAI services, "developer" messages pass through in conversation history. For the Responses API, they are kept as "developer" role (matching the existing "system" → "developer" conversion). From d779a5b4ea6cdfed8291eb8950aa37d31ec0f0a6 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Mar 2026 17:21:46 -0400 Subject: [PATCH 12/23] Use "developer" role for programmatic conversation-kickoff messages These messages are developer instructions to the assistant (e.g. "Please introduce yourself to the user"), not simulated user input. The "developer" role is semantically correct for this purpose. --- examples/foundational/02-llm-say-one-thing.py | 2 +- examples/foundational/04-transports-small-webrtc.py | 4 +++- examples/foundational/04a-transports-daily.py | 2 +- examples/foundational/06-listen-and-respond.py | 4 +++- examples/foundational/07-interruptible-cartesia-http.py | 2 +- examples/foundational/07-interruptible.py | 4 +++- examples/foundational/07a-interruptible-speechmatics-vad.py | 2 +- examples/foundational/07a-interruptible-speechmatics.py | 2 +- examples/foundational/07c-interruptible-deepgram-flux.py | 4 +++- examples/foundational/07c-interruptible-deepgram-http.py | 2 +- .../foundational/07c-interruptible-deepgram-sagemaker.py | 4 +++- examples/foundational/07c-interruptible-deepgram-vad.py | 4 +++- examples/foundational/07c-interruptible-deepgram.py | 4 +++- examples/foundational/07d-interruptible-elevenlabs-http.py | 2 +- examples/foundational/07d-interruptible-elevenlabs.py | 4 +++- examples/foundational/07e-interruptible-anthropic.py | 5 ++++- examples/foundational/07f-interruptible-azure-http.py | 4 +++- examples/foundational/07f-interruptible-azure.py | 4 +++- examples/foundational/07g-interruptible-openai-http.py | 4 +++- examples/foundational/07g-interruptible-openai.py | 4 +++- examples/foundational/07h-interruptible-openpipe.py | 4 +++- examples/foundational/07i-interruptible-xtts.py | 2 +- examples/foundational/07j-interruptible-gladia-vad.py | 4 +++- examples/foundational/07j-interruptible-gladia.py | 4 +++- examples/foundational/07k-interruptible-lmnt.py | 4 +++- examples/foundational/07l-interruptible-groq.py | 4 +++- examples/foundational/07m-interruptible-aws.py | 6 ++++-- examples/foundational/07n-interruptible-gemini-image.py | 4 +++- examples/foundational/07n-interruptible-google-http.py | 4 +++- examples/foundational/07n-interruptible-google.py | 4 +++- .../07o-interruptible-assemblyai-turn-detection.py | 4 +++- examples/foundational/07o-interruptible-assemblyai.py | 4 +++- examples/foundational/07p-interruptible-krisp-viva.py | 4 +++- examples/foundational/07p-interruptible-krisp.py | 4 +++- examples/foundational/07q-interruptible-rime-http.py | 2 +- examples/foundational/07q-interruptible-rime.py | 4 +++- examples/foundational/07r-interruptible-nvidia.py | 4 +++- examples/foundational/07s-interruptible-google-audio-in.py | 4 +++- examples/foundational/07t-interruptible-fish.py | 4 +++- examples/foundational/07v-interruptible-neuphonic-http.py | 2 +- examples/foundational/07v-interruptible-neuphonic.py | 4 +++- examples/foundational/07w-interruptible-fal.py | 2 +- examples/foundational/07x-interruptible-local.py | 2 +- examples/foundational/07y-interruptible-minimax.py | 2 +- examples/foundational/07z-interruptible-sarvam-http.py | 2 +- examples/foundational/07z-interruptible-sarvam.py | 4 +++- examples/foundational/07za-interruptible-soniox.py | 4 +++- examples/foundational/07zb-interruptible-inworld-http.py | 2 +- examples/foundational/07zb-interruptible-inworld.py | 4 +++- examples/foundational/07zc-interruptible-asyncai-http.py | 2 +- examples/foundational/07zc-interruptible-asyncai.py | 4 +++- examples/foundational/07zd-interruptible-aicoustics.py | 4 +++- examples/foundational/07ze-interruptible-hume.py | 4 +++- examples/foundational/07zf-interruptible-gradium.py | 4 +++- examples/foundational/07zg-interruptible-camb.py | 4 +++- examples/foundational/07zi-interruptible-piper.py | 4 +++- examples/foundational/07zj-interruptible-kokoro.py | 4 +++- examples/foundational/07zk-interruptible-resemble.py | 4 +++- examples/foundational/08-custom-frame-processor.py | 4 +++- examples/foundational/10-wake-phrase.py | 4 +++- examples/foundational/14a-function-calling-anthropic.py | 4 +++- examples/foundational/14i-function-calling-fireworks.py | 4 +++- examples/foundational/14n-function-calling-perplexity.py | 4 +++- examples/foundational/14r-function-calling-aws.py | 4 +++- examples/foundational/16-gpu-container-local-bot.py | 4 +++- examples/foundational/17-detect-user-idle.py | 4 +++- examples/foundational/19-openai-realtime-beta.py | 2 +- examples/foundational/19-openai-realtime.py | 2 +- examples/foundational/19a-azure-realtime-beta.py | 2 +- examples/foundational/19a-azure-realtime.py | 2 +- examples/foundational/19b-openai-realtime-beta-text.py | 2 +- examples/foundational/19b-openai-realtime-text.py | 2 +- examples/foundational/19c-openai-realtime-live-video.py | 2 +- .../foundational/20b-persistent-context-openai-realtime.py | 2 +- .../foundational/20e-persistent-context-aws-nova-sonic.py | 4 +++- .../foundational/20f-persistent-context-grok-realtime.py | 2 +- examples/foundational/23-bot-background-sound.py | 4 +++- examples/foundational/26b-gemini-live-function-calling.py | 4 +++- .../foundational/26h-gemini-live-vertex-function-calling.py | 2 +- examples/foundational/26i-gemini-live-graceful-end.py | 2 +- examples/foundational/28-user-assistant-turns.py | 4 +++- examples/foundational/29-turn-tracking-observer.py | 4 +++- examples/foundational/30-observer.py | 4 +++- examples/foundational/33-gemini-rag.py | 4 +++- examples/foundational/38-smart-turn-fal.py | 4 +++- examples/foundational/38a-smart-turn-local-coreml.py | 4 +++- examples/foundational/38b-smart-turn-local.py | 4 +++- .../foundational/39b-mcp-streamable-http-gemini-live.py | 2 +- examples/foundational/40-aws-nova-sonic.py | 4 +++- examples/foundational/42-interruption-config.py | 4 +++- examples/foundational/45-before-and-after-events.py | 4 +++- examples/foundational/47-sentry-metrics.py | 4 +++- examples/foundational/48-service-switcher.py | 4 +++- examples/foundational/49a-thinking-anthropic.py | 2 +- examples/foundational/49b-thinking-google.py | 2 +- examples/foundational/49c-thinking-functions-anthropic.py | 2 +- examples/foundational/49d-thinking-functions-google.py | 2 +- examples/foundational/51-grok-realtime.py | 2 +- examples/foundational/53-concurrent-llm-evaluation.py | 4 ++-- .../foundational/53-concurrent-llm-rtvi-ignored-sources.py | 2 +- examples/foundational/54-context-summarization-openai.py | 4 +++- examples/foundational/54a-context-summarization-google.py | 4 +++- .../foundational/54b-context-summarization-manual-openai.py | 4 +++- .../foundational/54c-context-summarization-dedicated-llm.py | 4 +++- .../foundational/55a-update-settings-deepgram-flux-stt.py | 4 +++- .../55a-update-settings-deepgram-sagemaker-stt.py | 4 +++- examples/foundational/55a-update-settings-deepgram-stt.py | 4 +++- examples/foundational/55b-update-settings-azure-stt.py | 4 +++- examples/foundational/55c-update-settings-google-stt.py | 4 +++- examples/foundational/55d-update-settings-assemblyai-stt.py | 4 +++- examples/foundational/55e-update-settings-gladia-stt.py | 4 +++- .../55f-update-settings-elevenlabs-realtime-stt.py | 4 +++- examples/foundational/55g-update-settings-elevenlabs-stt.py | 2 +- .../foundational/55h-update-settings-speechmatics-stt.py | 4 +++- .../foundational/55i-update-settings-whisper-api-stt.py | 4 +++- examples/foundational/55j-update-settings-sarvam-stt.py | 4 +++- examples/foundational/55k-update-settings-soniox-stt.py | 4 +++- .../foundational/55l-update-settings-aws-transcribe-stt.py | 4 +++- examples/foundational/55m-update-settings-cartesia-stt.py | 4 +++- .../foundational/55n-update-settings-cartesia-http-tts.py | 4 +++- examples/foundational/55n-update-settings-cartesia-tts.py | 4 +++- .../foundational/55o-update-settings-elevenlabs-http-tts.py | 2 +- examples/foundational/55o-update-settings-elevenlabs-tts.py | 4 +++- examples/foundational/55p-update-settings-openai-tts.py | 4 +++- .../foundational/55q-update-settings-deepgram-http-tts.py | 2 +- .../55q-update-settings-deepgram-sagemaker-tts.py | 4 +++- examples/foundational/55q-update-settings-deepgram-tts.py | 4 +++- examples/foundational/55r-update-settings-azure-http-tts.py | 4 +++- examples/foundational/55r-update-settings-azure-tts.py | 4 +++- .../foundational/55s-update-settings-google-http-tts.py | 4 +++- .../foundational/55s-update-settings-google-stream-tts.py | 4 +++- examples/foundational/55t-update-settings-piper-http-tts.py | 2 +- examples/foundational/55t-update-settings-piper-tts.py | 4 +++- examples/foundational/55u-update-settings-rime-http-tts.py | 2 +- examples/foundational/55u-update-settings-rime-tts.py | 4 +++- examples/foundational/55v-update-settings-lmnt-tts.py | 4 +++- examples/foundational/55w-update-settings-fish-tts.py | 4 +++- examples/foundational/55x-update-settings-minimax-tts.py | 2 +- examples/foundational/55y-update-settings-groq-tts.py | 4 +++- examples/foundational/55z-update-settings-hume-tts.py | 4 +++- .../foundational/55za-update-settings-neuphonic-http-tts.py | 2 +- examples/foundational/55za-update-settings-neuphonic-tts.py | 4 +++- .../foundational/55zb-update-settings-inworld-http-tts.py | 2 +- examples/foundational/55zb-update-settings-inworld-tts.py | 4 +++- examples/foundational/55zc-update-settings-gemini-tts.py | 4 +++- examples/foundational/55zd-update-settings-aws-polly-tts.py | 4 +++- .../foundational/55ze-update-settings-sarvam-http-tts.py | 2 +- examples/foundational/55ze-update-settings-sarvam-tts.py | 4 +++- examples/foundational/55zf-update-settings-camb-tts.py | 4 +++- examples/foundational/55zg-update-settings-kokoro-tts.py | 4 +++- .../foundational/55zh-update-settings-resembleai-tts.py | 4 +++- examples/foundational/55zi-update-settings-azure-llm.py | 4 +++- examples/foundational/55zi-update-settings-openai-llm.py | 4 +++- .../55zi-update-settings-openai-responses-llm.py | 4 +++- examples/foundational/55zj-update-settings-anthropic-llm.py | 4 +++- examples/foundational/55zk-update-settings-google-llm.py | 4 +++- .../foundational/55zk-update-settings-google-vertex-llm.py | 4 +++- .../foundational/55zm-update-settings-gemini-live-vertex.py | 4 +++- examples/foundational/55zm-update-settings-gemini-live.py | 4 +++- .../foundational/55zp-update-settings-aws-bedrock-llm.py | 4 +++- examples/foundational/55zq-update-settings-fal-stt.py | 4 +++- examples/foundational/55zr-update-settings-gradium-stt.py | 4 +++- .../foundational/55zs-update-settings-whisper-mlx-stt.py | 4 +++- examples/foundational/55zs-update-settings-whisper-stt.py | 4 +++- .../55zt-update-settings-nvidia-segmented-stt.py | 4 +++- examples/foundational/55zt-update-settings-nvidia-stt.py | 4 +++- .../55zu-update-settings-openai-realtime-stt.py | 4 +++- .../foundational/55zv-update-settings-asyncai-http-tts.py | 2 +- examples/foundational/55zv-update-settings-asyncai-tts.py | 4 +++- examples/foundational/55zw-update-settings-gradium-tts.py | 4 +++- examples/foundational/55zx-update-settings-cerebras-llm.py | 4 +++- examples/foundational/55zy-update-settings-deepseek-llm.py | 4 +++- examples/foundational/55zz-update-settings-fireworks-llm.py | 4 +++- examples/foundational/55zza-update-settings-grok-llm.py | 4 +++- examples/foundational/55zzb-update-settings-groq-llm.py | 4 +++- examples/foundational/55zzc-update-settings-mistral-llm.py | 4 +++- examples/foundational/55zzd-update-settings-nvidia-llm.py | 4 +++- examples/foundational/55zze-update-settings-ollama-llm.py | 4 +++- .../foundational/55zzf-update-settings-openrouter-llm.py | 4 +++- examples/foundational/55zzh-update-settings-qwen-llm.py | 4 +++- .../foundational/55zzi-update-settings-sambanova-llm.py | 4 +++- examples/foundational/55zzj-update-settings-together-llm.py | 4 +++- .../55zzk-update-settings-aws-nova-sonic-llm.py | 2 +- examples/foundational/55zzl-update-settings-nvidia-tts.py | 4 +++- .../foundational/55zzm-update-settings-speechmatics-tts.py | 2 +- examples/foundational/55zzn-update-settings-groq-stt.py | 4 +++- examples/foundational/55zzo-update-settings-openpipe-llm.py | 4 +++- examples/foundational/55zzp-update-settings-xtts-tts.py | 2 +- examples/quickstart/bot.py | 2 +- 189 files changed, 472 insertions(+), 191 deletions(-) diff --git a/examples/foundational/02-llm-say-one-thing.py b/examples/foundational/02-llm-say-one-thing.py index d699277ce..74809ed2a 100644 --- a/examples/foundational/02-llm-say-one-thing.py +++ b/examples/foundational/02-llm-say-one-thing.py @@ -60,7 +60,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): context = LLMContext() - context.add_message({"role": "user", "content": "Say hello to the world."}) + context.add_message({"role": "developer", "content": "Say hello to the world."}) await task.queue_frames([LLMContextFrame(context), EndFrame()]) runner = PipelineRunner(handle_sigint=runner_args.handle_sigint) diff --git a/examples/foundational/04-transports-small-webrtc.py b/examples/foundational/04-transports-small-webrtc.py index cc768a751..286b1143a 100644 --- a/examples/foundational/04-transports-small-webrtc.py +++ b/examples/foundational/04-transports-small-webrtc.py @@ -109,7 +109,9 @@ async def run_example(webrtc_connection: SmallWebRTCConnection): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/04a-transports-daily.py b/examples/foundational/04a-transports-daily.py index 83bc27aca..c6695e2f8 100644 --- a/examples/foundational/04a-transports-daily.py +++ b/examples/foundational/04a-transports-daily.py @@ -92,7 +92,7 @@ async def main(): await transport.capture_participant_transcription(participant["id"]) # Kick off the conversation. context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/06-listen-and-respond.py b/examples/foundational/06-listen-and-respond.py index 769975b9a..ec382681d 100644 --- a/examples/foundational/06-listen-and-respond.py +++ b/examples/foundational/06-listen-and-respond.py @@ -129,7 +129,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07-interruptible-cartesia-http.py b/examples/foundational/07-interruptible-cartesia-http.py index 6b3daecf0..15fe51142 100644 --- a/examples/foundational/07-interruptible-cartesia-http.py +++ b/examples/foundational/07-interruptible-cartesia-http.py @@ -103,7 +103,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.info(f"Client connected") # Kick off the conversation. context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/07-interruptible.py b/examples/foundational/07-interruptible.py index c0b410f50..057429589 100644 --- a/examples/foundational/07-interruptible.py +++ b/examples/foundational/07-interruptible.py @@ -98,7 +98,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07a-interruptible-speechmatics-vad.py b/examples/foundational/07a-interruptible-speechmatics-vad.py index 327701ff2..bcceb4dea 100644 --- a/examples/foundational/07a-interruptible-speechmatics-vad.py +++ b/examples/foundational/07a-interruptible-speechmatics-vad.py @@ -148,7 +148,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Say a short hello to the user."}) + context.add_message({"role": "developer", "content": "Say a short hello to the user."}) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07a-interruptible-speechmatics.py b/examples/foundational/07a-interruptible-speechmatics.py index 5181bf36f..d619715aa 100644 --- a/examples/foundational/07a-interruptible-speechmatics.py +++ b/examples/foundational/07a-interruptible-speechmatics.py @@ -128,7 +128,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Say a short hello to the user."}) + context.add_message({"role": "developer", "content": "Say a short hello to the user."}) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07c-interruptible-deepgram-flux.py b/examples/foundational/07c-interruptible-deepgram-flux.py index 2c4e63bb7..76a5bb783 100644 --- a/examples/foundational/07c-interruptible-deepgram-flux.py +++ b/examples/foundational/07c-interruptible-deepgram-flux.py @@ -109,7 +109,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07c-interruptible-deepgram-http.py b/examples/foundational/07c-interruptible-deepgram-http.py index cb1026d7f..c81b2cd2f 100644 --- a/examples/foundational/07c-interruptible-deepgram-http.py +++ b/examples/foundational/07c-interruptible-deepgram-http.py @@ -104,7 +104,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.info(f"Client connected") # Kick off the conversation. context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/07c-interruptible-deepgram-sagemaker.py b/examples/foundational/07c-interruptible-deepgram-sagemaker.py index b3b53b3db..e823f18f9 100644 --- a/examples/foundational/07c-interruptible-deepgram-sagemaker.py +++ b/examples/foundational/07c-interruptible-deepgram-sagemaker.py @@ -114,7 +114,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07c-interruptible-deepgram-vad.py b/examples/foundational/07c-interruptible-deepgram-vad.py index 420ec795b..bbdb01efe 100644 --- a/examples/foundational/07c-interruptible-deepgram-vad.py +++ b/examples/foundational/07c-interruptible-deepgram-vad.py @@ -106,7 +106,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07c-interruptible-deepgram.py b/examples/foundational/07c-interruptible-deepgram.py index a4d94f915..bebde3fe3 100644 --- a/examples/foundational/07c-interruptible-deepgram.py +++ b/examples/foundational/07c-interruptible-deepgram.py @@ -100,7 +100,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07d-interruptible-elevenlabs-http.py b/examples/foundational/07d-interruptible-elevenlabs-http.py index a83df1465..b9e35ec4f 100644 --- a/examples/foundational/07d-interruptible-elevenlabs-http.py +++ b/examples/foundational/07d-interruptible-elevenlabs-http.py @@ -108,7 +108,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.info(f"Client connected") # Kick off the conversation. context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/07d-interruptible-elevenlabs.py b/examples/foundational/07d-interruptible-elevenlabs.py index ad1873788..ebdfdde72 100644 --- a/examples/foundational/07d-interruptible-elevenlabs.py +++ b/examples/foundational/07d-interruptible-elevenlabs.py @@ -100,7 +100,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07e-interruptible-anthropic.py b/examples/foundational/07e-interruptible-anthropic.py index 8b4428999..01aae9c98 100644 --- a/examples/foundational/07e-interruptible-anthropic.py +++ b/examples/foundational/07e-interruptible-anthropic.py @@ -98,7 +98,10 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message({"role": "system", "content": "You're a pirate."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07f-interruptible-azure-http.py b/examples/foundational/07f-interruptible-azure-http.py index c372c7c7e..6d8206910 100644 --- a/examples/foundational/07f-interruptible-azure-http.py +++ b/examples/foundational/07f-interruptible-azure-http.py @@ -102,7 +102,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07f-interruptible-azure.py b/examples/foundational/07f-interruptible-azure.py index 6a0b39bb5..a7977b04b 100644 --- a/examples/foundational/07f-interruptible-azure.py +++ b/examples/foundational/07f-interruptible-azure.py @@ -102,7 +102,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07g-interruptible-openai-http.py b/examples/foundational/07g-interruptible-openai-http.py index 127b9b0c0..19f1a3e8a 100644 --- a/examples/foundational/07g-interruptible-openai-http.py +++ b/examples/foundational/07g-interruptible-openai-http.py @@ -106,7 +106,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07g-interruptible-openai.py b/examples/foundational/07g-interruptible-openai.py index 9b8fbea09..b749ce27a 100644 --- a/examples/foundational/07g-interruptible-openai.py +++ b/examples/foundational/07g-interruptible-openai.py @@ -108,7 +108,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07h-interruptible-openpipe.py b/examples/foundational/07h-interruptible-openpipe.py index 8d31125f2..e904b8f55 100644 --- a/examples/foundational/07h-interruptible-openpipe.py +++ b/examples/foundational/07h-interruptible-openpipe.py @@ -103,7 +103,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07i-interruptible-xtts.py b/examples/foundational/07i-interruptible-xtts.py index 0f51ff03e..f0bfe01c0 100644 --- a/examples/foundational/07i-interruptible-xtts.py +++ b/examples/foundational/07i-interruptible-xtts.py @@ -104,7 +104,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.info(f"Client connected") # Kick off the conversation. context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/07j-interruptible-gladia-vad.py b/examples/foundational/07j-interruptible-gladia-vad.py index ec3cc80e1..f29930c3e 100644 --- a/examples/foundational/07j-interruptible-gladia-vad.py +++ b/examples/foundational/07j-interruptible-gladia-vad.py @@ -114,7 +114,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07j-interruptible-gladia.py b/examples/foundational/07j-interruptible-gladia.py index 5ab2e16a3..3f417bf5a 100644 --- a/examples/foundational/07j-interruptible-gladia.py +++ b/examples/foundational/07j-interruptible-gladia.py @@ -109,7 +109,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07k-interruptible-lmnt.py b/examples/foundational/07k-interruptible-lmnt.py index c6f931413..41e42a961 100644 --- a/examples/foundational/07k-interruptible-lmnt.py +++ b/examples/foundational/07k-interruptible-lmnt.py @@ -99,7 +99,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07l-interruptible-groq.py b/examples/foundational/07l-interruptible-groq.py index 59e1a7fca..7ffa8b06b 100644 --- a/examples/foundational/07l-interruptible-groq.py +++ b/examples/foundational/07l-interruptible-groq.py @@ -95,7 +95,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07m-interruptible-aws.py b/examples/foundational/07m-interruptible-aws.py index ca44fb448..ae9f59943 100644 --- a/examples/foundational/07m-interruptible-aws.py +++ b/examples/foundational/07m-interruptible-aws.py @@ -66,7 +66,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): settings=AWSBedrockLLMService.Settings( model="us.anthropic.claude-sonnet-4-6", temperature=0.8, - system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", + # system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) @@ -101,7 +101,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07n-interruptible-gemini-image.py b/examples/foundational/07n-interruptible-gemini-image.py index 461e2d8fa..cb20178c5 100644 --- a/examples/foundational/07n-interruptible-gemini-image.py +++ b/examples/foundational/07n-interruptible-gemini-image.py @@ -124,7 +124,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation with a styled introduction - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07n-interruptible-google-http.py b/examples/foundational/07n-interruptible-google-http.py index d627f431f..d961f2d8d 100644 --- a/examples/foundational/07n-interruptible-google-http.py +++ b/examples/foundational/07n-interruptible-google-http.py @@ -112,7 +112,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07n-interruptible-google.py b/examples/foundational/07n-interruptible-google.py index f8ec7b037..10684b8cc 100644 --- a/examples/foundational/07n-interruptible-google.py +++ b/examples/foundational/07n-interruptible-google.py @@ -112,7 +112,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07o-interruptible-assemblyai-turn-detection.py b/examples/foundational/07o-interruptible-assemblyai-turn-detection.py index 14ec1af21..64d257675 100644 --- a/examples/foundational/07o-interruptible-assemblyai-turn-detection.py +++ b/examples/foundational/07o-interruptible-assemblyai-turn-detection.py @@ -153,7 +153,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07o-interruptible-assemblyai.py b/examples/foundational/07o-interruptible-assemblyai.py index dc8ecee12..6bcae005a 100644 --- a/examples/foundational/07o-interruptible-assemblyai.py +++ b/examples/foundational/07o-interruptible-assemblyai.py @@ -102,7 +102,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07p-interruptible-krisp-viva.py b/examples/foundational/07p-interruptible-krisp-viva.py index 0e4de2dfc..5fdefd2e1 100644 --- a/examples/foundational/07p-interruptible-krisp-viva.py +++ b/examples/foundational/07p-interruptible-krisp-viva.py @@ -134,7 +134,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07p-interruptible-krisp.py b/examples/foundational/07p-interruptible-krisp.py index 229d50d17..90aa076d6 100644 --- a/examples/foundational/07p-interruptible-krisp.py +++ b/examples/foundational/07p-interruptible-krisp.py @@ -103,7 +103,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07q-interruptible-rime-http.py b/examples/foundational/07q-interruptible-rime-http.py index f661c112b..165735d6e 100644 --- a/examples/foundational/07q-interruptible-rime-http.py +++ b/examples/foundational/07q-interruptible-rime-http.py @@ -107,7 +107,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.info(f"Client connected") # Kick off the conversation. context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/07q-interruptible-rime.py b/examples/foundational/07q-interruptible-rime.py index 694f25c25..07ea429c7 100644 --- a/examples/foundational/07q-interruptible-rime.py +++ b/examples/foundational/07q-interruptible-rime.py @@ -99,7 +99,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07r-interruptible-nvidia.py b/examples/foundational/07r-interruptible-nvidia.py index ed0918ec1..b308f71a2 100644 --- a/examples/foundational/07r-interruptible-nvidia.py +++ b/examples/foundational/07r-interruptible-nvidia.py @@ -95,7 +95,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07s-interruptible-google-audio-in.py b/examples/foundational/07s-interruptible-google-audio-in.py index 3f92872f0..fa314afed 100644 --- a/examples/foundational/07s-interruptible-google-audio-in.py +++ b/examples/foundational/07s-interruptible-google-audio-in.py @@ -269,7 +269,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07t-interruptible-fish.py b/examples/foundational/07t-interruptible-fish.py index 13612a887..5519b6c4f 100644 --- a/examples/foundational/07t-interruptible-fish.py +++ b/examples/foundational/07t-interruptible-fish.py @@ -100,7 +100,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07v-interruptible-neuphonic-http.py b/examples/foundational/07v-interruptible-neuphonic-http.py index ad5b7e996..6392d4606 100644 --- a/examples/foundational/07v-interruptible-neuphonic-http.py +++ b/examples/foundational/07v-interruptible-neuphonic-http.py @@ -105,7 +105,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.info(f"Client connected") # Kick off the conversation. context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/07v-interruptible-neuphonic.py b/examples/foundational/07v-interruptible-neuphonic.py index ba3350754..a0aaf82fa 100644 --- a/examples/foundational/07v-interruptible-neuphonic.py +++ b/examples/foundational/07v-interruptible-neuphonic.py @@ -99,7 +99,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07w-interruptible-fal.py b/examples/foundational/07w-interruptible-fal.py index 08f24fd79..5ce43bbe6 100644 --- a/examples/foundational/07w-interruptible-fal.py +++ b/examples/foundational/07w-interruptible-fal.py @@ -106,7 +106,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.info(f"Client connected") # Kick off the conversation. context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/07x-interruptible-local.py b/examples/foundational/07x-interruptible-local.py index 28e970403..3d183b405 100644 --- a/examples/foundational/07x-interruptible-local.py +++ b/examples/foundational/07x-interruptible-local.py @@ -82,7 +82,7 @@ async def main(): ), ) - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message({"role": "developer", "content": "Please introduce yourself to the user."}) await task.queue_frames([LLMRunFrame()]) runner = PipelineRunner() diff --git a/examples/foundational/07y-interruptible-minimax.py b/examples/foundational/07y-interruptible-minimax.py index f8323369e..92fda761d 100644 --- a/examples/foundational/07y-interruptible-minimax.py +++ b/examples/foundational/07y-interruptible-minimax.py @@ -107,7 +107,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.info(f"Client connected") # Kick off the conversation. context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/07z-interruptible-sarvam-http.py b/examples/foundational/07z-interruptible-sarvam-http.py index 09938cf96..bca27e051 100644 --- a/examples/foundational/07z-interruptible-sarvam-http.py +++ b/examples/foundational/07z-interruptible-sarvam-http.py @@ -111,7 +111,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.info(f"Client connected") # Kick off the conversation. context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/07z-interruptible-sarvam.py b/examples/foundational/07z-interruptible-sarvam.py index 031b82116..bd007d89e 100644 --- a/examples/foundational/07z-interruptible-sarvam.py +++ b/examples/foundational/07z-interruptible-sarvam.py @@ -104,7 +104,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) # Optionally, you can wait for 30 seconds and then change the voice. diff --git a/examples/foundational/07za-interruptible-soniox.py b/examples/foundational/07za-interruptible-soniox.py index c29fea9fb..857fd19e8 100644 --- a/examples/foundational/07za-interruptible-soniox.py +++ b/examples/foundational/07za-interruptible-soniox.py @@ -105,7 +105,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07zb-interruptible-inworld-http.py b/examples/foundational/07zb-interruptible-inworld-http.py index 9f0027b42..e8e7f293d 100644 --- a/examples/foundational/07zb-interruptible-inworld-http.py +++ b/examples/foundational/07zb-interruptible-inworld-http.py @@ -112,7 +112,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.info("Client connected") # Kick off the conversation. context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/07zb-interruptible-inworld.py b/examples/foundational/07zb-interruptible-inworld.py index 9f9384554..2cbbedc91 100644 --- a/examples/foundational/07zb-interruptible-inworld.py +++ b/examples/foundational/07zb-interruptible-inworld.py @@ -99,7 +99,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info("Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07zc-interruptible-asyncai-http.py b/examples/foundational/07zc-interruptible-asyncai-http.py index 96964f5e8..b861b32ad 100644 --- a/examples/foundational/07zc-interruptible-asyncai-http.py +++ b/examples/foundational/07zc-interruptible-asyncai-http.py @@ -105,7 +105,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.info(f"Client connected") # Kick off the conversation. context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/07zc-interruptible-asyncai.py b/examples/foundational/07zc-interruptible-asyncai.py index 39052720c..55daf5e93 100644 --- a/examples/foundational/07zc-interruptible-asyncai.py +++ b/examples/foundational/07zc-interruptible-asyncai.py @@ -100,7 +100,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07zd-interruptible-aicoustics.py b/examples/foundational/07zd-interruptible-aicoustics.py index eeb45a4c7..16b850052 100644 --- a/examples/foundational/07zd-interruptible-aicoustics.py +++ b/examples/foundational/07zd-interruptible-aicoustics.py @@ -128,7 +128,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.info(f"Client connected") await audiobuffer.start_recording() # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @audiobuffer.event_handler("on_audio_data") diff --git a/examples/foundational/07ze-interruptible-hume.py b/examples/foundational/07ze-interruptible-hume.py index a5e4253f6..2dbeeb924 100644 --- a/examples/foundational/07ze-interruptible-hume.py +++ b/examples/foundational/07ze-interruptible-hume.py @@ -113,7 +113,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): "💡 Word timestamps are enabled! Watch the console for TTSTextFrame logs showing each word with its PTS." ) # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07zf-interruptible-gradium.py b/examples/foundational/07zf-interruptible-gradium.py index 1a067d1b2..3573d0b39 100644 --- a/examples/foundational/07zf-interruptible-gradium.py +++ b/examples/foundational/07zf-interruptible-gradium.py @@ -106,7 +106,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07zg-interruptible-camb.py b/examples/foundational/07zg-interruptible-camb.py index ff9b7162d..90b6cabd4 100644 --- a/examples/foundational/07zg-interruptible-camb.py +++ b/examples/foundational/07zg-interruptible-camb.py @@ -99,7 +99,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info("Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07zi-interruptible-piper.py b/examples/foundational/07zi-interruptible-piper.py index 53f21811c..617223ceb 100644 --- a/examples/foundational/07zi-interruptible-piper.py +++ b/examples/foundational/07zi-interruptible-piper.py @@ -98,7 +98,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07zj-interruptible-kokoro.py b/examples/foundational/07zj-interruptible-kokoro.py index 5fb0ca55b..c55c2ba08 100644 --- a/examples/foundational/07zj-interruptible-kokoro.py +++ b/examples/foundational/07zj-interruptible-kokoro.py @@ -98,7 +98,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/07zk-interruptible-resemble.py b/examples/foundational/07zk-interruptible-resemble.py index 60d1d8495..c27ca9255 100644 --- a/examples/foundational/07zk-interruptible-resemble.py +++ b/examples/foundational/07zk-interruptible-resemble.py @@ -98,7 +98,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/08-custom-frame-processor.py b/examples/foundational/08-custom-frame-processor.py index f07711657..bf143c40d 100644 --- a/examples/foundational/08-custom-frame-processor.py +++ b/examples/foundational/08-custom-frame-processor.py @@ -141,7 +141,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected: {client}") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/10-wake-phrase.py b/examples/foundational/10-wake-phrase.py index 5b93efb0e..50f8b1fb8 100644 --- a/examples/foundational/10-wake-phrase.py +++ b/examples/foundational/10-wake-phrase.py @@ -120,7 +120,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/14a-function-calling-anthropic.py b/examples/foundational/14a-function-calling-anthropic.py index 6cf1e228f..b8bb4eac6 100644 --- a/examples/foundational/14a-function-calling-anthropic.py +++ b/examples/foundational/14a-function-calling-anthropic.py @@ -138,7 +138,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/14i-function-calling-fireworks.py b/examples/foundational/14i-function-calling-fireworks.py index f58ef73d9..6ddb85981 100644 --- a/examples/foundational/14i-function-calling-fireworks.py +++ b/examples/foundational/14i-function-calling-fireworks.py @@ -136,7 +136,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/14n-function-calling-perplexity.py b/examples/foundational/14n-function-calling-perplexity.py index a67e93ab8..0fa3ff928 100644 --- a/examples/foundational/14n-function-calling-perplexity.py +++ b/examples/foundational/14n-function-calling-perplexity.py @@ -105,7 +105,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/14r-function-calling-aws.py b/examples/foundational/14r-function-calling-aws.py index 86ee0f0dc..b8425a341 100644 --- a/examples/foundational/14r-function-calling-aws.py +++ b/examples/foundational/14r-function-calling-aws.py @@ -147,7 +147,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/16-gpu-container-local-bot.py b/examples/foundational/16-gpu-container-local-bot.py index b12d85dff..c61870866 100644 --- a/examples/foundational/16-gpu-container-local-bot.py +++ b/examples/foundational/16-gpu-container-local-bot.py @@ -109,7 +109,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) # Handle "latency-ping" messages. The client will send app messages that look like diff --git a/examples/foundational/17-detect-user-idle.py b/examples/foundational/17-detect-user-idle.py index 1cd772af2..a427ae47e 100644 --- a/examples/foundational/17-detect-user-idle.py +++ b/examples/foundational/17-detect-user-idle.py @@ -209,7 +209,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(30) logger.info(f"Disabling idle detection") diff --git a/examples/foundational/19-openai-realtime-beta.py b/examples/foundational/19-openai-realtime-beta.py index c69d0ca92..4d1714fe3 100644 --- a/examples/foundational/19-openai-realtime-beta.py +++ b/examples/foundational/19-openai-realtime-beta.py @@ -155,7 +155,7 @@ Remember, your responses should be short. Just one or two sentences, usually. Re # OpenAIRealtimeBetaLLMService will convert this internally to messages that the # openai WebSocket API can understand. context = OpenAILLMContext( - [{"role": "user", "content": "Say hello!"}], + [{"role": "developer", "content": "Say hello!"}], tools, ) diff --git a/examples/foundational/19-openai-realtime.py b/examples/foundational/19-openai-realtime.py index cb3abf953..9c47bac77 100644 --- a/examples/foundational/19-openai-realtime.py +++ b/examples/foundational/19-openai-realtime.py @@ -181,7 +181,7 @@ Remember, your responses should be short. Just one or two sentences, usually. Re # OpenAIRealtimeLLMService will convert this internally to messages that the # openai WebSocket API can understand. context = LLMContext( - [{"role": "user", "content": "Say hello!"}], + [{"role": "developer", "content": "Say hello!"}], tools, ) diff --git a/examples/foundational/19a-azure-realtime-beta.py b/examples/foundational/19a-azure-realtime-beta.py index d25f53e96..a7f1463db 100644 --- a/examples/foundational/19a-azure-realtime-beta.py +++ b/examples/foundational/19a-azure-realtime-beta.py @@ -151,7 +151,7 @@ Remember, your responses should be short. Just one or two sentences, usually. Re # OpenAIRealtimeBetaLLMService will convert this internally to messages that the # openai WebSocket API can understand. context = OpenAILLMContext( - [{"role": "user", "content": "Say hello!"}], + [{"role": "developer", "content": "Say hello!"}], # [{"role": "user", "content": [{"type": "text", "text": "Say hello!"}]}], # [ # { diff --git a/examples/foundational/19a-azure-realtime.py b/examples/foundational/19a-azure-realtime.py index ffba20f5d..1a401155b 100644 --- a/examples/foundational/19a-azure-realtime.py +++ b/examples/foundational/19a-azure-realtime.py @@ -158,7 +158,7 @@ Remember, your responses should be short. Just one or two sentences, usually. Re # OpenAIRealtimeBetaLLMService will convert this internally to messages that the # openai WebSocket API can understand. context = LLMContext( - [{"role": "user", "content": "Say hello!"}], + [{"role": "developer", "content": "Say hello!"}], # [{"role": "user", "content": [{"type": "text", "text": "Say hello!"}]}], # [ # { diff --git a/examples/foundational/19b-openai-realtime-beta-text.py b/examples/foundational/19b-openai-realtime-beta-text.py index 9e425537f..31f5d0560 100644 --- a/examples/foundational/19b-openai-realtime-beta-text.py +++ b/examples/foundational/19b-openai-realtime-beta-text.py @@ -161,7 +161,7 @@ Remember, your responses should be short. Just one or two sentences, usually. Re # OpenAIRealtimeBetaLLMService will convert this internally to messages that the # openai WebSocket API can understand. context = OpenAILLMContext( - [{"role": "user", "content": "Say hello!"}], + [{"role": "developer", "content": "Say hello!"}], tools, ) diff --git a/examples/foundational/19b-openai-realtime-text.py b/examples/foundational/19b-openai-realtime-text.py index 1fa6ea545..4147ca4c3 100644 --- a/examples/foundational/19b-openai-realtime-text.py +++ b/examples/foundational/19b-openai-realtime-text.py @@ -171,7 +171,7 @@ Remember, your responses should be short. Just one or two sentences, usually. Re # OpenAIRealtimeLLMService will convert this internally to messages that the # openai WebSocket API can understand. context = LLMContext( - [{"role": "user", "content": "Say hello!"}], + [{"role": "developer", "content": "Say hello!"}], tools, ) diff --git a/examples/foundational/19c-openai-realtime-live-video.py b/examples/foundational/19c-openai-realtime-live-video.py index f862b5511..fdf7c04e8 100644 --- a/examples/foundational/19c-openai-realtime-live-video.py +++ b/examples/foundational/19c-openai-realtime-live-video.py @@ -101,7 +101,7 @@ Remember, your responses should be short. Just one or two sentences, usually. Re # OpenAIRealtimeLLMService will convert this internally to messages that the # openai WebSocket API can understand. context = LLMContext( - [{"role": "user", "content": "Say hello!"}], + [{"role": "developer", "content": "Say hello!"}], ) user_aggregator, assistant_aggregator = LLMContextAggregatorPair( diff --git a/examples/foundational/20b-persistent-context-openai-realtime.py b/examples/foundational/20b-persistent-context-openai-realtime.py index bceca410d..ad27161bb 100644 --- a/examples/foundational/20b-persistent-context-openai-realtime.py +++ b/examples/foundational/20b-persistent-context-openai-realtime.py @@ -213,7 +213,7 @@ Remember, your responses should be short. Just one or two sentences, usually.""" llm.register_function("get_saved_conversation_filenames", get_saved_conversation_filenames) llm.register_function("load_conversation", load_conversation) - context = LLMContext([{"role": "user", "content": "Say hello!"}], tools) + context = LLMContext([{"role": "developer", "content": "Say hello!"}], tools) user_aggregator, assistant_aggregator = LLMContextAggregatorPair( context, user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), diff --git a/examples/foundational/20e-persistent-context-aws-nova-sonic.py b/examples/foundational/20e-persistent-context-aws-nova-sonic.py index 3d1043d18..15cabb456 100644 --- a/examples/foundational/20e-persistent-context-aws-nova-sonic.py +++ b/examples/foundational/20e-persistent-context-aws-nova-sonic.py @@ -264,7 +264,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) # HACK: if using the older Nova Sonic (pre-2) model, you need this special way of # triggering the first assistant response. Note that this trigger requires a special diff --git a/examples/foundational/20f-persistent-context-grok-realtime.py b/examples/foundational/20f-persistent-context-grok-realtime.py index efa04c732..58389d6d9 100644 --- a/examples/foundational/20f-persistent-context-grok-realtime.py +++ b/examples/foundational/20f-persistent-context-grok-realtime.py @@ -202,7 +202,7 @@ Remember, your responses should be short - just one or two sentences usually.""" llm.register_function("get_saved_conversation_filenames", get_saved_conversation_filenames) llm.register_function("load_conversation", load_conversation) - context = LLMContext([{"role": "user", "content": "Say hello!"}], tools) + context = LLMContext([{"role": "developer", "content": "Say hello!"}], tools) user_aggregator, assistant_aggregator = LLMContextAggregatorPair(context) pipeline = Pipeline( diff --git a/examples/foundational/23-bot-background-sound.py b/examples/foundational/23-bot-background-sound.py index 0229db419..ac9bc30b7 100644 --- a/examples/foundational/23-bot-background-sound.py +++ b/examples/foundational/23-bot-background-sound.py @@ -128,7 +128,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.info(f"Re-enabling background sound and starting bot...") await task.queue_frame(MixerEnableFrame(True)) # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/26b-gemini-live-function-calling.py b/examples/foundational/26b-gemini-live-function-calling.py index 75481c2cd..edc5f30b2 100644 --- a/examples/foundational/26b-gemini-live-function-calling.py +++ b/examples/foundational/26b-gemini-live-function-calling.py @@ -168,7 +168,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/26h-gemini-live-vertex-function-calling.py b/examples/foundational/26h-gemini-live-vertex-function-calling.py index 7dd894fd1..3c9862d39 100644 --- a/examples/foundational/26h-gemini-live-vertex-function-calling.py +++ b/examples/foundational/26h-gemini-live-vertex-function-calling.py @@ -127,7 +127,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm.register_function("get_current_weather", fetch_weather_from_api) llm.register_function("get_restaurant_recommendation", fetch_restaurant_recommendation) - context = LLMContext([{"role": "user", "content": "Say hello."}]) + context = LLMContext([{"role": "developer", "content": "Say hello."}]) user_aggregator, assistant_aggregator = LLMContextAggregatorPair( context, user_params=LLMUserAggregatorParams( diff --git a/examples/foundational/26i-gemini-live-graceful-end.py b/examples/foundational/26i-gemini-live-graceful-end.py index 5359f431c..6ee89ef7f 100644 --- a/examples/foundational/26i-gemini-live-graceful-end.py +++ b/examples/foundational/26i-gemini-live-graceful-end.py @@ -143,7 +143,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm.register_function("end_conversation", end_conversation) context = LLMContext( - [{"role": "user", "content": "Say hello."}], + [{"role": "developer", "content": "Say hello."}], ) user_aggregator, assistant_aggregator = LLMContextAggregatorPair( context, diff --git a/examples/foundational/28-user-assistant-turns.py b/examples/foundational/28-user-assistant-turns.py index d1c053710..ff8305087 100644 --- a/examples/foundational/28-user-assistant-turns.py +++ b/examples/foundational/28-user-assistant-turns.py @@ -169,7 +169,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Start conversation - empty prompt to let LLM follow system instructions - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/29-turn-tracking-observer.py b/examples/foundational/29-turn-tracking-observer.py index e4c33a379..14d63d40b 100644 --- a/examples/foundational/29-turn-tracking-observer.py +++ b/examples/foundational/29-turn-tracking-observer.py @@ -191,7 +191,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/30-observer.py b/examples/foundational/30-observer.py index 523e09583..457ba7bb4 100644 --- a/examples/foundational/30-observer.py +++ b/examples/foundational/30-observer.py @@ -158,7 +158,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/33-gemini-rag.py b/examples/foundational/33-gemini-rag.py index 4c107d35f..5380f98d8 100644 --- a/examples/foundational/33-gemini-rag.py +++ b/examples/foundational/33-gemini-rag.py @@ -248,7 +248,9 @@ Your response will be turned into speech so use only simple words and punctuatio async def on_client_connected(transport, client): logger.info(f"Client connected") # Start conversation - empty prompt to let LLM follow system instructions - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/38-smart-turn-fal.py b/examples/foundational/38-smart-turn-fal.py index b8a62626a..432bc3844 100644 --- a/examples/foundational/38-smart-turn-fal.py +++ b/examples/foundational/38-smart-turn-fal.py @@ -116,7 +116,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/38a-smart-turn-local-coreml.py b/examples/foundational/38a-smart-turn-local-coreml.py index fc2f1d14d..3042ceb5a 100644 --- a/examples/foundational/38a-smart-turn-local-coreml.py +++ b/examples/foundational/38a-smart-turn-local-coreml.py @@ -130,7 +130,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/38b-smart-turn-local.py b/examples/foundational/38b-smart-turn-local.py index 7e4a0abd8..7a73a4324 100644 --- a/examples/foundational/38b-smart-turn-local.py +++ b/examples/foundational/38b-smart-turn-local.py @@ -102,7 +102,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/39b-mcp-streamable-http-gemini-live.py b/examples/foundational/39b-mcp-streamable-http-gemini-live.py index eb276a91a..ba9dba6e0 100644 --- a/examples/foundational/39b-mcp-streamable-http-gemini-live.py +++ b/examples/foundational/39b-mcp-streamable-http-gemini-live.py @@ -102,7 +102,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): await mcp.register_tools_schema(tools, llm) - context = LLMContext([{"role": "user", "content": "Please introduce yourself."}]) + context = LLMContext([{"role": "developer", "content": "Please introduce yourself."}]) user_aggregator, assistant_aggregator = LLMContextAggregatorPair( context, user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), diff --git a/examples/foundational/40-aws-nova-sonic.py b/examples/foundational/40-aws-nova-sonic.py index 6f7c745ce..f3a1a0737 100644 --- a/examples/foundational/40-aws-nova-sonic.py +++ b/examples/foundational/40-aws-nova-sonic.py @@ -176,7 +176,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) # HACK: if using the older Nova Sonic (pre-2) model, you need this special way of # triggering the first assistant response. Note that this trigger requires a special diff --git a/examples/foundational/42-interruption-config.py b/examples/foundational/42-interruption-config.py index 64ea2cee0..d625910df 100644 --- a/examples/foundational/42-interruption-config.py +++ b/examples/foundational/42-interruption-config.py @@ -108,7 +108,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/45-before-and-after-events.py b/examples/foundational/45-before-and-after-events.py index 3a36a626a..b9111b945 100644 --- a/examples/foundational/45-before-and-after-events.py +++ b/examples/foundational/45-before-and-after-events.py @@ -120,7 +120,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) # Custom frames are pushed in order so they can be used for synchronization purposes. await task.queue_frames([CustomBeforeProcessFrame(), LLMRunFrame(), CustomAfterPushFrame()]) diff --git a/examples/foundational/47-sentry-metrics.py b/examples/foundational/47-sentry-metrics.py index 3294727dc..3d4146150 100644 --- a/examples/foundational/47-sentry-metrics.py +++ b/examples/foundational/47-sentry-metrics.py @@ -111,7 +111,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/48-service-switcher.py b/examples/foundational/48-service-switcher.py index 9225963f7..9ded72a60 100644 --- a/examples/foundational/48-service-switcher.py +++ b/examples/foundational/48-service-switcher.py @@ -164,7 +164,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(15) print(f"Switching to {stt_deepgram}") diff --git a/examples/foundational/49a-thinking-anthropic.py b/examples/foundational/49a-thinking-anthropic.py index 0495d5e97..0cd768925 100644 --- a/examples/foundational/49a-thinking-anthropic.py +++ b/examples/foundational/49a-thinking-anthropic.py @@ -103,7 +103,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Say hello briefly."}) + context.add_message({"role": "developer", "content": "Say hello briefly."}) # Here are some example prompts conducive to demonstrating # thinking (picked from Google and Anthropic docs). # context.add_message( diff --git a/examples/foundational/49b-thinking-google.py b/examples/foundational/49b-thinking-google.py index 1cfeba456..94d3d28eb 100644 --- a/examples/foundational/49b-thinking-google.py +++ b/examples/foundational/49b-thinking-google.py @@ -104,7 +104,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Say hello briefly."}) + context.add_message({"role": "developer", "content": "Say hello briefly."}) # Replace the above with one of these example prompts to demonstrate # thinking. # These examples come from Gemini and Anthropic docs. diff --git a/examples/foundational/49c-thinking-functions-anthropic.py b/examples/foundational/49c-thinking-functions-anthropic.py index 1ce5832fc..b62a96452 100644 --- a/examples/foundational/49c-thinking-functions-anthropic.py +++ b/examples/foundational/49c-thinking-functions-anthropic.py @@ -129,7 +129,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Say hello briefly."}) + context.add_message({"role": "developer", "content": "Say hello briefly."}) # Here is an example prompt conducive to demonstrating thinking and # function calling. # This example comes from Gemini docs. diff --git a/examples/foundational/49d-thinking-functions-google.py b/examples/foundational/49d-thinking-functions-google.py index 0b62fbf2e..e06a0195b 100644 --- a/examples/foundational/49d-thinking-functions-google.py +++ b/examples/foundational/49d-thinking-functions-google.py @@ -130,7 +130,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Say hello briefly."}) + context.add_message({"role": "developer", "content": "Say hello briefly."}) # Replace the above with one of these example prompts to demonstrate # thinking and function calling. # This example comes from Gemini docs. diff --git a/examples/foundational/51-grok-realtime.py b/examples/foundational/51-grok-realtime.py index 81a2c28c4..a233b85bc 100644 --- a/examples/foundational/51-grok-realtime.py +++ b/examples/foundational/51-grok-realtime.py @@ -211,7 +211,7 @@ Always be helpful and proactive in offering assistance.""", # Create context with initial message and tools context = LLMContext( - [{"role": "user", "content": "Say hello and introduce yourself!"}], + [{"role": "developer", "content": "Say hello and introduce yourself!"}], tools, ) diff --git a/examples/foundational/53-concurrent-llm-evaluation.py b/examples/foundational/53-concurrent-llm-evaluation.py index 69cebd0ac..66a595f0b 100644 --- a/examples/foundational/53-concurrent-llm-evaluation.py +++ b/examples/foundational/53-concurrent-llm-evaluation.py @@ -145,10 +145,10 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.info(f"Client connected") # Kick off the conversation. openai_context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) groq_context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/53-concurrent-llm-rtvi-ignored-sources.py b/examples/foundational/53-concurrent-llm-rtvi-ignored-sources.py index 481d865e1..c7519dd7a 100644 --- a/examples/foundational/53-concurrent-llm-rtvi-ignored-sources.py +++ b/examples/foundational/53-concurrent-llm-rtvi-ignored-sources.py @@ -155,7 +155,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info("Client connected") main_context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) evaluator_context.add_message( {"role": "user", "content": "Ready to evaluate user messages."} diff --git a/examples/foundational/54-context-summarization-openai.py b/examples/foundational/54-context-summarization-openai.py index 060eda8f7..53c5b26a5 100644 --- a/examples/foundational/54-context-summarization-openai.py +++ b/examples/foundational/54-context-summarization-openai.py @@ -172,7 +172,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info("Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/54a-context-summarization-google.py b/examples/foundational/54a-context-summarization-google.py index 2727d34a8..56e0dc3b8 100644 --- a/examples/foundational/54a-context-summarization-google.py +++ b/examples/foundational/54a-context-summarization-google.py @@ -172,7 +172,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info("Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/54b-context-summarization-manual-openai.py b/examples/foundational/54b-context-summarization-manual-openai.py index e6bdcaf00..017831695 100644 --- a/examples/foundational/54b-context-summarization-manual-openai.py +++ b/examples/foundational/54b-context-summarization-manual-openai.py @@ -146,7 +146,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info("Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/54c-context-summarization-dedicated-llm.py b/examples/foundational/54c-context-summarization-dedicated-llm.py index ed56e343e..e4de35cb7 100644 --- a/examples/foundational/54c-context-summarization-dedicated-llm.py +++ b/examples/foundational/54c-context-summarization-dedicated-llm.py @@ -211,7 +211,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info("Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/55a-update-settings-deepgram-flux-stt.py b/examples/foundational/55a-update-settings-deepgram-flux-stt.py index d710015bd..24bb8b7ad 100644 --- a/examples/foundational/55a-update-settings-deepgram-flux-stt.py +++ b/examples/foundational/55a-update-settings-deepgram-flux-stt.py @@ -97,7 +97,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) # Update configure-able fields mid-stream (sent via Configure message, diff --git a/examples/foundational/55a-update-settings-deepgram-sagemaker-stt.py b/examples/foundational/55a-update-settings-deepgram-sagemaker-stt.py index ebe3d4cce..aa61eaa1e 100644 --- a/examples/foundational/55a-update-settings-deepgram-sagemaker-stt.py +++ b/examples/foundational/55a-update-settings-deepgram-sagemaker-stt.py @@ -100,7 +100,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) # NOTE: after this change, the bot will only respond if you speak Spanish diff --git a/examples/foundational/55a-update-settings-deepgram-stt.py b/examples/foundational/55a-update-settings-deepgram-stt.py index 46adaad6b..a083244d0 100644 --- a/examples/foundational/55a-update-settings-deepgram-stt.py +++ b/examples/foundational/55a-update-settings-deepgram-stt.py @@ -97,7 +97,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) # NOTE: after this change, the bot will only respond if you speak Spanish diff --git a/examples/foundational/55b-update-settings-azure-stt.py b/examples/foundational/55b-update-settings-azure-stt.py index 8e5d3bfe3..adc242d3c 100644 --- a/examples/foundational/55b-update-settings-azure-stt.py +++ b/examples/foundational/55b-update-settings-azure-stt.py @@ -100,7 +100,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55c-update-settings-google-stt.py b/examples/foundational/55c-update-settings-google-stt.py index be62d90ad..58b0ff357 100644 --- a/examples/foundational/55c-update-settings-google-stt.py +++ b/examples/foundational/55c-update-settings-google-stt.py @@ -97,7 +97,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55d-update-settings-assemblyai-stt.py b/examples/foundational/55d-update-settings-assemblyai-stt.py index 213873ab5..8017b6021 100644 --- a/examples/foundational/55d-update-settings-assemblyai-stt.py +++ b/examples/foundational/55d-update-settings-assemblyai-stt.py @@ -104,7 +104,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.info( "Phase 1: No keyterms boosting - try saying 'Xiomara', 'Saoirse', or 'Krzystof'" ) - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(15) diff --git a/examples/foundational/55e-update-settings-gladia-stt.py b/examples/foundational/55e-update-settings-gladia-stt.py index 307ea2954..972456853 100644 --- a/examples/foundational/55e-update-settings-gladia-stt.py +++ b/examples/foundational/55e-update-settings-gladia-stt.py @@ -97,7 +97,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55f-update-settings-elevenlabs-realtime-stt.py b/examples/foundational/55f-update-settings-elevenlabs-realtime-stt.py index 4fc351de0..f2cba36ec 100644 --- a/examples/foundational/55f-update-settings-elevenlabs-realtime-stt.py +++ b/examples/foundational/55f-update-settings-elevenlabs-realtime-stt.py @@ -97,7 +97,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55g-update-settings-elevenlabs-stt.py b/examples/foundational/55g-update-settings-elevenlabs-stt.py index d610acea6..03efdc71e 100644 --- a/examples/foundational/55g-update-settings-elevenlabs-stt.py +++ b/examples/foundational/55g-update-settings-elevenlabs-stt.py @@ -103,7 +103,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/55h-update-settings-speechmatics-stt.py b/examples/foundational/55h-update-settings-speechmatics-stt.py index eaa1874f6..93c97c827 100644 --- a/examples/foundational/55h-update-settings-speechmatics-stt.py +++ b/examples/foundational/55h-update-settings-speechmatics-stt.py @@ -104,7 +104,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55i-update-settings-whisper-api-stt.py b/examples/foundational/55i-update-settings-whisper-api-stt.py index 179a65f83..84dea00a2 100644 --- a/examples/foundational/55i-update-settings-whisper-api-stt.py +++ b/examples/foundational/55i-update-settings-whisper-api-stt.py @@ -103,7 +103,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55j-update-settings-sarvam-stt.py b/examples/foundational/55j-update-settings-sarvam-stt.py index b65401dc5..ddb76275d 100644 --- a/examples/foundational/55j-update-settings-sarvam-stt.py +++ b/examples/foundational/55j-update-settings-sarvam-stt.py @@ -97,7 +97,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55k-update-settings-soniox-stt.py b/examples/foundational/55k-update-settings-soniox-stt.py index 2a2548888..05b3c267f 100644 --- a/examples/foundational/55k-update-settings-soniox-stt.py +++ b/examples/foundational/55k-update-settings-soniox-stt.py @@ -97,7 +97,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55l-update-settings-aws-transcribe-stt.py b/examples/foundational/55l-update-settings-aws-transcribe-stt.py index cd9cfec11..25c2f272a 100644 --- a/examples/foundational/55l-update-settings-aws-transcribe-stt.py +++ b/examples/foundational/55l-update-settings-aws-transcribe-stt.py @@ -97,7 +97,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55m-update-settings-cartesia-stt.py b/examples/foundational/55m-update-settings-cartesia-stt.py index acd7fb972..79daec9fd 100644 --- a/examples/foundational/55m-update-settings-cartesia-stt.py +++ b/examples/foundational/55m-update-settings-cartesia-stt.py @@ -97,7 +97,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55n-update-settings-cartesia-http-tts.py b/examples/foundational/55n-update-settings-cartesia-http-tts.py index 328940bb5..84d3188af 100644 --- a/examples/foundational/55n-update-settings-cartesia-http-tts.py +++ b/examples/foundational/55n-update-settings-cartesia-http-tts.py @@ -96,7 +96,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55n-update-settings-cartesia-tts.py b/examples/foundational/55n-update-settings-cartesia-tts.py index 67e0c599b..71d804875 100644 --- a/examples/foundational/55n-update-settings-cartesia-tts.py +++ b/examples/foundational/55n-update-settings-cartesia-tts.py @@ -99,7 +99,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55o-update-settings-elevenlabs-http-tts.py b/examples/foundational/55o-update-settings-elevenlabs-http-tts.py index d819f2bdf..60dab3017 100644 --- a/examples/foundational/55o-update-settings-elevenlabs-http-tts.py +++ b/examples/foundational/55o-update-settings-elevenlabs-http-tts.py @@ -100,7 +100,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/55o-update-settings-elevenlabs-tts.py b/examples/foundational/55o-update-settings-elevenlabs-tts.py index 9afa33252..83de8165f 100644 --- a/examples/foundational/55o-update-settings-elevenlabs-tts.py +++ b/examples/foundational/55o-update-settings-elevenlabs-tts.py @@ -94,7 +94,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55p-update-settings-openai-tts.py b/examples/foundational/55p-update-settings-openai-tts.py index a6f2b40c7..c75e56513 100644 --- a/examples/foundational/55p-update-settings-openai-tts.py +++ b/examples/foundational/55p-update-settings-openai-tts.py @@ -92,7 +92,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55q-update-settings-deepgram-http-tts.py b/examples/foundational/55q-update-settings-deepgram-http-tts.py index 706dcc357..16083e95e 100644 --- a/examples/foundational/55q-update-settings-deepgram-http-tts.py +++ b/examples/foundational/55q-update-settings-deepgram-http-tts.py @@ -99,7 +99,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/55q-update-settings-deepgram-sagemaker-tts.py b/examples/foundational/55q-update-settings-deepgram-sagemaker-tts.py index 76c67095f..1dfd2a365 100644 --- a/examples/foundational/55q-update-settings-deepgram-sagemaker-tts.py +++ b/examples/foundational/55q-update-settings-deepgram-sagemaker-tts.py @@ -95,7 +95,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55q-update-settings-deepgram-tts.py b/examples/foundational/55q-update-settings-deepgram-tts.py index 39a6ea7b7..596d12dfa 100644 --- a/examples/foundational/55q-update-settings-deepgram-tts.py +++ b/examples/foundational/55q-update-settings-deepgram-tts.py @@ -91,7 +91,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55r-update-settings-azure-http-tts.py b/examples/foundational/55r-update-settings-azure-http-tts.py index c75ba5ff4..a0193b063 100644 --- a/examples/foundational/55r-update-settings-azure-http-tts.py +++ b/examples/foundational/55r-update-settings-azure-http-tts.py @@ -94,7 +94,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55r-update-settings-azure-tts.py b/examples/foundational/55r-update-settings-azure-tts.py index 851e16de3..46daa6781 100644 --- a/examples/foundational/55r-update-settings-azure-tts.py +++ b/examples/foundational/55r-update-settings-azure-tts.py @@ -94,7 +94,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55s-update-settings-google-http-tts.py b/examples/foundational/55s-update-settings-google-http-tts.py index bb9b1862b..b9a605560 100644 --- a/examples/foundational/55s-update-settings-google-http-tts.py +++ b/examples/foundational/55s-update-settings-google-http-tts.py @@ -91,7 +91,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55s-update-settings-google-stream-tts.py b/examples/foundational/55s-update-settings-google-stream-tts.py index 68cfd2aaa..a1c5129a6 100644 --- a/examples/foundational/55s-update-settings-google-stream-tts.py +++ b/examples/foundational/55s-update-settings-google-stream-tts.py @@ -91,7 +91,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55t-update-settings-piper-http-tts.py b/examples/foundational/55t-update-settings-piper-http-tts.py index 57770cc91..685f81dd5 100644 --- a/examples/foundational/55t-update-settings-piper-http-tts.py +++ b/examples/foundational/55t-update-settings-piper-http-tts.py @@ -99,7 +99,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/55t-update-settings-piper-tts.py b/examples/foundational/55t-update-settings-piper-tts.py index 5416b7e58..df10d999a 100644 --- a/examples/foundational/55t-update-settings-piper-tts.py +++ b/examples/foundational/55t-update-settings-piper-tts.py @@ -95,7 +95,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) # NOTE: Local Piper loads the voice model once at init, so runtime voice diff --git a/examples/foundational/55u-update-settings-rime-http-tts.py b/examples/foundational/55u-update-settings-rime-http-tts.py index 1d7154107..af69dcda5 100644 --- a/examples/foundational/55u-update-settings-rime-http-tts.py +++ b/examples/foundational/55u-update-settings-rime-http-tts.py @@ -100,7 +100,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/55u-update-settings-rime-tts.py b/examples/foundational/55u-update-settings-rime-tts.py index 33a811228..44bc69e13 100644 --- a/examples/foundational/55u-update-settings-rime-tts.py +++ b/examples/foundational/55u-update-settings-rime-tts.py @@ -94,7 +94,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55v-update-settings-lmnt-tts.py b/examples/foundational/55v-update-settings-lmnt-tts.py index 1a6c9bd07..d700e22c2 100644 --- a/examples/foundational/55v-update-settings-lmnt-tts.py +++ b/examples/foundational/55v-update-settings-lmnt-tts.py @@ -94,7 +94,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55w-update-settings-fish-tts.py b/examples/foundational/55w-update-settings-fish-tts.py index 6ba018d0e..f486ad9d1 100644 --- a/examples/foundational/55w-update-settings-fish-tts.py +++ b/examples/foundational/55w-update-settings-fish-tts.py @@ -96,7 +96,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55x-update-settings-minimax-tts.py b/examples/foundational/55x-update-settings-minimax-tts.py index c91e728a4..b77e84554 100644 --- a/examples/foundational/55x-update-settings-minimax-tts.py +++ b/examples/foundational/55x-update-settings-minimax-tts.py @@ -98,7 +98,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/55y-update-settings-groq-tts.py b/examples/foundational/55y-update-settings-groq-tts.py index 2e1a7a09d..757276c6a 100644 --- a/examples/foundational/55y-update-settings-groq-tts.py +++ b/examples/foundational/55y-update-settings-groq-tts.py @@ -91,7 +91,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55z-update-settings-hume-tts.py b/examples/foundational/55z-update-settings-hume-tts.py index 4a1fc061b..034ff6797 100644 --- a/examples/foundational/55z-update-settings-hume-tts.py +++ b/examples/foundational/55z-update-settings-hume-tts.py @@ -94,7 +94,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55za-update-settings-neuphonic-http-tts.py b/examples/foundational/55za-update-settings-neuphonic-http-tts.py index 12d44aa02..3db9e5d2e 100644 --- a/examples/foundational/55za-update-settings-neuphonic-http-tts.py +++ b/examples/foundational/55za-update-settings-neuphonic-http-tts.py @@ -97,7 +97,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/55za-update-settings-neuphonic-tts.py b/examples/foundational/55za-update-settings-neuphonic-tts.py index f3b2970a6..bb10f8699 100644 --- a/examples/foundational/55za-update-settings-neuphonic-tts.py +++ b/examples/foundational/55za-update-settings-neuphonic-tts.py @@ -91,7 +91,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zb-update-settings-inworld-http-tts.py b/examples/foundational/55zb-update-settings-inworld-http-tts.py index fb96a8eeb..41b960dd4 100644 --- a/examples/foundational/55zb-update-settings-inworld-http-tts.py +++ b/examples/foundational/55zb-update-settings-inworld-http-tts.py @@ -96,7 +96,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/55zb-update-settings-inworld-tts.py b/examples/foundational/55zb-update-settings-inworld-tts.py index 07ee4d674..9f40247dd 100644 --- a/examples/foundational/55zb-update-settings-inworld-tts.py +++ b/examples/foundational/55zb-update-settings-inworld-tts.py @@ -91,7 +91,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zc-update-settings-gemini-tts.py b/examples/foundational/55zc-update-settings-gemini-tts.py index fd7a8a5f7..e984c3575 100644 --- a/examples/foundational/55zc-update-settings-gemini-tts.py +++ b/examples/foundational/55zc-update-settings-gemini-tts.py @@ -100,7 +100,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zd-update-settings-aws-polly-tts.py b/examples/foundational/55zd-update-settings-aws-polly-tts.py index d293e12ed..b00c6982b 100644 --- a/examples/foundational/55zd-update-settings-aws-polly-tts.py +++ b/examples/foundational/55zd-update-settings-aws-polly-tts.py @@ -91,7 +91,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55ze-update-settings-sarvam-http-tts.py b/examples/foundational/55ze-update-settings-sarvam-http-tts.py index 5bc5da404..e9a35f8cd 100644 --- a/examples/foundational/55ze-update-settings-sarvam-http-tts.py +++ b/examples/foundational/55ze-update-settings-sarvam-http-tts.py @@ -96,7 +96,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/55ze-update-settings-sarvam-tts.py b/examples/foundational/55ze-update-settings-sarvam-tts.py index 8df7f781b..77bb80d57 100644 --- a/examples/foundational/55ze-update-settings-sarvam-tts.py +++ b/examples/foundational/55ze-update-settings-sarvam-tts.py @@ -91,7 +91,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zf-update-settings-camb-tts.py b/examples/foundational/55zf-update-settings-camb-tts.py index 66a90b6a8..97166ad0f 100644 --- a/examples/foundational/55zf-update-settings-camb-tts.py +++ b/examples/foundational/55zf-update-settings-camb-tts.py @@ -92,7 +92,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zg-update-settings-kokoro-tts.py b/examples/foundational/55zg-update-settings-kokoro-tts.py index 2ef247f66..8c7e1f492 100644 --- a/examples/foundational/55zg-update-settings-kokoro-tts.py +++ b/examples/foundational/55zg-update-settings-kokoro-tts.py @@ -95,7 +95,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zh-update-settings-resembleai-tts.py b/examples/foundational/55zh-update-settings-resembleai-tts.py index 45f8785a0..ae295cfa2 100644 --- a/examples/foundational/55zh-update-settings-resembleai-tts.py +++ b/examples/foundational/55zh-update-settings-resembleai-tts.py @@ -94,7 +94,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zi-update-settings-azure-llm.py b/examples/foundational/55zi-update-settings-azure-llm.py index 20bae3aac..60d151484 100644 --- a/examples/foundational/55zi-update-settings-azure-llm.py +++ b/examples/foundational/55zi-update-settings-azure-llm.py @@ -98,7 +98,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zi-update-settings-openai-llm.py b/examples/foundational/55zi-update-settings-openai-llm.py index 5862e3cab..15ee4554a 100644 --- a/examples/foundational/55zi-update-settings-openai-llm.py +++ b/examples/foundational/55zi-update-settings-openai-llm.py @@ -96,7 +96,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zi-update-settings-openai-responses-llm.py b/examples/foundational/55zi-update-settings-openai-responses-llm.py index 61bd8329e..3ab5bacd0 100644 --- a/examples/foundational/55zi-update-settings-openai-responses-llm.py +++ b/examples/foundational/55zi-update-settings-openai-responses-llm.py @@ -96,7 +96,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zj-update-settings-anthropic-llm.py b/examples/foundational/55zj-update-settings-anthropic-llm.py index 437769643..67ba6e9e9 100644 --- a/examples/foundational/55zj-update-settings-anthropic-llm.py +++ b/examples/foundational/55zj-update-settings-anthropic-llm.py @@ -96,7 +96,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zk-update-settings-google-llm.py b/examples/foundational/55zk-update-settings-google-llm.py index a9fbfd093..09a9d686d 100644 --- a/examples/foundational/55zk-update-settings-google-llm.py +++ b/examples/foundational/55zk-update-settings-google-llm.py @@ -96,7 +96,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zk-update-settings-google-vertex-llm.py b/examples/foundational/55zk-update-settings-google-vertex-llm.py index c2c8ea2bf..b63b176bc 100644 --- a/examples/foundational/55zk-update-settings-google-vertex-llm.py +++ b/examples/foundational/55zk-update-settings-google-vertex-llm.py @@ -98,7 +98,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zm-update-settings-gemini-live-vertex.py b/examples/foundational/55zm-update-settings-gemini-live-vertex.py index 59b9a5a41..c18fb34c5 100644 --- a/examples/foundational/55zm-update-settings-gemini-live-vertex.py +++ b/examples/foundational/55zm-update-settings-gemini-live-vertex.py @@ -85,7 +85,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zm-update-settings-gemini-live.py b/examples/foundational/55zm-update-settings-gemini-live.py index fd5ef213a..dafc9efe4 100644 --- a/examples/foundational/55zm-update-settings-gemini-live.py +++ b/examples/foundational/55zm-update-settings-gemini-live.py @@ -83,7 +83,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zp-update-settings-aws-bedrock-llm.py b/examples/foundational/55zp-update-settings-aws-bedrock-llm.py index 96f151463..168bb6bf0 100644 --- a/examples/foundational/55zp-update-settings-aws-bedrock-llm.py +++ b/examples/foundational/55zp-update-settings-aws-bedrock-llm.py @@ -98,7 +98,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zq-update-settings-fal-stt.py b/examples/foundational/55zq-update-settings-fal-stt.py index 8047c04e3..83b7dbc79 100644 --- a/examples/foundational/55zq-update-settings-fal-stt.py +++ b/examples/foundational/55zq-update-settings-fal-stt.py @@ -96,7 +96,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zr-update-settings-gradium-stt.py b/examples/foundational/55zr-update-settings-gradium-stt.py index b906306b4..aebced030 100644 --- a/examples/foundational/55zr-update-settings-gradium-stt.py +++ b/examples/foundational/55zr-update-settings-gradium-stt.py @@ -99,7 +99,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zs-update-settings-whisper-mlx-stt.py b/examples/foundational/55zs-update-settings-whisper-mlx-stt.py index c60cd072b..f4a96fac6 100644 --- a/examples/foundational/55zs-update-settings-whisper-mlx-stt.py +++ b/examples/foundational/55zs-update-settings-whisper-mlx-stt.py @@ -101,7 +101,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) # NOTE: after this change, the bot will only respond if you speak Spanish diff --git a/examples/foundational/55zs-update-settings-whisper-stt.py b/examples/foundational/55zs-update-settings-whisper-stt.py index 5af0049ea..f129c4c3b 100644 --- a/examples/foundational/55zs-update-settings-whisper-stt.py +++ b/examples/foundational/55zs-update-settings-whisper-stt.py @@ -101,7 +101,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zt-update-settings-nvidia-segmented-stt.py b/examples/foundational/55zt-update-settings-nvidia-segmented-stt.py index 28717e035..d2880d446 100644 --- a/examples/foundational/55zt-update-settings-nvidia-segmented-stt.py +++ b/examples/foundational/55zt-update-settings-nvidia-segmented-stt.py @@ -96,7 +96,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zt-update-settings-nvidia-stt.py b/examples/foundational/55zt-update-settings-nvidia-stt.py index c2ad7c813..e339218e8 100644 --- a/examples/foundational/55zt-update-settings-nvidia-stt.py +++ b/examples/foundational/55zt-update-settings-nvidia-stt.py @@ -97,7 +97,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zu-update-settings-openai-realtime-stt.py b/examples/foundational/55zu-update-settings-openai-realtime-stt.py index 5ddea6181..e5366eb7f 100644 --- a/examples/foundational/55zu-update-settings-openai-realtime-stt.py +++ b/examples/foundational/55zu-update-settings-openai-realtime-stt.py @@ -97,7 +97,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zv-update-settings-asyncai-http-tts.py b/examples/foundational/55zv-update-settings-asyncai-http-tts.py index caaba5ff2..93ca099b0 100644 --- a/examples/foundational/55zv-update-settings-asyncai-http-tts.py +++ b/examples/foundational/55zv-update-settings-asyncai-http-tts.py @@ -103,7 +103,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/55zv-update-settings-asyncai-tts.py b/examples/foundational/55zv-update-settings-asyncai-tts.py index 6aa678df7..a24e37c9e 100644 --- a/examples/foundational/55zv-update-settings-asyncai-tts.py +++ b/examples/foundational/55zv-update-settings-asyncai-tts.py @@ -97,7 +97,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zw-update-settings-gradium-tts.py b/examples/foundational/55zw-update-settings-gradium-tts.py index 71f19dc98..7498557a9 100644 --- a/examples/foundational/55zw-update-settings-gradium-tts.py +++ b/examples/foundational/55zw-update-settings-gradium-tts.py @@ -95,7 +95,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zx-update-settings-cerebras-llm.py b/examples/foundational/55zx-update-settings-cerebras-llm.py index 6e03a1d8b..edda71d49 100644 --- a/examples/foundational/55zx-update-settings-cerebras-llm.py +++ b/examples/foundational/55zx-update-settings-cerebras-llm.py @@ -96,7 +96,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zy-update-settings-deepseek-llm.py b/examples/foundational/55zy-update-settings-deepseek-llm.py index 4d34cf67b..adcb63259 100644 --- a/examples/foundational/55zy-update-settings-deepseek-llm.py +++ b/examples/foundational/55zy-update-settings-deepseek-llm.py @@ -96,7 +96,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zz-update-settings-fireworks-llm.py b/examples/foundational/55zz-update-settings-fireworks-llm.py index 85d83097e..3468d8dec 100644 --- a/examples/foundational/55zz-update-settings-fireworks-llm.py +++ b/examples/foundational/55zz-update-settings-fireworks-llm.py @@ -97,7 +97,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zza-update-settings-grok-llm.py b/examples/foundational/55zza-update-settings-grok-llm.py index 0a793d58e..ef58b7805 100644 --- a/examples/foundational/55zza-update-settings-grok-llm.py +++ b/examples/foundational/55zza-update-settings-grok-llm.py @@ -96,7 +96,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zzb-update-settings-groq-llm.py b/examples/foundational/55zzb-update-settings-groq-llm.py index 896f5d6ab..f1549505a 100644 --- a/examples/foundational/55zzb-update-settings-groq-llm.py +++ b/examples/foundational/55zzb-update-settings-groq-llm.py @@ -97,7 +97,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zzc-update-settings-mistral-llm.py b/examples/foundational/55zzc-update-settings-mistral-llm.py index 6a03b9e8c..5da4d47b7 100644 --- a/examples/foundational/55zzc-update-settings-mistral-llm.py +++ b/examples/foundational/55zzc-update-settings-mistral-llm.py @@ -96,7 +96,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zzd-update-settings-nvidia-llm.py b/examples/foundational/55zzd-update-settings-nvidia-llm.py index c34d45a2a..06c45dd5d 100644 --- a/examples/foundational/55zzd-update-settings-nvidia-llm.py +++ b/examples/foundational/55zzd-update-settings-nvidia-llm.py @@ -97,7 +97,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zze-update-settings-ollama-llm.py b/examples/foundational/55zze-update-settings-ollama-llm.py index 666f96e6b..90ddcbfb0 100644 --- a/examples/foundational/55zze-update-settings-ollama-llm.py +++ b/examples/foundational/55zze-update-settings-ollama-llm.py @@ -96,7 +96,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zzf-update-settings-openrouter-llm.py b/examples/foundational/55zzf-update-settings-openrouter-llm.py index 2647848bc..7b8aef6d8 100644 --- a/examples/foundational/55zzf-update-settings-openrouter-llm.py +++ b/examples/foundational/55zzf-update-settings-openrouter-llm.py @@ -96,7 +96,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zzh-update-settings-qwen-llm.py b/examples/foundational/55zzh-update-settings-qwen-llm.py index 6cfcfc97d..ff7d126d3 100644 --- a/examples/foundational/55zzh-update-settings-qwen-llm.py +++ b/examples/foundational/55zzh-update-settings-qwen-llm.py @@ -97,7 +97,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zzi-update-settings-sambanova-llm.py b/examples/foundational/55zzi-update-settings-sambanova-llm.py index 46a6561ae..4f03db5f2 100644 --- a/examples/foundational/55zzi-update-settings-sambanova-llm.py +++ b/examples/foundational/55zzi-update-settings-sambanova-llm.py @@ -96,7 +96,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zzj-update-settings-together-llm.py b/examples/foundational/55zzj-update-settings-together-llm.py index 3ffbe9c3e..2d8cd559d 100644 --- a/examples/foundational/55zzj-update-settings-together-llm.py +++ b/examples/foundational/55zzj-update-settings-together-llm.py @@ -97,7 +97,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zzk-update-settings-aws-nova-sonic-llm.py b/examples/foundational/55zzk-update-settings-aws-nova-sonic-llm.py index 0d03efe58..99fa5cd7f 100644 --- a/examples/foundational/55zzk-update-settings-aws-nova-sonic-llm.py +++ b/examples/foundational/55zzk-update-settings-aws-nova-sonic-llm.py @@ -85,7 +85,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Tell me a fun fact."}) + context.add_message({"role": "developer", "content": "Tell me a fun fact."}) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zzl-update-settings-nvidia-tts.py b/examples/foundational/55zzl-update-settings-nvidia-tts.py index 3282c981d..2b41e1cdd 100644 --- a/examples/foundational/55zzl-update-settings-nvidia-tts.py +++ b/examples/foundational/55zzl-update-settings-nvidia-tts.py @@ -92,7 +92,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zzm-update-settings-speechmatics-tts.py b/examples/foundational/55zzm-update-settings-speechmatics-tts.py index 908ff9918..4cefa1cc8 100644 --- a/examples/foundational/55zzm-update-settings-speechmatics-tts.py +++ b/examples/foundational/55zzm-update-settings-speechmatics-tts.py @@ -97,7 +97,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/55zzn-update-settings-groq-stt.py b/examples/foundational/55zzn-update-settings-groq-stt.py index e5c903753..f612ba05b 100644 --- a/examples/foundational/55zzn-update-settings-groq-stt.py +++ b/examples/foundational/55zzn-update-settings-groq-stt.py @@ -98,7 +98,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zzo-update-settings-openpipe-llm.py b/examples/foundational/55zzo-update-settings-openpipe-llm.py index 89a94d61f..d7b554c15 100644 --- a/examples/foundational/55zzo-update-settings-openpipe-llm.py +++ b/examples/foundational/55zzo-update-settings-openpipe-llm.py @@ -100,7 +100,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info(f"Client connected") - context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) diff --git a/examples/foundational/55zzp-update-settings-xtts-tts.py b/examples/foundational/55zzp-update-settings-xtts-tts.py index adb90247b..b1fb4f309 100644 --- a/examples/foundational/55zzp-update-settings-xtts-tts.py +++ b/examples/foundational/55zzp-update-settings-xtts-tts.py @@ -101,7 +101,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") context.add_message( - {"role": "user", "content": "Please introduce yourself to the user."} + {"role": "developer", "content": "Please introduce yourself to the user."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/quickstart/bot.py b/examples/quickstart/bot.py index c201edeb3..b4f392cab 100644 --- a/examples/quickstart/bot.py +++ b/examples/quickstart/bot.py @@ -106,7 +106,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.info(f"Client connected") # Kick off the conversation. context.add_message( - {"role": "user", "content": "Say hello and briefly introduce yourself."} + {"role": "developer", "content": "Say hello and briefly introduce yourself."} ) await task.queue_frames([LLMRunFrame()]) From 27fabfc1b31a6f61b2bd8c97e2d915b061a3c4f9 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Mar 2026 17:25:43 -0400 Subject: [PATCH 13/23] Improve warning message wording and formatting --- src/pipecat/adapters/base_llm_adapter.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/pipecat/adapters/base_llm_adapter.py b/src/pipecat/adapters/base_llm_adapter.py index aeef313b0..3e8037629 100644 --- a/src/pipecat/adapters/base_llm_adapter.py +++ b/src/pipecat/adapters/base_llm_adapter.py @@ -178,10 +178,10 @@ class BaseLLMAdapter(ABC, Generic[TLLMInvocationParams]): if not self._warned_system_instruction: self._warned_system_instruction = True logger.warning( - "Both system_instruction and a system message in context are set." - " Using system_instruction. The system message in context is being" - " converted to a user message to avoid sending an empty conversation" - " history." + "Both system_instruction and an initial system message in" + " context are set. Using system_instruction. The context" + " system message is being converted to a user message to" + " avoid sending an empty conversation history." ) messages[0]["role"] = "user" return None @@ -223,13 +223,16 @@ class BaseLLMAdapter(ABC, Generic[TLLMInvocationParams]): self._warned_system_instruction = True if discard_context_system: logger.warning( - "Both system_instruction and a system message in context are set." - " Using system_instruction." + "Both system_instruction and an initial system message" + " in context are set. Using system_instruction." ) else: logger.warning( - "Both system_instruction and an initial system message in context" - " are set, which may be unintended. Prefer system_instruction." + "Both system_instruction and an initial system message" + " in context are set, which may be unintended. Keeping" + " both, but consider using system_instruction for" + " system-level instructions and developer messages in" + " context for supplementary guidance." ) if system_instruction: From 5806a3f0fa5215a04b3a1e509d0e1efc24576904 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Mar 2026 17:46:47 -0400 Subject: [PATCH 14/23] Use "developer" role for remaining developer-intent messages in examples --- examples/foundational/07m-interruptible-aws-strands.py | 2 +- examples/foundational/07n-interruptible-gemini.py | 2 +- examples/foundational/14d-function-calling-anthropic-video.py | 2 +- examples/foundational/14d-function-calling-aws-video.py | 2 +- .../foundational/14d-function-calling-gemini-flash-video.py | 2 +- examples/foundational/14d-function-calling-moondream-video.py | 2 +- .../14d-function-calling-openai-responses-video.py | 2 +- examples/foundational/14d-function-calling-openai-video.py | 2 +- examples/foundational/14e-function-calling-google.py | 2 +- .../foundational/14o-function-calling-gemini-openai-format.py | 2 +- .../foundational/14p-function-calling-gemini-vertex-ai.py | 2 +- examples/foundational/15-switch-voices.py | 2 +- examples/foundational/15a-switch-languages.py | 2 +- examples/foundational/17-detect-user-idle.py | 4 ++-- examples/foundational/19a-azure-realtime-beta.py | 4 ++-- examples/foundational/19a-azure-realtime.py | 4 ++-- examples/foundational/20c-persistent-context-anthropic.py | 2 +- examples/foundational/20d-persistent-context-gemini.py | 2 +- .../foundational/20e-persistent-context-aws-nova-sonic.py | 2 +- examples/foundational/21-tavus-transport.py | 2 +- examples/foundational/22-filter-incomplete-turns.py | 2 +- examples/foundational/24-user-mute-strategy.py | 2 +- examples/foundational/25-google-audio-in.py | 4 ++-- examples/foundational/26c-gemini-live-video.py | 2 +- examples/foundational/26d-gemini-live-text.py | 2 +- examples/foundational/26e-gemini-live-google-search.py | 2 +- examples/foundational/26f-gemini-live-files-api.py | 4 ++-- examples/foundational/32-gemini-grounding-metadata.py | 2 +- examples/foundational/43-heygen-transport.py | 2 +- examples/foundational/43a-heygen-video-service.py | 2 +- examples/foundational/46-video-processing.py | 2 +- .../foundational/53-concurrent-llm-rtvi-ignored-sources.py | 2 +- examples/foundational/55zzg-update-settings-perplexity-llm.py | 2 +- examples/foundational/56-lemonslice-transport.py | 2 +- 34 files changed, 39 insertions(+), 39 deletions(-) diff --git a/examples/foundational/07m-interruptible-aws-strands.py b/examples/foundational/07m-interruptible-aws-strands.py index 5d7b5fc8e..ee5d77aad 100644 --- a/examples/foundational/07m-interruptible-aws-strands.py +++ b/examples/foundational/07m-interruptible-aws-strands.py @@ -151,7 +151,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): LLMMessagesAppendFrame( messages=[ { - "role": "user", + "role": "developer", "content": f"Greet the user and introduce yourself. Don't use emojis.", } ], diff --git a/examples/foundational/07n-interruptible-gemini.py b/examples/foundational/07n-interruptible-gemini.py index 00290bd3a..7b995a564 100644 --- a/examples/foundational/07n-interruptible-gemini.py +++ b/examples/foundational/07n-interruptible-gemini.py @@ -128,7 +128,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): # Kick off the conversation context.add_message( { - "role": "user", + "role": "developer", "content": "You are an AI assistant. You can help with a variety of tasks. Introduce yourself and ask the user what they would like to know.", } ) diff --git a/examples/foundational/14d-function-calling-anthropic-video.py b/examples/foundational/14d-function-calling-anthropic-video.py index f2653101e..c3a292f31 100644 --- a/examples/foundational/14d-function-calling-anthropic-video.py +++ b/examples/foundational/14d-function-calling-anthropic-video.py @@ -164,7 +164,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): # Kick off the conversation. context.add_message( { - "role": "user", + "role": "developer", "content": f"Please introduce yourself to the user. Use '{client_id}' as the user ID during function calls.", } ) diff --git a/examples/foundational/14d-function-calling-aws-video.py b/examples/foundational/14d-function-calling-aws-video.py index 9fccdecdf..8db25671b 100644 --- a/examples/foundational/14d-function-calling-aws-video.py +++ b/examples/foundational/14d-function-calling-aws-video.py @@ -169,7 +169,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): # Kick off the conversation. context.add_message( { - "role": "user", + "role": "developer", "content": f"Please introduce yourself to the user briefly; don't mention the camera. Use '{client_id}' as the user ID during function calls.", } ) diff --git a/examples/foundational/14d-function-calling-gemini-flash-video.py b/examples/foundational/14d-function-calling-gemini-flash-video.py index ba76de44b..59816fdd5 100644 --- a/examples/foundational/14d-function-calling-gemini-flash-video.py +++ b/examples/foundational/14d-function-calling-gemini-flash-video.py @@ -164,7 +164,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): # Kick off the conversation. context.add_message( { - "role": "user", + "role": "developer", "content": f"Please introduce yourself to the user. Use '{client_id}' as the user ID during function calls.", } ) diff --git a/examples/foundational/14d-function-calling-moondream-video.py b/examples/foundational/14d-function-calling-moondream-video.py index 2bbfbd426..b8076e7ce 100644 --- a/examples/foundational/14d-function-calling-moondream-video.py +++ b/examples/foundational/14d-function-calling-moondream-video.py @@ -202,7 +202,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): # Kick off the conversation. context.add_message( { - "role": "user", + "role": "developer", "content": f"Please introduce yourself to the user. Use '{client_id}' as the user ID during function calls.", } ) diff --git a/examples/foundational/14d-function-calling-openai-responses-video.py b/examples/foundational/14d-function-calling-openai-responses-video.py index 440c51cc1..9a77af400 100644 --- a/examples/foundational/14d-function-calling-openai-responses-video.py +++ b/examples/foundational/14d-function-calling-openai-responses-video.py @@ -163,7 +163,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): # Kick off the conversation. context.add_message( { - "role": "user", + "role": "developer", "content": f"Please introduce yourself to the user. Use '{client_id}' as the user ID during function calls.", } ) diff --git a/examples/foundational/14d-function-calling-openai-video.py b/examples/foundational/14d-function-calling-openai-video.py index 59bef64b6..3c4fcd2f6 100644 --- a/examples/foundational/14d-function-calling-openai-video.py +++ b/examples/foundational/14d-function-calling-openai-video.py @@ -163,7 +163,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): # Kick off the conversation. context.add_message( { - "role": "user", + "role": "developer", "content": f"Please introduce yourself to the user. Use '{client_id}' as the user ID during function calls.", } ) diff --git a/examples/foundational/14e-function-calling-google.py b/examples/foundational/14e-function-calling-google.py index e5a2cfb40..421a02a1e 100644 --- a/examples/foundational/14e-function-calling-google.py +++ b/examples/foundational/14e-function-calling-google.py @@ -220,7 +220,7 @@ indicate you should use the get_image tool are: # Kick off the conversation. context.add_message( { - "role": "user", + "role": "developer", "content": f"Please introduce yourself to the user. Use '{client_id}' as the user ID during function calls.", } ) diff --git a/examples/foundational/14o-function-calling-gemini-openai-format.py b/examples/foundational/14o-function-calling-gemini-openai-format.py index 70e8af3d3..e3b06c0d6 100644 --- a/examples/foundational/14o-function-calling-gemini-openai-format.py +++ b/examples/foundational/14o-function-calling-gemini-openai-format.py @@ -102,7 +102,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): tools = ToolsSchema(standard_tools=[weather_function]) messages = [ { - "role": "user", + "role": "developer", "content": "Start a conversation with 'Hey there' to get the current weather.", }, ] diff --git a/examples/foundational/14p-function-calling-gemini-vertex-ai.py b/examples/foundational/14p-function-calling-gemini-vertex-ai.py index fb3910ed3..3bee2f1bb 100644 --- a/examples/foundational/14p-function-calling-gemini-vertex-ai.py +++ b/examples/foundational/14p-function-calling-gemini-vertex-ai.py @@ -105,7 +105,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): messages = [ { - "role": "user", + "role": "developer", "content": "Start a conversation with 'Hey there' to get the current weather.", }, ] diff --git a/examples/foundational/15-switch-voices.py b/examples/foundational/15-switch-voices.py index daf77ecde..0c47d69ae 100644 --- a/examples/foundational/15-switch-voices.py +++ b/examples/foundational/15-switch-voices.py @@ -172,7 +172,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): # Kick off the conversation. context.add_message( { - "role": "user", + "role": "developer", "content": f"Please introduce yourself to the user and let them know the voices you can do. Your initial responses should be as if you were a {tts.current_voice}.", } ) diff --git a/examples/foundational/15a-switch-languages.py b/examples/foundational/15a-switch-languages.py index 0ff13df1a..c47d66847 100644 --- a/examples/foundational/15a-switch-languages.py +++ b/examples/foundational/15a-switch-languages.py @@ -162,7 +162,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): # Kick off the conversation. context.add_message( { - "role": "user", + "role": "developer", "content": f"Please introduce yourself to the user and let them know the languages you speak. Your initial responses should be in {tts.current_language}.", } ) diff --git a/examples/foundational/17-detect-user-idle.py b/examples/foundational/17-detect-user-idle.py index a427ae47e..a159f0c4e 100644 --- a/examples/foundational/17-detect-user-idle.py +++ b/examples/foundational/17-detect-user-idle.py @@ -60,14 +60,14 @@ class IdleHandler: if self._retry_count == 1: # First attempt: Add a gentle prompt to the conversation message = { - "role": "user", + "role": "developer", "content": "The user has been quiet. Politely and briefly ask if they're still there.", } await aggregator.push_frame(LLMMessagesAppendFrame([message], run_llm=True)) elif self._retry_count == 2: # Second attempt: More direct prompt message = { - "role": "user", + "role": "developer", "content": "The user is still inactive. Ask if they'd like to continue our conversation.", } await aggregator.push_frame(LLMMessagesAppendFrame([message], run_llm=True)) diff --git a/examples/foundational/19a-azure-realtime-beta.py b/examples/foundational/19a-azure-realtime-beta.py index a7f1463db..4e9b7bcb1 100644 --- a/examples/foundational/19a-azure-realtime-beta.py +++ b/examples/foundational/19a-azure-realtime-beta.py @@ -152,10 +152,10 @@ Remember, your responses should be short. Just one or two sentences, usually. Re # openai WebSocket API can understand. context = OpenAILLMContext( [{"role": "developer", "content": "Say hello!"}], - # [{"role": "user", "content": [{"type": "text", "text": "Say hello!"}]}], + # [{"role": "developer", "content": [{"type": "text", "text": "Say hello!"}]}], # [ # { - # "role": "user", + # "role": "developer", # "content": [ # {"type": "text", "text": "Say"}, # {"type": "text", "text": "yo what's up!"}, diff --git a/examples/foundational/19a-azure-realtime.py b/examples/foundational/19a-azure-realtime.py index 1a401155b..c57602f6f 100644 --- a/examples/foundational/19a-azure-realtime.py +++ b/examples/foundational/19a-azure-realtime.py @@ -159,10 +159,10 @@ Remember, your responses should be short. Just one or two sentences, usually. Re # openai WebSocket API can understand. context = LLMContext( [{"role": "developer", "content": "Say hello!"}], - # [{"role": "user", "content": [{"type": "text", "text": "Say hello!"}]}], + # [{"role": "developer", "content": [{"type": "text", "text": "Say hello!"}]}], # [ # { - # "role": "user", + # "role": "developer", # "content": [ # {"type": "text", "text": "Say"}, # {"type": "text", "text": "yo what's up!"}, diff --git a/examples/foundational/20c-persistent-context-anthropic.py b/examples/foundational/20c-persistent-context-anthropic.py index 1b74c87e1..c401becb2 100644 --- a/examples/foundational/20c-persistent-context-anthropic.py +++ b/examples/foundational/20c-persistent-context-anthropic.py @@ -226,7 +226,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): # Kick off the conversation. context.add_message( { - "role": "user", + "role": "developer", "content": "Start the call by saying the word 'hello'. Say only that word.", } ) diff --git a/examples/foundational/20d-persistent-context-gemini.py b/examples/foundational/20d-persistent-context-gemini.py index ae37e19ce..d236a15b1 100644 --- a/examples/foundational/20d-persistent-context-gemini.py +++ b/examples/foundational/20d-persistent-context-gemini.py @@ -303,7 +303,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): # Kick off the conversation. context.add_message( { - "role": "user", + "role": "developer", "content": f"Please introduce yourself to the user. Use '{client_id}' as the user ID during function calls.", } ) diff --git a/examples/foundational/20e-persistent-context-aws-nova-sonic.py b/examples/foundational/20e-persistent-context-aws-nova-sonic.py index 15cabb456..e4dc635bf 100644 --- a/examples/foundational/20e-persistent-context-aws-nova-sonic.py +++ b/examples/foundational/20e-persistent-context-aws-nova-sonic.py @@ -112,7 +112,7 @@ async def load_conversation(params: FunctionCallParams): # commented out below, is part of this. # messages.append( # { - # "role": "user", + # "role": "developer", # "content": f"{AWSNovaSonicLLMService.AWAIT_TRIGGER_ASSISTANT_RESPONSE_INSTRUCTION}", # } # ) diff --git a/examples/foundational/21-tavus-transport.py b/examples/foundational/21-tavus-transport.py index f09902fcd..f4a4aab01 100644 --- a/examples/foundational/21-tavus-transport.py +++ b/examples/foundational/21-tavus-transport.py @@ -97,7 +97,7 @@ async def main(): # Kick off the conversation. context.add_message( { - "role": "user", + "role": "developer", "content": "Start by greeting the user and ask how you can help.", } ) diff --git a/examples/foundational/22-filter-incomplete-turns.py b/examples/foundational/22-filter-incomplete-turns.py index 8a3001366..fca89f66c 100644 --- a/examples/foundational/22-filter-incomplete-turns.py +++ b/examples/foundational/22-filter-incomplete-turns.py @@ -130,7 +130,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): # Kick off the conversation. context.add_message( { - "role": "user", + "role": "developer", "content": "Please introduce yourself to the user, asking them a question that will require a complete response. To start, say 'Let me start with a fun one. If you could travel anywhere in the world right now, where would you go and why?'", } ) diff --git a/examples/foundational/24-user-mute-strategy.py b/examples/foundational/24-user-mute-strategy.py index f7e29c171..6e826f81d 100644 --- a/examples/foundational/24-user-mute-strategy.py +++ b/examples/foundational/24-user-mute-strategy.py @@ -143,7 +143,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): # Kick off the conversation with a weather-related prompt context.add_message( { - "role": "user", + "role": "developer", "content": "Ask the user what city they'd like to know the weather for.", } ) diff --git a/examples/foundational/25-google-audio-in.py b/examples/foundational/25-google-audio-in.py index 7a6c910e7..c22ed85a1 100644 --- a/examples/foundational/25-google-audio-in.py +++ b/examples/foundational/25-google-audio-in.py @@ -179,7 +179,7 @@ class InputTranscriptionContextFilter(FrameProcessor): } ) new_message_content.append(last_part) - msg = {"role": "user", "content": new_message_content} + msg = {"role": "developer", "content": new_message_content} ctx = LLMContext([{"role": "system", "content": transcriber_system_message}, msg]) await self.push_frame(LLMContextFrame(context=ctx)) @@ -318,7 +318,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): messages = [ { - "role": "user", + "role": "developer", "content": "Start by saying hello.", }, ] diff --git a/examples/foundational/26c-gemini-live-video.py b/examples/foundational/26c-gemini-live-video.py index dc5044617..38626cb6d 100644 --- a/examples/foundational/26c-gemini-live-video.py +++ b/examples/foundational/26c-gemini-live-video.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): context = LLMContext( [ { - "role": "user", + "role": "developer", "content": "Say hello.", }, ], diff --git a/examples/foundational/26d-gemini-live-text.py b/examples/foundational/26d-gemini-live-text.py index 29a4fd112..645b53b56 100644 --- a/examples/foundational/26d-gemini-live-text.py +++ b/examples/foundational/26d-gemini-live-text.py @@ -88,7 +88,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): messages = [ { - "role": "user", + "role": "developer", "content": 'Start by saying "Hello, I\'m Gemini".', }, ] diff --git a/examples/foundational/26e-gemini-live-google-search.py b/examples/foundational/26e-gemini-live-google-search.py index 28ab77c2a..0d124fa8d 100644 --- a/examples/foundational/26e-gemini-live-google-search.py +++ b/examples/foundational/26e-gemini-live-google-search.py @@ -83,7 +83,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): context = LLMContext( [ { - "role": "user", + "role": "developer", "content": "Start by greeting the user warmly, introducing yourself, and mentioning the current day. Be friendly and engaging to set a positive tone for the interaction.", } ], diff --git a/examples/foundational/26f-gemini-live-files-api.py b/examples/foundational/26f-gemini-live-files-api.py index 5abb50788..fd019fc2a 100644 --- a/examples/foundational/26f-gemini-live-files-api.py +++ b/examples/foundational/26f-gemini-live-files-api.py @@ -133,7 +133,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): context = LLMContext( [ { - "role": "user", + "role": "developer", "content": [ { "type": "text", @@ -156,7 +156,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): context = LLMContext( [ { - "role": "user", + "role": "developer", "content": "Greet the user and explain that there was an issue with file upload, but you're ready to help with other tasks.", } ] diff --git a/examples/foundational/32-gemini-grounding-metadata.py b/examples/foundational/32-gemini-grounding-metadata.py index 89afacfc3..5b706d6c9 100644 --- a/examples/foundational/32-gemini-grounding-metadata.py +++ b/examples/foundational/32-gemini-grounding-metadata.py @@ -116,7 +116,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): context = LLMContext( [ { - "role": "user", + "role": "developer", "content": "Start by greeting the user warmly, introducing yourself, and mentioning the current day. Be friendly and engaging to set a positive tone for the interaction.", } ], diff --git a/examples/foundational/43-heygen-transport.py b/examples/foundational/43-heygen-transport.py index 07190da16..47ec01abd 100644 --- a/examples/foundational/43-heygen-transport.py +++ b/examples/foundational/43-heygen-transport.py @@ -100,7 +100,7 @@ async def main(): # Kick off the conversation. context.add_message( { - "role": "user", + "role": "developer", "content": "Start by saying 'Hello' and then a short greeting.", } ) diff --git a/examples/foundational/43a-heygen-video-service.py b/examples/foundational/43a-heygen-video-service.py index 580aa52fc..35d2e3221 100644 --- a/examples/foundational/43a-heygen-video-service.py +++ b/examples/foundational/43a-heygen-video-service.py @@ -133,7 +133,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): # Kick off the conversation. context.add_message( { - "role": "user", + "role": "developer", "content": "Start by saying 'Hello' and then a short greeting.", } ) diff --git a/examples/foundational/46-video-processing.py b/examples/foundational/46-video-processing.py index 0551728a8..531c7c940 100644 --- a/examples/foundational/46-video-processing.py +++ b/examples/foundational/46-video-processing.py @@ -104,7 +104,7 @@ async def run_bot(pipecat_transport): messages = [ { - "role": "user", + "role": "developer", "content": "Start by greeting the user warmly and introducing yourself.", } ] diff --git a/examples/foundational/53-concurrent-llm-rtvi-ignored-sources.py b/examples/foundational/53-concurrent-llm-rtvi-ignored-sources.py index c7519dd7a..678c56a0b 100644 --- a/examples/foundational/53-concurrent-llm-rtvi-ignored-sources.py +++ b/examples/foundational/53-concurrent-llm-rtvi-ignored-sources.py @@ -158,7 +158,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): {"role": "developer", "content": "Please introduce yourself to the user."} ) evaluator_context.add_message( - {"role": "user", "content": "Ready to evaluate user messages."} + {"role": "developer", "content": "Ready to evaluate user messages."} ) await task.queue_frames([LLMRunFrame()]) diff --git a/examples/foundational/55zzg-update-settings-perplexity-llm.py b/examples/foundational/55zzg-update-settings-perplexity-llm.py index c495830dd..99a40c614 100644 --- a/examples/foundational/55zzg-update-settings-perplexity-llm.py +++ b/examples/foundational/55zzg-update-settings-perplexity-llm.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): messages = [ { - "role": "user", + "role": "developer", "content": "You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way. Start by introducing yourself.", }, ] diff --git a/examples/foundational/56-lemonslice-transport.py b/examples/foundational/56-lemonslice-transport.py index 667b317f8..3707ccab5 100644 --- a/examples/foundational/56-lemonslice-transport.py +++ b/examples/foundational/56-lemonslice-transport.py @@ -103,7 +103,7 @@ async def main(): # Kick off the conversation. context.add_message( { - "role": "user", + "role": "developer", "content": "Start by greeting the user and ask how you can help.", } ) From 7a0f7b58d104704ab04fcff024aa23ce96285dbb Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Mar 2026 17:54:27 -0400 Subject: [PATCH 15/23] Remove bit of unintentionally-left-in debugging logic --- examples/foundational/07e-interruptible-anthropic.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/foundational/07e-interruptible-anthropic.py b/examples/foundational/07e-interruptible-anthropic.py index 01aae9c98..fbb497d9b 100644 --- a/examples/foundational/07e-interruptible-anthropic.py +++ b/examples/foundational/07e-interruptible-anthropic.py @@ -98,7 +98,6 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message({"role": "system", "content": "You're a pirate."}) context.add_message( {"role": "developer", "content": "Please introduce yourself to the user."} ) From 0d1b834770f747b977e8b87b7b7b9cf84910987c Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Mar 2026 18:02:32 -0400 Subject: [PATCH 16/23] Add developer message support to realtime adapters OpenAI Realtime, Grok Realtime, and AWS Nova Sonic adapters now convert "developer" role messages to "user" (consistent with all other non-OpenAI adapters). Previously these messages were silently dropped. Adds starter unit tests for all three realtime adapters. --- .../services/aws_nova_sonic_adapter.py | 10 +- .../services/grok_realtime_adapter.py | 5 + .../services/open_ai_realtime_adapter.py | 9 +- tests/test_get_llm_invocation_params.py | 148 ++++++++++++++++++ 4 files changed, 168 insertions(+), 4 deletions(-) diff --git a/src/pipecat/adapters/services/aws_nova_sonic_adapter.py b/src/pipecat/adapters/services/aws_nova_sonic_adapter.py index 1b62f5edc..657c83bf9 100644 --- a/src/pipecat/adapters/services/aws_nova_sonic_adapter.py +++ b/src/pipecat/adapters/services/aws_nova_sonic_adapter.py @@ -125,7 +125,8 @@ class AWSNovaSonicLLMAdapter(BaseLLMAdapter[AWSNovaSonicLLMInvocationParams]): universal_context_messages = copy.deepcopy(universal_context_messages) - # If we have a "system" message as our first message, let's pull that out into "instruction" + # If we have a "system" message as our first message, + # pull that out into "instruction" if universal_context_messages[0].get("role") == "system": system = universal_context_messages.pop(0) content = system.get("content") @@ -136,8 +137,13 @@ class AWSNovaSonicLLMAdapter(BaseLLMAdapter[AWSNovaSonicLLMInvocationParams]): if system_instruction: self._system_instruction = system_instruction + # Convert any remaining "system"/"developer" messages to "user", + # as Nova Sonic only supports "user" and "assistant" in history. + for msg in universal_context_messages: + if msg.get("role") in ("system", "developer"): + msg["role"] = "user" + # Process remaining messages to fill out conversation history. - # Nova Sonic supports "user" and "assistant" messages in history. for universal_context_message in universal_context_messages: message = self._from_universal_context_message(universal_context_message) if message: diff --git a/src/pipecat/adapters/services/grok_realtime_adapter.py b/src/pipecat/adapters/services/grok_realtime_adapter.py index 03a3ed475..07b1de843 100644 --- a/src/pipecat/adapters/services/grok_realtime_adapter.py +++ b/src/pipecat/adapters/services/grok_realtime_adapter.py @@ -128,6 +128,11 @@ class GrokRealtimeLLMAdapter(BaseLLMAdapter): if not messages: return self.ConvertedMessages(messages=[], system_instruction=system_instruction) + # Convert any remaining "system"/"developer" messages to "user" + for msg in messages: + if msg.get("role") in ("system", "developer"): + msg["role"] = "user" + # Single user message can be sent normally if len(messages) == 1 and messages[0].get("role") == "user": return self.ConvertedMessages( diff --git a/src/pipecat/adapters/services/open_ai_realtime_adapter.py b/src/pipecat/adapters/services/open_ai_realtime_adapter.py index 136dc1a2f..6bea0e2ed 100644 --- a/src/pipecat/adapters/services/open_ai_realtime_adapter.py +++ b/src/pipecat/adapters/services/open_ai_realtime_adapter.py @@ -116,8 +116,8 @@ class OpenAIRealtimeLLMAdapter(BaseLLMAdapter): messages = copy.deepcopy(universal_context_messages) system_instruction = None - # If we have a "system" message as our first message, let's pull that out into session - # "instructions" + # If we have a "system" message as our first message, + # pull that out into session "instructions" if messages[0].get("role") == "system": system = messages.pop(0) content = system.get("content") @@ -128,6 +128,11 @@ class OpenAIRealtimeLLMAdapter(BaseLLMAdapter): if not messages: return self.ConvertedMessages(messages=[], system_instruction=system_instruction) + # Convert any remaining "system"/"developer" messages to "user" + for msg in messages: + if msg.get("role") in ("system", "developer"): + msg["role"] = "user" + # If we have just a single "user" item, we can just send it normally if len(messages) == 1 and messages[0].get("role") == "user": return self.ConvertedMessages( diff --git a/tests/test_get_llm_invocation_params.py b/tests/test_get_llm_invocation_params.py index 524d409c5..e409e2cd3 100644 --- a/tests/test_get_llm_invocation_params.py +++ b/tests/test_get_llm_invocation_params.py @@ -70,9 +70,12 @@ from google.genai.types import Content, Part from pipecat.adapters.schemas.function_schema import FunctionSchema from pipecat.adapters.schemas.tools_schema import ToolsSchema from pipecat.adapters.services.anthropic_adapter import AnthropicLLMAdapter +from pipecat.adapters.services.aws_nova_sonic_adapter import AWSNovaSonicLLMAdapter from pipecat.adapters.services.bedrock_adapter import AWSBedrockLLMAdapter from pipecat.adapters.services.gemini_adapter import GeminiLLMAdapter +from pipecat.adapters.services.grok_realtime_adapter import GrokRealtimeLLMAdapter from pipecat.adapters.services.open_ai_adapter import OpenAILLMAdapter +from pipecat.adapters.services.open_ai_realtime_adapter import OpenAIRealtimeLLMAdapter from pipecat.adapters.services.open_ai_responses_adapter import OpenAIResponsesLLMAdapter from pipecat.adapters.services.perplexity_adapter import PerplexityLLMAdapter from pipecat.processors.aggregators.llm_context import ( @@ -1950,6 +1953,151 @@ class TestOpenAIResponsesGetLLMInvocationParams(unittest.TestCase): self.assertNotIn("instructions", params) +class TestOpenAIRealtimeGetLLMInvocationParams(unittest.TestCase): + def setUp(self) -> None: + self.adapter = OpenAIRealtimeLLMAdapter() + + def test_system_message_extracted_as_instruction(self): + """Initial system message is extracted as system_instruction.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["system_instruction"], "You are helpful.") + self.assertEqual(len(params["messages"]), 1) + + def test_developer_message_becomes_user(self): + """Developer message is converted to user, not extracted as system instruction.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertIsNone(params["system_instruction"]) + # Developer converted to user, then packed with the other user message + self.assertEqual(len(params["messages"]), 1) + + def test_subsequent_developer_message_becomes_user(self): + """Non-initial developer message is converted to user.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "developer", "content": "Extra context."}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["system_instruction"], "You are helpful.") + # Developer message converted to user + self.assertEqual(len(params["messages"]), 1) + + def test_empty_messages(self): + """Empty messages list returns empty.""" + context = LLMContext(messages=[]) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["messages"], []) + self.assertIsNone(params["system_instruction"]) + + +class TestGrokRealtimeGetLLMInvocationParams(unittest.TestCase): + def setUp(self) -> None: + self.adapter = GrokRealtimeLLMAdapter() + + def test_system_message_extracted_as_instruction(self): + """Initial system message is extracted as system_instruction.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["system_instruction"], "You are helpful.") + self.assertEqual(len(params["messages"]), 1) + + def test_developer_message_becomes_user(self): + """Developer message is converted to user, not extracted as system instruction.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertIsNone(params["system_instruction"]) + # Developer converted to user, then packed with the other user message + self.assertEqual(len(params["messages"]), 1) + + def test_subsequent_developer_message_becomes_user(self): + """Non-initial developer message is converted to user.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "developer", "content": "Extra context."}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["system_instruction"], "You are helpful.") + self.assertEqual(len(params["messages"]), 1) + + def test_empty_messages(self): + """Empty messages list returns empty.""" + context = LLMContext(messages=[]) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["messages"], []) + self.assertIsNone(params["system_instruction"]) + + +class TestAWSNovaSonicGetLLMInvocationParams(unittest.TestCase): + def setUp(self) -> None: + self.adapter = AWSNovaSonicLLMAdapter() + + def test_system_message_extracted_as_instruction(self): + """Initial system message is extracted as system_instruction.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["system_instruction"], "You are helpful.") + self.assertEqual(len(params["messages"]), 1) + + def test_developer_message_becomes_user(self): + """Developer message is converted to user, not extracted as system instruction.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertIsNone(params["system_instruction"]) + # Both messages should be present (developer as user, plus the real user) + self.assertEqual(len(params["messages"]), 2) + + def test_subsequent_developer_message_becomes_user(self): + """Non-initial developer message is converted to user.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "developer", "content": "Extra context."}, + {"role": "assistant", "content": "Hi"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["system_instruction"], "You are helpful.") + # Developer becomes user, plus assistant + self.assertEqual(len(params["messages"]), 2) + + class TestBaseLLMAdapterHelpers(unittest.TestCase): """Tests for the shared helper methods on BaseLLMAdapter.""" From 0530722c5887b51d49431b43e034a8d331f5baf6 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Mar 2026 18:14:48 -0400 Subject: [PATCH 17/23] Convert developer messages to user in Perplexity adapter Perplexity doesn't support the "developer" role. Developer messages are now converted to "user" before other transformations are applied. --- .../adapters/services/perplexity_adapter.py | 6 ++++ tests/test_get_llm_invocation_params.py | 35 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/pipecat/adapters/services/perplexity_adapter.py b/src/pipecat/adapters/services/perplexity_adapter.py index 2055914e8..c5d7f1d6c 100644 --- a/src/pipecat/adapters/services/perplexity_adapter.py +++ b/src/pipecat/adapters/services/perplexity_adapter.py @@ -109,6 +109,12 @@ class PerplexityLLMAdapter(OpenAILLMAdapter): messages = copy.deepcopy(messages) + # Step 0: Convert "developer" messages to "user". + # Perplexity doesn't support the "developer" role. + for msg in messages: + if msg.get("role") == "developer": + msg["role"] = "user" + # Step 1: Convert non-initial system messages to "user". # Perplexity allows system messages at the start, but rejects them # after any non-system message. diff --git a/tests/test_get_llm_invocation_params.py b/tests/test_get_llm_invocation_params.py index e409e2cd3..6ad0af157 100644 --- a/tests/test_get_llm_invocation_params.py +++ b/tests/test_get_llm_invocation_params.py @@ -1585,6 +1585,41 @@ class TestPerplexityGetLLMInvocationParams(unittest.TestCase): self.assertEqual(params["messages"][2]["content"], "Sunny, 72F") self.assertEqual(params["messages"][3]["role"], "user") + def test_developer_message_converted_to_user(self): + """Developer messages are converted to user role.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "assistant", "content": "Hi"}, + {"role": "user", "content": "Hello"}, + ] + + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["messages"][0]["role"], "user") + self.assertEqual(params["messages"][0]["content"], "Extra context.") + + def test_developer_message_merged_with_adjacent_user(self): + """Developer→user conversion merges with adjacent user messages.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Be concise."}, + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi"}, + {"role": "user", "content": "Bye"}, + ] + + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + # developer→user merged with following user + self.assertEqual(len(params["messages"]), 3) + merged = params["messages"][0] + self.assertEqual(merged["role"], "user") + self.assertIsInstance(merged["content"], list) + self.assertEqual(len(merged["content"]), 2) + self.assertEqual(merged["content"][0]["text"], "Be concise.") + self.assertEqual(merged["content"][1]["text"], "Hello") + def test_empty_messages(self): """Test that empty messages list returns empty.""" context = LLMContext(messages=[]) From 19bcc8620c31458db61e6a83bcb0ce108efec597 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Mar 2026 18:29:22 -0400 Subject: [PATCH 18/23] Fix Gemini Live not honoring settings.system_instruction _system_instruction_from_init was being set from the deprecated `system_instruction` constructor parameter instead of `self._settings.system_instruction`, so system instructions provided via settings were silently ignored. --- src/pipecat/services/google/gemini_live/llm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipecat/services/google/gemini_live/llm.py b/src/pipecat/services/google/gemini_live/llm.py index 18b2c2e8a..3e282e7ee 100644 --- a/src/pipecat/services/google/gemini_live/llm.py +++ b/src/pipecat/services/google/gemini_live/llm.py @@ -794,7 +794,7 @@ class GeminiLiveLLMService(LLMService): self._last_sent_time = 0 self._base_url = base_url - self._system_instruction_from_init = system_instruction + self._system_instruction_from_init = self._settings.system_instruction self._tools_from_init = tools self._inference_on_context_initialization = inference_on_context_initialization self._needs_initial_turn_complete_message = False From 74686f919060d4fc2242b8bff9d0db6ad9eb3e8d Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 20 Mar 2026 18:31:31 -0400 Subject: [PATCH 19/23] Add changelog for Gemini Live system_instruction fix --- changelog/4089.fixed.2.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/4089.fixed.2.md diff --git a/changelog/4089.fixed.2.md b/changelog/4089.fixed.2.md new file mode 100644 index 000000000..43f302096 --- /dev/null +++ b/changelog/4089.fixed.2.md @@ -0,0 +1 @@ +- Fixed Gemini Live (`GoogleGeminiLiveLLMService`) not honoring `settings.system_instruction`. The system instruction was being read from a deprecated constructor parameter instead of the settings object, causing it to be silently ignored. From 4c121332cf880f5527bae050d2e7d1ad0b73c9c5 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Mon, 23 Mar 2026 13:24:43 -0400 Subject: [PATCH 20/23] Convert developer messages to user for Cerebras (and lay groundwork for other incompatible services) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenAI-compatible services that don't support the "developer" message role can now set supports_developer_role = False on the service class. BaseOpenAILLMService passes this as convert_developer_to_user to the adapter, which converts developer messages to user messages before sending them to the API. Applied to Cerebras and Perplexity. Also removes the now-redundant developer→user conversion step from PerplexityLLMAdapter (handled by the parent adapter via the flag). --- .../adapters/services/open_ai_adapter.py | 24 ++++- .../adapters/services/perplexity_adapter.py | 21 ++-- src/pipecat/services/cerebras/llm.py | 4 + src/pipecat/services/openai/base_llm.py | 17 +++- src/pipecat/services/perplexity/llm.py | 3 + tests/test_get_llm_invocation_params.py | 95 ++++++++++++++----- tests/test_run_inference.py | 10 +- 7 files changed, 135 insertions(+), 39 deletions(-) diff --git a/src/pipecat/adapters/services/open_ai_adapter.py b/src/pipecat/adapters/services/open_ai_adapter.py index f544f2c29..37adcfc9a 100644 --- a/src/pipecat/adapters/services/open_ai_adapter.py +++ b/src/pipecat/adapters/services/open_ai_adapter.py @@ -52,7 +52,11 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]): return "openai" def get_llm_invocation_params( - self, context: LLMContext, *, system_instruction: Optional[str] = None + self, + context: LLMContext, + *, + system_instruction: Optional[str] = None, + convert_developer_to_user: bool, ) -> OpenAILLMInvocationParams: """Get OpenAI-specific LLM invocation parameters from a universal LLM context. @@ -60,11 +64,16 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]): context: The LLM context containing messages, tools, etc. system_instruction: Optional system instruction from service settings or ``run_inference``. If provided, prepended as a system message. + convert_developer_to_user: If True, convert "developer"-role messages + to "user"-role messages. Used by OpenAI-compatible services that + don't support the "developer" role. Returns: Dictionary of parameters for OpenAI's ChatCompletion API. """ - messages = self._from_universal_context_messages(self.get_messages(context)) + messages = self._from_universal_context_messages( + self.get_messages(context), convert_developer_to_user=convert_developer_to_user + ) if system_instruction: # Detect initial system message for warning purposes (don't extract) @@ -131,7 +140,10 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]): return msgs def _from_universal_context_messages( - self, messages: List[LLMContextMessage] + self, + messages: List[LLMContextMessage], + *, + convert_developer_to_user: bool, ) -> List[ChatCompletionMessageParam]: result = [] for message in messages: @@ -141,6 +153,12 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]): else: # Standard message, pass through unchanged result.append(message) + + if convert_developer_to_user: + for msg in result: + if msg.get("role") == "developer": + msg["role"] = "user" + return result def _from_standard_tool_choice( diff --git a/src/pipecat/adapters/services/perplexity_adapter.py b/src/pipecat/adapters/services/perplexity_adapter.py index c5d7f1d6c..18ebea648 100644 --- a/src/pipecat/adapters/services/perplexity_adapter.py +++ b/src/pipecat/adapters/services/perplexity_adapter.py @@ -50,7 +50,11 @@ class PerplexityLLMAdapter(OpenAILLMAdapter): """ def get_llm_invocation_params( - self, context: LLMContext, *, system_instruction: Optional[str] = None + self, + context: LLMContext, + *, + system_instruction: Optional[str] = None, + convert_developer_to_user: bool, ) -> OpenAILLMInvocationParams: """Get OpenAI-compatible invocation parameters with Perplexity message fixes applied. @@ -58,12 +62,18 @@ class PerplexityLLMAdapter(OpenAILLMAdapter): context: The LLM context containing messages, tools, etc. system_instruction: Optional system instruction from service settings or ``run_inference``. Forwarded to the parent adapter. + convert_developer_to_user: If True, convert "developer"-role messages + to "user"-role messages. Forwarded to the parent adapter. Returns: Dictionary of parameters for Perplexity's ChatCompletion API, with messages transformed to satisfy Perplexity's constraints. """ - params = super().get_llm_invocation_params(context, system_instruction=system_instruction) + params = super().get_llm_invocation_params( + context, + system_instruction=system_instruction, + convert_developer_to_user=convert_developer_to_user, + ) params["messages"] = self._transform_messages(list(params["messages"])) return params @@ -109,11 +119,8 @@ class PerplexityLLMAdapter(OpenAILLMAdapter): messages = copy.deepcopy(messages) - # Step 0: Convert "developer" messages to "user". - # Perplexity doesn't support the "developer" role. - for msg in messages: - if msg.get("role") == "developer": - msg["role"] = "user" + # Note: "developer" → "user" conversion is handled by the parent adapter + # via the convert_developer_to_user parameter. # Step 1: Convert non-initial system messages to "user". # Perplexity allows system messages at the start, but rejects them diff --git a/src/pipecat/services/cerebras/llm.py b/src/pipecat/services/cerebras/llm.py index 5be270ae1..5b883ecd3 100644 --- a/src/pipecat/services/cerebras/llm.py +++ b/src/pipecat/services/cerebras/llm.py @@ -30,6 +30,10 @@ class CerebrasLLMService(OpenAILLMService): maintaining full compatibility with OpenAI's interface and functionality. """ + # Cerebras doesn't support the "developer" message role. + # This value is used by BaseOpenAILLMService when calling the adapter. + supports_developer_role = False + Settings = CerebrasLLMSettings _settings: Settings diff --git a/src/pipecat/services/openai/base_llm.py b/src/pipecat/services/openai/base_llm.py index d2b19e2df..53de03d09 100644 --- a/src/pipecat/services/openai/base_llm.py +++ b/src/pipecat/services/openai/base_llm.py @@ -71,6 +71,15 @@ class BaseOpenAILLMService(LLMService): Settings = OpenAILLMSettings _settings: Settings + supports_developer_role: bool = True + """Whether this service's API supports the "developer" message role. + + OpenAI's native API supports it, but some OpenAI-compatible services + (e.g. Cerebras) do not. Subclasses that don't support it should set + this to ``False``, which causes the adapter to convert "developer" + messages to "user" messages before sending them to the API. + """ + class InputParams(BaseModel): """Input parameters for OpenAI model configuration. @@ -351,7 +360,9 @@ class BaseOpenAILLMService(LLMService): if isinstance(context, LLMContext): adapter = self.get_llm_adapter() invocation_params: OpenAILLMInvocationParams = adapter.get_llm_invocation_params( - context, system_instruction=effective_instruction + context, + system_instruction=effective_instruction, + convert_developer_to_user=not self.supports_developer_role, ) else: invocation_params = OpenAILLMInvocationParams( @@ -421,7 +432,9 @@ class BaseOpenAILLMService(LLMService): ) params: OpenAILLMInvocationParams = adapter.get_llm_invocation_params( - context, system_instruction=self._settings.system_instruction + context, + system_instruction=self._settings.system_instruction, + convert_developer_to_user=not self.supports_developer_role, ) chunks = await self.get_chat_completions(params) diff --git a/src/pipecat/services/perplexity/llm.py b/src/pipecat/services/perplexity/llm.py index b4aedc3f7..50953691a 100644 --- a/src/pipecat/services/perplexity/llm.py +++ b/src/pipecat/services/perplexity/llm.py @@ -41,6 +41,9 @@ class PerplexityLLMService(OpenAILLMService): """ adapter_class = PerplexityLLMAdapter + # Perplexity doesn't support the "developer" message role. + # This value is used by BaseOpenAILLMService when calling the adapter. + supports_developer_role = False Settings = PerplexityLLMSettings _settings: Settings diff --git a/tests/test_get_llm_invocation_params.py b/tests/test_get_llm_invocation_params.py index 6ad0af157..883c60631 100644 --- a/tests/test_get_llm_invocation_params.py +++ b/tests/test_get_llm_invocation_params.py @@ -15,7 +15,8 @@ For OpenAI adapter: 3. Complex message structures (like multi-part content) are preserved 4. System instructions are preserved throughout messages at any position 5. system_instruction is prepended as a system message, with conflict warnings -6. Developer messages pass through without triggering warnings +6. Developer messages pass through when convert_developer_to_user is False +7. Developer messages are converted to user when convert_developer_to_user is True For Gemini adapter: 1. LLMStandardMessage objects are converted to Gemini Content format @@ -85,6 +86,11 @@ from pipecat.processors.aggregators.llm_context import ( class TestOpenAIGetLLMInvocationParams(unittest.TestCase): + # In production, BaseOpenAILLMService always passes convert_developer_to_user + # to the adapter (True or False depending on the service's supports_developer_role). + # Tests below use False to simulate native OpenAI usage, except for the + # developer-conversion-specific tests which use True. + def setUp(self) -> None: """Sets up a common adapter instance for all tests.""" self.adapter = OpenAILLMAdapter() @@ -102,7 +108,7 @@ class TestOpenAIGetLLMInvocationParams(unittest.TestCase): context = LLMContext(messages=standard_messages) # Get invocation params - params = self.adapter.get_llm_invocation_params(context) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=False) # Verify messages are passed through unchanged self.assertEqual(params["messages"], standard_messages) @@ -134,7 +140,7 @@ class TestOpenAIGetLLMInvocationParams(unittest.TestCase): context = LLMContext(messages=messages) # Get invocation params - params = self.adapter.get_llm_invocation_params(context) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=False) # Should only include standard messages and OpenAI-specific ones # (3 total: system, standard user, openai assistant) @@ -181,7 +187,7 @@ class TestOpenAIGetLLMInvocationParams(unittest.TestCase): context = LLMContext(messages=messages) # Get invocation params - params = self.adapter.get_llm_invocation_params(context) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=False) # Verify complex content is preserved self.assertEqual(len(params["messages"]), 3) @@ -222,7 +228,7 @@ class TestOpenAIGetLLMInvocationParams(unittest.TestCase): context = LLMContext(messages=messages) # Get invocation params - params = self.adapter.get_llm_invocation_params(context) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=False) # OpenAI should preserve all messages unchanged, including multiple system messages self.assertEqual(len(params["messages"]), 7) @@ -249,7 +255,9 @@ class TestOpenAIGetLLMInvocationParams(unittest.TestCase): {"role": "user", "content": "Hello"}, ] context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context, system_instruction="Be helpful.") + params = self.adapter.get_llm_invocation_params( + context, system_instruction="Be helpful.", convert_developer_to_user=False + ) self.assertEqual(params["messages"][0]["role"], "system") self.assertEqual(params["messages"][0]["content"], "Be helpful.") @@ -262,7 +270,7 @@ class TestOpenAIGetLLMInvocationParams(unittest.TestCase): {"role": "user", "content": "Hello"}, ] context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=False) self.assertEqual(len(params["messages"]), 2) self.assertEqual(params["messages"][0]["role"], "system") @@ -278,7 +286,7 @@ class TestOpenAIGetLLMInvocationParams(unittest.TestCase): with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: params = self.adapter.get_llm_invocation_params( - context, system_instruction="Be concise." + context, system_instruction="Be concise.", convert_developer_to_user=False ) mock_logger.warning.assert_called_once() warning_msg = mock_logger.warning.call_args[0][0] @@ -298,7 +306,7 @@ class TestOpenAIGetLLMInvocationParams(unittest.TestCase): with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: params = self.adapter.get_llm_invocation_params( - context, system_instruction="Be concise." + context, system_instruction="Be concise.", convert_developer_to_user=False ) mock_logger.warning.assert_not_called() @@ -315,10 +323,45 @@ class TestOpenAIGetLLMInvocationParams(unittest.TestCase): context = LLMContext(messages=messages) with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger: - self.adapter.get_llm_invocation_params(context, system_instruction="Be concise.") - self.adapter.get_llm_invocation_params(context, system_instruction="Be concise.") + self.adapter.get_llm_invocation_params( + context, system_instruction="Be concise.", convert_developer_to_user=False + ) + self.adapter.get_llm_invocation_params( + context, system_instruction="Be concise.", convert_developer_to_user=False + ) mock_logger.warning.assert_called_once() + def test_developer_messages_converted_to_user(self): + """Developer messages are converted to user role when convert_developer_to_user is True.""" + messages: list[LLMStandardMessage] = [ + {"role": "developer", "content": "Extra context."}, + {"role": "user", "content": "Hello"}, + ] + + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=True) + + self.assertEqual(params["messages"][0]["role"], "user") + self.assertEqual(params["messages"][0]["content"], "Extra context.") + + def test_developer_conversion_does_not_affect_other_roles(self): + """convert_developer_to_user only affects developer messages, not system/user/assistant.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "System prompt."}, + {"role": "developer", "content": "Dev guidance."}, + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi"}, + ] + + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=True) + + self.assertEqual(params["messages"][0]["role"], "system") + self.assertEqual(params["messages"][1]["role"], "user") + self.assertEqual(params["messages"][1]["content"], "Dev guidance.") + self.assertEqual(params["messages"][2]["role"], "user") + self.assertEqual(params["messages"][3]["role"], "assistant") + class TestGeminiGetLLMInvocationParams(unittest.TestCase): def setUp(self) -> None: @@ -1377,6 +1420,10 @@ class TestAWSBedrockGetLLMInvocationParams(unittest.TestCase): class TestPerplexityGetLLMInvocationParams(unittest.TestCase): + # Perplexity doesn't support the "developer" role, so PerplexityLLMService + # sets supports_developer_role = False. Tests below pass + # convert_developer_to_user=True to match production behavior. + def setUp(self) -> None: """Sets up a common adapter instance for all tests.""" self.adapter = PerplexityLLMAdapter() @@ -1390,7 +1437,7 @@ class TestPerplexityGetLLMInvocationParams(unittest.TestCase): ] context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=True) self.assertEqual(len(params["messages"]), 3) self.assertEqual(params["messages"][0]["role"], "user") @@ -1410,7 +1457,7 @@ class TestPerplexityGetLLMInvocationParams(unittest.TestCase): ] context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=True) self.assertEqual(len(params["messages"]), 4) self.assertEqual(params["messages"][0]["role"], "system") @@ -1429,7 +1476,7 @@ class TestPerplexityGetLLMInvocationParams(unittest.TestCase): ] context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=True) self.assertEqual(len(params["messages"]), 3) @@ -1457,7 +1504,7 @@ class TestPerplexityGetLLMInvocationParams(unittest.TestCase): ] context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=True) # system(initial), user, assistant, merged(system→user + user) self.assertEqual(len(params["messages"]), 4) @@ -1482,7 +1529,7 @@ class TestPerplexityGetLLMInvocationParams(unittest.TestCase): ] context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=True) self.assertEqual(len(params["messages"]), 3) self.assertEqual(params["messages"][0]["role"], "system") @@ -1500,7 +1547,7 @@ class TestPerplexityGetLLMInvocationParams(unittest.TestCase): ] context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=True) self.assertEqual(len(params["messages"]), 1) self.assertEqual(params["messages"][0]["role"], "user") @@ -1519,7 +1566,7 @@ class TestPerplexityGetLLMInvocationParams(unittest.TestCase): ] context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=True) self.assertEqual(len(params["messages"]), 1) self.assertEqual(params["messages"][0]["role"], "system") @@ -1538,7 +1585,7 @@ class TestPerplexityGetLLMInvocationParams(unittest.TestCase): ] context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=True) # Trailing assistant removed → [system], system stays as-is self.assertEqual(len(params["messages"]), 1) @@ -1554,7 +1601,7 @@ class TestPerplexityGetLLMInvocationParams(unittest.TestCase): ] context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=True) # After merging assistants we get [user, assistant(merged)], then trailing # assistant is removed, leaving just [user] @@ -1576,7 +1623,7 @@ class TestPerplexityGetLLMInvocationParams(unittest.TestCase): ] context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=True) self.assertEqual(len(params["messages"]), 4) self.assertEqual(params["messages"][0]["role"], "user") @@ -1594,7 +1641,7 @@ class TestPerplexityGetLLMInvocationParams(unittest.TestCase): ] context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=True) self.assertEqual(params["messages"][0]["role"], "user") self.assertEqual(params["messages"][0]["content"], "Extra context.") @@ -1609,7 +1656,7 @@ class TestPerplexityGetLLMInvocationParams(unittest.TestCase): ] context = LLMContext(messages=messages) - params = self.adapter.get_llm_invocation_params(context) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=True) # developer→user merged with following user self.assertEqual(len(params["messages"]), 3) @@ -1623,7 +1670,7 @@ class TestPerplexityGetLLMInvocationParams(unittest.TestCase): def test_empty_messages(self): """Test that empty messages list returns empty.""" context = LLMContext(messages=[]) - params = self.adapter.get_llm_invocation_params(context) + params = self.adapter.get_llm_invocation_params(context, convert_developer_to_user=True) self.assertEqual(params["messages"], []) diff --git a/tests/test_run_inference.py b/tests/test_run_inference.py index 884b1b922..bb1868973 100644 --- a/tests/test_run_inference.py +++ b/tests/test_run_inference.py @@ -60,8 +60,9 @@ async def test_openai_run_inference_with_llm_context(): # Verify assert result == "Hello! How can I help you today?" service.get_llm_adapter.assert_called_once() + # convert_developer_to_user=False because OpenAILLMService.supports_developer_role is True mock_adapter.get_llm_invocation_params.assert_called_once_with( - mock_context, system_instruction=None + mock_context, system_instruction=None, convert_developer_to_user=False ) service._client.chat.completions.create.assert_called_once_with( model="gpt-4", @@ -549,9 +550,12 @@ async def test_openai_run_inference_system_instruction_overrides_context(): ) assert result == "Response" - # Verify the adapter was called with the correct system_instruction + # Verify the adapter was called with the correct system_instruction. + # convert_developer_to_user=False because OpenAILLMService.supports_developer_role is True. mock_adapter.get_llm_invocation_params.assert_called_once_with( - mock_context, system_instruction="New system instruction" + mock_context, + system_instruction="New system instruction", + convert_developer_to_user=False, ) From 8c678c1c98e437662832ed61ba45e73587ddd6b3 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Mon, 23 Mar 2026 15:04:53 -0400 Subject: [PATCH 21/23] Set supports_developer_role = False for more OpenAI-compatible services DeepSeek, Mistral, OLLama, Qwen, SambaNova, and Together don't support the "developer" message role. --- src/pipecat/services/deepseek/llm.py | 4 ++++ src/pipecat/services/mistral/llm.py | 4 ++++ src/pipecat/services/ollama/llm.py | 5 +++++ src/pipecat/services/qwen/llm.py | 4 ++++ src/pipecat/services/sambanova/llm.py | 4 ++++ src/pipecat/services/together/llm.py | 5 +++++ 6 files changed, 26 insertions(+) diff --git a/src/pipecat/services/deepseek/llm.py b/src/pipecat/services/deepseek/llm.py index cfb69cb9a..177a87e63 100644 --- a/src/pipecat/services/deepseek/llm.py +++ b/src/pipecat/services/deepseek/llm.py @@ -30,6 +30,10 @@ class DeepSeekLLMService(OpenAILLMService): maintaining full compatibility with OpenAI's interface and functionality. """ + # DeepSeek doesn't support the "developer" message role. + # This value is used by BaseOpenAILLMService when calling the adapter. + supports_developer_role = False + Settings = DeepSeekLLMSettings _settings: Settings diff --git a/src/pipecat/services/mistral/llm.py b/src/pipecat/services/mistral/llm.py index a2edb4cb6..d280aaada 100644 --- a/src/pipecat/services/mistral/llm.py +++ b/src/pipecat/services/mistral/llm.py @@ -32,6 +32,10 @@ class MistralLLMService(OpenAILLMService): maintaining full compatibility with OpenAI's interface and functionality. """ + # Mistral doesn't support the "developer" message role. + # This value is used by BaseOpenAILLMService when calling the adapter. + supports_developer_role = False + Settings = MistralLLMSettings _settings: Settings diff --git a/src/pipecat/services/ollama/llm.py b/src/pipecat/services/ollama/llm.py index a24ebfcaf..89488787e 100644 --- a/src/pipecat/services/ollama/llm.py +++ b/src/pipecat/services/ollama/llm.py @@ -29,6 +29,11 @@ class OLLamaLLMService(OpenAILLMService): providing a compatible interface for running large language models locally. """ + # OLLama doesn't support the "developer" message role (it seems to quietly + # ignore "developer" messages). + # This value is used by BaseOpenAILLMService when calling the adapter. + supports_developer_role = False + Settings = OllamaLLMSettings _settings: Settings diff --git a/src/pipecat/services/qwen/llm.py b/src/pipecat/services/qwen/llm.py index 857c89bea..df07467ba 100644 --- a/src/pipecat/services/qwen/llm.py +++ b/src/pipecat/services/qwen/llm.py @@ -29,6 +29,10 @@ class QwenLLMService(OpenAILLMService): maintaining full compatibility with OpenAI's interface and functionality. """ + # Qwen doesn't support the "developer" message role. + # This value is used by BaseOpenAILLMService when calling the adapter. + supports_developer_role = False + Settings = QwenLLMSettings _settings: Settings diff --git a/src/pipecat/services/sambanova/llm.py b/src/pipecat/services/sambanova/llm.py index 35cf60886..20e17b4f2 100644 --- a/src/pipecat/services/sambanova/llm.py +++ b/src/pipecat/services/sambanova/llm.py @@ -41,6 +41,10 @@ class SambaNovaLLMService(OpenAILLMService): # type: ignore maintaining full compatibility with OpenAI's interface and functionality. """ + # SambaNova doesn't support the "developer" message role. + # This value is used by BaseOpenAILLMService when calling the adapter. + supports_developer_role = False + Settings = SambaNovaLLMSettings _settings: Settings diff --git a/src/pipecat/services/together/llm.py b/src/pipecat/services/together/llm.py index 4ec2f8244..80aa91590 100644 --- a/src/pipecat/services/together/llm.py +++ b/src/pipecat/services/together/llm.py @@ -29,6 +29,11 @@ class TogetherLLMService(OpenAILLMService): maintaining full compatibility with OpenAI's interface and functionality. """ + # Together.ai doesn't support the "developer" message role (it seems to quietly + # ignore "developer" messages). + # This value is used by BaseOpenAILLMService when calling the adapter. + supports_developer_role = False + Settings = TogetherLLMSettings _settings: Settings From 45926a7135a8e3fe23d04c6ca496e1627f2c52e6 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Mon, 23 Mar 2026 15:13:18 -0400 Subject: [PATCH 22/23] Update Together.ai default model to openai/gpt-oss-20b The previous default (meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo) is no longer available as a serverless Together.ai model and now requires a custom deployment. The new default is openai/gpt-oss-20b, one of Together's recommended models for small & fast use-cases. --- src/pipecat/services/together/llm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pipecat/services/together/llm.py b/src/pipecat/services/together/llm.py index 80aa91590..3711daa72 100644 --- a/src/pipecat/services/together/llm.py +++ b/src/pipecat/services/together/llm.py @@ -51,7 +51,7 @@ class TogetherLLMService(OpenAILLMService): Args: api_key: The API key for accessing Together.ai's API. base_url: The base URL for Together.ai API. Defaults to "https://api.together.xyz/v1". - model: The model identifier to use. Defaults to "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo". + model: The model identifier to use. Defaults to "openai/gpt-oss-20b". .. deprecated:: 0.0.105 Use ``settings=TogetherLLMService.Settings(model=...)`` instead. @@ -61,7 +61,7 @@ class TogetherLLMService(OpenAILLMService): **kwargs: Additional keyword arguments passed to OpenAILLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = self.Settings(model="meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo") + default_settings = self.Settings(model="openai/gpt-oss-20b") # 2. Apply direct init arg overrides (deprecated) if model is not None: From e0c49927cf3316ad5bf877ea4e240d09e5463afc Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Mon, 23 Mar 2026 15:20:17 -0400 Subject: [PATCH 23/23] Remove hard-coded model overrides from Together and Groq examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prefer service defaults — the hard-coded models we were using are no longer available on these providers. --- examples/foundational/14c-function-calling-together.py | 1 - examples/foundational/53-concurrent-llm-evaluation.py | 1 - examples/foundational/55zzb-update-settings-groq-llm.py | 1 - examples/foundational/55zzj-update-settings-together-llm.py | 1 - 4 files changed, 4 deletions(-) diff --git a/examples/foundational/14c-function-calling-together.py b/examples/foundational/14c-function-calling-together.py index 5bd176237..927c30bbd 100644 --- a/examples/foundational/14c-function-calling-together.py +++ b/examples/foundational/14c-function-calling-together.py @@ -72,7 +72,6 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = TogetherLLMService( api_key=os.getenv("TOGETHER_API_KEY"), settings=TogetherLLMService.Settings( - model="meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/53-concurrent-llm-evaluation.py b/examples/foundational/53-concurrent-llm-evaluation.py index 66a595f0b..25b9372d0 100644 --- a/examples/foundational/53-concurrent-llm-evaluation.py +++ b/examples/foundational/53-concurrent-llm-evaluation.py @@ -77,7 +77,6 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): groq_llm = GroqLLMService( api_key=os.getenv("GROQ_API_KEY"), settings=GroqLLMService.Settings( - model="meta-llama/llama-4-maverick-17b-128e-instruct", system_instruction="You are a very helpful assistant. Your goal is to demonstrate your capabilities in detail in a creative and helpful way.", ), ) diff --git a/examples/foundational/55zzb-update-settings-groq-llm.py b/examples/foundational/55zzb-update-settings-groq-llm.py index f1549505a..771a5cbe9 100644 --- a/examples/foundational/55zzb-update-settings-groq-llm.py +++ b/examples/foundational/55zzb-update-settings-groq-llm.py @@ -62,7 +62,6 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = GroqLLMService( api_key=os.getenv("GROQ_API_KEY"), settings=GroqLLMService.Settings( - model="meta-llama/llama-4-maverick-17b-128e-instruct", system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zzj-update-settings-together-llm.py b/examples/foundational/55zzj-update-settings-together-llm.py index 2d8cd559d..3144f1906 100644 --- a/examples/foundational/55zzj-update-settings-together-llm.py +++ b/examples/foundational/55zzj-update-settings-together-llm.py @@ -62,7 +62,6 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = TogetherLLMService( api_key=os.getenv("TOGETHER_API_KEY"), settings=TogetherLLMService.Settings( - model="meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), )