core/generate/articulation.py
Shay 0dd22bb4dd
Add ADR-0009 articulation planner
Add geometry-backed ArticulationPlan and realize(), wire articulation into ChatRuntime and trace output, expose proposition relation_norm, and add articulation/runtime/CLI tests.
2026-05-13 21:39:25 -07:00

95 lines
2.9 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
import numpy as np
from generate.proposition import Proposition
from vocab.manifold import VocabManifold
@dataclass(frozen=True, slots=True)
class ArticulationPlan:
subject: str
predicate: str
object: str | None
surface: str
output_language: str
frame_id: str
def _candidate_indices(vocab: VocabManifold, output_language: str) -> np.ndarray:
indices = vocab.indices_for_language(output_language)
if len(indices) == 0:
raise ValueError(f"No articulation candidates for output language {output_language!r}.")
return indices
def _surface_for_word(vocab: VocabManifold, word: str) -> str:
morphology = vocab.morphology_for_word(word)
if morphology is None:
return word
return morphology.lemma or morphology.surface
def _resolve_slot(
versor: np.ndarray | None,
vocab: VocabManifold,
output_language: str,
) -> str | None:
if versor is None:
return None
word, _ = vocab.nearest(
versor,
candidate_indices=_candidate_indices(vocab, output_language),
)
return _surface_for_word(vocab, word)
def _assemble(subject: str, predicate: str, object_: str | None, output_language: str) -> str:
if output_language == "he":
parts = [predicate, subject]
if object_ is not None:
parts.append(object_)
return " ".join(part for part in parts if part)
if output_language == "grc":
parts = [subject]
if object_ is not None:
parts.append(object_)
parts.append(predicate)
return " ".join(part for part in parts if part)
parts = [subject, predicate]
if object_ is not None:
parts.append(object_)
return " ".join(part for part in parts if part)
def realize(
proposition: Proposition,
vocab: VocabManifold,
output_language: str = "en",
) -> ArticulationPlan:
"""
Map proposition frame slots to morphology-backed target-language surface forms.
Slot resolution is purely geometric:
1. take the slot versor from Proposition
2. restrict candidates to the configured output language
3. choose nearest manifold point by CGA inner product through VocabManifold
4. return MorphologyEntry.lemma when available, else surface
"""
subject = _resolve_slot(proposition.subject_versor, vocab, output_language)
predicate = _resolve_slot(proposition.predicate_versor, vocab, output_language)
object_ = _resolve_slot(proposition.object_versor, vocab, output_language)
if subject is None or predicate is None:
raise ValueError("Articulation requires subject and predicate slot versors.")
return ArticulationPlan(
subject=subject,
predicate=predicate,
object=object_,
surface=_assemble(subject, predicate, object_, output_language),
output_language=output_language,
frame_id=proposition.frame_id,
)