Skip to content

Commit b0b8e08

Browse files
committed
[WIP] "Warm transfer" demo
1 parent 94bfacd commit b0b8e08

File tree

1 file changed

+58
-16
lines changed

1 file changed

+58
-16
lines changed

examples/static/warm_transfer.py

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"""
3737

3838
import asyncio
39-
import json
39+
from dataclasses import dataclass
4040
import os
4141
import sys
4242
from pathlib import Path
@@ -47,10 +47,12 @@
4747
from loguru import logger
4848

4949
from pipecat.audio.vad.silero import SileroVADAnalyzer
50+
from pipecat.frames.frames import ControlFrame, EndFrame, Frame
5051
from pipecat.pipeline.pipeline import Pipeline
5152
from pipecat.pipeline.runner import PipelineRunner
5253
from pipecat.pipeline.task import PipelineParams, PipelineTask
5354
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
55+
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
5456
from pipecat.services.cartesia import CartesiaTTSService
5557
from pipecat.services.deepgram import DeepgramSTTService
5658
from pipecat.services.google import GoogleLLMService
@@ -146,10 +148,14 @@ async def pre_transferring_to_human_agent(action: dict, flow_manager: FlowManage
146148
}
147149
)
148150

149-
# TODO: this should only run after LLM "transferring you to agent" speech is spoken
150-
async def post_transferring_to_human_agent(action: dict, flow_manager: FlowManager):
151+
async def queue_post_transferring_to_human_agent(action: dict, flow_manager: FlowManager):
152+
task = flow_manager.task
153+
await task.queue_frame(PostTransferringToHumanAgentFrame())
154+
155+
# NOTE: this isn't a "real" post-action because it needs to run after the "transferring you to a
156+
# human agent" speech is actually spoken, not just after sending the LLM the instruction to do so.
157+
async def post_transferring_to_human_agent(transport: DailyTransport):
151158
"""Post-action after starting transferring to the human agent."""
152-
transport: DailyTransport = flow_manager.transport
153159
customer_participant_id = get_customer_participant_id(transport=transport)
154160

155161
# Update the customer:
@@ -172,10 +178,15 @@ async def post_transferring_to_human_agent(action: dict, flow_manager: FlowManag
172178
}
173179
)
174180

175-
# TODO: this should only run after LLM "I'm patching you through" speech is spoken
176-
async def post_end_human_agent_conversation(action: dict, flow_manager: FlowManager):
177-
"""Configure the participants' settings (send and receive permissions) for when the customer is taken off of hold."""
178-
transport: DailyTransport = flow_manager.transport
181+
async def queue_post_end_human_agent_conversation(action: dict, flow_manager: FlowManager):
182+
task = flow_manager.task
183+
await task.queue_frame(PostEndHumanAgentConversationFrame())
184+
185+
# NOTE: this isn't a "real" post-action because it needs to run after the "I'm patching you through
186+
# to the customer" speech is actually spoken, not just after sending the LLM the instruction to do
187+
# so.
188+
async def post_end_human_agent_conversation(transport: DailyTransport):
189+
"""Post-action after starting to end the conversation with the human agent, when the agent is being patched through to the customer."""
179190
customer_participant_id = get_customer_participant_id(transport=transport)
180191
agent_participant_id = get_human_agent_participant_id(transport=transport)
181192

@@ -361,11 +372,11 @@ def create_transferring_to_human_agent_node() -> NodeConfig:
361372
handler=pre_transferring_to_human_agent
362373
)
363374
],
364-
# TODO: this should only run after LLM "transferring you to agent" speech is spoken
375+
# NOTE: "real" post action (post_transferring_to_human_agent) is triggered by CustomControlProcessor
365376
post_actions=[
366377
ActionConfig(
367-
type="post_transferring_to_human_agent",
368-
handler=post_transferring_to_human_agent
378+
type="queue_post_transferring_to_human_agent",
379+
handler=queue_post_transferring_to_human_agent
369380
)
370381
]
371382
)
@@ -431,13 +442,12 @@ def create_end_human_agent_conversation_node() -> NodeConfig:
431442
},
432443
],
433444
functions=[],
434-
# TODO: this should only run after LLM "I'm patching you through" speech is spoken
445+
# NOTE: "real" post action (post_end_human_agent_conversation) is triggered by CustomControlProcessor
435446
post_actions=[
436447
ActionConfig(
437-
type="post_end_human_agent_conversation",
438-
handler=post_end_human_agent_conversation
439-
),
440-
ActionConfig(type="end_conversation")
448+
type="queue_post_end_human_agent_conversation",
449+
handler=queue_post_end_human_agent_conversation
450+
)
441451
]
442452
)
443453

@@ -532,6 +542,36 @@ async def get_token(user_id: str, permissions: dict, daily_rest_helper: DailyRES
532542
))
533543
)
534544

545+
@dataclass
546+
class PostTransferringToHumanAgentFrame(ControlFrame):
547+
"""
548+
Indicates that the bot has finished speaking the "transferring you to human agent" speech.
549+
"""
550+
pass
551+
552+
@dataclass
553+
class PostEndHumanAgentConversationFrame(ControlFrame):
554+
"""
555+
Indicates that the bot has finished speaking the "I'm patching you through to the customer" speech.
556+
"""
557+
pass
558+
559+
class CustomControlProcessor(FrameProcessor):
560+
def __init__(self, transport: DailyTransport):
561+
super().__init__()
562+
self.__transport = transport
563+
564+
async def process_frame(self, frame: Frame, direction: FrameDirection):
565+
await super().process_frame(frame, direction)
566+
567+
if isinstance(frame, PostTransferringToHumanAgentFrame):
568+
await post_transferring_to_human_agent(transport=self.__transport)
569+
elif isinstance(frame, PostEndHumanAgentConversationFrame):
570+
await post_end_human_agent_conversation(transport=self.__transport)
571+
# TODO: how to trigger EndFrame() from here? we don't have reference to PipelineTask
572+
# await self.queue_frame(EndFrame())
573+
574+
await self.push_frame(frame, direction)
535575

536576
async def main():
537577
"""Main function to set up and run the bot."""
@@ -557,6 +597,7 @@ async def main():
557597
voice_id="820a3788-2b37-4d21-847a-b65d8a68c99a", # Salesman
558598
)
559599
llm = GoogleLLMService(api_key=os.getenv("GOOGLE_API_KEY"))
600+
custom_control_processor = CustomControlProcessor(transport=transport)
560601

561602
# Initialize context
562603
context = OpenAILLMContext()
@@ -572,6 +613,7 @@ async def main():
572613
tts,
573614
transport.output(),
574615
context_aggregator.assistant(),
616+
custom_control_processor
575617
]
576618
)
577619
task = PipelineTask(pipeline=pipeline, params=PipelineParams(allow_interruptions=True))

0 commit comments

Comments
 (0)