From 2b78cd1179280ab072a9530cdfd5c30193611506 Mon Sep 17 00:00:00 2001 From: Shay Date: Wed, 13 May 2026 20:19:21 -0700 Subject: [PATCH] Add dialogue frame selection --- field/propagate.py | 2 +- generate/__init__.py | 14 +++ generate/dialogue.py | 106 ++++++++++++++++++ generate/proposition.py | 9 ++ .../data/grc_logos_micro_v1/lexicon.jsonl | 3 + .../data/grc_logos_micro_v1/manifest.json | 2 +- .../data/grc_logos_micro_v1/morphology.jsonl | 3 + packs/common/schema/frame.schema.json | 10 +- packs/el/frames.jsonl | 8 +- packs/en/frames.jsonl | 7 +- packs/grc/frames.jsonl | 8 +- packs/grc/lemmas.jsonl | 3 + packs/he/frames.jsonl | 8 +- session/context.py | 41 ++++++- tests/test_dialogue.py | 72 ++++++++++++ 15 files changed, 272 insertions(+), 24 deletions(-) create mode 100644 generate/dialogue.py create mode 100644 tests/test_dialogue.py diff --git a/field/propagate.py b/field/propagate.py index 38636ee6..dc9977d9 100644 --- a/field/propagate.py +++ b/field/propagate.py @@ -21,4 +21,4 @@ def propagate_step(state: FieldState, V) -> FieldState: Returns a new FieldState one step forward on the manifold. """ new_F = versor_apply(V, state.F) - return FieldState(F=new_F, node=state.node, step=state.step + 1) + return FieldState(F=new_F, node=state.node, step=state.step + 1, holonomy=state.holonomy) diff --git a/generate/__init__.py b/generate/__init__.py index 869ab987..c941619f 100644 --- a/generate/__init__.py +++ b/generate/__init__.py @@ -5,14 +5,28 @@ from .proposition import ( PropositionFrame, propose, ) +from .dialogue import ( + DialogueRole, + DialogueTurn, + blade_alignment, + classify_dialogue_blade, + propose_dialogue, + trajectory_blade, +) from .stream import generate, agenerate __all__ = [ + "DialogueRole", + "DialogueTurn", "FrameRegistry", "FrameSlot", "Proposition", "PropositionFrame", "agenerate", + "blade_alignment", + "classify_dialogue_blade", "generate", "propose", + "propose_dialogue", + "trajectory_blade", ] diff --git a/generate/dialogue.py b/generate/dialogue.py new file mode 100644 index 00000000..599a696d --- /dev/null +++ b/generate/dialogue.py @@ -0,0 +1,106 @@ +""" +Dialogue move selection from proposition relation blades. + +The proposition layer constructs X_prompt ^ X_field. Dialogue reads that +grade-2 relation against the prior relation blade and chooses the next move +kind: assertion, elaboration, question/contrast, or refutation. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Literal + +import numpy as np + +from algebra.cga import cga_inner, outer_product +from field.state import FieldState +from generate.proposition import FrameRegistry, Proposition, propose + +DialogueRole = Literal["assert", "elaborate", "question", "refute"] + +_PARALLEL_THRESHOLD = 0.35 +_ANTI_PARALLEL_THRESHOLD = -0.35 +_ORTHOGONAL_THRESHOLD = 0.20 + + +@dataclass(frozen=True, slots=True) +class DialogueTurn: + proposition: Proposition + outer_product_blade: np.ndarray = field(repr=False) + + def __post_init__(self) -> None: + blade = np.asarray(self.outer_product_blade, dtype=np.float32).copy() + object.__setattr__(self, "outer_product_blade", blade) + + +def blade_alignment(blade: np.ndarray, reference: np.ndarray) -> float: + """ + Return signed orientation of two relation blades. + + Positive values mean same plane orientation, near-zero values mean + orthogonal relation, and negative values mean the same relation reversed. + The scalar is derived from CORE's algebraic inner product, not an external + approximate metric. + """ + blade = np.asarray(blade, dtype=np.float32) + reference = np.asarray(reference, dtype=np.float32) + blade_norm = abs(cga_inner(blade, blade)) ** 0.5 + reference_norm = abs(cga_inner(reference, reference)) ** 0.5 + if blade_norm < 1e-8 or reference_norm < 1e-8: + return 0.0 + return float(-cga_inner(blade, reference) / (blade_norm * reference_norm)) + + +def classify_dialogue_blade( + blade: np.ndarray, + reference_blade: np.ndarray | None = None, +) -> DialogueRole: + """ + Classify a relation blade as the next dialogue move. + + With no prior relation the engine has no contrastive plane yet, so the + first move is an assertion. After that: + same orientation -> elaboration + near-orthogonal -> question/contrast + reversed orientation -> refutation + """ + if reference_blade is None: + return "assert" + + alignment = blade_alignment(blade, reference_blade) + if alignment >= _PARALLEL_THRESHOLD: + return "elaborate" + if alignment <= _ANTI_PARALLEL_THRESHOLD: + return "refute" + if abs(alignment) <= _ORTHOGONAL_THRESHOLD: + return "question" + return "question" + + +def trajectory_blade(blades: tuple[np.ndarray, ...] | list[np.ndarray]) -> np.ndarray | None: + """Fold a dialogue path into the running outer product of its blades.""" + if not blades: + return None + running = np.asarray(blades[0], dtype=np.float32).copy() + for blade in blades[1:]: + running = outer_product(running, blade) + return running + + +def propose_dialogue( + field_state: FieldState, + vault, + vocab, + frame_registry: FrameRegistry, + reference_blade: np.ndarray | None = None, +) -> Proposition: + """ + Generate a proposition through a dialogue-role constrained frame choice. + """ + base = propose(field_state, None, vocab, frame_registry) + role = classify_dialogue_blade(base.relation, reference_blade) + frame = frame_registry.select_dialogue(base.relation, role) + role_registry = FrameRegistry((frame,)) + proposition = propose(field_state, vault, vocab, role_registry) + return proposition diff --git a/generate/proposition.py b/generate/proposition.py index 3e5c585d..dfe28f4f 100644 --- a/generate/proposition.py +++ b/generate/proposition.py @@ -41,6 +41,7 @@ class PropositionFrame: frame_id: str language: str predicate_type: str + dialogue_role: str slots: tuple[FrameSlot, ...] constraints: tuple[str, ...] = field(default_factory=tuple) relation: np.ndarray = field(default_factory=lambda: np.zeros(32, dtype=np.float32)) @@ -113,6 +114,13 @@ class FrameRegistry: relation = np.asarray(relation, dtype=np.float32) return max(self._frames, key=lambda frame: cga_inner(relation, frame.relation)) + def select_dialogue(self, relation: np.ndarray, dialogue_role: str) -> PropositionFrame: + relation = np.asarray(relation, dtype=np.float32) + candidates = tuple(frame for frame in self._frames if frame.dialogue_role == dialogue_role) + if not candidates: + candidates = self._frames + return max(candidates, key=lambda frame: cga_inner(relation, frame.relation)) + def __iter__(self): return iter(self._frames) @@ -193,6 +201,7 @@ def _parse_frame(payload: dict, vocab) -> PropositionFrame: frame_id=payload["frame_id"], language=payload["language"], predicate_type=payload["predicate_type"], + dialogue_role=payload.get("dialogue_role", "assert"), slots=slots, constraints=tuple(payload.get("constraints", ())), relation=relation, diff --git a/language_packs/data/grc_logos_micro_v1/lexicon.jsonl b/language_packs/data/grc_logos_micro_v1/lexicon.jsonl index 9d46fab1..307878ce 100644 --- a/language_packs/data/grc_logos_micro_v1/lexicon.jsonl +++ b/language_packs/data/grc_logos_micro_v1/lexicon.jsonl @@ -6,3 +6,6 @@ {"entry_id": "grc-006", "surface": "πνεῦμα", "lemma": "πνεῦμα", "language": "grc", "pos": "NOUN", "morphology_id": "grc-morph-006", "semantic_domains": ["logos.pneuma.breath", "logos.core", "breath", "existence.animating", "logos.presence"], "morphology_tags": ["noun", "neuter", "nominative", "singular", "third_declension"]} {"entry_id": "grc-007", "surface": "κτίζω", "lemma": "κτίζω", "language": "grc", "pos": "VERB", "morphology_id": "grc-morph-007", "semantic_domains": ["logos.ktizo.formation", "logos.core", "formation", "logos.genesis", "divine.agency"], "morphology_tags": ["verb", "present", "active", "indicative", "first_person", "singular"]} {"entry_id": "grc-008", "surface": "λαμβάνω", "lemma": "λαμβάνω", "language": "grc", "pos": "VERB", "morphology_id": "grc-morph-008", "semantic_domains": ["logos.reception", "relation.shared", "epistemic.grasp", "divine.agency"], "morphology_tags": ["verb", "present", "active", "indicative", "first_person", "singular"]} +{"entry_id": "grc-009", "surface": "ἀποκρίνομαι", "lemma": "ἀποκρίνομαι", "language": "grc", "pos": "VERB", "morphology_id": "grc-morph-009", "semantic_domains": ["response", "communication.articulation", "relation.dialogic", "knowledge.ground"], "morphology_tags": ["verb", "present", "middle", "indicative", "first_person", "singular", "deponent"]} +{"entry_id": "grc-010", "surface": "λέγω", "lemma": "λέγω", "language": "grc", "pos": "VERB", "morphology_id": "grc-morph-010", "semantic_domains": ["speech", "logos.utterance", "communication.articulation", "relation.dialogic"], "morphology_tags": ["verb", "present", "active", "indicative", "first_person", "singular"]} +{"entry_id": "grc-011", "surface": "ἐρωτάω", "lemma": "ἐρωτάω", "language": "grc", "pos": "VERB", "morphology_id": "grc-morph-011", "semantic_domains": ["question", "communication.articulation", "relation.dialogic", "epistemic.seeking"], "morphology_tags": ["verb", "present", "active", "indicative", "first_person", "singular"]} diff --git a/language_packs/data/grc_logos_micro_v1/manifest.json b/language_packs/data/grc_logos_micro_v1/manifest.json index 69e4a178..4df25336 100644 --- a/language_packs/data/grc_logos_micro_v1/manifest.json +++ b/language_packs/data/grc_logos_micro_v1/manifest.json @@ -6,7 +6,7 @@ "normalization_policy": "unitize_versor", "source_manifest": "grc_logos_micro_v1.lexicon.jsonl", "determinism_class": "D0", - "checksum": "580ce1b32872fb39768ea0398f6d5d77bab2b9d04a1e808cdc1ecc09fbcb959b", + "checksum": "9a66f98f2adcbecb907b9afb2d6bf0de5b8c200b3b67df3cc548bf29d9aec935", "version": "1.0.0", "gate_engaged": true, "oov_policy": "fail_closed" diff --git a/language_packs/data/grc_logos_micro_v1/morphology.jsonl b/language_packs/data/grc_logos_micro_v1/morphology.jsonl index c573082a..dabc53cc 100644 --- a/language_packs/data/grc_logos_micro_v1/morphology.jsonl +++ b/language_packs/data/grc_logos_micro_v1/morphology.jsonl @@ -6,3 +6,6 @@ {"morphology_id": "grc-morph-006", "surface": "\u03c0\u03bd\u03b5\u1fe6\u03bc\u03b1", "lemma": "\u03c0\u03bd\u03b5\u1fe6\u03bc\u03b1", "language": "grc", "root": "\u03c0\u03bd\u03b5\u03c5", "prefix_chain": [], "stem": "\u03c0\u03bd\u03b5\u03c5\u03bc\u03b1\u03c4", "inflection": {"pos": "noun", "case": "nominative", "gender": "neuter", "number": "singular", "declension": "third"}, "suffix_chain": []} {"morphology_id": "grc-morph-007", "surface": "\u03ba\u03c4\u03af\u03b6\u03c9", "lemma": "\u03ba\u03c4\u03af\u03b6\u03c9", "language": "grc", "root": "\u03ba\u03c4\u03b9\u03b4", "prefix_chain": [], "stem": "\u03ba\u03c4\u03b9\u03b6", "inflection": {"pos": "verb", "tense": "present", "voice": "active", "mood": "indicative", "person": "first", "number": "singular"}, "suffix_chain": ["\u03c9"]} {"morphology_id": "grc-morph-008", "surface": "λαμβάνω", "lemma": "λαμβάνω", "language": "grc", "root": "λαβ", "prefix_chain": [], "stem": "λαμβαν", "inflection": {"pos": "verb", "tense": "present", "voice": "active", "mood": "indicative", "person": "first", "number": "singular"}, "suffix_chain": ["ω"]} +{"morphology_id": "grc-morph-009", "surface": "ἀποκρίνομαι", "lemma": "ἀποκρίνομαι", "language": "grc", "root": "κριν", "prefix_chain": ["ἀπο"], "stem": "κριν", "inflection": {"pos": "verb", "tense": "present", "voice": "middle", "mood": "indicative", "person": "first", "number": "singular", "deponency": "deponent"}, "suffix_chain": ["ομαι"]} +{"morphology_id": "grc-morph-010", "surface": "λέγω", "lemma": "λέγω", "language": "grc", "root": "λεγ", "prefix_chain": [], "stem": "λεγ", "inflection": {"pos": "verb", "tense": "present", "voice": "active", "mood": "indicative", "person": "first", "number": "singular"}, "suffix_chain": ["ω"]} +{"morphology_id": "grc-morph-011", "surface": "ἐρωτάω", "lemma": "ἐρωτάω", "language": "grc", "root": "ἐρωτα", "prefix_chain": [], "stem": "ἐρωτα", "inflection": {"pos": "verb", "tense": "present", "voice": "active", "mood": "indicative", "person": "first", "number": "singular"}, "suffix_chain": ["ω"]} diff --git a/packs/common/schema/frame.schema.json b/packs/common/schema/frame.schema.json index ad755a18..a8c66dbe 100644 --- a/packs/common/schema/frame.schema.json +++ b/packs/common/schema/frame.schema.json @@ -3,7 +3,7 @@ "title": "CORE Frame Record", "description": "A syntactic-semantic frame template defining participant structure for a predicate type.", "type": "object", - "required": ["frame_id", "language", "predicate_type", "slots"], + "required": ["frame_id", "language", "predicate_type", "dialogue_role", "slots"], "properties": { "frame_id": { "type": "string", @@ -17,6 +17,11 @@ "type": "string", "description": "The predicate class this frame serves. Examples: copular, transitive, ditransitive, existential, causative." }, + "dialogue_role": { + "type": "string", + "enum": ["assert", "elaborate", "question", "refute"], + "description": "Dialogue move kind selected from relation-blade geometry." + }, "slots": { "type": "array", "items": { @@ -34,6 +39,9 @@ "constraints": { "type": "array", "items": { "type": "string" } + }, + "notes": { + "type": "string" } }, "additionalProperties": false diff --git a/packs/el/frames.jsonl b/packs/el/frames.jsonl index 98552742..e0bbc16e 100644 --- a/packs/el/frames.jsonl +++ b/packs/el/frames.jsonl @@ -1,4 +1,4 @@ -{"frame_id":"el:copular-basic","language":"el","predicate_type":"copular","slots":[{"name":"subject","required":true,"semantic_role":"carrier","agreement_target":"verb"},{"name":"predicate_nominative","required":true,"semantic_role":"attribute","agreement_target":null}],"constraints":["subject-verb-agreement","predicate-nominative-case"]} -{"frame_id":"el:colwell-construction","language":"el","predicate_type":"copular-qualitative","slots":[{"name":"subject","required":true,"semantic_role":"carrier","agreement_target":"verb"},{"name":"anarthrous_predicate","required":true,"semantic_role":"qualitative-attribute","agreement_target":null}],"constraints":["predicate-precedes-copula","predicate-is-anarthrous","subject-has-article"],"notes":"Colwell's Rule: a definite predicate nominative that precedes the copula is anarthrous. John 1:1c is the canonical instance."} -{"frame_id":"el:existential-imperfect","language":"el","predicate_type":"existential-continuous","slots":[{"name":"existent","required":true,"semantic_role":"continuous-being","agreement_target":"verb"},{"name":"locus","required":false,"semantic_role":"sphere","agreement_target":null}],"constraints":["verb-must-be-imperfect"],"notes":"The imperfect of eimi signals durative pre-existent being. Distinct from aorist which would signal point-in-time origin."} -{"frame_id":"el:relational-pros","language":"el","predicate_type":"relational-directional","slots":[{"name":"figure","required":true,"semantic_role":"oriented-entity","agreement_target":null},{"name":"ground","required":true,"semantic_role":"orientation-target","agreement_target":null}],"constraints":["pros-governs-accusative"]} +{"frame_id":"el:copular-basic","language":"el","predicate_type":"copular","dialogue_role":"assert","slots":[{"name":"subject","required":true,"semantic_role":"carrier","agreement_target":"verb"},{"name":"predicate_nominative","required":true,"semantic_role":"attribute","agreement_target":null}],"constraints":["subject-verb-agreement","predicate-nominative-case"]} +{"frame_id":"el:colwell-construction","language":"el","predicate_type":"copular-qualitative","dialogue_role":"elaborate","slots":[{"name":"subject","required":true,"semantic_role":"carrier","agreement_target":"verb"},{"name":"anarthrous_predicate","required":true,"semantic_role":"qualitative-attribute","agreement_target":null}],"constraints":["predicate-precedes-copula","predicate-is-anarthrous","subject-has-article"],"notes":"Colwell's Rule: a definite predicate nominative that precedes the copula is anarthrous. John 1:1c is the canonical instance."} +{"frame_id":"el:existential-imperfect","language":"el","predicate_type":"existential-continuous","dialogue_role":"question","slots":[{"name":"existent","required":true,"semantic_role":"continuous-being","agreement_target":"verb"},{"name":"locus","required":false,"semantic_role":"sphere","agreement_target":null}],"constraints":["verb-must-be-imperfect"],"notes":"The imperfect of eimi signals durative pre-existent being. Distinct from aorist which would signal point-in-time origin."} +{"frame_id":"el:relational-pros","language":"el","predicate_type":"relational-directional","dialogue_role":"refute","slots":[{"name":"figure","required":true,"semantic_role":"oriented-entity","agreement_target":null},{"name":"ground","required":true,"semantic_role":"orientation-target","agreement_target":null}],"constraints":["pros-governs-accusative"]} diff --git a/packs/en/frames.jsonl b/packs/en/frames.jsonl index bd86f46d..2f14a8be 100644 --- a/packs/en/frames.jsonl +++ b/packs/en/frames.jsonl @@ -1,3 +1,4 @@ -{"frame_id":"en:copular-basic","language":"en","predicate_type":"copular","slots":[{"name":"subject","required":true,"semantic_role":"carrier","agreement_target":"verb"},{"name":"predicate","required":true,"semantic_role":"attribute","agreement_target":null}],"constraints":["subject-verb-agreement"]} -{"frame_id":"en:existential-basic","language":"en","predicate_type":"existential","slots":[{"name":"pivot","required":true,"semantic_role":"existent","agreement_target":"verb"},{"name":"location","required":false,"semantic_role":"locus","agreement_target":null}],"constraints":[]} -{"frame_id":"en:accompaniment-prep","language":"en","predicate_type":"prepositional","slots":[{"name":"figure","required":true,"semantic_role":"accompanier","agreement_target":null},{"name":"ground","required":true,"semantic_role":"accompanied","agreement_target":null}],"constraints":[]} +{"frame_id":"en:copular-basic","language":"en","predicate_type":"copular","dialogue_role":"assert","slots":[{"name":"subject","required":true,"semantic_role":"carrier","agreement_target":"verb"},{"name":"predicate","required":true,"semantic_role":"attribute","agreement_target":null}],"constraints":["subject-verb-agreement"]} +{"frame_id":"en:existential-basic","language":"en","predicate_type":"existential","dialogue_role":"elaborate","slots":[{"name":"pivot","required":true,"semantic_role":"existent","agreement_target":"verb"},{"name":"location","required":false,"semantic_role":"locus","agreement_target":null}],"constraints":[]} +{"frame_id":"en:accompaniment-prep","language":"en","predicate_type":"prepositional","dialogue_role":"question","slots":[{"name":"figure","required":true,"semantic_role":"accompanier","agreement_target":null},{"name":"ground","required":true,"semantic_role":"accompanied","agreement_target":null}],"constraints":[]} +{"frame_id":"en:negated-copular","language":"en","predicate_type":"copular-negated","dialogue_role":"refute","slots":[{"name":"subject","required":true,"semantic_role":"carrier","agreement_target":"verb"},{"name":"predicate","required":true,"semantic_role":"rejected-attribute","agreement_target":null}],"constraints":["negation-precedes-predicate"]} diff --git a/packs/grc/frames.jsonl b/packs/grc/frames.jsonl index 98552742..e0bbc16e 100644 --- a/packs/grc/frames.jsonl +++ b/packs/grc/frames.jsonl @@ -1,4 +1,4 @@ -{"frame_id":"el:copular-basic","language":"el","predicate_type":"copular","slots":[{"name":"subject","required":true,"semantic_role":"carrier","agreement_target":"verb"},{"name":"predicate_nominative","required":true,"semantic_role":"attribute","agreement_target":null}],"constraints":["subject-verb-agreement","predicate-nominative-case"]} -{"frame_id":"el:colwell-construction","language":"el","predicate_type":"copular-qualitative","slots":[{"name":"subject","required":true,"semantic_role":"carrier","agreement_target":"verb"},{"name":"anarthrous_predicate","required":true,"semantic_role":"qualitative-attribute","agreement_target":null}],"constraints":["predicate-precedes-copula","predicate-is-anarthrous","subject-has-article"],"notes":"Colwell's Rule: a definite predicate nominative that precedes the copula is anarthrous. John 1:1c is the canonical instance."} -{"frame_id":"el:existential-imperfect","language":"el","predicate_type":"existential-continuous","slots":[{"name":"existent","required":true,"semantic_role":"continuous-being","agreement_target":"verb"},{"name":"locus","required":false,"semantic_role":"sphere","agreement_target":null}],"constraints":["verb-must-be-imperfect"],"notes":"The imperfect of eimi signals durative pre-existent being. Distinct from aorist which would signal point-in-time origin."} -{"frame_id":"el:relational-pros","language":"el","predicate_type":"relational-directional","slots":[{"name":"figure","required":true,"semantic_role":"oriented-entity","agreement_target":null},{"name":"ground","required":true,"semantic_role":"orientation-target","agreement_target":null}],"constraints":["pros-governs-accusative"]} +{"frame_id":"el:copular-basic","language":"el","predicate_type":"copular","dialogue_role":"assert","slots":[{"name":"subject","required":true,"semantic_role":"carrier","agreement_target":"verb"},{"name":"predicate_nominative","required":true,"semantic_role":"attribute","agreement_target":null}],"constraints":["subject-verb-agreement","predicate-nominative-case"]} +{"frame_id":"el:colwell-construction","language":"el","predicate_type":"copular-qualitative","dialogue_role":"elaborate","slots":[{"name":"subject","required":true,"semantic_role":"carrier","agreement_target":"verb"},{"name":"anarthrous_predicate","required":true,"semantic_role":"qualitative-attribute","agreement_target":null}],"constraints":["predicate-precedes-copula","predicate-is-anarthrous","subject-has-article"],"notes":"Colwell's Rule: a definite predicate nominative that precedes the copula is anarthrous. John 1:1c is the canonical instance."} +{"frame_id":"el:existential-imperfect","language":"el","predicate_type":"existential-continuous","dialogue_role":"question","slots":[{"name":"existent","required":true,"semantic_role":"continuous-being","agreement_target":"verb"},{"name":"locus","required":false,"semantic_role":"sphere","agreement_target":null}],"constraints":["verb-must-be-imperfect"],"notes":"The imperfect of eimi signals durative pre-existent being. Distinct from aorist which would signal point-in-time origin."} +{"frame_id":"el:relational-pros","language":"el","predicate_type":"relational-directional","dialogue_role":"refute","slots":[{"name":"figure","required":true,"semantic_role":"oriented-entity","agreement_target":null},{"name":"ground","required":true,"semantic_role":"orientation-target","agreement_target":null}],"constraints":["pros-governs-accusative"]} diff --git a/packs/grc/lemmas.jsonl b/packs/grc/lemmas.jsonl index 3043f606..f10fb4ee 100644 --- a/packs/grc/lemmas.jsonl +++ b/packs/grc/lemmas.jsonl @@ -7,3 +7,6 @@ {"lemma_id":"grc:eimi","language":"grc","script_form":"εἰμί","root":"εσ","stem":"εἰμ","transliteration":"eimi","gloss_seed":"be","pos":"verb","morph_class":"athematic","semantic_family":"existence","field_hooks":["existence.state","existence.state.identity","existence.state.continuous"],"readback_priority":1} {"lemma_id":"grc:theos","language":"grc","script_form":"θεός","root":"θε","stem":"θεο","transliteration":"theos","gloss_seed":"God","pos":"noun","morph_class":"second-declension-masculine","semantic_family":"divine","field_hooks":["divine.identity","divine.presence"],"readback_priority":1} {"lemma_id":"grc:pros","language":"grc","script_form":"πρός","root":null,"stem":"πρός","transliteration":"pros","gloss_seed":"with","pos":"preposition","morph_class":"regular","semantic_family":"relation","field_hooks":["relation.directional","relation.accompaniment","relation.presence-toward"],"readback_priority":1} +{"lemma_id":"grc:apokrinomai","language":"grc","script_form":"ἀποκρίνομαι","root":"κριν","stem":"κριν","transliteration":"apokrinomai","gloss_seed":"answer","pos":"verb","morph_class":"present-middle-deponent-indicative","semantic_family":"response","field_hooks":["response","communication.articulation","relation.dialogic","knowledge.ground"],"readback_priority":1} +{"lemma_id":"grc:lego","language":"grc","script_form":"λέγω","root":"λεγ","stem":"λεγ","transliteration":"lego","gloss_seed":"say","pos":"verb","morph_class":"present-active-indicative","semantic_family":"speech","field_hooks":["speech","logos.utterance","communication.articulation","relation.dialogic"],"readback_priority":1} +{"lemma_id":"grc:erotao","language":"grc","script_form":"ἐρωτάω","root":"ἐρωτα","stem":"ἐρωτα","transliteration":"erotao","gloss_seed":"ask","pos":"verb","morph_class":"present-active-indicative","semantic_family":"question","field_hooks":["question","communication.articulation","relation.dialogic","epistemic.seeking"],"readback_priority":1} diff --git a/packs/he/frames.jsonl b/packs/he/frames.jsonl index 34e8ab61..cf826519 100644 --- a/packs/he/frames.jsonl +++ b/packs/he/frames.jsonl @@ -1,4 +1,4 @@ -{"frame_id":"he:verbal-basic","language":"he","predicate_type":"verbal","slots":[{"name":"subject","required":true,"semantic_role":"agent","agreement_target":"verb"},{"name":"object","required":false,"semantic_role":"patient","agreement_target":null}],"constraints":["verb-subject-agreement","direct-object-requires-et"]} -{"frame_id":"he:copular-implicit","language":"he","predicate_type":"copular","slots":[{"name":"subject","required":true,"semantic_role":"carrier","agreement_target":null},{"name":"predicate","required":true,"semantic_role":"attribute","agreement_target":null}],"constraints":["copula-may-be-absent"]} -{"frame_id":"he:construct-chain","language":"he","predicate_type":"genitive-relational","slots":[{"name":"head","required":true,"semantic_role":"possessed","agreement_target":null},{"name":"genitive","required":true,"semantic_role":"possessor-or-qualifier","agreement_target":null}],"constraints":["head-must-be-construct-state","no-article-on-construct-head"]} -{"frame_id":"he:creation-act","language":"he","predicate_type":"creation","slots":[{"name":"creator","required":true,"semantic_role":"divine-agent","agreement_target":"verb"},{"name":"created","required":true,"semantic_role":"patient","agreement_target":null}],"constraints":["bara-qal-subject-must-be-divine"]} +{"frame_id":"he:verbal-basic","language":"he","predicate_type":"verbal","dialogue_role":"assert","slots":[{"name":"subject","required":true,"semantic_role":"agent","agreement_target":"verb"},{"name":"object","required":false,"semantic_role":"patient","agreement_target":null}],"constraints":["verb-subject-agreement","direct-object-requires-et"]} +{"frame_id":"he:copular-implicit","language":"he","predicate_type":"copular","dialogue_role":"elaborate","slots":[{"name":"subject","required":true,"semantic_role":"carrier","agreement_target":null},{"name":"predicate","required":true,"semantic_role":"attribute","agreement_target":null}],"constraints":["copula-may-be-absent"]} +{"frame_id":"he:construct-chain","language":"he","predicate_type":"genitive-relational","dialogue_role":"question","slots":[{"name":"head","required":true,"semantic_role":"possessed","agreement_target":null},{"name":"genitive","required":true,"semantic_role":"possessor-or-qualifier","agreement_target":null}],"constraints":["head-must-be-construct-state","no-article-on-construct-head"]} +{"frame_id":"he:creation-act","language":"he","predicate_type":"creation","dialogue_role":"refute","slots":[{"name":"creator","required":true,"semantic_role":"divine-agent","agreement_target":"verb"},{"name":"created","required":true,"semantic_role":"patient","agreement_target":null}],"constraints":["bara-qal-subject-must-be-divine"]} diff --git a/session/context.py b/session/context.py index 92732b90..c51d028f 100644 --- a/session/context.py +++ b/session/context.py @@ -12,13 +12,18 @@ field that entered the turn. from __future__ import annotations -from field.state import FieldState -from vault.store import VaultStore -from persona.motor import PersonaMotor -from ingest.gate import inject -from generate.stream import generate -from generate.result import GenerationResult +import numpy as np + from algebra.backend import versor_apply +from algebra.cga import outer_product +from field.state import FieldState +from generate.dialogue import DialogueTurn +from generate.proposition import Proposition +from generate.result import GenerationResult +from generate.stream import generate +from ingest.gate import inject +from persona.motor import PersonaMotor +from vault.store import VaultStore class SessionContext: @@ -28,6 +33,8 @@ class SessionContext: self.vault = vault or VaultStore() self.state: FieldState | None = None self.turn: int = 0 + self.dialogue_history: list[DialogueTurn] = [] + self.running_dialogue_blade: np.ndarray | None = None def ingest(self, tokens: list) -> FieldState: """Inject a prompt into the running field. Stores the user field in vault.""" @@ -50,6 +57,28 @@ class SessionContext: self.vault.store(self.state.F, {"turn": self.turn, "role": "user"}) return self.state + def record_dialogue(self, proposition: Proposition) -> DialogueTurn: + """ + Store a proposition as geometric dialogue state. + + The transcript surface is deliberately not used as session memory here; + the retained object is the proposition paired with its relation blade. + """ + blade = proposition.relation + turn = DialogueTurn(proposition=proposition, outer_product_blade=blade) + self.dialogue_history.append(turn) + if self.running_dialogue_blade is None: + self.running_dialogue_blade = blade.copy() + else: + self.running_dialogue_blade = outer_product(self.running_dialogue_blade, blade) + return turn + + @property + def last_dialogue_blade(self) -> np.ndarray | None: + if not self.dialogue_history: + return None + return self.dialogue_history[-1].outer_product_blade.copy() + def respond(self, max_tokens: int = 128) -> GenerationResult: """ Generate a response from current state and preserve the evolved field. diff --git a/tests/test_dialogue.py b/tests/test_dialogue.py new file mode 100644 index 00000000..ba34b534 --- /dev/null +++ b/tests/test_dialogue.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import numpy as np + +from generate.dialogue import ( + blade_alignment, + classify_dialogue_blade, + propose_dialogue, +) +from generate.proposition import FrameRegistry +from language_packs.compiler import load_mounted_packs +from session.context import SessionContext + + +def _dialogue_proposition(tokens: list[str], registry: FrameRegistry, vocab, reference=None): + session = SessionContext(vocab=vocab) + session.ingest(tokens) + generated = session.respond(max_tokens=3) + return propose_dialogue(generated.final_state, None, vocab, registry, reference) + + +def test_relation_blade_classifies_parallel_as_elaboration(): + vocab = load_mounted_packs( + ("en_minimal_v1", "he_logos_micro_v1", "grc_logos_micro_v1") + ) + registry = FrameRegistry.from_pack("grc", vocab) + + first = _dialogue_proposition(["light", "φῶς"], registry, vocab) + second = _dialogue_proposition(["φῶς", "אוֹר"], registry, vocab, first.relation) + + assert classify_dialogue_blade(second.relation, first.relation) == "elaborate" + assert second.frame_id == "el:colwell-construction" + assert second.surface != first.surface + + +def test_two_turn_light_exchange_tracks_parallel_dialogue_trajectory(): + vocab = load_mounted_packs( + ("en_minimal_v1", "he_logos_micro_v1", "grc_logos_micro_v1") + ) + registry = FrameRegistry.from_pack("grc", vocab) + session = SessionContext(vocab=vocab) + + session.ingest(["light", "φῶς"]) + first_generated = session.respond(max_tokens=3) + first = propose_dialogue(first_generated.final_state, None, vocab, registry) + first_turn = session.record_dialogue(first) + + session.ingest(["light", "truth"]) + second_generated = session.respond(max_tokens=3) + second = propose_dialogue( + second_generated.final_state, + None, + vocab, + registry, + session.last_dialogue_blade, + ) + second_turn = session.record_dialogue(second) + + random_turn = _dialogue_proposition(["truth", "λόγος"], registry, vocab) + second_alignment = blade_alignment( + second_turn.outer_product_blade, + first_turn.outer_product_blade, + ) + random_alignment = blade_alignment(random_turn.relation, first_turn.outer_product_blade) + + assert second.frame_id == "el:colwell-construction" + assert second.surface != first.surface + assert second_alignment > random_alignment + assert second_alignment > 0.35 + assert len(session.dialogue_history) == 2 + assert session.running_dialogue_blade is not None + assert np.linalg.norm(session.running_dialogue_blade) > 0.0