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)
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user