The Phase 1 multi-clause renderer (commit 63ffd88) produces grounded
content but reads mechanically because the subject lemma repeats in
every clause:
"Truth is what is true. Furthermore, truth belongs to cognition.truth.
In turn, truth grounds knowledge. Truth belongs to epistemic.ground.
Furthermore, truth belongs to logos.core. In turn, truth requires
evidence."
This is the literal articulation gap that motivated Phase 2 —
"reasoning at meaningful checkpoints during sentence construction
in order to have a stronger idea of what has come prior and is
already done to help better inform the next move." Between move
``i`` and move ``i+1`` the renderer now reflects on what subject
has just been established (the "focus") and renders the next clause
with a pronoun when the focus carries forward:
"Truth is what is true. Furthermore, it belongs to cognition.truth.
In turn, it grounds knowledge. It belongs to epistemic.ground.
Furthermore, it belongs to logos.core. In turn, it requires
evidence."
Rules
-----
* Track ``focus_subject`` across moves (the lemma most recently used
as a fact subject).
* When the next move's ``fact.subject`` is byte-equal to the current
focus → swap subject token to ``"it"``.
* When the next move's subject differs → preserve the explicit lemma
AND update focus. Topic shifts (TRANSITION moves; compound bridge
TRANSITION) thus reset the pronominalization channel naturally.
* Sentence-initial position (no connective): capitalised ``"It"``.
* Mid-sentence (after connective + comma): lowercase ``"it"``.
Doctrine alignment
------------------
Pure deterministic transformation of the existing plan; no new
content introduced, no LLM, no stochastic sampling. Same plan in →
same surface out, always. trace_hash invariance holds because:
* BRIEF-mode prompts short-circuit the planner before render
(commit 63ffd88's fast path) and are unaffected.
* Multi-move plans render to a deterministically-different string
that compute_trace_hash already folds in via ``surface``.
Wiring
------
* New ``reflective: bool = False`` parameter on ``render_plan``
(back-compat default — every existing call site and test pinning
Phase 1 output continues to work).
* ``_clause_for`` gains optional ``prior_focus_subject`` arg used by
the reflective path; unchanged default behaviour.
* Runtime hook ``chat.runtime._maybe_apply_discourse_planner``
passes ``reflective=True`` so the default chat path benefits.
Tests
-----
New ``tests/test_discourse_planner_reflective.py``:
* ``test_reflective_replaces_repeated_subject_with_it``
* ``test_reflective_handles_three_consecutive_same_subject_moves``
* ``test_reflective_capitalises_sentence_initial_pronoun``
* ``test_reflective_resets_focus_on_topic_shift``
* ``test_reflective_off_preserves_phase1_output``
* ``test_reflective_default_is_off_for_back_compat``
* ``test_reflective_is_deterministic``
* ``test_reflective_single_move_byte_identical_to_non_reflective``
(load-bearing — pins that the cognition eval stays byte-equal
across the Phase 2 flip because every cognition case is single-
move).
Verification
------------
pytest tests/test_discourse_planner_*.py 99/99 pass
(91 existing + 8 new)
pytest tests/test_articulation_demo.py all claims supported
pytest tests/test_narrative_example_intents.py pass
pytest tests/test_runtime_config.py pass
cognition eval OFF vs ON 45/45 surface byte-equal
45/45 trace_hash byte-equal
4/4 aggregate metrics
identical
core test --suite smoke 67/67 pass
core test --suite runtime 19/19 pass
Live demo (default config):
"What is knowledge?" → unchanged (BRIEF, fast-path)
"Tell me about
memory." → "Memory is what a person recalls.
Furthermore, it belongs to cognition.memory.
In turn, it requires recall."
"What is truth, and
why does it matter?"→ "Truth is what is true. Furthermore, it
belongs to cognition.truth. In turn, it
grounds knowledge. It belongs to
epistemic.ground. Furthermore, it belongs
to logos.core. In turn, it requires
evidence."
"Explain truth." → "Truth is what is true. Furthermore, it
belongs to cognition.truth. In turn, it
grounds knowledge."
Out of scope for this commit (future Phase 2 follow-ons):
* Connective rotation ("Furthermore" → "Also" → "In addition"
to break the repetitive cascade).
* Cross-clause de-duplication (skip moves whose ``new`` lemmas
were already introduced by an earlier move).
* Generalised pronoun selection beyond ``it`` (requires gender /
number / animacy signals the pack lexicon doesn't carry today).
293 lines
9.6 KiB
Python
293 lines
9.6 KiB
Python
"""Phase 2 — reflective rendering via subject pronominalization.
|
|
|
|
The Phase 1 (commit ``63ffd88``) planner produces multi-clause plans
|
|
but the renderer walks moves in order without awareness of what was
|
|
just surfaced. On a typical EXPLAIN/PARAGRAPH/compound output that
|
|
yields a mechanical-feeling cascade in which the subject lemma
|
|
repeats in every clause:
|
|
|
|
Truth is what is true.
|
|
Furthermore, truth belongs to cognition.truth.
|
|
In turn, truth grounds knowledge.
|
|
Truth belongs to epistemic.ground.
|
|
Furthermore, truth belongs to logos.core.
|
|
In turn, truth requires evidence.
|
|
|
|
Phase 2 wires the renderer with the **lightest possible interleaved
|
|
reflection**: track the most-recently-introduced subject (the
|
|
"focus") and, when the next move's subject is byte-equal to it,
|
|
emit a pronoun ("it") instead of repeating the lemma. The
|
|
substitution rules:
|
|
|
|
* Same subject AND no topic shift (i.e. ``move.topic`` agrees with
|
|
the prior focus) → swap to ``"it"``.
|
|
* Topic shift (TRANSITION move, or any move whose topic differs
|
|
from prior focus) → reset focus, keep the explicit subject.
|
|
* Sentence-initial position (no connective): capitalise ``"It"``.
|
|
* Mid-sentence position (after connective + comma): lowercase ``"it"``.
|
|
|
|
This is deterministic, replayable, and adds nothing the bundle
|
|
didn't already contain — it is a pure rendering improvement.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from generate.discourse_planner import (
|
|
DiscourseMove,
|
|
DiscourseMoveKind,
|
|
DiscoursePlan,
|
|
FactSource,
|
|
GroundedFact,
|
|
render_plan,
|
|
)
|
|
from generate.intent import DialogueIntent, IntentTag, ResponseMode
|
|
|
|
|
|
def _fact(subject: str, predicate: str, obj: str) -> GroundedFact:
|
|
return GroundedFact(
|
|
subject=subject,
|
|
predicate=predicate,
|
|
obj=obj,
|
|
source=FactSource.PACK,
|
|
source_id="test_pack_v1",
|
|
)
|
|
|
|
|
|
def _intent(subject: str = "truth") -> DialogueIntent:
|
|
return DialogueIntent(tag=IntentTag.DEFINITION, subject=subject)
|
|
|
|
|
|
def _move(
|
|
kind: DiscourseMoveKind,
|
|
fact: GroundedFact,
|
|
*,
|
|
given: tuple[str, ...] = (),
|
|
new: tuple[str, ...] = (),
|
|
) -> DiscourseMove:
|
|
return DiscourseMove(
|
|
kind=kind,
|
|
topic=fact.subject,
|
|
given=given,
|
|
new=new,
|
|
relation_to_previous=None,
|
|
fact=fact,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pronominalization fires across consecutive same-subject moves
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_reflective_replaces_repeated_subject_with_it() -> None:
|
|
"""Two moves with byte-equal subject → second clause uses ``it``."""
|
|
plan = DiscoursePlan(
|
|
intent=_intent(),
|
|
mode=ResponseMode.EXPLAIN,
|
|
moves=(
|
|
_move(
|
|
DiscourseMoveKind.ANCHOR,
|
|
_fact("truth", "is_defined_as", "what is true"),
|
|
new=("truth",),
|
|
),
|
|
_move(
|
|
DiscourseMoveKind.SUPPORT,
|
|
_fact("truth", "belongs_to", "cognition.truth"),
|
|
given=("truth",),
|
|
new=("cognition.truth",),
|
|
),
|
|
),
|
|
)
|
|
rendered = render_plan(plan, reflective=True)
|
|
assert (
|
|
rendered
|
|
== "Truth is what is true. Furthermore, it belongs to cognition.truth."
|
|
)
|
|
|
|
|
|
def test_reflective_handles_three_consecutive_same_subject_moves() -> None:
|
|
"""Three same-subject moves → first is canonical, next two are ``it``."""
|
|
plan = DiscoursePlan(
|
|
intent=_intent(),
|
|
mode=ResponseMode.PARAGRAPH,
|
|
moves=(
|
|
_move(
|
|
DiscourseMoveKind.ANCHOR,
|
|
_fact("truth", "is_defined_as", "what is true"),
|
|
new=("truth",),
|
|
),
|
|
_move(
|
|
DiscourseMoveKind.SUPPORT,
|
|
_fact("truth", "belongs_to", "cognition.truth"),
|
|
),
|
|
_move(
|
|
DiscourseMoveKind.RELATION,
|
|
_fact("truth", "grounds", "knowledge"),
|
|
),
|
|
),
|
|
)
|
|
rendered = render_plan(plan, reflective=True)
|
|
assert rendered == (
|
|
"Truth is what is true. "
|
|
"Furthermore, it belongs to cognition.truth. "
|
|
"In turn, it grounds knowledge."
|
|
)
|
|
|
|
|
|
def test_reflective_capitalises_sentence_initial_pronoun() -> None:
|
|
"""When the next clause has no connective (e.g. CLOSURE), the
|
|
pronoun must be sentence-initial and therefore capitalised."""
|
|
plan = DiscoursePlan(
|
|
intent=_intent(),
|
|
mode=ResponseMode.PARAGRAPH,
|
|
moves=(
|
|
_move(
|
|
DiscourseMoveKind.ANCHOR,
|
|
_fact("truth", "is_defined_as", "what is true"),
|
|
),
|
|
_move(
|
|
DiscourseMoveKind.CLOSURE,
|
|
_fact("truth", "belongs_to", "epistemic.ground"),
|
|
),
|
|
),
|
|
)
|
|
rendered = render_plan(plan, reflective=True)
|
|
assert rendered == (
|
|
"Truth is what is true. It belongs to epistemic.ground."
|
|
)
|
|
|
|
|
|
def test_reflective_resets_focus_on_topic_shift() -> None:
|
|
"""When the next move's subject differs (topic shift), keep the
|
|
explicit lemma — pronouns would be ambiguous."""
|
|
plan = DiscoursePlan(
|
|
intent=_intent(),
|
|
mode=ResponseMode.PARAGRAPH,
|
|
moves=(
|
|
_move(
|
|
DiscourseMoveKind.ANCHOR,
|
|
_fact("truth", "is_defined_as", "what is true"),
|
|
),
|
|
_move(
|
|
DiscourseMoveKind.TRANSITION,
|
|
_fact("knowledge", "belongs_to", "cognition.knowledge"),
|
|
),
|
|
_move(
|
|
DiscourseMoveKind.RELATION,
|
|
_fact("knowledge", "grounds", "judgment"),
|
|
),
|
|
),
|
|
)
|
|
rendered = render_plan(plan, reflective=True)
|
|
# After the TRANSITION shifts focus to ``knowledge``, the next
|
|
# ``knowledge`` reference becomes ``it`` again.
|
|
assert rendered == (
|
|
"Truth is what is true. "
|
|
"Consequently, knowledge belongs to cognition.knowledge. "
|
|
"In turn, it grounds judgment."
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Backward compatibility: reflective=False preserves Phase 1 output
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_reflective_off_preserves_phase1_output() -> None:
|
|
"""``reflective=False`` reproduces the pre-Phase-2 byte-equivalent
|
|
output — the default kept for any caller pinning the raw shape."""
|
|
plan = DiscoursePlan(
|
|
intent=_intent(),
|
|
mode=ResponseMode.EXPLAIN,
|
|
moves=(
|
|
_move(
|
|
DiscourseMoveKind.ANCHOR,
|
|
_fact("truth", "is_defined_as", "what is true"),
|
|
),
|
|
_move(
|
|
DiscourseMoveKind.SUPPORT,
|
|
_fact("truth", "belongs_to", "cognition.truth"),
|
|
),
|
|
),
|
|
)
|
|
rendered = render_plan(plan, reflective=False)
|
|
assert rendered == (
|
|
"Truth is what is true. "
|
|
"Furthermore, truth belongs to cognition.truth."
|
|
)
|
|
|
|
|
|
def test_reflective_default_is_off_for_back_compat() -> None:
|
|
"""``render_plan(plan)`` without an explicit kwarg keeps the
|
|
Phase-1 default so existing call sites (and existing tests) are
|
|
not silently rewritten. The runtime wiring explicitly opts in."""
|
|
plan = DiscoursePlan(
|
|
intent=_intent(),
|
|
mode=ResponseMode.EXPLAIN,
|
|
moves=(
|
|
_move(
|
|
DiscourseMoveKind.ANCHOR,
|
|
_fact("truth", "is_defined_as", "what is true"),
|
|
),
|
|
_move(
|
|
DiscourseMoveKind.SUPPORT,
|
|
_fact("truth", "belongs_to", "cognition.truth"),
|
|
),
|
|
),
|
|
)
|
|
assert render_plan(plan) == render_plan(plan, reflective=False)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Determinism: same input → same output, twice
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_reflective_is_deterministic() -> None:
|
|
plan = DiscoursePlan(
|
|
intent=_intent(),
|
|
mode=ResponseMode.PARAGRAPH,
|
|
moves=(
|
|
_move(
|
|
DiscourseMoveKind.ANCHOR,
|
|
_fact("truth", "is_defined_as", "what is true"),
|
|
),
|
|
_move(
|
|
DiscourseMoveKind.SUPPORT,
|
|
_fact("truth", "belongs_to", "cognition.truth"),
|
|
),
|
|
_move(
|
|
DiscourseMoveKind.RELATION,
|
|
_fact("truth", "grounds", "knowledge"),
|
|
),
|
|
),
|
|
)
|
|
a = render_plan(plan, reflective=True)
|
|
b = render_plan(plan, reflective=True)
|
|
assert a == b
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Single-move plans are byte-identical regardless of reflective mode
|
|
# (no second move ⇒ no pronoun substitution possible)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_reflective_single_move_byte_identical_to_non_reflective() -> None:
|
|
"""A BRIEF/single-move plan can't trigger pronominalization — the
|
|
output must be byte-identical to ``reflective=False``. This is
|
|
the load-bearing claim that lets the cognition eval (mostly
|
|
single-fact prompts) stay byte-equal across the Phase 2 flip."""
|
|
plan = DiscoursePlan(
|
|
intent=_intent(),
|
|
mode=ResponseMode.BRIEF,
|
|
moves=(
|
|
_move(
|
|
DiscourseMoveKind.ANCHOR,
|
|
_fact("truth", "is_defined_as", "what is true"),
|
|
),
|
|
),
|
|
)
|
|
assert render_plan(plan, reflective=True) == render_plan(
|
|
plan, reflective=False
|
|
)
|