SignalMesh / editor /answer.py
KyklosIO's picture
Claude Opus 5
feat(editor): fold the live-site editor into the Studio as a mounted app
c05106c
Raw History Blame Contribute Delete
6.82 kB
"""Answers questions from the site's own words. No model, no inference.
Everything returned here is a direct quote from published page content plus the
link it came from. If the site does not say it, this says so rather than
producing something plausible — a school site cannot afford invented answers
about pricing, dates or who is responsible for a child.
"""
import re
import mesh
STOPWORDS = {
"a", "an", "the", "is", "are", "was", "were", "do", "does", "did", "i", "you",
"my", "our", "we", "can", "could", "would", "should", "please", "tell", "me",
"about", "for", "of", "to", "in", "on", "at", "and", "or", "it", "this", "that",
"there", "here", "what", "whats", "when", "where", "who", "whos", "how", "why",
"which", "have", "has", "any", "get", "find", "know", "want", "need", "am",
"be", "with", "from", "if", "so", "up", "out", "will", "much", "many", "some",
}
# Question shape → how to phrase the reply. Order matters: first match wins.
INTENTS = [
("who", re.compile(r"\bwho(?:'s|s\b|\s+is|\s+are|\s+teaches|\s+do)?\b", re.I),
"Here is what the site says about who is involved:"),
("when", re.compile(r"\bwhen\b|\bwhat time\b|\bdates?\b|\bschedule\b|\bcalendar\b|\bbreak\b|\bholiday\b", re.I),
"Here are the dates and times listed:"),
("cost", re.compile(r"\bcost|price|pricing|how much|fee|fees|pay|payment|free\b", re.I),
"Here is the pricing information listed:"),
("where", re.compile(r"\bwhere\b|\baddress\b|\blocation\b|\bdirections?\b", re.I),
"Here is the location information listed:"),
("contact", re.compile(r"\bcontact\b|\bphone\b|\bemail\b|\bcall\b|\breach\b", re.I),
"Here are the contact details listed:"),
("menu", re.compile(r"\bmenu\b|\blunch\b|\bbreakfast\b|\bmeal\b|\bcafeteria\b", re.I),
"Here is what the menu information says:"),
]
DEFAULT_LEAD = "Here is what the site says:"
DATE_RE = re.compile(
r"\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\.?\s+\d{1,2}"
r"|\b\d{1,2}/\d{1,2}(?:/\d{2,4})?"
r"|\b(?:mon|tues|wednes|thurs|fri|satur|sun)day\b"
r"|\b\d{1,2}:\d{2}\s*(?:a\.?m\.?|p\.?m\.?)?", re.I)
MONEY_RE = re.compile(r"\$\s?\d[\d,]*(?:\.\d{2})?")
PHONE_RE = re.compile(r"\b\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b")
EMAIL_RE = re.compile(r"\b[\w.+-]+@[\w-]+\.[\w.]+\b")
INTENT_PATTERNS = {"when": DATE_RE, "cost": MONEY_RE, "contact": PHONE_RE}
def keywords(question):
words = re.findall(r"[\w']+", (question or "").lower())
kept = [w for w in words if w not in STOPWORDS and len(w) > 2]
return kept or words
def intent_of(question):
for name, pattern, lead in INTENTS:
if pattern.search(question or ""):
return name, lead
return "what", DEFAULT_LEAD
DANGLING = re.compile(r"\b(?:the|a|an|on|at|to|of|for|in|and|or|with|by|from|our|your)$", re.I)
def clean(text):
"""Editors write literal ** in this content; visitors should not see it."""
text = re.sub(r"\*\*(.+?)\*\*", r"\1", text or "")
text = text.replace("**", "").replace("__", "")
return re.sub(r"\s+", " ", text).strip()
def sentences(text):
parts = re.split(r"(?<=[.!?])\s+|\s*\n+\s*|\s{2,}", text or "")
return [clean(s).strip(" \t·-–—") for s in parts if len(clean(s)) > 2]
def score_sentence(s, terms, intent):
low = s.lower()
score = sum(3 for t in terms if t in low)
pattern = INTENT_PATTERNS.get(intent)
if pattern and pattern.search(s):
score += 4
if intent == "contact" and (EMAIL_RE.search(s) or PHONE_RE.search(s)):
score += 3
# A bare heading like "Superintendent" repeats the question instead of
# answering it. Reward sentences that actually carry information.
if len(s) < 20:
score -= 6
elif len(s) >= 45:
score += 2
# "For school-specific contacts, visit the" — a fragment cut mid-thought
# reads as broken. Prefer sentences that finish.
if DANGLING.search(s.rstrip(" .,:;")):
score -= 5
score -= abs(len(s) - 130) / 400 # prefer a readable length
return score
def best_sentence(text, terms, intent):
"""The one sentence that most answers the question, not the whole block."""
best, best_score = "", -999
for s in sentences(text):
score = score_sentence(s, terms, intent)
if score > best_score:
best, best_score = s, score
return best, best_score
def ask(question, limit=3):
"""Answer from published content only. Returns quotes plus where to read more."""
terms = keywords(question)
intent, lead = intent_of(question)
if not terms:
return {"ok": False, "intent": intent,
"answer": "Ask me about something on the site — lunch menus, "
"calendars, staff, or a school office.",
"sources": []}
hits = mesh.public_search(" ".join(terms))
if not hits:
return {"ok": False, "intent": intent,
"answer": f"I could not find anything on the site about "
f"“{question.strip()}”. Try different words, or use the "
f"site menu to browse.",
"sources": []}
# Collapse to the strongest passage per page.
pages = {}
for h in hits:
aid = h["article_id"]
quote, quality = best_sentence(h["text"], terms, intent)
if not quote:
continue
# A headline names the topic; the body answers the question.
if h["type"] == "headline":
quality -= 4
page = pages.get(aid)
if page is None or quality > page["quality"]:
pages[aid] = {"article_id": aid, "title": h["article"], "quote": quote,
"quality": quality, "coord": h["coord"],
"terms": page["terms"] if page else set()}
pages[aid]["terms"] |= {t for t in terms if t in (h["text"] or "").lower()}
# Rank pages by how much of the question they actually cover, not by any
# single passage — a page mentioning every term is the likelier answer.
ranked = sorted(pages.values(),
key=lambda p: (len(p["terms"]), p["quality"]), reverse=True)
top = ranked[:limit]
if not top:
return {"ok": False, "intent": intent,
"answer": "I found related pages but no clear sentence to quote. "
"Try opening one of them.",
"sources": []}
return {
"ok": True,
"intent": intent,
"lead": lead,
"answer": top[0]["quote"],
"source": {"title": top[0]["title"], "article_id": top[0]["article_id"]},
"sources": [{"title": t["title"], "article_id": t["article_id"], "quote": t["quote"]}
for t in top],
}