core/tests/test_intent_proposition_graph.py
Shay 8dcc26581a feat: add intent-proposition graph comprehension layer
Implements the dialogue understanding pipeline:
  prompt -> dialogue intent -> proposition graph -> articulation target

New modules:
  - generate/intent.py: rule-based classifier (7 intent tags + UNKNOWN)
  - generate/graph_planner.py: immutable PropositionGraph DAG, topological
    walk to ArticulationTarget with rhetorical moves

Tests cover definition, cause, comparison, correction with prior-turn
linking, and deterministic serialization.
2026-05-14 19:52:57 -07:00

87 lines
2.5 KiB
Python

"""Tests for the intent -> proposition graph -> articulation target pipeline."""
from __future__ import annotations
import json
from generate.graph_planner import (
Relation,
RhetoricalMove,
graph_from_intent,
plan_articulation,
)
from generate.intent import IntentTag, classify_intent
def test_what_is_definition_intent() -> None:
intent = classify_intent("What is a multivector?")
assert intent.tag is IntentTag.DEFINITION
assert "multivector" in intent.subject.lower()
graph = graph_from_intent(intent)
assert len(graph.nodes) == 1
assert graph.nodes[0].predicate == "is_defined_as"
assert graph.nodes[0].source_intent is IntentTag.DEFINITION
def test_why_cause_intent() -> None:
intent = classify_intent("Why does the field diverge?")
assert intent.tag is IntentTag.CAUSE
assert "field" in intent.subject.lower()
graph = graph_from_intent(intent)
assert len(graph.nodes) == 1
assert graph.nodes[0].predicate == "is_caused_by"
def test_compare_intent() -> None:
intent = classify_intent("Compare MLX and PyTorch")
assert intent.tag is IntentTag.COMPARISON
assert intent.subject.lower() == "mlx"
assert intent.secondary_subject is not None
assert intent.secondary_subject.lower() == "pytorch"
graph = graph_from_intent(intent)
assert len(graph.nodes) == 2
assert len(graph.edges) == 1
assert graph.edges[0].relation is Relation.CONTRAST
target = plan_articulation(graph)
assert target.source_intent is IntentTag.COMPARISON
moves = [s.move for s in target.steps]
assert RhetoricalMove.CONTRAST in moves
def test_correction_intent_links_prior_turn() -> None:
intent = classify_intent("No, that's wrong — it should be grade 2")
assert intent.tag is IntentTag.CORRECTION
assert intent.requires_prior_turn()
prior_id = "prev_p0"
graph = graph_from_intent(intent, prior_node_id=prior_id)
assert len(graph.nodes) == 1
assert graph.nodes[0].predicate == "corrects"
assert graph.nodes[0].obj == prior_id
assert len(graph.edges) == 1
assert graph.edges[0].relation is Relation.CORRECTION
assert graph.edges[0].target == prior_id
def test_graph_serialization_is_deterministic() -> None:
intent = classify_intent("Compare cats and dogs")
graph = graph_from_intent(intent)
json_a = graph.to_json()
json_b = graph.to_json()
assert json_a == json_b
parsed = json.loads(json_a)
assert "nodes" in parsed
assert "edges" in parsed
assert len(parsed["nodes"]) == 2
assert len(parsed["edges"]) == 1