File size: 4,531 Bytes
b74c6ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0649f1b
 
 
b74c6ae
 
 
 
0649f1b
b74c6ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e637380
 
 
 
b74c6ae
29c052e
 
 
 
b74c6ae
 
 
 
 
 
 
 
 
 
 
 
 
 
e637380
 
b74c6ae
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
"""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