Add support for OpenTelemetry tracing (#1729)
* Also added TurnTrackingObserver, TurnTraceObserver, foundational 29, open-telemetry-example
This commit is contained in:
261
tests/test_turn_tracking_observer.py
Normal file
261
tests/test_turn_tracking_observer.py
Normal file
@@ -0,0 +1,261 @@
|
||||
#
|
||||
# Copyright (c) 2024-2025 Daily
|
||||
#
|
||||
# SPDX-License-Identifier: BSD 2-Clause License
|
||||
#
|
||||
|
||||
import unittest
|
||||
|
||||
from pipecat.frames.frames import (
|
||||
BotStartedSpeakingFrame,
|
||||
BotStoppedSpeakingFrame,
|
||||
UserStartedSpeakingFrame,
|
||||
UserStoppedSpeakingFrame,
|
||||
)
|
||||
from pipecat.observers.turn_tracking_observer import TurnTrackingObserver
|
||||
from pipecat.processors.filters.identity_filter import IdentityFilter
|
||||
from pipecat.tests.utils import SleepFrame, run_test
|
||||
|
||||
|
||||
class TestTurnTrackingObserver(unittest.IsolatedAsyncioTestCase):
|
||||
"""Tests for TurnTrackingObserver."""
|
||||
|
||||
async def test_normal_conversation_flow(self):
|
||||
"""Test a normal conversation with two complete turns."""
|
||||
# Create observer with a short timeout
|
||||
turn_observer = TurnTrackingObserver(turn_end_timeout_secs=0.2)
|
||||
|
||||
# Create identity filter (passes all frames through)
|
||||
processor = IdentityFilter()
|
||||
|
||||
# Record start/end events with turn numbers
|
||||
turn_events = []
|
||||
|
||||
@turn_observer.event_handler("on_turn_started")
|
||||
async def on_turn_started(observer, turn_number):
|
||||
turn_events.append(f"Turn {turn_number} started")
|
||||
|
||||
@turn_observer.event_handler("on_turn_ended")
|
||||
async def on_turn_ended(observer, turn_number, duration, was_interrupted):
|
||||
turn_events.append(f"Turn {turn_number} ended (interrupted: {was_interrupted})")
|
||||
|
||||
frames_to_send = [
|
||||
# Turn 1
|
||||
UserStartedSpeakingFrame(),
|
||||
UserStoppedSpeakingFrame(),
|
||||
BotStartedSpeakingFrame(),
|
||||
BotStoppedSpeakingFrame(),
|
||||
SleepFrame(sleep=0.05), # < 0.2 seconds turn_end_timeout
|
||||
# Turn 2
|
||||
UserStartedSpeakingFrame(), # New turn starts
|
||||
UserStoppedSpeakingFrame(),
|
||||
BotStartedSpeakingFrame(),
|
||||
BotStoppedSpeakingFrame(),
|
||||
# Add a sleep frame to allow turn timeout to occur
|
||||
SleepFrame(sleep=0.4), # > 0.2 seconds turn_end_timeout
|
||||
]
|
||||
|
||||
expected_down_frames = [
|
||||
UserStartedSpeakingFrame,
|
||||
UserStoppedSpeakingFrame,
|
||||
BotStartedSpeakingFrame,
|
||||
BotStoppedSpeakingFrame,
|
||||
UserStartedSpeakingFrame,
|
||||
UserStoppedSpeakingFrame,
|
||||
BotStartedSpeakingFrame,
|
||||
BotStoppedSpeakingFrame,
|
||||
]
|
||||
|
||||
await run_test(
|
||||
processor,
|
||||
frames_to_send=frames_to_send,
|
||||
expected_down_frames=expected_down_frames,
|
||||
observers=[turn_observer],
|
||||
)
|
||||
|
||||
# Verify turn events
|
||||
expected_events = [
|
||||
"Turn 1 started",
|
||||
"Turn 1 ended (interrupted: False)",
|
||||
"Turn 2 started",
|
||||
"Turn 2 ended (interrupted: False)",
|
||||
]
|
||||
self.assertEqual(turn_events, expected_events)
|
||||
self.assertEqual(turn_observer._turn_count, 2)
|
||||
|
||||
async def test_user_speaks_twice_before_bot(self):
|
||||
"""Test when user speaks twice before bot responds, should be same turn."""
|
||||
# Create observer with a short timeout
|
||||
turn_observer = TurnTrackingObserver(turn_end_timeout_secs=0.2)
|
||||
|
||||
# Create identity filter (passes all frames through)
|
||||
processor = IdentityFilter()
|
||||
|
||||
# Record start/end events with turn numbers
|
||||
turn_events = []
|
||||
|
||||
@turn_observer.event_handler("on_turn_started")
|
||||
async def on_turn_started(observer, turn_number):
|
||||
turn_events.append(f"Turn {turn_number} started")
|
||||
|
||||
@turn_observer.event_handler("on_turn_ended")
|
||||
async def on_turn_ended(observer, turn_number, duration, was_interrupted):
|
||||
turn_events.append(f"Turn {turn_number} ended (interrupted: {was_interrupted})")
|
||||
|
||||
frames_to_send = [
|
||||
# Turn 1 - User speaks twice before bot responds
|
||||
UserStartedSpeakingFrame(),
|
||||
UserStoppedSpeakingFrame(),
|
||||
UserStartedSpeakingFrame(), # Second user speaking event should not start a new turn
|
||||
UserStoppedSpeakingFrame(),
|
||||
BotStartedSpeakingFrame(),
|
||||
BotStoppedSpeakingFrame(),
|
||||
# Turn 2
|
||||
UserStartedSpeakingFrame(),
|
||||
UserStoppedSpeakingFrame(),
|
||||
BotStartedSpeakingFrame(),
|
||||
BotStoppedSpeakingFrame(),
|
||||
# Add a sleep frame to allow turn timeout to occur
|
||||
SleepFrame(sleep=0.4), # > 0.2 seconds turn_end_timeout
|
||||
]
|
||||
|
||||
expected_down_frames = [
|
||||
UserStartedSpeakingFrame,
|
||||
UserStoppedSpeakingFrame,
|
||||
UserStartedSpeakingFrame,
|
||||
UserStoppedSpeakingFrame,
|
||||
BotStartedSpeakingFrame,
|
||||
BotStoppedSpeakingFrame,
|
||||
UserStartedSpeakingFrame,
|
||||
UserStoppedSpeakingFrame,
|
||||
BotStartedSpeakingFrame,
|
||||
BotStoppedSpeakingFrame,
|
||||
]
|
||||
|
||||
await run_test(
|
||||
processor,
|
||||
frames_to_send=frames_to_send,
|
||||
expected_down_frames=expected_down_frames,
|
||||
observers=[turn_observer],
|
||||
)
|
||||
|
||||
# Verify turn events - should only see two turns despite user speaking twice
|
||||
expected_events = [
|
||||
"Turn 1 started",
|
||||
"Turn 1 ended (interrupted: False)",
|
||||
"Turn 2 started",
|
||||
"Turn 2 ended (interrupted: False)",
|
||||
]
|
||||
self.assertEqual(turn_events, expected_events)
|
||||
self.assertEqual(turn_observer._turn_count, 2)
|
||||
|
||||
async def test_user_interrupts_bot(self):
|
||||
"""Test when user interrupts bot speaking, should end current turn and start new one."""
|
||||
# Create observer with a short timeout
|
||||
turn_observer = TurnTrackingObserver(turn_end_timeout_secs=0.2)
|
||||
|
||||
# Create identity filter (passes all frames through)
|
||||
processor = IdentityFilter()
|
||||
|
||||
# Record start/end events with turn numbers
|
||||
turn_events = []
|
||||
|
||||
@turn_observer.event_handler("on_turn_started")
|
||||
async def on_turn_started(observer, turn_number):
|
||||
turn_events.append(f"Turn {turn_number} started")
|
||||
|
||||
@turn_observer.event_handler("on_turn_ended")
|
||||
async def on_turn_ended(observer, turn_number, duration, was_interrupted):
|
||||
turn_events.append(f"Turn {turn_number} ended (interrupted: {was_interrupted})")
|
||||
|
||||
frames_to_send = [
|
||||
# Turn 1
|
||||
UserStartedSpeakingFrame(),
|
||||
UserStoppedSpeakingFrame(),
|
||||
BotStartedSpeakingFrame(),
|
||||
# Interruption here - user starts speaking while bot is still speaking
|
||||
UserStartedSpeakingFrame(), # This should end Turn 1 and start Turn 2
|
||||
SleepFrame(sleep=0.4), # > 0.2 seconds turn_end_timeout
|
||||
]
|
||||
|
||||
expected_down_frames = [
|
||||
UserStartedSpeakingFrame,
|
||||
UserStoppedSpeakingFrame,
|
||||
BotStartedSpeakingFrame,
|
||||
UserStartedSpeakingFrame,
|
||||
]
|
||||
|
||||
await run_test(
|
||||
processor,
|
||||
frames_to_send=frames_to_send,
|
||||
expected_down_frames=expected_down_frames,
|
||||
observers=[turn_observer],
|
||||
)
|
||||
|
||||
# Verify turn events - should see Turn 1 interrupted
|
||||
expected_events = [
|
||||
"Turn 1 started",
|
||||
"Turn 1 ended (interrupted: True)", # First turn was interrupted
|
||||
"Turn 2 started", # New turn started after interruption
|
||||
]
|
||||
self.assertEqual(turn_events, expected_events)
|
||||
self.assertEqual(turn_observer._turn_count, 2)
|
||||
|
||||
async def test_bot_starts_stops_multiple_times(self):
|
||||
"""Test that multiple bot start/stop frames in the same turn work correctly."""
|
||||
# Create observer with a short timeout
|
||||
turn_observer = TurnTrackingObserver(turn_end_timeout_secs=0.2)
|
||||
|
||||
# Create identity filter (passes all frames through)
|
||||
processor = IdentityFilter()
|
||||
|
||||
turn_events = []
|
||||
|
||||
@turn_observer.event_handler("on_turn_started")
|
||||
async def on_turn_started(observer, turn_number):
|
||||
turn_events.append(f"Turn {turn_number} started")
|
||||
|
||||
@turn_observer.event_handler("on_turn_ended")
|
||||
async def on_turn_ended(observer, turn_number, duration, was_interrupted):
|
||||
turn_events.append(f"Turn {turn_number} ended (interrupted: {was_interrupted})")
|
||||
|
||||
frames_to_send = [
|
||||
# Start turn with user speaking
|
||||
UserStartedSpeakingFrame(),
|
||||
UserStoppedSpeakingFrame(),
|
||||
# Bot speaks, stops, speaks again (simulating HTTP TTS or function calls)
|
||||
BotStartedSpeakingFrame(),
|
||||
BotStoppedSpeakingFrame(),
|
||||
BotStartedSpeakingFrame(), # Bot speaks again, should not end turn
|
||||
BotStoppedSpeakingFrame(),
|
||||
# Add a sleep frame to allow turn timeout to occur
|
||||
SleepFrame(sleep=0.4), # > 0.2 seconds turn_end_timeout
|
||||
]
|
||||
|
||||
expected_down_frames = [
|
||||
UserStartedSpeakingFrame,
|
||||
UserStoppedSpeakingFrame,
|
||||
BotStartedSpeakingFrame,
|
||||
BotStoppedSpeakingFrame,
|
||||
BotStartedSpeakingFrame,
|
||||
BotStoppedSpeakingFrame,
|
||||
]
|
||||
|
||||
await run_test(
|
||||
processor,
|
||||
frames_to_send=frames_to_send,
|
||||
expected_down_frames=expected_down_frames,
|
||||
observers=[turn_observer],
|
||||
)
|
||||
|
||||
# Should only be one turn with a normal end
|
||||
expected_events = [
|
||||
"Turn 1 started",
|
||||
"Turn 1 ended (interrupted: False)",
|
||||
]
|
||||
self.assertEqual(turn_events, expected_events)
|
||||
self.assertEqual(turn_observer._turn_count, 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user