Spaces:
Running
Running
AuthorBot
Add objection intelligence: typed concern counters, personal anchors, stage-aware persuasion.
e0756f1 | """Objection intelligence β detection, typed counters, stage-aware persuasion.""" | |
| from app.services.objection import count_prior_objections, detect_objection | |
| from app.services.prompter import ( | |
| OBJECTION_HANDLING_INSTRUCTIONS, | |
| get_conversation_stage_directive, | |
| get_objection_instruction, | |
| get_reading_anchor, | |
| ) | |
| # ββ Detection βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_price_objection_detected(): | |
| assert detect_objection("this seems too expensive for me") == "price" | |
| assert detect_objection("I can't afford another book right now") == "price" | |
| def test_time_objection_detected(): | |
| assert detect_objection("i don't have time to read these days") == "time" | |
| assert detect_objection("looks too long, I'd never finish it") == "time" | |
| def test_relevance_objection_detected(): | |
| assert detect_objection("i have no interest in politics") == "relevance" | |
| assert detect_objection("this is not my thing honestly") == "relevance" | |
| def test_skepticism_objection_detected(): | |
| assert detect_objection("is it actually good or just hype?") == "skepticism" | |
| assert detect_objection("what makes it different from other books") == "skepticism" | |
| def test_genre_objection_detected(): | |
| assert detect_objection("i only read romance novels") == "genre" | |
| def test_alternatives_objection_detected(): | |
| assert detect_objection("i already read something similar last year") == "alternatives" | |
| def test_generic_value_challenge_maps_to_relevance(): | |
| assert detect_objection("so why should i buy this") == "relevance" | |
| assert detect_objection("convince me") == "relevance" | |
| def test_normal_question_is_not_objection(): | |
| assert detect_objection("who is the main character?") is None | |
| assert detect_objection("what is this book about") is None | |
| def test_prior_objections_counted_from_history(): | |
| history = [ | |
| {"role": "user", "content": "what is this about"}, | |
| {"role": "assistant", "content": "It explores..."}, | |
| {"role": "user", "content": "why should i buy this, i have no interest in politics"}, | |
| {"role": "assistant", "content": "Fair question..."}, | |
| ] | |
| assert count_prior_objections(history) == 1 | |
| # ββ Instructions ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_each_objection_type_has_instruction(): | |
| for obj_type in ("price", "time", "relevance", "skepticism", "genre", "alternatives"): | |
| assert obj_type in OBJECTION_HANDLING_INSTRUCTIONS | |
| assert len(OBJECTION_HANDLING_INSTRUCTIONS[obj_type]) > 40 | |
| def test_objection_instruction_empty_when_no_objection(): | |
| assert get_objection_instruction(None) == "" | |
| def test_repeat_objection_softens_the_counter(): | |
| first = get_objection_instruction("price", prior_objections=0) | |
| repeat = get_objection_instruction("price", prior_objections=1) | |
| assert len(repeat) > len(first) | |
| assert "do not repeat" in repeat.lower() | |
| # ββ Personal anchor βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_reading_anchor_uses_most_recent_tag(): | |
| anchor = get_reading_anchor(["genre", "characters"]) | |
| assert "characters" in anchor.lower() or "who they will meet" in anchor.lower() | |
| def test_reading_anchor_empty_without_tags(): | |
| assert get_reading_anchor([]) == "" | |
| # ββ Conversation stage ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_stage_discovery_on_early_turns(): | |
| directive = get_conversation_stage_directive(0, 0.0) | |
| assert "DISCOVERY" in directive | |
| def test_stage_decision_when_hot(): | |
| directive = get_conversation_stage_directive(6, 0.9) | |
| assert "DECISION" in directive | |
| def test_stage_consideration_mid_journey(): | |
| directive = get_conversation_stage_directive(4, 0.6) | |
| assert "CONSIDERATION" in directive | |