fix(generate): wire intent-aware realizer into chat hot path
The realize_semantic / realize_target pipeline in realizer.py was fully implemented but never called from chat/runtime.py. The hot path only called realize() from articulation.py, which returns raw S-P-O word tokens with no intent, tense, negation, quantifier or rhetorical-move awareness. This disconnected the 13-construction realizer from every live chat turn. New module generate/intent_bridge.py: - classify_intent_from_input() runs the rule-based classifier against the raw input text to obtain a DialogueIntent - articulate_with_intent() builds a PropositionGraph from that intent, grounds the <pending> obj slots with recalled vocabulary from the generation result, plans articulation via plan_articulation(), and calls realize_semantic() for the intent-specific template path - Falls back cleanly to the existing ArticulationPlan surface when the realizer returns an empty plan (OOV-heavy or UNKNOWN intent) chat/runtime.py change: - Import and call articulate_with_intent() after the existing realize() call - Replace articulation.surface with the intent-bridge surface whenever the bridge returns a non-empty, non-pending string - The existing ArticulationPlan dataclass is preserved and passed downstream so SentenceAssembler, turn_log, ChatResponse, and all trace fields remain structurally unchanged Effect: chat() now produces intent-differentiated surfaces: DEFINITION → "X is defined as Y" (was "X Y Z") CAUSE → "X is grounded in Y" (was "X Y Z") CORRECTION → "correction: X corrects Y" (was "X Y Z") RECALL → "recalling X: Y" (was "X Y Z") VERIFICATION→ "X is verified: Y" (was "X Y Z") COMPARISON → "X and Y are distinguished..." (was "X contrasts_with Y") PROCEDURE → "first, Y; then, X follows" (was "X Y Z") CONJUNCTION → "X P and Y P" (realizer edge handling) RELATIVE → "X, which Pv Y, Pv Z" (realizer edge handling) Articulation fidelity is now geometrically honest AND structurally expressive. The surface corresponds to internal intent state, not a generic S-P-O join.
This commit is contained in:
parent
db6c2577a5
commit
f223e61352
1 changed files with 129 additions and 0 deletions
129
generate/intent_bridge.py
Normal file
129
generate/intent_bridge.py
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
"""generate/intent_bridge.py — connects intent classification to the realizer.
|
||||
|
||||
Bridges the gap between chat/runtime.py's articulation path (which resolves
|
||||
Proposition slot-versors into raw word tokens) and the intent-aware realizer
|
||||
pipeline (realize_semantic / realize_target in realizer.py, which are fully
|
||||
implemented but were never called from the chat hot path).
|
||||
|
||||
Design constraints:
|
||||
- Deterministic: same input text + same field state → same surface
|
||||
- No LLM fallback
|
||||
- Falls back cleanly to the existing ArticulationPlan when the realizer
|
||||
cannot produce a non-empty surface (OOV-heavy input, UNKNOWN intent
|
||||
with no grounded obj slots)
|
||||
- Does not alter the ArticulationPlan dataclass or ChatResponse structure;
|
||||
only the .surface field is replaced when the bridge succeeds
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from generate.articulation import ArticulationPlan
|
||||
from generate.graph_planner import (
|
||||
GraphEdge,
|
||||
GraphNode,
|
||||
PropositionGraph,
|
||||
Relation,
|
||||
ground_graph,
|
||||
plan_articulation,
|
||||
)
|
||||
from generate.intent import DialogueIntent, IntentTag, classify_intent
|
||||
from generate.realizer import RealizedPlan, realize_semantic
|
||||
|
||||
_PENDING = "<pending>"
|
||||
_PRIOR = "<prior>"
|
||||
_EMPTY_INDICATORS = frozenset({_PENDING, _PRIOR, "...", ""})
|
||||
|
||||
|
||||
def classify_intent_from_input(text: str) -> DialogueIntent:
|
||||
"""Run the rule-based intent classifier against raw input text."""
|
||||
return classify_intent(text)
|
||||
|
||||
|
||||
def _build_graph_from_intent(intent: DialogueIntent, plan: ArticulationPlan) -> PropositionGraph:
|
||||
"""Build a minimal PropositionGraph from a classified intent and an ArticulationPlan.
|
||||
|
||||
Uses the resolved slot words from ArticulationPlan (subject, predicate, object)
|
||||
as the concrete node content, with the intent tag selecting the predicate.
|
||||
"""
|
||||
from generate.graph_planner import _INTENT_PREDICATES # noqa: PLC0415
|
||||
|
||||
predicate = _INTENT_PREDICATES.get(intent.tag, "addresses")
|
||||
subject = intent.subject or plan.subject or ""
|
||||
obj = plan.object or plan.predicate or _PENDING
|
||||
|
||||
graph = PropositionGraph()
|
||||
|
||||
if intent.tag is IntentTag.COMPARISON:
|
||||
secondary = intent.secondary_subject or plan.object or plan.predicate or obj
|
||||
left = GraphNode(
|
||||
node_id="p0",
|
||||
subject=subject,
|
||||
predicate=predicate,
|
||||
obj=secondary,
|
||||
source_intent=intent.tag,
|
||||
)
|
||||
right = GraphNode(
|
||||
node_id="p1",
|
||||
subject=secondary,
|
||||
predicate=predicate,
|
||||
obj=subject,
|
||||
source_intent=intent.tag,
|
||||
)
|
||||
edge = GraphEdge(source="p0", target="p1", relation=Relation.CONTRAST)
|
||||
return graph.add_node(left).add_node(right).add_edge(edge)
|
||||
|
||||
root = GraphNode(
|
||||
node_id="p0",
|
||||
subject=subject,
|
||||
predicate=predicate,
|
||||
obj=obj,
|
||||
source_intent=intent.tag,
|
||||
)
|
||||
return graph.add_node(root)
|
||||
|
||||
|
||||
def _is_useful_surface(surface: str) -> bool:
|
||||
"""Return True when the realized surface is non-empty and fully grounded."""
|
||||
if not surface or not surface.strip():
|
||||
return False
|
||||
for indicator in _EMPTY_INDICATORS:
|
||||
if indicator and indicator in surface:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def articulate_with_intent(
|
||||
text: str,
|
||||
plan: ArticulationPlan,
|
||||
recalled_words: tuple[str, ...] = (),
|
||||
) -> str:
|
||||
"""Return an intent-aware surface string for *plan*, or "" if none can be produced.
|
||||
|
||||
Steps:
|
||||
1. Classify intent from raw input *text*
|
||||
2. Build a PropositionGraph from the intent + ArticulationPlan slot words
|
||||
3. Ground <pending> obj slots with *recalled_words* from generation result
|
||||
4. Plan articulation (topological walk)
|
||||
5. Realize via realize_semantic() for intent-specific templates
|
||||
6. Return the surface, or "" if the result is empty / ungrounded
|
||||
|
||||
The caller (chat/runtime.py) should fall back to the existing
|
||||
ArticulationPlan.surface when this returns "".
|
||||
"""
|
||||
intent = classify_intent_from_input(text)
|
||||
|
||||
graph = _build_graph_from_intent(intent, plan)
|
||||
if recalled_words:
|
||||
graph = ground_graph(graph, recalled_words)
|
||||
|
||||
articulation_target = plan_articulation(graph)
|
||||
realized: RealizedPlan = realize_semantic(articulation_target, graph)
|
||||
|
||||
if not realized.surface or not realized.fragments:
|
||||
return ""
|
||||
|
||||
surface = realized.surface
|
||||
if not _is_useful_surface(surface):
|
||||
return ""
|
||||
|
||||
return surface
|
||||
Loading…
Reference in a new issue