- Create new OpenAIAgentService that integrates OpenAI Agents SDK with Pipecat - Support for agent loops, handoffs, guardrails, and session management - Add streaming and non-streaming response modes - Include comprehensive tool integration and error handling - Add optional dependency for openai-agents package - Create foundational examples showing basic usage and agent handoffs - Add comprehensive tests with mocked dependencies - Include detailed documentation and README Key features: - Real-time streaming responses compatible with Pipecat pipelines - Agent handoffs for specialized task delegation - Tool calling with automatic schema generation - Input/output guardrails for safety and validation - Session context management for conversation continuity - Built-in tracing and monitoring integration Examples: - 45-openai-agent-basic.py: Basic agent with weather and trivia tools - 46-openai-agent-handoffs.py: Multi-agent system with specialist handoffs
287 lines
8.4 KiB
Python
287 lines
8.4 KiB
Python
#
|
||
# Copyright (c) 2024–2025, Daily
|
||
#
|
||
# SPDX-License-Identifier: BSD 2-Clause License
|
||
#
|
||
|
||
"""Tests for OpenAI Agent service."""
|
||
|
||
import asyncio
|
||
import os
|
||
import sys
|
||
import unittest.mock
|
||
from unittest.mock import AsyncMock, MagicMock, patch
|
||
|
||
import pytest
|
||
|
||
# Add src to path for testing
|
||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||
|
||
from pipecat.frames.frames import (
|
||
CancelFrame,
|
||
EndFrame,
|
||
LLMFullResponseEndFrame,
|
||
LLMFullResponseStartFrame,
|
||
LLMTextFrame,
|
||
StartFrame,
|
||
TextFrame,
|
||
)
|
||
from pipecat.processors.frame_processor import FrameDirection
|
||
|
||
|
||
class MockAgent:
|
||
"""Mock Agent for testing."""
|
||
|
||
def __init__(self, name="Test Agent", instructions="Test instructions"):
|
||
self.name = name
|
||
self.instructions = instructions
|
||
self.tools = []
|
||
self.handoffs = []
|
||
|
||
|
||
class MockRunResult:
|
||
"""Mock RunResult for testing."""
|
||
|
||
def __init__(self, final_output="Test response"):
|
||
self.final_output = final_output
|
||
|
||
|
||
class MockStreamEvent:
|
||
"""Mock StreamEvent for testing."""
|
||
|
||
def __init__(self, event_type, data=None, item=None):
|
||
self.type = event_type
|
||
self.data = data
|
||
self.item = item
|
||
|
||
|
||
class MockMessageItem:
|
||
"""Mock message item for testing."""
|
||
|
||
def __init__(self, content="Test content"):
|
||
self.type = "message_output_item"
|
||
self.content = content
|
||
|
||
|
||
class MockRunner:
|
||
"""Mock Runner for testing."""
|
||
|
||
@staticmethod
|
||
async def run(agent, input_text, context=None):
|
||
return MockRunResult("Mocked response")
|
||
|
||
@staticmethod
|
||
def run_streamed(agent, input_text, context=None):
|
||
class MockStreamResult:
|
||
async def stream_events(self):
|
||
yield MockStreamEvent("raw_response_event", data=MagicMock(delta="Test "))
|
||
yield MockStreamEvent("raw_response_event", data=MagicMock(delta="response"))
|
||
yield MockStreamEvent(
|
||
"run_item_stream_event", item=MockMessageItem("Test response")
|
||
)
|
||
|
||
return MockStreamResult()
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_openai_agents():
|
||
"""Mock the OpenAI Agents SDK imports."""
|
||
with patch.dict(
|
||
"sys.modules",
|
||
{
|
||
"agents": MagicMock(),
|
||
"agents.stream_events": MagicMock(),
|
||
"agents.result": MagicMock(),
|
||
},
|
||
):
|
||
# Mock the classes and functions we need
|
||
mock_agent = MagicMock()
|
||
mock_agent.return_value = MockAgent()
|
||
|
||
mock_runner = MagicMock()
|
||
mock_runner.run = AsyncMock(return_value=MockRunResult())
|
||
mock_runner.run_streamed = MagicMock(return_value=MockRunner.run_streamed(None, None))
|
||
|
||
with (
|
||
patch("pipecat.services.openai_agent.agent_service.Agent", mock_agent),
|
||
patch("pipecat.services.openai_agent.agent_service.Runner", mock_runner),
|
||
):
|
||
yield {
|
||
"Agent": mock_agent,
|
||
"Runner": mock_runner,
|
||
}
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_openai_agent_service_init(mock_openai_agents):
|
||
"""Test OpenAI Agent service initialization."""
|
||
from pipecat.services.openai_agent.agent_service import OpenAIAgentService
|
||
|
||
service = OpenAIAgentService(
|
||
name="Test Agent", instructions="Test instructions", api_key="test-key", streaming=True
|
||
)
|
||
|
||
assert service.agent.name == "Test Agent"
|
||
assert service._streaming is True
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_openai_agent_service_process_text_frame_streaming(mock_openai_agents):
|
||
"""Test processing text frame with streaming enabled."""
|
||
from pipecat.services.openai_agent.agent_service import OpenAIAgentService
|
||
|
||
service = OpenAIAgentService(
|
||
name="Test Agent", instructions="Test instructions", api_key="test-key", streaming=True
|
||
)
|
||
|
||
# Mock the push_frame method to capture output
|
||
output_frames = []
|
||
|
||
async def mock_push_frame(frame, direction=FrameDirection.DOWNSTREAM):
|
||
output_frames.append(frame)
|
||
|
||
service.push_frame = mock_push_frame
|
||
|
||
# Process a text frame
|
||
text_frame = TextFrame("Hello, agent!")
|
||
await service.process_frame(text_frame, FrameDirection.DOWNSTREAM)
|
||
|
||
# Wait a bit for async processing
|
||
await asyncio.sleep(0.1)
|
||
|
||
# Check that appropriate frames were generated
|
||
assert len(output_frames) > 0
|
||
assert any(isinstance(frame, LLMFullResponseStartFrame) for frame in output_frames)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_openai_agent_service_process_text_frame_non_streaming(mock_openai_agents):
|
||
"""Test processing text frame with streaming disabled."""
|
||
from pipecat.services.openai_agent.agent_service import OpenAIAgentService
|
||
|
||
service = OpenAIAgentService(
|
||
name="Test Agent", instructions="Test instructions", api_key="test-key", streaming=False
|
||
)
|
||
|
||
# Mock the push_frame method to capture output
|
||
output_frames = []
|
||
|
||
async def mock_push_frame(frame, direction=FrameDirection.DOWNSTREAM):
|
||
output_frames.append(frame)
|
||
|
||
service.push_frame = mock_push_frame
|
||
|
||
# Process a text frame
|
||
text_frame = TextFrame("Hello, agent!")
|
||
await service.process_frame(text_frame, FrameDirection.DOWNSTREAM)
|
||
|
||
# Wait a bit for async processing
|
||
await asyncio.sleep(0.1)
|
||
|
||
# Check that appropriate frames were generated
|
||
assert len(output_frames) > 0
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_openai_agent_service_update_config(mock_openai_agents):
|
||
"""Test updating agent configuration."""
|
||
from pipecat.services.openai_agent.agent_service import OpenAIAgentService
|
||
|
||
service = OpenAIAgentService(
|
||
name="Test Agent", instructions="Test instructions", api_key="test-key"
|
||
)
|
||
|
||
# Update configuration
|
||
service.update_agent_config(
|
||
instructions="Updated instructions", model_config={"model": "gpt-4o", "temperature": 0.7}
|
||
)
|
||
|
||
assert service.agent.instructions == "Updated instructions"
|
||
assert service.agent.model_config["model"] == "gpt-4o"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_openai_agent_service_session_context(mock_openai_agents):
|
||
"""Test session context management."""
|
||
from pipecat.services.openai_agent.agent_service import OpenAIAgentService
|
||
|
||
service = OpenAIAgentService(
|
||
name="Test Agent",
|
||
instructions="Test instructions",
|
||
api_key="test-key",
|
||
session_config={"user_id": "test-user"},
|
||
)
|
||
|
||
# Get initial context
|
||
context = service.get_session_context()
|
||
assert context["user_id"] == "test-user"
|
||
|
||
# Update context
|
||
service.update_session_context({"session_id": "test-session"})
|
||
|
||
updated_context = service.get_session_context()
|
||
assert updated_context["user_id"] == "test-user"
|
||
assert updated_context["session_id"] == "test-session"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_openai_agent_service_add_tools(mock_openai_agents):
|
||
"""Test adding tools to the agent."""
|
||
from pipecat.services.openai_agent.agent_service import OpenAIAgentService
|
||
|
||
service = OpenAIAgentService(
|
||
name="Test Agent", instructions="Test instructions", api_key="test-key"
|
||
)
|
||
|
||
# Define a test tool
|
||
def test_tool():
|
||
return "test result"
|
||
|
||
# Add the tool
|
||
await service.add_tool(test_tool)
|
||
|
||
# Check if tool was added (this depends on the mock implementation)
|
||
assert hasattr(service.agent, "tools")
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_openai_agent_service_lifecycle(mock_openai_agents):
|
||
"""Test service lifecycle methods."""
|
||
from pipecat.frames.frames import CancelFrame, EndFrame, StartFrame
|
||
from pipecat.services.openai_agent.agent_service import OpenAIAgentService
|
||
|
||
service = OpenAIAgentService(
|
||
name="Test Agent", instructions="Test instructions", api_key="test-key"
|
||
)
|
||
|
||
# Test start
|
||
start_frame = StartFrame()
|
||
await service.start(start_frame)
|
||
|
||
# Test cancel
|
||
cancel_frame = CancelFrame()
|
||
await service.cancel(cancel_frame)
|
||
|
||
# Test stop
|
||
end_frame = EndFrame()
|
||
await service.stop(end_frame)
|
||
|
||
|
||
def test_openai_agent_service_import_error():
|
||
"""Test that import error is handled gracefully."""
|
||
# Mock the import to fail
|
||
with patch.dict("sys.modules", {"agents": None}):
|
||
with pytest.raises(Exception) as exc_info:
|
||
# This should trigger the import error
|
||
import importlib
|
||
|
||
import pipecat.services.openai_agent.agent_service
|
||
|
||
importlib.reload(pipecat.services.openai_agent.agent_service)
|
||
|
||
assert "Missing module" in str(exc_info.value)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
pytest.main([__file__])
|