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:
parent
8dcc26581a
commit
c68b0734a2
4 changed files with 129 additions and 8 deletions
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue