Refactor callback to event
This commit is contained in:
@@ -50,27 +50,6 @@ transport_params = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def handle_voicemail(processor):
|
|
||||||
"""Called when a voicemail is detected.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
processor: The VoicemailProcessor instance. processor.push_frame() is
|
|
||||||
available to push frames.
|
|
||||||
"""
|
|
||||||
logger.info("Voicemail detected! Leaving a message...")
|
|
||||||
|
|
||||||
# Push frames using standard Pipecat pattern
|
|
||||||
await processor.push_frame(
|
|
||||||
TTSSpeakFrame(
|
|
||||||
"Hello, this is Jamie calling about your appointment. Please call me back at 555-0123 when you get this."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# NOTE: A common pattern is to end pipeline after the voicemail is left.
|
|
||||||
# Uncomment the following line to end the pipeline after leaving the voicemail.
|
|
||||||
# await processor.push_frame(EndTaskFrame(), FrameDirection.UPSTREAM)
|
|
||||||
|
|
||||||
|
|
||||||
async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
||||||
logger.info(f"Starting bot")
|
logger.info(f"Starting bot")
|
||||||
|
|
||||||
@@ -84,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
|||||||
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
classifier_llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
classifier_llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
|
|
||||||
voicemail = VoicemailDetector(llm=classifier_llm, on_voicemail_detected=handle_voicemail)
|
voicemail = VoicemailDetector(llm=classifier_llm)
|
||||||
|
|
||||||
messages = [
|
messages = [
|
||||||
{
|
{
|
||||||
@@ -128,6 +107,21 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
|||||||
logger.info(f"Client disconnected")
|
logger.info(f"Client disconnected")
|
||||||
await task.cancel()
|
await task.cancel()
|
||||||
|
|
||||||
|
@voicemail.event_handler("on_voicemail_detected")
|
||||||
|
async def handle_voicemail(processor):
|
||||||
|
logger.info("Voicemail detected! Leaving a message...")
|
||||||
|
|
||||||
|
# Push frames using standard Pipecat pattern
|
||||||
|
await processor.push_frame(
|
||||||
|
TTSSpeakFrame(
|
||||||
|
"Hello, this is Jamie calling about your appointment. Please call me back at 555-0123 when you get this."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# NOTE: A common pattern is to end pipeline after the voicemail is left.
|
||||||
|
# Uncomment the following line to end the pipeline after leaving the voicemail.
|
||||||
|
# await processor.push_frame(EndTaskFrame(), FrameDirection.UPSTREAM)
|
||||||
|
|
||||||
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
|
runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
|
||||||
|
|
||||||
await runner.run(task)
|
await runner.run(task)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ a bot needs to determine if a human answered or if the call went to voicemail.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Awaitable, Callable, List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ class ConversationGate(FrameProcessor):
|
|||||||
|
|
||||||
|
|
||||||
class ClassificationProcessor(FrameProcessor):
|
class ClassificationProcessor(FrameProcessor):
|
||||||
"""Processor that handles LLM classification responses and triggers callbacks.
|
"""Processor that handles LLM classification responses and triggers events.
|
||||||
|
|
||||||
This processor aggregates LLM text tokens into complete responses and analyzes
|
This processor aggregates LLM text tokens into complete responses and analyzes
|
||||||
them to determine if the call reached a voicemail system or a live person.
|
them to determine if the call reached a voicemail system or a live person.
|
||||||
@@ -218,9 +218,9 @@ class ClassificationProcessor(FrameProcessor):
|
|||||||
|
|
||||||
The processor expects responses containing either "CONVERSATION" (indicating
|
The processor expects responses containing either "CONVERSATION" (indicating
|
||||||
a human answered) or "VOICEMAIL" (indicating an automated system). Once a
|
a human answered) or "VOICEMAIL" (indicating an automated system). Once a
|
||||||
decision is made, it triggers the appropriate notifications and callbacks.
|
decision is made, it triggers the appropriate notifications and event handlers.
|
||||||
|
|
||||||
For voicemail detection, the callback timer starts immediately and is cancelled
|
For voicemail detection, the event handler timer starts immediately and is cancelled
|
||||||
and restarted based on user speech patterns to ensure proper timing.
|
and restarted based on user speech patterns to ensure proper timing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -230,9 +230,6 @@ class ClassificationProcessor(FrameProcessor):
|
|||||||
gate_notifier: BaseNotifier,
|
gate_notifier: BaseNotifier,
|
||||||
conversation_notifier: BaseNotifier,
|
conversation_notifier: BaseNotifier,
|
||||||
voicemail_notifier: BaseNotifier,
|
voicemail_notifier: BaseNotifier,
|
||||||
on_voicemail_detected: Optional[
|
|
||||||
Callable[["ClassificationProcessor"], Awaitable[None]]
|
|
||||||
] = None,
|
|
||||||
voicemail_response_delay: float,
|
voicemail_response_delay: float,
|
||||||
):
|
):
|
||||||
"""Initialize the voicemail processor.
|
"""Initialize the voicemail processor.
|
||||||
@@ -244,20 +241,19 @@ class ClassificationProcessor(FrameProcessor):
|
|||||||
all buffered TTS frames for normal conversation flow.
|
all buffered TTS frames for normal conversation flow.
|
||||||
voicemail_notifier: Notifier to signal the TTSBuffer to clear
|
voicemail_notifier: Notifier to signal the TTSBuffer to clear
|
||||||
buffered TTS frames since voicemail was detected.
|
buffered TTS frames since voicemail was detected.
|
||||||
on_voicemail_detected: Optional callback function called when voicemail
|
|
||||||
is detected. The callback receives this processor instance and can
|
|
||||||
use it to push custom frames (like voicemail greetings).
|
|
||||||
voicemail_response_delay: Delay in seconds after user stops speaking
|
voicemail_response_delay: Delay in seconds after user stops speaking
|
||||||
before triggering the voicemail callback. This ensures the voicemail
|
before triggering the voicemail event handler. This ensures the voicemail
|
||||||
greeting or user message is complete before responding.
|
greeting or user message is complete before responding.
|
||||||
"""
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._gate_notifier = gate_notifier
|
self._gate_notifier = gate_notifier
|
||||||
self._conversation_notifier = conversation_notifier
|
self._conversation_notifier = conversation_notifier
|
||||||
self._voicemail_notifier = voicemail_notifier
|
self._voicemail_notifier = voicemail_notifier
|
||||||
self._on_voicemail_detected = on_voicemail_detected
|
|
||||||
self._voicemail_response_delay = voicemail_response_delay
|
self._voicemail_response_delay = voicemail_response_delay
|
||||||
|
|
||||||
|
# Register the voicemail detected event
|
||||||
|
self._register_event_handler("on_voicemail_detected")
|
||||||
|
|
||||||
# Aggregation state for collecting complete LLM responses
|
# Aggregation state for collecting complete LLM responses
|
||||||
self._processing_response = False
|
self._processing_response = False
|
||||||
self._response_buffer = ""
|
self._response_buffer = ""
|
||||||
@@ -265,7 +261,7 @@ class ClassificationProcessor(FrameProcessor):
|
|||||||
|
|
||||||
# Voicemail timing state
|
# Voicemail timing state
|
||||||
self._voicemail_detected = False
|
self._voicemail_detected = False
|
||||||
self._voicemail_callback_task: Optional[asyncio.Task] = None
|
self._voicemail_handler_task: Optional[asyncio.Task] = None
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
"""Process frames and handle LLM classification responses.
|
"""Process frames and handle LLM classification responses.
|
||||||
@@ -299,15 +295,15 @@ class ClassificationProcessor(FrameProcessor):
|
|||||||
self._response_buffer += frame.text
|
self._response_buffer += frame.text
|
||||||
|
|
||||||
elif isinstance(frame, UserStartedSpeakingFrame):
|
elif isinstance(frame, UserStartedSpeakingFrame):
|
||||||
# User started speaking - cancel voicemail callback timer
|
# User started speaking - cancel voicemail handler timer
|
||||||
if self._voicemail_callback_task:
|
if self._voicemail_handler_task:
|
||||||
await self.cancel_task(self._voicemail_callback_task)
|
await self.cancel_task(self._voicemail_handler_task)
|
||||||
self._voicemail_callback_task = None
|
self._voicemail_handler_task = None
|
||||||
|
|
||||||
elif isinstance(frame, UserStoppedSpeakingFrame):
|
elif isinstance(frame, UserStoppedSpeakingFrame):
|
||||||
# User stopped speaking - restart voicemail callback timer if voicemail detected
|
# User stopped speaking - restart voicemail handler timer if voicemail detected
|
||||||
if self._voicemail_detected and not self._voicemail_callback_task:
|
if self._voicemail_detected and not self._voicemail_handler_task:
|
||||||
self._voicemail_callback_task = self.create_task(self._delayed_voicemail_callback())
|
self._voicemail_handler_task = self.create_task(self._delayed_voicemail_handler())
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Pass all non-LLM frames through
|
# Pass all non-LLM frames through
|
||||||
@@ -348,36 +344,35 @@ class ClassificationProcessor(FrameProcessor):
|
|||||||
# Interrupt the current pipeline to stop any ongoing processing
|
# Interrupt the current pipeline to stop any ongoing processing
|
||||||
await self.push_frame(BotInterruptionFrame(), FrameDirection.UPSTREAM)
|
await self.push_frame(BotInterruptionFrame(), FrameDirection.UPSTREAM)
|
||||||
|
|
||||||
# Always start the callback timer immediately
|
# Always start the handler timer immediately
|
||||||
# It will be cancelled and restarted if user starts/stops speaking
|
# It will be cancelled and restarted if user starts/stops speaking
|
||||||
if not self._voicemail_callback_task:
|
if not self._voicemail_handler_task:
|
||||||
self._voicemail_callback_task = self.create_task(self._delayed_voicemail_callback())
|
self._voicemail_handler_task = self.create_task(self._delayed_voicemail_handler())
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# This can happen if the LLM is interrupted before completing the response
|
# This can happen if the LLM is interrupted before completing the response
|
||||||
logger.debug(f"{self}: No classification found: '{full_response}'")
|
logger.debug(f"{self}: No classification found: '{full_response}'")
|
||||||
|
|
||||||
async def _delayed_voicemail_callback(self):
|
async def _delayed_voicemail_handler(self):
|
||||||
"""Execute the voicemail callback after the configured delay.
|
"""Execute the voicemail event handler after the configured delay.
|
||||||
|
|
||||||
This method waits for the specified delay period, then triggers the
|
This method waits for the specified delay period, then triggers the
|
||||||
developer's voicemail callback. The timer can be cancelled and restarted
|
developer's voicemail event handler. The timer can be cancelled and restarted
|
||||||
based on user speech patterns to ensure proper timing.
|
based on user speech patterns to ensure proper timing.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
await asyncio.sleep(self._voicemail_response_delay)
|
await asyncio.sleep(self._voicemail_response_delay)
|
||||||
|
|
||||||
if self._on_voicemail_detected:
|
try:
|
||||||
try:
|
logger.debug(f"{self}: Triggering voicemail detected event")
|
||||||
logger.debug(f"{self}: Executing voicemail callback")
|
await self._call_event_handler("on_voicemail_detected")
|
||||||
await self._on_voicemail_detected(self)
|
except Exception as e:
|
||||||
except Exception as e:
|
logger.exception(f"{self}: Error in voicemail event handler: {e}")
|
||||||
logger.exception(f"{self}: Error in voicemail callback: {e}")
|
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
self._voicemail_callback_task = None
|
self._voicemail_handler_task = None
|
||||||
|
|
||||||
|
|
||||||
class TTSBuffer(FrameProcessor):
|
class TTSBuffer(FrameProcessor):
|
||||||
@@ -482,7 +477,7 @@ class TTSBuffer(FrameProcessor):
|
|||||||
|
|
||||||
When voicemail is detected, all buffered TTS frames are discarded
|
When voicemail is detected, all buffered TTS frames are discarded
|
||||||
since they were intended for human conversation and are not appropriate
|
since they were intended for human conversation and are not appropriate
|
||||||
for voicemail systems. The developer callback will handle voicemail-
|
for voicemail systems. The developer event handlers will handle voicemail-
|
||||||
specific audio output.
|
specific audio output.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@@ -520,20 +515,17 @@ class VoicemailDetector(ParallelPipeline):
|
|||||||
Once a decision is made, the appropriate action is taken:
|
Once a decision is made, the appropriate action is taken:
|
||||||
|
|
||||||
- CONVERSATION: Continue normal bot dialogue
|
- CONVERSATION: Continue normal bot dialogue
|
||||||
- VOICEMAIL: Trigger developer callback for custom voicemail handling
|
- VOICEMAIL: Trigger developer event handler for custom voicemail handling
|
||||||
|
|
||||||
Example::
|
Example::
|
||||||
|
|
||||||
classification_llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
classification_llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
|
detector = VoicemailDetector(llm=classification_llm)
|
||||||
|
|
||||||
|
@detector.event_handler("on_voicemail_detected")
|
||||||
async def handle_voicemail(processor):
|
async def handle_voicemail(processor):
|
||||||
await processor.push_frame(TTSSpeakFrame("Please leave a message."))
|
await processor.push_frame(TTSSpeakFrame("Please leave a message."))
|
||||||
|
|
||||||
detector = VoicemailDetector(
|
|
||||||
llm=classification_llm,
|
|
||||||
on_voicemail_detected=handle_voicemail
|
|
||||||
)
|
|
||||||
|
|
||||||
pipeline = Pipeline([
|
pipeline = Pipeline([
|
||||||
transport.input(),
|
transport.input(),
|
||||||
stt,
|
stt,
|
||||||
@@ -545,6 +537,11 @@ class VoicemailDetector(ParallelPipeline):
|
|||||||
transport.output(),
|
transport.output(),
|
||||||
context_aggregator.assistant(),
|
context_aggregator.assistant(),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
Events:
|
||||||
|
on_voicemail_detected: Triggered when voicemail is detected after the configured
|
||||||
|
delay. The event handler receives one argument: the ClassificationProcessor
|
||||||
|
instance which can be used to push frames.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DEFAULT_SYSTEM_PROMPT = """You are a voicemail detection classifier for an OUTBOUND calling system. A bot has called a phone number and you need to determine if a human answered or if the call went to voicemail based on the provided text.
|
DEFAULT_SYSTEM_PROMPT = """You are a voicemail detection classifier for an OUTBOUND calling system. A bot has called a phone number and you need to determine if a human answered or if the call went to voicemail based on the provided text.
|
||||||
@@ -573,7 +570,6 @@ Respond with ONLY "CONVERSATION" if a person answered, or "VOICEMAIL" if it's vo
|
|||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
llm: LLMService,
|
llm: LLMService,
|
||||||
on_voicemail_detected: Callable[["ClassificationProcessor"], Awaitable[None]],
|
|
||||||
voicemail_response_delay: float = 2.0,
|
voicemail_response_delay: float = 2.0,
|
||||||
system_prompt: Optional[str] = None,
|
system_prompt: Optional[str] = None,
|
||||||
):
|
):
|
||||||
@@ -582,11 +578,8 @@ Respond with ONLY "CONVERSATION" if a person answered, or "VOICEMAIL" if it's vo
|
|||||||
Args:
|
Args:
|
||||||
llm: LLM service used for voicemail vs conversation classification.
|
llm: LLM service used for voicemail vs conversation classification.
|
||||||
Should be fast and reliable for real-time classification.
|
Should be fast and reliable for real-time classification.
|
||||||
on_voicemail_detected: Optional callback function invoked when voicemail
|
|
||||||
is detected. Receives the ClassificationProcessor instance which can be
|
|
||||||
used to push frames (like custom voicemail greetings).
|
|
||||||
voicemail_response_delay: Delay in seconds after user stops speaking
|
voicemail_response_delay: Delay in seconds after user stops speaking
|
||||||
before triggering the voicemail callback. This allows voicemail
|
before triggering the voicemail event handler. This allows voicemail
|
||||||
responses to be played back after a short delay to ensure the response
|
responses to be played back after a short delay to ensure the response
|
||||||
occurs during the voicemail recording. Default is 2.0 seconds.
|
occurs during the voicemail recording. Default is 2.0 seconds.
|
||||||
system_prompt: Optional custom system prompt for classification. If None,
|
system_prompt: Optional custom system prompt for classification. If None,
|
||||||
@@ -626,7 +619,6 @@ Respond with ONLY "CONVERSATION" if a person answered, or "VOICEMAIL" if it's vo
|
|||||||
gate_notifier=self._gate_notifier,
|
gate_notifier=self._gate_notifier,
|
||||||
conversation_notifier=self._conversation_notifier,
|
conversation_notifier=self._conversation_notifier,
|
||||||
voicemail_notifier=self._voicemail_notifier,
|
voicemail_notifier=self._voicemail_notifier,
|
||||||
on_voicemail_detected=on_voicemail_detected,
|
|
||||||
voicemail_response_delay=voicemail_response_delay,
|
voicemail_response_delay=voicemail_response_delay,
|
||||||
)
|
)
|
||||||
self._voicemail_buffer = TTSBuffer(self._conversation_notifier, self._voicemail_notifier)
|
self._voicemail_buffer = TTSBuffer(self._conversation_notifier, self._voicemail_notifier)
|
||||||
@@ -645,6 +637,9 @@ Respond with ONLY "CONVERSATION" if a person answered, or "VOICEMAIL" if it's vo
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Register the voicemail detected event after super().__init__()
|
||||||
|
self._register_event_handler("on_voicemail_detected")
|
||||||
|
|
||||||
def _validate_prompt(self, prompt: str) -> None:
|
def _validate_prompt(self, prompt: str) -> None:
|
||||||
"""Validate custom prompt contains required response format instructions.
|
"""Validate custom prompt contains required response format instructions.
|
||||||
|
|
||||||
@@ -686,3 +681,15 @@ Respond with ONLY "CONVERSATION" if a person answered, or "VOICEMAIL" if it's vo
|
|||||||
The TTSBuffer processor instance.
|
The TTSBuffer processor instance.
|
||||||
"""
|
"""
|
||||||
return self._voicemail_buffer
|
return self._voicemail_buffer
|
||||||
|
|
||||||
|
def add_event_handler(self, event_name: str, handler):
|
||||||
|
"""Add an event handler for voicemail detection events.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_name: The name of the event to handle.
|
||||||
|
handler: The function to call when the event occurs.
|
||||||
|
"""
|
||||||
|
if event_name == "on_voicemail_detected":
|
||||||
|
self._classification_processor.add_event_handler(event_name, handler)
|
||||||
|
else:
|
||||||
|
super().add_event_handler(event_name, handler)
|
||||||
|
|||||||
Reference in New Issue
Block a user