finally getting accurate answers
Build and Push Containers / build-and-push-app (push) Successful in 1m46s
Build and Push Containers / build-and-push-db (push) Successful in 12s

This commit is contained in:
2026-09-05 10:26:39 -04:00
parent bb2803bebd
commit 766702c750
9 changed files with 1628 additions and 41 deletions
+114 -7
View File
@@ -66,6 +66,16 @@ task 04):
each line truncated to 200 chars. A grep is a **locator**, not a
context-adder: it never appends to the answer context (only
``read`` does — ``holder.read_docs`` is untouched by a grep).
The 2026-09-05 incident teaching (the harness prior is that grep
takes a REGEX; this grep is a fixed substring and the contract does
not change): when a grep RAN but found nothing and the pattern is
regex-shaped (:func:`looks_like_regex`) with a non-empty
:func:`plain_form`, the no-match result is the TEACHING line —
:data:`NO_MATCHES_REGEX` / :data:`NO_MATCHES_REGEX_SCOPED` — which
states the plain-substring contract and hands over the plain-form
retry hint; a grep that matched, or a no-match for a plain pattern
(or one that reduces to nothing), is byte-identical to the ordinary
line. Still a (counted) result, never a refusal.
3. Rejected calls get a one-line refusal and count in nothing
(``holder.tool_calls`` tracks executed calls only): unknown tool name
→ ``"Unknown tool."``; a ``read`` without a usable ``path`` (missing,
@@ -163,6 +173,7 @@ from __future__ import annotations
import json
import logging
import re
from collections.abc import AsyncIterator, Sequence
from dataclasses import dataclass, field
from typing import Any, cast
@@ -268,12 +279,16 @@ AGENT_TOOLS: list[dict[str, Any]] = [
"Search the indexed documents for an exact string "
"(case-insensitive) and return up to 20 matching lines "
"as `source/path:line: text` — a locator, not a "
"context-adder: read the winner with `read`. For a "
"normal search pass ONLY `pattern` — it searches every "
"document and that is how you search the knowledge "
"base; never pass a source name as `path` (a source "
"name is not a document). Call one tool at a time — "
"wait for this result before your next call."
"context-adder: read the winner with `read`. The "
"pattern is a plain substring, NEVER a regex — if a "
"pattern with regex syntax (like '.*' or '\\.') comes "
"back with no matches, retry with the plain text you "
"expect to see. For a normal search pass ONLY `pattern` "
"— it searches every document and that is how you "
"search the knowledge base; never pass a source name "
"as `path` (a source name is not a document). Call one "
"tool at a time — wait for this result before your "
"next call."
),
"parameters": {
"type": "object",
@@ -282,7 +297,8 @@ AGENT_TOOLS: list[dict[str, Any]] = [
"type": "string",
"description": (
"The exact text to search for (a plain "
"substring, not a regex)"
"substring, not a regex — no '.*', no "
"'\\.', no character classes)"
),
},
"path": {
@@ -417,6 +433,79 @@ SEARCH_LINE_LIMIT = 200
NO_MATCHES = "No matches for '{pattern}' in the knowledge base."
NO_MATCHES_SCOPED = "No matches for '{pattern}' in {source}/{path}."
#: Teaching no-match lines (the 2026-09-05 incident — owner chat:
#: "Qwen 3.8" question failed repeatedly). The harness-trained prior is
#: that ``grep(pattern)`` takes a REGEX (pi.dev's grep, ripgrep, grep
#: itself); this app's grep is a case-insensitive FIXED SUBSTRING
#: (owner-locked A5 — the contract does not change). A regex-shaped
#: pattern (``qwen.*3\.8``, ``qwen 3\.8``) can therefore never match, and
#: the bare no-match line above made the turbo model trust the miss and
#: end the turn with a wrong "I searched the entire knowledge base"
#: refusal. These lines are the deterministic teaching: the same result
#: (a no-match is still a *result* — counted, no context added) with the
#: correct contract stated and a plain-text retry hint (the
#: :func:`plain_form` of the pattern, when non-empty). Deterministic only
#: (owner permission 2026-09-03: "deterministic guardrails only right
#: now"): no model participates in detection or repair.
NO_MATCHES_REGEX = (
"No matches for '{pattern}'. grep matches a plain substring "
"(case-insensitive), not a regex — '.*', '\\.' and the like are "
"literal text here, so that pattern can never match. Retry with the "
"plain text you expect to see (e.g. '{plain}')."
)
NO_MATCHES_REGEX_SCOPED = (
"No matches for '{pattern}' in {source}/{path}. grep matches a plain "
"substring (case-insensitive), not a regex — retry with the plain "
"text you expect to see (e.g. '{plain}')."
)
#: One of these metacharacters anywhere in the pattern marks it
#: regex-shaped (the detection for the teaching no-match lines above).
#: Plain substrings that happen to contain one ("C++", "a|b") are
#: affected only on a NO-MATCH — a pattern that matched literally still
#: gets its ordinary result, so the teaching can never suppress a real
#: hit.
_REGEX_META_RE = re.compile(r"[*+?(){}\[\]|\\^$]")
# Backslash + a non-alphanumeric char is an escaped literal (``\\.`` →
# ``.``); backslash + an alphanumeric is a class shorthand (``\\d``,
# ``\\w``, ``\\s``) with no plain-text equivalent (→ dropped).
_ESCAPE_RE = re.compile(r"\\(.)")
def looks_like_regex(pattern: str) -> bool:
"""True when *pattern* carries regex metacharacters (see
:data:`_REGEX_META_RE`). Pure detection — the grep itself stays a
fixed substring (owner-locked A5)."""
return _REGEX_META_RE.search(pattern) is not None
def plain_form(pattern: str) -> str:
"""A deterministic plain-text retry hint for a regex-shaped pattern.
The hint is the pattern reduced to literal text, in this order:
drop ``.*`` runs on the RAW pattern first (the wildcard — an escaped
``\\.`` + ``\\*`` pair carries no raw ``.*`` run, so a literal dot
survives), unescape (``\\.`` → ``.``; class shorthands like ``\\d``
dropped), keep only the FIRST ``|`` alternative, drop character
classes (``[0-9]``), drop quantifier runs (``*``, ``+``, ``?``,
``{2,3}``), drop group parens and anchors (contents kept). Whitespace
is preserved. ``qwen.*3\\.8`` → ``qwen3.8`` (the incident's exact
recovery), ``llama\\.cpp`` → ``llama.cpp``, ``qwen[0-9]+`` →
``qwen``. A pattern made of pure metacharacters reduces to ``""`` —
callers then fall back to the ordinary no-match line (no hint).
"""
p = re.sub(r"\.\*", "", pattern) # .* wildcard runs (raw form)
p = _ESCAPE_RE.sub(lambda m: m.group(1) if not m.group(1).isalnum() else "", p)
p = p.split("|", 1)[0] # first alternative only — a hint, not an answer
p = re.sub(r"\[[^\]]*\]", "", p) # character classes carry no literal text
p = re.sub(r"\{[^{}]*\}", "", p) # {n} / {n,} / {n,m} quantifiers
p = re.sub(r"[*+?]+", "", p) # stray * + ? quantifiers
p = p.replace("(", "").replace(")", "")
p = p.replace("^", "").replace("$", "")
return p.strip()
def list_catalog(db: Session) -> list[tuple[str, str, str]]:
"""Every indexed document as ``(source, path, title)``.
@@ -694,6 +783,24 @@ def _execute_tool(
# Locked A5: a grep never adds context — read_docs untouched.
if not matches:
shown = pattern[:100] # keep a long pattern short in the line
# The 2026-09-05 incident teaching: a regex-shaped pattern
# (the harness prior) can NEVER match a fixed-substring
# grep, so a no-match for one is not "the KB lacks this" —
# it is "the pattern was in the wrong form". Teach the
# contract and hand over the plain-form retry hint (when the
# reduction is non-empty); a plain pattern (or a pattern
# that reduces to nothing) keeps the ordinary line
# byte-identical.
plain = plain_form(pattern) if looks_like_regex(pattern) else ""
if plain:
if scoped_to is not None:
return NO_MATCHES_REGEX_SCOPED.format(
pattern=shown,
source=scoped_to[0],
path=scoped_to[1],
plain=plain[:100],
)
return NO_MATCHES_REGEX.format(pattern=shown, plain=plain[:100])
if scoped_to is not None:
# The scoped no-match line is keyed on the resolved
# source/path (== the argument, stripped).
+5 -2
View File
@@ -149,8 +149,11 @@ TOOLS_SECTION: str = (
"path (without the source name) will not resolve. `grep` locates an "
"exact string (case-insensitive) in the indexed documents and "
"returns up to 20 matching `source/path:line: text` lines — a "
"locator, not a context-adder: read the winner with `read`; for a "
"normal search pass only `pattern` — its optional `path` argument "
"locator, not a context-adder: read the winner with `read`. A grep "
"pattern is a plain substring, NEVER a regex — '.*' and '\\.' are "
"literal text there; if such a pattern returns no matches, retry "
"with the plain text you expect to see. For a normal search pass "
"only `pattern` — its optional `path` argument "
"limits the search to one document you already know, by the same "
"combined `source/path` string; never a source name — a bare "
"document path (without the source name) will not resolve there "
+230 -17
View File
@@ -3,8 +3,27 @@
* **Vector list** — top-N chunks by cosine distance (``embedding <=> $1``),
each carrying its cosine ``1 − distance`` (the honesty-gate input).
* **Lexical list** — top-N chunks matching an OR-``tsquery`` over the
question's tokens, ordered by ``ts_rank``. This is what finds
name-your-tool questions ("gitlab") that vector similarity buries.
question's tokens, ordered by ``ts_rank``, UNION the name-hit list:
documents whose normalized name (title + path stem, alnum-only,
lowercased) contains a DIGIT-BEARING normalized question token of
length >= 4 — bare tokens (``1panel``) and the join of adjacent
question tokens starting with a letter ("Qwen 3.8" → ``qwen38``).
The digit requirement is the precision guard: plain prose words
("server", "arguments" — 4+ chars but no digit) never name-match,
while versioned product names (the incident's whole point) always
carry one. The name-hit documents LEAD the lexical list (ranked by
match count, then total matched length, then catalog order; capped
at :data:`NAME_HIT_LIMIT`),
the FTS rows follow. This is what finds name-your-tool questions
("gitlab") that vector similarity buries — and, the 2026-09-05
incident, the versioned-name case the default parser lexes
incompatibly (question "Qwen 3.8" → ``qwen``/``3``/``8`` can never
match a document's ``qwen3``/``8``/``27b`` tokens, while every
unrelated llama.cpp quadlet out-ranks the target on the shared
``llama``/``cpp`` tokens). The name-hit rows carry ``fts_hit=True``
(they ARE the lexical signal — the A8 honesty gate then answers
instead of deflecting) and ``cosine=0.0``; the RRF fusion is
unchanged (same lists, same ``1/(k+rank)`` terms).
* **Fusion** — Reciprocal Rank Fusion (``score = Σ 1/(k + rank)`` over the
lists a chunk appears in; chunks hit by both lists get both terms). The
fused score ranks; :meth:`select_documents` and :func:`weak_hit_titles`
@@ -26,6 +45,7 @@ import re
import uuid
from collections.abc import Sequence
from dataclasses import dataclass, replace
from pathlib import Path
from sqlalchemy import select, text
from sqlalchemy.orm import Session
@@ -38,8 +58,78 @@ from app.models import Chunk, Document
#: path never truncates (A7 revised, owner permission 2026-08-24).
TRUNCATION_MARKER = "[…truncated…]"
#: Alphanumeric tokens of a question (``to_tsquery`` input, OR-joined).
_TOKEN_RE = re.compile(r"[a-z0-9]+")
#: Alphanumeric tokens of a question (``to_tsquery`` input, OR-joined),
#: with DOTTED tokens kept whole (``llama.cpp`` → ``llama.cpp``). The
#: default parser lexes a dotted word as ONE lexeme (``to_tsvector`` of
#: "llama.cpp" → ``'llama.cpp'``; of "Qwen 3.8" → ``'3.8'``), so a split
#: token (``llama`` | ``cpp``) can never match the document side — the
#: 2026-09-05 incident's question phrase "llama.cpp" lexed
#: incompatibly on both sides of the query. Dotted tokens are passed
#: through as single ``to_tsquery`` lexemes (verified: Postgres accepts
#: ``'llama.cpp'`` and ``'3.8'`` as lexemes).
_TOKEN_RE = re.compile(r"[a-z0-9]+(?:\.[a-z0-9]+)*")
#: Name-hit policy (the 2026-09-05 incident): a question token shorter
#: than this (normalized) is too weak a name signal ("3", "8", "the") —
#: it would match half the KB by accident.
NAME_TOKEN_MIN_LEN = 4
#: The name-hit list may contribute at most this many documents to the
#: lexical side (before the FTS rows); the rest are dropped.
NAME_HIT_LIMIT = 10
#: Alphanumeric runs of a lowercased string (name normalization).
_ALNUM_RE = re.compile(r"[a-z0-9]+")
def _normalize_name(s: str) -> str:
"""Lowercased, alnum-only form of *s* (``Qwen 3.8`` → ``qwen38``)."""
return "".join(_ALNUM_RE.findall(s.lower()))
def name_hit_tokens(question: str) -> list[str]:
"""The normalized name-match candidates of one question.
Only DIGIT-BEARING candidates count (the precision guard — a plain
prose word like "server" or "arguments" must never name-match a
document; a versioned product name always carries a digit):
* the :func:`_normalize_name` form of every whitespace token, kept
when at least :data:`NAME_TOKEN_MIN_LEN` chars (``1panel``,
``qwen38`` from a single written token);
* the versioned-name JOIN — the normalized concatenation of every
ADJACENT token pair whose SECOND token is purely numeric (a
version number: ``"… for Qwen 3.8"`` → ``Qwen`` + ``3.8`` joins
to ``qwen38``; prose joins like ``correct`` + ``llama`` and the
word-after-version boundaries like ``3.8`` + ``show`` are
dropped). A purely-numeric second token means the join always
starts with the first token's text (a letter in practice), so no
digit-leading artifact (``38show``) can survive.
Order of first appearance, de-duplicated.
"""
tokens = question.split()
out: list[str] = []
seen: set[str] = set()
def add(tok: str) -> None:
if (
len(tok) >= NAME_TOKEN_MIN_LEN
and any(ch.isdigit() for ch in tok)
and tok not in seen
):
seen.add(tok)
out.append(tok)
for i, raw in enumerate(tokens):
norm = _normalize_name(raw)
add(norm)
if i + 1 < len(tokens):
next_norm = _normalize_name(tokens[i + 1])
if next_norm and next_norm.isdigit():
add(norm + next_norm) # the versioned-name join
return out
#: One row of the lexical candidate query (all fields needed to build a
#: detached :class:`Document` plus the chunk fields and ``ts_rank``).
@@ -96,11 +186,14 @@ class RetrievedChunk:
def lexical_tsquery(question: str) -> str | None:
"""OR-joined token string for ``to_tsquery('english', …)``, or ``None``.
Tokens are lowercased ``[a-z0-9]+`` runs, de-duplicated in order of
first appearance. Postgres does the lexing/stemming; a question whose
tokens are all stopwords lexes to an *empty* tsquery (which matches
nothing), so no special-casing is needed there. Pure-symbol questions
("???", "🔧") yield no tokens → ``None`` → no lexical query at all.
Tokens are lowercased ``[a-z0-9]+`` runs with DOTTED words kept whole
(see :data:`_TOKEN_RE` — the default parser lexes ``llama.cpp`` as
one lexeme, so the query must carry it whole too), de-duplicated in
order of first appearance. Postgres does the lexing/stemming; a
question whose tokens are all stopwords lexes to an *empty* tsquery
(which matches nothing), so no special-casing is needed there.
Pure-symbol questions ("???", "🔧") yield no tokens → ``None`` → no
lexical query at all.
"""
seen: set[str] = set()
tokens: list[str] = []
@@ -181,21 +274,141 @@ def _vector_candidates(
]
def _lexical_candidates(db: Session, question: str, limit: int) -> list[RetrievedChunk]:
"""Top-*limit* chunks matching the question's OR-tsquery (A7).
Ordered by ``ts_rank`` (with ``d.path, c.position`` as the
deterministic tie-break); an empty tsquery (stopword-only question)
simply matches nothing.
#: One name-hit row: the document fields (detached :class:`Document`)
#: plus the representative chunk — the ``is_summary`` chunk when the
#: document has one (its natural-language summary is the best chunk of
#: a machine file, and the chunk the vector list is likeliest to have
#: ranked too, so the RRF merge dedupes cleanly), else chunk 0.
_NAME_HIT_SQL = text(
"""
SELECT d.id AS doc_id,
d.source AS source,
d.path AS path,
d.full_path AS full_path,
d.title AS title,
d.content AS doc_content,
d.content_hash AS content_hash,
d.indexed_at AS indexed_at,
c.id AS chunk_id,
c.position AS position,
c.content AS content,
c.is_summary AS is_summary
FROM documents d
LEFT JOIN LATERAL (
SELECT id, position, content, is_summary
FROM chunks
WHERE document_id = d.id
ORDER BY is_summary DESC, position ASC
LIMIT 1
) c ON true
WHERE d.id = ANY(:ids) AND c.id IS NOT NULL
"""
)
def _name_hit_chunks(db: Session, question: str) -> list[RetrievedChunk]:
"""The documents whose NAME matches the question (the 2026-09-05
incident, the versioned-name signal).
A document is a name hit when its normalized name —
:func:`_normalize_name` of its ``title`` followed by the normalized
path stem (``qwen3.8-27b-juggernaut-vulkan.container`` →
``qwen3827bjuggernautvulkan``) — contains at least one
:func:`name_hit_tokens` candidate (``qwen38``). Ranked by
(distinct matched tokens, total matched length, source, path) —
catalog order is the final deterministic tie-break — capped at
:data:`NAME_HIT_LIMIT`. Each hit becomes one lexical
:class:`RetrievedChunk` (its representative chunk, ``fts_hit=True``,
``cosine=0.0``). Two lightweight queries: one projection over
(id, source, path, title) in catalog order, one LATERAL chunk
fetch for the ≤ :data:`NAME_HIT_LIMIT` winners (no full-content
load; the content joins in via the row fetch below).
"""
tokens = name_hit_tokens(question)
if not tokens:
return []
rows = db.execute(
select(Document.id, Document.source, Document.path, Document.title).order_by(
Document.source, Document.path
)
).all()
scored: list[tuple[int, int, uuid.UUID]] = []
by_id: dict[uuid.UUID, tuple[str, str]] = {} # id -> (source, path)
for doc_id, source, path, title in rows:
by_id[doc_id] = (source, path)
name = _normalize_name(title) + _normalize_name(Path(path).stem)
matched = [t for t in tokens if t in name]
if matched:
scored.append((len(matched), sum(len(t) for t in matched), doc_id))
if not scored:
return []
# Ranked by (distinct matched tokens, total matched length), the
# deterministic catalog tie-break (source, path) last.
scored.sort(
key=lambda s: (-s[0], -s[1], by_id[s[2]][0], by_id[s[2]][1])
)
ids = [s[2] for s in scored[:NAME_HIT_LIMIT]]
hit_rows = list(db.execute(_NAME_HIT_SQL, {"ids": ids}).all())
# The LATERAL query returns winners in id order; re-order by the
# ranked order computed above so the lexical list is deterministic.
order = {doc_id: rank for rank, doc_id in enumerate(ids)}
hit_rows.sort(key=lambda row: order[row.doc_id])
out: list[RetrievedChunk] = []
for row in hit_rows:
doc = Document(
id=row.doc_id,
source=row.source,
path=row.path,
full_path=row.full_path,
title=row.title,
content=row.doc_content,
content_hash=row.content_hash,
indexed_at=row.indexed_at,
)
out.append(
RetrievedChunk(
chunk_id=row.chunk_id,
position=row.position,
content=row.content,
score=0.0, # filled in by :func:`fuse`
document=doc,
cosine=0.0, # no vector rank — name-only hit
fts_hit=True, # the lexical signal — the A8 gate answers
is_summary=bool(row.is_summary),
)
)
return out
def _lexical_candidates(db: Session, question: str, limit: int) -> list[RetrievedChunk]:
"""The lexical candidate list: name hits, then FTS rows (A7).
The name-hit documents (:func:`_name_hit_chunks`, the versioned-name
signal) LEAD the list — they are the strongest lexical evidence for
a name-your-thing question — followed by the top-*limit* chunks
matching the question's OR-tsquery, ordered by ``ts_rank`` (with
``d.path, c.position`` as the deterministic tie-break). Deduped by
chunk id (a name-hit representative chunk the FTS list also ranked
appears once). An empty tsquery (stopword-only question) simply
contributes no FTS rows; a question with no name tokens contributes
no name hits — both halves are independent.
"""
out: list[RetrievedChunk] = []
seen: set[uuid.UUID] = set()
for rc in _name_hit_chunks(db, question):
if rc.chunk_id not in seen:
seen.add(rc.chunk_id)
out.append(rc)
tsquery = lexical_tsquery(question)
if tsquery is None:
return []
return out
rows = db.execute(
_LEXICAL_SQL, {"tsquery": tsquery, "limit": limit}
).all()
out: list[RetrievedChunk] = []
for row in rows:
if row.chunk_id in seen:
continue
seen.add(row.chunk_id)
doc = Document(
id=row.doc_id,
source=row.source,