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
This commit is contained in:
Shay 2026-05-14 20:05:00 -07:00 committed by GitHub
parent 8dcc26581a
commit c68b0734a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 129 additions and 8 deletions

View file

@ -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)
# 27. 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,
)

View file

@ -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

View file

@ -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,
)

View file

@ -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