185 lines
7.5 KiB
Python
185 lines
7.5 KiB
Python
#
|
||
# 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()
|