File size: 5,793 Bytes
fe3dd99 c1de4ca fe3dd99 | 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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | """
Probe tests for the chatbot's knowledge base.
Runs real questions through app.respond() — the same code path the Space serves —
so the system prompt, the knowledge base and the answer are exactly what a user
would get. send_email_if_needed is stubbed out, so no unanswered-question mail is
sent to the support alias; instead, a probe FAILS when the real code path *would*
have sent one. That is the signal worth testing: every question the bot cannot
answer becomes an email to rishi@, support@ and ubaid@.
A probe WARNs when the bot answers but never mentions a term a correct answer has
to contain — that catches an answer about the wrong feature.
pip install -r requirements.txt
HF_TOKEN=... python test_knowledge_base.py
Exit code is non-zero if anything failed or warned.
"""
import os
import sys
REPO = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, REPO)
if not os.environ.get("HF_TOKEN"):
env = os.path.join(REPO, ".env")
if os.path.exists(env):
with open(env) as handle:
for line in handle:
if line.startswith("HF_TOKEN"):
os.environ["HF_TOKEN"] = line.split("=", 1)[1].strip().strip('"').strip("'")
if not os.environ.get("HF_TOKEN"):
sys.exit("HF_TOKEN not set (environment or .env)")
import app # noqa: E402 — imported after HF_TOKEN so the client picks it up
# Never mail the support alias from a test run — but keep the REAL decision
# logic. send_email_if_needed runs after every answer and decides internally
# whether to send, so stubbing that function would record every question rather
# than every alert. Stub the SES client underneath it instead: what lands here
# is exactly what production would have mailed.
would_have_emailed = []
class _RecordingSES:
def send_email(self, **kwargs):
would_have_emailed.append(kwargs)
app.ses = _RecordingSES()
# (question, a substring a correct answer must contain)
PROBES = [
# --- file types (KB 5.7) ---
("Can I email an iPhone photo of an invoice?", "yes"),
("Why was my HEIC photo rejected when I dragged it into the web app?", "email"),
("Can I send an invoice as an Excel file?", "pdf"),
("What file types can Beiing Human read?", "png"),
# --- pay control (KB 6.2, 6.5) ---
("What is the Pay Control column?", "erp"),
("Why can't I see the Pay Control column?", "foundation"),
("How do I set the pay control on several invoices at once?", "assign"),
("Can I bulk assign pay control on Vista?", "foundation"),
("A pay control name is missing from the list. Why?", "sync"),
# --- annotation (KB 7.8) ---
("How do I put a TAX EXEMPT stamp on an invoice?", "annotate"),
("What stamps can I use on a document?", "void"),
("Can I write a job number on the invoice image?", "note"),
("How do I remove a stamp I already saved?", "again"),
# --- mobile ---
("Is there a mobile app?", "android"),
("Where do I download the Beiing Human app?", "play"),
("Why can't I log into the mobile app?", "accountant"),
("Can our AP admin use the phone app?", "owner"),
("How do I upload a receipt from my phone?", "upload file"),
("I don't see the Create PO button on my phone, why?", "sign"),
("Why is there no approve button on the To Review list in the app?", "details"),
("The app says fix 3 issues before approving. What do I do?", "validation"),
("Can I reject an invoice on mobile without leaving a comment?", "comment"),
("Does the mobile app send push notifications?", "not"),
("How do I get out of the Update Required screen?", "store"),
("Can I edit an exported document on my phone?", "read-only"),
("How do I answer a question assigned to me in the app?", "reply"),
("Can I search for a specific invoice in the mobile app?", "web"),
("Can I match an invoice to a PO on my phone?", "web"),
("Why did my app log me out?", "session"),
# --- web app regressions ---
("What does the INYA report show?", "approv"),
("How do I use the pull back feature?", "pull back"),
("What is a Sub Admin and what can they do?", "admin"),
("How do I attach a delivery ticket to an invoice?", "match"),
("What's the difference between Approved and Verified statuses?", "api"),
("How do I trigger the document splitter?", "split"),
("Why is a vendor missing from the COI tracker?", "active job"),
("Can I bulk delete documents?", "does not"),
]
def ask(question):
"""Drive the Space's own respond() generator and return the final answer."""
answer = ""
for chunk in app.respond(question, [], "", 2000, 0.3, 0.95, None):
answer = chunk
return answer
def main():
print(f"Knowledge base: {len(app.doc_context):,} chars\n")
if len(app.doc_context) < 2000:
sys.exit("Knowledge base looks empty or failed to load")
problems = 0
for question, expect in PROBES:
before = len(would_have_emailed)
try:
answer = ask(question)
except Exception as e:
print(f"ERROR {question}\n {e}")
problems += 1
continue
emailed = len(would_have_emailed) > before
if emailed:
verdict, note = "FAIL", "unanswered -> would have emailed the support alias"
elif expect not in answer.lower():
verdict, note = "WARN", f"answered but never mentions '{expect}'"
else:
verdict, note = "PASS", ""
if verdict != "PASS":
problems += 1
print(f"{verdict} {question}")
if note:
print(f" {note}")
print(f"\n{len(PROBES) - problems}/{len(PROBES)} clean")
return 1 if problems else 0
if __name__ == "__main__":
sys.exit(main())
|