Spaces:
Running
Running
AuthorBot Cursor commited on
Commit Β·
2bb94f4
1
Parent(s): 2e8c5a3
Humanize bot responses for greetings, catalog questions, and empty RAG retrieval.
Browse files- app/admin/templates/admin.html +1 -1
- app/models/user.py +1 -1
- app/services/prompter.py +20 -7
- app/services/rag_pipeline.py +130 -5
app/admin/templates/admin.html
CHANGED
|
@@ -678,7 +678,7 @@
|
|
| 678 |
<div class="form-group"><label class="form-label">Response Style</label>
|
| 679 |
<select class="form-input" id="p-style"><option value="balanced">Balanced</option><option value="formal">Formal</option><option value="casual">Casual</option><option value="enthusiastic">Enthusiastic</option></select>
|
| 680 |
</div>
|
| 681 |
-
<div class="form-group"><label class="form-label">Fallback Message (when bot can't answer)</label><textarea class="form-input" id="p-fallback" placeholder="I
|
| 682 |
<div class="form-group"><label class="form-label">Out-of-Scope Message (non-book questions)</label><textarea class="form-input" id="p-outofscope" placeholder="I'm specialized in discussing books..."></textarea></div>
|
| 683 |
<button class="btn btn-primary" id="save-personality-btn">Save Personality Settings</button>
|
| 684 |
</div>
|
|
|
|
| 678 |
<div class="form-group"><label class="form-label">Response Style</label>
|
| 679 |
<select class="form-input" id="p-style"><option value="balanced">Balanced</option><option value="formal">Formal</option><option value="casual">Casual</option><option value="enthusiastic">Enthusiastic</option></select>
|
| 680 |
</div>
|
| 681 |
+
<div class="form-group"><label class="form-label">Fallback Message (when bot can't answer)</label><textarea class="form-input" id="p-fallback" placeholder="I'm not sure about that detail β but I can tell you plenty about the books. What would you like to know?"></textarea></div>
|
| 682 |
<div class="form-group"><label class="form-label">Out-of-Scope Message (non-book questions)</label><textarea class="form-input" id="p-outofscope" placeholder="I'm specialized in discussing books..."></textarea></div>
|
| 683 |
<button class="btn btn-primary" id="save-personality-btn">Save Personality Settings</button>
|
| 684 |
</div>
|
app/models/user.py
CHANGED
|
@@ -44,7 +44,7 @@ class User(Base, TimestampMixin):
|
|
| 44 |
)
|
| 45 |
fallback_message: Mapped[str] = mapped_column(
|
| 46 |
String(500),
|
| 47 |
-
default="I
|
| 48 |
nullable=False,
|
| 49 |
)
|
| 50 |
response_style: Mapped[str] = mapped_column(
|
|
|
|
| 44 |
)
|
| 45 |
fallback_message: Mapped[str] = mapped_column(
|
| 46 |
String(500),
|
| 47 |
+
default="I'm not sure about that specific detail β but I can tell you plenty about the books. What would you like to know?",
|
| 48 |
nullable=False,
|
| 49 |
)
|
| 50 |
response_style: Mapped[str] = mapped_column(
|
app/services/prompter.py
CHANGED
|
@@ -109,6 +109,9 @@ Rules:
|
|
| 109 |
|
| 110 |
INTENT_CLASSIFICATION_PROMPT = """Classify this reader message for a book sales chatbot.
|
| 111 |
|
|
|
|
|
|
|
|
|
|
| 112 |
MESSAGE: {query}
|
| 113 |
|
| 114 |
Output ONLY a JSON object:
|
|
@@ -148,14 +151,24 @@ COMPETITOR_RESPONSE = """I'm specifically focused on {author_name}'s work, \
|
|
| 148 |
so I can't speak to other authors. But I'd love to show you what makes \
|
| 149 |
{author_name}'s approach different β what are you hoping a book will help you with?"""
|
| 150 |
|
| 151 |
-
NO_CONTEXT_RESPONSE = """I
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
-
|
| 156 |
-
accurate information. Let me point you to exactly where you can find this β \
|
| 157 |
-
the answer is in {author_name}'s book, and I'd hate to paraphrase it poorly. \
|
| 158 |
-
Is there something more specific I can help you find?"""
|
| 159 |
|
| 160 |
TOKEN_EXHAUSTED_RESPONSE = "I'm taking a short break to recharge! Check back soon."
|
| 161 |
|
|
|
|
| 109 |
|
| 110 |
INTENT_CLASSIFICATION_PROMPT = """Classify this reader message for a book sales chatbot.
|
| 111 |
|
| 112 |
+
RECENT CONVERSATION:
|
| 113 |
+
{history}
|
| 114 |
+
|
| 115 |
MESSAGE: {query}
|
| 116 |
|
| 117 |
Output ONLY a JSON object:
|
|
|
|
| 151 |
so I can't speak to other authors. But I'd love to show you what makes \
|
| 152 |
{author_name}'s approach different β what are you hoping a book will help you with?"""
|
| 153 |
|
| 154 |
+
NO_CONTEXT_RESPONSE = """Hmm, I don't have that exact detail in front of me right now. \
|
| 155 |
+
{book_hint}What would you like to know β the story, the themes, or who it's perfect for?"""
|
| 156 |
+
|
| 157 |
+
HALLUCINATION_FALLBACK_RESPONSE = """I want to stay accurate rather than guess. \
|
| 158 |
+
{book_hint}Ask me something specific β a character, a theme, or what the book helps with β \
|
| 159 |
+
and I'll answer from what's actually in the text."""
|
| 160 |
+
|
| 161 |
+
GREETING_RESPONSE = """Hey! I'm {bot_name}, {author_name}'s book advisor.
|
| 162 |
+
|
| 163 |
+
{book_teaser}
|
| 164 |
+
|
| 165 |
+
What are you in the mood for β a quick recommendation, or curious about a specific book?"""
|
| 166 |
+
|
| 167 |
+
CATALOG_RESPONSE = """Here's what's in the library right now:
|
| 168 |
+
|
| 169 |
+
{book_list}
|
| 170 |
|
| 171 |
+
Tell me which one catches your eye, or describe what you're looking for and I'll point you to the best fit."""
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
TOKEN_EXHAUSTED_RESPONSE = "I'm taking a short break to recharge! Check back soon."
|
| 174 |
|
app/services/rag_pipeline.py
CHANGED
|
@@ -41,6 +41,7 @@ from app.services.prompter import (
|
|
| 41 |
MASTER_SYSTEM_PROMPT,
|
| 42 |
JAILBREAK_RESPONSE, OFF_TOPIC_RESPONSE,
|
| 43 |
NO_CONTEXT_RESPONSE, HALLUCINATION_FALLBACK_RESPONSE,
|
|
|
|
| 44 |
)
|
| 45 |
from app.services.reranker import rerank_chunks
|
| 46 |
from app.services.vector_store import retrieve_chunks
|
|
@@ -178,6 +179,13 @@ async def run_pipeline(
|
|
| 178 |
if not active_books:
|
| 179 |
return _no_books_response(start_ms)
|
| 180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
# Resolve which book to search
|
| 182 |
target_book_id = await _resolve_book(
|
| 183 |
intent_result, session_context, active_books, author.id
|
|
@@ -208,7 +216,7 @@ async def run_pipeline(
|
|
| 208 |
|
| 209 |
if not raw_chunks:
|
| 210 |
log.warning("No chunks retrieved")
|
| 211 |
-
return _no_context_response(query, author, start_ms)
|
| 212 |
|
| 213 |
# ββ Step 6: Re-ranking ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 214 |
top_chunks = await rerank_chunks(
|
|
@@ -219,7 +227,11 @@ async def run_pipeline(
|
|
| 219 |
)
|
| 220 |
|
| 221 |
if not top_chunks:
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
|
| 224 |
# ββ Step 7: Context Assembly βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 225 |
context_str, context_tokens = build_context(top_chunks)
|
|
@@ -264,7 +276,7 @@ async def run_pipeline(
|
|
| 264 |
is_faithful2, faithfulness_score = await check_faithfulness(raw_response, top_chunks)
|
| 265 |
if not is_faithful2:
|
| 266 |
raw_response = HALLUCINATION_FALLBACK_RESPONSE.format(
|
| 267 |
-
|
| 268 |
)
|
| 269 |
|
| 270 |
# ββ Step 10: Scope Check ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -426,9 +438,122 @@ def _boundary_response(text: str, start_ms: float, violation_type: str) -> Pipel
|
|
| 426 |
)
|
| 427 |
|
| 428 |
|
| 429 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
elapsed_ms = int((time.monotonic() - start_ms) * 1000)
|
| 431 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
return PipelineResult(
|
| 433 |
response={"text": text, "links": [], "has_links": False},
|
| 434 |
response_ms=elapsed_ms,
|
|
|
|
| 41 |
MASTER_SYSTEM_PROMPT,
|
| 42 |
JAILBREAK_RESPONSE, OFF_TOPIC_RESPONSE,
|
| 43 |
NO_CONTEXT_RESPONSE, HALLUCINATION_FALLBACK_RESPONSE,
|
| 44 |
+
GREETING_RESPONSE, CATALOG_RESPONSE,
|
| 45 |
)
|
| 46 |
from app.services.reranker import rerank_chunks
|
| 47 |
from app.services.vector_store import retrieve_chunks
|
|
|
|
| 179 |
if not active_books:
|
| 180 |
return _no_books_response(start_ms)
|
| 181 |
|
| 182 |
+
# Short-circuit: greetings and catalog questions (no vector search needed)
|
| 183 |
+
if intent_result.intent == "greeting" or _is_greeting(query):
|
| 184 |
+
return _greeting_response(author, active_books, start_ms)
|
| 185 |
+
|
| 186 |
+
if intent_result.intent in ("meta", "comparison") or _is_catalog_question(query):
|
| 187 |
+
return _catalog_response(author, active_books, start_ms)
|
| 188 |
+
|
| 189 |
# Resolve which book to search
|
| 190 |
target_book_id = await _resolve_book(
|
| 191 |
intent_result, session_context, active_books, author.id
|
|
|
|
| 216 |
|
| 217 |
if not raw_chunks:
|
| 218 |
log.warning("No chunks retrieved")
|
| 219 |
+
return _no_context_response(query, author, active_books, start_ms)
|
| 220 |
|
| 221 |
# ββ Step 6: Re-ranking ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 222 |
top_chunks = await rerank_chunks(
|
|
|
|
| 227 |
)
|
| 228 |
|
| 229 |
if not top_chunks:
|
| 230 |
+
log.warning("Re-ranker filtered all chunks β using top retrieval results")
|
| 231 |
+
top_chunks = raw_chunks[: cfg.RAG_RERANK_TOP_N]
|
| 232 |
+
|
| 233 |
+
if not top_chunks:
|
| 234 |
+
return _no_context_response(query, author, active_books, start_ms)
|
| 235 |
|
| 236 |
# ββ Step 7: Context Assembly βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 237 |
context_str, context_tokens = build_context(top_chunks)
|
|
|
|
| 276 |
is_faithful2, faithfulness_score = await check_faithfulness(raw_response, top_chunks)
|
| 277 |
if not is_faithful2:
|
| 278 |
raw_response = HALLUCINATION_FALLBACK_RESPONSE.format(
|
| 279 |
+
book_hint=_book_hint(active_books),
|
| 280 |
)
|
| 281 |
|
| 282 |
# ββ Step 10: Scope Check ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 438 |
)
|
| 439 |
|
| 440 |
|
| 441 |
+
_CATALOG_PHRASES = (
|
| 442 |
+
"uploaded book", "your book", "what book", "which book", "list book",
|
| 443 |
+
"books do you", "books you have", "books available", "about the book",
|
| 444 |
+
"tell me about", "what is the book", "what's the book", "book about",
|
| 445 |
+
"in the catalog", "in your catalog",
|
| 446 |
+
)
|
| 447 |
+
|
| 448 |
+
_GREETINGS = frozenset({
|
| 449 |
+
"hi", "hello", "hey", "hiya", "howdy", "yo", "sup",
|
| 450 |
+
"good morning", "good afternoon", "good evening", "good day",
|
| 451 |
+
})
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
def _is_greeting(query: str) -> bool:
|
| 455 |
+
q = query.strip().lower().rstrip("!.?")
|
| 456 |
+
return q in _GREETINGS or (len(q) <= 12 and q.startswith(("hi ", "hey ", "hello ")))
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
def _is_catalog_question(query: str) -> bool:
|
| 460 |
+
q = query.lower()
|
| 461 |
+
return any(phrase in q for phrase in _CATALOG_PHRASES)
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
def _format_book_list(books: list) -> str:
|
| 465 |
+
lines = []
|
| 466 |
+
for book in books:
|
| 467 |
+
line = f"π **{book.title}**"
|
| 468 |
+
if book.tagline:
|
| 469 |
+
line += f" β {book.tagline}"
|
| 470 |
+
elif book.ai_summary:
|
| 471 |
+
summary = book.ai_summary.strip().replace("\n", " ")
|
| 472 |
+
line += f" β {summary[:160]}{'β¦' if len(summary) > 160 else ''}"
|
| 473 |
+
elif book.description:
|
| 474 |
+
desc = book.description.strip().replace("\n", " ")
|
| 475 |
+
line += f" β {desc[:160]}{'β¦' if len(desc) > 160 else ''}"
|
| 476 |
+
if book.status != "ready":
|
| 477 |
+
line += " *(indexing in progress)*"
|
| 478 |
+
lines.append(line)
|
| 479 |
+
return "\n".join(lines)
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def _book_hint(books: list) -> str:
|
| 483 |
+
if not books:
|
| 484 |
+
return ""
|
| 485 |
+
if len(books) == 1:
|
| 486 |
+
return f"I do know **{books[0].title}** well. "
|
| 487 |
+
return f"I have {len(books)} books I can walk you through. "
|
| 488 |
+
|
| 489 |
+
|
| 490 |
+
def _greeting_response(author: User, books: list, start_ms: float) -> PipelineResult:
|
| 491 |
elapsed_ms = int((time.monotonic() - start_ms) * 1000)
|
| 492 |
+
if len(books) == 1:
|
| 493 |
+
book_teaser = f"I can tell you everything about **{books[0].title}** β themes, characters, who it's for, all of it."
|
| 494 |
+
else:
|
| 495 |
+
titles = ", ".join(b.title for b in books[:3])
|
| 496 |
+
extra = f" and {len(books) - 3} more" if len(books) > 3 else ""
|
| 497 |
+
book_teaser = f"I know {len(books)} books inside out β including {titles}{extra}."
|
| 498 |
+
text = GREETING_RESPONSE.format(
|
| 499 |
+
bot_name=author.bot_name,
|
| 500 |
+
author_name=author.full_name or "the author",
|
| 501 |
+
book_teaser=book_teaser,
|
| 502 |
+
)
|
| 503 |
+
return PipelineResult(
|
| 504 |
+
response={"text": text, "links": [], "has_links": False},
|
| 505 |
+
intent="greeting",
|
| 506 |
+
response_ms=elapsed_ms,
|
| 507 |
+
)
|
| 508 |
+
|
| 509 |
+
|
| 510 |
+
def _catalog_response(author: User, books: list, start_ms: float) -> PipelineResult:
|
| 511 |
+
elapsed_ms = int((time.monotonic() - start_ms) * 1000)
|
| 512 |
+
text = CATALOG_RESPONSE.format(book_list=_format_book_list(books))
|
| 513 |
+
return PipelineResult(
|
| 514 |
+
response={"text": text, "links": [], "has_links": False},
|
| 515 |
+
intent="meta",
|
| 516 |
+
response_ms=elapsed_ms,
|
| 517 |
+
)
|
| 518 |
+
|
| 519 |
+
|
| 520 |
+
def _no_context_response(query: str, author: User, books: list, start_ms: float) -> PipelineResult:
|
| 521 |
+
elapsed_ms = int((time.monotonic() - start_ms) * 1000)
|
| 522 |
+
|
| 523 |
+
# Prefer book metadata when vector search came up empty
|
| 524 |
+
if len(books) == 1:
|
| 525 |
+
book = books[0]
|
| 526 |
+
if book.ai_summary and _is_catalog_question(query):
|
| 527 |
+
text = (
|
| 528 |
+
f"**{book.title}** β here's the gist:\n\n{book.ai_summary.strip()}\n\n"
|
| 529 |
+
"Want to go deeper on characters, themes, or who it's best for?"
|
| 530 |
+
)
|
| 531 |
+
return PipelineResult(
|
| 532 |
+
response={"text": text, "links": [], "has_links": False},
|
| 533 |
+
intent="question",
|
| 534 |
+
response_ms=elapsed_ms,
|
| 535 |
+
)
|
| 536 |
+
if book.status != "ready":
|
| 537 |
+
text = (
|
| 538 |
+
f"**{book.title}** is still being indexed β give it a minute and ask again. "
|
| 539 |
+
"I'll be able to answer detailed questions once processing finishes."
|
| 540 |
+
)
|
| 541 |
+
return PipelineResult(
|
| 542 |
+
response={"text": text, "links": [], "has_links": False},
|
| 543 |
+
intent="question",
|
| 544 |
+
response_ms=elapsed_ms,
|
| 545 |
+
)
|
| 546 |
+
|
| 547 |
+
if len(books) > 1 and _is_catalog_question(query):
|
| 548 |
+
return _catalog_response(author, books, start_ms)
|
| 549 |
+
|
| 550 |
+
default_robotic = "I want to give you the most accurate answer"
|
| 551 |
+
fallback = author.fallback_message or ""
|
| 552 |
+
if fallback and default_robotic not in fallback:
|
| 553 |
+
text = fallback
|
| 554 |
+
else:
|
| 555 |
+
text = NO_CONTEXT_RESPONSE.format(book_hint=_book_hint(books))
|
| 556 |
+
|
| 557 |
return PipelineResult(
|
| 558 |
response={"text": text, "links": [], "has_links": False},
|
| 559 |
response_ms=elapsed_ms,
|