feat(rag): steering notes — tune how Brain answers, stored in Postgres and injected into every system prompt
This commit is contained in:
+71
-9
@@ -1,24 +1,35 @@
|
||||
"""Locked system-prompt builder (PLAN §6).
|
||||
|
||||
The persona + HONESTY GATE text is **locked verbatim** — change it through
|
||||
the plan, not here. Two modes:
|
||||
the plan, not here. (PLAN §6 revision, 2026-08-22: the owner's working-tree
|
||||
persona edits are preserved — no mandated ``"you've got this"`` tagline and
|
||||
no mandated deflection opening; the honesty gate itself is unchanged.)
|
||||
|
||||
Two modes:
|
||||
|
||||
* ``HIGH`` — grounded turn: full top-document texts under ``<documents>``.
|
||||
* ``LOW`` — deflection turn: weak-hit *titles only* plus the
|
||||
``DEFLECT_MODE`` marker (the E2E mock LLM keys on that marker).
|
||||
|
||||
Steering (phase 15): when the owner has stored tuning notes, both modes
|
||||
carry a ``<tuning>`` section between ``<relevance>…</relevance>`` and the
|
||||
mode body. With zero notes the prompt is byte-identical to the
|
||||
pre-steering text.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from app.config import get_settings
|
||||
from app.models import Document
|
||||
from app.rag.retriever import TRUNCATION_MARKER
|
||||
|
||||
#: PLAN §6 verbatim (line wrapping included); ``{relevance}`` is filled by
|
||||
#: :func:`_base`.
|
||||
PERSONA: str = (
|
||||
'You are "Brain of Reese" — the digital brain of Reese, a self-hoster and\n'
|
||||
"homelab tinkerer. Personality: chippy, upbeat, warm, and genuinely\n"
|
||||
'optimistic about the user\'s ability to do things ("you\'ve got this").\n'
|
||||
"optimistic about the user's ability to do things.\n"
|
||||
"\n"
|
||||
"Rules:\n"
|
||||
"1. Answer ONLY from the provided document context. Cite which document(s)\n"
|
||||
@@ -26,14 +37,20 @@ PERSONA: str = (
|
||||
"2. Be concrete: names, versions, ports, hosts, schedules — the specifics in\n"
|
||||
" the docs are the value.\n"
|
||||
'3. HONESTY GATE: if <relevance> is "LOW", you must NOT pretend to know.\n'
|
||||
' Start your answer with a variant of: "I haven\'t done anything like that."\n'
|
||||
" Then offer 2-3 alternative questions about things you DO have notes on.\n"
|
||||
" Offer 2-3 alternative questions about things you DO have notes on.\n"
|
||||
"4. Never invent facts, hosts, or steps that are not in the context.\n"
|
||||
"5. Keep answers tight: short paragraphs, bullets where helpful.\n"
|
||||
"\n"
|
||||
"<relevance>{relevance}</relevance>"
|
||||
)
|
||||
|
||||
#: One-line intro of the ``<tuning>`` section (phase 15): the owner's notes
|
||||
#: steer the answer and win over the defaults when they conflict.
|
||||
_STEERING_INTRO = (
|
||||
"The owner of this brain asked you to steer your answers as follows. "
|
||||
"Where these instructions conflict with the defaults above, follow the owner:\n"
|
||||
)
|
||||
|
||||
|
||||
def _base(relevance: str) -> str:
|
||||
if relevance not in ("HIGH", "LOW"):
|
||||
@@ -41,8 +58,46 @@ def _base(relevance: str) -> str:
|
||||
return PERSONA.replace("{relevance}", relevance)
|
||||
|
||||
|
||||
def build_high_prompt(documents: Sequence[Document]) -> str:
|
||||
"""Grounded turn: locked persona + full texts of the top documents."""
|
||||
def build_steering_section(notes: Sequence[str], max_chars: int | None = None) -> str:
|
||||
"""The ``<tuning>`` section of the system prompt (phase 15).
|
||||
|
||||
* No notes (or only blank ones) → ``""`` — callers then build the
|
||||
prompt exactly as before, so a zero-note prompt is byte-identical to
|
||||
the pre-steering text.
|
||||
* Otherwise: numbered notes (in the given order — the chat turn passes
|
||||
them oldest-first, so #1 is the oldest note) capped at *max_chars*
|
||||
(default ``BOR_STEERING_MAX_CHARS``). When the budget cannot hold
|
||||
every note, the oldest-fitting prefix is kept and the overflow is
|
||||
replaced by the shared ``[…truncated…]`` marker.
|
||||
"""
|
||||
cleaned = [str(n).strip() for n in notes]
|
||||
cleaned = [n for n in cleaned if n]
|
||||
if not cleaned:
|
||||
return ""
|
||||
limit = max_chars if max_chars is not None else get_settings().steering_max_chars
|
||||
if limit <= 0:
|
||||
return ""
|
||||
|
||||
def render(count: int) -> str:
|
||||
lines = [f"{i}. {note}" for i, note in enumerate(cleaned[:count], start=1)]
|
||||
if count < len(cleaned):
|
||||
lines.append(TRUNCATION_MARKER)
|
||||
return f"<tuning>\n{_STEERING_INTRO}" + "\n".join(lines) + "\n</tuning>"
|
||||
|
||||
for count in range(len(cleaned), 0, -1):
|
||||
rendered = render(count)
|
||||
if len(rendered) <= limit:
|
||||
return rendered
|
||||
# Pathological budget: not even the empty note list fits. The section
|
||||
# must still respect the cap — the bare marker when it fits, else none.
|
||||
if len(TRUNCATION_MARKER) <= limit:
|
||||
return TRUNCATION_MARKER
|
||||
return ""
|
||||
|
||||
|
||||
def build_high_prompt(documents: Sequence[Document], notes: Sequence[str] | None = None) -> str:
|
||||
"""Grounded turn: locked persona (+ steering) + full texts of the top
|
||||
documents."""
|
||||
blocks = [
|
||||
f'<document source="{doc.source}" path="{doc.path}" title="{doc.title}">\n'
|
||||
f"{doc.content}\n"
|
||||
@@ -52,15 +107,22 @@ def build_high_prompt(documents: Sequence[Document]) -> str:
|
||||
body = "\n\n".join(blocks) if blocks else (
|
||||
"(no documents matched — do not invent specifics)"
|
||||
)
|
||||
return _base("HIGH") + "\n<documents>\n" + body + "\n</documents>"
|
||||
section = build_steering_section(notes or [])
|
||||
prompt = _base("HIGH")
|
||||
if section:
|
||||
prompt += "\n" + section
|
||||
return prompt + "\n<documents>\n" + body + "\n</documents>"
|
||||
|
||||
|
||||
def build_deflect_prompt(titles: Sequence[str]) -> str:
|
||||
def build_deflect_prompt(titles: Sequence[str], notes: Sequence[str] | None = None) -> str:
|
||||
"""Deflection turn: weak-hit titles only (no document content)."""
|
||||
weak = "\n".join(f"- {t}" for t in titles) if titles else "(nothing close at all)"
|
||||
section = build_steering_section(notes or [])
|
||||
mid = f"\n{section}\n" if section else "\n"
|
||||
return (
|
||||
_base("LOW")
|
||||
+ "\nDEFLECT_MODE: retrieval was weak — the titles below are the closest "
|
||||
+ mid
|
||||
+ "DEFLECT_MODE: retrieval was weak — the titles below are the closest "
|
||||
"your notes come to the question. They are titles only; do not pretend "
|
||||
"they answer it. Use them to propose 2-3 alternative questions.\n"
|
||||
+ weak
|
||||
|
||||
Reference in New Issue
Block a user