feat(rag): steering notes — tune how Brain answers, stored in Postgres and injected into every system prompt

This commit is contained in:
2026-08-22 16:44:42 -04:00
parent 19df7df99d
commit fc0d9a2d5c
19 changed files with 1589 additions and 34 deletions
+35 -7
View File
@@ -16,6 +16,11 @@ gets a grounded answer. Deflection mode carries weak-hit *titles only*
(never document content) plus deterministic "Maybe try" chips, and the
``done`` event / ``query_log`` row record ``deflected=true``, the weak
score and the ``fts_hits`` count.
Steering (phase 15): the owner's stored tuning notes are loaded per turn
(oldest first) and injected into the system prompt as a ``<tuning>``
section — both the HIGH and the LOW prompt carry it. The per-turn log
line records ``tuning=N`` (the number of injected notes).
"""
from __future__ import annotations
@@ -30,6 +35,7 @@ from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse, StreamingResponse
from sqlalchemy.orm import Session
from app.api.steering import load_steering_notes
from app.config import Settings, get_settings
from app.db import db_available, get_db
from app.models import Document, QueryLog
@@ -71,9 +77,14 @@ class TurnPlan:
system_prompt: str
docs: list[Document] # cited sources (weak hits when deflected)
suggestions: list[str] # "Maybe try" chips (deflected turns only)
tuning_count: int = 0 # steering notes injected into the system prompt
def plan_turn(chunks: Sequence[RetrievedChunk], settings: Settings) -> TurnPlan:
def plan_turn(
chunks: Sequence[RetrievedChunk],
settings: Settings,
notes: Sequence[str] | None = None,
) -> TurnPlan:
"""Apply the honesty gate (A8, revised) and assemble prompt + context.
* **HIGH (grounded)** when ``best_cosine >= threshold`` **or**
@@ -88,22 +99,36 @@ def plan_turn(chunks: Sequence[RetrievedChunk], settings: Settings) -> TurnPlan:
``top_score`` (stored in ``query_log``) is the best cosine, so the
gate input is always a pure vector-similarity number; the lexical
signal is recorded separately as ``fts_hits``.
*notes* are the owner's steering notes (phase 15, oldest first):
when non-empty, both the HIGH and the LOW prompt carry the
``<tuning>`` section; with no notes the prompts are unchanged.
"""
steering = list(notes or [])
best_cosine = max((c.cosine for c in chunks), default=0.0)
fts_hits = sum(1 for c in chunks if c.fts_hit)
if best_cosine >= settings.relevance_threshold or fts_hits > 0:
docs = select_documents(
chunks, n=settings.top_n_docs, max_chars=settings.max_context_chars
)
return TurnPlan(best_cosine, fts_hits, False, build_high_prompt(docs), docs, [])
return TurnPlan(
best_cosine,
fts_hits,
False,
build_high_prompt(docs, notes=steering),
docs,
[],
len(steering),
)
titles = weak_hit_titles(chunks)
return TurnPlan(
best_cosine,
fts_hits,
True,
build_deflect_prompt(titles),
build_deflect_prompt(titles, notes=steering),
select_documents(chunks, n=settings.top_n_docs, max_chars=settings.max_context_chars),
derive_suggestions(titles, settings.suggestions),
len(steering),
)
@@ -150,12 +175,14 @@ async def chat(
return
embed_ms = int((time.monotonic() - t0) * 1000)
# 2. Retrieve top-K chunks, then the honesty gate (A8) picks the
# HIGH (grounded) or LOW (deflected) prompt + context.
# 2. Retrieve top-K chunks, load the owner's steering notes
# (phase 15), then the honesty gate (A8) picks the HIGH
# (grounded) or LOW (deflected) prompt + context.
settings = get_settings()
try:
steering_notes = load_steering_notes(db)
chunks = retrieve(db, request.message, question_vec)
plan = plan_turn(chunks, settings)
plan = plan_turn(chunks, settings, notes=steering_notes)
except Exception: # noqa: BLE001 — DB failure mid-turn
logger.exception(
"chat: retrieval failed question=%r total_ms=%d",
@@ -211,12 +238,13 @@ async def chat(
logger.exception("chat: failed to write query_log question=%r", request.message)
logger.info(
"question=%r embed_ms=%d top_score=%.3f fts_hits=%d threshold=%.2f "
"question=%r embed_ms=%d top_score=%.3f fts_hits=%d tuning=%d threshold=%.2f "
"deflected=%s sources=%r total_ms=%d",
request.message,
embed_ms,
plan.top_score,
plan.fts_hits,
plan.tuning_count,
settings.relevance_threshold,
plan.deflected,
source_paths,