Skip to content

Commit 770b175

Browse files
committed
feat(breeze_buddy): add IVR mode — DTMF-driven menu flow
New flow.mode="ivr" runs a pure DTMF state machine (no STT/LLM/pipeline): walks a per-template menu tree, plays TTS prompts, routes on keypresses. Supports nested sub-menus, back-navigation, greeting-driven opening node, per-node timeout/retry, invalid-key replay (no retry consumed), digit logging (dtmf_inputs), synthetic transcription, and BUSY-on-incomplete so unanswered calls retry.
1 parent 786a4b7 commit 770b175

10 files changed

Lines changed: 1008 additions & 13 deletions

File tree

app/ai/voice/agents/breeze_buddy/agent/__init__.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,6 @@
3838
create_lead_from_template_id,
3939
handle_inbound_call,
4040
)
41-
from app.ai.voice.agents.breeze_buddy.agent.ivr import (
42-
BLOCK_MESSAGE_PLAY_SECONDS,
43-
_send_audio,
44-
get_template_id_from_call,
45-
prepare_block_audio,
46-
)
4741
from app.ai.voice.agents.breeze_buddy.agent.pipeline import (
4842
build_pipeline,
4943
create_pipeline_task,
@@ -62,6 +56,13 @@
6256
from app.ai.voice.agents.breeze_buddy.handlers.internal.end_conversation import (
6357
end_conversation,
6458
)
59+
from app.ai.voice.agents.breeze_buddy.ivr.selection import (
60+
BLOCK_MESSAGE_PLAY_SECONDS,
61+
_send_audio,
62+
get_template_id_from_call,
63+
prepare_block_audio,
64+
)
65+
from app.ai.voice.agents.breeze_buddy.ivr.walker import IvrWalker
6566
from app.ai.voice.agents.breeze_buddy.managers.utils import (
6667
prepare_and_store_initial_greeting,
6768
)
@@ -82,6 +83,7 @@
8283
from app.ai.voice.agents.breeze_buddy.template.types import (
8384
LEGACY_VOICE_TO_PROVIDER,
8485
ConfigurationModel,
86+
FlowMode,
8587
InterruptionConfig,
8688
TemplateModel,
8789
TTSConfig,
@@ -1054,6 +1056,22 @@ async def run(self, runner_args: Optional[RunnerArguments] = None) -> None:
10541056
f"Invalid TTS provider '{payload_provider}' in payload, keeping existing config"
10551057
)
10561058

1059+
# ── IVR mode: pure DTMF state machine (no STT/LLM/pipeline) ─────────
1060+
# Telephony-only. Runs the menu tree directly over the websocket and
1061+
# returns before any pipeline is built — self.task/self.context stay
1062+
# None, which the reused end_conversation finaliser already handles.
1063+
if (
1064+
not self.is_daily_mode
1065+
and self.template
1066+
and self.template.flow.get("mode") == FlowMode.IVR.value
1067+
):
1068+
logger.info(
1069+
f"[IVR] flow.mode=ivr -> running DTMF walker for "
1070+
f"call {self.call_sid}"
1071+
)
1072+
await IvrWalker(self).run()
1073+
return
1074+
10571075
# Build services and pipeline. Stream mode skips LLM creation and
10581076
# runs build_pipeline with mode="stream" (no LLM processor, no
10591077
# assistant aggregator, transcript collector inserted, no user idle).

app/ai/voice/agents/breeze_buddy/handlers/internal/end_conversation.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -302,12 +302,22 @@ async def end_conversation(context: TemplateContext, args, transition_to=None):
302302
if approval_manager:
303303
approval_manager.deny_all("conversation_ended")
304304

305-
# Send EndFrame to gracefully terminate the pipeline
306-
logger.info(
307-
f"Sending EndFrame to terminate pipeline for call {context.call_sid}"
308-
)
309-
await context.task.queue_frame(EndFrame())
310-
logger.info(f"EndFrame queued for call {context.call_sid}")
305+
# Send EndFrame to gracefully terminate the pipeline.
306+
# IVR mode (flow.mode == "ivr") runs without a Pipecat pipeline, so
307+
# context.task is None — there is nothing to terminate. The IVR walker
308+
# ends the call by closing the websocket itself. Guard the EndFrame so
309+
# this finalization path is reusable from the pipeline-less walker.
310+
if context.task:
311+
logger.info(
312+
f"Sending EndFrame to terminate pipeline for call {context.call_sid}"
313+
)
314+
await context.task.queue_frame(EndFrame())
315+
logger.info(f"EndFrame queued for call {context.call_sid}")
316+
else:
317+
logger.info(
318+
f"No pipeline task for call {context.call_sid} (IVR mode); "
319+
"skipping EndFrame"
320+
)
311321

312322
# Clear log context AFTER all logs to prevent leakage between calls
313323
clear_log_context()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""IVR (DTMF) subpackage for Breeze Buddy telephony.
2+
3+
- ``selection``: answer-time, pre-pipeline IVR for inbound template selection
4+
(caller picks which agent/template via keypad before the pipeline is built).
5+
- ``walker``: mid-call IVR mode (``flow.mode == "ivr"``) — a pure DTMF state
6+
machine that runs the conversation itself, no STT/LLM/pipeline.
7+
8+
Import from the explicit submodules (``ivr.selection`` /
9+
``ivr.walker``); this package intentionally does not re-export.
10+
"""
File renamed without changes.

0 commit comments

Comments
 (0)