core/chat/narrative_surface.py
Shay ce8226e9a2 feat(adr-0066): NARRATIVE + EXAMPLE intents with multi-clause composers (Phase 3.3 + 3.4)
Two new intent shapes + composers turn the runtime's corpus
density into operator-visible articulation.  Both consult the
cross-corpus aggregator from ADR-0064; no new ratification needed.

P3.3 — chat/narrative_surface.py + IntentTag.NARRATIVE.

  Classifier patterns (registered BEFORE generic DEFINITION):
    ^tell\s+me\s+about\s+
    ^describe\s+
    ^what\s+(?:can|do)\s+you\s+(?:say|know)\s+about\s+

  narrative_grounded_surface(subject, max_clauses=4) walks every
  reviewed chain rooted on subject across all registered teaching
  corpora.  Dedupes by (connective, object) — cause + verification
  carrying the same predicate emit one clause, not two.  Sorts by
  (intent, connective, object) for replay stability.

  Surface format:
    "{X} — narrative-grounded ({corpus_ids}): {dX1}; {dX2}.
     {X} {conn1} {O1} ({dO1}); {X} {conn2} {O2} ({dO2}).
     No session evidence yet."

  Cross-corpus subjects (e.g. mother in relations_v2) emit
  narrative-grounded (relations_chains_v2) tag; cognition subjects
  emit cognition_chains_v1 tag.  Multi-corpus subjects (when
  applicable) emit composite "corpus_a + corpus_b" tag.

P3.4 — chat/example_surface.py + IntentTag.EXAMPLE.

  Classifier patterns:
    ^(?:give|show)\s+(?:me\s+)?an?\s+(?:example|instance)\s+of\s+
    ^example\s+of\s+

  example_grounded_surface(object_lemma, max_examples=3) walks chains
  where the lemma is the OBJECT — inverts the typical subject-keyed
  access pattern.  Dedupes by subject; sorts by (intent, subject,
  connective).

  Surface format:
    "{X} — example-grounded ({corpus_ids}): {dX1}.
     Example: {subj1} {conn1} {X}; {subj2} {conn2} {X}.
     No session evidence yet."

Cross-cutting:
  - Both intents added to _OOV_INTENT_TAGS — fall through to OOV
    invitation when subject is unknown (Phase 2 gradient discipline).
  - Both tagged grounding_source="teaching" (same provenance tier
    as the existing teaching_grounded_surface).
  - No prose generation, no new mutation surface.

Live verification:
  > Tell me about truth.
    [teaching] truth — narrative-grounded (cognition_chains_v1):
    cognition.truth; logos.core. truth grounds knowledge
    (cognition.knowledge); truth requires evidence (cognition.evidence).

  > Give me an example of knowledge.
    [teaching] knowledge — example-grounded (cognition_chains_v1):
    cognition.knowledge. Example: truth grounds knowledge;
    understanding requires knowledge; evidence grounds knowledge.

  > Tell me about mother.
    [teaching] mother — narrative-grounded (relations_chains_v2):
    kinship.parent.female. mother precedes daughter (kinship.child.female).

  > Describe photosynthesis.
    [oov] I haven't learned 'photosynthesis' yet (intent: narrative). ...

ADR-0066 (this commit completes the ADR).  30 new tests passed.
Full lane: 2067 passed, 2 skipped, 0 failed in 2:32.
2026-05-18 17:01:55 -07:00

158 lines
5.7 KiB
Python

"""chat/narrative_surface.py — Phase 3.3: NARRATIVE intent composer.
When a prompt classifies as NARRATIVE — "Tell me about X", "Describe
X", "What can you say about X" — the composer walks every reviewed
chain rooted on X across every registered teaching corpus and emits
a multi-clause surface that surfaces *everything* the system has
reviewed about X.
Sibling to:
- :func:`chat.teaching_grounding.teaching_grounded_surface` —
surfaces ONE chain rooted on X for a specific intent.
- :func:`chat.teaching_grounding.teaching_grounded_surface_composed`
— extends one chain with a follow-up (depth-1 chain-of-chains).
- :func:`chat.pack_grounding.pack_grounded_surface` — surfaces X's
pack semantic_domains.
Whereas those composers pick one chain or one extension, NARRATIVE
aggregates *every distinct (predicate, object) clause* rooted on X
across both cause and verification intents. Surface format:
"{X} — narrative-grounded ({corpus_ids}): {dX1}; {dX2}.
{X} {conn1} {O1} ({dO1}); {X} {conn2} {O2} ({dO2}); ...
No session evidence yet."
Design constraints (matching ADR-0052..0065 doctrine):
- **No content synthesis.** Every visible non-template token is
either the lemma X, a verbatim pack ``semantic_domains`` atom, a
reviewed chain object lemma, or a fixed connective from
``humanize_predicate``.
- **Deterministic ordering.** Clauses sort by (intent_name,
connective, object) so identical corpus state always produces
the identical surface.
- **Dedup by (connective, object).** When cause and verification
carry the same predicate + object, only one clause is emitted —
the dual-tag is implicit in the chain provenance and adding both
reads as noise to the user.
- **Pack-internal.** Chains are loaded from the cross-corpus
aggregator (:func:`_all_chains_index`); each chain's object
domains are read from its bound pack via
:func:`_pack_for_corpus`.
- **Bounded clause count.** Default ``max_clauses=4`` to keep the
surface readable. Operators can raise the cap for analytic
workloads.
Returns ``None`` when no chain references X as subject — caller
falls through to the pack-grounded surface (DEFINITION-like
narrative) or to the OOV invitation if X is also not pack-resident.
"""
from __future__ import annotations
from chat.pack_resolver import resolve_lemma
from chat.teaching_grounding import (
_all_chains_index,
_pack_for_corpus,
)
from generate.semantic_templates import humanize_predicate
def narrative_grounded_surface(
subject_lemma: str,
*,
max_clauses: int = 4,
) -> str | None:
"""Return a deterministic NARRATIVE-tier surface, or ``None``.
Aggregates every reviewed chain whose subject equals *subject_lemma*
across all registered teaching corpora. Dedups by (connective,
object). Sorts clauses lexicographically for replay stability.
``max_clauses`` caps the emitted clause count. Default 4 reads
smoothly; operators can raise for analytic workloads.
Returns ``None`` when no chain references *subject_lemma* — the
caller routes through pack-grounded DEFINITION (or OOV if the
lemma is unknown).
"""
if not subject_lemma or not isinstance(subject_lemma, str):
return None
key = subject_lemma.strip().lower()
if not key:
return None
if max_clauses < 1:
return None
index = _all_chains_index()
matching = [
chain for (s, _), chain in index.items() if s == key
]
if not matching:
return None
# Dedup by (connective, object) — verification and cause carrying
# the same predicate produce one clause, not two. Stable sort
# by (intent, connective, object) so replay produces byte-identical
# output.
seen: set[tuple[str, str]] = set()
deduped: list = []
for chain in sorted(
matching, key=lambda c: (c.intent, c.connective, c.object),
):
sig = (chain.connective, chain.object)
if sig in seen:
continue
seen.add(sig)
deduped.append(chain)
if len(deduped) >= max_clauses:
break
# Subject domains: take from the first chain's bound pack so the
# narrative header is sourced from the lemma's own pack — even
# when the matching chains span multiple corpora.
first = deduped[0]
subject_pack = _pack_for_corpus(first.corpus_id)
subject_domains = subject_pack.get(first.subject, ())
if not subject_domains:
# Fall back to cross-pack resolver — subject may live in a
# different pack than its chains' corpus binding (defensive).
resolved = resolve_lemma(first.subject)
if resolved is None:
return None
subject_domains = resolved[1]
head_subject = "; ".join(
subject_domains[: max(1, first.domains_subject_k)]
)
# Collect involved corpora for the tag.
corpora = tuple(sorted({c.corpus_id for c in deduped}))
corpora_tag = corpora[0] if len(corpora) == 1 else " + ".join(corpora)
# Emit one clause per deduped chain.
clauses: list[str] = []
for chain in deduped:
obj_pack = _pack_for_corpus(chain.corpus_id)
obj_domains = obj_pack.get(chain.object, ())
if not obj_domains:
continue
obj_head = "; ".join(
obj_domains[: max(1, chain.domains_object_k)]
)
connective = humanize_predicate(chain.connective)
clauses.append(
f"{chain.subject} {connective} {chain.object} ({obj_head})"
)
if not clauses:
return None
return (
f"{first.subject} — narrative-grounded ({corpora_tag}): "
f"{head_subject}. {'; '.join(clauses)}. "
f"No session evidence yet."
)
__all__ = ["narrative_grounded_surface"]