Adds referent tracking, session graph traversal, unknown-domain gating, correction propagation, compositional surface assembly, and regression coverage. Follow-up fixes included before merge: - split probe/commit/finalize turn flow so unknown-domain checks run before current-query vault writes - record real input tokens and input versors for sync and async session paths - return true graph distances from backward walks and consume them in correction decay - synchronize corrected graph outputs into vault-backed recall and live referent state - regenerate correction responses from corrected context rather than correction text - keep coreference pronouns lowercase in question bodies - centralize elaboration-string construction to avoid plan/surface drift - add targeted dialogue fluency regression tests
220 lines
8.1 KiB
Python
220 lines
8.1 KiB
Python
"""
|
|
vault/decompose.py — FieldDecomposer and UnknownDomainGate
|
|
|
|
When VaultStore.recall() returns no results above the coherence floor for a
|
|
query versor, this module provides two complementary mechanisms:
|
|
|
|
1. FieldDecomposer
|
|
Decomposes the query versor into its top-3 grade components
|
|
(scalar, vector, bivector) in Cl(4,1) and recalls each component
|
|
separately from the vault. The results are blended by component
|
|
norm weight, giving a composed recall even for novel query points
|
|
that have no direct vault entry. This is the geometric
|
|
generalisation mechanism — unknown concepts return a weighted
|
|
combination of familiar field directions.
|
|
|
|
2. UnknownDomainGate
|
|
If the best recall score across both direct and decomposed recall
|
|
is below UNKNOWN_FLOOR, the gate fires. ChatRuntime checks the gate
|
|
before proposition formation and routes to a safe "I don't have field
|
|
coordinates for that" surface rather than fabricating a low-confidence
|
|
answer.
|
|
|
|
Grade structure for Cl(4,1), 32-dimensional multivector
|
|
--------------------------------------------------------
|
|
Grade 0 (scalar): index 0 → 1 component
|
|
Grade 1 (vector): indices 1-5 → 5 components (e1..e4, e_inf or e0)
|
|
Grade 2 (bivector): indices 6-15 → 10 components
|
|
Grade 3 (trivector):indices 16-25 → 10 components
|
|
Grade 4: indices 26-30 → 5 components
|
|
Grade 5 (pseudoscalar): index 31 → 1 component
|
|
|
|
We decompose into grades 0-2 because they carry the bulk of CGA point/
|
|
direction/circle semantics. Grades 3-5 are preserved as a residual.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING
|
|
|
|
import numpy as np
|
|
|
|
if TYPE_CHECKING:
|
|
from vault.store import VaultStore
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Constants
|
|
# ---------------------------------------------------------------------------
|
|
|
|
#: Minimum best-match score to consider recall meaningful.
|
|
#: Below this, UnknownDomainGate fires.
|
|
UNKNOWN_FLOOR: float = 0.15
|
|
|
|
#: Grade slice boundaries for a 32-dim Cl(4,1) multivector.
|
|
_GRADE_SLICES: tuple[tuple[int, int], ...] = (
|
|
(0, 1), # grade 0 — scalar
|
|
(1, 6), # grade 1 — vector
|
|
(6, 16), # grade 2 — bivector
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# FieldDecomposer
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class DecomposedRecallResult:
|
|
"""Aggregated recall from grade-split decomposition."""
|
|
hits: list[dict] # merged, weight-sorted recall hits
|
|
best_score: float # highest individual component score
|
|
component_scores: tuple[float, ...] # one score per grade component
|
|
unknown: bool # True if best_score < UNKNOWN_FLOOR
|
|
|
|
|
|
class FieldDecomposer:
|
|
"""
|
|
Decomposes a query versor into grade components and recalls each
|
|
from a VaultStore, then blends the results by component norm.
|
|
"""
|
|
|
|
def recall(
|
|
self,
|
|
vault: "VaultStore",
|
|
query: np.ndarray,
|
|
top_k: int = 5,
|
|
) -> DecomposedRecallResult:
|
|
"""
|
|
Parameters
|
|
----------
|
|
vault : VaultStore instance to query
|
|
query : 1-D float32 array of length 32 (Cl(4,1) multivector)
|
|
top_k : results per grade component; final list deduped and trimmed
|
|
|
|
Returns
|
|
-------
|
|
DecomposedRecallResult
|
|
"""
|
|
q = np.asarray(query, dtype=np.float32)
|
|
if q.ndim != 1:
|
|
raise ValueError(f"query must be 1-D, got shape {q.shape}")
|
|
q_len = q.shape[0]
|
|
|
|
component_scores: list[float] = []
|
|
component_hits: list[list[dict]] = []
|
|
component_weights: list[float] = []
|
|
|
|
for start, end in _GRADE_SLICES:
|
|
if start >= q_len:
|
|
continue
|
|
end_clamped = min(end, q_len)
|
|
grade_vec = np.zeros(q_len, dtype=np.float32)
|
|
grade_vec[start:end_clamped] = q[start:end_clamped]
|
|
norm = float(np.linalg.norm(grade_vec))
|
|
if norm < 1e-8:
|
|
component_scores.append(0.0)
|
|
component_hits.append([])
|
|
component_weights.append(0.0)
|
|
continue
|
|
|
|
hits = vault.recall(grade_vec, top_k=top_k)
|
|
best = max((h["score"] for h in hits), default=0.0)
|
|
component_scores.append(best)
|
|
component_hits.append(hits)
|
|
component_weights.append(norm)
|
|
|
|
# Blend results: weight each hit list by its component's vector norm.
|
|
# Deduplicate by vault index, keeping the highest weighted score.
|
|
total_weight = sum(component_weights) or 1.0
|
|
merged: dict[int, dict] = {}
|
|
for hits, w in zip(component_hits, component_weights):
|
|
rel_w = w / total_weight
|
|
for h in hits:
|
|
idx = h["index"]
|
|
weighted_score = h["score"] * rel_w
|
|
if idx not in merged or merged[idx]["score"] < weighted_score:
|
|
merged[idx] = {
|
|
"versor": h["versor"],
|
|
"score": weighted_score,
|
|
"metadata": h["metadata"],
|
|
"index": idx,
|
|
}
|
|
|
|
sorted_hits = sorted(merged.values(), key=lambda h: h["score"], reverse=True)
|
|
best_score = sorted_hits[0]["score"] if sorted_hits else 0.0
|
|
|
|
return DecomposedRecallResult(
|
|
hits=sorted_hits[:top_k],
|
|
best_score=best_score,
|
|
component_scores=tuple(component_scores),
|
|
unknown=best_score < UNKNOWN_FLOOR,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# UnknownDomainGate
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class GateDecision:
|
|
"""Result of an UnknownDomainGate check."""
|
|
fire: bool # True → query is outside known domain
|
|
best_score: float # best recall score seen
|
|
source: str # "direct" | "decomposed" | "empty_vault"
|
|
|
|
|
|
class UnknownDomainGate:
|
|
"""
|
|
Checks whether a query versor falls outside the vault's known domain.
|
|
|
|
Usage in ChatRuntime.chat()
|
|
---------------------------
|
|
gate = UnknownDomainGate()
|
|
decomposer = FieldDecomposer()
|
|
|
|
direct_hits = ctx.vault.recall(query_F, top_k=3)
|
|
direct_best = max((h["score"] for h in direct_hits), default=0.0)
|
|
|
|
decision = gate.check(direct_best, vault=ctx.vault, query=query_F, decomposer=decomposer)
|
|
if decision.fire:
|
|
return _unknown_domain_response() # safe surface, no fabrication
|
|
"""
|
|
|
|
def __init__(self, floor: float = UNKNOWN_FLOOR) -> None:
|
|
self._floor = floor
|
|
|
|
def check(
|
|
self,
|
|
direct_best_score: float,
|
|
vault: "VaultStore",
|
|
query: np.ndarray,
|
|
decomposer: FieldDecomposer | None = None,
|
|
) -> GateDecision:
|
|
"""
|
|
Returns a GateDecision. If the vault is empty, fires immediately.
|
|
If direct recall passes the floor, clears immediately.
|
|
Otherwise, falls back to decomposed recall.
|
|
"""
|
|
if len(vault) == 0:
|
|
return GateDecision(fire=True, best_score=0.0, source="empty_vault")
|
|
|
|
if direct_best_score >= self._floor:
|
|
return GateDecision(fire=False, best_score=direct_best_score, source="direct")
|
|
|
|
# Attempt decomposed recall as fallback
|
|
if decomposer is not None:
|
|
result = decomposer.recall(vault, query, top_k=3)
|
|
if not result.unknown:
|
|
return GateDecision(fire=False, best_score=result.best_score, source="decomposed")
|
|
return GateDecision(fire=True, best_score=result.best_score, source="decomposed")
|
|
|
|
return GateDecision(fire=True, best_score=direct_best_score, source="direct")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Module-level default instances (stateless — safe to share)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
default_decomposer = FieldDecomposer()
|
|
default_gate = UnknownDomainGate()
|