Arag / app /services /pipeline /guards.py
AuthorBot
feat: let visitors re-list books mid-chat and switch selection
5543326
Raw
History Blame Contribute Delete
5.6 kB
"""pipeline/guards.py β€” Lightweight query guard detectors.
Pure Python, zero API cost. Used by both the pipeline core and the
intent classifier. Single source of truth β€” never duplicated elsewhere.
"""
# ── Full story / Spoiler ──────────────────────────────────────────────────────
_FULL_STORY_PHRASES: tuple[str, ...] = (
"complete story", "full story", "whole story", "entire story", "entire book",
"whole book", "full plot", "whole plot", "summarize the book", "summary of the book",
"tell me everything", "what happens in the book", "what happens in the story",
"end of the book", "how does it end", "how does the book end", "full summary",
"complete summary", "recap the book", "recap the story",
)
def is_full_story_request(query: str) -> bool:
"""Return True if the query asks for the complete plot / ending / full summary."""
q = query.lower()
return any(phrase in q for phrase in _FULL_STORY_PHRASES)
# ── Catalog / Book list ───────────────────────────────────────────────────────
_CATALOG_PHRASES: tuple[str, ...] = (
"uploaded book", "your books", "what books", "which books", "list book",
"books do you", "books you have", "books available", "about the books",
"tell me about book", "tell me about the book", "tell me about your book",
"tell me about the books",
"what is the book", "what's the book", "what is this book", "what's this book",
"in the catalog", "in your catalog",
"show me the book", "show me your book",
)
_CATALOG_EXACT: frozenset[str] = frozenset({
"books", "tell me about books", "about books", "the books",
})
def is_catalog_question(query: str) -> bool:
"""Return True if the query is asking about the author's book catalog."""
q = query.lower()
if any(phrase in q for phrase in _CATALOG_PHRASES):
return True
return q in _CATALOG_EXACT
_CATALOG_INTENTS: frozenset[str] = frozenset({"meta", "comparison", "book_comparison"})
def should_short_circuit_to_catalog(
query: str,
intent: str,
selected_book_id: str | None,
) -> bool:
"""Return True only when the reader needs the book picker β€” not mid-book Q&A.
Comparison/meta intents while a book is already selected are substantive
questions (e.g. 'why this book vs others?') and must reach RAG.
If a book is already selected, catalog-ish phrasing like "what is the book"
/ "tell me about the book" means the *current* book β€” do not bounce to the picker.
"""
if selected_book_id:
# Explicit "list my books / show catalog" still opens the picker
q = query.lower()
list_phrases = (
"your books", "what books", "which books", "list book",
"books do you", "books you have", "books available",
"about the books", "tell me about your book", "tell me about the books",
"in the catalog", "in your catalog", "show me your book",
"uploaded book",
"show books", "show me books", "show the books",
"change book", "switch book", "another book", "other books",
"different book", "pick another", "choose another", "select another",
"book list", "list of books",
)
if any(p in q for p in list_phrases) or q in _CATALOG_EXACT:
return True
return False
if is_catalog_question(query):
return True
if intent not in _CATALOG_INTENTS:
return False
return True
# ── Book selection turn ───────────────────────────────────────────────────────
def is_book_selection_turn(
query: str,
selected_book_id: str | None,
books: list,
turn_count: int = 0,
) -> bool:
"""Return True if this turn is the reader selecting a book by name.
Used to trigger the warm book-intro response instead of Q&A retrieval.
Only fires on the selection turn β€” never on later mentions of the title.
"""
if not selected_book_id:
return False
book = next((b for b in books if b.id == selected_book_id), None)
if not book:
return False
q = query.lower().strip().strip("\"'")
title_lower = book.title.lower()
if q == title_lower:
# Exact title match after turn 0 is a re-mention, not a fresh selection.
return turn_count < 1
if q.startswith("tell me about") and title_lower in q:
return turn_count < 1
return q.startswith("i'm interested in") and title_lower in q
# ── Greeting ─────────────────────────────────────────────────────────────────
# NOTE: Intentionally kept here as the SINGLE source of truth.
# Import this function anywhere greeting detection is needed.
# Do NOT redefine _is_greeting in other modules.
_GREETINGS: frozenset[str] = frozenset({
"hi", "hello", "hey", "hiya", "howdy", "yo", "sup",
"good morning", "good afternoon", "good evening", "good day",
})
def is_greeting(query: str) -> bool:
"""Return True if the query is a simple greeting with no book content."""
q = query.strip().lower().rstrip("!.?")
return q in _GREETINGS or (len(q) <= 12 and q.startswith(("hi ", "hey ", "hello ")))