finally getting accurate answers
This commit is contained in:
+114
-7
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user