feat(breeze_buddy): add IVR mode — DTMF-driven menu flow#833
feat(breeze_buddy): add IVR mode — DTMF-driven menu flow#833manas-narra wants to merge 1 commit into
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughAdds a DTMF IVR mode to the Breeze Buddy voice agent. Introduces new Pydantic schemas ( ChangesIVR DTMF Mode
Sequence DiagramsequenceDiagram
participant Caller as Caller (WebSocket)
participant Agent as Agent.run()
participant IvrWalker as IvrWalker
participant TTS as TTS Provider (ElevenLabs)
participant DB as Lead Persistence
Agent->>IvrWalker: detect FlowMode.IVR → IvrWalker(self).run()
IvrWalker->>IvrWalker: parse IvrModeFlow, resolve TTS config
loop Node traversal
IvrWalker->>TTS: prepare_ivr_menu_audio(node.prompt)
TTS-->>IvrWalker: audio bytes
IvrWalker->>Caller: _send_audio(bytes)
IvrWalker->>Caller: _wait_for_digit(timeout_window)
alt DTMF digit received
Caller-->>IvrWalker: digit
IvrWalker->>IvrWalker: _apply_option() → record IVR input
IvrWalker->>DB: _persist_outcome() background task
IvrWalker->>IvrWalker: transition to next node or end
else invalid digit
Caller-->>IvrWalker: unrecognized digit
IvrWalker->>TTS: prepare_ivr_menu_audio(invalid_prompt)
IvrWalker->>Caller: replay invalid prompt
else hangup / timeout exhausted
Caller-->>IvrWalker: stop event or retries=0
IvrWalker->>IvrWalker: _finalize_and_close()
end
end
IvrWalker->>DB: end_conversation (transcript + outcome)
IvrWalker->>Caller: close WebSocket
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
app/ai/voice/agents/breeze_buddy/template/types.py (2)
2117-2122: 💤 Low valueConsider using
List[HookConfig]for type consistency.
FlowFunction.hooksusesList[HookConfig](line 1685), butIvrOption.hooksusesList[Dict[str, Any]]. This loses Pydantic validation thatHookConfigprovides (e.g.,http_requeststructure,expected_fieldstyping). If IVR hooks truly follow the same shape, usingList[HookConfig]would catch authoring errors at template validation time.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/ai/voice/agents/breeze_buddy/template/types.py` around lines 2117 - 2122, The `IvrOption.hooks` field is typed as `List[Dict[str, Any]]` but should be typed as `List[HookConfig]` to match `FlowFunction.hooks` (line 1685) and maintain consistency. Change the type annotation of the hooks field in the IvrOption class from `List[Dict[str, Any]]` to `List[HookConfig]` so that Pydantic validation is applied consistently across all hook configurations, enabling validation of structures like http_request and expected_fields at template validation time.
2193-2195: ⚡ Quick winConsider validating
initial_nodeexists innodes.If
initial_nodereferences a non-existent node, the error is only discovered at call time (the walker logs an error and ends withIVR_NODE_MISSINGoutcome). Adding amodel_validatorwould catch this authoring mistake at template creation/update time, improving template author experience.✨ Proposed validation
+ `@model_validator`(mode="after") + def _validate_initial_node_exists(self) -> "IvrModeFlow": + if self.initial_node not in self.nodes: + raise ValueError( + f"initial_node '{self.initial_node}' not found in nodes. " + f"Available nodes: {list(self.nodes.keys())}" + ) + return self🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/ai/voice/agents/breeze_buddy/template/types.py` around lines 2193 - 2195, Add a Pydantic model_validator to the class containing the initial_node and nodes fields to validate that initial_node references an existing key in the nodes dictionary. The validator should check if the value of initial_node exists as a key in nodes, and if not, raise a validation error with a clear message indicating the referenced node does not exist. This catches authoring mistakes at template creation/update time instead of deferring the error to runtime call time.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/ai/voice/agents/breeze_buddy/agent/ivr_walker.py`:
- Around line 515-517: The finalize method in ivr_walker.py is using
close_websocket_safely(ws) for socket closure, but this bypasses the Breeze
Buddy graceful-disconnect handler. Replace the direct close_websocket_safely(ws)
call with a call to end_call_with_errors(ws, stream_sid, errors) to ensure
consistent disconnect behavior and proper error semantics as per Breeze Buddy
guidelines. You will need to ensure the stream_sid and errors parameters are
available in the finalize context or passed appropriately to maintain the
graceful disconnect flow.
In `@app/ai/voice/agents/breeze_buddy/template/types.py`:
- Around line 2092-2101: The `target_node` field in the IvrOption class (or
relevant parent class) is marked as Optional with a default of None, but the
description states it is required when action is a transition. Add a
`model_validator` to enforce this constraint: when the `action` field equals the
TRANSITION variant of IvrAction, the `target_node` field must not be None, and
raise a validation error if this condition is violated. This ensures template
authors receive immediate feedback instead of silent failures during walker
execution.
In `@examples/templates/order-confirmation-ivr.json`:
- Line 29: The on_timeout_outcome field set to "NO_RESPONSE" on the main entry
node disables the default BUSY behavior for incomplete calls, preventing the
retry logic from triggering as intended. Either remove the on_timeout_outcome
field entirely from the main node to preserve the default BUSY fallback
behavior, or replace its value with "BUSY" if your outcome model explicitly
supports it.
---
Nitpick comments:
In `@app/ai/voice/agents/breeze_buddy/template/types.py`:
- Around line 2117-2122: The `IvrOption.hooks` field is typed as `List[Dict[str,
Any]]` but should be typed as `List[HookConfig]` to match `FlowFunction.hooks`
(line 1685) and maintain consistency. Change the type annotation of the hooks
field in the IvrOption class from `List[Dict[str, Any]]` to `List[HookConfig]`
so that Pydantic validation is applied consistently across all hook
configurations, enabling validation of structures like http_request and
expected_fields at template validation time.
- Around line 2193-2195: Add a Pydantic model_validator to the class containing
the initial_node and nodes fields to validate that initial_node references an
existing key in the nodes dictionary. The validator should check if the value of
initial_node exists as a key in nodes, and if not, raise a validation error with
a clear message indicating the referenced node does not exist. This catches
authoring mistakes at template creation/update time instead of deferring the
error to runtime call time.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d611e202-5dbf-4def-a1cd-4f220a5b7249
📒 Files selected for processing (7)
app/ai/voice/agents/breeze_buddy/agent/__init__.pyapp/ai/voice/agents/breeze_buddy/agent/ivr_walker.pyapp/ai/voice/agents/breeze_buddy/handlers/internal/end_conversation.pyapp/ai/voice/agents/breeze_buddy/template/context.pyapp/ai/voice/agents/breeze_buddy/template/loader.pyapp/ai/voice/agents/breeze_buddy/template/types.pyexamples/templates/order-confirmation-ivr.json
04fdfc8 to
44a19c8
Compare
| if self.lead.id: | ||
| # Non-blocking: never stall the menu on a DB write (same principle | ||
| # as hook persistence in transition.py). | ||
| asyncio.create_task( |
There was a problem hiding this comment.
🟥 [MAJOR — functional] Fire-and-forget full-overwrite meta_data flush can lose the final transcript
_persist_outcome schedules asyncio.create_task(_flush_outcome(..., dict(self.lead.metaData))) on every option press (and on errors). _flush_outcome → update_lead_call_completion_details, whose query sets "meta_data" = $N — a full column overwrite, not a jsonb merge (database/queries/breeze_buddy/lead_call_tracker.py:355; the merge path at line 582 is a different function). The dict(self.lead.metaData) snapshot is taken here, before _finalize_and_close adds transcription / call_ended_by / node_traversal / errors.
Because these flush tasks are neither awaited nor ordered relative to _finalize_and_close's final write, a stale intermediate flush can land after the final write and overwrite it with a snapshot missing the transcript → lost transcripts and analytics on IVR calls, nondeterministically. (Separately, neither create_task here nor the one at line 347 retains a reference, so either can be GC'd before completing — Task was destroyed but it is pending!.)
Fix: await the final flush before end_conversation in _finalize_and_close, and/or make intermediate flushes write only their own keys via the jsonb-merge query (meta_data || $N) so they can't clobber the finalizer; retain task refs (self._bg_tasks.add(t); t.add_done_callback(...)).
PR #833 —
|
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.
44a19c8 to
770b175
Compare
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.
Summary by CodeRabbit
Release Notes