From a7febd48ef51fba088e7242f7bba8c3442c9b76f Mon Sep 17 00:00:00 2001 From: Shay Date: Fri, 15 May 2026 07:08:37 -0700 Subject: [PATCH] Integrate semantic realizer into cognition pipeline - add intent-aware semantic templates for seed-pack relation predicates - add semantic realization path for ArticulationTarget outputs - wire semantic realization into CognitiveTurnPipeline results without changing ChatRuntime.chat - expand cognition CLI suite coverage for semantic realizer integration - add focused tests for deterministic semantic surfaces and response contract stability --- core/cli.py | 1 + core/cognition/pipeline.py | 25 ++- generate/realizer.py | 62 ++++++ generate/semantic_templates.py | 73 +++++++ generate/templates.py | 10 + tests/test_semantic_realizer_integration.py | 222 ++++++++++++++++++++ 6 files changed, 387 insertions(+), 6 deletions(-) create mode 100644 generate/semantic_templates.py create mode 100644 tests/test_semantic_realizer_integration.py diff --git a/core/cli.py b/core/cli.py index 1be54228..fb36b711 100644 --- a/core/cli.py +++ b/core/cli.py @@ -43,6 +43,7 @@ _TEST_SUITES: dict[str, tuple[str, ...]] = { "tests/test_intent_proposition_graph.py", "tests/test_cognitive_turn_pipeline.py", "tests/test_articulation_realizer_v2.py", + "tests/test_semantic_realizer_integration.py", ), "teaching": ( "tests/test_reviewed_teaching_loop.py", diff --git a/core/cognition/pipeline.py b/core/cognition/pipeline.py index 490ad71e..5f53e444 100644 --- a/core/cognition/pipeline.py +++ b/core/cognition/pipeline.py @@ -20,6 +20,7 @@ from core.cognition.result import CognitiveTurnResult from core.cognition.trace import compute_trace_hash from generate.intent import classify_intent from generate.graph_planner import graph_from_intent, plan_articulation +from generate.realizer import realize_semantic from teaching.correction import CorrectionCandidate, extract_correction from teaching.review import ReviewedTeachingExample, review_correction from teaching.store import PackMutationProposal, TeachingStore @@ -55,11 +56,23 @@ class CognitiveTurnPipeline: graph = graph_from_intent(intent, prior_node_id=prior_node_id) target = plan_articulation(graph) + # 1c. REALIZE — semantic realization from graph + intent + realized_plan = realize_semantic(target, graph) + # 2–7. INGEST / UNDERSTAND / RECALL / THINK / ARTICULATE / LEARN - # Delegated to ChatRuntime.chat() in Phase 1. + # Delegated to ChatRuntime.chat(). # ChatResponse is the stable contract surface. response = self.runtime.chat(text, max_tokens=max_tokens) + # Override surfaces when semantic realizer produced a result. + # The ChatResponse contract fields are preserved; we select + # the better articulation surface from the semantic path. + surface = response.surface + articulation_surface = response.articulation_surface + if realized_plan.surface: + surface = realized_plan.surface + articulation_surface = realized_plan.surface + # Track last node id for correction-intent chaining if graph.nodes: self._last_node_id = graph.nodes[-1].node_id @@ -84,7 +97,7 @@ class CognitiveTurnPipeline: # Advance turn counter and remember surface for next correction binding self._turn_number += 1 - self._prior_surface = response.surface + self._prior_surface = surface # 11. TRACE — deterministic hash (includes teaching IDs when present) review_hash = reviewed_example.review_hash if reviewed_example is not None else "" @@ -92,9 +105,9 @@ class CognitiveTurnPipeline: trace_hash = compute_trace_hash( input_text=text, filtered_tokens=filtered_tokens, - surface=response.surface, + surface=surface, walk_surface=response.walk_surface, - articulation_surface=response.articulation_surface, + articulation_surface=articulation_surface, dialogue_role=str(response.dialogue_role), versor_condition=response.versor_condition, vault_hits=response.vault_hits, @@ -111,9 +124,9 @@ class CognitiveTurnPipeline: field_state_after=field_state_after, proposition=response.proposition, articulation=response.articulation, - surface=response.surface, + surface=surface, walk_surface=response.walk_surface, - articulation_surface=response.articulation_surface, + articulation_surface=articulation_surface, dialogue_role=response.dialogue_role, identity_score=response.identity_score, vault_hits=response.vault_hits, diff --git a/generate/realizer.py b/generate/realizer.py index a91b828c..f1dea678 100644 --- a/generate/realizer.py +++ b/generate/realizer.py @@ -22,6 +22,7 @@ from generate.graph_planner import ( RhetoricalMove, ) from generate.intent import IntentTag +from generate.semantic_templates import render_semantic from generate.templates import render_step @@ -51,6 +52,67 @@ class RealizedPlan: } +def realize_semantic( + target: ArticulationTarget, + graph: PropositionGraph | None = None, +) -> RealizedPlan: + """Realize using intent-aware semantic templates. + + Uses the source intent to select a template that produces structurally + better surfaces (e.g. "X is defined as Y" for definition intents) + rather than the generic rhetorical-move templates. + + Returns an empty RealizedPlan for empty/None targets so the caller + can fall back to the older articulation path. + """ + if target is None or not target.steps: + return RealizedPlan(fragments=(), surface="") + + intent = target.source_intent + fragments: list[RealizedFragment] = [] + + if intent is IntentTag.COMPARISON and len(target.steps) >= 2: + step_a = target.steps[0] + step_b = target.steps[1] + obj_a = _resolve_obj(step_a, graph) + secondary = step_b.subject if step_b.subject != step_a.subject else obj_a + surface = render_semantic( + intent=intent, + subject=step_a.subject, + predicate=step_a.predicate, + obj=obj_a, + secondary=secondary, + ) + fragments.append(RealizedFragment( + node_id=step_a.node_id, + move=RhetoricalMove.CONTRAST, + surface=surface, + )) + else: + for step in target.steps: + obj = _resolve_obj(step, graph) + surface = render_semantic( + intent=intent, + subject=step.subject, + predicate=step.predicate, + obj=obj, + ) + move = step.move + if move is RhetoricalMove.ASSERT and intent is IntentTag.CORRECTION: + move = RhetoricalMove.CORRECT + fragments.append(RealizedFragment( + node_id=step.node_id, + move=move, + surface=surface, + )) + + joined = ". ".join(f.surface for f in fragments) + if joined and not joined.endswith("."): + joined += "." + + return RealizedPlan(fragments=tuple(fragments), surface=joined) + + def _resolve_obj(step: ArticulationStep, graph: PropositionGraph | None) -> str: """Look up the object slot from the graph node matching this step.""" if graph is None: diff --git a/generate/semantic_templates.py b/generate/semantic_templates.py new file mode 100644 index 00000000..11ee5678 --- /dev/null +++ b/generate/semantic_templates.py @@ -0,0 +1,73 @@ +"""Intent-aware semantic templates for the realizer. + +Maps (IntentTag, relation_predicate) pairs to deterministic surface +templates that use the seed pack's relation predicates (defines, means, +grounds, supports, contrasts_with, corrects). + +Design constraints: + - No LLM fallback + - No random template selection + - Deterministic: same (intent, predicate, subject, object) -> same surface + - Uses seed pack vocabulary directly +""" + +from __future__ import annotations + +from generate.intent import IntentTag + + +_INTENT_TEMPLATES: dict[IntentTag, str] = { + IntentTag.DEFINITION: "{subject} is defined as {obj}", + IntentTag.CAUSE: "{subject} is caused by {obj}", + IntentTag.PROCEDURE: "{subject} has the following steps: {obj}", + IntentTag.COMPARISON: "{subject} and {secondary} are contrasted by {predicate_h}", + IntentTag.CORRECTION: "correction: {subject} {predicate_h} {obj}", + IntentTag.RECALL: "{subject} recalls {obj}", + IntentTag.VERIFICATION: "{subject} is verified as {obj}", + IntentTag.UNKNOWN: "{subject} {predicate_h} {obj}", +} + +_PREDICATE_HUMANIZE: dict[str, str] = { + "is_defined_as": "is defined as", + "is_caused_by": "is caused by", + "has_steps": "has the following steps", + "contrasts_with": "contrasts with", + "corrects": "corrects", + "recalls": "recalls", + "is_verified_as": "is verified as", + "addresses": "addresses", + "defines": "defines", + "means": "means", + "grounds": "grounds", + "supports": "supports", + "causes": "causes", + "reveals": "reveals", + "precedes": "precedes", + "follows": "follows", + "belongs_to": "belongs to", + "answers": "answers", +} + + +def humanize_predicate(predicate: str) -> str: + return _PREDICATE_HUMANIZE.get(predicate, predicate.replace("_", " ")) + + +def render_semantic( + intent: IntentTag, + subject: str, + predicate: str, + obj: str, + secondary: str | None = None, +) -> str: + """Render a semantic surface from intent, subject, predicate, and object.""" + template = _INTENT_TEMPLATES.get(intent, _INTENT_TEMPLATES[IntentTag.UNKNOWN]) + predicate_h = humanize_predicate(predicate) + obj_display = obj if obj not in ("", "") else "..." + + return template.format( + subject=subject, + predicate_h=predicate_h, + obj=obj_display, + secondary=secondary or obj_display, + ) diff --git a/generate/templates.py b/generate/templates.py index 2b11c984..bbdf09e1 100644 --- a/generate/templates.py +++ b/generate/templates.py @@ -24,6 +24,16 @@ _PREDICATE_DISPLAY: dict[str, str] = { "recalls": "recalls", "is_verified_as": "is verified as", "addresses": "addresses", + "defines": "defines", + "means": "means", + "grounds": "grounds", + "supports": "supports", + "causes": "causes", + "reveals": "reveals", + "precedes": "precedes", + "follows": "follows", + "belongs_to": "belongs to", + "answers": "answers", } diff --git a/tests/test_semantic_realizer_integration.py b/tests/test_semantic_realizer_integration.py new file mode 100644 index 00000000..afa352b0 --- /dev/null +++ b/tests/test_semantic_realizer_integration.py @@ -0,0 +1,222 @@ +"""Tests for semantic realizer integration into the cognitive pipeline. + +Verifies that the semantic realizer produces structurally better surfaces +from intent + proposition graph, and that the ChatResponse contract holds. +""" + +from __future__ import annotations + +import pytest + +from generate.intent import IntentTag, classify_intent +from generate.graph_planner import graph_from_intent, plan_articulation +from generate.realizer import realize_semantic, realize_target, RealizedPlan +from generate.semantic_templates import humanize_predicate, render_semantic + + +# --------------------------------------------------------------------------- +# Unit tests: semantic_templates +# --------------------------------------------------------------------------- + +class TestSemanticTemplates: + def test_humanize_known_predicate(self) -> None: + assert humanize_predicate("is_defined_as") == "is defined as" + assert humanize_predicate("contrasts_with") == "contrasts with" + assert humanize_predicate("defines") == "defines" + assert humanize_predicate("means") == "means" + assert humanize_predicate("grounds") == "grounds" + assert humanize_predicate("supports") == "supports" + assert humanize_predicate("corrects") == "corrects" + + def test_humanize_unknown_predicate_uses_underscore_replacement(self) -> None: + assert humanize_predicate("some_new_predicate") == "some new predicate" + + def test_render_definition(self) -> None: + surface = render_semantic( + intent=IntentTag.DEFINITION, + subject="truth", + predicate="is_defined_as", + obj="coherence", + ) + assert "truth" in surface + assert "is defined as" in surface + assert "coherence" in surface + + def test_render_comparison(self) -> None: + surface = render_semantic( + intent=IntentTag.COMPARISON, + subject="truth", + predicate="contrasts_with", + obj="light", + secondary="light", + ) + assert "truth" in surface + assert "light" in surface + + def test_render_correction(self) -> None: + surface = render_semantic( + intent=IntentTag.CORRECTION, + subject="correction", + predicate="corrects", + obj="reviewed repair", + ) + assert "correction" in surface.lower() + + def test_pending_obj_displays_as_ellipsis(self) -> None: + surface = render_semantic( + intent=IntentTag.DEFINITION, + subject="truth", + predicate="is_defined_as", + obj="", + ) + assert "" not in surface + assert "..." in surface + + +# --------------------------------------------------------------------------- +# Unit tests: realize_semantic +# --------------------------------------------------------------------------- + +class TestRealizeSemantic: + def test_definition_prompt_uses_semantic_realizer(self) -> None: + intent = classify_intent("What is truth?") + assert intent.tag is IntentTag.DEFINITION + graph = graph_from_intent(intent) + target = plan_articulation(graph) + plan = realize_semantic(target, graph) + assert isinstance(plan, RealizedPlan) + assert plan.surface + assert "truth" in plan.surface.lower() + assert "is defined as" in plan.surface.lower() + + def test_comparison_prompt_mentions_both_terms(self) -> None: + intent = classify_intent("Compare truth and light") + assert intent.tag is IntentTag.COMPARISON + graph = graph_from_intent(intent) + target = plan_articulation(graph) + plan = realize_semantic(target, graph) + assert plan.surface + assert "truth" in plan.surface.lower() + assert "light" in plan.surface.lower() + + def test_correction_prompt_uses_correction_template(self) -> None: + intent = classify_intent("No, correction means reviewed repair") + assert intent.tag is IntentTag.CORRECTION + graph = graph_from_intent(intent) + target = plan_articulation(graph) + plan = realize_semantic(target, graph) + assert plan.surface + assert "correction" in plan.surface.lower() + + def test_cause_prompt(self) -> None: + intent = classify_intent("Why does light exist?") + assert intent.tag is IntentTag.CAUSE + graph = graph_from_intent(intent) + target = plan_articulation(graph) + plan = realize_semantic(target, graph) + assert plan.surface + assert "is caused by" in plan.surface.lower() + + def test_empty_target_returns_empty_plan(self) -> None: + from generate.graph_planner import ArticulationTarget + plan = realize_semantic( + ArticulationTarget(steps=(), source_intent=IntentTag.UNKNOWN), + ) + assert plan.surface == "" + assert plan.fragments == () + + def test_none_target_returns_empty_plan(self) -> None: + plan = realize_semantic(None) + assert plan.surface == "" + + def test_seed_relation_predicates_humanize_deterministically(self) -> None: + seed_predicates = [ + "defines", "means", "grounds", "supports", + "contrasts_with", "corrects", "causes", "reveals", + "precedes", "follows", "belongs_to", "answers", + ] + for pred in seed_predicates: + h = humanize_predicate(pred) + assert "_" not in h, f"{pred} humanized to {h!r} still has underscores" + assert h == humanize_predicate(pred), f"{pred} not deterministic" + + +# --------------------------------------------------------------------------- +# Integration: realize_semantic vs realize_target produce valid plans +# --------------------------------------------------------------------------- + +class TestSemanticVsRhetoricalRealization: + @pytest.mark.parametrize("prompt,expected_intent", [ + ("What is truth?", IntentTag.DEFINITION), + ("Compare truth and light", IntentTag.COMPARISON), + ("Why does light exist?", IntentTag.CAUSE), + ("No, that's wrong", IntentTag.CORRECTION), + ]) + def test_both_realizers_produce_nonempty_surface( + self, prompt: str, expected_intent: IntentTag, + ) -> None: + intent = classify_intent(prompt) + assert intent.tag is expected_intent + graph = graph_from_intent(intent) + target = plan_articulation(graph) + + rhetorical = realize_target(target, graph) + semantic = realize_semantic(target, graph) + + assert rhetorical.surface, f"rhetorical plan empty for {prompt!r}" + assert semantic.surface, f"semantic plan empty for {prompt!r}" + + def test_semantic_surfaces_are_deterministic(self) -> None: + prompt = "What is truth?" + results = set() + for _ in range(5): + intent = classify_intent(prompt) + graph = graph_from_intent(intent) + target = plan_articulation(graph) + plan = realize_semantic(target, graph) + results.add(plan.surface) + assert len(results) == 1, f"Non-deterministic: {results}" + + +# --------------------------------------------------------------------------- +# Contract: ChatResponse shape still holds through the pipeline +# --------------------------------------------------------------------------- + +class TestChatResponseContractStillHolds: + def test_chat_response_has_required_fields(self) -> None: + try: + from chat.runtime import ChatRuntime, ChatResponse + except Exception: + pytest.skip("ChatRuntime not importable in this environment") + + runtime = ChatRuntime() + response = runtime.chat("What is truth?") + assert isinstance(response, ChatResponse) + assert isinstance(response.surface, str) + assert response.surface + assert isinstance(response.versor_condition, float) + assert response.versor_condition < 1e-6 + assert response.proposition is not None + assert response.articulation is not None + assert isinstance(response.articulation_surface, str) + assert isinstance(response.walk_surface, str) + assert isinstance(response.dialogue_role, str) + assert isinstance(response.vault_hits, int) + + def test_pipeline_result_uses_semantic_surface(self) -> None: + try: + from chat.runtime import ChatRuntime + from core.cognition.pipeline import CognitiveTurnPipeline + except Exception: + pytest.skip("ChatRuntime not importable in this environment") + + runtime = ChatRuntime() + pipeline = CognitiveTurnPipeline(runtime) + result = pipeline.run("What is truth?") + + assert result.surface + assert "truth" in result.surface.lower() + assert "is defined as" in result.surface.lower() + assert result.articulation_surface == result.surface + assert result.versor_condition < 1e-6 + assert result.trace_hash