-
Notifications
You must be signed in to change notification settings - Fork 49
feat: Send webhook for BUSY/NO_ANSWER calls on all attempts #458
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release
Are you sure you want to change the base?
feat: Send webhook for BUSY/NO_ANSWER calls on all attempts #458
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughThis change adds webhook notification functionality for unanswered calls in the Breeze Buddy voice agent. When calls remain unanswered and a reporting webhook URL is configured, the system sends call outcome details with automatic retry logic (max 3 retries). The webhook decision logic is also updated to send notifications for all outcomes when configured, rather than only for the last attempt. Changes
Sequence Diagram(s)sequenceDiagram
participant CallHandler as Call Handler
participant LeadMgr as Lead Manager
participant WebhookClient as Webhook Client
participant ExternalWebhook as External Webhook
CallHandler->>LeadMgr: Mark lead finished (NO_ANSWER outcome)
activate LeadMgr
LeadMgr->>LeadMgr: Update lead status
deactivate LeadMgr
CallHandler->>CallHandler: Check reporting_webhook_url exists
alt Webhook URL configured
CallHandler->>WebhookClient: Construct payload<br/>(order_id, call_sid, outcome,<br/>attempt_number, is_last_attempt)
activate WebhookClient
WebhookClient->>ExternalWebhook: POST webhook (attempt 1)
alt Success
ExternalWebhook-->>WebhookClient: 2xx response
WebhookClient->>WebhookClient: Log success
else Failure
ExternalWebhook-->>WebhookClient: Error response
rect rgb(200, 150, 150)
note right of WebhookClient: Retry up to 3 times
WebhookClient->>ExternalWebhook: POST webhook (retry)
end
alt Retry Success
ExternalWebhook-->>WebhookClient: 2xx response
WebhookClient->>WebhookClient: Log success
else Final Failure
WebhookClient->>WebhookClient: Log error
end
end
deactivate WebhookClient
else No webhook URL
CallHandler->>CallHandler: Skip webhook
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
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 |
b07c72e to
615f618
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (2)
app/ai/voice/agents/breeze_buddy/managers/calls.py (1)
501-501: Consider more specific exception handling.While catching
Exceptionis acceptable for webhook sending (which can fail in various ways), consider catchingaiohttp.ClientErrorandasyncio.TimeoutErrorspecifically, or add a comment explaining why broad exception handling is necessary here.Alternative approach with specific exceptions
+ except (aiohttp.ClientError, asyncio.TimeoutError, Exception) as e: - except Exception as e: logger.error(f"Error sending webhook for unanswered call {call_id}: {e}")Or add a clarifying comment:
+ # Catch all exceptions to prevent webhook failures from blocking retry logic except Exception as e: logger.error(f"Error sending webhook for unanswered call {call_id}: {e}")app/ai/voice/agents/breeze_buddy/websocket_bot.py (1)
509-518: Consider addingisLastAttemptfield for consistency.The webhook payload in
calls.py(lines 480-486) includesisLastAttemptto help consumers understand retry context. Since this information is already calculated at lines 603-627, consider adding it to thesummary_datadictionary for consistency across all webhook notifications.Suggested enhancement
summary_data = { "callSid": self.call_sid, "cancellationReason": self.cancellation_reason, "outcome": self.outcome, "updatedAddress": self.updated_address, "attemptCount": self.lead.attempt_count + 1, + "isLastAttempt": is_last_attempt, "transcription": json.dumps(filtered_transcript, ensure_ascii=False), "callDuration": call_duration, "orderId": self.order_id, }Note: You'll need to calculate
is_last_attemptbefore this point in the code flow, as it's currently computed aftersummary_datais defined.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
app/ai/voice/agents/breeze_buddy/managers/calls.pyapp/ai/voice/agents/breeze_buddy/websocket_bot.py
🧰 Additional context used
🧬 Code graph analysis (2)
app/ai/voice/agents/breeze_buddy/websocket_bot.py (1)
app/ai/voice/agents/breeze_buddy/template/context.py (1)
reporting_webhook_url(97-99)
app/ai/voice/agents/breeze_buddy/managers/calls.py (4)
app/ai/voice/agents/breeze_buddy/template/context.py (3)
reporting_webhook_url(97-99)lead(77-79)lead(82-84)app/ai/voice/agents/breeze_buddy/template/hooks.py (1)
get(263-273)app/core/transport/http_client.py (1)
create_aiohttp_session(58-77)app/ai/voice/agents/breeze_buddy/utils/common.py (1)
send_webhook_with_retry(57-99)
🪛 Ruff (0.14.10)
app/ai/voice/agents/breeze_buddy/managers/calls.py
501-501: Do not catch blind exception: Exception
(BLE001)
🔇 Additional comments (2)
app/ai/voice/agents/breeze_buddy/managers/calls.py (1)
484-485: Logic for attempt tracking looks correct.The attempt number calculation (
lead.attempt_count + 1) correctly converts from 0-indexed internal tracking to 1-indexed user-facing values, and theis_last_attemptlogic correctly identifies the final retry attempt based on the_retry_calllogic at line 127.app/ai/voice/agents/breeze_buddy/websocket_bot.py (1)
628-630: Webhook delivery logic updated correctly.The change to send webhooks for all attempts (not just the last one) aligns well with the PR objective and maintains consistency with the new behavior in
calls.py.
| # Send webhook for unanswered calls (BUSY/NO_ANSWER) | ||
| reporting_webhook_url = ( | ||
| lead.payload.get("reporting_webhook_url") if lead.payload else None | ||
| ) | ||
| if reporting_webhook_url: | ||
| logger.info( | ||
| f"Sending webhook for unanswered call {call_id} with outcome NO_ANSWER" | ||
| ) | ||
| try: | ||
| async with create_aiohttp_session() as session: | ||
| webhook_data = { | ||
| "order_id": lead.payload.get("order_id"), | ||
| "call_sid": call_id, | ||
| "outcome": "NO_ANSWER", | ||
| "attempt_number": lead.attempt_count + 1, | ||
| "is_last_attempt": lead.attempt_count >= config.max_retry - 1, | ||
| } | ||
| success = await send_webhook_with_retry( | ||
| session, | ||
| reporting_webhook_url, | ||
| webhook_data, | ||
| max_retries=3, | ||
| ) | ||
| if success: | ||
| logger.info( | ||
| f"Successfully sent webhook for unanswered call {call_id}" | ||
| ) | ||
| else: | ||
| logger.error( | ||
| f"Failed to send webhook for unanswered call {call_id} after all retries" | ||
| ) | ||
| except Exception as e: | ||
| logger.error(f"Error sending webhook for unanswered call {call_id}: {e}") | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: Webhook payload format inconsistent with existing code.
The new webhook payload uses snake_case field names (order_id, call_sid, attempt_number, is_last_attempt), but existing webhooks in this same file (lines 303-308, 375-379) and in websocket_bot.py consistently use camelCase (orderId, callSid, attemptCount). This inconsistency will break webhook consumers expecting a uniform format.
Additionally, consider using lead.request_id as fallback like websocket_bot.py does at line 172.
🔎 Proposed fix to match existing webhook format
webhook_data = {
- "order_id": lead.payload.get("order_id"),
- "call_sid": call_id,
+ "orderId": lead.request_id or (lead.payload.get("order_id") if lead.payload else None),
+ "callSid": call_id,
"outcome": "NO_ANSWER",
- "attempt_number": lead.attempt_count + 1,
- "is_last_attempt": lead.attempt_count >= config.max_retry - 1,
+ "attemptCount": lead.attempt_count + 1,
+ "isLastAttempt": lead.attempt_count >= config.max_retry - 1,
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Send webhook for unanswered calls (BUSY/NO_ANSWER) | |
| reporting_webhook_url = ( | |
| lead.payload.get("reporting_webhook_url") if lead.payload else None | |
| ) | |
| if reporting_webhook_url: | |
| logger.info( | |
| f"Sending webhook for unanswered call {call_id} with outcome NO_ANSWER" | |
| ) | |
| try: | |
| async with create_aiohttp_session() as session: | |
| webhook_data = { | |
| "order_id": lead.payload.get("order_id"), | |
| "call_sid": call_id, | |
| "outcome": "NO_ANSWER", | |
| "attempt_number": lead.attempt_count + 1, | |
| "is_last_attempt": lead.attempt_count >= config.max_retry - 1, | |
| } | |
| success = await send_webhook_with_retry( | |
| session, | |
| reporting_webhook_url, | |
| webhook_data, | |
| max_retries=3, | |
| ) | |
| if success: | |
| logger.info( | |
| f"Successfully sent webhook for unanswered call {call_id}" | |
| ) | |
| else: | |
| logger.error( | |
| f"Failed to send webhook for unanswered call {call_id} after all retries" | |
| ) | |
| except Exception as e: | |
| logger.error(f"Error sending webhook for unanswered call {call_id}: {e}") | |
| # Send webhook for unanswered calls (BUSY/NO_ANSWER) | |
| reporting_webhook_url = ( | |
| lead.payload.get("reporting_webhook_url") if lead.payload else None | |
| ) | |
| if reporting_webhook_url: | |
| logger.info( | |
| f"Sending webhook for unanswered call {call_id} with outcome NO_ANSWER" | |
| ) | |
| try: | |
| async with create_aiohttp_session() as session: | |
| webhook_data = { | |
| "orderId": lead.request_id or (lead.payload.get("order_id") if lead.payload else None), | |
| "callSid": call_id, | |
| "outcome": "NO_ANSWER", | |
| "attemptCount": lead.attempt_count + 1, | |
| "isLastAttempt": lead.attempt_count >= config.max_retry - 1, | |
| } | |
| success = await send_webhook_with_retry( | |
| session, | |
| reporting_webhook_url, | |
| webhook_data, | |
| max_retries=3, | |
| ) | |
| if success: | |
| logger.info( | |
| f"Successfully sent webhook for unanswered call {call_id}" | |
| ) | |
| else: | |
| logger.error( | |
| f"Failed to send webhook for unanswered call {call_id} after all retries" | |
| ) | |
| except Exception as e: | |
| logger.error(f"Error sending webhook for unanswered call {call_id}: {e}") |
🧰 Tools
🪛 Ruff (0.14.10)
501-501: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
In app/ai/voice/agents/breeze_buddy/managers/calls.py around lines 470-503, the
webhook payload uses snake_case keys which is inconsistent with other webhooks;
change the payload keys to camelCase (orderId, callSid, outcome, attemptCount,
isLastAttempt) and use lead.request_id as a fallback when orderId is missing
(like websocket_bot.py does). Keep the same values (attemptCount =
lead.attempt_count + 1, isLastAttempt = lead.attempt_count >= config.max_retry -
1), send via the existing send_webhook_with_retry call, and preserve the same
logging and error handling.
b43847b to
5f10717
Compare
|
|
||
| await _retry_call(lead, config, "NO_ANSWER") | ||
| # Send webhook for unanswered calls (BUSY/NO_ANSWER) | ||
| reporting_webhook_url = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why this is required? check retry call function
5f10717 to
cc7cd09
Compare
DEV PROOF:


Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.