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 language_packs import OOVPolicy, load_pack, load_pack_entries
|
||||||
from persona.motor import PersonaMotor
|
from persona.motor import PersonaMotor
|
||||||
from field.state import FieldState
|
|
||||||
from session.context import SessionContext
|
from session.context import SessionContext
|
||||||
|
|
||||||
_TOKEN_RE = re.compile(r"\w+", re.UNICODE)
|
_TOKEN_RE = re.compile(r"\w+", re.UNICODE)
|
||||||
|
|
@ -58,13 +57,6 @@ class ChatRuntime:
|
||||||
if not filtered:
|
if not filtered:
|
||||||
return ""
|
return ""
|
||||||
self._context.ingest(filtered)
|
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)
|
result = self._context.respond(max_tokens=max_tokens)
|
||||||
guarded = self._syntactic_guard(result.tokens)
|
guarded = self._syntactic_guard(result.tokens)
|
||||||
return " ".join(guarded)
|
return " ".join(guarded)
|
||||||
|
|
|
||||||
|
|
@ -16,22 +16,50 @@ F is always on the manifold. nearest() is exact.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
from field.state import FieldState
|
from field.state import FieldState
|
||||||
from field.propagate import propagate_step
|
from field.propagate import propagate_step
|
||||||
from algebra.rotor import word_transition_rotor
|
from algebra.rotor import word_transition_rotor
|
||||||
from generate.result import GenerationResult
|
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
|
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.
|
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
|
if len(vocab) <= 1:
|
||||||
return vocab.nearest(F_voiced, exclude_idx=exclude_idx)
|
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(
|
def generate(
|
||||||
|
|
@ -59,10 +87,22 @@ def generate(
|
||||||
tokens = []
|
tokens = []
|
||||||
trajectory = [] if record_trajectory else None
|
trajectory = [] if record_trajectory else None
|
||||||
current = state
|
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):
|
for _ in range(max_tokens):
|
||||||
F_voiced = persona.apply(current.F)
|
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)
|
tokens.append(word)
|
||||||
|
|
||||||
if record_trajectory:
|
if record_trajectory:
|
||||||
|
|
@ -74,6 +114,7 @@ def generate(
|
||||||
|
|
||||||
current = propagate_step(current, V)
|
current = propagate_step(current, V)
|
||||||
current = FieldState(F=current.F, node=word_idx, step=current.step, holonomy=current.holonomy)
|
current = FieldState(F=current.F, node=word_idx, step=current.step, holonomy=current.holonomy)
|
||||||
|
recent_nodes.append(word_idx)
|
||||||
|
|
||||||
return GenerationResult(
|
return GenerationResult(
|
||||||
tokens=tokens,
|
tokens=tokens,
|
||||||
|
|
@ -99,9 +140,21 @@ async def agenerate(
|
||||||
Yields: str (one token per iteration)
|
Yields: str (one token per iteration)
|
||||||
"""
|
"""
|
||||||
current = state
|
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):
|
for _ in range(max_tokens):
|
||||||
F_voiced = persona.apply(current.F)
|
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
|
yield word
|
||||||
|
|
||||||
A = vocab.get_versor_at(current.node)
|
A = vocab.get_versor_at(current.node)
|
||||||
|
|
@ -110,3 +163,4 @@ async def agenerate(
|
||||||
|
|
||||||
current = propagate_step(current, V)
|
current = propagate_step(current, V)
|
||||||
current = FieldState(F=current.F, node=word_idx, step=current.step, holonomy=current.holonomy)
|
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:
|
def ingest(self, tokens: list) -> FieldState:
|
||||||
"""Inject a prompt. Sets self.state. Stores the user field in vault."""
|
"""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"})
|
self.vault.store(self.state.F, {"turn": self.turn, "role": "user"})
|
||||||
return self.state
|
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:
|
def test_session_context_respond_preserves_and_vaults_final_state() -> None:
|
||||||
session = SessionContext(vocab=_minimal_vocab())
|
session = SessionContext(vocab=_minimal_vocab())
|
||||||
initial = session.ingest(["logos", "arche"])
|
initial = session.ingest(["logos", "arche"])
|
||||||
|
assert initial.node == session.vocab.index_of("logos")
|
||||||
|
|
||||||
result = session.respond(max_tokens=3)
|
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:
|
except ValueError:
|
||||||
raise KeyError(f"Word '{word}' not in vocabulary.")
|
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.
|
Find the word whose versor is closest to F by CGA inner product.
|
||||||
Returns (word, index). O(|vocab|), exact, no approximation.
|
Returns (word, index). O(|vocab|), exact, no approximation.
|
||||||
|
|
@ -100,15 +105,21 @@ class VocabManifold:
|
||||||
|
|
||||||
Hot path: cga_inner routes through algebra.backend.
|
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_score = -np.inf
|
||||||
best_idx = 0
|
best_idx = -1
|
||||||
for i, v in enumerate(self._versors):
|
for i, v in enumerate(self._versors):
|
||||||
if i == exclude_idx:
|
if i in blocked:
|
||||||
continue
|
continue
|
||||||
score = cga_inner(F, v)
|
score = cga_inner(F, v)
|
||||||
if score > best_score:
|
if score > best_score:
|
||||||
best_score = score
|
best_score = score
|
||||||
best_idx = i
|
best_idx = i
|
||||||
|
if best_idx < 0:
|
||||||
|
raise ValueError("No candidate word available after exclusions.")
|
||||||
return self._words[best_idx], best_idx
|
return self._words[best_idx], best_idx
|
||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue