From c68b0734a227727707791092d1913e653f46123e Mon Sep 17 00:00:00 2001 From: Shay Date: Thu, 14 May 2026 20:05:00 -0700 Subject: [PATCH] Wire intent graph into cognitive pipeline - add intent, proposition graph, and articulation target to CognitiveTurnResult - compute classify_intent -> graph_from_intent -> plan_articulation in CognitiveTurnPipeline - include intent_tag in deterministic trace hash payload - add pipeline tests for definition/comparison graph capture, articulation target exposure, trace hash changes, and ChatResponse contract stability --- core/cognition/pipeline.py | 17 +++++ core/cognition/result.py | 11 ++- core/cognition/trace.py | 4 + tests/test_cognitive_turn_pipeline.py | 105 ++++++++++++++++++++++++-- 4 files changed, 129 insertions(+), 8 deletions(-) diff --git a/core/cognition/pipeline.py b/core/cognition/pipeline.py index 87e43927..236c0841 100644 --- a/core/cognition/pipeline.py +++ b/core/cognition/pipeline.py @@ -18,6 +18,8 @@ from __future__ import annotations from field.state import FieldState 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 class CognitiveTurnPipeline: @@ -29,6 +31,7 @@ class CognitiveTurnPipeline: def __init__(self, runtime) -> None: # runtime: ChatRuntime (no import cycle) self.runtime = runtime + self._last_node_id: str | None = None # ------------------------------------------------------------------ # Public API @@ -40,11 +43,21 @@ class CognitiveTurnPipeline: # 1. LISTEN — capture pre-turn field state field_state_before: FieldState | None = self._capture_field_state() + # 1b. CLASSIFY — intent and proposition graph (deterministic, pre-chat) + intent = classify_intent(text) + prior_node_id = self._last_node_id + graph = graph_from_intent(intent, prior_node_id=prior_node_id) + target = plan_articulation(graph) + # 2–7. INGEST / UNDERSTAND / RECALL / THINK / ARTICULATE / LEARN # Delegated to ChatRuntime.chat() in Phase 1. # ChatResponse is the stable contract surface. response = self.runtime.chat(text, max_tokens=max_tokens) + # Track last node id for correction-intent chaining + if graph.nodes: + self._last_node_id = graph.nodes[-1].node_id + # 8. CAPTURE post-turn field state field_state_after: FieldState = self.runtime.session.state @@ -69,6 +82,7 @@ class CognitiveTurnPipeline: dialogue_role=str(response.dialogue_role), versor_condition=response.versor_condition, vault_hits=response.vault_hits, + intent_tag=intent.tag.value, ) return CognitiveTurnResult( @@ -85,6 +99,9 @@ class CognitiveTurnPipeline: dialogue_role=response.dialogue_role, identity_score=response.identity_score, vault_hits=response.vault_hits, + intent=intent, + proposition_graph=graph, + articulation_target=target, versor_condition=response.versor_condition, trace_hash=trace_hash, ) diff --git a/core/cognition/result.py b/core/cognition/result.py index cb50f5b9..3cc85b5c 100644 --- a/core/cognition/result.py +++ b/core/cognition/result.py @@ -13,6 +13,8 @@ from dataclasses import dataclass from field.state import FieldState from generate.articulation import ArticulationPlan from generate.dialogue import DialogueRole +from generate.graph_planner import ArticulationTarget, PropositionGraph +from generate.intent import DialogueIntent from generate.proposition import Proposition from core.physics.identity import IdentityScore @@ -48,6 +50,11 @@ class CognitiveTurnResult: # --- vault / memory --- vault_hits: int + # --- intent / graph telemetry --- + intent: DialogueIntent | None = None + proposition_graph: PropositionGraph | None = None + articulation_target: ArticulationTarget | None = None + # --- invariant bookkeeping --- - versor_condition: float # must be < 1e-6 - trace_hash: str # SHA-256 over deterministic key fields + versor_condition: float = 0.0 # must be < 1e-6 + trace_hash: str = "" # SHA-256 over deterministic key fields diff --git a/core/cognition/trace.py b/core/cognition/trace.py index 7470b1a9..1b7a73da 100644 --- a/core/cognition/trace.py +++ b/core/cognition/trace.py @@ -33,6 +33,7 @@ def compute_trace_hash( dialogue_role: str, versor_condition: float, vault_hits: int, + intent_tag: str = "unknown", ) -> str: """Return a deterministic SHA-256 hex digest over the turn's key outputs. @@ -48,6 +49,7 @@ def compute_trace_hash( "dialogue_role": str(dialogue_role), "versor_condition": _round_float(versor_condition), "vault_hits": int(vault_hits), + "intent_tag": intent_tag, } serialized = json.dumps(payload, sort_keys=True, ensure_ascii=False) return hashlib.sha256(serialized.encode("utf-8")).hexdigest() @@ -55,6 +57,7 @@ def compute_trace_hash( def trace_hash_from_result(result: "CognitiveTurnResult") -> str: """Convenience wrapper — compute the hash directly from a result object.""" + intent_tag = result.intent.tag.value if result.intent is not None else "unknown" return compute_trace_hash( input_text=result.input_text, filtered_tokens=result.filtered_tokens, @@ -64,4 +67,5 @@ def trace_hash_from_result(result: "CognitiveTurnResult") -> str: dialogue_role=str(result.dialogue_role), versor_condition=result.versor_condition, vault_hits=result.vault_hits, + intent_tag=intent_tag, ) diff --git a/tests/test_cognitive_turn_pipeline.py b/tests/test_cognitive_turn_pipeline.py index cade91c2..e82bdb08 100644 --- a/tests/test_cognitive_turn_pipeline.py +++ b/tests/test_cognitive_turn_pipeline.py @@ -1,12 +1,8 @@ """ Tests for CognitiveTurnPipeline — the cognitive spine. -Five tests, no micro-test explosion: - 1. test_pipeline_known_token_turn — happy-path turn with known tokens - 2. test_pipeline_unknown_token_grounding — OOV token handled; field still valid - 3. test_pipeline_two_turn_memory_continuity — field evolves across turns - 4. test_pipeline_trace_hash_deterministic — identical inputs → identical hash - 5. test_pipeline_preserves_versor_closure — versor_condition < 1e-6 per turn +Tests 1-5: original pipeline contract tests. +Tests 6-10: intent-proposition graph wiring tests. """ from __future__ import annotations @@ -17,6 +13,8 @@ import pytest from chat.runtime import ChatRuntime from core.cognition import CognitiveTurnPipeline, CognitiveTurnResult from core.cognition.trace import trace_hash_from_result +from generate.intent import IntentTag +from generate.graph_planner import RhetoricalMove # --------------------------------------------------------------------------- @@ -147,3 +145,98 @@ def test_pipeline_preserves_versor_closure(pipeline: CognitiveTurnPipeline) -> N ) # Field state invariant: shape must be intact assert result.field_state_after.F.shape == (32,) + + +# --------------------------------------------------------------------------- +# 6. Definition intent recorded +# --------------------------------------------------------------------------- + +def test_pipeline_records_definition_intent(pipeline: CognitiveTurnPipeline) -> None: + """A 'what is' prompt should produce a DEFINITION intent in the result.""" + result = pipeline.run("what is light", max_tokens=6) + + assert result.intent is not None + assert result.intent.tag is IntentTag.DEFINITION + assert "light" in result.intent.subject.lower() + + assert result.proposition_graph is not None + assert len(result.proposition_graph.nodes) == 1 + assert result.proposition_graph.nodes[0].predicate == "is_defined_as" + + assert result.articulation_target is not None + assert len(result.articulation_target.steps) == 1 + assert result.articulation_target.source_intent is IntentTag.DEFINITION + + +# --------------------------------------------------------------------------- +# 7. Comparison graph recorded +# --------------------------------------------------------------------------- + +def test_pipeline_records_comparison_graph(pipeline: CognitiveTurnPipeline) -> None: + """A comparison prompt produces a 2-node graph with a CONTRAST edge.""" + result = pipeline.run("compare light and truth", max_tokens=6) + + assert result.intent is not None + assert result.intent.tag is IntentTag.COMPARISON + + graph = result.proposition_graph + assert graph is not None + assert len(graph.nodes) == 2 + assert len(graph.edges) == 1 + assert graph.edges[0].relation.value == "contrast" + + target = result.articulation_target + assert target is not None + moves = [s.move for s in target.steps] + assert RhetoricalMove.CONTRAST in moves + + +# --------------------------------------------------------------------------- +# 8. Articulation target recorded +# --------------------------------------------------------------------------- + +def test_pipeline_records_articulation_target(pipeline: CognitiveTurnPipeline) -> None: + """Every turn produces an ArticulationTarget with at least one step.""" + result = pipeline.run("logos truth", max_tokens=6) + + assert result.articulation_target is not None + assert len(result.articulation_target.steps) >= 1 + step = result.articulation_target.steps[0] + assert step.move is RhetoricalMove.ASSERT + assert step.node_id == "p0" + + +# --------------------------------------------------------------------------- +# 9. Trace hash changes with intent +# --------------------------------------------------------------------------- + +def test_pipeline_trace_hash_changes_with_intent() -> None: + """Different intent classifications produce different trace hashes.""" + rt1 = ChatRuntime() + rt2 = ChatRuntime() + + r1 = CognitiveTurnPipeline(rt1).run("what is light", max_tokens=6) + r2 = CognitiveTurnPipeline(rt2).run("why light", max_tokens=6) + + assert r1.intent.tag is IntentTag.DEFINITION + assert r2.intent.tag is IntentTag.CAUSE + assert r1.trace_hash != r2.trace_hash + + +# --------------------------------------------------------------------------- +# 10. ChatResponse contract unchanged +# --------------------------------------------------------------------------- + +def test_pipeline_chat_response_contract_unchanged(pipeline: CognitiveTurnPipeline) -> None: + """Adding intent fields must not break the existing ChatResponse contract.""" + result = pipeline.run("light logos", max_tokens=8) + + assert isinstance(result.surface, str) and result.surface.strip() + assert isinstance(result.walk_surface, str) + assert isinstance(result.articulation_surface, str) + assert result.dialogue_role in {"assert", "elaborate", "question", "refute"} + assert isinstance(result.versor_condition, float) + assert isinstance(result.trace_hash, str) and len(result.trace_hash) == 64 + assert isinstance(result.vault_hits, int) + assert result.proposition is not None + assert result.articulation is not None