diff --git a/Makefile b/Makefile index ebe5175..6db6a79 100644 --- a/Makefile +++ b/Makefile @@ -166,11 +166,11 @@ QUERY_TOP_K ?= 8 # ANSWER_MODE=quote for the legacy substring verifier. ANSWER_MODE= # (empty) defers to DEFAULT_QUERY_POLICY. ANSWER_MODE ?= claim_lattice -query: bootstrap ## ask the corpus a question [JSON=1 BURN=1 REPAIR=1 REPROMPTS=N ANSWER_MODE=claim_lattice|claim_lattice_pointer|quote]; JSON by default +query: bootstrap ## ask the corpus a question [JSON=1 BURN=1 REPAIR=1 REPROMPTS=N K="extra retrieval keywords" ANSWER_MODE=claim_lattice|claim_lattice_pointer|quote]; JSON by default @if [ -z "$$Q" ] && [ -z "$(Q)" ]; then \ - echo "usage: make query Q=\"your question\" [JSON=1 BURN=1 REPAIR=1 REPROMPTS=N ANSWER_MODE=claim_lattice|claim_lattice_pointer|quote]"; exit 2; \ + echo "usage: make query Q=\"your question\" [JSON=1 BURN=1 REPAIR=1 REPROMPTS=N K=\"extra retrieval keywords\" ANSWER_MODE=claim_lattice|claim_lattice_pointer|quote]"; exit 2; \ fi - $(ABORIST) --shards-dir $(SHARDS_DIR) query --top-k $(QUERY_TOP_K) $(if $(JSON),--json,) $(if $(BURN),--burn,) $(if $(REPAIR),--repair,) $(if $(REPROMPTS),--repair-reprompts $(REPROMPTS),) $(if $(ANSWER_MODE),--answer-mode $(ANSWER_MODE),) "$(Q)" + $(ABORIST) --shards-dir $(SHARDS_DIR) query --top-k $(QUERY_TOP_K) $(if $(JSON),--json,) $(if $(BURN),--burn,) $(if $(REPAIR),--repair,) $(if $(REPROMPTS),--repair-reprompts $(REPROMPTS),) $(if $(ANSWER_MODE),--answer-mode $(ANSWER_MODE),) $(if $(K),--retrieval-keywords "$(K)",) "$(Q)" query-dry: bootstrap ## like 'make query' but skip the LLM call (dry-run) [JSON=1 BURN=1 ANSWER_MODE=...] @if [ -z "$$Q" ] && [ -z "$(Q)" ]; then \ diff --git a/aborist/cli.py b/aborist/cli.py index 727ed6e..40496b7 100644 --- a/aborist/cli.py +++ b/aborist/cli.py @@ -412,6 +412,7 @@ def _cmd_query(args: argparse.Namespace) -> int: policy=call_policy, fidelity=getattr(args, "fidelity", None), burn_existing=bool(getattr(args, "burn", False)), + retrieval_keywords=getattr(args, "retrieval_keywords", None), ) if args.json: @@ -3058,6 +3059,21 @@ def build_parser() -> argparse.ArgumentParser: "constrained inference like Qwen 3.6 reasoner / Claude / GPT-4)." ), ) + query_cmd.add_argument( + "--retrieval-keywords", dest="retrieval_keywords", default=None, + help=( + "operator-supplied keywords appended to the question for " + "FTS5 retrieval ONLY — never sent to the LLM, never enters " + "cache_key, never reaches the verifier. Use to narrow " + "OR-mode retrieval on long discursive questions whose " + "content tokens get diluted by template phrasing. Example: " + "make query Q='what tech may enable one person to " + "reconstruct another person's thoughts...' " + "K='transcranial knowledge acquisition'. Pair with --burn " + "to force fresh inference (keywords are session-only and " + "cache-hits ignore them)." + ), + ) query_cmd.set_defaults(func=_cmd_query) inspect_cmd = sub.add_parser( diff --git a/aborist/qa/query.py b/aborist/qa/query.py index 0071514..46e0fd0 100644 --- a/aborist/qa/query.py +++ b/aborist/qa/query.py @@ -1115,6 +1115,7 @@ def query( chain: str = "private", fidelity: str | None = None, burn_existing: bool = False, + retrieval_keywords: str | None = None, ) -> dict: """Answer `question` using the corpus. Cache to qa_db. Returns a result dict. @@ -1135,6 +1136,22 @@ def query( rows were deleted (0 or 1 for the primary key; the equivalence- class fallback key is left alone so prior alt-mode records stay historic). + + `retrieval_keywords` augments the FTS5 search and title-filter + token set with operator-supplied keywords WITHOUT changing what + the LLM sees, what the verifier checks, or what enters cache_key. + Empirically observed 2026-05-01: long discursive questions like + 'what technology is currently or soon available which may enable + one person to reconstruct another person's thoughts...' under- + retrieve because their content tokens get diluted by template + phrasing. Appending domain keywords ('transcranial knowledge + acquisition') narrows OR-mode FTS5 to the topical article + (Neurotechnology) and lifts the verdict from HYBRID to STRICT. + This flag exposes that pattern explicitly. Cache implication: + keywords are session-only — not in cache_key, so successive + calls with different keywords on the same question can cache- + hit each other. Pair with ``burn_existing=True`` for fresh + inference each call. """ policy = policy or DEFAULT_QUERY_POLICY if fidelity is None: @@ -1151,8 +1168,19 @@ def query( t_start = time.monotonic() # 1. Search. + # + # Retrieval-only query string: question + operator-supplied + # ``retrieval_keywords`` (a hint, never part of the cache_key / + # LLM prompt / verifier surface). When the user passes + # `--retrieval-keywords "transcranial knowledge acquisition"`, + # only the FTS5 MATCH and title-filter token set see those + # tokens; the question text fed to the LLM and to question_hash + # stays untouched. + retrieval_query = question + if retrieval_keywords and retrieval_keywords.strip(): + retrieval_query = f"{question} {retrieval_keywords.strip()}" t_phase = time.monotonic() - hits = _search_corpus(shards_dir, single_db, question, over_fetch) + hits = _search_corpus(shards_dir, single_db, retrieval_query, over_fetch) if not hits: return { "status": "no_sources", @@ -1164,7 +1192,7 @@ def query( } core_match_roots = getattr(hits, "_core_match_roots", set()) root_to_shard = getattr(hits, "_root_to_shard", {}) - qtokens_lower = {t.lower() for t in _title_query_tokens(question)} + qtokens_lower = {t.lower() for t in _title_query_tokens(retrieval_query)} def _body_density_check(h) -> bool: # Lazy per-hit check: open the shard, count token mentions in this doc. @@ -1179,7 +1207,7 @@ def query( hits = _rerank( hits, - question, + retrieval_query, core_match_roots=core_match_roots, body_density_check=_body_density_check, )