feat: Add OpenAI Agents SDK integration service
- 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
This commit is contained in:
286
tests/test_openai_agent_service.py
Normal file
286
tests/test_openai_agent_service.py
Normal file
@@ -0,0 +1,286 @@
|
||||
#
|
||||
# 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__])
|
||||
Reference in New Issue
Block a user