Add ADR-0008 salience attention
Add salience and attention operators, wire salience-gated candidate selection into generation, expose vault/salience trace telemetry, and add tests proving non-placeholder salience behavior.
This commit is contained in:
parent
df9ced7104
commit
aadaf11612
12 changed files with 304 additions and 12 deletions
|
|
@ -38,6 +38,8 @@ class ChatResponse:
|
|||
output_language: str
|
||||
frame_pack: str
|
||||
walk_surface: str
|
||||
salience_top_k: int | None
|
||||
candidates_used: int | None
|
||||
|
||||
|
||||
class ChatRuntime:
|
||||
|
|
@ -57,6 +59,10 @@ class ChatRuntime:
|
|||
max_tokens=config.max_tokens,
|
||||
allow_cross_language_recall=config.allow_cross_language_recall,
|
||||
allow_cross_language_generation=config.allow_cross_language_generation,
|
||||
vault_reproject_interval=config.vault_reproject_interval,
|
||||
use_salience=config.use_salience,
|
||||
salience_top_k=config.salience_top_k,
|
||||
inhibition_threshold=config.inhibition_threshold,
|
||||
)
|
||||
else:
|
||||
resolved_config = config
|
||||
|
|
@ -74,7 +80,11 @@ class ChatRuntime:
|
|||
|
||||
manifold = manifolds[0] if len(pack_ids) == 1 else load_mounted_packs(pack_ids)
|
||||
self._manifests = tuple(manifests)
|
||||
self._context = SessionContext(manifold, persona=PersonaMotor.identity())
|
||||
self._context = SessionContext(
|
||||
manifold,
|
||||
persona=PersonaMotor.identity(),
|
||||
vault_reproject_interval=resolved_config.vault_reproject_interval,
|
||||
)
|
||||
self._frame_registry = FrameRegistry.from_pack(
|
||||
resolved_config.frame_pack,
|
||||
self._context.vocab,
|
||||
|
|
@ -176,6 +186,9 @@ class ChatRuntime:
|
|||
recall_top_k=3 if self.config.allow_cross_language_recall else 0,
|
||||
output_lang=self.config.output_language,
|
||||
allow_cross_language_generation=self.config.allow_cross_language_generation,
|
||||
use_salience=self.config.use_salience,
|
||||
salience_top_k=self.config.salience_top_k,
|
||||
inhibition_threshold=self.config.inhibition_threshold,
|
||||
)
|
||||
self._context.state = result.final_state
|
||||
self._context.vault.store(
|
||||
|
|
@ -194,6 +207,8 @@ class ChatRuntime:
|
|||
output_language=self.config.output_language,
|
||||
frame_pack=self.config.frame_pack,
|
||||
walk_surface=walk_surface,
|
||||
salience_top_k=result.salience_top_k,
|
||||
candidates_used=result.candidates_used,
|
||||
)
|
||||
|
||||
def respond(self, text: str, max_tokens: int | None = None) -> str:
|
||||
|
|
|
|||
19
core/cli.py
19
core/cli.py
|
|
@ -58,6 +58,10 @@ def _runtime_config_from_args(args: argparse.Namespace):
|
|||
max_tokens=args.max_tokens,
|
||||
allow_cross_language_recall=not args.no_cross_language_recall,
|
||||
allow_cross_language_generation=args.allow_cross_language_generation,
|
||||
vault_reproject_interval=args.vault_reproject_interval,
|
||||
use_salience=not args.no_salience,
|
||||
salience_top_k=args.salience_top_k,
|
||||
inhibition_threshold=args.inhibition_threshold,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -135,6 +139,7 @@ def _runtime_for_trace(args: argparse.Namespace):
|
|||
def _trace_payload(text: str, resp: Any, runtime: Any) -> dict[str, Any]:
|
||||
proposition = resp.proposition
|
||||
articulation = resp.articulation
|
||||
vault = runtime.session.vault
|
||||
payload: dict[str, Any] = {
|
||||
"input": text,
|
||||
"surface": resp.surface,
|
||||
|
|
@ -143,6 +148,8 @@ def _trace_payload(text: str, resp: Any, runtime: Any) -> dict[str, Any]:
|
|||
"frame_pack": resp.frame_pack,
|
||||
"dialogue_role": str(resp.dialogue_role),
|
||||
"versor_condition": float(resp.versor_condition),
|
||||
"salience_top_k": resp.salience_top_k,
|
||||
"candidates_used": resp.candidates_used,
|
||||
"articulation": {
|
||||
"surface": articulation.surface,
|
||||
"frame_id": articulation.frame_id,
|
||||
|
|
@ -159,7 +166,9 @@ def _trace_payload(text: str, resp: Any, runtime: Any) -> dict[str, Any]:
|
|||
"object": proposition.object_,
|
||||
"relation_norm": proposition.relation_norm,
|
||||
},
|
||||
"vault_entries": len(runtime.session.vault),
|
||||
"vault_entries": len(vault),
|
||||
"vault_reproject_every": vault.reproject_interval,
|
||||
"vault_store_count": vault.store_count,
|
||||
"oov_grounded": list(getattr(runtime.session.vocab, "unknown_token_log", [])),
|
||||
}
|
||||
return payload
|
||||
|
|
@ -171,6 +180,8 @@ def _print_trace(payload: dict[str, Any]) -> None:
|
|||
print(f"raw_walk : {payload['walk_surface']}")
|
||||
print(f"output_language: {payload['output_language']}")
|
||||
print(f"frame_pack : {payload['frame_pack']}")
|
||||
print(f"salience_top_k : {payload['salience_top_k']}")
|
||||
print(f"candidates_used: {payload['candidates_used']}")
|
||||
print(f"dialogue_role : {payload['dialogue_role']}")
|
||||
print(f"versor_cond : {payload['versor_condition']:.2e}")
|
||||
articulation = payload["articulation"]
|
||||
|
|
@ -188,6 +199,8 @@ def _print_trace(payload: dict[str, Any]) -> None:
|
|||
print(f" object : {proposition['object']!r}")
|
||||
print(f" relation_norm: {proposition['relation_norm']:.4f}")
|
||||
print(f"vault_entries : {payload['vault_entries']}")
|
||||
print(f"vault_reproject_every: {payload['vault_reproject_every']}")
|
||||
print(f"vault_store_count : {payload['vault_store_count']}")
|
||||
oov_entries = payload["oov_grounded"]
|
||||
if oov_entries:
|
||||
print(f"oov_grounded : {len(oov_entries)} token(s)")
|
||||
|
|
@ -361,6 +374,10 @@ def _add_runtime_policy_args(parser: argparse.ArgumentParser) -> None:
|
|||
parser.add_argument("--output-language", default="en", help="target output language code; default: en")
|
||||
parser.add_argument("--frame-pack", help="frame pack to use; defaults to output language")
|
||||
parser.add_argument("--max-tokens", type=int, default=32, help="maximum generated tokens; default: 32")
|
||||
parser.add_argument("--vault-reproject-interval", type=int, default=20, help="vault null-cone reprojection cadence; default: 20 stores")
|
||||
parser.add_argument("--salience-top-k", type=int, default=16, help="salience candidate budget; default: 16")
|
||||
parser.add_argument("--inhibition-threshold", type=float, default=0.3, help="attention inhibition threshold; default: 0.3")
|
||||
parser.add_argument("--no-salience", action="store_true", help="disable salience attention and use full-manifold generation")
|
||||
parser.add_argument(
|
||||
"--allow-cross-language-generation",
|
||||
action="store_true",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ class RuntimeConfig:
|
|||
max_tokens: int = 32
|
||||
allow_cross_language_recall: bool = True
|
||||
allow_cross_language_generation: bool = False
|
||||
vault_reproject_interval: int = 20
|
||||
use_salience: bool = True
|
||||
salience_top_k: int = 16
|
||||
inhibition_threshold: float = 0.3
|
||||
|
||||
|
||||
DEFAULT_CONFIG = RuntimeConfig()
|
||||
|
|
|
|||
43
generate/attention.py
Normal file
43
generate/attention.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
|
||||
from generate.salience import SalienceMap
|
||||
from vocab.manifold import VocabManifold
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AttentionPlan:
|
||||
allowed_indices: np.ndarray
|
||||
salience_map: SalienceMap
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
object.__setattr__(self, "allowed_indices", np.asarray(self.allowed_indices, dtype=np.int64).copy())
|
||||
|
||||
|
||||
class AttentionOperator:
|
||||
"""
|
||||
Convert SalienceMap to AttentionPlan by applying budget and inhibition.
|
||||
|
||||
Inhibition excludes indices whose score is below max_score * threshold,
|
||||
removing the weak long-tail of manifold points before generation walks.
|
||||
"""
|
||||
|
||||
def __init__(self, inhibition_threshold: float = 0.3) -> None:
|
||||
if inhibition_threshold < 0.0:
|
||||
raise ValueError("inhibition_threshold must be non-negative")
|
||||
self.inhibition_threshold = float(inhibition_threshold)
|
||||
|
||||
def plan(self, salience: SalienceMap, vocab: VocabManifold) -> AttentionPlan:
|
||||
if len(salience.indices) == 0:
|
||||
return AttentionPlan(allowed_indices=np.asarray([], dtype=np.int64), salience_map=salience)
|
||||
max_score = float(salience.scores[0])
|
||||
threshold = max_score * self.inhibition_threshold
|
||||
mask = salience.scores >= threshold
|
||||
allowed = salience.indices[mask]
|
||||
if len(allowed) == 0:
|
||||
allowed = salience.indices[:1]
|
||||
allowed = allowed[: min(len(allowed), salience.budget, len(vocab))]
|
||||
return AttentionPlan(allowed_indices=allowed, salience_map=salience)
|
||||
|
|
@ -14,7 +14,7 @@ Contracts:
|
|||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from field.state import FieldState
|
||||
|
||||
|
||||
|
|
@ -23,6 +23,8 @@ class GenerationResult:
|
|||
tokens: tuple # decoded token sequence, immutable
|
||||
final_state: FieldState
|
||||
trajectory: tuple | None = None # (FieldState, ...) or None
|
||||
salience_top_k: int | None = None
|
||||
candidates_used: int | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# Coerce list inputs to tuple for immutability.
|
||||
|
|
|
|||
52
generate/salience.py
Normal file
52
generate/salience.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
|
||||
from algebra.backend import cga_inner
|
||||
from field.state import FieldState
|
||||
from vocab.manifold import VocabManifold
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SalienceMap:
|
||||
indices: np.ndarray
|
||||
scores: np.ndarray
|
||||
budget: int
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
object.__setattr__(self, "indices", np.asarray(self.indices, dtype=np.int64).copy())
|
||||
object.__setattr__(self, "scores", np.asarray(self.scores, dtype=np.float32).copy())
|
||||
object.__setattr__(self, "budget", int(self.budget))
|
||||
|
||||
|
||||
class SalienceOperator:
|
||||
"""
|
||||
Compute geometric salience of manifold points relative to current FieldState.
|
||||
|
||||
Salience is field-relative CGA activation:
|
||||
salience(v_i) = |cga_inner(F, v_i)| / (||F|| * ||v_i||)
|
||||
|
||||
No learned weights. No softmax. Pure geometry routed through algebra.backend,
|
||||
which uses core_rs when active.
|
||||
"""
|
||||
|
||||
def compute(self, field: FieldState, vocab: VocabManifold, top_k: int = 16) -> SalienceMap:
|
||||
if top_k <= 0:
|
||||
return SalienceMap(indices=np.asarray([], dtype=np.int64), scores=np.asarray([], dtype=np.float32), budget=0)
|
||||
if len(vocab) == 0:
|
||||
return SalienceMap(indices=np.asarray([], dtype=np.int64), scores=np.asarray([], dtype=np.float32), budget=0)
|
||||
|
||||
query = np.asarray(field.F, dtype=np.float32)
|
||||
query_norm = max(float(np.linalg.norm(query)), 1e-8)
|
||||
scores: list[float] = []
|
||||
for idx in range(len(vocab)):
|
||||
v = vocab.get_versor_at(idx)
|
||||
denom = query_norm * max(float(np.linalg.norm(v)), 1e-8)
|
||||
scores.append(abs(float(cga_inner(query, v))) / denom)
|
||||
|
||||
scores_arr = np.asarray(scores, dtype=np.float32)
|
||||
k = min(int(top_k), len(vocab))
|
||||
order = np.argsort(-scores_arr, kind="stable")[:k]
|
||||
return SalienceMap(indices=order.astype(np.int64), scores=scores_arr[order], budget=k)
|
||||
|
|
@ -23,7 +23,9 @@ import numpy as np
|
|||
from field.state import FieldState
|
||||
from field.propagate import propagate_step
|
||||
from algebra.rotor import word_transition_rotor
|
||||
from generate.attention import AttentionOperator
|
||||
from generate.result import GenerationResult
|
||||
from generate.salience import SalienceOperator
|
||||
|
||||
_RECENT_WINDOW = 3
|
||||
_STOP_TOKENS = frozenset({"it", "to", "word"})
|
||||
|
|
@ -60,6 +62,10 @@ def _nearest_next(
|
|||
Recent-node exclusion reduces two- and three-token attractor cycles.
|
||||
Stop-node exclusion keeps function-word wells from dominating when more
|
||||
informative neighbors are available.
|
||||
|
||||
If attention/language filtering leaves only the current node available,
|
||||
the final fallback deliberately permits that singleton candidate instead
|
||||
of crashing. That keeps inhibition fail-closed to the attended region.
|
||||
"""
|
||||
if len(vocab) <= 1:
|
||||
return vocab.nearest(F_voiced, candidate_indices=candidate_indices)
|
||||
|
|
@ -82,7 +88,7 @@ def _nearest_next(
|
|||
)
|
||||
except ValueError:
|
||||
continue
|
||||
return vocab.nearest(F_voiced, exclude_idx=current_node, candidate_indices=candidate_indices)
|
||||
return vocab.nearest(F_voiced, candidate_indices=candidate_indices)
|
||||
|
||||
|
||||
def _voiced_state(state: FieldState, persona) -> FieldState:
|
||||
|
|
@ -131,6 +137,31 @@ def _candidate_indices_for_language(vocab, output_lang: str | None) -> np.ndarra
|
|||
return indices
|
||||
|
||||
|
||||
def _intersect_candidates(a: np.ndarray | None, b: np.ndarray | None) -> np.ndarray | None:
|
||||
if a is None:
|
||||
return b
|
||||
if b is None:
|
||||
return a
|
||||
if len(a) == 0 or len(b) == 0:
|
||||
return np.asarray([], dtype=np.int64)
|
||||
b_set = {int(idx) for idx in b}
|
||||
return np.asarray([int(idx) for idx in a if int(idx) in b_set], dtype=np.int64)
|
||||
|
||||
|
||||
def _attention_candidates(
|
||||
state: FieldState,
|
||||
vocab,
|
||||
use_salience: bool,
|
||||
salience_top_k: int,
|
||||
inhibition_threshold: float,
|
||||
) -> tuple[np.ndarray | None, int | None, int | None]:
|
||||
if not use_salience:
|
||||
return None, None, None
|
||||
salience = SalienceOperator().compute(state, vocab, top_k=salience_top_k)
|
||||
attention = AttentionOperator(inhibition_threshold).plan(salience, vocab)
|
||||
return attention.allowed_indices, salience.budget, len(attention.allowed_indices)
|
||||
|
||||
|
||||
def generate(
|
||||
state: FieldState,
|
||||
vocab,
|
||||
|
|
@ -141,6 +172,9 @@ def generate(
|
|||
recall_top_k: int = 3,
|
||||
output_lang: str | None = None,
|
||||
allow_cross_language_generation: bool = True,
|
||||
use_salience: bool = False,
|
||||
salience_top_k: int = 16,
|
||||
inhibition_threshold: float = 0.3,
|
||||
) -> GenerationResult:
|
||||
"""
|
||||
Generate a token sequence from an initial FieldState.
|
||||
|
|
@ -156,20 +190,34 @@ def generate(
|
|||
7. Advance node pointer
|
||||
|
||||
Returns:
|
||||
GenerationResult with tokens, final_state, and optional trajectory.
|
||||
GenerationResult with tokens, final_state, optional trajectory,
|
||||
and salience telemetry when attention is enabled.
|
||||
"""
|
||||
tokens = []
|
||||
trajectory = [] if record_trajectory else None
|
||||
current = state
|
||||
recent_nodes = deque([state.node], maxlen=_RECENT_WINDOW)
|
||||
candidate_indices = None if allow_cross_language_generation else _candidate_indices_for_language(vocab, output_lang)
|
||||
language_candidates = None if allow_cross_language_generation else _candidate_indices_for_language(vocab, output_lang)
|
||||
salience_candidates, salience_budget, candidates_used = _attention_candidates(
|
||||
state,
|
||||
vocab,
|
||||
use_salience=use_salience,
|
||||
salience_top_k=salience_top_k,
|
||||
inhibition_threshold=inhibition_threshold,
|
||||
)
|
||||
candidate_indices = _intersect_candidates(language_candidates, salience_candidates)
|
||||
if candidate_indices is not None and len(candidate_indices) == 0:
|
||||
candidate_indices = language_candidates if language_candidates is not None else salience_candidates
|
||||
candidates_used = None if candidate_indices is None else len(candidate_indices)
|
||||
|
||||
stop_nodes = frozenset(
|
||||
vocab.index_of(token)
|
||||
for token in _STOP_TOKENS
|
||||
if token in {vocab.get_word_at(i) for i in range(len(vocab))}
|
||||
)
|
||||
|
||||
for _ in range(max_tokens):
|
||||
token_budget = min(max_tokens, int(candidates_used)) if candidates_used is not None else max_tokens
|
||||
for _ in range(token_budget):
|
||||
current = _recall_state(_voiced_state(current, persona), vault, recall_top_k)
|
||||
word, word_idx = _nearest_next(
|
||||
vocab,
|
||||
|
|
@ -196,6 +244,8 @@ def generate(
|
|||
tokens=tokens,
|
||||
final_state=current,
|
||||
trajectory=trajectory,
|
||||
salience_top_k=salience_budget,
|
||||
candidates_used=candidates_used,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@ from vault.store import VaultStore
|
|||
|
||||
|
||||
class SessionContext:
|
||||
def __init__(self, vocab, persona=None, vault=None):
|
||||
def __init__(self, vocab, persona=None, vault=None, vault_reproject_interval: int = 100):
|
||||
self.vocab = vocab
|
||||
self.persona = persona or PersonaMotor.identity()
|
||||
self.vault = vault or VaultStore()
|
||||
self.vault = vault or VaultStore(reproject_interval=vault_reproject_interval)
|
||||
self.state: FieldState | None = None
|
||||
self.turn: int = 0
|
||||
self.dialogue_history: list[DialogueTurn] = []
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ def test_trace_help_exits_without_runtime_import(capsys: pytest.CaptureFixture[s
|
|||
assert "--pack" in out
|
||||
assert "--output-language" in out
|
||||
assert "--frame-pack" in out
|
||||
assert "--salience-top-k" in out
|
||||
assert "--no-salience" in out
|
||||
assert "--json" in out
|
||||
|
||||
|
||||
|
|
@ -76,11 +78,15 @@ def test_doctor_rust_reports_backend_state(capsys: pytest.CaptureFixture[str]) -
|
|||
|
||||
|
||||
def test_trace_formats_real_runtime_payload(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
assert main(["trace", "--pack", "en_minimal_v1", "word", "beginning", "truth"]) == 0
|
||||
assert main(["trace", "--pack", "en_minimal_v1", "--salience-top-k", "8", "word", "beginning", "truth"]) == 0
|
||||
out = capsys.readouterr().out
|
||||
assert "input : word beginning truth" in out
|
||||
assert "output_language: en" in out
|
||||
assert "frame_pack : en" in out
|
||||
assert "salience_top_k : 8" in out
|
||||
assert "candidates_used:" in out
|
||||
assert "vault_reproject_every:" in out
|
||||
assert "vault_store_count" in out
|
||||
assert "articulation" in out
|
||||
assert "raw_walk" in out
|
||||
assert "proposition" in out
|
||||
|
|
@ -89,13 +95,24 @@ def test_trace_formats_real_runtime_payload(capsys: pytest.CaptureFixture[str])
|
|||
|
||||
|
||||
def test_trace_json_formats_real_runtime_payload(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
assert main(["trace", "--pack", "en_minimal_v1", "--json", "word", "beginning", "truth"]) == 0
|
||||
assert main(["trace", "--pack", "en_minimal_v1", "--json", "--salience-top-k", "8", "word", "beginning", "truth"]) == 0
|
||||
out = capsys.readouterr().out
|
||||
assert '"input": "word beginning truth"' in out
|
||||
assert '"output_language": "en"' in out
|
||||
assert '"frame_pack": "en"' in out
|
||||
assert '"salience_top_k": 8' in out
|
||||
assert '"candidates_used"' in out
|
||||
assert '"vault_reproject_every"' in out
|
||||
assert '"vault_store_count"' in out
|
||||
assert '"articulation"' in out
|
||||
assert '"walk_surface"' in out
|
||||
assert '"proposition"' in out
|
||||
assert '"subject"' in out
|
||||
assert '"predicate"' in out
|
||||
|
||||
|
||||
def test_trace_json_no_salience_has_null_salience_telemetry(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
assert main(["trace", "--pack", "en_minimal_v1", "--json", "--no-salience", "word", "beginning", "truth"]) == 0
|
||||
out = capsys.readouterr().out
|
||||
assert '"salience_top_k": null' in out
|
||||
assert '"candidates_used": null' in out
|
||||
|
|
|
|||
66
tests/test_salience.py
Normal file
66
tests/test_salience.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from chat.runtime import ChatRuntime
|
||||
from core.config import RuntimeConfig
|
||||
from generate.attention import AttentionOperator
|
||||
from generate.salience import SalienceOperator
|
||||
|
||||
|
||||
def test_salience_map_has_top_k_entries_and_descending_scores() -> None:
|
||||
runtime = ChatRuntime(config=RuntimeConfig(output_language="en", frame_pack="en"))
|
||||
field = runtime.session.ingest(runtime.tokenize("word beginning truth"))
|
||||
salience = SalienceOperator().compute(field, runtime.session.vocab, top_k=8)
|
||||
|
||||
assert len(salience.indices) == 8
|
||||
assert len(salience.scores) == 8
|
||||
assert salience.budget == 8
|
||||
assert np.all(salience.scores[:-1] >= salience.scores[1:])
|
||||
|
||||
|
||||
def test_attention_plan_inhibits_salience_tail() -> None:
|
||||
runtime = ChatRuntime(config=RuntimeConfig(output_language="en", frame_pack="en"))
|
||||
field = runtime.session.ingest(runtime.tokenize("word beginning truth"))
|
||||
salience = SalienceOperator().compute(field, runtime.session.vocab, top_k=16)
|
||||
plan = AttentionOperator(inhibition_threshold=0.9).plan(salience, runtime.session.vocab)
|
||||
|
||||
assert 0 < len(plan.allowed_indices) <= len(salience.indices)
|
||||
assert set(plan.allowed_indices).issubset(set(salience.indices))
|
||||
assert len(plan.allowed_indices) < len(salience.indices)
|
||||
|
||||
|
||||
def test_salience_enabled_bounds_generation_walk() -> None:
|
||||
config = RuntimeConfig(output_language="en", frame_pack="en", salience_top_k=8)
|
||||
runtime = ChatRuntime(config=config)
|
||||
response = runtime.chat("word beginning truth")
|
||||
|
||||
assert response.salience_top_k == 8
|
||||
assert response.candidates_used is not None
|
||||
assert 0 < response.candidates_used <= 8
|
||||
assert len(response.walk_surface.split()) <= response.candidates_used
|
||||
|
||||
|
||||
def test_salience_disabled_preserves_full_generation_budget_telemetry() -> None:
|
||||
config = RuntimeConfig(output_language="en", frame_pack="en", use_salience=False, max_tokens=12)
|
||||
runtime = ChatRuntime(config=config)
|
||||
response = runtime.chat("word beginning truth")
|
||||
|
||||
assert response.salience_top_k is None
|
||||
assert response.candidates_used is None
|
||||
assert len(response.walk_surface.split()) <= 12
|
||||
|
||||
|
||||
def test_salience_changes_candidate_budget_without_changing_response_contract() -> None:
|
||||
enabled = ChatRuntime(config=RuntimeConfig(output_language="en", frame_pack="en", salience_top_k=8))
|
||||
disabled = ChatRuntime(config=RuntimeConfig(output_language="en", frame_pack="en", use_salience=False, max_tokens=8))
|
||||
|
||||
salience_response = enabled.chat("word beginning truth")
|
||||
full_response = disabled.chat("word beginning truth")
|
||||
|
||||
assert salience_response.candidates_used is not None
|
||||
assert full_response.candidates_used is None
|
||||
assert salience_response.surface
|
||||
assert full_response.surface
|
||||
assert enabled.session.state is not None
|
||||
assert disabled.session.state is not None
|
||||
16
tests/test_vault_config.py
Normal file
16
tests/test_vault_config.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from chat.runtime import ChatRuntime
|
||||
from core.config import RuntimeConfig
|
||||
|
||||
|
||||
def test_runtime_config_controls_vault_reproject_interval_and_store_count() -> None:
|
||||
runtime = ChatRuntime(config=RuntimeConfig(vault_reproject_interval=5, output_language="en", frame_pack="en"))
|
||||
|
||||
turns = 3
|
||||
for text in ("word beginning truth", "light truth word", "begin thought word"):
|
||||
runtime.chat(text)
|
||||
|
||||
assert runtime.session.vault.reproject_interval == 5
|
||||
assert runtime.session.vault.store_count == turns * 3
|
||||
assert len(runtime.session.vault) == turns * 3
|
||||
|
|
@ -31,7 +31,7 @@ class VaultStore:
|
|||
self._versors.append(np.asarray(F, dtype=np.float32).copy())
|
||||
self._metadata.append(metadata or {})
|
||||
self._store_count += 1
|
||||
if self._store_count % self._reproject_interval == 0:
|
||||
if self._reproject_interval > 0 and self._store_count % self._reproject_interval == 0:
|
||||
self.reproject()
|
||||
return len(self._versors) - 1
|
||||
|
||||
|
|
@ -67,5 +67,15 @@ class VaultStore:
|
|||
"""
|
||||
self._versors = [null_project(v) for v in self._versors]
|
||||
|
||||
@property
|
||||
def reproject_interval(self) -> int:
|
||||
"""Return the configured auto-reprojection cadence in store operations."""
|
||||
return self._reproject_interval
|
||||
|
||||
@property
|
||||
def store_count(self) -> int:
|
||||
"""Return how many store() operations have occurred in this vault."""
|
||||
return self._store_count
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._versors)
|
||||
|
|
|
|||
Loading…
Reference in a new issue