Add session coherence across turns
This commit is contained in:
parent
531acfd40b
commit
ed04fc5b15
3 changed files with 130 additions and 20 deletions
|
|
@ -77,24 +77,59 @@ def _nearest_next(
|
|||
return vocab.nearest(F_voiced, exclude_idx=current_node)
|
||||
|
||||
|
||||
def _voiced_state(state: FieldState, persona) -> FieldState:
|
||||
"""Compose the session persona motor into the live field path."""
|
||||
return FieldState(
|
||||
F=persona.apply(state.F),
|
||||
node=state.node,
|
||||
step=state.step,
|
||||
holonomy=state.holonomy,
|
||||
)
|
||||
|
||||
|
||||
def _recall_state(state: FieldState, vault, top_k: int) -> FieldState:
|
||||
"""
|
||||
Feed exact vault recall back into the field as sequential operators.
|
||||
|
||||
Recall returns stored versors ranked by the vault's exact metric. Each hit
|
||||
is treated as an additional operator in the propagation path.
|
||||
"""
|
||||
if vault is None or top_k <= 0:
|
||||
return state
|
||||
|
||||
current = state
|
||||
for hit in vault.recall(current.F, top_k=top_k):
|
||||
current = propagate_step(current, hit["versor"])
|
||||
current = FieldState(
|
||||
F=current.F,
|
||||
node=state.node,
|
||||
step=current.step,
|
||||
holonomy=state.holonomy,
|
||||
)
|
||||
return current
|
||||
|
||||
|
||||
def generate(
|
||||
state: FieldState,
|
||||
vocab,
|
||||
persona,
|
||||
max_tokens: int = 128,
|
||||
record_trajectory: bool = False,
|
||||
vault=None,
|
||||
recall_top_k: int = 3,
|
||||
) -> GenerationResult:
|
||||
"""
|
||||
Generate a token sequence from an initial FieldState.
|
||||
|
||||
Loop:
|
||||
1. Apply persona motor to current field
|
||||
2. Find nearest non-current vocab node via CGA inner product
|
||||
3. Emit token
|
||||
4. Build transition rotor: V = word_transition_rotor(A, B)
|
||||
1. Compose the persistent persona motor into the current field
|
||||
2. Propagate exact vault recall hits into the current field
|
||||
3. Find nearest non-current vocab node via CGA inner product
|
||||
4. Emit token
|
||||
5. Build transition rotor: V = word_transition_rotor(A, B)
|
||||
where A = versor at current node, B = versor at nearest node
|
||||
5. Propagate: F <- versor_apply(V, F)
|
||||
6. Advance node pointer
|
||||
6. Propagate: F <- versor_apply(V, F)
|
||||
7. Advance node pointer
|
||||
|
||||
Returns:
|
||||
GenerationResult with tokens, final_state, and optional trajectory.
|
||||
|
|
@ -110,10 +145,10 @@ def generate(
|
|||
)
|
||||
|
||||
for _ in range(max_tokens):
|
||||
F_voiced = persona.apply(current.F)
|
||||
current = _recall_state(_voiced_state(current, persona), vault, recall_top_k)
|
||||
word, word_idx = _nearest_next(
|
||||
vocab,
|
||||
F_voiced,
|
||||
current.F,
|
||||
current.node,
|
||||
recent_nodes=tuple(recent_nodes),
|
||||
stop_nodes=stop_nodes,
|
||||
|
|
@ -162,10 +197,10 @@ async def agenerate(
|
|||
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)
|
||||
current = _voiced_state(current, persona)
|
||||
word, word_idx = _nearest_next(
|
||||
vocab,
|
||||
F_voiced,
|
||||
current.F,
|
||||
current.node,
|
||||
recent_nodes=tuple(recent_nodes),
|
||||
stop_nodes=stop_nodes,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from persona.motor import PersonaMotor
|
|||
from ingest.gate import inject
|
||||
from generate.stream import generate
|
||||
from generate.result import GenerationResult
|
||||
from algebra.backend import versor_apply
|
||||
|
||||
|
||||
class SessionContext:
|
||||
|
|
@ -29,15 +30,23 @@ class SessionContext:
|
|||
self.turn: int = 0
|
||||
|
||||
def ingest(self, tokens: list) -> FieldState:
|
||||
"""Inject a prompt. Sets self.state. Stores the user field in vault."""
|
||||
state = inject(tokens, self.vocab)
|
||||
"""Inject a prompt into the running field. Stores the user field in vault."""
|
||||
injected = 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,
|
||||
)
|
||||
if self.state is None:
|
||||
self.state = FieldState(
|
||||
F=injected.F,
|
||||
node=node_idx,
|
||||
step=injected.step,
|
||||
holonomy=injected.holonomy,
|
||||
)
|
||||
else:
|
||||
self.state = FieldState(
|
||||
F=versor_apply(injected.F, self.state.F),
|
||||
node=node_idx,
|
||||
step=self.state.step + 1,
|
||||
holonomy=injected.holonomy,
|
||||
)
|
||||
self.vault.store(self.state.F, {"turn": self.turn, "role": "user"})
|
||||
return self.state
|
||||
|
||||
|
|
@ -49,7 +58,7 @@ class SessionContext:
|
|||
GenerationResult carrying emitted tokens and final_state.
|
||||
"""
|
||||
assert self.state is not None, "Call ingest() before respond()."
|
||||
result = generate(self.state, self.vocab, self.persona, max_tokens)
|
||||
result = generate(self.state, self.vocab, self.persona, max_tokens, vault=self.vault)
|
||||
self.state = result.final_state
|
||||
self.vault.store(result.final_state.F, {"turn": self.turn, "role": "assistant"})
|
||||
self.turn += 1
|
||||
|
|
@ -64,7 +73,7 @@ class SessionContext:
|
|||
yielding the surface tokens.
|
||||
"""
|
||||
assert self.state is not None, "Call ingest() before arespond()."
|
||||
result = generate(self.state, self.vocab, self.persona, max_tokens)
|
||||
result = generate(self.state, self.vocab, self.persona, max_tokens, vault=self.vault)
|
||||
for token in result.tokens:
|
||||
yield token
|
||||
self.state = result.final_state
|
||||
|
|
|
|||
66
tests/test_session_coherence.py
Normal file
66
tests/test_session_coherence.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from algebra.backend import cga_inner
|
||||
from algebra.versor import unitize_versor
|
||||
from session.context import SessionContext
|
||||
from vocab.manifold import VocabManifold
|
||||
|
||||
|
||||
def _positive_unit_reflector(seed: int) -> np.ndarray:
|
||||
rng = np.random.default_rng(seed)
|
||||
vec4 = rng.standard_normal(4).astype(np.float32)
|
||||
norm4 = float(np.linalg.norm(vec4))
|
||||
if norm4 < 1e-6:
|
||||
vec4[0] = 1.0
|
||||
norm4 = 1.0
|
||||
|
||||
vec = np.zeros(5, dtype=np.float32)
|
||||
vec[:4] = vec4
|
||||
vec[4] = 0.25 * norm4 * np.tanh(float(rng.standard_normal()))
|
||||
|
||||
mv = np.zeros(32, dtype=np.float32)
|
||||
mv[1:6] = vec
|
||||
return unitize_versor(mv)
|
||||
|
||||
|
||||
def _vocab() -> VocabManifold:
|
||||
vocab = VocabManifold()
|
||||
vocab.add("logos", _positive_unit_reflector(1))
|
||||
vocab.add("arche", _positive_unit_reflector(2))
|
||||
vocab.add("pneuma", _positive_unit_reflector(3))
|
||||
vocab.add("truth", _positive_unit_reflector(4))
|
||||
vocab.add("nous", _positive_unit_reflector(5))
|
||||
return vocab
|
||||
|
||||
|
||||
def _farther_unrelated(result_F: np.ndarray, prompt_F: np.ndarray, start_seed: int) -> np.ndarray:
|
||||
prompt_score = cga_inner(result_F, prompt_F)
|
||||
for seed in range(start_seed, start_seed + 256):
|
||||
candidate = _positive_unit_reflector(seed)
|
||||
if prompt_score > cga_inner(result_F, candidate):
|
||||
return candidate
|
||||
raise AssertionError("Could not construct a deterministic farther unrelated versor.")
|
||||
|
||||
|
||||
def test_repeated_prompt_accumulates_field_and_stays_prompt_coherent() -> None:
|
||||
session = SessionContext(vocab=_vocab())
|
||||
prompt = ["logos", "arche"]
|
||||
|
||||
initial = session.ingest(prompt)
|
||||
first = session.respond(max_tokens=4)
|
||||
|
||||
second_prompt_state = session.ingest(prompt)
|
||||
assert not np.array_equal(second_prompt_state.F, initial.F)
|
||||
|
||||
second = session.respond(max_tokens=4)
|
||||
|
||||
assert second.tokens != first.tokens
|
||||
assert not np.array_equal(second.final_state.F, first.final_state.F)
|
||||
|
||||
for i, result in enumerate((first, second)):
|
||||
random_unrelated = _farther_unrelated(result.final_state.F, initial.F, 11 + (i * 64))
|
||||
prompt_score = cga_inner(result.final_state.F, initial.F)
|
||||
random_score = cga_inner(result.final_state.F, random_unrelated)
|
||||
assert prompt_score > random_score
|
||||
Loading…
Reference in a new issue