Simplify: don't promote developer messages to system instruction
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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user