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 Falls back to proposition.surface when subject or predicate slot versors are absent (e.g. OOV-heavy input where grounding partially failed), rather than raising and crashing ChatRuntime.chat(). """ 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: # Graceful fallback: slot versors unavailable, use proposition surface directly. return ArticulationPlan( subject=proposition.subject or "", predicate=proposition.predicate or "", object=proposition.object_ if hasattr(proposition, "object_") else None, surface=proposition.surface, output_language=output_language, frame_id=proposition.frame_id, ) return ArticulationPlan( subject=subject, predicate=predicate, object=object_, surface=_assemble(subject, predicate, object_, output_language), output_language=output_language, frame_id=proposition.frame_id, )