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 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
|
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
|
# Fallback to return the same tools in case they are not in a standard format
|
||||||
return tools
|
return tools
|
||||||
|
|
||||||
def _extract_initial_system_or_developer(
|
def _extract_initial_system(
|
||||||
self,
|
self,
|
||||||
messages: list,
|
messages: list,
|
||||||
*,
|
*,
|
||||||
system_instruction: Optional[str],
|
system_instruction: Optional[str] = None,
|
||||||
) -> Tuple[Optional[str], Optional[str]]:
|
) -> Optional[str]:
|
||||||
"""Extract an initial system/developer message for use as a system instruction.
|
"""Extract an initial ``"system"`` message for use as a system instruction.
|
||||||
|
|
||||||
Only useful for services that expect the system instruction as a
|
Only useful for services that expect the system instruction as a
|
||||||
separate parameter, not inline in conversation history (today, all
|
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:
|
Checks ``messages[0]``. If the role is ``"system"``, pops and returns
|
||||||
|
its content. If extracting would leave the messages list empty
|
||||||
- ``"system"`` role: assumed to be intended as the system instruction.
|
(``len(messages) == 1``), the message is converted to ``"user"``
|
||||||
Extract (pop from messages).
|
role instead of being extracted, to prevent sending an empty
|
||||||
- ``"developer"`` role **without** ``system_instruction``: also assumed
|
conversation history to providers that require at least one
|
||||||
to be intended as the system instruction. Extract (pop).
|
non-system message.
|
||||||
- ``"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.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
messages: Message list in standard format (mutated in-place).
|
messages: Message list in standard format (mutated in-place).
|
||||||
system_instruction: The system instruction from service settings
|
system_instruction: The system instruction from service settings
|
||||||
or ``run_inference``, used to decide whether to extract a
|
or ``run_inference``. Only used to decide whether to warn
|
||||||
``"developer"`` message.
|
about a conflict in the single-message case.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
``(extracted_content, original_role)`` where *original_role* is
|
The extracted system message content, or ``None`` if nothing
|
||||||
``"system"`` or ``"developer"``, or ``(None, None)`` if nothing
|
|
||||||
was extracted.
|
was extracted.
|
||||||
"""
|
"""
|
||||||
if not messages:
|
if not messages:
|
||||||
return None, None
|
return None
|
||||||
|
|
||||||
role = messages[0].get("role")
|
if messages[0].get("role") != "system":
|
||||||
if role not in ("system", "developer"):
|
return None
|
||||||
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.
|
# Would extracting empty the list? Convert to "user" instead.
|
||||||
if len(messages) == 1:
|
if len(messages) == 1:
|
||||||
if role == "system" and system_instruction:
|
if system_instruction:
|
||||||
if not self._warned_system_instruction:
|
if not self._warned_system_instruction:
|
||||||
self._warned_system_instruction = True
|
self._warned_system_instruction = True
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -198,7 +184,7 @@ class BaseLLMAdapter(ABC, Generic[TLLMInvocationParams]):
|
|||||||
" history."
|
" history."
|
||||||
)
|
)
|
||||||
messages[0]["role"] = "user"
|
messages[0]["role"] = "user"
|
||||||
return None, None
|
return None
|
||||||
|
|
||||||
# Extract
|
# Extract
|
||||||
content = messages[0].get("content", "")
|
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"
|
part.get("text", "") for part in content if part.get("type") == "text"
|
||||||
)
|
)
|
||||||
messages.pop(0)
|
messages.pop(0)
|
||||||
return content, role
|
return content
|
||||||
|
|
||||||
def _resolve_system_instruction(
|
def _resolve_system_instruction(
|
||||||
self,
|
self,
|
||||||
initial_context_message: Optional[str],
|
system_from_context: Optional[str],
|
||||||
initial_context_message_role: Optional[str],
|
|
||||||
system_instruction: Optional[str],
|
system_instruction: Optional[str],
|
||||||
*,
|
*,
|
||||||
discard_context_system: bool,
|
discard_context_system: bool,
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""Resolve conflict between ``system_instruction`` and an initial context message.
|
"""Resolve conflict between ``system_instruction`` and an extracted context system 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:
|
Args:
|
||||||
initial_context_message: Content extracted from ``messages[0]``
|
system_from_context: Content extracted from an initial ``"system"``
|
||||||
by :meth:`_extract_initial_system_or_developer`, or detected
|
message by :meth:`_extract_initial_system`, or detected
|
||||||
inline (OpenAI adapters).
|
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.
|
system_instruction: From service settings or ``run_inference`` param.
|
||||||
discard_context_system: If ``True`` (non-OpenAI adapters), the
|
discard_context_system: If ``True`` (non-OpenAI adapters), the
|
||||||
context system message is discarded when ``system_instruction``
|
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
|
The effective system instruction to use, or ``None`` if the system
|
||||||
instruction is already represented in the messages (OpenAI path).
|
instruction is already represented in the messages (OpenAI path).
|
||||||
"""
|
"""
|
||||||
both_present = initial_context_message and system_instruction
|
if system_from_context and system_instruction:
|
||||||
from_system_role = initial_context_message_role == "system"
|
|
||||||
|
|
||||||
if both_present and from_system_role:
|
|
||||||
if not self._warned_system_instruction:
|
if not self._warned_system_instruction:
|
||||||
self._warned_system_instruction = True
|
self._warned_system_instruction = True
|
||||||
if discard_context_system:
|
if discard_context_system:
|
||||||
@@ -257,15 +233,11 @@ class BaseLLMAdapter(ABC, Generic[TLLMInvocationParams]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if system_instruction:
|
if system_instruction:
|
||||||
if discard_context_system:
|
return system_instruction
|
||||||
return system_instruction
|
|
||||||
else:
|
|
||||||
# OpenAI path: caller prepends; return the instruction for prepending
|
|
||||||
return system_instruction
|
|
||||||
|
|
||||||
if initial_context_message:
|
if system_from_context:
|
||||||
if discard_context_system:
|
if discard_context_system:
|
||||||
return initial_context_message
|
return system_from_context
|
||||||
else:
|
else:
|
||||||
# Content is already in messages; nothing to prepend
|
# Content is already in messages; nothing to prepend
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ class AnthropicLLMAdapter(BaseLLMAdapter[AnthropicLLMInvocationParams]):
|
|||||||
)
|
)
|
||||||
system = self._resolve_system_instruction(
|
system = self._resolve_system_instruction(
|
||||||
converted.system if converted.system is not NOT_GIVEN else None,
|
converted.system if converted.system is not NOT_GIVEN else None,
|
||||||
converted.system_role,
|
|
||||||
system_instruction,
|
system_instruction,
|
||||||
discard_context_system=True,
|
discard_context_system=True,
|
||||||
)
|
)
|
||||||
@@ -118,7 +117,6 @@ class AnthropicLLMAdapter(BaseLLMAdapter[AnthropicLLMInvocationParams]):
|
|||||||
|
|
||||||
messages: List[MessageParam]
|
messages: List[MessageParam]
|
||||||
system: str | NotGiven
|
system: str | NotGiven
|
||||||
system_role: Optional[str] = None # "system" or "developer" — origin of extracted system
|
|
||||||
|
|
||||||
def _from_universal_context_messages(
|
def _from_universal_context_messages(
|
||||||
self,
|
self,
|
||||||
@@ -127,18 +125,16 @@ class AnthropicLLMAdapter(BaseLLMAdapter[AnthropicLLMInvocationParams]):
|
|||||||
system_instruction: Optional[str] = None,
|
system_instruction: Optional[str] = None,
|
||||||
) -> ConvertedMessages:
|
) -> ConvertedMessages:
|
||||||
system = NOT_GIVEN
|
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).
|
# so the helper works with standard message format (not provider-specific).
|
||||||
remaining = list(universal_context_messages)
|
remaining = list(universal_context_messages)
|
||||||
if remaining and not isinstance(remaining[0], LLMSpecificMessage):
|
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
|
remaining, system_instruction=system_instruction
|
||||||
)
|
)
|
||||||
if extracted_content is not None:
|
if extracted is not None:
|
||||||
system = extracted_content
|
system = extracted
|
||||||
system_role = extracted_role
|
|
||||||
|
|
||||||
# Convert remaining messages to Anthropic format
|
# Convert remaining messages to Anthropic format
|
||||||
messages = []
|
messages = []
|
||||||
@@ -180,7 +176,7 @@ class AnthropicLLMAdapter(BaseLLMAdapter[AnthropicLLMInvocationParams]):
|
|||||||
elif isinstance(message["content"], list) and len(message["content"]) == 0:
|
elif isinstance(message["content"], list) and len(message["content"]) == 0:
|
||||||
message["content"] = [{"type": "text", "text": "(empty)"}]
|
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:
|
def _from_universal_context_message(self, message: LLMContextMessage) -> MessageParam:
|
||||||
if isinstance(message, LLMSpecificMessage):
|
if isinstance(message, LLMSpecificMessage):
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ class AWSBedrockLLMAdapter(BaseLLMAdapter[AWSBedrockLLMInvocationParams]):
|
|||||||
)
|
)
|
||||||
effective_system = self._resolve_system_instruction(
|
effective_system = self._resolve_system_instruction(
|
||||||
converted.system,
|
converted.system,
|
||||||
converted.system_role,
|
|
||||||
system_instruction,
|
system_instruction,
|
||||||
discard_context_system=True,
|
discard_context_system=True,
|
||||||
)
|
)
|
||||||
@@ -112,7 +111,6 @@ class AWSBedrockLLMAdapter(BaseLLMAdapter[AWSBedrockLLMInvocationParams]):
|
|||||||
|
|
||||||
messages: List[dict[str, Any]]
|
messages: List[dict[str, Any]]
|
||||||
system: Optional[str]
|
system: Optional[str]
|
||||||
system_role: Optional[str] = None # "system" or "developer" — origin of extracted system
|
|
||||||
|
|
||||||
def _from_universal_context_messages(
|
def _from_universal_context_messages(
|
||||||
self,
|
self,
|
||||||
@@ -121,18 +119,12 @@ class AWSBedrockLLMAdapter(BaseLLMAdapter[AWSBedrockLLMInvocationParams]):
|
|||||||
system_instruction: Optional[str] = None,
|
system_instruction: Optional[str] = None,
|
||||||
) -> ConvertedMessages:
|
) -> ConvertedMessages:
|
||||||
system = None
|
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).
|
# so the helper works with standard message format (not provider-specific).
|
||||||
remaining = list(universal_context_messages)
|
remaining = list(universal_context_messages)
|
||||||
if remaining and not isinstance(remaining[0], LLMSpecificMessage):
|
if remaining and not isinstance(remaining[0], LLMSpecificMessage):
|
||||||
extracted_content, extracted_role = self._extract_initial_system_or_developer(
|
system = self._extract_initial_system(remaining, system_instruction=system_instruction)
|
||||||
remaining, system_instruction=system_instruction
|
|
||||||
)
|
|
||||||
if extracted_content is not None:
|
|
||||||
system = extracted_content
|
|
||||||
system_role = extracted_role
|
|
||||||
|
|
||||||
# Convert remaining messages to Bedrock format
|
# Convert remaining messages to Bedrock format
|
||||||
messages = []
|
messages = []
|
||||||
@@ -174,7 +166,7 @@ class AWSBedrockLLMAdapter(BaseLLMAdapter[AWSBedrockLLMInvocationParams]):
|
|||||||
elif isinstance(message["content"], list) and len(message["content"]) == 0:
|
elif isinstance(message["content"], list) and len(message["content"]) == 0:
|
||||||
message["content"] = [{"type": "text", "text": "(empty)"}]
|
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]:
|
def _from_universal_context_message(self, message: LLMContextMessage) -> dict[str, Any]:
|
||||||
if isinstance(message, LLMSpecificMessage):
|
if isinstance(message, LLMSpecificMessage):
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]):
|
|||||||
)
|
)
|
||||||
effective_system = self._resolve_system_instruction(
|
effective_system = self._resolve_system_instruction(
|
||||||
converted.system_instruction,
|
converted.system_instruction,
|
||||||
converted.system_instruction_role,
|
|
||||||
system_instruction,
|
system_instruction,
|
||||||
discard_context_system=True,
|
discard_context_system=True,
|
||||||
)
|
)
|
||||||
@@ -176,7 +175,6 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]):
|
|||||||
|
|
||||||
messages: List[Content]
|
messages: List[Content]
|
||||||
system_instruction: Optional[str] = None
|
system_instruction: Optional[str] = None
|
||||||
system_instruction_role: Optional[str] = None # "system" or "developer"
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MessageConversionResult:
|
class MessageConversionResult:
|
||||||
@@ -220,12 +218,11 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]):
|
|||||||
# We work on a mutable copy so we can pop messages[0] if needed.
|
# We work on a mutable copy so we can pop messages[0] if needed.
|
||||||
remaining_messages = list(universal_context_messages)
|
remaining_messages = list(universal_context_messages)
|
||||||
extracted_system = None
|
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.
|
# so the helper works with standard message format.
|
||||||
if remaining_messages and not isinstance(remaining_messages[0], LLMSpecificMessage):
|
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
|
remaining_messages, system_instruction=system_instruction
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -294,7 +291,6 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]):
|
|||||||
return self.ConvertedMessages(
|
return self.ConvertedMessages(
|
||||||
messages=messages,
|
messages=messages,
|
||||||
system_instruction=extracted_system,
|
system_instruction=extracted_system,
|
||||||
system_instruction_role=extracted_role,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _from_standard_message(
|
def _from_standard_message(
|
||||||
|
|||||||
@@ -68,11 +68,13 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]):
|
|||||||
|
|
||||||
if system_instruction:
|
if system_instruction:
|
||||||
# Detect initial system message for warning purposes (don't extract)
|
# Detect initial system message for warning purposes (don't extract)
|
||||||
initial_role = messages[0].get("role") if messages else None
|
initial_content = (
|
||||||
initial_content = messages[0].get("content", "") if initial_role == "system" else None
|
messages[0].get("content", "")
|
||||||
|
if messages and messages[0].get("role") == "system"
|
||||||
|
else None
|
||||||
|
)
|
||||||
self._resolve_system_instruction(
|
self._resolve_system_instruction(
|
||||||
initial_content,
|
initial_content,
|
||||||
initial_role if initial_role == "system" else None,
|
|
||||||
system_instruction,
|
system_instruction,
|
||||||
discard_context_system=False,
|
discard_context_system=False,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ class OpenAIResponsesLLMAdapter(BaseLLMAdapter[OpenAIResponsesLLMInvocationParam
|
|||||||
if first_msg and first_msg.get("role") == "system":
|
if first_msg and first_msg.get("role") == "system":
|
||||||
self._resolve_system_instruction(
|
self._resolve_system_instruction(
|
||||||
first_msg.get("content", ""),
|
first_msg.get("content", ""),
|
||||||
"system",
|
|
||||||
system_instruction,
|
system_instruction,
|
||||||
discard_context_system=False,
|
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
|
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
|
6. Multiple system instructions: first extracted, later ones converted to user messages
|
||||||
7. system_instruction overrides context system message, with conflict warnings
|
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:
|
For Anthropic adapter:
|
||||||
1. LLMStandardMessage objects are converted to Anthropic MessageParam format
|
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
|
5. Consecutive messages with same role are merged into multi-content-block messages
|
||||||
6. Empty text content is converted to "(empty)"
|
6. Empty text content is converted to "(empty)"
|
||||||
7. system_instruction overrides context system message, with conflict warnings
|
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:
|
For AWS Bedrock adapter:
|
||||||
1. LLMStandardMessage objects are converted to AWS Bedrock format
|
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
|
5. Consecutive messages with same role are merged into multi-content-block messages
|
||||||
6. Empty text content is converted to "(empty)"
|
6. Empty text content is converted to "(empty)"
|
||||||
7. system_instruction overrides context system message, with conflict warnings
|
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:
|
For OpenAI Responses adapter:
|
||||||
1. LLMContext messages are converted to Responses API input items
|
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
|
8. Developer messages pass through as developer role without triggering warnings
|
||||||
|
|
||||||
For BaseLLMAdapter helpers:
|
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
|
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(params["system_instruction"], "You are helpful.")
|
||||||
self.assertEqual(len(params["messages"]), 1)
|
self.assertEqual(len(params["messages"]), 1)
|
||||||
|
|
||||||
def test_initial_developer_message_promoted(self):
|
def test_initial_developer_message_becomes_user(self):
|
||||||
"""Initial developer message without system_instruction is promoted."""
|
"""Initial developer message without system_instruction becomes user, not system_instruction."""
|
||||||
messages: list[LLMStandardMessage] = [
|
messages: list[LLMStandardMessage] = [
|
||||||
{"role": "developer", "content": "Extra context."},
|
{"role": "developer", "content": "Extra context."},
|
||||||
{"role": "user", "content": "Hello"},
|
{"role": "user", "content": "Hello"},
|
||||||
@@ -611,7 +611,10 @@ class TestGeminiGetLLMInvocationParams(unittest.TestCase):
|
|||||||
context = LLMContext(messages=messages)
|
context = LLMContext(messages=messages)
|
||||||
params = self.adapter.get_llm_invocation_params(context)
|
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):
|
def test_both_system_instruction_and_system_message_warns(self):
|
||||||
"""system_instruction + initial system message warns and uses system_instruction."""
|
"""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(len(params["messages"]), 1)
|
||||||
self.assertEqual(params["messages"][0]["role"], "user")
|
self.assertEqual(params["messages"][0]["role"], "user")
|
||||||
|
|
||||||
def test_initial_developer_message_promoted(self):
|
def test_initial_developer_message_becomes_user(self):
|
||||||
"""Initial developer message without system_instruction is promoted to system."""
|
"""Initial developer message without system_instruction becomes user, not system."""
|
||||||
|
from anthropic import NOT_GIVEN
|
||||||
|
|
||||||
messages: list[LLMStandardMessage] = [
|
messages: list[LLMStandardMessage] = [
|
||||||
{"role": "developer", "content": "Extra context."},
|
{"role": "developer", "content": "Extra context."},
|
||||||
|
{"role": "assistant", "content": "OK"},
|
||||||
{"role": "user", "content": "Hello"},
|
{"role": "user", "content": "Hello"},
|
||||||
]
|
]
|
||||||
context = LLMContext(messages=messages)
|
context = LLMContext(messages=messages)
|
||||||
params = self.adapter.get_llm_invocation_params(context, enable_prompt_caching=False)
|
params = self.adapter.get_llm_invocation_params(context, enable_prompt_caching=False)
|
||||||
|
|
||||||
self.assertEqual(params["system"], "Extra context.")
|
self.assertEqual(params["system"], NOT_GIVEN)
|
||||||
self.assertEqual(len(params["messages"]), 1)
|
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):
|
def test_both_system_instruction_and_system_message_warns(self):
|
||||||
"""system_instruction + initial system message warns and uses system_instruction."""
|
"""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."}])
|
self.assertEqual(params["system"], [{"text": "Be helpful."}])
|
||||||
|
|
||||||
def test_initial_developer_message_promoted(self):
|
def test_initial_developer_message_becomes_user(self):
|
||||||
"""Initial developer message without system_instruction is promoted."""
|
"""Initial developer message without system_instruction becomes user, not system."""
|
||||||
messages: list[LLMStandardMessage] = [
|
messages: list[LLMStandardMessage] = [
|
||||||
{"role": "developer", "content": "Extra context."},
|
{"role": "developer", "content": "Extra context."},
|
||||||
|
{"role": "assistant", "content": "OK"},
|
||||||
{"role": "user", "content": "Hello"},
|
{"role": "user", "content": "Hello"},
|
||||||
]
|
]
|
||||||
context = LLMContext(messages=messages)
|
context = LLMContext(messages=messages)
|
||||||
params = self.adapter.get_llm_invocation_params(context)
|
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):
|
def test_both_system_instruction_and_system_message_warns(self):
|
||||||
"""system_instruction + initial system message warns and uses system_instruction."""
|
"""system_instruction + initial system message warns and uses system_instruction."""
|
||||||
@@ -1951,54 +1963,43 @@ class TestBaseLLMAdapterHelpers(unittest.TestCase):
|
|||||||
{"role": "system", "content": "Be helpful."},
|
{"role": "system", "content": "Be helpful."},
|
||||||
{"role": "user", "content": "Hello"},
|
{"role": "user", "content": "Hello"},
|
||||||
]
|
]
|
||||||
content, role = self.adapter._extract_initial_system_or_developer(
|
content = self.adapter._extract_initial_system(messages, system_instruction=None)
|
||||||
messages, system_instruction=None
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(content, "Be helpful.")
|
self.assertEqual(content, "Be helpful.")
|
||||||
self.assertEqual(role, "system")
|
|
||||||
self.assertEqual(len(messages), 1) # popped
|
self.assertEqual(len(messages), 1) # popped
|
||||||
|
|
||||||
def test_extract_developer_without_system_instruction(self):
|
def test_extract_developer_not_extracted(self):
|
||||||
"""Developer message is extracted when no system_instruction."""
|
"""Developer message is not extracted by _extract_initial_system."""
|
||||||
messages = [
|
messages = [
|
||||||
{"role": "developer", "content": "Context."},
|
{"role": "developer", "content": "Context."},
|
||||||
{"role": "user", "content": "Hello"},
|
{"role": "user", "content": "Hello"},
|
||||||
]
|
]
|
||||||
content, role = self.adapter._extract_initial_system_or_developer(
|
content = self.adapter._extract_initial_system(messages, system_instruction=None)
|
||||||
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(content)
|
||||||
self.assertIsNone(role)
|
|
||||||
self.assertEqual(len(messages), 2) # not popped
|
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):
|
def test_single_system_message_becomes_user(self):
|
||||||
"""Single system message is converted to user instead of extracting (empty prevention)."""
|
"""Single system message is converted to user instead of extracting (empty prevention)."""
|
||||||
messages = [
|
messages = [
|
||||||
{"role": "system", "content": "Be helpful."},
|
{"role": "system", "content": "Be helpful."},
|
||||||
]
|
]
|
||||||
content, role = self.adapter._extract_initial_system_or_developer(
|
content = self.adapter._extract_initial_system(messages, system_instruction=None)
|
||||||
messages, system_instruction=None
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertIsNone(content)
|
self.assertIsNone(content)
|
||||||
self.assertIsNone(role)
|
|
||||||
self.assertEqual(len(messages), 1) # not popped
|
self.assertEqual(len(messages), 1) # not popped
|
||||||
self.assertEqual(messages[0]["role"], "user")
|
self.assertEqual(messages[0]["role"], "user")
|
||||||
|
|
||||||
@@ -2010,13 +2011,10 @@ class TestBaseLLMAdapterHelpers(unittest.TestCase):
|
|||||||
|
|
||||||
adapter = OpenAILLMAdapter()
|
adapter = OpenAILLMAdapter()
|
||||||
with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger:
|
with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger:
|
||||||
content, role = adapter._extract_initial_system_or_developer(
|
content = adapter._extract_initial_system(messages, system_instruction="Be concise.")
|
||||||
messages, system_instruction="Be concise."
|
|
||||||
)
|
|
||||||
mock_logger.warning.assert_called_once()
|
mock_logger.warning.assert_called_once()
|
||||||
|
|
||||||
self.assertIsNone(content)
|
self.assertIsNone(content)
|
||||||
self.assertIsNone(role)
|
|
||||||
self.assertEqual(messages[0]["role"], "user")
|
self.assertEqual(messages[0]["role"], "user")
|
||||||
|
|
||||||
def test_non_system_message_ignored(self):
|
def test_non_system_message_ignored(self):
|
||||||
@@ -2024,29 +2022,23 @@ class TestBaseLLMAdapterHelpers(unittest.TestCase):
|
|||||||
messages = [
|
messages = [
|
||||||
{"role": "user", "content": "Hello"},
|
{"role": "user", "content": "Hello"},
|
||||||
]
|
]
|
||||||
content, role = self.adapter._extract_initial_system_or_developer(
|
content = self.adapter._extract_initial_system(messages, system_instruction=None)
|
||||||
messages, system_instruction=None
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertIsNone(content)
|
self.assertIsNone(content)
|
||||||
self.assertIsNone(role)
|
|
||||||
self.assertEqual(len(messages), 1)
|
self.assertEqual(len(messages), 1)
|
||||||
|
|
||||||
def test_empty_messages(self):
|
def test_empty_messages(self):
|
||||||
"""Empty messages list returns None."""
|
"""Empty messages list returns None."""
|
||||||
messages = []
|
messages = []
|
||||||
content, role = self.adapter._extract_initial_system_or_developer(
|
content = self.adapter._extract_initial_system(messages, system_instruction=None)
|
||||||
messages, system_instruction=None
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertIsNone(content)
|
self.assertIsNone(content)
|
||||||
self.assertIsNone(role)
|
|
||||||
|
|
||||||
def test_resolve_both_system_discard(self):
|
def test_resolve_both_system_discard(self):
|
||||||
"""Resolve with discard=True: system_instruction wins, warns."""
|
"""Resolve with discard=True: system_instruction wins, warns."""
|
||||||
with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger:
|
with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger:
|
||||||
result = self.adapter._resolve_system_instruction(
|
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()
|
mock_logger.warning.assert_called_once()
|
||||||
|
|
||||||
@@ -2056,7 +2048,7 @@ class TestBaseLLMAdapterHelpers(unittest.TestCase):
|
|||||||
"""Resolve with discard=False: warns but returns system_instruction."""
|
"""Resolve with discard=False: warns but returns system_instruction."""
|
||||||
with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger:
|
with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger:
|
||||||
result = self.adapter._resolve_system_instruction(
|
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()
|
mock_logger.warning.assert_called_once()
|
||||||
|
|
||||||
@@ -2066,7 +2058,7 @@ class TestBaseLLMAdapterHelpers(unittest.TestCase):
|
|||||||
"""Only system_instruction: returns it, no warning."""
|
"""Only system_instruction: returns it, no warning."""
|
||||||
with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger:
|
with patch("pipecat.adapters.base_llm_adapter.logger") as mock_logger:
|
||||||
result = self.adapter._resolve_system_instruction(
|
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()
|
mock_logger.warning.assert_not_called()
|
||||||
|
|
||||||
@@ -2075,7 +2067,7 @@ class TestBaseLLMAdapterHelpers(unittest.TestCase):
|
|||||||
def test_resolve_only_context_system_discard(self):
|
def test_resolve_only_context_system_discard(self):
|
||||||
"""Only context system (discard=True): returns it."""
|
"""Only context system (discard=True): returns it."""
|
||||||
result = self.adapter._resolve_system_instruction(
|
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")
|
self.assertEqual(result, "from context")
|
||||||
@@ -2083,7 +2075,7 @@ class TestBaseLLMAdapterHelpers(unittest.TestCase):
|
|||||||
def test_resolve_only_context_system_keep(self):
|
def test_resolve_only_context_system_keep(self):
|
||||||
"""Only context system (discard=False): returns None (already in messages)."""
|
"""Only context system (discard=False): returns None (already in messages)."""
|
||||||
result = self.adapter._resolve_system_instruction(
|
result = self.adapter._resolve_system_instruction(
|
||||||
"from context", "system", None, discard_context_system=False
|
"from context", None, discard_context_system=False
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertIsNone(result)
|
self.assertIsNone(result)
|
||||||
|
|||||||
Reference in New Issue
Block a user