Skip to content

Answer quality: leaner prompt, consent-only handoff, company facts, credential checks - #474

Merged
jadhavgaurav merged 23 commits into
mainfrom
development
Sep 17, 2026
Merged

jadhavgaurav merged 23 commits into
mainfrom
development

Conversation

@jadhavgaurav

Copy link
Copy Markdown
Collaborator

Answer-quality batch from the 2026-09-17 production evaluation (reviewed pass rate before this PR: Eventus 80%, CleanStart 77%) and the same-day review of the answer system prompt.

What changes for visitors

  • Answer prompt restructured (QA_PROMPT_VERSION 5 to 6). About 3,700 to 4,000 tokens, down from 8,000 to 8,500. One priority order instead of five competing "highest priority" blocks; one length and depth rule ("tell me more" gets real detail, ordinals resolve from history, never a bare link); the gap line is used only when a fact is truly missing; first-person claims (certifications, reports, SLAs, terms, locations) only from the company's own pages; team offers are questions the visitor can accept; featured services are not exhaustive, so a bot no longer denies services it sells; frustration gets a next step; general coding help is out of scope; mixed pricing turns answer the non-price part.
  • Handoff form only with consent. Model-written text no longer opens the form. It opens when the visitor asks for a person, says yes to an offer, or a fixed route decides it. After a support alert a new question is answered. "yes" to the greeting offer continues with the services. More offer wordings are recognised.
  • Company facts. Questions about locations, offices, HQ, SOC or delivery centres, contact details, countries and leadership pull the company's own contact, locations, about and team pages into the context. Production case: "where are the soc centers?" on Eventus was refused although the contact page lists Jeddah, Doha, Ahmedabad and Mumbai. Partnership, credential and continuation follow-ups are no longer refused as off-topic.
  • Credential facts. Before answering a certification or compliance question, one bounded gate-model check classifies each credential as held, offered as a service, not found or unverified, and the answer prompt gets that verdict. These turns skip the QA cache.
  • Mixed requests and cards. A mixed pricing turn keeps its non-price sentences. A file plus a meeting request gets both cards. Download cards are deduplicated and "a pdf of this" reuses the previous file. Hinglish call and meeting requests get the booking card. Off-site files are measured (media_file_offsite) but not dropped yet.
  • Care before everything. Crisis wording is answered first, before urgent, support, the name step, moderation and the router, with no sales route and no invented numbers. Frustration and insults get an acknowledgement and a next step; medication questions go to a doctor or pharmacist; "do u remember me" gets the memory reply.
  • No more "sits with the team". The canned no-info reply says "I don't have that detail here." with an acceptable team offer on paid plans.

Review

An independent review of the combined diff found crisis-route ordering, a live-support promise the code no longer backed, garbled option continuations, file-host false negatives, credential and company-facts false positives and a Hinglish misread. All are fixed in the last commits.

Deploy notes

  • Backend only in this batch; no migration.
  • Development also carries earlier commits not yet on main: settings defaults (owner contact defaults and effective recipients) and widget launcher and CSP changes. Merging deploys those too (backend, and the widget workflow for widget changes).
  • New metrics: company_facts_pinned, credential_facts_checked, media_file_offsite, handoff_offer_awaiting_consent, affirmed_offered_option, support_repeat_answered, and the redactor and card metrics.
  • Model calls: one extra gate-tier call only on credential questions (bounded at 2.5 s, parallel with the relevance judge); company-facts turns add two small database lookups in parallel with search.

Checks

  • Ruff clean.
  • Full backend suite: 13,318 passed, 5 skipped, 1 deselected (test_superadmin_impersonate_mint::test_redirect_url_uses_the_shared_app_url_setting, which fails locally on the base commit too).
  • Every classifier is mocked in tests. Real-model behaviour is checked after deploy by re-running the 120-case evaluation plus 43 new held-out cases on Eventus and CleanStart.

Follow-ups (not in this PR)

  • Widget: the automatic form after two fallback-looking replies bypasses consent.
  • Switch the off-site file filter to enforce after a week of the metric.
  • Owner content: Eventus services list and company description, Riyadh vs Jeddah SOC location, an "ISO 27001 certified" listicle line; CleanStart draft vendor-risk page.

An answer that offered the team ("I can connect you with them if you want")
set suggest_handoff from the model's own words, and the widget opened the
form with nobody saying yes (CleanStart x-tech-mttd-sla, 2026-09-17 eval).
The offer now only waits for the visitor: it is counted as
handoff_offer_awaiting_consent and kept out of the QA cache, and a bare
"yes" to it opens the form through the existing affirmation path.

After the support reply, a new question such as "i need the escalation
matrix now" got the repeat form line. The repeat is now kept for a visitor
who asks for a person or reports a problem again; any other message is
answered while the form stays in the chat (support_repeat_answered).

A bare "yes", "sure" or "ok" to an offer with options (the greeting's
"Want to hear about our services, see recent work, or chat with the team?")
got "Got it." It now continues with the first option, so the greeting and
the name welcome answer "what services do you offer". A team-first offer is
still a handoff, and "ok" after an ordinary answer is still an ack.
On a managed SOC's bot (about 7,900 chunks) "where are the soc centers ?"
was refused as off-topic and "list soc centers" was answered with the
services list. The SOC cities are on the contact page, but hybrid search
and rerank never put that chunk in the top 15.

A question about the company's locations, contact details or people is
now detected by a linear prefilter and counts as clearly on scope. On
the retrieval path it also reads up to four chunks of the bot's own
contact, locations, about and team pages (chosen by URL path) and puts
them ahead of the retrieved chunks, after the rerank, so the relevance
judge sees them. The list keeps its 15-result length, CAG-lite is left
alone, and each pin emits company_facts_pinned.
From the 2026-09-17 evaluation, on-scope turns that still got the scope
line:

- "whats the margin for channel partners" after a partner program
  answer: partnership words now count as on scope.
- "what certifications do you have": the assurance check now also reads
  the term before "do you", and knows accreditations, awards and
  empanelment.
- "paid or unpaid? and is remote ok" and "d'accord, et c'est disponible
  en France ?" after an answer: retrieval searched the fragment's own
  words, so no chunk bore on it. The judge is now told that a follow-up
  about the business's offering scores 0.5 when the previous reply is
  about that subject, and a rejected fragment shaped as a continuation
  gets the no-info pivot instead of the scope line.
…ration

The 2026-09-17 evaluation found the model stating certifications and reports
as the company's own when the knowledge base only had guides, mapping pages
or services ("Our SOC 2 Type 2 report is typically shared under NDA").

credential_facts adds a linear prefilter for questions about the company's
own certifications, a fenced gate-tier check over the credential sentences
of the retrieved chunks (HELD with a verified quote, OFFERED, NOT_FOUND),
precision-first fallback rules and a self-contained CREDENTIAL FACTS block.
The test suite defaults the check's model to down, like the price and
support classifiers.
A credential question starts the check once the chunks are final, runs it
alongside the relevance gate and puts the block after the reference
information. The question neither reads nor writes the QA cache, so an
answer cached before the check is never replayed past it. A non-English
turn skips the check like the other English-tuned judges. The metric
credential_facts_checked carries the verdict counts.
The production review of 2026-09-17 (240 conversations on two live bots)
traced most answer defects to the prompt itself: five blocks each claimed to
override everything, the SERVICES list made a bot deny services its website
sells, length rules starved follow-ups, the gap path fired on facts that were
present, and team offers read like a handoff already under way.

Prompt (system 8,027 and 8,514 tokens down to 3,665 and 4,023 on the two bots):
- One PRIORITY ORDER near the top replaces every "HIGHEST PRIORITY",
  "OVERRIDES" and "wins over every other instruction" claim.
- FEATURED SERVICES is explicitly not exhaustive; a denial needs both the
  list and the reference material silent; location, SOC centre, office and
  team questions are not services questions.
- RULE 1 LENGTH AND DEPTH: short facts stay short, lists are complete,
  "tell me more" and ordinal follow-ups resolve the item from history and
  give at least three facts, never only a link.
- RULE 5a gap path only for an absent fact; "sits with our team" retired;
  reports and office or SOC locations are own claims; listicles, templates,
  comparison articles and provider-choice pages describe the topic.
- TEAM OFFERS: a question the visitor can accept; no form announced unless
  the visitor asked for a person. Example offers are recognised by
  intent_service.bot_offers_handoff and not by _HANDOFF_RESPONSE_RE.
- SCOPE: small talk merged in, FRUSTRATION gets an acknowledgement and a next
  step, general coding help is out of scope, comparisons stay in.
- ABOUT replaces COMPANY CONTEXT and says the summary may be third person
  and ranks below the reference material.
- Dates said once (RULE 6); RULE 0, 2b, 6a to 6c, 7 to 11 and the style
  block's duplicates removed; examples that used banned openers or invented
  roles removed; no dashes in the per-turn qualification text.
- New per-turn line for a mixed pricing turn (gate outcome
  escalate_deferred): answer the rest, state no figure. Wired through
  build_hybrid_prompt(pricing_mixed=...).
- The prompt leak detector keys on the new PRIORITY ORDER heading.

QA_PROMPT_VERSION 5 -> 6 with a new fingerprint; 5 is kept.

Tests changed on purpose:
- test_answer_prompt_structure.py (new): priority order, length rule, gap
  rule, team offers against both regexes, featured services, scope,
  frustration, ABOUT, mixed pricing line and its wiring, dashes, invented
  roles, leak sentinel.
- test_answer_prompt_claims_capabilities_comparisons.py: SCOPE anchor
  renamed; the services test pinned "say plainly that we do not offer it",
  the behaviour that produced the false denials, and now pins the
  both-sources condition instead.
- test_brand_tone_guarded.py, test_rag_prompt_hardening.py: SCOPE anchor
  renamed. The "SCOPE line says above and below" check became a check that
  the priority order ranks grounding and scope above business instructions.
- test_em_dash_prompt_rules.py: RULE 11 duplicated the style block's rule and
  was removed, so the check reads the assembled prompt (rule present once)
  instead of the builder source.
- test_prompt_has_one_owner_per_rule.py: "1-3 sentences" became
  "1 to 3 sentences", stated once; the style block's "fewest words" is gone.
- test_prompt_budget.py: ceilings 7,300/7,900/8,300/8,800 lowered to
  3,000/3,350/3,750/4,050; media delta bound 900 lowered to 500.
- test_qa_cache_prompt_version.py: fingerprint for version 6.
Eval 2026-09-17 (defect 6): "this bot is absolute trash" got "Sorry to
hear that." and nothing else, "stupid bot" got a scope refusal, and a
visitor asking which sleeping tablets to take got the off-topic redirect.

The router now reads insults aimed at the bot as frustration, and its
frustration and abuse replies acknowledge the feeling in one clause, ask
for one thing at a time and offer the team when the plan has one. A reply
right after a reaction reply uses new words and no second apology, in the
router and in the dissatisfied reply. Praise sent with an annoyed face is
left to the dissatisfaction check instead of being thanked.

Self-harm and medical emergency signals get care and a pointer to the
local emergency number or a crisis helpline, with no sales route, before
moderation and before the name request. A request for medication gets a
suggestion to see a doctor or pharmacist.
…ry reply

"do u remember me?" missed the remember route, reached the relevance gate
and opened the unhelped handoff form in production. The route now reads
the "u" and "ya" forms, a bare "remember me" and common misspellings,
as the whole message only, so a question about a "remember me" login
still reaches retrieval.
… the team'

The canned no-info reply for an on-scope question the knowledge base cannot
answer opened with "That specific detail sits with the team", the evasive line
the answer prompt now forbids. It now says "I don't have that detail here."
On a paid plan it ends with a question the visitor can accept ("Want me to
loop in the team on this?"), which bot_offers_handoff reads as an offer, so a
following yes opens the form; Free plans keep the contact link or the
something-else line and never offer the team. The widget's own fallback
pattern does not match the new wording, so it does not open the form by itself.
…pped

A message that asks the price and something else lost the something
else: the price guard replaced the whole answer with the escalation.
On a MIXED price decision a sentence redactor now drops only the
sentences that state a figure (whole leads and tables where a drop
would leave them dangling), and the escalation follows the rest.
Stream and whole-answer outcomes stay equal; PRICE turns are unchanged.
…urns together

A Hinglish request for time ("kal team se call pe baat ho sakti hai
kya", "mujhe demo chahiye", "meeting set karo") was not read as a
meeting request, so the handoff reply opened the form on bots with a
scheduler. meeting_gate now reads those shapes, refusals and things
("call nahi chahiye", "call center") excluded, and the stream attaches
the booking card when the model leaves it out.

"send the datasheet and book a demo" kept only one of the two. The
files are now picked before the meeting gate, and the document reply
carries the booking card, or the meeting pivot when no scheduler is
configured.
A conversation held in another language keeps its model: the meeting
pivot and the handoff skip the Hinglish shapes lead to are English.
…y the bot's own files

- media_cards: a card's identity is its normalised URL and, for a name
  that says what the file is about, its normalised title. The document
  pick, the secondary chip and the per-session dedupe use it, so one
  case study stored at two URLs is offered once.
- A document request that points back ("is there a pdf of this i can
  share with my boss") reuses the card the last reply carried, or the
  topic of the visitor's previous message.
- The media catalog, the whitelist, the topical card and the chip admit
  a file only on the bot's own registrable domains (website and allowed
  domains) or a website builder or storage host. A bot with no website
  is not filtered. Videos are unchanged.
…n accept

The LIVE SUPPORT block told the model to say a team member would be with the
visitor shortly. Model text no longer opens the handoff form and that sentence
is no question, so the visitor never said yes and no form opened. The block
now asks "Would you like to speak with our team now?", and the offline block
ends with "Want me to take a message for our team?". Both examples are read
by bot_offers_handoff and not by _HANDOFF_RESPONSE_RE. The v6 prompt
fingerprint is re-recorded; v6 has not been deployed.
…offers

offered_option_question turned actions into "tell me about your book an
appointment" and split a listed topic at its commas. It now continues only
when the first option is a topic (a stripped verb such as hear or see, or an
opening such as interested in or exploring), keeps an "A, B and C" list whole,
and returns None for an action so the handoff check decides.

bot_offers_handoff now reads question offers to talk with, discuss something
with, or arrange a call with our team, so a following sure is a handoff.
…sitives

The crisis reply lived in the intent router, behind the urgent route, the
support route and the name step. "my name is Sam, i want to kill myself"
saved the name and answered the deferred question; "i want to kill myself,
my account got hacked" and "i want to die, you charged me twice" alerted the
team with no care. rag_pipeline_stream now checks the visitor's own message
right after saving it and sends the router's crisis reply (repeat wording
included) with no classifier, moderation, model call or alert.

Healthcare and retail questions no longer get the crisis reply: breathing
"through", "in" or "with" something, "a stroke of" and chest pain beside a
booking or service question with nothing acute. That last one is answered
under a short emergency line and skips the QA cache. A dosage question that
says "your" or names the business reaches retrieval.
…he rule log-only

Wix, GoDaddy, WordPress.com, Dropbox, DigitalOcean Spaces and Firebase file
hosts and a same-name domain on another TLD now count as the bot's own. A file
the rule rejects is counted as media_file_offsite and logged with its host, and
still offered while ENFORCE_OWNED_FILES is False.
…ice requests

Gift, completion, participation, SSL/TLS and insurance certificates are not the
business's credentials, and supporting, selling or offering a compliant product
is a request for the product. The check's deadline drops to 2.5 s with the
model call at 2 s.
…pany's facts

A bare phone, whatsapp or owners counts only at the end of a request, a head
count needs the company as its object, and work, deliver, ship and available
count as a location question only with a country, city, region or market.
…person

A talk with the team asks for time only with a medium (call pe, demo ke liye)
or a time word (kal, shaam 5 baje), so a visitor who wants a person is no
longer handed a calendar.
@jadhavgaurav
jadhavgaurav merged commit a73dc43 into main Sep 17, 2026
6 checks passed
jadhavgaurav added a commit that referenced this pull request Sep 17, 2026
…ly the search rewrite

With history, the meeting check read the search rewrite, which can come back as
keywords ('datasheet and demo booking next week') that name no request. The
production smoke test after PR #474 sent 'send me your datasheet and also book a
demo for next week' on Eventus and CleanStart and got the file cards without the
booking card. The raw message now counts as well as the rewrite.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant