Spaces:
Running
Running
| """Author RAG Chatbot SaaS β Purchase Objection Detector. | |
| Classifies reader pushback so the LLM can counter the SPECIFIC concern | |
| instead of giving a generic reply. An objection is a buying signal β | |
| the reader is weighing the purchase, not leaving. | |
| RULE: Objections are modifiers on top of intent β they never block the pipeline. | |
| RULE: Objection turns are never cached (responses are session-personalized). | |
| """ | |
| import re | |
| import structlog | |
| logger = structlog.get_logger(__name__) | |
| # Objection type β trigger phrases. Ordered most-specific first inside each list. | |
| _OBJECTION_PATTERNS: dict[str, tuple[str, ...]] = { | |
| # "Too expensive / not worth the money" | |
| "price": ( | |
| "too expensive", "too pricey", "costs too much", "cost too much", | |
| "can't afford", "cannot afford", "cheaper", "expensive for", | |
| "not worth the money", "waste of money", "that price", "overpriced", | |
| ), | |
| # "No time to read / too long" | |
| "time": ( | |
| "no time to read", "don't have time", "do not have time", | |
| "too long", "too many pages", "never finish", "takes too long", | |
| "busy to read", "too busy", | |
| ), | |
| # "Not my topic / no interest in the subject" | |
| "relevance": ( | |
| "no interest in", "not interested in", "not into", | |
| "don't care about", "do not care about", "not my thing", | |
| "not for me", "doesn't apply to me", "why would i need", | |
| "don't like politics", "boring topic", "sounds boring", | |
| ), | |
| # "Is it actually good? Prove it." | |
| "skepticism": ( | |
| "is it actually good", "is it any good", "why should i trust", | |
| "prove it", "sounds like hype", "everyone says that", | |
| "heard that before", "what makes it different", "how is it different", | |
| "why is it special", "what's so special", | |
| ), | |
| # "I only read <other genre>" | |
| "genre": ( | |
| "i only read", "i usually read", "i prefer fiction", "i prefer non-fiction", | |
| "not my genre", "don't read this genre", "do not read this genre", | |
| ), | |
| # "I already read something like this" | |
| "alternatives": ( | |
| "already read", "read something similar", "similar book", | |
| "same as", "just like every other", "nothing new", | |
| ), | |
| } | |
| # Generic value-challenge phrasing β relevance objection when no specific type hits | |
| _GENERIC_OBJECTION_MARKERS: tuple[str, ...] = ( | |
| "why should i buy", "why would i buy", "why should i get", | |
| "convince me", "why bother", "give me one reason", | |
| "not sure if i should", "should i really", | |
| ) | |
| _WS_RE = re.compile(r"\s+") | |
| def detect_objection(query: str) -> str | None: | |
| """Detect and classify a purchase objection in the reader's message. | |
| Args: | |
| query: Sanitized user message. | |
| Returns: | |
| Objection type ('price', 'time', 'relevance', 'skepticism', | |
| 'genre', 'alternatives') or None if no objection detected. | |
| """ | |
| q = _WS_RE.sub(" ", query.lower().strip()) | |
| for objection_type, phrases in _OBJECTION_PATTERNS.items(): | |
| if any(phrase in q for phrase in phrases): | |
| logger.debug("Objection detected", type=objection_type, query=q[:60]) | |
| return objection_type | |
| if any(marker in q for marker in _GENERIC_OBJECTION_MARKERS): | |
| logger.debug("Generic value objection detected", query=q[:60]) | |
| return "relevance" | |
| return None | |
| def count_prior_objections(history: list[dict]) -> int: | |
| """Count how many previous user messages contained an objection. | |
| Used to vary the counter-angle β never repeat the same persuasion | |
| twice, and soften after repeated hesitation. | |
| Args: | |
| history: Session history as [{role, content}] dicts. | |
| Returns: | |
| Number of prior user turns that contained an objection. | |
| """ | |
| count = 0 | |
| for message in history: | |
| if message.get("role") != "user": | |
| continue | |
| if detect_objection(message.get("content", "")): | |
| count += 1 | |
| return count | |