HumeraR's picture
refactor: improve chat sorting logic in NotebookView component for better handling of timestamps
e637380
Raw History Blame Contribute Delete
4.53 kB
"""Scope gate: the help bot explains how Notestack works. It never reads or discusses a writer's own notebooks,
posts, sources, files or generated work (the chat inside a notebook does that, with citations).
This runs before any model call, so the rule does not depend on the model behaving."""
import re
_ID_SEGMENT = re.compile(
r"/(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-f]{16,}|\d+)(?=/|$)", re.I
)
def scrub_path(path: str | None) -> str | None:
"""Keep only the route shape: no query string, no ids. /app/notebooks/<uuid> -> /app/notebooks/:id"""
if not path:
return None
path = path.split("?", 1)[0].split("#", 1)[0][:200]
if not path.startswith("/"):
return None
return _ID_SEGMENT.sub("/:id", path)
# "How do I ..." style questions are product help even when they mention "my notebook".
_HOWTO = re.compile(
r"^\s*(?:hi|hello|hey|please|ok|okay)?[\s,!.]*(?:how\s+(?:do|can|could|should|to|does|would)\b|i(?:\s+(?:really|just))?\s+(?:want|need|wanna|would\s+like|'d\s+like)\s+to\b|make\s+me\b|where\s+(?:do|can|is|are)\b|"
r"can\s+(?:i|you|notestack)\b|could\s+(?:i|you)\b|is\s+it\s+possible\b|which\s+plan\b|what\s+(?:does|is\s+a|is\s+the)\b|"
r"(?:can|could|will|would|do|does|are|is)\s+(?:people|anyone|anybody|others|someone|they|readers?|visitors?)\b)",
re.I,
)
_MINE = (
r"(?:notebooks?|posts?|articles?|sources?|archive|documents?|newsletters?|blog|videos?|audio|podcasts?|"
r"reports?|quiz(?:zes)?|flashcards?|infographics?|mind\s*(?:map|constellation)s?|launch\s*kits?|drafts?|files?|chats?|conversations?|topics?|quotes?|summar(?:y|ies)|writing|content|stuff|data)"
)
_ABOUT_MY_DATA = re.compile(
rf"""(?ix)
\bmy\s+(?:[\w'-]+\s+){{0,3}}{_MINE}\b
| \b(?:what|which|show|list|find|tell)\b[^.?!]{{0,40}}\b(?:i|we)\s+(?:wrote|written|said|posted|published|have|uploaded|added)\b
| \bhow\s+many\b[^.?!]{{0,40}}\b(?:do\s+i|have\s+i|did\s+i|i\s+have)\b
| \b(?:summari[sz]e|analy[sz]e|review|compare|search)\b[^.?!]{{0,30}}\b(?:my|this|the)\s+(?:\w+\s+)?{_MINE}\b
| \bwhat(?:'s|\s+is)\s+in\s+(?:my|this|the)\s+{_MINE}\b
| \bwhat\s+did\s+i\s+(?:write|say|post|publish)\b
| \bwhat\s+(?:did|have|were)\s+we\s+(?:talk|chat|discuss|speak|cover|say)\w*\b
| \bwhat\s+(?:we|i)\s+(?:talked|chatted|discussed|spoke|covered)\b
| \b(?:earlier|previous|last|past|old|other)\s+(?:chat|conversation|discussion|thread)s?\b
| \bour\s+(?:chat|conversation|discussion|thread)s?\b
| \b(?:recap|remind\s+me|repeat)\b[^.?!]{{0,40}}\b(?:chat|conversation|discussion|said|discussed)\b
"""
)
# A question about what the plans include ("How many Launch Kits do I get on each plan?") is product help, not about the
# writer's own content, unless it also asks what they have made or hold.
_PLAN_WORDS = re.compile(r"(?i)\b(?:plans?|tiers?|pricing|prices?|free|writer|studio|allowance|limits?|upgrade|subscription)\b")
_OWN_STATE = re.compile(r"(?i)\b(?:i\s+have|have\s+i|did\s+i|i\s+(?:wrote|made|created|used|posted|published))\b")
# Words that mean something went wrong with their work; the reply also offers the human form.
_BROKEN = re.compile(
r"(?i)\b(?:fail\w*|slow|forever|too\s+long|taking\s+(?:so\s+|way\s+)?long|hang\w*"
r"|never\s+(?:finish|load|appear)\w*|error|broken|stuck|crash\w*|not\s+working|won'?t|doesn'?t\s+work|missing|lost)\b"
)
OUT_OF_SCOPE_REPLY = (
"I can help with how Notestack works, but I can't see your notebooks, posts, files or account, so I can't answer "
"questions about your own content. For questions about what you wrote, open the notebook and ask in its chat. "
"Answers there cite your exact lines."
)
OUT_OF_SCOPE_BROKEN_SUFFIX = " If something looks broken, I can pass it to our team."
def about_user_data(message: str) -> bool:
"""True when the message asks about the writer's own content or account state rather than how to do something."""
text = message.strip()
if _HOWTO.match(text) and not re.search(r"(?i)\b(?:i|we)\s+(?:wrote|said|posted|published)\b", text):
return False
if _PLAN_WORDS.search(text) and not _OWN_STATE.search(text) and not re.search(rf"(?i)\bmy\s+(?:[\w'-]+\s+){{0,3}}{_MINE}\b", text):
return False
return bool(_ABOUT_MY_DATA.search(text))
def out_of_scope_reply(message: str) -> tuple[str, bool]:
"""(reply, offer_human_form)"""
broken = bool(_BROKEN.search(message))
return OUT_OF_SCOPE_REPLY + (OUT_OF_SCOPE_BROKEN_SUFFIX if broken else ""), broken