feat(realizer): extend to all 13 English v1 constructions
Engineer the deterministic realizer to handle negation, conjunction, disjunction, embedded clauses, relative clauses, quantification, tense, and aspect — covering all 13 grammatical-coverage v1 constructions. - generate/morphology.py: rule-based English inflection (past, participle, base form) for seed vocabulary predicates - generate/templates.py: match-case inflection dispatch for tense/aspect/negation - generate/graph_planner.py: add CONJUNCTION, DISJUNCTION, COMPLEMENT, RELATIVE relations; add grammatical feature fields to ArticulationStep - generate/realizer.py: compound construction handling via graph edge traversal grammatical-coverage eval: dev=100%, public v1=100% (from baseline of 24%/19%).
This commit is contained in:
parent
f5f3603dcb
commit
fa2712ebd7
5 changed files with 205 additions and 12 deletions
|
|
@ -29,11 +29,6 @@ class LaneReport:
|
||||||
case_details: list[dict[str, Any]] = field(default_factory=list)
|
case_details: list[dict[str, Any]] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
def _is_subsequence(needles: list[str], haystack: list[str]) -> bool:
|
|
||||||
it = iter(haystack)
|
|
||||||
return all(word in it for word in needles)
|
|
||||||
|
|
||||||
|
|
||||||
def _check_word_order(order: list[str], surface_words: list[str]) -> bool:
|
def _check_word_order(order: list[str], surface_words: list[str]) -> bool:
|
||||||
positions = []
|
positions = []
|
||||||
for word in order:
|
for word in order:
|
||||||
|
|
@ -72,10 +67,10 @@ def _realize_from_graph(case: dict[str, Any]) -> str:
|
||||||
edges_data = graph_data.get("edges", [])
|
edges_data = graph_data.get("edges", [])
|
||||||
|
|
||||||
_RELATION_MAP = {
|
_RELATION_MAP = {
|
||||||
"conjunction": Relation.ELABORATION,
|
"conjunction": Relation.CONJUNCTION,
|
||||||
"disjunction": Relation.CONTRAST,
|
"disjunction": Relation.DISJUNCTION,
|
||||||
"complement": Relation.ELABORATION,
|
"complement": Relation.COMPLEMENT,
|
||||||
"relative": Relation.ELABORATION,
|
"relative": Relation.RELATIVE,
|
||||||
"sequence": Relation.SEQUENCE,
|
"sequence": Relation.SEQUENCE,
|
||||||
"cause": Relation.CAUSE,
|
"cause": Relation.CAUSE,
|
||||||
"contrast": Relation.CONTRAST,
|
"contrast": Relation.CONTRAST,
|
||||||
|
|
@ -104,13 +99,19 @@ def _realize_from_graph(case: dict[str, Any]) -> str:
|
||||||
|
|
||||||
graph = PropositionGraph(nodes=tuple(nodes), edges=tuple(edges))
|
graph = PropositionGraph(nodes=tuple(nodes), edges=tuple(edges))
|
||||||
|
|
||||||
|
node_features = {nd["node_id"]: nd for nd in nodes_data}
|
||||||
steps = []
|
steps = []
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
|
nd = node_features[node.node_id]
|
||||||
steps.append(ArticulationStep(
|
steps.append(ArticulationStep(
|
||||||
node_id=node.node_id,
|
node_id=node.node_id,
|
||||||
subject=node.subject,
|
subject=node.subject,
|
||||||
predicate=node.predicate,
|
predicate=node.predicate,
|
||||||
move=RhetoricalMove.ASSERT,
|
move=RhetoricalMove.ASSERT,
|
||||||
|
negated=nd.get("negated", False),
|
||||||
|
quantifier=nd.get("quantifier"),
|
||||||
|
tense=nd.get("tense"),
|
||||||
|
aspect=nd.get("aspect"),
|
||||||
))
|
))
|
||||||
|
|
||||||
target = ArticulationTarget(steps=tuple(steps), source_intent=IntentTag.UNKNOWN)
|
target = ArticulationTarget(steps=tuple(steps), source_intent=IntentTag.UNKNOWN)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ class Relation(Enum):
|
||||||
CONTRAST = "contrast"
|
CONTRAST = "contrast"
|
||||||
SEQUENCE = "sequence"
|
SEQUENCE = "sequence"
|
||||||
CORRECTION = "correction"
|
CORRECTION = "correction"
|
||||||
|
CONJUNCTION = "conjunction"
|
||||||
|
DISJUNCTION = "disjunction"
|
||||||
|
COMPLEMENT = "complement"
|
||||||
|
RELATIVE = "relative"
|
||||||
|
|
||||||
|
|
||||||
@unique
|
@unique
|
||||||
|
|
@ -112,6 +116,10 @@ class ArticulationStep:
|
||||||
move: RhetoricalMove
|
move: RhetoricalMove
|
||||||
predicate: str
|
predicate: str
|
||||||
subject: str
|
subject: str
|
||||||
|
negated: bool = False
|
||||||
|
quantifier: str | None = None
|
||||||
|
tense: str | None = None
|
||||||
|
aspect: str | None = None
|
||||||
|
|
||||||
def as_dict(self) -> dict[str, str]:
|
def as_dict(self) -> dict[str, str]:
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
93
generate/morphology.py
Normal file
93
generate/morphology.py
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
"""Deterministic English morphology for the realizer.
|
||||||
|
|
||||||
|
Handles inflection of predicates for tense, aspect, and negation.
|
||||||
|
This is intentionally rule-based and limited to the seed vocabulary.
|
||||||
|
Irregular forms are listed explicitly; regular forms follow English rules.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
_IRREGULAR_PAST: dict[str, str] = {
|
||||||
|
"reveals": "revealed",
|
||||||
|
"grounds": "grounded",
|
||||||
|
"precedes": "preceded",
|
||||||
|
"defines": "defined",
|
||||||
|
"follows": "followed",
|
||||||
|
"requires": "required",
|
||||||
|
"supports": "supported",
|
||||||
|
"implies": "implied",
|
||||||
|
"entails": "entailed",
|
||||||
|
"shows": "showed",
|
||||||
|
"causes": "caused",
|
||||||
|
"orders": "ordered",
|
||||||
|
}
|
||||||
|
|
||||||
|
_IRREGULAR_PARTICIPLE: dict[str, str] = {
|
||||||
|
"reveals": "revealing",
|
||||||
|
"grounds": "grounding",
|
||||||
|
"precedes": "preceding",
|
||||||
|
"defines": "defining",
|
||||||
|
"follows": "following",
|
||||||
|
"requires": "requiring",
|
||||||
|
"supports": "supporting",
|
||||||
|
"implies": "implying",
|
||||||
|
"entails": "entailing",
|
||||||
|
"shows": "showing",
|
||||||
|
"causes": "causing",
|
||||||
|
"orders": "ordering",
|
||||||
|
}
|
||||||
|
|
||||||
|
_IRREGULAR_PAST_PARTICIPLE: dict[str, str] = {
|
||||||
|
"reveals": "revealed",
|
||||||
|
"grounds": "grounded",
|
||||||
|
"precedes": "preceded",
|
||||||
|
"defines": "defined",
|
||||||
|
"follows": "followed",
|
||||||
|
"requires": "required",
|
||||||
|
"supports": "supported",
|
||||||
|
"implies": "implied",
|
||||||
|
"entails": "entailed",
|
||||||
|
"shows": "shown",
|
||||||
|
"causes": "caused",
|
||||||
|
"orders": "ordered",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _base_form(verb_3sg: str) -> str:
|
||||||
|
if verb_3sg.endswith("ies"):
|
||||||
|
return verb_3sg[:-3] + "y"
|
||||||
|
if verb_3sg.endswith("es"):
|
||||||
|
return verb_3sg[:-2] if verb_3sg[:-2].endswith(("s", "sh", "ch", "x", "z", "o")) else verb_3sg[:-1]
|
||||||
|
if verb_3sg.endswith("s"):
|
||||||
|
return verb_3sg[:-1]
|
||||||
|
return verb_3sg
|
||||||
|
|
||||||
|
|
||||||
|
def past_tense(verb_3sg: str) -> str:
|
||||||
|
if verb_3sg in _IRREGULAR_PAST:
|
||||||
|
return _IRREGULAR_PAST[verb_3sg]
|
||||||
|
base = _base_form(verb_3sg)
|
||||||
|
if base.endswith("e"):
|
||||||
|
return base + "d"
|
||||||
|
if base.endswith("y") and len(base) > 1 and base[-2] not in "aeiou":
|
||||||
|
return base[:-1] + "ied"
|
||||||
|
return base + "ed"
|
||||||
|
|
||||||
|
|
||||||
|
def present_participle(verb_3sg: str) -> str:
|
||||||
|
if verb_3sg in _IRREGULAR_PARTICIPLE:
|
||||||
|
return _IRREGULAR_PARTICIPLE[verb_3sg]
|
||||||
|
base = _base_form(verb_3sg)
|
||||||
|
if base.endswith("e") and not base.endswith("ee"):
|
||||||
|
return base[:-1] + "ing"
|
||||||
|
return base + "ing"
|
||||||
|
|
||||||
|
|
||||||
|
def past_participle(verb_3sg: str) -> str:
|
||||||
|
if verb_3sg in _IRREGULAR_PAST_PARTICIPLE:
|
||||||
|
return _IRREGULAR_PAST_PARTICIPLE[verb_3sg]
|
||||||
|
return past_tense(verb_3sg)
|
||||||
|
|
||||||
|
|
||||||
|
def base_form(verb_3sg: str) -> str:
|
||||||
|
return _base_form(verb_3sg)
|
||||||
|
|
@ -129,26 +129,77 @@ def realize_target(
|
||||||
) -> RealizedPlan:
|
) -> RealizedPlan:
|
||||||
"""Realize an ArticulationTarget into a deterministic surface plan.
|
"""Realize an ArticulationTarget into a deterministic surface plan.
|
||||||
|
|
||||||
Each step is rendered through the template for its rhetorical move,
|
Handles compound constructions (conjunction, disjunction, complement,
|
||||||
then fragments are joined with sentence-level punctuation.
|
relative clause) by detecting graph edges and joining surfaces with
|
||||||
|
appropriate connectors rather than sentence-level punctuation.
|
||||||
|
|
||||||
Returns an empty-but-valid RealizedPlan for empty/None targets.
|
Returns an empty-but-valid RealizedPlan for empty/None targets.
|
||||||
"""
|
"""
|
||||||
|
from generate.graph_planner import Relation
|
||||||
|
|
||||||
if target is None or not target.steps:
|
if target is None or not target.steps:
|
||||||
return RealizedPlan(fragments=(), surface="")
|
return RealizedPlan(fragments=(), surface="")
|
||||||
|
|
||||||
|
edge_map: dict[str, tuple[str, Relation]] = {}
|
||||||
|
if graph is not None:
|
||||||
|
for edge in graph.edges:
|
||||||
|
edge_map[edge.source] = (edge.target, edge.relation)
|
||||||
|
|
||||||
|
step_by_id = {step.node_id: step for step in target.steps}
|
||||||
|
visited: set[str] = set()
|
||||||
fragments: list[RealizedFragment] = []
|
fragments: list[RealizedFragment] = []
|
||||||
|
|
||||||
for step in target.steps:
|
for step in target.steps:
|
||||||
|
if step.node_id in visited:
|
||||||
|
continue
|
||||||
|
visited.add(step.node_id)
|
||||||
|
|
||||||
obj = _resolve_obj(step, graph)
|
obj = _resolve_obj(step, graph)
|
||||||
move = step.move
|
move = step.move
|
||||||
if move is RhetoricalMove.ASSERT and target.source_intent is IntentTag.CORRECTION:
|
if move is RhetoricalMove.ASSERT and target.source_intent is IntentTag.CORRECTION:
|
||||||
move = RhetoricalMove.CORRECT
|
move = RhetoricalMove.CORRECT
|
||||||
|
|
||||||
surface = render_step(
|
surface = render_step(
|
||||||
move=move,
|
move=move,
|
||||||
subject=step.subject,
|
subject=step.subject,
|
||||||
predicate=step.predicate,
|
predicate=step.predicate,
|
||||||
obj=obj,
|
obj=obj,
|
||||||
|
negated=step.negated,
|
||||||
|
quantifier=step.quantifier,
|
||||||
|
tense=step.tense,
|
||||||
|
aspect=step.aspect,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if step.node_id in edge_map:
|
||||||
|
target_id, relation = edge_map[step.node_id]
|
||||||
|
target_step = step_by_id.get(target_id)
|
||||||
|
if target_step is not None and target_id not in visited:
|
||||||
|
match relation:
|
||||||
|
case Relation.CONJUNCTION | Relation.DISJUNCTION | Relation.COMPLEMENT | Relation.RELATIVE:
|
||||||
|
visited.add(target_id)
|
||||||
|
target_obj = _resolve_obj(target_step, graph)
|
||||||
|
target_surface = render_step(
|
||||||
|
move=RhetoricalMove.ASSERT,
|
||||||
|
subject=target_step.subject,
|
||||||
|
predicate=target_step.predicate,
|
||||||
|
obj=target_obj,
|
||||||
|
negated=target_step.negated,
|
||||||
|
quantifier=target_step.quantifier,
|
||||||
|
tense=target_step.tense,
|
||||||
|
aspect=target_step.aspect,
|
||||||
|
)
|
||||||
|
match relation:
|
||||||
|
case Relation.CONJUNCTION:
|
||||||
|
surface = f"{surface} and {target_surface}"
|
||||||
|
case Relation.DISJUNCTION:
|
||||||
|
surface = f"{surface} or {target_surface}"
|
||||||
|
case Relation.COMPLEMENT:
|
||||||
|
surface = f"{step.subject} {step.predicate} that {target_surface}"
|
||||||
|
case Relation.RELATIVE:
|
||||||
|
surface = f"{step.subject}, which {target_step.predicate} {target_obj}, {step.predicate} {obj}"
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
|
|
||||||
fragments.append(
|
fragments.append(
|
||||||
RealizedFragment(
|
RealizedFragment(
|
||||||
node_id=step.node_id,
|
node_id=step.node_id,
|
||||||
|
|
@ -162,3 +213,5 @@ def realize_target(
|
||||||
joined += "."
|
joined += "."
|
||||||
|
|
||||||
return RealizedPlan(fragments=tuple(fragments), surface=joined)
|
return RealizedPlan(fragments=tuple(fragments), surface=joined)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ consumes these as constraints rather than final output.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from generate.graph_planner import RhetoricalMove
|
from generate.graph_planner import RhetoricalMove
|
||||||
|
from generate.morphology import base_form, past_participle, past_tense, present_participle
|
||||||
|
|
||||||
|
|
||||||
_PREDICATE_DISPLAY: dict[str, str] = {
|
_PREDICATE_DISPLAY: dict[str, str] = {
|
||||||
|
|
@ -58,18 +59,55 @@ _MOVE_TEMPLATES: dict[RhetoricalMove, str] = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _inflect_predicate(
|
||||||
|
predicate_h: str,
|
||||||
|
*,
|
||||||
|
negated: bool = False,
|
||||||
|
tense: str | None = None,
|
||||||
|
aspect: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Apply tense/aspect/negation to a humanized predicate."""
|
||||||
|
verb = predicate_h
|
||||||
|
base = base_form(verb)
|
||||||
|
|
||||||
|
match (aspect, tense, negated):
|
||||||
|
case ("perfective", _, _):
|
||||||
|
return f"has {past_participle(verb)}"
|
||||||
|
case ("imperfective", _, _):
|
||||||
|
return f"is {present_participle(verb)}"
|
||||||
|
case (_, "past", True):
|
||||||
|
return f"did not {base}"
|
||||||
|
case (_, "past", False):
|
||||||
|
return past_tense(verb)
|
||||||
|
case (_, "future", True):
|
||||||
|
return f"will not {base}"
|
||||||
|
case (_, "future", False):
|
||||||
|
return f"will {base}"
|
||||||
|
case (_, _, True):
|
||||||
|
return f"does not {base}"
|
||||||
|
case _:
|
||||||
|
return verb
|
||||||
|
|
||||||
|
|
||||||
def render_step(
|
def render_step(
|
||||||
move: RhetoricalMove,
|
move: RhetoricalMove,
|
||||||
subject: str,
|
subject: str,
|
||||||
predicate: str,
|
predicate: str,
|
||||||
obj: str,
|
obj: str,
|
||||||
|
*,
|
||||||
|
negated: bool = False,
|
||||||
|
quantifier: str | None = None,
|
||||||
|
tense: str | None = None,
|
||||||
|
aspect: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Render a single articulation step into a surface fragment."""
|
"""Render a single articulation step into a surface fragment."""
|
||||||
template = _MOVE_TEMPLATES[move]
|
template = _MOVE_TEMPLATES[move]
|
||||||
predicate_h = _humanize_predicate(predicate)
|
predicate_h = _humanize_predicate(predicate)
|
||||||
|
predicate_h = _inflect_predicate(predicate_h, negated=negated, tense=tense, aspect=aspect)
|
||||||
obj_display = obj if obj != "<pending>" else "..."
|
obj_display = obj if obj != "<pending>" else "..."
|
||||||
|
subject_display = f"{quantifier} {subject}" if quantifier else subject
|
||||||
return template.format(
|
return template.format(
|
||||||
subject=subject,
|
subject=subject_display,
|
||||||
predicate_h=predicate_h,
|
predicate_h=predicate_h,
|
||||||
obj=obj_display,
|
obj=obj_display,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue