mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-25 08:48:13 +02:00
fix: changes to update pipecat version to 0.0.100 (#122)
* feat: add stt evals * add smart turn as provider * chore: remove deprecations * chore: format files * fix: remove deprecated UserIdleProcessor * fix: remove deprecated TranscriptProcessor * chore: update pipecat submodule * feat: add evals visualisation * fix: trigger llm generation on client connected and pipeline started * chore: update pipecat * chore: update pipecat submodule * Add tests * fix: slow loading of workflow page * chore: update pipecat submodule * Show version after release * Fixes #99 * fix: provider check for websocket connection * Fixes #107 * Fix #96 * chore: fix documentation * fix: cloudonix campaign call error --------- Co-authored-by: Sabiha Khan <sabihak89@gmail.com>
This commit is contained in:
parent
a4367bd83b
commit
911c5ed416
104 changed files with 16919 additions and 597 deletions
|
|
@ -1,5 +1,5 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Any, Dict
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, Optional
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
|
@ -28,6 +28,87 @@ from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
|||
START_CALL_SYSTEM_PROMPT = "start_call_system_prompt"
|
||||
END_CALL_SYSTEM_PROMPT = "end_call_system_prompt"
|
||||
|
||||
# Default workflow definition for mocking database WorkflowModel
|
||||
DEFAULT_WORKFLOW_DEFINITION = {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "1",
|
||||
"type": "startCall",
|
||||
"position": {"x": 0, "y": 0},
|
||||
"data": {
|
||||
"name": "Start",
|
||||
"prompt": START_CALL_SYSTEM_PROMPT,
|
||||
"is_start": True,
|
||||
"allow_interrupt": False,
|
||||
"add_global_prompt": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"type": "endCall",
|
||||
"position": {"x": 0, "y": 200},
|
||||
"data": {
|
||||
"name": "End",
|
||||
"prompt": END_CALL_SYSTEM_PROMPT,
|
||||
"is_end": True,
|
||||
"allow_interrupt": False,
|
||||
"add_global_prompt": False,
|
||||
},
|
||||
},
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "1-2",
|
||||
"source": "1",
|
||||
"target": "2",
|
||||
"data": {"label": "End", "condition": "End the call"},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class MockWorkflowModel:
|
||||
"""Mock database WorkflowModel for testing.
|
||||
|
||||
This mimics the structure of the database WorkflowModel, not the parsed WorkflowGraph.
|
||||
Use this when mocking db_client.get_workflow() responses.
|
||||
"""
|
||||
|
||||
workflow_id: int = 1
|
||||
organization_id: int = 1
|
||||
workflow_configurations: Dict[str, Any] = field(default_factory=dict)
|
||||
workflow_definition_with_fallback: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.workflow_definition_with_fallback:
|
||||
self.workflow_definition_with_fallback = DEFAULT_WORKFLOW_DEFINITION.copy()
|
||||
|
||||
|
||||
@dataclass
|
||||
class MockWorkflowRun:
|
||||
"""Mock database WorkflowRun for testing.
|
||||
|
||||
Use this when mocking db_client.get_workflow_run() responses.
|
||||
"""
|
||||
|
||||
is_completed: bool = False
|
||||
initial_context: Dict[str, Any] = field(default_factory=dict)
|
||||
gathered_context: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MockUserConfig:
|
||||
"""Mock user configuration for testing.
|
||||
|
||||
Use this when mocking db_client.get_user_configurations() responses.
|
||||
"""
|
||||
|
||||
stt: Optional[Any] = None
|
||||
tts: Optional[Any] = None
|
||||
llm: Optional[Any] = None
|
||||
embeddings: Optional[Any] = None
|
||||
|
||||
|
||||
class MockTransportProcessor(FrameProcessor):
|
||||
"""
|
||||
|
|
@ -41,7 +122,7 @@ class MockTransportProcessor(FrameProcessor):
|
|||
|
||||
Args:
|
||||
emit_bot_speaking: If True, also emits BotSpeakingFrame on TTSAudioRawFrame
|
||||
which is needed for UserIdleProcessor to start conversation tracking. Default True.
|
||||
which is needed for user idle tracking to start conversation tracking. Default True.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -63,7 +144,7 @@ class MockTransportProcessor(FrameProcessor):
|
|||
BotStartedSpeakingFrame(), direction=FrameDirection.UPSTREAM
|
||||
)
|
||||
elif isinstance(frame, TTSAudioRawFrame):
|
||||
# Emit BotSpeakingFrame - this is what triggers the UserIdleProcessor
|
||||
# Emit BotSpeakingFrame - this is what triggers user idle tracking
|
||||
# to start conversation tracking
|
||||
if self._emit_bot_speaking:
|
||||
await self.push_frame(BotSpeakingFrame())
|
||||
|
|
@ -101,6 +182,24 @@ def mock_engine():
|
|||
return engine
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_workflow_model():
|
||||
"""Create a mock WorkflowModel for testing database responses."""
|
||||
return MockWorkflowModel()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_workflow_run():
|
||||
"""Create a mock WorkflowRun for testing database responses."""
|
||||
return MockWorkflowRun()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_user_config():
|
||||
"""Create a mock user configuration for testing."""
|
||||
return MockUserConfig()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_tools():
|
||||
"""Create sample mock tools for testing."""
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ from pipecat.processors.aggregators.llm_response_universal import (
|
|||
)
|
||||
from pipecat.tests import MockLLMService, MockTTSService
|
||||
|
||||
|
||||
# Define prompts for test nodes
|
||||
START_NODE_PROMPT = "Start Node System Prompt"
|
||||
AGENT_NODE_PROMPT = "Agent Node System Prompt"
|
||||
|
|
@ -143,14 +142,20 @@ class ContextCapturingMockLLM(MockLLMService):
|
|||
msg_copy = dict(msg)
|
||||
# Copy content to avoid reference issues
|
||||
if "content" in msg_copy:
|
||||
msg_copy["content"] = str(msg_copy["content"]) if msg_copy["content"] else None
|
||||
msg_copy["content"] = (
|
||||
str(msg_copy["content"]) if msg_copy["content"] else None
|
||||
)
|
||||
messages_snapshot.append(msg_copy)
|
||||
|
||||
self.captured_contexts.append({
|
||||
"step": self._current_step,
|
||||
"messages": messages_snapshot,
|
||||
"system_prompt": messages_snapshot[0]["content"] if messages_snapshot else None,
|
||||
})
|
||||
self.captured_contexts.append(
|
||||
{
|
||||
"step": self._current_step,
|
||||
"messages": messages_snapshot,
|
||||
"system_prompt": messages_snapshot[0]["content"]
|
||||
if messages_snapshot
|
||||
else None,
|
||||
}
|
||||
)
|
||||
|
||||
# Call parent implementation to stream the mock chunks
|
||||
return await super()._stream_chat_completions_universal_context(context)
|
||||
|
|
@ -306,14 +311,26 @@ class TestContextUpdateBeforeNextCompletion:
|
|||
transition completes. The test verifies the context is still correctly updated.
|
||||
"""
|
||||
# Step 0 (Start node): call collect_info to transition to agent
|
||||
step_0_chunks = MockLLMService.create_multiple_function_call_chunks([
|
||||
{"name": "collect_info", "arguments": {}, "tool_call_id": "call_transition_1"},
|
||||
])
|
||||
step_0_chunks = MockLLMService.create_multiple_function_call_chunks(
|
||||
[
|
||||
{
|
||||
"name": "collect_info",
|
||||
"arguments": {},
|
||||
"tool_call_id": "call_transition_1",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# Step 1 (Agent node): call end_call to transition to end
|
||||
step_1_chunks = MockLLMService.create_multiple_function_call_chunks([
|
||||
{"name": "end_call", "arguments": {}, "tool_call_id": "call_transition_2"},
|
||||
])
|
||||
step_1_chunks = MockLLMService.create_multiple_function_call_chunks(
|
||||
[
|
||||
{
|
||||
"name": "end_call",
|
||||
"arguments": {},
|
||||
"tool_call_id": "call_transition_2",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# Step 2 (End node): text response (end node has no outgoing edges)
|
||||
step_2_chunks = MockLLMService.create_text_chunks("Goodbye!")
|
||||
|
|
@ -327,7 +344,7 @@ class TestContextUpdateBeforeNextCompletion:
|
|||
)
|
||||
|
||||
# Should have been called 3 times: start node, agent node, end node
|
||||
assert llm.get_current_step() == 2, (
|
||||
assert llm.get_current_step() == 3, (
|
||||
f"Expected 3 LLM generations (start, agent, end), got {llm.get_current_step()}"
|
||||
)
|
||||
|
||||
|
|
@ -376,14 +393,26 @@ class TestContextUpdateBeforeNextCompletion:
|
|||
is handled correctly.
|
||||
"""
|
||||
# Step 0 (Start node): call collect_info to transition to agent
|
||||
step_0_chunks = MockLLMService.create_multiple_function_call_chunks([
|
||||
{"name": "collect_info", "arguments": {}, "tool_call_id": "call_transition_1"},
|
||||
])
|
||||
step_0_chunks = MockLLMService.create_multiple_function_call_chunks(
|
||||
[
|
||||
{
|
||||
"name": "collect_info",
|
||||
"arguments": {},
|
||||
"tool_call_id": "call_transition_1",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# Step 1 (Agent node): call end_call to transition to end
|
||||
step_1_chunks = MockLLMService.create_multiple_function_call_chunks([
|
||||
{"name": "end_call", "arguments": {}, "tool_call_id": "call_transition_2"},
|
||||
])
|
||||
step_1_chunks = MockLLMService.create_multiple_function_call_chunks(
|
||||
[
|
||||
{
|
||||
"name": "end_call",
|
||||
"arguments": {},
|
||||
"tool_call_id": "call_transition_2",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# Step 2 (End node): text response
|
||||
step_2_chunks = MockLLMService.create_text_chunks("Goodbye!")
|
||||
|
|
@ -397,7 +426,7 @@ class TestContextUpdateBeforeNextCompletion:
|
|||
)
|
||||
|
||||
# Verify all three nodes were executed
|
||||
assert llm.get_current_step() == 2, (
|
||||
assert llm.get_current_step() == 3, (
|
||||
f"Expected 3 steps, got {llm.get_current_step()}"
|
||||
)
|
||||
|
||||
|
|
@ -408,8 +437,7 @@ class TestContextUpdateBeforeNextCompletion:
|
|||
assert AGENT_NODE_PROMPT in llm.get_system_prompt_at_step(1)
|
||||
|
||||
# Step 2: End node - should have end prompt
|
||||
# FIXME - EndFrame is getting processed before LLMContextFrame
|
||||
# assert END_NODE_PROMPT in llm.get_system_prompt_at_step(2)
|
||||
assert END_NODE_PROMPT in llm.get_system_prompt_at_step(2)
|
||||
|
||||
# Verify each subsequent step has the previous tool results
|
||||
step_1_ctx = llm.get_context_at_step(1)
|
||||
|
|
@ -423,14 +451,14 @@ class TestContextUpdateBeforeNextCompletion:
|
|||
assert step_1_has_tool, "Agent node should see collect_info tool result"
|
||||
|
||||
# Step 2 should have tool results from both transitions
|
||||
# FIXME - EndFrame is getting processed before LLMContextFrame
|
||||
# step_2_tool_messages = [
|
||||
# msg for msg in step_2_ctx["messages"]
|
||||
# if msg.get("role") == "tool" or msg.get("tool_call_id")
|
||||
# ]
|
||||
# assert len(step_2_tool_messages) >= 2, (
|
||||
# f"End node should see at least 2 tool results, got {len(step_2_tool_messages)}"
|
||||
# )
|
||||
step_2_tool_messages = [
|
||||
msg
|
||||
for msg in step_2_ctx["messages"]
|
||||
if msg.get("role") == "tool" or msg.get("tool_call_id")
|
||||
]
|
||||
assert len(step_2_tool_messages) >= 2, (
|
||||
f"End node should see at least 2 tool results, got {len(step_2_tool_messages)}"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_context_messages_preserve_conversation_history(
|
||||
|
|
@ -444,14 +472,26 @@ class TestContextUpdateBeforeNextCompletion:
|
|||
- Tool call messages and results
|
||||
"""
|
||||
# Step 0 (Start node): call collect_info to transition to agent
|
||||
step_0_chunks = MockLLMService.create_multiple_function_call_chunks([
|
||||
{"name": "collect_info", "arguments": {}, "tool_call_id": "call_transition_1"},
|
||||
])
|
||||
step_0_chunks = MockLLMService.create_multiple_function_call_chunks(
|
||||
[
|
||||
{
|
||||
"name": "collect_info",
|
||||
"arguments": {},
|
||||
"tool_call_id": "call_transition_1",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# Step 1 (Agent node): call end_call to transition to end
|
||||
step_1_chunks = MockLLMService.create_multiple_function_call_chunks([
|
||||
{"name": "end_call", "arguments": {}, "tool_call_id": "call_transition_2"},
|
||||
])
|
||||
step_1_chunks = MockLLMService.create_multiple_function_call_chunks(
|
||||
[
|
||||
{
|
||||
"name": "end_call",
|
||||
"arguments": {},
|
||||
"tool_call_id": "call_transition_2",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# Step 2 (End node): text response
|
||||
step_2_chunks = MockLLMService.create_text_chunks("Goodbye!")
|
||||
|
|
@ -472,18 +512,15 @@ class TestContextUpdateBeforeNextCompletion:
|
|||
assert len(ctx_1["messages"]) > len(ctx_0["messages"]), (
|
||||
"Context at step 1 should have more messages than step 0"
|
||||
)
|
||||
|
||||
# FIXME
|
||||
# assert len(ctx_2["messages"]) > len(ctx_1["messages"]), (
|
||||
# "Context at step 2 should have more messages than step 1"
|
||||
# )
|
||||
|
||||
assert len(ctx_2["messages"]) > len(ctx_1["messages"]), (
|
||||
"Context at step 2 should have more messages than step 1"
|
||||
)
|
||||
|
||||
# Verify assistant messages are accumulated
|
||||
# FIXME
|
||||
# assistant_messages_at_step_2 = [
|
||||
# msg for msg in ctx_2["messages"]
|
||||
# if msg.get("role") == "assistant"
|
||||
# ]
|
||||
# assert len(assistant_messages_at_step_2) >= 2, (
|
||||
# "Should have at least 2 assistant messages by step 2"
|
||||
# )
|
||||
assistant_messages_at_step_2 = [
|
||||
msg for msg in ctx_2["messages"] if msg.get("role") == "assistant"
|
||||
]
|
||||
assert len(assistant_messages_at_step_2) >= 2, (
|
||||
"Should have at least 2 assistant messages by step 2"
|
||||
)
|
||||
|
|
|
|||
100
api/tests/test_pipeline_cancellation.py
Normal file
100
api/tests/test_pipeline_cancellation.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import asyncio
|
||||
|
||||
import pytest
|
||||
from loguru import logger
|
||||
|
||||
from pipecat.frames.frames import (
|
||||
EndTaskFrame,
|
||||
Frame,
|
||||
InterruptionTaskFrame,
|
||||
LLMRunFrame,
|
||||
)
|
||||
from pipecat.pipeline.base_task import PipelineTaskParams
|
||||
from pipecat.pipeline.pipeline import Pipeline
|
||||
from pipecat.pipeline.task import PipelineTask
|
||||
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||
|
||||
|
||||
class MockTransport(FrameProcessor):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||
await super().process_frame(frame, direction)
|
||||
await self.push_frame(frame, direction)
|
||||
|
||||
|
||||
class BusyWaitProcessor(FrameProcessor):
|
||||
def __init__(self, wait_time=5.0, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._wait_time = wait_time
|
||||
|
||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||
await super().process_frame(frame, direction)
|
||||
if isinstance(frame, LLMRunFrame):
|
||||
# Simulate a delay, which can happen sometimes due to slow LLM Inferencing or
|
||||
# other reasons
|
||||
try:
|
||||
logger.debug(f"{self} sleeping with frame: {frame}")
|
||||
await asyncio.sleep(5)
|
||||
logger.debug(f"{self} woke up with frame: {frame}")
|
||||
except asyncio.CancelledError:
|
||||
logger.debug(f"{self} was cancelled")
|
||||
raise
|
||||
await self.push_frame(frame, direction)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_interruption_with_blocked_end_frame():
|
||||
busy_wait_processor = BusyWaitProcessor(wait_time=5)
|
||||
transport = MockTransport()
|
||||
pipeline = Pipeline([transport, busy_wait_processor])
|
||||
|
||||
task = PipelineTask(pipeline)
|
||||
|
||||
async def run_pipeline():
|
||||
loop = asyncio.get_running_loop()
|
||||
params = PipelineTaskParams(loop=loop)
|
||||
await task.run(params=params)
|
||||
|
||||
async def queue_frame():
|
||||
await task.queue_frames([LLMRunFrame()])
|
||||
|
||||
# Send EndTaskFrame to simulate EndFrame
|
||||
await asyncio.sleep(0.1)
|
||||
await transport.queue_frame(EndTaskFrame(), direction=FrameDirection.UPSTREAM)
|
||||
|
||||
# Simulate an Interruption, which can happen if the user
|
||||
# has started to speak
|
||||
await asyncio.sleep(0.1)
|
||||
await transport.queue_frame(
|
||||
InterruptionTaskFrame(), direction=FrameDirection.UPSTREAM
|
||||
)
|
||||
|
||||
# Create tasks explicitly for better control
|
||||
pipeline_task = asyncio.create_task(run_pipeline())
|
||||
queue_task = asyncio.create_task(queue_frame())
|
||||
|
||||
# Wait with timeout
|
||||
done, pending = await asyncio.wait(
|
||||
[pipeline_task, queue_task],
|
||||
timeout=1.0,
|
||||
return_when=asyncio.ALL_COMPLETED,
|
||||
)
|
||||
|
||||
# If there are pending tasks, we timed out
|
||||
if pending:
|
||||
# Cancel all pending tasks
|
||||
for t in pending:
|
||||
t.cancel()
|
||||
|
||||
# Give limited time for cleanup, then move on regardless
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
asyncio.gather(*pending, return_exceptions=True),
|
||||
timeout=1.0,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
pass # Cleanup took too long, continue anyway
|
||||
|
||||
pytest.fail("Test timed out after 1 second")
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
"""
|
||||
Simulates a user idle condition and tests the behaviour
|
||||
of the user idle processor.
|
||||
of the user idle handler.
|
||||
|
||||
This module tests the behavior when the user becomes idle during a conversation,
|
||||
ensuring the UserIdleProcessor properly triggers the callback and the engine
|
||||
handles it correctly.
|
||||
ensuring the user_idle_timeout in LLMUserAggregatorParams properly triggers
|
||||
the on_user_turn_idle event and the engine handles it correctly.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
|
@ -23,8 +23,8 @@ from pipecat.processors.aggregators.llm_context import LLMContext
|
|||
from pipecat.processors.aggregators.llm_response import LLMAssistantAggregatorParams
|
||||
from pipecat.processors.aggregators.llm_response_universal import (
|
||||
LLMContextAggregatorPair,
|
||||
LLMUserAggregatorParams,
|
||||
)
|
||||
from pipecat.processors.user_idle_processor import UserIdleProcessor
|
||||
from pipecat.tests import MockLLMService, MockTTSService
|
||||
|
||||
|
||||
|
|
@ -32,8 +32,8 @@ async def run_pipeline_with_user_idle(
|
|||
workflow: WorkflowGraph,
|
||||
user_idle_timeout: float = 0.2,
|
||||
mock_steps: list | None = None,
|
||||
) -> tuple[MockLLMService, LLMContext, UserIdleProcessor]:
|
||||
"""Run a pipeline with UserIdleProcessor and simulate user idle condition.
|
||||
) -> tuple[MockLLMService, LLMContext]:
|
||||
"""Run a pipeline with user_idle_timeout and simulate user idle condition.
|
||||
|
||||
Args:
|
||||
workflow: The workflow graph to use.
|
||||
|
|
@ -42,7 +42,7 @@ async def run_pipeline_with_user_idle(
|
|||
defaults to a simple greeting followed by text responses.
|
||||
|
||||
Returns:
|
||||
Tuple of (MockLLMService, LLMContext, UserIdleProcessor) for assertions.
|
||||
Tuple of (MockLLMService, LLMContext) for assertions.
|
||||
"""
|
||||
# Create mock responses - bot will speak first, then respond to idle prompts
|
||||
# Step 1: Initial greeting
|
||||
|
|
@ -64,10 +64,11 @@ async def run_pipeline_with_user_idle(
|
|||
# Create LLM context
|
||||
context = LLMContext()
|
||||
|
||||
# Create context aggregator with both user and assistant aggregators
|
||||
# Create context aggregator with user_idle_timeout in user_params
|
||||
assistant_params = LLMAssistantAggregatorParams(expect_stripped_words=True)
|
||||
user_params = LLMUserAggregatorParams(user_idle_timeout=user_idle_timeout)
|
||||
context_aggregator = LLMContextAggregatorPair(
|
||||
context, assistant_params=assistant_params
|
||||
context, assistant_params=assistant_params, user_params=user_params
|
||||
)
|
||||
user_context_aggregator = context_aggregator.user()
|
||||
assistant_context_aggregator = context_aggregator.assistant()
|
||||
|
|
@ -81,18 +82,20 @@ async def run_pipeline_with_user_idle(
|
|||
workflow_run_id=1,
|
||||
)
|
||||
|
||||
# Create UserIdleProcessor with engine's callback and a short timeout
|
||||
user_idle_processor = UserIdleProcessor(
|
||||
callback=engine.create_user_idle_callback(),
|
||||
timeout=user_idle_timeout,
|
||||
)
|
||||
# Register user idle event handlers
|
||||
user_idle_handler = engine.create_user_idle_handler()
|
||||
|
||||
# Build the pipeline:
|
||||
# llm -> mock_transport -> user_idle_processor -> assistant_context_aggregator
|
||||
# The user_context_aggregator would normally be at the start for user input
|
||||
@user_context_aggregator.event_handler("on_user_turn_idle")
|
||||
async def on_user_turn_idle(aggregator):
|
||||
await user_idle_handler.handle_idle(aggregator)
|
||||
|
||||
@user_context_aggregator.event_handler("on_user_turn_started")
|
||||
async def on_user_turn_started(aggregator, strategy):
|
||||
user_idle_handler.reset()
|
||||
|
||||
# Build the pipeline
|
||||
pipeline = Pipeline(
|
||||
[
|
||||
user_idle_processor,
|
||||
user_context_aggregator,
|
||||
llm,
|
||||
tts,
|
||||
|
|
@ -154,11 +157,11 @@ async def run_pipeline_with_user_idle(
|
|||
return_exceptions=True,
|
||||
)
|
||||
|
||||
return llm, context, user_idle_processor
|
||||
return llm, context
|
||||
|
||||
|
||||
class TestUserIdleHandler:
|
||||
"""Test user idle handling through PipecatEngine and UserIdleProcessor."""
|
||||
"""Test user idle handling through PipecatEngine and UserIdleHandler."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_idle_triggers_callback(self, simple_workflow: WorkflowGraph):
|
||||
|
|
@ -167,13 +170,13 @@ class TestUserIdleHandler:
|
|||
This test verifies that when:
|
||||
1. The bot starts speaking (triggers conversation tracking)
|
||||
2. No user input is received for the timeout period
|
||||
3. The UserIdleProcessor triggers the idle callback
|
||||
3. The on_user_turn_idle event triggers the idle handler
|
||||
|
||||
The engine's user idle callback should:
|
||||
The engine's user idle handler should:
|
||||
- First retry: Send a message asking if user is still there
|
||||
- Second retry: Send goodbye message and end the call
|
||||
"""
|
||||
llm, context, user_idle_processor = await run_pipeline_with_user_idle(
|
||||
llm, context = await run_pipeline_with_user_idle(
|
||||
workflow=simple_workflow,
|
||||
user_idle_timeout=0.2, # Short timeout for faster test
|
||||
)
|
||||
|
|
@ -220,7 +223,7 @@ class TestUserIdleHandler:
|
|||
MockLLMService.create_text_chunks("Response 3"),
|
||||
]
|
||||
|
||||
llm, context, user_idle_processor = await run_pipeline_with_user_idle(
|
||||
llm, context = await run_pipeline_with_user_idle(
|
||||
workflow=three_node_workflow,
|
||||
user_idle_timeout=0.2,
|
||||
mock_steps=mock_steps,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue