nanochat/modal/_query_classifier.py
Manmohan Sharma fd8e10a820
fix(classifier): expand identity veto to cover all self-introspection queries
Added patterns for: tell me about yourself / you / about you, what do/can you do, what are your capabilities / skills, how do you work, what are you good at, what's your purpose / story / mission, where did you come from, how were you built, are you an AI / chatbot / language model, model meta (model/version/context/training cutoff), creator socials (github/linkedin/twitter), and more writing tasks (song, joke). All 27 identity cases now short-circuit without hitting Tavily.
2026-04-22 15:25:33 -07:00

244 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Heuristic query classifier for forced tool use.
This module answers a single question: given a user message, does it likely
need a fresh web search to be answered correctly?
We use this to *force* the model to call `web_search` even when its natural
generation path would skip the tool (e.g. "whos the present president" where
"present" doesn't appear in the tool-use SFT training distribution).
Two outputs:
needs_web_search(text) -> (bool, rewritten_query)
needs_calculator(text) -> (bool, expression)
Keep the rules simple and fast. False positives are cheap (we pay a Tavily
call); false negatives are the real cost (user gets stale training-data).
"""
from __future__ import annotations
import re
from typing import Tuple
# ---------------------------------------------------------------------------
# Web-search triggers
# ---------------------------------------------------------------------------
# Time words that almost always indicate the user wants fresh info
_TIME_WORDS = r"""
(?:
current | currently | now | today | tonight | tomorrow | yesterday |
present | this\ (?:week|month|year|morning|afternoon|evening) |
latest | recent | recently | upcoming | right\ now | as\ of |
20\d{2} | this\ very\ (?:moment|second|minute)
)
"""
# Role / position words that change over time
_POSITION_WORDS = r"""
(?:
president | vice[-\s]?president | prime\ minister | chancellor | governor |
senator | congressman | congresswoman | representative | mayor |
ceo | cto | cfo | coo | chairman | chief\s+executive |
chief\s+justice | speaker | foreign\s+minister | attorney\s+general |
pope | monarch | king | queen | emperor | dictator | leader
)
"""
# Topic categories that are inherently time-sensitive
_CATEGORY_PATTERNS = [
# weather / climate
r"\b(?:weather|temperature|forecast|humidity|rainfall|snowfall|wind\s+speed)\b",
# finance / markets
r"\b(?:stock\s+price|share\s+price|market\s+cap|exchange\s+rate|forex|crypto(?:currency)?\s+price)\b",
r"\b(?:nasdaq|s&p\s*500|dow\s+jones|nifty|sensex|ftse|nikkei)\b",
r"\bprice\s+of\s+(?:gold|silver|oil|bitcoin|ethereum)\b",
# news / events
r"\b(?:breaking\s+news|headlines?|latest\s+news|news\s+(?:today|now))\b",
r"\bwhat(?:'|\s+i)s\s+happening\s+(?:in|with|at)\b",
# sports scores
r"\b(?:score|result|winner|loser|champion|finalist)\b.*(?:match|game|tournament|series|cup|open|championship)",
r"\b(?:ipl|world\s+cup|super\s+bowl|nba|nfl|nhl|olympics?|wimbledon|us\s+open|australian\s+open|french\s+open)\b",
# time of day / where
r"\bwhat\s+time\s+is\s+it\b",
r"\btime\s+in\s+[A-Z][a-z]+\b",
# people (often changes) — VIPs, CEOs, recent releases
r"\bwho\s+(?:is|'s|runs|leads|owns|founded|heads)\b",
# "is X still alive" / "did X die"
r"\bis\s+\w+\s+(?:still\s+)?(?:alive|dead)\b",
# recent product / release
r"\b(?:latest|newest|recent|upcoming)\s+(?:version|release|model|iphone|android|macbook|tesla|game|album|movie|film|book)\b",
# recent statistics that change
r"\b(?:population|number\s+of)\s+\w+\s+(?:of|in)\b",
# explicit search instructions
r"\b(?:search|look\s+up|google|find\s+out|check)\s+(?:online|on\s+the\s+web|on\s+google)\b",
r"\buse\s+(?:the\s+)?web[_\s]?search\b",
]
# Standalone keyword patterns that, combined with any position/role, trigger search
_KEYWORD_GATE = re.compile(
rf"""
\b{_TIME_WORDS}\b # at least one time word
| \b(?:who|what|where|when)\b.*\b{_POSITION_WORDS}\b # who/what + position
| \b{_POSITION_WORDS}\b.*\b(?:of|in|for|at)\s+[A-Z] # position + proper noun (of America, in India)
""",
re.IGNORECASE | re.VERBOSE,
)
_CATEGORY_REGEXES = [re.compile(p, re.IGNORECASE) for p in _CATEGORY_PATTERNS]
# ---------------------------------------------------------------------------
# Negative veto — queries about the model itself or its creator should NOT
# trigger a web search. The model's SFT training has the correct grounded
# identity answers (samosaChaat, Manmohan Sharma, socials, etc.). Sending
# these to Tavily returns irrelevant results (Tyler the Creator, Waaree CFO, etc.)
# ---------------------------------------------------------------------------
_IDENTITY_VETO_PATTERNS = [
# self-referential questions directed at the model
r"\bwho\s+are\s+you\b",
r"\bwhat\s+are\s+you\b",
r"\bwho\s+are\s+you\s+really\b",
r"\bwhat(?:'|\s+i)s\s+your\s+name\b",
r"\bintroduce\s+yourself\b",
r"\btell\s+me\s+about\s+(?:yourself|you|you\s+first)\b",
r"\btell\s+me\s+(?:more\s+)?about\s+yourself\b",
r"\babout\s+you(?:rself)?\s*[?!.]*\s*$",
r"\bdescribe\s+yourself\b",
r"\bwhat(?:'|\s+i)s\s+your\s+(?:story|backstory|purpose|origin|deal|role|job|gig|mission|goal|objective)\b",
r"\bwhat\s+do\s+you\s+(?:do|know\s+about\s+yourself|stand\s+for)\b",
# creator / maker / trainer questions
r"\bwho\s+(?:is\s+)?(?:made|created|built|trained|developed|designed|coded|programmed|engineered|authored|invented|produced|fine[-\s]?tuned|wrote)\s+you\b",
r"\bwho(?:'|\s+i)s\s+(?:your|ur)\s+(?:creator|creater|maker|author|developer|designer|engineer|architect|founder|builder|programmer|trainer|daddy|mom|parent|boss|owner|father|mother|papa|mama)\b",
r"\bwho\s+(?:brought|gave)\s+you\s+(?:to\s+life|into\s+being|into\s+existence)\b",
r"\bwhere\s+(?:did\s+)?you\s+come\s+from\b",
r"\bhow\s+(?:did|were)\s+you\s+(?:come|born|made|created|built)\b",
# competitor/provenance questions (identity confusion attacks)
r"\bare\s+you\s+(?:chatgpt|gpt[-\s]?\d|claude|gemini|bard|llama|mistral|perplexity|copilot|sonnet|opus|haiku|grok)\b",
r"\bare\s+you\s+(?:made|created|built|owned|developed|trained)\s+by\s+(?:openai|anthropic|google|meta|microsoft|deepmind|x\.ai|xai)\b",
r"\bwhich\s+(?:company|organization|team|ai|model)\s+(?:made|created|built|trained|owns|are\s+you)\s*(?:you)?\b",
r"\bare\s+you\s+(?:an?\s+)?(?:ai|chat\s*bot|assistant|language\s+model|llm|robot)\b",
# samosaChaat / creator name references
r"\bsamosachaat\b",
r"\bwho(?:'|\s+i)s\s+manmohan(?:\s+sharma)?\b",
r"\bwho\s+is\s+manmohan\b",
r"\bmanmohan\s+sharma\b",
r"\btell\s+me\s+about\s+manmohan\b",
r"\bwhat(?:'|\s+i)s\s+(?:manmohan|your\s+creator)(?:'s)?\s+(?:github|linkedin|twitter|x|website|email|socials?)\b",
# model meta-questions
r"\bhow\s+(?:many|much)\s+parameters?\b",
r"\bwhat\s+(?:model|version|size|architecture|type|kind)\s+(?:are\s+you|of\s+(?:ai|model)\s+are\s+you)\b",
r"\bwhat(?:'|\s+i)s\s+your\s+(?:model|version|size|architecture|context\s+(?:size|length|window))\b",
r"\bare\s+you\s+(?:open[-\s]?source|open\s+weight|closed\s+source|proprietary)\b",
r"\bwhere\s+(?:can\s+i\s+)?(?:find|download|get)\s+your\s+(?:weights|code|source|github|repo)\b",
r"\bwhat\s+hardware\s+(?:were|are)\s+you\s+(?:trained|running)\b",
r"\bhow\s+(?:were|are)\s+you\s+trained\b",
r"\bwhen\s+were\s+you\s+(?:trained|released|built|made|created)\b",
r"\bwhat(?:'|\s+i)s\s+your\s+(?:training|knowledge)\s+(?:data|cut[-\s]?off|cutoff)\b",
r"\bwhat\s+data\s+(?:were|are)\s+you\s+trained\s+on\b",
# capability / tooling questions (not factual queries)
r"\bwhat\s+(?:tools|abilities|capabilities|languages|skills|features)\s+(?:do\s+)?you\s+(?:have|support|speak|offer|provide)\b",
r"\bwhat\s+(?:can|could)\s+you\s+(?:do|help\s+(?:me\s+)?with)\b",
r"\bcan\s+you\s+(?:search|do|use|access|help)\b",
r"\bwhat\s+are\s+you\s+(?:good\s+at|capable\s+of|able\s+to\s+do)\b",
r"\bhow\s+do\s+you\s+work\b",
# greetings & social
r"^(?:hi|hello|hey|yo|sup|greetings|namaste|good\s+(?:morning|afternoon|evening|night))\b",
r"\bhow\s+are\s+you\b",
r"\bwhat(?:'|\s+i)s\s+up\b",
r"\bnice\s+to\s+meet\s+you\b",
# general small talk / thanks
r"^\s*(?:thanks?|thank\s+you|thx|ty|ok|okay|cool|nice|great|awesome|bye|goodbye)\s*[!.?]*\s*$",
# writing / reasoning / coding tasks (answered by the model, not the web)
r"\bwrite\s+(?:a|an|me)\s+(?:poem|haiku|limerick|story|essay|letter|email|code|function|script|query|sql|song|joke)\b",
r"\bexplain\s+(?:what|how|why)\s+(?:is\s+)?(?:recursion|gradient\s+descent|backprop|attention|a\s+transformer|machine\s+learning|neural\s+network|rope|softmax)\b",
r"\bsolve\b.*=", # math equations
]
_IDENTITY_VETO_REGEXES = [re.compile(p, re.IGNORECASE) for p in _IDENTITY_VETO_PATTERNS]
def _is_identity_or_meta(text: str) -> bool:
for rx in _IDENTITY_VETO_REGEXES:
if rx.search(text):
return True
return False
def needs_web_search(text: str) -> Tuple[bool, str]:
"""Classify whether a user query likely needs a live web search.
Returns (needs, rewritten_query). The rewritten_query strips filler and
reformulates for better Tavily results (e.g. "whos the present president" ->
"who is the current president of the United States 2026").
Identity / meta / greeting / writing-task queries are vetoed — the model's
SFT training has the correct grounded answer.
"""
if not text or not isinstance(text, str):
return False, ""
stripped = text.strip()
if len(stripped) < 3:
return False, ""
# Veto: identity / self-referential / meta / greeting / writing tasks
if _is_identity_or_meta(stripped):
return False, ""
# Any category pattern hit
for rx in _CATEGORY_REGEXES:
if rx.search(stripped):
return True, _rewrite_query(stripped)
# Keyword gate (time words or position + specifier)
if _KEYWORD_GATE.search(stripped):
return True, _rewrite_query(stripped)
return False, ""
def _rewrite_query(text: str) -> str:
"""Clean up the query for Tavily — expand contractions, normalize 'present'->'current',
strip filler, add a year anchor."""
q = text.strip().rstrip("?.!")
# contractions
q = re.sub(r"\bwho(?:'| i)s\b", "who is", q, flags=re.IGNORECASE)
q = re.sub(r"\bwhat(?:'| i)s\b", "what is", q, flags=re.IGNORECASE)
q = re.sub(r"\bwhere(?:'| i)s\b", "where is", q, flags=re.IGNORECASE)
q = re.sub(r"\bwhen(?:'| i)s\b", "when is", q, flags=re.IGNORECASE)
q = re.sub(r"\bits\b", "it is", q, flags=re.IGNORECASE)
# strip quantifiers the model tends to hallucinate
q = re.sub(r"\b(please|kindly|could\s+you|can\s+you)\b\s*", "", q, flags=re.IGNORECASE)
# "present X" -> "current X" (better Tavily results)
q = re.sub(r"\bpresent\b", "current", q, flags=re.IGNORECASE)
# collapse whitespace
q = re.sub(r"\s+", " ", q).strip()
# anchor to the current year if no year is already present
if not re.search(r"\b20\d{2}\b", q):
q = q + " 2026"
return q
# ---------------------------------------------------------------------------
# Calculator triggers (cheap, local)
# ---------------------------------------------------------------------------
_CALC_RX = re.compile(
r"""
\b(?:calculate|compute|what\s+is|what's)\b.*?
(?:
\d[\d,\.\s]*\s*[+\-\*/x×÷]\s*\d # basic arithmetic
| \d+\s*%\s+(?:of|tip|tax|discount)\s+\d # percentage
| \b(?:emi|cagr|compound\s+interest|tip|discount|percent(?:age)?)\b.*\d
)
""",
re.IGNORECASE | re.VERBOSE,
)
def needs_calculator(text: str) -> Tuple[bool, str]:
if not text:
return False, ""
m = _CALC_RX.search(text)
if not m:
return False, ""
return True, text.strip()