Skip to content

Conversation

@Swetha-160303
Copy link

@Swetha-160303 Swetha-160303 commented Dec 30, 2025

DEV PROOF:
image
image

Summary by CodeRabbit

  • New Features
    • Added webhook notifications for call outcomes, including unanswered and busy calls.
    • Improved webhook delivery to consistently send notifications when configured, regardless of call type.

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

@coderabbitai
Copy link

coderabbitai bot commented Dec 30, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This 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

Cohort / File(s) Summary
Unanswered call webhook notification
app/ai/voice/agents/breeze_buddy/managers/calls.py
Adds webhook dispatch logic in handle_unanswered_calls() that constructs and sends webhook payloads containing order_id, call_sid, outcome ("NO_ANSWER"), attempt_number, and is_last_attempt flag. Implements aiohttp session with 3-retry mechanism and comprehensive error handling. Executes after lead is marked as finished.
Webhook send decision logic update
app/ai/voice/agents/breeze_buddy/websocket_bot.py
Changes webhook decision flow from conditional last-attempt guard (that skipped BUSY/NO_ANSWER webhooks except on final attempt) to unconditionally sending webhooks whenever reporting_webhook_url is present, regardless of outcome type or attempt count.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • swaroopvarma1

Poem

🐰 A webhook hops when calls don't ring,
Retrying thrice—what news it'll bring!
NO_ANSWER becomes a whisper sent,
To distant servers, swift as scent,
Call outcomes flutter on the breeze,
Three tries to tell them, if you please! 📞✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: enabling webhook delivery for BUSY/NO_ANSWER call outcomes on all retry attempts, which aligns directly with the implementation changes in both modified files.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Swetha-160303 Swetha-160303 reopened this Dec 30, 2025
@Swetha-160303 Swetha-160303 force-pushed the Handling-the-case-of-the-busy-no-answer-webhook branch from b07c72e to 615f618 Compare December 30, 2025 10:51
Copy link

@coderabbitai coderabbitai bot left a 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 Exception is acceptable for webhook sending (which can fail in various ways), consider catching aiohttp.ClientError and asyncio.TimeoutError specifically, 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 adding isLastAttempt field for consistency.

The webhook payload in calls.py (lines 480-486) includes isLastAttempt to help consumers understand retry context. Since this information is already calculated at lines 603-627, consider adding it to the summary_data dictionary 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_attempt before this point in the code flow, as it's currently computed after summary_data is defined.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ef71043 and 615f618.

📒 Files selected for processing (2)
  • app/ai/voice/agents/breeze_buddy/managers/calls.py
  • app/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 the is_last_attempt logic correctly identifies the final retry attempt based on the _retry_call logic 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.

Comment on lines 470 to 697
# 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}")

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
# 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.

@Swetha-160303 Swetha-160303 force-pushed the Handling-the-case-of-the-busy-no-answer-webhook branch 2 times, most recently from b43847b to 5f10717 Compare December 30, 2025 11:40

await _retry_call(lead, config, "NO_ANSWER")
# Send webhook for unanswered calls (BUSY/NO_ANSWER)
reporting_webhook_url = (
Copy link
Contributor

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

@Swetha-160303 Swetha-160303 force-pushed the Handling-the-case-of-the-busy-no-answer-webhook branch from 5f10717 to cc7cd09 Compare December 30, 2025 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants