CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 44s
Step 2 of the Scribe -> Grimoire rename. - src/scribe -> src/grimoire, and every import, patch target, module path, hypercorn entry point and `python -m grimoire.restore` with it; pyproject, uv.lock and the frontend package are named grimoire. - Image git.fabledsword.com/bvandeusen/fabledgrimoire; CI's Postgres service runs as grimoire / grimoire_test. - Compose files default the DB user, database and (prod) volume to grimoire, each overridable from .env, so an install created as Scribe keeps its data by setting POSTGRES_USER, POSTGRES_DB and PGDATA_VOLUME. - Backups are written as grimoire-<ts>.tar with format grimoire-backup-tar. Archives named scribe-* with the scribe-backup-tar manifest still restore and are still counted at the destination (folder and S3), with tests. - Exports, the MCP auth realm, the ASGI scope key and the persistent logger carry the new name. Kept on purpose: the secret-sealing key label stays "scribe.secrets.v1", because every stored secret was derived under it. Plugin identity (plugin name, hooks, env vars, .scribe marker, skills) is step 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
147 lines
6.5 KiB
Python
147 lines
6.5 KiB
Python
"""One registry of the retrieval surfaces, and the two numbers each one has.
|
|
|
|
WHY THIS EXISTS
|
|
|
|
Every push arm has the same shape: a settings key, a default, and a limit. A
|
|
floor is chosen and adjusted by the model that uses it, with the operator able
|
|
to touch it — and a tuning surface cannot be consistent for the model if every
|
|
arm spells its own configuration differently. So the arms do not own their
|
|
numbers; they read them from here, and the tuning tool, the routes and the
|
|
Settings UI all enumerate THIS table rather than hard-coding special cases.
|
|
|
|
WHAT A FLOOR IS, AND WHAT IT IS NOT
|
|
|
|
It is not a relevance judgement. Relevance is decided by the reader, which is
|
|
the only participant that can read a trigger against a situation — and the
|
|
injected line says so out loud ("read it before deciding it does not apply").
|
|
|
|
A floor answers the cheaper question: *is this worth ranking at all*. `k` is
|
|
what binds, and `k` is a BUDGET — how much of this surface's attention a
|
|
candidate list may spend. An arm that fires before every Bash call cannot
|
|
afford what an arm that fires once a turn can.
|
|
|
|
WHY THE DEFAULTS BELOW ARE STARTING POINTS AND NOT ANSWERS
|
|
|
|
A cosine score is a distance in BAAI/bge-small-en-v1.5's vector space, measured
|
|
against one corpus. It cannot transfer to an install with different records,
|
|
and a shipped default cannot be defended from any one install's telemetry.
|
|
Grimoire ships a starting point and the means to correct it, so the values carry
|
|
the measurement that motivated them without claiming to be right for anybody
|
|
else.
|
|
|
|
HOW A FLOOR SHOULD ACTUALLY BE MOVED
|
|
|
|
By reading the records the floor refused — `retrieval_telemetry(
|
|
near_miss_samples=N)` returns them by id — and never by the percentile alone.
|
|
An arm can log dozens of consecutive declines with the refused score a hair
|
|
under the bar while every percentile says "lower it", and the refused record,
|
|
once read, turns out to be a false positive that a lower bar would deliver on
|
|
every matching act. The statistic and the correct action can point in opposite
|
|
directions, and only opening the record tells them apart.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from grimoire.services.settings import bounded_float, get_setting
|
|
from grimoire.services.retrieval_pipeline import ( # noqa: F401 - Surface re-exported
|
|
TUNED_ARMS, Surface,
|
|
)
|
|
|
|
# A budget nobody should be able to set past. Not a tuning value — a guard on
|
|
# the worst case, so a mistyped setting cannot turn a menu into a wall of text.
|
|
# Shared by every surface because it bounds the same thing everywhere: how many
|
|
# lines a single unsolicited injection may occupy.
|
|
MAX_BUDGET = 10
|
|
|
|
|
|
# THE TABLE IS READ OFF THE SPECS. Each ranked arm in
|
|
# `retrieval_pipeline` carries its own `Surface` — the floor and budget pair,
|
|
# its settings keys and defaults, and the prose a tuner reads — so an arm added
|
|
# there is tunable by construction, and nothing here can drift from it.
|
|
SURFACES: dict[str, Surface] = {
|
|
arm.tuning.name: arm.tuning for arm in TUNED_ARMS if arm.tuning is not None
|
|
}
|
|
|
|
# Reserved slots are deliberately absent. `preference_slot`, `reuse_slot` and
|
|
# `lesson_slot` borrow their parent arm's floor and are hard-limited to one hit
|
|
# each, because their entire purpose is to guarantee a single line to a kind of
|
|
# record that keeps losing a general score contest — or, for
|
|
# `lesson_slot`, to a kind whose loss is total rather than merely a lost
|
|
# convenience, since a lesson has no act arm to fall back on and nobody browses
|
|
# lessons looking for one. A budget of "1" is the feature;
|
|
# exposing it as tunable would invite setting it to 0 and silently removing the
|
|
# guarantee.
|
|
#
|
|
# They are still logged under their own `source`, which is what keeps them
|
|
# judgeable without being tunable: `best_available_id` names the record each
|
|
# bar refused, so a slot that never places, or one that places weak hits, shows
|
|
# up as evidence rather than as an argument.
|
|
|
|
|
|
def surface_names() -> list[str]:
|
|
"""Every tunable surface, in a stable order for menus and listings."""
|
|
return list(SURFACES)
|
|
|
|
|
|
def get_surface(name: str) -> Surface:
|
|
"""Look one up, refusing an unknown name loudly.
|
|
|
|
A typo'd surface must not be writable. Settings keys are free-form strings
|
|
in a generic table, so a tuning call naming `pretool_rule` would otherwise
|
|
write a key nothing ever reads — a change that appears to succeed, reports a
|
|
new value, and alters nothing.
|
|
"""
|
|
try:
|
|
return SURFACES[name]
|
|
except KeyError:
|
|
raise ValueError(
|
|
f"unknown retrieval surface {name!r}. Tunable surfaces are: "
|
|
+ ", ".join(surface_names())
|
|
) from None
|
|
|
|
|
|
def dial_for_key(key: str) -> tuple[str, str] | None:
|
|
"""Which `(surface, dial)` a settings key belongs to, or None.
|
|
|
|
The registry read backwards, and it exists for one caller: the generic
|
|
`/api/settings` endpoint, which accepts any key at all. Without this, a
|
|
floor written through that endpoint moves with no event recorded, and the
|
|
tuning history says nothing happened — a trail with holes in it, which is
|
|
worse than no trail because it reads as complete.
|
|
|
|
Derived rather than listed so a seventh surface is covered the moment it is
|
|
added here, which is the only way this stays true.
|
|
"""
|
|
for surface in SURFACES.values():
|
|
if key == surface.floor_key:
|
|
return (surface.name, "floor")
|
|
if key == surface.budget_key:
|
|
return (surface.name, "budget")
|
|
return None
|
|
|
|
|
|
async def floor_for(user_id: int, name: str) -> float:
|
|
"""This install's current floor for a surface, clamped to [0, 1]."""
|
|
s = get_surface(name)
|
|
raw = await get_setting(user_id, s.floor_key, str(s.floor_default))
|
|
return bounded_float(raw, s.floor_default)
|
|
|
|
|
|
async def budget_for(user_id: int, name: str) -> int:
|
|
"""This install's current budget for a surface, clamped to [1, MAX_BUDGET].
|
|
|
|
The lower clamp is 1, never 0: a surface turned off is turned off by its
|
|
`enabled` switch, which says so. A budget of zero would be an arm that runs
|
|
a search, logs a retrieval, and renders nothing — indistinguishable in the
|
|
telemetry from a bar nothing cleared, which is the exact confusion this
|
|
milestone exists to remove.
|
|
"""
|
|
s = get_surface(name)
|
|
raw = await get_setting(user_id, s.budget_key, "")
|
|
if not raw and s.budget_falls_back_to:
|
|
raw = await get_setting(user_id, s.budget_falls_back_to, "")
|
|
try:
|
|
value = int(float(raw)) if raw else s.budget_default
|
|
except (TypeError, ValueError):
|
|
value = s.budget_default
|
|
return min(MAX_BUDGET, max(1, value))
|