Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ to know when a release changes behavior.
honest "no source reports a forecast discussion yet" when none of your sources
carries one, instead of a stat built from the day's temperature. "Hourly rain
forecast" now prefers the hour-by-hour list over a single hour's reading.
- **A card's place comes from the place you named, never from a stray word.** "How
many quakes hit Alaska" used to run at Many, Louisiana (the word "many" matched the
town) and "hurricanes in the Atlantic" at Atlantic, Iowa; a small town's one-word name
now needs a cue (a capital letter, its state, or "in / near / at" before it), a
region such as the Atlantic or the Gulf Coast is never a point on the map, and a
state you name wins over a same-named town elsewhere. Big cities keep working in
lowercase ("seattle weather tomorrow"). An ask that compares two places ("is it
warmer in Phoenix or Miami") no longer silently answers for one of them: it stops
with links and says that one source answers one place.
- **Asks that say "advisories" or "watches" find the alerts sources, and a transit
ask no longer lands on airport status.** "Heat advisories in Arizona" and "coastal
flood advisories for Long Island" went to links because the plural never matched the
Expand Down
73 changes: 68 additions & 5 deletions app/smartbrain_3000/library_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,17 +258,75 @@ def folded(t: str) -> str:
return bool({_fold(w) for w in need} & set((text + folded(name)).split()))


# G2 (blind50g review): a two-subject ask ("X or Y", "X vs Y", "X compared with Y") must not seal a
# one-subject card. A comparison word joining two independently-cued places (or a place and a state
# that doesn't contain it) makes the ask "compound": locate offers no candidate, closed.
_COMPARISON_PHRASES = ("compared with", "compared to", "versus", "against", "vs", "than", "or")
_COMPARISON_RE = re.compile(
r"\b(" + "|".join(p.replace(" ", r"\s+") for p in _COMPARISON_PHRASES) + r")\b", re.IGNORECASE)


def _comparison_split(ask: str) -> tuple[str, str] | None:
"""``ask`` split at its leftmost comparison word/phrase, case-insensitive; None when it names
none. Splits the ORIGINAL ask (not a lowercased copy) so each half keeps its own capitalisation
— ``by_name``'s cue grammar (G1b) reads that back on each side independently."""
assert isinstance(ask, str) and ask, "ask must be a non-empty string"
m = _COMPARISON_RE.search(ask)
if m is None:
return None
assert m.start() < m.end(), "a real match has a non-empty span"
return ask[:m.start()], ask[m.end():]


def _side_location(res, half: str) -> tuple[str, str, str]:
"""(name, "place"|"state"|"", its state/own code) ``half`` names with its own cue, else ("", "", "").
An "ambiguous" reading (no single dominant population, e.g. "Charleston") has no ``best``; its
likeliest candidate (``by_name`` ranks these by population, largest first) stands in for it."""
assert isinstance(half, str), "half must be a string"
from .library_resolve import norm
low = f" {norm(half)} "
r = res.by_name("place", half)
words, _passed = _place_words_in(res, r, low, half, "")
top = r.get("best") or next(iter(r.get("candidates") or []), None)
if words and top:
assert isinstance(top, dict), "a resolved reading carries its entry dict"
return norm(top["name"]), "place", str(top.get("state") or "")
st = res.by_name("us_state", half)
if st["status"] == "resolved":
return norm(st["best"]["name"]), "state", str(st["best"].get("key") or "").upper()
return "", "", ""


def _is_compound(res, ask: str) -> bool:
"""Two distinct, independently-cued places (or a place and a non-containing state) joined by a
comparison word: "is it warmer in Phoenix or Miami", never "Lake Michigan... in Chicago" (no
comparison word) or "Dodgers vs Giants" (neither side is a place)."""
assert isinstance(ask, str) and ask, "ask must be a non-empty string"
split = _comparison_split(ask)
if split is None:
return False
assert len(split) == 2, "a comparison split always returns two halves"
name_a, kind_a, code_a = _side_location(res, split[0])
name_b, kind_b, code_b = _side_location(res, split[1])
if not name_a or not name_b or name_a == name_b:
return False
return not (code_a and code_a == code_b and "state" in (kind_a, kind_b))


def named_place(res, ask: str, hint_place: str | None = None) -> tuple[set[str], bool, str]:
"""The place the ask names, decided as the fill decides it: (its words, named?, the resolver's status).

Starts from the resolver's own reading (a "largest of its name" pick is a big place), then the other
ties; a place is the ask's when it is big, follows in/at/near/for/around, is a reviewed area name, comes
with a state, or is the intent's own place. A place the resolver can't settle is "ambiguous", never
dropped. A ZIP or a state name is a place too."""
dropped. A ZIP or a state name is a place too. Two independently-cued places joined by a comparison
word are "compound" (G2): never one place sealed for an ask that names two."""
from .library_resolve import norm
low = f" {norm(ask)} "
if re.search(r"\b\d{5}\b", ask or ""):
return set(), True, "resolved"
if _is_compound(res, ask):
return set(), False, "compound"
st = res.by_name("us_state", ask)
state_words = set(norm(st["best"]["name"]).split()) if st["status"] == "resolved" else set()
hinted = f" {norm(hint_place or '')} "
Expand All @@ -283,10 +341,11 @@ def named_place(res, ask: str, hint_place: str | None = None) -> tuple[set[str],
status = r["status"]
if not words and passed:
# a longer name that isn't the place can hide the one that is ("Lake Erie Beach" in "Lake Erie water
# temp Cleveland"): the rest of the ask is read once more
rest = low
# temp Cleveland"): the rest of the ask is read once more, from the ORIGINAL ask (not the lowercased
# `low`) so a one-word alias elsewhere in it ("Cleveland") keeps its own capitalisation cue (G1b)
rest = ask
for said in passed:
rest = rest.replace(f" {said} ", " ")
rest = re.sub(rf"\b{re.escape(said)}\b", " ", rest, flags=re.IGNORECASE)
r = res.by_name("place", rest)
words, _ = _place_words_in(res, r, low, ask, hinted)
status = r["status"] if words else status
Expand All @@ -305,7 +364,7 @@ def _said_place(res, hint_place: str, low: str) -> tuple[str, str]:
"""The words of the intent's place as the ask says them (its name or an alias), and the resolver's status
for it; ('', 'none') when the ask doesn't say it."""
from .library_resolve import norm
h = res.by_name("place", hint_place)
h = res.by_name("place", hint_place, cued=True) # the intent named it: no G1b cue required of its text
for c in ([h["best"]] if h["best"] else h["candidates"][:3]): # bounded: three readings
nm = norm(c["name"])
said = nm if f" {nm} " in low else next((w for w in _aliases_in(res, c["id"], low)), "")
Expand Down Expand Up @@ -1149,6 +1208,10 @@ def candidates(self, ask: str, limit: int = 3, hint: dict | None = None) -> tupl
if route is None: # a pack without route asks: the keyword frame, as without vectors
vectors = None
ctx = self._context(con, ask, hint, route)
if ctx["place_status"] == "compound":
# G2: a two-subject ask ("Phoenix or Miami") gets no candidate — one source answers
# one place; the caller's existing "no row" path is the honest stop with links
return [], ["this compares two places; one source answers one place"]
if vectors is not None:
keyword = [sid for sid, _score in self._ranked(con, ctx, "s.tier <> 'harvested'", [])]
ranked = vectors.fuse(keyword, vectors.dense(q), route)
Expand Down
45 changes: 44 additions & 1 deletion app/smartbrain_3000/library_resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,22 @@

GEO_NEAR = ("tide_station", "buoy", "radar_site", "nwps_gauge")

# G1c (blind50g review, 2026-10-12): a basin/ocean/region word is never a point place — closed, from
# the ruling's own list. "Atlantic City" / "Pacific Grove" / "Midwest City" are different, longer
# aliases and are untouched (the veto matches only these exact words).
REGION_WORDS = frozenset([
"atlantic", "pacific", "gulf of mexico", "caribbean", "arctic", "indian ocean", "great lakes",
"gulf coast", "east coast", "west coast", "midwest", "northeast", "southeast", "southwest",
"pacific northwest", "new england",
])

# G1b: a place cue that can admit a bare one-word place alias ("quakes near Many")
PLACE_CUES = frozenset(["in", "near", "at", "around", "for", "from", "to", "of"])
# G1b: a prominent place needs no cue — its one-word name IS the cue ("seattle weather tomorrow"); the
# pack's place rank is ~log10(population), so 5.0 is a city of 100,000 people or more. Below it a bare
# lowercase one-word alias ("many", "normal", "hope", "winter", "gas") is an ordinary word first.
BARE_PLACE_RANK = 5.0


def norm(text: str) -> str:
return re.sub(r"\s+", " ", re.sub(r"[^a-z0-9 ]+", " ", (text or "").lower().replace("&", " and "))).strip()
Expand Down Expand Up @@ -83,6 +99,16 @@ def _row(r: tuple) -> dict:
_ENTRY_COLS = "e.id, e.resolver, e.kind, e.key, e.name, e.lat, e.lon, e.state, e.attrs, e.rank"


def _place_cued(gram: str, i: int, tokens: list[str], titled: set[str], state: str, named: set[str]) -> bool:
"""A bare one-word place alias ("many", "normal", "hope") is the ask's place only with a cue
(G1b): the ask capitalises it, names its own state, or a place-cue word comes right before it."""
assert isinstance(gram, str) and gram, "gram must be a non-empty string"
assert isinstance(tokens, list) and 0 <= i < len(tokens), "i must index tokens"
if gram in titled or (state and state in named):
return True
return i > 0 and tokens[i - 1] in PLACE_CUES


class Resolver:
"""The one matcher over the pack's resolver tables (see the module docstring)."""

Expand All @@ -108,7 +134,10 @@ def source_record(self, source_id: str) -> dict | None:
row = self._con.execute("SELECT record FROM library_sources WHERE id = ?", [source_id]).fetchone()
return json.loads(row[0]) if row else None

def by_name(self, resolver: str, ask: str, *, many: bool = False) -> dict:
def by_name(self, resolver: str, ask: str, *, many: bool = False, cued: bool = False) -> dict:
"""``cued``: the caller vouches that ``ask`` names a place (the intent's own place text), so the
G1b bare-alias cue is not required of it."""
assert isinstance(cued, bool), "cued must be a bool"
tokens = norm(ask).split()
grams = []
for i in range(len(tokens)):
Expand All @@ -129,6 +158,9 @@ def by_name(self, resolver: str, ask: str, *, many: bool = False) -> dict:
by_alias.setdefault(r[10], []).append((e["id"], bool(r[11])))
aliases_of.setdefault(e["id"], []).append(r[10])
upper = set(re.findall(r"\b[A-Z0-9$]{1,6}\b", ask or ""))
# "capitalised" (G1b) is Title case ("Many") or a spelled-out code ("NYC") — the same signal
# the two-letter rule already reads off `upper`, generalised to any length
titled = {m.lower() for m in re.findall(r"\b[A-Z][a-z]+\b", ask or "")} | {u.lower() for u in upper}
states = states_in(ask, self)
# a capitalised state code that is also a place's nickname ("LA", "DC") names that place only when
# no other place is named: "LA weather" is Los Angeles, "Lafayette LA" is Lafayette, Louisiana
Expand All @@ -143,6 +175,8 @@ def by_name(self, resolver: str, ask: str, *, many: bool = False) -> dict:
spec = sum(1 for w in gram.split() if w not in ENGLISH) or 1
if n == 1 and gram in ENGLISH:
continue
if resolver == "place" and gram in REGION_WORDS:
continue # G1c: a basin/ocean/region word is never a point place
for eid, partial in by_alias.get(gram, []):
e = entries[eid]
if n == 1 and gram == norm(e["key"]) and len(gram) <= 5 and gram not in norm(e["name"]).split():
Expand All @@ -155,6 +189,15 @@ def by_name(self, resolver: str, ask: str, *, many: bool = False) -> dict:
continue
if n == 1 and e["kind"] == "place" and len(gram) <= 2 and gram.upper() not in upper:
continue # a two-letter place nickname counts only in capitals ("LA", never the word "la")
# G1b: a longer one-word place alias written lowercase ("many") needs a cue too — the
# ask capitalises it, names its own state, or a place-cue word sits right before it.
# A prominent place (BARE_PLACE_RANK) is its own cue ("seattle weather tomorrow") unless
# the ask names some OTHER state (G1a: "mobile … across illinois" is not Mobile, AL); the
# caller can vouch for the text instead (``cued``: the intent named it as the place).
if n == 1 and e["kind"] == "place" and len(gram) > 2 and not cued \
and not _place_cued(gram, i, tokens, titled, e["state"], states) \
and ((e["rank"] or 0.0) < BARE_PLACE_RANK or (states and e["state"] not in states)):
continue
score = (2.0 * spec - (0.5 if partial else 0.0) + (0.5 if gram == norm(e["name"]) else 0.0)
+ RANK_WEIGHT.get(resolver, 0.05) * e["rank"])
said = states - {gram.upper()} if n == 1 and gram in codes else states
Expand Down
12 changes: 11 additions & 1 deletion app/smartbrain_3000/ni_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,9 @@ def _parse_json_reply(text: str) -> dict:
_INTENT_NAME_WORD_RE = re.compile(r"[A-Za-z0-9]+")
_MAX_INTENT_NAMES = 5
_MAX_INTENT_NAME_CHARS = 60
# G2: a compound ask ("Phoenix or Miami") can make the model name both places as a list
_MAX_COMPOUND_PLACES = 3
_MAX_COMPOUND_PLACE_CHARS = 60


def _clean_intent_names(raw: object, request: str) -> list[str]:
Expand Down Expand Up @@ -506,10 +509,17 @@ def _validate_intent(reply: dict, request: str = "") -> dict:
# geocode-consent (2026-09-15): ``place`` is optional and bounded — it only
# ever becomes a percent-encoded geocode query behind the confirm card.
place = reply.get("place")
if isinstance(place, list) and len(place) <= _MAX_COMPOUND_PLACES \
and all(isinstance(p, str) and len(p) <= _MAX_COMPOUND_PLACE_CHARS for p in place):
# G2: a compound ask ("Phoenix or Miami") can make the model name both places as a list;
# the ask's own words, not this field, drive locate's compound honest stop (named_place) —
# never a stage failure over the model's shape choice
place = " / ".join(p.strip() for p in place if p.strip()) or None
if place is not None and not isinstance(place, str):
raise ValueError("intent.place must be a string or null")
if isinstance(place, str) and len(place) > 120:
reply["place"] = place[:120]
place = place[:120]
reply["place"] = place
reply["names"] = _clean_intent_names(reply.get("names"), request)
return reply

Expand Down
Loading
Loading