Spaces:
Running
Running
| """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 "))) | |