-
Notifications
You must be signed in to change notification settings - Fork 57
feat(cart-recovery): add abandonment reason mapping in hooks #718
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "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. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
AI
Apr 21, 2026
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.
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.
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.
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.
| 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.
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.
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.