Add unit tests for OpenAILLMAdapter.get_llm_invocation_params(), focusing on messages specifically. Also, fix a bug in OpenAILLMAdapter (found thanks to the unit tests) where it wasn't "unwrapping" LLMSpecificMessages.
This commit is contained in:
@@ -24,6 +24,7 @@ from pipecat.processors.aggregators.llm_context import (
|
||||
LLMContext,
|
||||
LLMContextMessage,
|
||||
LLMContextToolChoice,
|
||||
LLMSpecificMessage,
|
||||
NotGiven,
|
||||
)
|
||||
|
||||
@@ -110,8 +111,15 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]):
|
||||
def _from_universal_context_messages(
|
||||
self, messages: List[LLMContextMessage]
|
||||
) -> List[ChatCompletionMessageParam]:
|
||||
# Just a pass-through: messages are already the right type
|
||||
return messages
|
||||
result = []
|
||||
for message in messages:
|
||||
if isinstance(message, LLMSpecificMessage):
|
||||
# Extract the actual message content from LLMSpecificMessage
|
||||
result.append(message.message)
|
||||
else:
|
||||
# Standard message, pass through unchanged
|
||||
result.append(message)
|
||||
return result
|
||||
|
||||
def _from_standard_tool_choice(
|
||||
self, tool_choice: LLMContextToolChoice | NotGiven
|
||||
|
||||
184
tests/test_get_llm_invocation_params.py
Normal file
184
tests/test_get_llm_invocation_params.py
Normal file
@@ -0,0 +1,184 @@
|
||||
#
|
||||
# Copyright (c) 2024–2025, Daily
|
||||
#
|
||||
# SPDX-License-Identifier: BSD 2-Clause License
|
||||
#
|
||||
|
||||
"""
|
||||
Unit tests for OpenAI adapter's get_llm_invocation_params() method.
|
||||
|
||||
These tests focus specifically on the "messages" field generation, ensuring:
|
||||
1. LLMStandardMessage objects are passed through unchanged
|
||||
2. LLMSpecificMessage objects with llm='openai' are included and their content extracted
|
||||
3. LLMSpecificMessage objects with llm != 'openai' are filtered out
|
||||
4. Complex message structures (like multi-part content) are preserved
|
||||
5. Edge cases like empty message lists are handled correctly
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from openai.types.chat import ChatCompletionMessage
|
||||
|
||||
from pipecat.adapters.services.open_ai_adapter import OpenAILLMAdapter
|
||||
from pipecat.processors.aggregators.llm_context import (
|
||||
LLMContext,
|
||||
LLMSpecificMessage,
|
||||
LLMStandardMessage,
|
||||
)
|
||||
|
||||
|
||||
class TestOpenAIGetLLMInvocationParams(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
"""Sets up a common adapter instance for all tests."""
|
||||
self.adapter = OpenAILLMAdapter()
|
||||
|
||||
def test_standard_messages_passed_through_unchanged(self):
|
||||
"""Test that LLMStandardMessage objects are passed through unchanged to OpenAI params."""
|
||||
# Create standard messages (OpenAI format)
|
||||
standard_messages: list[LLMStandardMessage] = [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Hello, how are you?"},
|
||||
{"role": "assistant", "content": "I'm doing well, thank you for asking!"},
|
||||
]
|
||||
|
||||
# Create context with these messages
|
||||
context = LLMContext(messages=standard_messages)
|
||||
|
||||
# Get invocation params
|
||||
params = self.adapter.get_llm_invocation_params(context)
|
||||
|
||||
# Verify messages are passed through unchanged
|
||||
self.assertEqual(params["messages"], standard_messages)
|
||||
self.assertEqual(len(params["messages"]), 3)
|
||||
|
||||
# Verify content matches exactly
|
||||
self.assertEqual(params["messages"][0]["content"], "You are a helpful assistant.")
|
||||
self.assertEqual(params["messages"][1]["content"], "Hello, how are you?")
|
||||
self.assertEqual(params["messages"][2]["content"], "I'm doing well, thank you for asking!")
|
||||
|
||||
def test_openai_specific_messages_included(self):
|
||||
"""Test that LLMSpecificMessage objects with llm='openai' are included."""
|
||||
# Create a mix of standard and OpenAI-specific messages
|
||||
messages = [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
LLMSpecificMessage(
|
||||
llm="openai", message={"role": "user", "content": "OpenAI specific message"}
|
||||
),
|
||||
{"role": "assistant", "content": "Standard response"},
|
||||
]
|
||||
|
||||
# Create context with these messages
|
||||
context = LLMContext(messages=messages)
|
||||
|
||||
# Get invocation params
|
||||
params = self.adapter.get_llm_invocation_params(context)
|
||||
|
||||
# Verify all messages are included (OpenAI-specific should be included)
|
||||
self.assertEqual(len(params["messages"]), 3)
|
||||
|
||||
# First message should be standard
|
||||
self.assertEqual(params["messages"][0]["content"], "You are a helpful assistant.")
|
||||
|
||||
# Second message should be the OpenAI-specific one
|
||||
self.assertEqual(
|
||||
params["messages"][1], {"role": "user", "content": "OpenAI specific message"}
|
||||
)
|
||||
|
||||
# Third message should be standard
|
||||
self.assertEqual(params["messages"][2]["content"], "Standard response")
|
||||
|
||||
def test_non_openai_specific_messages_filtered_out(self):
|
||||
"""Test that LLMSpecificMessage objects with llm != 'openai' are filtered out."""
|
||||
# Create messages with different LLM-specific ones
|
||||
messages = [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
LLMSpecificMessage(
|
||||
llm="anthropic", message={"role": "user", "content": "Anthropic specific message"}
|
||||
),
|
||||
LLMSpecificMessage(
|
||||
llm="gemini", message={"role": "user", "content": "Gemini specific message"}
|
||||
),
|
||||
{"role": "user", "content": "Standard user message"},
|
||||
LLMSpecificMessage(
|
||||
llm="openai", message={"role": "assistant", "content": "OpenAI specific response"}
|
||||
),
|
||||
]
|
||||
|
||||
# Create context with these messages
|
||||
context = LLMContext(messages=messages)
|
||||
|
||||
# Get invocation params
|
||||
params = self.adapter.get_llm_invocation_params(context)
|
||||
|
||||
# Should only include standard messages and OpenAI-specific ones
|
||||
# (3 total: system, standard user, openai assistant)
|
||||
self.assertEqual(len(params["messages"]), 3)
|
||||
|
||||
# Verify the correct messages are included
|
||||
self.assertEqual(params["messages"][0]["content"], "You are a helpful assistant.")
|
||||
self.assertEqual(params["messages"][1]["content"], "Standard user message")
|
||||
self.assertEqual(
|
||||
params["messages"][2], {"role": "assistant", "content": "OpenAI specific response"}
|
||||
)
|
||||
|
||||
def test_complex_message_content_preserved(self):
|
||||
"""Test that complex message content (like multi-part messages) is preserved."""
|
||||
# Create a message with complex content structure (text + image)
|
||||
complex_image_message = {
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "What's in this image?"},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD..."},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
# Create a message with multiple text blocks
|
||||
multi_text_message = {
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "text", "text": "Let me analyze this step by step:"},
|
||||
{"type": "text", "text": "1. First, I'll examine the visual elements"},
|
||||
{"type": "text", "text": "2. Then I'll provide my conclusions"},
|
||||
],
|
||||
}
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "You are a helpful assistant that can analyze images."},
|
||||
complex_image_message,
|
||||
multi_text_message,
|
||||
]
|
||||
|
||||
# Create context with these messages
|
||||
context = LLMContext(messages=messages)
|
||||
|
||||
# Get invocation params
|
||||
params = self.adapter.get_llm_invocation_params(context)
|
||||
|
||||
# Verify complex content is preserved
|
||||
self.assertEqual(len(params["messages"]), 3)
|
||||
self.assertEqual(params["messages"][1], complex_image_message)
|
||||
self.assertEqual(params["messages"][2], multi_text_message)
|
||||
|
||||
# Verify the image message structure is maintained
|
||||
image_content = params["messages"][1]["content"]
|
||||
self.assertIsInstance(image_content, list)
|
||||
self.assertEqual(len(image_content), 2)
|
||||
self.assertEqual(image_content[0]["type"], "text")
|
||||
self.assertEqual(image_content[1]["type"], "image_url")
|
||||
|
||||
# Verify the multi-text message structure is maintained
|
||||
text_content = params["messages"][2]["content"]
|
||||
self.assertIsInstance(text_content, list)
|
||||
self.assertEqual(len(text_content), 3)
|
||||
for i, text_block in enumerate(text_content):
|
||||
self.assertEqual(text_block["type"], "text")
|
||||
self.assertEqual(text_content[0]["text"], "Let me analyze this step by step:")
|
||||
self.assertEqual(text_content[1]["text"], "1. First, I'll examine the visual elements")
|
||||
self.assertEqual(text_content[2]["text"], "2. Then I'll provide my conclusions")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user