Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions app/ai/voice/agents/breeze_buddy/template/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,26 @@
)


# Mapping from LLM-friendly display labels to database-friendly machine codes
# Used for abandoned checkout recovery "abandonment_reason" field
ABANDONMENT_REASON_DISPLAY_TO_CODE = {
# High-Intent Reasons
"Payment failed": "PAYMENT_FAILED",
"Payment method unavailable": "PAYMENT_METHOD_UNAVAILABLE",
"Technical issue": "TECHNICAL_ISSUE",
Comment on lines +35 to +41
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The PR description says the hook converts labels to consistent snake_case identifiers, but this mapping emits SCREAMING_SNAKE_CASE values (e.g., "PAYMENT_FAILED"). Please either change the mapped codes to the intended snake_case format, or update the PR description/comments/constant name to match the chosen convention so downstream consumers (Nautilus/db queries) are consistent.

Copilot uses AI. Check for mistakes.
"Product detail gap": "PRODUCT_DETAIL_GAP",
"Delivery / pincode blocker": "DELIVERY_PINCODE_BLOCKER",
"Wants discount": "WANTS_DISCOUNT",
# Low-Intent Reasons
"Just browsing": "JUST_BROWSING",
"Price too high / no offers": "PRICE_TOO_HIGH",
"Product not available": "PRODUCT_NOT_AVAILABLE",
"Already purchased elsewhere": "ALREADY_PURCHASED_ELSEWHERE",
"Still deciding": "STILL_DECIDING",
"Changed mind": "CHANGED_MIND",
}


class Hook(ABC):
"""
Base class for all hooks.
Expand Down Expand Up @@ -213,6 +233,23 @@ async def execute(
if "outcome" not in meta_data:
meta_data["outcome"] = {}

# Map abandonment_reason from display label to machine code
# (e.g., "Payment failed" -> "PAYMENT_FAILED")
if "abandonment_reason" in final_data:
display_value = final_data["abandonment_reason"]
if display_value in ABANDONMENT_REASON_DISPLAY_TO_CODE:
machine_code = ABANDONMENT_REASON_DISPLAY_TO_CODE[display_value]
final_data["abandonment_reason"] = machine_code
logger.debug(
f"Mapped abandonment_reason from '{display_value}' to '{machine_code}' "
f"for lead {context.lead.id}"
)
else:
logger.warning(
f"Unknown abandonment_reason display value '{display_value}' "
f"for lead {context.lead.id}. Using as-is."
Comment on lines +240 to +250
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

display_value comes directly from LLM args and may be non-string (or even unhashable like a list/dict). In that case display_value in ABANDONMENT_REASON_DISPLAY_TO_CODE will raise TypeError and prevent the DB update. Consider guarding with isinstance(display_value, str) (and optionally normalizing via .strip()), otherwise skip mapping/log a structured warning.

Suggested change
if display_value in ABANDONMENT_REASON_DISPLAY_TO_CODE:
machine_code = ABANDONMENT_REASON_DISPLAY_TO_CODE[display_value]
final_data["abandonment_reason"] = machine_code
logger.debug(
f"Mapped abandonment_reason from '{display_value}' to '{machine_code}' "
f"for lead {context.lead.id}"
)
else:
logger.warning(
f"Unknown abandonment_reason display value '{display_value}' "
f"for lead {context.lead.id}. Using as-is."
if isinstance(display_value, str):
normalized_display_value = display_value.strip()
if normalized_display_value in ABANDONMENT_REASON_DISPLAY_TO_CODE:
machine_code = ABANDONMENT_REASON_DISPLAY_TO_CODE[
normalized_display_value
]
final_data["abandonment_reason"] = machine_code
logger.debug(
f"Mapped abandonment_reason from '{display_value}' to '{machine_code}' "
f"for lead {context.lead.id}"
)
else:
logger.warning(
f"Unknown abandonment_reason display value '{display_value}' "
f"for lead {context.lead.id}. Using as-is."
)
else:
logger.warning(
f"Invalid abandonment_reason type '{type(display_value).__name__}' "
f"for lead {context.lead.id}. Skipping display-to-code mapping."

Copilot uses AI. Check for mistakes.
)
Comment on lines +247 to +251
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The else branch logs a warning for any non-matching value, which will also fire when the value is already a machine code (or is None/empty). This can create noisy logs during normal operation. Consider: (1) skipping mapping entirely when the value is falsy, and (2) treating values already in ABANDONMENT_REASON_DISPLAY_TO_CODE.values() as already-normalized (no warning), only warning for truly unknown non-empty strings.

Copilot uses AI. Check for mistakes.
Comment on lines +238 to +251
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate before mapping and do not persist unknown LLM values as-is.

abandonment_reason is Any; a list/dict value can raise in the membership check, and unknown strings are logged then persisted by the metadata loop. Drop invalid/unknown values or map them to an explicit sentinel before storage.

Proposed validation guard
             if "abandonment_reason" in final_data:
                 display_value = final_data["abandonment_reason"]
-                if display_value in ABANDONMENT_REASON_DISPLAY_TO_CODE:
+                if not isinstance(display_value, str):
+                    logger.warning(
+                        f"Invalid abandonment_reason type for lead {context.lead.id}. "
+                        "Dropping field."
+                    )
+                    final_data.pop("abandonment_reason", None)
+                elif display_value in ABANDONMENT_REASON_DISPLAY_TO_CODE:
                     machine_code = ABANDONMENT_REASON_DISPLAY_TO_CODE[display_value]
                     final_data["abandonment_reason"] = machine_code
                     logger.debug(
                         f"Mapped abandonment_reason from '{display_value}' to '{machine_code}' "
                         f"for lead {context.lead.id}"
                     )
+                elif display_value in set(ABANDONMENT_REASON_DISPLAY_TO_CODE.values()):
+                    logger.debug(
+                        f"abandonment_reason already normalized for lead {context.lead.id}"
+                    )
                 else:
                     logger.warning(
-                        f"Unknown abandonment_reason display value '{display_value}' "
-                        f"for lead {context.lead.id}. Using as-is."
+                        f"Unknown abandonment_reason value for lead {context.lead.id}. "
+                        "Dropping field."
                     )
+                    final_data.pop("abandonment_reason", None)

Based on learnings, Breeze Buddy LLM payloads should be predetermined and must not persist sensitive or free-form data through this path.

📝 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
if "abandonment_reason" in final_data:
display_value = final_data["abandonment_reason"]
if display_value in ABANDONMENT_REASON_DISPLAY_TO_CODE:
machine_code = ABANDONMENT_REASON_DISPLAY_TO_CODE[display_value]
final_data["abandonment_reason"] = machine_code
logger.debug(
f"Mapped abandonment_reason from '{display_value}' to '{machine_code}' "
f"for lead {context.lead.id}"
)
else:
logger.warning(
f"Unknown abandonment_reason display value '{display_value}' "
f"for lead {context.lead.id}. Using as-is."
)
if "abandonment_reason" in final_data:
display_value = final_data["abandonment_reason"]
if not isinstance(display_value, str):
logger.warning(
f"Invalid abandonment_reason type for lead {context.lead.id}. "
"Dropping field."
)
final_data.pop("abandonment_reason", None)
elif display_value in ABANDONMENT_REASON_DISPLAY_TO_CODE:
machine_code = ABANDONMENT_REASON_DISPLAY_TO_CODE[display_value]
final_data["abandonment_reason"] = machine_code
logger.debug(
f"Mapped abandonment_reason from '{display_value}' to '{machine_code}' "
f"for lead {context.lead.id}"
)
elif display_value in set(ABANDONMENT_REASON_DISPLAY_TO_CODE.values()):
logger.debug(
f"abandonment_reason already normalized for lead {context.lead.id}"
)
else:
logger.warning(
f"Unknown abandonment_reason value for lead {context.lead.id}. "
"Dropping field."
)
final_data.pop("abandonment_reason", None)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/ai/voice/agents/breeze_buddy/template/hooks.py` around lines 238 - 251,
The current block that maps final_data["abandonment_reason"] assumes a string
and persists unknown/free-form values; first guard the value type by checking
isinstance(final_data["abandonment_reason"], str) before doing the membership
test against ABANDONMENT_REASON_DISPLAY_TO_CODE, and if it's not a string or not
in the mapping, replace it with a safe sentinel (e.g., None or an explicit
"UNKNOWN_ABANDONMENT" marker) instead of leaving the original list/dict/string
to be persisted; update the logger calls in this branch to reflect
dropping/mapping to the sentinel and reference final_data,
ABANDONMENT_REASON_DISPLAY_TO_CODE, and context.lead.id so reviewers can find
the change.


# Add all other fields except outcome to the outcome object
for key, value in final_data.items():
if key != "outcome" and value is not None:
Expand Down
Loading