Tighten session node tracking and generation selection
This commit is contained in:
parent
4c3004c73a
commit
d997b88d32
6 changed files with 138 additions and 19 deletions
|
|
@ -4,7 +4,6 @@ import re
|
|||
|
||||
from language_packs import OOVPolicy, load_pack, load_pack_entries
|
||||
from persona.motor import PersonaMotor
|
||||
from field.state import FieldState
|
||||
from session.context import SessionContext
|
||||
|
||||
_TOKEN_RE = re.compile(r"\w+", re.UNICODE)
|
||||
|
|
@ -58,13 +57,6 @@ class ChatRuntime:
|
|||
if not filtered:
|
||||
return ""
|
||||
self._context.ingest(filtered)
|
||||
node_idx = self._context.vocab.index_of(filtered[0])
|
||||
self._context.state = FieldState(
|
||||
F=self._context.state.F,
|
||||
node=node_idx,
|
||||
step=self._context.state.step,
|
||||
holonomy=self._context.state.holonomy,
|
||||
)
|
||||
result = self._context.respond(max_tokens=max_tokens)
|
||||
guarded = self._syntactic_guard(result.tokens)
|
||||
return " ".join(guarded)
|
||||
|
|
|
|||
|
|
@ -16,22 +16,50 @@ F is always on the manifold. nearest() is exact.
|
|||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from collections import deque
|
||||
|
||||
from field.state import FieldState
|
||||
from field.propagate import propagate_step
|
||||
from algebra.rotor import word_transition_rotor
|
||||
from generate.result import GenerationResult
|
||||
|
||||
_RECENT_WINDOW = 3
|
||||
_STOP_TOKENS = frozenset({"it", "to", "word"})
|
||||
|
||||
def _nearest_next(vocab, F_voiced, current_node: int) -> tuple[str, int]:
|
||||
|
||||
def _nearest_next(
|
||||
vocab,
|
||||
F_voiced,
|
||||
current_node: int,
|
||||
recent_nodes: tuple[int, ...] = (),
|
||||
stop_nodes: frozenset[int] = frozenset(),
|
||||
) -> tuple[str, int]:
|
||||
"""
|
||||
Select the nearest non-current vocabulary point when possible.
|
||||
Select the nearest vocabulary point while avoiding short loops.
|
||||
|
||||
Allowing the current node to win makes V = transition(A, A), which is an
|
||||
identity-like transition and can stall generation forever on one token.
|
||||
VocabManifold already exposes exclude_idx for this exact seam.
|
||||
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.
|
||||
"""
|
||||
exclude_idx = current_node if len(vocab) > 1 else -1
|
||||
return vocab.nearest(F_voiced, exclude_idx=exclude_idx)
|
||||
if len(vocab) <= 1:
|
||||
return vocab.nearest(F_voiced)
|
||||
|
||||
recent = set(recent_nodes)
|
||||
stop = set(stop_nodes)
|
||||
fallback_orders = (
|
||||
recent | stop,
|
||||
stop,
|
||||
recent,
|
||||
set(),
|
||||
)
|
||||
for extra in fallback_orders:
|
||||
try:
|
||||
return vocab.nearest(F_voiced, exclude_idx=current_node, exclude_indices=extra)
|
||||
except ValueError:
|
||||
continue
|
||||
return vocab.nearest(F_voiced, exclude_idx=current_node)
|
||||
|
||||
|
||||
def generate(
|
||||
|
|
@ -59,10 +87,22 @@ def generate(
|
|||
tokens = []
|
||||
trajectory = [] if record_trajectory else None
|
||||
current = state
|
||||
recent_nodes = deque([state.node], maxlen=_RECENT_WINDOW)
|
||||
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):
|
||||
F_voiced = persona.apply(current.F)
|
||||
word, word_idx = _nearest_next(vocab, F_voiced, current.node)
|
||||
word, word_idx = _nearest_next(
|
||||
vocab,
|
||||
F_voiced,
|
||||
current.node,
|
||||
recent_nodes=tuple(recent_nodes),
|
||||
stop_nodes=stop_nodes,
|
||||
)
|
||||
tokens.append(word)
|
||||
|
||||
if record_trajectory:
|
||||
|
|
@ -74,6 +114,7 @@ def generate(
|
|||
|
||||
current = propagate_step(current, V)
|
||||
current = FieldState(F=current.F, node=word_idx, step=current.step, holonomy=current.holonomy)
|
||||
recent_nodes.append(word_idx)
|
||||
|
||||
return GenerationResult(
|
||||
tokens=tokens,
|
||||
|
|
@ -99,9 +140,21 @@ async def agenerate(
|
|||
Yields: str (one token per iteration)
|
||||
"""
|
||||
current = state
|
||||
recent_nodes = deque([state.node], maxlen=_RECENT_WINDOW)
|
||||
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):
|
||||
F_voiced = persona.apply(current.F)
|
||||
word, word_idx = _nearest_next(vocab, F_voiced, current.node)
|
||||
word, word_idx = _nearest_next(
|
||||
vocab,
|
||||
F_voiced,
|
||||
current.node,
|
||||
recent_nodes=tuple(recent_nodes),
|
||||
stop_nodes=stop_nodes,
|
||||
)
|
||||
yield word
|
||||
|
||||
A = vocab.get_versor_at(current.node)
|
||||
|
|
@ -110,3 +163,4 @@ async def agenerate(
|
|||
|
||||
current = propagate_step(current, V)
|
||||
current = FieldState(F=current.F, node=word_idx, step=current.step, holonomy=current.holonomy)
|
||||
recent_nodes.append(word_idx)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,14 @@ class SessionContext:
|
|||
|
||||
def ingest(self, tokens: list) -> FieldState:
|
||||
"""Inject a prompt. Sets self.state. Stores the user field in vault."""
|
||||
self.state = inject(tokens, self.vocab)
|
||||
state = inject(tokens, self.vocab)
|
||||
node_idx = self.vocab.index_of(tokens[0])
|
||||
self.state = FieldState(
|
||||
F=state.F,
|
||||
node=node_idx,
|
||||
step=state.step,
|
||||
holonomy=state.holonomy,
|
||||
)
|
||||
self.vault.store(self.state.F, {"turn": self.turn, "role": "user"})
|
||||
return self.state
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ def test_minimum_engine_loop_is_deterministic_and_stores_generated_state() -> No
|
|||
def test_session_context_respond_preserves_and_vaults_final_state() -> None:
|
||||
session = SessionContext(vocab=_minimal_vocab())
|
||||
initial = session.ingest(["logos", "arche"])
|
||||
assert initial.node == session.vocab.index_of("logos")
|
||||
|
||||
result = session.respond(max_tokens=3)
|
||||
|
||||
|
|
|
|||
54
tests/test_generate_stream.py
Normal file
54
tests/test_generate_stream.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from generate.stream import _nearest_next
|
||||
|
||||
|
||||
class _StubVocab:
|
||||
def __init__(self, words: list[str]):
|
||||
self._words = words
|
||||
self.calls: list[tuple[int, frozenset[int]]] = []
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._words)
|
||||
|
||||
def nearest(self, F, exclude_idx: int = -1, exclude_indices=None):
|
||||
blocked = frozenset(exclude_indices or ())
|
||||
self.calls.append((exclude_idx, blocked))
|
||||
for idx, word in enumerate(self._words):
|
||||
if idx == exclude_idx or idx in blocked:
|
||||
continue
|
||||
return word, idx
|
||||
raise ValueError("No candidate word available after exclusions.")
|
||||
|
||||
|
||||
def test_nearest_next_excludes_recent_and_stop_nodes_when_possible():
|
||||
vocab = _StubVocab(["seed", "to", "it", "meaning", "truth"])
|
||||
|
||||
word, idx = _nearest_next(
|
||||
vocab,
|
||||
F_voiced=None,
|
||||
current_node=0,
|
||||
recent_nodes=(3,),
|
||||
stop_nodes=frozenset({1, 2}),
|
||||
)
|
||||
|
||||
assert (word, idx) == ("truth", 4)
|
||||
assert vocab.calls[0] == (0, frozenset({1, 2, 3}))
|
||||
|
||||
|
||||
def test_nearest_next_relaxes_recent_window_before_stop_tokens():
|
||||
vocab = _StubVocab(["seed", "to", "truth"])
|
||||
|
||||
word, idx = _nearest_next(
|
||||
vocab,
|
||||
F_voiced=None,
|
||||
current_node=0,
|
||||
recent_nodes=(2,),
|
||||
stop_nodes=frozenset({1}),
|
||||
)
|
||||
|
||||
assert (word, idx) == ("truth", 2)
|
||||
assert vocab.calls == [
|
||||
(0, frozenset({1, 2})),
|
||||
(0, frozenset({1})),
|
||||
]
|
||||
|
|
@ -92,7 +92,12 @@ class VocabManifold:
|
|||
except ValueError:
|
||||
raise KeyError(f"Word '{word}' not in vocabulary.")
|
||||
|
||||
def nearest(self, F: np.ndarray, exclude_idx: int = -1) -> tuple[str, int]:
|
||||
def nearest(
|
||||
self,
|
||||
F: np.ndarray,
|
||||
exclude_idx: int = -1,
|
||||
exclude_indices: set[int] | frozenset[int] | None = None,
|
||||
) -> tuple[str, int]:
|
||||
"""
|
||||
Find the word whose versor is closest to F by CGA inner product.
|
||||
Returns (word, index). O(|vocab|), exact, no approximation.
|
||||
|
|
@ -100,15 +105,21 @@ class VocabManifold:
|
|||
|
||||
Hot path: cga_inner routes through algebra.backend.
|
||||
"""
|
||||
blocked = set(exclude_indices or ())
|
||||
if exclude_idx >= 0:
|
||||
blocked.add(exclude_idx)
|
||||
|
||||
best_score = -np.inf
|
||||
best_idx = 0
|
||||
best_idx = -1
|
||||
for i, v in enumerate(self._versors):
|
||||
if i == exclude_idx:
|
||||
if i in blocked:
|
||||
continue
|
||||
score = cga_inner(F, v)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_idx = i
|
||||
if best_idx < 0:
|
||||
raise ValueError("No candidate word available after exclusions.")
|
||||
return self._words[best_idx], best_idx
|
||||
|
||||
def __len__(self) -> int:
|
||||
|
|
|
|||
Loading…
Reference in a new issue