Adding sanity to the recollection decider
This commit is contained in:
@@ -44,7 +44,7 @@ from .cue_scanner import scan_cues
|
||||
from .recollection import build_recollection_block, inject_recollection
|
||||
from .resolution_job import run_resolution_job, last_run_timestamp
|
||||
from .tokenizer import tokenize
|
||||
from .write_queue import enqueue_context_extract, enqueue_cue, start_worker, stop_worker
|
||||
from .write_queue import enqueue_context_discover, enqueue_cue, start_worker, stop_worker
|
||||
from .urd_writer import InsertRequest, insert_urd_edge
|
||||
from .wordnet import import_wordnet, CITATION as WORDNET_CITATION
|
||||
from .test_scenarios import SCENARIOS, seed_scenario, reset_scenario
|
||||
@@ -484,6 +484,21 @@ def extract_prompt_text(body: dict, path: str) -> str:
|
||||
return body.get("prompt", "")
|
||||
|
||||
|
||||
def _last_user_message_text(body: dict, path: str) -> str:
|
||||
"""
|
||||
Extract only the last user message for the write path.
|
||||
Agent responses, thinking traces, and system prompts are excluded —
|
||||
they are noise for concept discovery.
|
||||
"""
|
||||
if path in ("/api/chat", "/v1/chat/completions", "/v1/messages"):
|
||||
messages = body.get("messages", [])
|
||||
last_user = next((m for m in reversed(messages) if m.get("role") == "user"), None)
|
||||
if last_user:
|
||||
return " ".join(_extract_text_strings(last_user.get("content", "")))
|
||||
return ""
|
||||
return body.get("prompt", "")
|
||||
|
||||
|
||||
def inject_recollection_anthropic(body: dict, block: str) -> dict:
|
||||
"""
|
||||
Inject a recollection block into an Anthropic Messages API request.
|
||||
@@ -512,69 +527,80 @@ async def process_prompt(body: dict, path: str, pool, cfg: dict) -> dict:
|
||||
Returns a (possibly modified) body dict with the recollection block injected.
|
||||
"""
|
||||
read_threshold = float(await get_config(pool, "saliency_read_threshold", "0.5"))
|
||||
write_threshold = float(await get_config(pool, "saliency_write_threshold", "1.2"))
|
||||
conf_floor = float(await get_config(pool, "recollection_confidence_floor", "0.6"))
|
||||
recency_days = int(await get_config(pool, "recollection_recency_days", "90"))
|
||||
|
||||
prompt_text = extract_prompt_text(body, path)
|
||||
if not prompt_text.strip():
|
||||
# Extract only the last user message — agent responses and reasoning traces
|
||||
# are noise for both cue scanning and concept discovery.
|
||||
user_text = _last_user_message_text(body, path)
|
||||
if not user_text.strip():
|
||||
return body
|
||||
|
||||
# 1. Scan for explicit relationship cues (bypass threshold)
|
||||
cues = scan_cues(prompt_text)
|
||||
for cue in cues:
|
||||
# 1. Scan user message for explicit relationship cues (fast, inline, bypasses LLM).
|
||||
for cue in scan_cues(user_text):
|
||||
await enqueue_cue(cue)
|
||||
|
||||
# 2. Tokenise + update saliency
|
||||
# 2. Tokenise the recent context (last user + last assistant) for the read path.
|
||||
# Novel words from the user turn are also collected as LLM candidates.
|
||||
prompt_text = extract_prompt_text(body, path)
|
||||
tokens = tokenize(prompt_text)
|
||||
salient_for_read: list[int] = []
|
||||
|
||||
# Novel domain words found in this turn — not in the standard dictionary.
|
||||
# Capped to avoid flooding on unexpectedly large turns.
|
||||
MAX_NOVEL_PER_PROMPT = 8
|
||||
novel_this_prompt: list[str] = []
|
||||
# Candidate novel tokens from the USER message only — structural tokens
|
||||
# (paths, versions, numbers) are filtered out. Capped to avoid flooding
|
||||
# on very long messages.
|
||||
MAX_NOVEL_PER_TURN = 8
|
||||
novel_candidates: list[str] = []
|
||||
|
||||
# Only collect candidates from user-side tokens
|
||||
user_tokens = set(tokenize(user_text))
|
||||
|
||||
for token in tokens:
|
||||
soas_row = cache.soas_by_token.get(token)
|
||||
|
||||
if soas_row is None:
|
||||
# Token absent from dictionary → candidate novel domain word.
|
||||
# Skip structural tokens (paths, versions, numbers).
|
||||
if not _is_structural_token(token) and len(novel_this_prompt) < MAX_NOVEL_PER_PROMPT:
|
||||
novel_this_prompt.append(token)
|
||||
# Token absent from cache entirely — candidate domain word.
|
||||
# Restrict to user-side tokens so we don't mine agent responses.
|
||||
if (
|
||||
token in user_tokens
|
||||
and not _is_structural_token(token)
|
||||
and len(novel_candidates) < MAX_NOVEL_PER_TURN
|
||||
):
|
||||
novel_candidates.append(token)
|
||||
continue
|
||||
|
||||
if soas_row.saliency == 0.0 and soas_row.novelty == 0.0:
|
||||
# Common English word pre-seeded from the dictionary — not interesting.
|
||||
# Common English word — skip.
|
||||
continue
|
||||
|
||||
cache.record_encounter(soas_row.id)
|
||||
|
||||
# Only surface in recollection if saliency is above threshold.
|
||||
# Unconfirmed novel words (saliency=NOVEL_INITIAL_SALIENCY=0.1) are
|
||||
# deliberately kept below the threshold until the LLM confirms them.
|
||||
if soas_row.saliency >= read_threshold:
|
||||
salient_for_read.append(soas_row.id)
|
||||
|
||||
# Create SOAS entries for novel words and add them to the read list.
|
||||
# Capture first-seen context so zero-hit recollection can include a hint.
|
||||
for token in novel_this_prompt:
|
||||
ctx = _sentence_containing(prompt_text, token)
|
||||
soas_row = await create_novel_soas(pool, token, context=ctx)
|
||||
salient_for_read.append(soas_row.id)
|
||||
# Register novel candidates in SOAS at low saliency (below read threshold).
|
||||
# They become recollection attractors only after the LLM confirms them.
|
||||
for token in novel_candidates:
|
||||
ctx = _sentence_containing(user_text, token)
|
||||
await create_novel_soas(pool, token, context=ctx)
|
||||
# Do NOT add to salient_for_read — no zero-hit recollection until confirmed.
|
||||
|
||||
# Enqueue context-aware LLM extraction for all novel words found this turn.
|
||||
# The LLM reads the actual conversation text and extracts relationships from
|
||||
# evidence — one call per turn, not one per concept.
|
||||
if novel_this_prompt:
|
||||
await enqueue_context_extract(novel_this_prompt, prompt_text)
|
||||
# 3. Enqueue for LLM-driven discovery if there are candidates to evaluate.
|
||||
if novel_candidates and len(user_text) >= 20:
|
||||
await enqueue_context_discover(user_text, novel_candidates)
|
||||
|
||||
if not salient_for_read:
|
||||
return body
|
||||
|
||||
# 3. Build recollection block
|
||||
# 5. Build recollection block
|
||||
block = build_recollection_block(salient_for_read, conf_floor, recency_days)
|
||||
if not block:
|
||||
return body
|
||||
|
||||
# 4. Inject into messages
|
||||
# 6. Inject into messages
|
||||
if path == "/api/chat" or path == "/v1/chat/completions":
|
||||
body = dict(body)
|
||||
body["messages"] = inject_recollection(body.get("messages", []), block)
|
||||
|
||||
Reference in New Issue
Block a user