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)
|
||||
|
||||
|
||||
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:
|
||||
positions = []
|
||||
for word in order:
|
||||
|
|
@ -72,10 +67,10 @@ def _realize_from_graph(case: dict[str, Any]) -> str:
|
|||
edges_data = graph_data.get("edges", [])
|
||||
|
||||
_RELATION_MAP = {
|
||||
"conjunction": Relation.ELABORATION,
|
||||
"disjunction": Relation.CONTRAST,
|
||||
"complement": Relation.ELABORATION,
|
||||
"relative": Relation.ELABORATION,
|
||||
"conjunction": Relation.CONJUNCTION,
|
||||
"disjunction": Relation.DISJUNCTION,
|
||||
"complement": Relation.COMPLEMENT,
|
||||
"relative": Relation.RELATIVE,
|
||||
"sequence": Relation.SEQUENCE,
|
||||
"cause": Relation.CAUSE,
|
||||
"contrast": Relation.CONTRAST,
|
||||
|
|
@ -104,13 +99,19 @@ def _realize_from_graph(case: dict[str, Any]) -> str:
|
|||
|
||||
graph = PropositionGraph(nodes=tuple(nodes), edges=tuple(edges))
|
||||
|
||||
node_features = {nd["node_id"]: nd for nd in nodes_data}
|
||||
steps = []
|
||||
for node in nodes:
|
||||
nd = node_features[node.node_id]
|
||||
steps.append(ArticulationStep(
|
||||
node_id=node.node_id,
|
||||
subject=node.subject,
|
||||
predicate=node.predicate,
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ class Relation(Enum):
|
|||
CONTRAST = "contrast"
|
||||
SEQUENCE = "sequence"
|
||||
CORRECTION = "correction"
|
||||
CONJUNCTION = "conjunction"
|
||||
DISJUNCTION = "disjunction"
|
||||
COMPLEMENT = "complement"
|
||||
RELATIVE = "relative"
|
||||
|
||||
|
||||
@unique
|
||||
|
|
@ -112,6 +116,10 @@ class ArticulationStep:
|
|||
move: RhetoricalMove
|
||||
predicate: 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]:
|
||||
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:
|
||||
"""Realize an ArticulationTarget into a deterministic surface plan.
|
||||
|
||||
Each step is rendered through the template for its rhetorical move,
|
||||
then fragments are joined with sentence-level punctuation.
|
||||
Handles compound constructions (conjunction, disjunction, complement,
|
||||
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.
|
||||
"""
|
||||
from generate.graph_planner import Relation
|
||||
|
||||
if target is None or not target.steps:
|
||||
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] = []
|
||||
|
||||
for step in target.steps:
|
||||
if step.node_id in visited:
|
||||
continue
|
||||
visited.add(step.node_id)
|
||||
|
||||
obj = _resolve_obj(step, graph)
|
||||
move = step.move
|
||||
if move is RhetoricalMove.ASSERT and target.source_intent is IntentTag.CORRECTION:
|
||||
move = RhetoricalMove.CORRECT
|
||||
|
||||
surface = render_step(
|
||||
move=move,
|
||||
subject=step.subject,
|
||||
predicate=step.predicate,
|
||||
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(
|
||||
RealizedFragment(
|
||||
node_id=step.node_id,
|
||||
|
|
@ -162,3 +213,5 @@ def realize_target(
|
|||
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 generate.graph_planner import RhetoricalMove
|
||||
from generate.morphology import base_form, past_participle, past_tense, present_participle
|
||||
|
||||
|
||||
_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(
|
||||
move: RhetoricalMove,
|
||||
subject: str,
|
||||
predicate: str,
|
||||
obj: str,
|
||||
*,
|
||||
negated: bool = False,
|
||||
quantifier: str | None = None,
|
||||
tense: str | None = None,
|
||||
aspect: str | None = None,
|
||||
) -> str:
|
||||
"""Render a single articulation step into a surface fragment."""
|
||||
template = _MOVE_TEMPLATES[move]
|
||||
predicate_h = _humanize_predicate(predicate)
|
||||
predicate_h = _inflect_predicate(predicate_h, negated=negated, tense=tense, aspect=aspect)
|
||||
obj_display = obj if obj != "<pending>" else "..."
|
||||
subject_display = f"{quantifier} {subject}" if quantifier else subject
|
||||
return template.format(
|
||||
subject=subject,
|
||||
subject=subject_display,
|
||||
predicate_h=predicate_h,
|
||||
obj=obj_display,
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue