feat(plugin): auto-inject retrieves on the conversation, not the typed words alone (#4364)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / integration (push) Successful in 49s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Failing after 1m7s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / integration (push) Successful in 49s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Failing after 1m7s
CI & Build / Build & push image (push) Skipped
The prompt hook sent only the operator's message, so a mid-session
follow-up ("yes do that") named nothing a note, lesson or rule could
match. The hook now reads the tail of the last assistant reply from the
transcript and sends it as `ctx`; the notes and rule arms append it to a
short prompt (<= 280 chars), prompt first, capped at 600 chars. No model
tokens: it is embedding input, and the injected menu's budget is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -537,6 +537,36 @@ _AUTOINJECT_BAND = 0.10
|
||||
# was an accident of which arm got a configurable budget first.
|
||||
_AUTOINJECT_MAX_TOP_K = MAX_BUDGET
|
||||
|
||||
# THE CONVERSATION BESIDE THE PROMPT (#4364). The operator's message is the
|
||||
# only query this arm had, and mid-session it is mostly a follow-up — "yes do
|
||||
# that", "now fix the filter" — that names nothing a record could match. The
|
||||
# hook now sends the tail of the last assistant reply as `context`, and it is
|
||||
# appended to a SHORT prompt only: a prompt that already says what it is about
|
||||
# is the better query on its own, and diluting it is the one way this can make
|
||||
# retrieval worse.
|
||||
#
|
||||
# Neither number costs a model token. The query is embedding input; what the
|
||||
# session pays for is the menu, and the budget bounds that unchanged.
|
||||
# 280 — about two sentences. Above it a prompt carries its own subject.
|
||||
# 600 — the context cap. Prompt + context stays well inside bge-small's
|
||||
# 512-token window, prompt FIRST, so truncation can only ever cut
|
||||
# context and never the operator's words.
|
||||
_AUTOINJECT_CONTEXT_PROMPT_MAX = 280
|
||||
_AUTOINJECT_CONTEXT_MAX = 600
|
||||
|
||||
|
||||
def _autoinject_query(prompt: str, context: str) -> str:
|
||||
"""The prompt, with recent conversation appended when the prompt is thin.
|
||||
|
||||
The prompt leads and context is its tail, cut from the END of the reply —
|
||||
a reply's closing lines are where it says what it did and what is next,
|
||||
which is what the operator's follow-up is answering.
|
||||
"""
|
||||
ctx = " ".join((context or "").split())[-_AUTOINJECT_CONTEXT_MAX:]
|
||||
if not ctx or len(prompt) > _AUTOINJECT_CONTEXT_PROMPT_MAX:
|
||||
return prompt
|
||||
return f"{prompt}\n\n{ctx}"
|
||||
|
||||
# --- the prompt-boundary rule arm (#3852) ------------------------------------
|
||||
#
|
||||
# Both existing rule arms are keyed on something the session is about to DO —
|
||||
@@ -961,6 +991,7 @@ async def build_autoinject_hint(
|
||||
query: str,
|
||||
project_id: int = 0,
|
||||
exclude_ids: list[int] | None = None,
|
||||
context: str = "",
|
||||
) -> dict:
|
||||
"""Title-first awareness hint for the plugin's UserPromptSubmit hook.
|
||||
|
||||
@@ -982,6 +1013,9 @@ async def build_autoinject_hint(
|
||||
q = (query or "").strip()
|
||||
if not cfg["enabled"] or not q:
|
||||
return empty
|
||||
# Everything below searches, logs and fills its slots on the ENRICHED
|
||||
# query, so the telemetry row records what was actually asked (#4364).
|
||||
q = _autoinject_query(q, context)
|
||||
|
||||
# THE LEDGER LEAVES THE SEARCH (#4101). `exclude_ids` used to go into
|
||||
# `semantic_search_notes` itself, so a record this session had already been
|
||||
@@ -1286,6 +1320,7 @@ async def build_prompt_rule_hint(
|
||||
project_id: int = 0,
|
||||
exclude_rule_ids: list[int] | None = None,
|
||||
held_rule_ids: list[int] | None = None,
|
||||
context: str = "",
|
||||
) -> dict:
|
||||
"""Rules and preferences that may apply to what the operator just asked.
|
||||
|
||||
@@ -1319,6 +1354,11 @@ async def build_prompt_rule_hint(
|
||||
q = (query or "").strip()
|
||||
if not q:
|
||||
return out
|
||||
# The same enrichment the notes arm gets (#4364). A rule is meant to
|
||||
# arrive while the work it governs is under way, not once the operator
|
||||
# names it — and "yes, go ahead" names nothing a trigger can match, while
|
||||
# the reply it answers ("commit this to dev and push") does.
|
||||
q = _autoinject_query(q, context)
|
||||
|
||||
try:
|
||||
threshold = await floor_for(user_id, "prompt_rule")
|
||||
|
||||
Reference in New Issue
Block a user