Fix post-contract runtime regressions
- remove normalization and unitization calls from generation path - skip invalid recalled fields instead of repairing them in generation - punctuate selected articulation surfaces - stabilize assertive dialogue roles - anchor proposition slots to live field - preserve session anchor orientation for coherence
This commit is contained in:
parent
7401eae7ae
commit
a683912ad2
4 changed files with 84 additions and 52 deletions
|
|
@ -40,6 +40,8 @@ _SEED_ALIASES = {
|
|||
"arche": "ἀρχή",
|
||||
"aletheia": "ἀλήθεια",
|
||||
}
|
||||
_QUESTION_WORDS = frozenset({"what", "who", "how", "why", "when", "where", "which"})
|
||||
_TERMINALS = frozenset({".", "?", ";", "!"})
|
||||
|
||||
|
||||
def _energy_scalar(energy_obj) -> float:
|
||||
|
|
@ -54,6 +56,33 @@ def _energy_scalar(energy_obj) -> float:
|
|||
return 1.0
|
||||
|
||||
|
||||
def _is_question_input(raw_text: str, tokens: Sequence[str]) -> bool:
|
||||
if raw_text.strip().endswith("?"):
|
||||
return True
|
||||
return bool(tokens and tokens[0].casefold() in _QUESTION_WORDS)
|
||||
|
||||
|
||||
def _stable_dialogue_role(role: DialogueRole, *, raw_text: str, tokens: Sequence[str]) -> DialogueRole:
|
||||
if role == "question" and not _is_question_input(raw_text, tokens):
|
||||
return "elaborate"
|
||||
return role
|
||||
|
||||
|
||||
def _terminal_for_role(role: DialogueRole, output_language: str) -> str:
|
||||
if role == "question":
|
||||
return ";" if output_language == "grc" else "?"
|
||||
return "."
|
||||
|
||||
|
||||
def _terminate_surface(surface: str, *, role: DialogueRole, output_language: str) -> str:
|
||||
stripped = surface.strip()
|
||||
if not stripped:
|
||||
return stripped
|
||||
if stripped[-1] in _TERMINALS:
|
||||
return stripped
|
||||
return f"{stripped}{_terminal_for_role(role, output_language)}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class _StubBindingFrame:
|
||||
frame_id: str
|
||||
|
|
@ -272,9 +301,10 @@ class ChatRuntime:
|
|||
self._frame_registry,
|
||||
output_lang=self.config.output_language,
|
||||
)
|
||||
dialogue_role = classify_dialogue_blade(
|
||||
base_proposition.relation,
|
||||
reference_blade,
|
||||
dialogue_role = _stable_dialogue_role(
|
||||
classify_dialogue_blade(base_proposition.relation, reference_blade),
|
||||
raw_text=text,
|
||||
tokens=tokens,
|
||||
)
|
||||
proposition = propose_dialogue(
|
||||
field_state,
|
||||
|
|
@ -346,7 +376,12 @@ class ChatRuntime:
|
|||
)
|
||||
walk_surface = sentence_plan.surface
|
||||
|
||||
surface = articulation.surface
|
||||
surface = _terminate_surface(
|
||||
articulation.surface,
|
||||
role=dialogue_role,
|
||||
output_language=self.config.output_language,
|
||||
)
|
||||
articulation_surface = surface
|
||||
vault_hits = int(result.vault_hits)
|
||||
|
||||
turn_event = TurnEvent(
|
||||
|
|
@ -354,7 +389,7 @@ class ChatRuntime:
|
|||
input_tokens=tuple(filtered),
|
||||
surface=surface,
|
||||
walk_surface=walk_surface,
|
||||
articulation_surface=articulation.surface,
|
||||
articulation_surface=articulation_surface,
|
||||
dialogue_role=str(dialogue_role),
|
||||
identity_score=identity_score,
|
||||
cycle_cost_total=cycle_cost.total,
|
||||
|
|
@ -369,7 +404,7 @@ class ChatRuntime:
|
|||
surface=surface,
|
||||
proposition=proposition,
|
||||
articulation=articulation,
|
||||
articulation_surface=articulation.surface,
|
||||
articulation_surface=articulation_surface,
|
||||
dialogue_role=dialogue_role,
|
||||
versor_condition=versor_condition(result.final_state.F),
|
||||
output_language=self.config.output_language,
|
||||
|
|
|
|||
|
|
@ -153,9 +153,6 @@ def propose(
|
|||
preferred_pos=frozenset({"noun", "pronoun"}),
|
||||
candidate_indices=candidate_indices,
|
||||
)
|
||||
# Predicate selection must remain anchored to the prompt field, not a
|
||||
# recall-contaminated or drive-biased current field, so slot evidence stays
|
||||
# closer to prompt than unrelated vault points.
|
||||
predicate_word, predicate_idx = _nearest_content_word(
|
||||
vocab,
|
||||
prompt,
|
||||
|
|
@ -275,7 +272,7 @@ def _first_existing(vocab, candidates: tuple[str, ...]) -> str | None:
|
|||
|
||||
|
||||
def _prompt_versor(field_state: FieldState) -> np.ndarray:
|
||||
return field_state.holonomy if field_state.holonomy is not None else field_state.F
|
||||
return field_state.F
|
||||
|
||||
|
||||
def _nearest_content_word(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,11 @@ Generation loop — token streaming from the versor manifold.
|
|||
|
||||
Every token: nearest non-current word to current F via CGA inner product.
|
||||
Every step: F <- versor_apply(V, F) where V = word_transition_rotor(A, B).
|
||||
|
||||
Generation is not a normalization boundary. Raw prompt normalization belongs
|
||||
at ingest/gate.py; construction normalization belongs in algebra/vocab/persona.
|
||||
If vault recall returns a non-operator-like field that cannot form a stable
|
||||
transition, recall skips that hit instead of repairing it here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -13,7 +18,6 @@ import numpy as np
|
|||
from field.state import FieldState
|
||||
from field.propagate import propagate_step
|
||||
from algebra.rotor import word_transition_rotor
|
||||
from algebra.versor import normalize_to_versor, unitize_versor
|
||||
from generate.attention import AttentionOperator
|
||||
from generate.result import GenerationResult
|
||||
from generate.salience import SalienceOperator
|
||||
|
|
@ -22,29 +26,6 @@ _RECENT_WINDOW = 3
|
|||
_STOP_TOKENS = frozenset({"it", "to", "word"})
|
||||
|
||||
|
||||
def _closed_F(F: np.ndarray) -> np.ndarray:
|
||||
arr = np.asarray(F, dtype=np.float64)
|
||||
try:
|
||||
return unitize_versor(arr)
|
||||
except ValueError:
|
||||
return normalize_to_versor(arr)
|
||||
|
||||
|
||||
def _renorm(state: FieldState) -> FieldState:
|
||||
"""Return state with F reclosed onto the versor manifold."""
|
||||
closed = _closed_F(state.F)
|
||||
if np.allclose(closed, state.F, atol=1e-12, rtol=1e-12):
|
||||
return state
|
||||
return FieldState(
|
||||
F=closed,
|
||||
node=state.node,
|
||||
step=state.step,
|
||||
holonomy=state.holonomy,
|
||||
energy=state.energy,
|
||||
valence=state.valence,
|
||||
)
|
||||
|
||||
|
||||
def _articulate(vocab, word: str) -> str:
|
||||
morphology_for_word = getattr(vocab, "morphology_for_word", None)
|
||||
if morphology_for_word is None:
|
||||
|
|
@ -117,26 +98,33 @@ def _nearest_with_optional_candidates(
|
|||
|
||||
|
||||
def _voiced_state(state: FieldState, persona) -> FieldState:
|
||||
return _renorm(FieldState(
|
||||
return FieldState(
|
||||
F=persona.apply(state.F),
|
||||
node=state.node,
|
||||
step=state.step,
|
||||
holonomy=state.holonomy,
|
||||
energy=state.energy,
|
||||
valence=state.valence,
|
||||
))
|
||||
)
|
||||
|
||||
|
||||
def _recall_state(state: FieldState, vault, top_k: int) -> tuple[FieldState, int]:
|
||||
if vault is None or top_k <= 0:
|
||||
return state, 0
|
||||
|
||||
current = _renorm(state)
|
||||
current = state
|
||||
hits_applied = 0
|
||||
for hit in vault.recall(current.F, top_k=top_k):
|
||||
recalled_F = _closed_F(np.asarray(hit["versor"], dtype=np.float64))
|
||||
V = word_transition_rotor(current.F, recalled_F)
|
||||
current = _renorm(propagate_step(current, V))
|
||||
recalled_F = np.asarray(hit["versor"], dtype=np.float64)
|
||||
try:
|
||||
V = word_transition_rotor(current.F, recalled_F)
|
||||
except ValueError:
|
||||
# Vault stores field states as well as proposition/memory payloads.
|
||||
# Not every recalled versor is a valid transition target for the
|
||||
# live generation operator. Generation must fail closed here rather
|
||||
# than normalizing or repairing recalled memory in the hot path.
|
||||
continue
|
||||
current = propagate_step(current, V)
|
||||
current = FieldState(
|
||||
F=current.F,
|
||||
node=state.node,
|
||||
|
|
@ -203,7 +191,7 @@ def generate(
|
|||
tokens = []
|
||||
trajectory = [] if record_trajectory else None
|
||||
vault_hits = 0
|
||||
current = _renorm(state)
|
||||
current = state
|
||||
recent_nodes = deque([state.node], maxlen=_RECENT_WINDOW)
|
||||
language_candidates = None if allow_cross_language_generation else _candidate_indices_for_language(vocab, output_lang)
|
||||
salience_candidates, salience_budget, candidates_used = _attention_candidates(
|
||||
|
|
@ -245,7 +233,7 @@ def generate(
|
|||
B = vocab.get_versor_at(word_idx)
|
||||
V = word_transition_rotor(A, B)
|
||||
|
||||
current = _renorm(propagate_step(current, V))
|
||||
current = propagate_step(current, V)
|
||||
current = FieldState(
|
||||
F=current.F,
|
||||
node=word_idx,
|
||||
|
|
@ -258,7 +246,7 @@ def generate(
|
|||
|
||||
return GenerationResult(
|
||||
tokens=tokens,
|
||||
final_state=_renorm(current),
|
||||
final_state=current,
|
||||
trajectory=trajectory,
|
||||
salience_top_k=salience_budget,
|
||||
candidates_used=candidates_used,
|
||||
|
|
@ -274,7 +262,7 @@ async def agenerate(
|
|||
vault=None,
|
||||
recall_top_k: int = 3,
|
||||
):
|
||||
current = _renorm(state)
|
||||
current = state
|
||||
recent_nodes = deque([state.node], maxlen=_RECENT_WINDOW)
|
||||
stop_nodes = frozenset(
|
||||
vocab.index_of(token)
|
||||
|
|
@ -300,7 +288,7 @@ async def agenerate(
|
|||
B = vocab.get_versor_at(word_idx)
|
||||
V = word_transition_rotor(A, B)
|
||||
|
||||
current = _renorm(propagate_step(current, V))
|
||||
current = propagate_step(current, V)
|
||||
current = FieldState(
|
||||
F=current.F,
|
||||
node=word_idx,
|
||||
|
|
|
|||
|
|
@ -110,9 +110,21 @@ class SessionContext:
|
|||
valence=self.state.valence,
|
||||
)
|
||||
result = generate(pivot, self.vocab, self.persona, max_tokens, vault=self.vault)
|
||||
result = self._orient_result_to_anchor(result)
|
||||
self.state = result.final_state
|
||||
self.vault.store(result.final_state.F, {"turn": self.turn, "role": "assistant"})
|
||||
self.turn += 1
|
||||
self._last_response_tokens = result.tokens
|
||||
return result
|
||||
|
||||
def _orient_result_to_anchor(self, result: GenerationResult) -> GenerationResult:
|
||||
final_state = result.final_state
|
||||
coherence_anchor = self._anchor_field if self._anchor_field is not None else self.state.F
|
||||
if cga_inner(final_state.F, coherence_anchor) < 0.0:
|
||||
if coherence_anchor is None:
|
||||
return result
|
||||
cga_score = cga_inner(final_state.F, coherence_anchor)
|
||||
euclidean_score = float(np.dot(final_state.F, coherence_anchor))
|
||||
if cga_score < 0.0 or euclidean_score < 0.0:
|
||||
final_state = FieldState(
|
||||
F=-final_state.F,
|
||||
node=final_state.node,
|
||||
|
|
@ -121,17 +133,15 @@ class SessionContext:
|
|||
energy=final_state.energy,
|
||||
valence=final_state.valence,
|
||||
)
|
||||
result = GenerationResult(
|
||||
return GenerationResult(
|
||||
tokens=result.tokens,
|
||||
final_state=final_state,
|
||||
trajectory=result.trajectory,
|
||||
salience_top_k=result.salience_top_k,
|
||||
candidates_used=result.candidates_used,
|
||||
vault_hits=result.vault_hits,
|
||||
identity_score=result.identity_score,
|
||||
)
|
||||
self.state = result.final_state
|
||||
self.vault.store(result.final_state.F, {"turn": self.turn, "role": "assistant"})
|
||||
self.turn += 1
|
||||
self._last_response_tokens = result.tokens
|
||||
return result
|
||||
|
||||
async def arespond(self, max_tokens: int = 128):
|
||||
|
|
@ -143,7 +153,9 @@ 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, vault=self.vault)
|
||||
result = self._orient_result_to_anchor(
|
||||
generate(self.state, self.vocab, self.persona, max_tokens, vault=self.vault)
|
||||
)
|
||||
for token in result.tokens:
|
||||
yield token
|
||||
self.state = result.final_state
|
||||
|
|
|
|||
Loading…
Reference in a new issue