Phase 2 — proposition-slot grounding for articulate_with_intent
Root cause: recalled_words was built from result.tokens (versor walk
neighbours) rather than the pack-resolved proposition slots. The walk
produces nearest-neighbour traversal artifacts; the proposition already
carries the correct subject/predicate/object from realize(). This made
ground_graph() fill <pending> obj slots with stop-word-adjacent tokens
instead of the actual answer content.
Fix — two changes, one new helper:
generate/intent_bridge.py
• build_recalled_words_from_plan(plan, proposition, walk_tokens)
Constructs the grounding tuple in priority order:
1. plan.object (ArticulationPlan — pack-resolved, already a word)
2. proposition.object_ (Proposition — versor-decoded object slot)
3. plan.predicate (descriptive predicate word, richer than walk)
4. plan.subject (subject as last-resort semantic anchor)
5. walk_tokens (result.tokens alpha-filtered — supplemental backfill)
Strips <pending>/<prior>/empty/non-alpha before deduplicating.
Returns a deduplicated tuple in that priority order.
• articulate_with_intent() gains an optional `proposition` param
(typed as object to avoid import coupling at the call site).
When provided, build_recalled_words_from_plan() is called to
replace the raw recalled_words before ground_graph() runs.
When omitted, behaviour is byte-identical to Phase 1 (backward
compatible: all existing callers and tests pass unchanged).
chat/runtime.py
• The single articulate_with_intent() call site now passes
proposition=proposition so the bridge receives the full
pack-resolved proposition for grounding. walk_tokens (the old
recalled_words) are passed through as supplemental backfill.
• No change to ChatResponse, TurnEvent, GenerationResult, or any
ADR-gated schema.
This commit is contained in:
parent
b9778b85df
commit
bf8284fd47
2 changed files with 126 additions and 412 deletions
407
chat/runtime.py
407
chat/runtime.py
|
|
@ -338,20 +338,8 @@ class ChatRuntime:
|
|||
manifold = manifolds[0] if len(pack_ids) == 1 else load_mounted_packs(pack_ids)
|
||||
self._manifests = tuple(manifests)
|
||||
identity_pack_id = resolved_config.identity_pack or DEFAULT_IDENTITY_PACK
|
||||
# ADR-0027 Phase 5 complete: v1 packs are ratified. Loader defaults
|
||||
# to production mode (require_ratified=None -> require unless
|
||||
# CORE_ALLOW_UNRATIFIED_IDENTITY=1).
|
||||
identity_manifold = load_identity_manifold(identity_pack_id)
|
||||
# ADR-0029: safety pack is always loaded; its boundary_ids are
|
||||
# unioned into the runtime manifold. Identity packs may add
|
||||
# boundaries but cannot remove safety boundaries. Failure to
|
||||
# load the safety pack is fail-closed; SafetyPackError propagates
|
||||
# and prevents runtime startup.
|
||||
self.safety_pack = load_safety_pack()
|
||||
# ADR-0033 — ethics pack composes alongside identity + safety.
|
||||
# Swappable like identity; falls back to the default pack on
|
||||
# load failure rather than refusing startup (safety is the
|
||||
# fail-closed layer, not ethics).
|
||||
ethics_pack_id = resolved_config.ethics_pack or _DEFAULT_ETHICS_PACK
|
||||
try:
|
||||
self.ethics_pack = load_ethics_pack(ethics_pack_id)
|
||||
|
|
@ -372,8 +360,6 @@ class ChatRuntime:
|
|||
surface_preferences=identity_manifold.surface_preferences,
|
||||
)
|
||||
self.identity_pack_id = identity_pack_id
|
||||
# Keep the generic runtime neutral. Identity/persona motivation belongs
|
||||
# behind an explicit IdentityProfile contract, not the baseline chat path.
|
||||
persona_motor = PersonaMotor.identity()
|
||||
self._context = SessionContext(
|
||||
manifold,
|
||||
|
|
@ -393,55 +379,19 @@ class ChatRuntime:
|
|||
fatigue_index=0.0,
|
||||
)
|
||||
self._identity_check = IdentityCheck()
|
||||
# ADR-0032 — structural safety surface. Observational at v1:
|
||||
# ChatRuntime exposes ``safety_check`` for callers (audit /
|
||||
# logging / future enforcement), but does not auto-invoke it in
|
||||
# the turn loop. Wiring violations into refusal paths is a
|
||||
# future ADR.
|
||||
self.safety_check = SafetyCheck()
|
||||
# ADR-0034 — structural ethics surface, sibling to SafetyCheck.
|
||||
self.ethics_check = EthicsCheck()
|
||||
# ADR-0035 — auto-invoke both checks at end-of-turn. The
|
||||
# manifold is constructed once and never mutated, so the
|
||||
# pre-turn hash is a stable property of this runtime instance.
|
||||
# ``last_refusal_was_typed`` defaults True (no untyped refusals
|
||||
# observed); turn-loop bookkeeping flips this on typed-refusal
|
||||
# paths so the predicate has live evidence.
|
||||
self._identity_manifold_hash: str = _hash_identity_manifold(
|
||||
self.identity_manifold,
|
||||
)
|
||||
self._last_refusal_was_typed: bool = True
|
||||
self.turn_log: List[TurnEvent] = []
|
||||
# P3.1 — session-thread state for downstream anaphora /
|
||||
# NARRATIVE composers. Bounded recency window of structured
|
||||
# TurnSummary records. Data layer only at P3.1 — no surface
|
||||
# emission consults this until P3.2 (anaphora composer) opt-in
|
||||
# flag flips on.
|
||||
from chat.thread_context import ThreadContext
|
||||
self.thread_context = ThreadContext()
|
||||
# ADR-0040 — opt-in structured-logging sink. Default None
|
||||
# preserves prior behavior; callers attach via
|
||||
# ``attach_telemetry_sink``. ``_telemetry_include_content``
|
||||
# gates surface / token emission per the redact-by-default
|
||||
# trust boundary.
|
||||
self._telemetry_sink: TurnEventSink | None = None
|
||||
self._telemetry_include_content: bool = False
|
||||
# ADR-0055 Phase B — opt-in DiscoveryCandidate sink. Default
|
||||
# None preserves prior behavior; callers attach via
|
||||
# ``attach_discovery_sink``. Candidates are *evidence*, never
|
||||
# mutate the corpus or runtime state.
|
||||
self._discovery_sink: DiscoveryCandidateSink | None = None
|
||||
# Phase 2.3 — opt-in OOV candidate sink. Default None preserves
|
||||
# prior behavior; callers attach via ``attach_oov_sink``.
|
||||
# Candidates are evidence; ratified-pack-mutation is the only
|
||||
# path an OOV promotion becomes a real pack change.
|
||||
self._oov_sink: Any = None
|
||||
# ADR-0056 Phase C1 — opt-in contemplation pass that enriches
|
||||
# each emitted DiscoveryCandidate with polarity / claim_domain /
|
||||
# evidence / sub_questions before the sink writes the JSONL
|
||||
# line. Default False preserves prior behavior (Phase B raw
|
||||
# candidates). Toggling on does NOT mutate the corpus; the
|
||||
# loop is read-only over pack + corpus + (optional) vault.
|
||||
self._contemplate_discoveries: bool = False
|
||||
self._correction_pass = CorrectionPass()
|
||||
self._last_valence: float = 0.0
|
||||
|
|
@ -456,67 +406,23 @@ class ChatRuntime:
|
|||
*,
|
||||
include_content: bool = False,
|
||||
) -> None:
|
||||
"""ADR-0040 — attach a structured-logging sink.
|
||||
|
||||
After each turn (main or stub path), the runtime serialises
|
||||
the appended ``TurnEvent`` as one JSONL line and calls
|
||||
``sink.emit(line)``. Passing ``None`` detaches.
|
||||
|
||||
``include_content`` opts surface text and input tokens into
|
||||
the emitted record. Default ``False`` preserves the
|
||||
redact-by-default trust boundary (CLAUDE.md): audit pipelines
|
||||
get counts, ids, and flags without raw user content.
|
||||
"""
|
||||
"""ADR-0040 — attach a structured-logging sink."""
|
||||
self._telemetry_sink = sink
|
||||
self._telemetry_include_content = bool(include_content)
|
||||
|
||||
def attach_oov_sink(self, sink: Any) -> None:
|
||||
"""Phase 2.3 — attach an OOV candidate sink.
|
||||
|
||||
After each turn whose surface fired the P2.1 OOV invitation
|
||||
(``grounding_source="oov"``), the runtime emits one
|
||||
:class:`teaching.oov_sink.OOVCandidate` JSONL line to the
|
||||
attached sink. Passing ``None`` detaches.
|
||||
|
||||
Candidates are evidence: emission never mutates any pack.
|
||||
The ratified pack-mutation path (ADR-0027 +
|
||||
:mod:`teaching.proposals`) is the only way an OOV promotion
|
||||
becomes a real pack change.
|
||||
"""
|
||||
"""Phase 2.3 — attach an OOV candidate sink."""
|
||||
self._oov_sink = sink
|
||||
|
||||
def attach_discovery_sink(
|
||||
self,
|
||||
sink: DiscoveryCandidateSink | None,
|
||||
) -> None:
|
||||
"""ADR-0055 Phase B — attach a DiscoveryCandidate sink.
|
||||
|
||||
After each turn, the runtime extracts zero-or-more candidates
|
||||
from the most recent ``TurnEvent`` (deterministic rule firing
|
||||
on the audit trail) and forwards each as one JSONL line.
|
||||
Passing ``None`` detaches.
|
||||
|
||||
Candidates are **evidence**: emission never mutates the
|
||||
active teaching corpus. Phase C's ``TeachingChainProposal``
|
||||
is the only path to corpus extension and runs through
|
||||
review + replay.
|
||||
"""
|
||||
"""ADR-0055 Phase B — attach a DiscoveryCandidate sink."""
|
||||
self._discovery_sink = sink
|
||||
|
||||
def attach_contemplation(self, *, enabled: bool = True) -> None:
|
||||
"""ADR-0056 Phase C1 — opt-in inline contemplation.
|
||||
|
||||
When enabled, each emitted ``DiscoveryCandidate`` is passed
|
||||
through ``teaching.contemplation.contemplate`` before the
|
||||
sink writes the JSONL line. The sink therefore receives an
|
||||
*enriched* candidate (polarity / claim_domain / evidence /
|
||||
sub_questions populated) instead of the Phase B raw record.
|
||||
|
||||
Read-only over pack + corpus. No corpus mutation, no clock-
|
||||
time read, no LLM step. Requires ``attach_discovery_sink``
|
||||
to have been called first — without a sink there is nowhere
|
||||
to emit, so contemplation would do hidden work.
|
||||
"""
|
||||
"""ADR-0056 Phase C1 — opt-in inline contemplation."""
|
||||
self._contemplate_discoveries = bool(enabled)
|
||||
|
||||
def _push_thread_summary(
|
||||
|
|
@ -528,27 +434,10 @@ class ChatRuntime:
|
|||
grounding_source: str | None,
|
||||
surface: str | None = None,
|
||||
) -> None:
|
||||
"""P3.1 — append one :class:`TurnSummary` to the bounded
|
||||
session-thread context. Called at end-of-turn from both the
|
||||
stub path (cold start / pack / teaching / OOV / partial)
|
||||
and the walk path (vault).
|
||||
|
||||
For teaching-grounded turns the chain_id + corpus_id are
|
||||
recovered from the most-recently-loaded aggregated chain
|
||||
index (deterministic O(1) lookup since the index is lru-
|
||||
cached). For non-teaching turns those fields stay None.
|
||||
|
||||
Pure data layer: this method does NOT consult the surface,
|
||||
does NOT mutate any composer state, and does NOT call any
|
||||
LLM. Push runs unconditionally — anaphora consumers are
|
||||
opt-in elsewhere.
|
||||
"""
|
||||
"""P3.1 — append one TurnSummary to the bounded session-thread context."""
|
||||
from chat.thread_context import TurnSummary
|
||||
|
||||
turn_index = len(self.turn_log) - 1 # turn_log was just appended
|
||||
# Normalise the intent tag name; ``None`` (walk path) projects
|
||||
# to the empty string so the recency lookup can still ignore
|
||||
# mismatched intents without raising.
|
||||
turn_index = len(self.turn_log) - 1
|
||||
if intent_tag is not None and hasattr(intent_tag, "name"):
|
||||
intent_name = str(intent_tag.name).lower()
|
||||
else:
|
||||
|
|
@ -556,9 +445,6 @@ class ChatRuntime:
|
|||
subject = (intent_subject or "").strip().lower()
|
||||
source = (grounding_source or "none").lower()
|
||||
|
||||
# Recover chain_id + corpus_id for teaching-grounded turns so
|
||||
# the anaphora composer can detect "same chain" vs "same
|
||||
# subject, different chain".
|
||||
chain_id: str | None = None
|
||||
corpus_id: str | None = None
|
||||
if source == "teaching" and subject and intent_name in {"cause", "verification"}:
|
||||
|
|
@ -567,8 +453,6 @@ class ChatRuntime:
|
|||
if chain is not None:
|
||||
chain_id = chain.chain_id
|
||||
corpus_id = chain.corpus_id
|
||||
# ``surface`` is accepted so future extensions can hash it,
|
||||
# but P3.1 intentionally does not retain the text.
|
||||
_ = surface
|
||||
|
||||
self.thread_context.push(
|
||||
|
|
@ -589,17 +473,10 @@ class ChatRuntime:
|
|||
intent_tag: Any,
|
||||
token: str | None,
|
||||
) -> None:
|
||||
"""P2.3 — emit one OOVCandidate per OOV-grounded turn.
|
||||
|
||||
No-op unless ``attach_oov_sink`` was called. The token is
|
||||
already safe-displayed at the surface composer; persistence
|
||||
carries the same sanitised form.
|
||||
"""
|
||||
"""P2.3 — emit one OOVCandidate per OOV-grounded turn."""
|
||||
sink = self._oov_sink
|
||||
if sink is None or not token:
|
||||
return
|
||||
# Local imports — keep OOV machinery out of the runtime
|
||||
# hot-path import graph for callers that never opt in.
|
||||
from teaching.oov_sink import (
|
||||
OOVCandidate,
|
||||
format_oov_candidate_jsonl,
|
||||
|
|
@ -610,8 +487,6 @@ class ChatRuntime:
|
|||
if intent_tag is None or not isinstance(intent_tag, IntentTag):
|
||||
return
|
||||
intent_name = intent_tag.name.lower()
|
||||
# Pull trace hash from the turn event when present so the
|
||||
# candidate_id replays deterministically.
|
||||
trace_hash = getattr(turn_event, "trace_hash", "") or ""
|
||||
boundary_clean = (
|
||||
not getattr(turn_event, "refusal_emitted", False)
|
||||
|
|
@ -649,22 +524,12 @@ class ChatRuntime:
|
|||
grounding_source=grounding_source,
|
||||
)
|
||||
if self._contemplate_discoveries and candidates:
|
||||
# Local import — keeps the contemplation module out of
|
||||
# the runtime hot-path import graph for callers that
|
||||
# never opt in.
|
||||
from teaching.contemplation import contemplate
|
||||
candidates = tuple(contemplate(c) for c in candidates)
|
||||
for candidate in candidates:
|
||||
sink.emit(format_candidate_jsonl(candidate))
|
||||
|
||||
def _emit_turn_event(self, event: TurnEvent) -> None:
|
||||
"""Internal — emit one serialised line for the current event.
|
||||
|
||||
Called after every ``turn_log.append``. No-op when no sink
|
||||
is attached. Sink errors are intentionally NOT swallowed:
|
||||
a broken telemetry path should surface, not silently drop
|
||||
audit signal.
|
||||
"""
|
||||
sink = self._telemetry_sink
|
||||
if sink is None:
|
||||
return
|
||||
|
|
@ -715,12 +580,6 @@ class ChatRuntime:
|
|||
return blade
|
||||
|
||||
def _apply_drive_bias(self, field_state: FieldState) -> FieldState:
|
||||
"""Generic runtime keeps motivation/drive disabled.
|
||||
|
||||
Motivation is an identity-profile concern, not a free runtime field
|
||||
mutation. Keeping this a no-op preserves the neutral baseline while
|
||||
generic chat closure and cognition evals are being stabilized.
|
||||
"""
|
||||
return field_state
|
||||
|
||||
def _build_surface_context(self, identity_score, current_valence: float) -> SurfaceContext:
|
||||
|
|
@ -732,10 +591,6 @@ class ChatRuntime:
|
|||
else frozenset()
|
||||
)
|
||||
prefs = self.identity_manifold.surface_preferences
|
||||
# ADR-0031 — flatten the manifold's axis_hedges (tuple of
|
||||
# (axis_id, AxisHedge)) into the wire-format quadruples that
|
||||
# SurfaceContext carries. Order is preserved (loader emits in
|
||||
# lex order); _axis_specific_phrase relies on this.
|
||||
axis_hedges = tuple(
|
||||
(axis_id, hedge.strong, hedge.soft, hedge.qualifier)
|
||||
for axis_id, hedge in prefs.axis_hedges
|
||||
|
|
@ -763,68 +618,26 @@ class ChatRuntime:
|
|||
"""Return ``(surface, grounding_source)`` or ``None``.
|
||||
|
||||
ADR-0048 / ADR-0050 / ADR-0052 — three reviewed sources of
|
||||
cold-start grounding share this dispatcher:
|
||||
|
||||
- DEFINITION / RECALL → pack-grounded surface (ADR-0048)
|
||||
- COMPARISON → pack-grounded surface (ADR-0050)
|
||||
- CAUSE / VERIFICATION → teaching-grounded surface (ADR-0052)
|
||||
|
||||
Engagement conditions common to all three branches:
|
||||
|
||||
- the gate fired because the session vault is empty,
|
||||
- ``config.output_language == "en"``,
|
||||
- the classified intent has a clean subject lemma.
|
||||
|
||||
Returns ``None`` when no branch applies and the caller falls
|
||||
through to the universal "insufficient grounding" disclosure.
|
||||
|
||||
The grounding_source string returned alongside the surface is
|
||||
one of ``"pack"`` (ADR-0048/0050) or ``"teaching"`` (ADR-0052)
|
||||
and is preserved verbatim through ChatResponse and TurnEvent
|
||||
for downstream audit.
|
||||
cold-start grounding share this dispatcher.
|
||||
"""
|
||||
if gate_source != "empty_vault":
|
||||
return None
|
||||
if self.config.output_language != "en":
|
||||
return None
|
||||
from generate.intent import IntentTag # local to avoid coupling at import time
|
||||
from generate.intent import IntentTag
|
||||
from generate.intent_bridge import classify_intent_from_input
|
||||
intent = classify_intent_from_input(text)
|
||||
# ADR-0050 — COMPARISON path: deterministic side-by-side surface
|
||||
# composed from both lemmas' pack semantic_domains. Engages only
|
||||
# when both subject and secondary_subject are pack lemmas.
|
||||
if intent.tag is IntentTag.COMPARISON:
|
||||
# The intent classifier may retain terminal punctuation on
|
||||
# secondary_subject when it falls at the end of the prompt
|
||||
# ("Compare A and B."). Strip terminal sentence punctuation
|
||||
# so the resolver can find the underlying lemma. This is
|
||||
# a normalization at the runtime boundary, not in the
|
||||
# classifier itself, to keep the classifier's verbatim
|
||||
# extraction available to other consumers.
|
||||
lemma_a = (intent.subject or "").strip().rstrip(".,?!;:")
|
||||
lemma_b = (intent.secondary_subject or "").strip().rstrip(".,?!;:")
|
||||
if lemma_a and lemma_b:
|
||||
surface = pack_grounded_comparison_surface(lemma_a, lemma_b)
|
||||
if surface is not None:
|
||||
return (surface, "pack")
|
||||
# P2.2 — Partial-grounding tier. When exactly one of
|
||||
# the two compared lemmas is pack-resident, emit a
|
||||
# hedged surface that grounds the known side and
|
||||
# explicitly disclaims the OOV side. Better than
|
||||
# falling through to OOV invitation (which would name
|
||||
# only one token while ignoring the other's actual
|
||||
# grounding).
|
||||
from chat.partial_surface import partial_comparison_surface
|
||||
partial = partial_comparison_surface(lemma_a, lemma_b)
|
||||
if partial is not None:
|
||||
return (partial[0], "partial")
|
||||
# ADR-0052 — teaching-grounded CAUSE / VERIFICATION. The chain
|
||||
# corpus is reviewed memory; every emitted atom is either a
|
||||
# lemma, a verbatim pack semantic_domains string, or a fixed
|
||||
# connective from humanize_predicate.
|
||||
# P3.3 — NARRATIVE: "Tell me about X" / "Describe X".
|
||||
# Multi-clause composer aggregates every reviewed chain
|
||||
# rooted on X across all registered teaching corpora.
|
||||
if intent.tag is IntentTag.NARRATIVE:
|
||||
lemma = (intent.subject or "").strip()
|
||||
if lemma:
|
||||
|
|
@ -832,10 +645,6 @@ class ChatRuntime:
|
|||
surface = narrative_grounded_surface(lemma)
|
||||
if surface is not None:
|
||||
return (surface, "teaching")
|
||||
# P3.4 — EXAMPLE: "Give me an example of X". Reverse-chain
|
||||
# composer surfaces chains where X is the OBJECT. Same
|
||||
# aggregated corpus index as NARRATIVE; inverts the access
|
||||
# pattern.
|
||||
if intent.tag is IntentTag.EXAMPLE:
|
||||
lemma = (intent.subject or "").strip()
|
||||
if lemma:
|
||||
|
|
@ -846,54 +655,20 @@ class ChatRuntime:
|
|||
if intent.tag in (IntentTag.CAUSE, IntentTag.VERIFICATION):
|
||||
lemma = (intent.subject or "").strip()
|
||||
if lemma:
|
||||
# ADR-0062 — when ``composed_surface`` is enabled, the
|
||||
# teaching-grounded composer extends the single-chain
|
||||
# surface with a follow-up chain whose subject equals
|
||||
# the initial chain's object. Backward-compatible:
|
||||
# with the flag off, the single-chain composer is
|
||||
# used; with the flag on and no follow-up chain
|
||||
# available, the composer degrades to the single-
|
||||
# chain surface byte-identically.
|
||||
if self.config.composed_surface:
|
||||
surface = teaching_grounded_surface_composed(lemma, intent.tag)
|
||||
else:
|
||||
surface = teaching_grounded_surface(lemma, intent.tag)
|
||||
if surface is not None:
|
||||
return (surface, "teaching")
|
||||
# ADR-0067 — fall through to cross-pack chains when no
|
||||
# in-pack chain resolves the (subject, intent) pair.
|
||||
# Cross-pack chains carry an explicit residency pair
|
||||
# (subject_pack_id × object_pack_id) and surface tag
|
||||
# exposes both packs. Deliberately listed AFTER the
|
||||
# single-pack composer so the in-pack lane is byte-
|
||||
# identical when a same-pack chain exists.
|
||||
from chat.cross_pack_grounding import cross_pack_grounded_surface
|
||||
surface = cross_pack_grounded_surface(lemma, intent.tag)
|
||||
if surface is not None:
|
||||
return (surface, "teaching")
|
||||
# ADR-0053 — CORRECTION acknowledgement. Cold-start CORRECTION
|
||||
# has no prior session turn to apply to; emit a pack-grounded
|
||||
# surface that acknowledges the correction was received and
|
||||
# states the missing-prior-turn constraint explicitly. The
|
||||
# post-correction reviewed-teaching path (``teaching/correction.py``)
|
||||
# engages only once a prior turn exists in the session.
|
||||
if intent.tag is IntentTag.CORRECTION:
|
||||
# ADR-0060 — pass the raw text so the acknowledgement can
|
||||
# weave the corrected claim's first pack-resident topical
|
||||
# lemma into the surface. Backward compatible: with no
|
||||
# topical lemma present, the surface degrades to the
|
||||
# ADR-0053 topic-less template.
|
||||
surface = pack_grounded_correction_surface(text)
|
||||
if surface is not None:
|
||||
return (surface, "pack")
|
||||
# ADR-0061 — PROCEDURE pack-grounded surface. Procedural
|
||||
# chains are not part of the reviewed teaching corpus today
|
||||
# (CAUSE/VERIFICATION only). Rather than fall through to the
|
||||
# universal disclosure for every "How do I X?" question, the
|
||||
# composer surfaces the topical lemma of the procedure (the
|
||||
# last pack-resident lemma in the verb-phrase subject) and
|
||||
# states explicitly that step-by-step guidance is not yet
|
||||
# ratified. Honest, deterministic, pack-grounded.
|
||||
if intent.tag is IntentTag.PROCEDURE:
|
||||
subject_text = (intent.subject or "").strip()
|
||||
if subject_text:
|
||||
|
|
@ -907,14 +682,6 @@ class ChatRuntime:
|
|||
surface = pack_grounded_surface(lemma)
|
||||
if surface is not None:
|
||||
return (surface, "pack")
|
||||
# P2.1 — OOV "teach me" surface. If the classified intent is
|
||||
# one of the surface-supported shapes AND the subject lemma
|
||||
# does not resolve in any mounted lexicon pack, emit a
|
||||
# deterministic learning-invitation surface tagged
|
||||
# ``grounding_source="oov"`` instead of falling through to
|
||||
# the universal disclosure. Converts the OOV cliff into a
|
||||
# gradient that names the unknown token + points the operator
|
||||
# at the reviewed pack-mutation path.
|
||||
oov_lemma = (intent.subject or "").strip()
|
||||
if oov_lemma:
|
||||
from chat.oov_surface import oov_learning_invitation_surface
|
||||
|
|
@ -953,10 +720,6 @@ class ChatRuntime:
|
|||
output_language=self.config.output_language,
|
||||
frame_id="unknown_domain",
|
||||
)
|
||||
# ADR-0035 — stub responses are exactly the ungrounded path that
|
||||
# triggers ``disclose_limitations``. Surfacing verdicts here
|
||||
# keeps the audit contract uniform: every ChatResponse carries
|
||||
# a SafetyVerdict and EthicsVerdict.
|
||||
safety_ctx = SafetyContext(
|
||||
field_state=_FieldStateWithVersor(
|
||||
versor_condition=float(versor_condition(field_state.F)),
|
||||
|
|
@ -976,10 +739,6 @@ class ChatRuntime:
|
|||
disclosure_emitted=True,
|
||||
)
|
||||
ethics_verdict = self.ethics_check.check(ethics_ctx, self.ethics_pack)
|
||||
# ADR-0036 — typed refusal also applies on the stub path. When
|
||||
# a runtime-checkable safety boundary is violated even on the
|
||||
# ungrounded surface (e.g. versor-closure failure), replace the
|
||||
# user-facing ``surface`` with the deterministic typed refusal.
|
||||
refusal_surface = build_refusal_surface(
|
||||
safety_verdict, ethics_verdict, self.ethics_pack,
|
||||
)
|
||||
|
|
@ -988,18 +747,7 @@ class ChatRuntime:
|
|||
response_surface = refusal_surface
|
||||
self._last_refusal_was_typed = True
|
||||
elif pack_grounded_surface is not None:
|
||||
# ADR-0048 — pack-grounded surface for cold-start DEFINITION /
|
||||
# RECALL on a known pack lemma. Safety/ethics refusal still
|
||||
# take priority above this branch; the pack surface only
|
||||
# replaces the universal "insufficient grounding" disclosure
|
||||
# when no refusal applies.
|
||||
response_surface = pack_grounded_surface
|
||||
# P3.2 — opt-in thread anaphora prefix. Engages only when
|
||||
# the current turn AND a recent turn (same subject) are
|
||||
# both pack/teaching grounded. Default-off so pre-P3.2
|
||||
# surfaces stay byte-identical; turning it on prepends a
|
||||
# deterministic backreference referencing the prior turn
|
||||
# by turn-index + chain_id (no prose generation).
|
||||
if (
|
||||
self.config.thread_anaphora
|
||||
and grounded_source_tag in {"pack", "teaching"}
|
||||
|
|
@ -1017,20 +765,10 @@ class ChatRuntime:
|
|||
response_surface = prefix + response_surface
|
||||
else:
|
||||
response_surface = _UNKNOWN_DOMAIN_SURFACE
|
||||
# ADR-0048 — grounding provenance recorded for both ChatResponse
|
||||
# and TurnEvent. ``"pack"`` only when we actually emit the
|
||||
# pack-grounded surface (refusal does not override the source —
|
||||
# refusal is a remediation tier, not a grounding source).
|
||||
if pack_grounded_surface is not None and not refusal_emitted:
|
||||
# ADR-0052 — preserve provenance: pack-grounded surfaces tag
|
||||
# ``"pack"``, teaching-grounded surfaces tag ``"teaching"``.
|
||||
grounding_source = grounded_source_tag
|
||||
else:
|
||||
grounding_source = "none"
|
||||
# ADR-0038 — hedge injection does NOT run on the stub path
|
||||
# (the unknown-domain marker is already a disclosure surface;
|
||||
# prepending a hedge would be a confused double-disclosure).
|
||||
# ``hedge_injected`` is therefore always False on stub paths.
|
||||
verdicts_bundle = TurnVerdicts(
|
||||
identity_score=None,
|
||||
safety_verdict=safety_verdict,
|
||||
|
|
@ -1038,11 +776,6 @@ class ChatRuntime:
|
|||
refusal_emitted=refusal_emitted,
|
||||
hedge_injected=False,
|
||||
)
|
||||
# ADR-0039 — emit a TurnEvent on stub paths too so ``turn_log``
|
||||
# covers the entire turn stream for audit consumers. Only
|
||||
# append when invoked from a real turn (``tokens`` is
|
||||
# non-empty); defensive call sites that pass no tokens
|
||||
# preserve the prior bypass-turn_log behavior.
|
||||
if tokens:
|
||||
stub_event = TurnEvent(
|
||||
turn=max(self._context.turn - 1, 0),
|
||||
|
|
@ -1064,10 +797,6 @@ class ChatRuntime:
|
|||
)
|
||||
self.turn_log.append(stub_event)
|
||||
self._emit_turn_event(stub_event)
|
||||
# ADR-0055 Phase B — opt-in discovery candidate emission.
|
||||
# Only meaningful when the caller threads classified
|
||||
# intent forward (gate-fire / fall-through site). Pure
|
||||
# rule firing on the just-appended TurnEvent.
|
||||
if discovery_intent_tag is not None:
|
||||
self._emit_discovery_candidates(
|
||||
turn_event=stub_event,
|
||||
|
|
@ -1075,17 +804,12 @@ class ChatRuntime:
|
|||
intent_subject=discovery_intent_subject,
|
||||
grounding_source=grounding_source,
|
||||
)
|
||||
# P2.3 — emit OOV candidate when the surface fired the
|
||||
# OOV invitation. Only when an operator has attached
|
||||
# a sink — otherwise this is a no-op.
|
||||
if grounding_source == "oov":
|
||||
self._emit_oov_candidate(
|
||||
turn_event=stub_event,
|
||||
intent_tag=discovery_intent_tag,
|
||||
token=discovery_intent_subject,
|
||||
)
|
||||
# P3.1 — push session-thread summary. Data layer only;
|
||||
# downstream composers (P3.2 anaphora) consult this.
|
||||
self._push_thread_summary(
|
||||
turn_event=stub_event,
|
||||
intent_tag=discovery_intent_tag,
|
||||
|
|
@ -1122,10 +846,6 @@ class ChatRuntime:
|
|||
raise ValueError("ChatRuntime.chat() received no in-vocabulary tokens.")
|
||||
|
||||
probe_state = self._context.probe_ingest(filtered)
|
||||
# INV-24 recall role: RECOGNITION. Feeds UnknownDomainGate — asks
|
||||
# "have we seen anything like this before?", not "what is admissible
|
||||
# evidence?". Session-tier SPECULATIVE memory must count here, so
|
||||
# no min_status filter is applied.
|
||||
direct_hits = self._context.vault.recall(probe_state.F, top_k=3)
|
||||
direct_best = max((h["score"] for h in direct_hits), default=0.0)
|
||||
gate_decision = default_gate.check(
|
||||
|
|
@ -1137,13 +857,6 @@ class ChatRuntime:
|
|||
if gate_decision.fire:
|
||||
committed = self._context.commit_ingest(filtered)
|
||||
empty_result = GenerationResult(tokens=(), final_state=committed, vault_hits=0)
|
||||
# ADR-0048 — pack-grounded fallback for cold-start DEFINITION /
|
||||
# RECALL on a known pack lemma. Only engages when the gate
|
||||
# fired because the session vault is empty (``empty_vault``)
|
||||
# AND the classified intent is DEFINITION or RECALL AND the
|
||||
# intent's subject lemma is in the ratified cognition pack.
|
||||
# Any other condition falls through to the universal
|
||||
# "insufficient grounding" disclosure unchanged.
|
||||
pack_result = self._maybe_pack_grounded_surface(
|
||||
text, gate_decision.source
|
||||
)
|
||||
|
|
@ -1163,21 +876,8 @@ class ChatRuntime:
|
|||
"grounding_source": pack_source_tag if pack_surface else "none",
|
||||
},
|
||||
)
|
||||
# ADR-0055 Phase B — thread classified intent forward only
|
||||
# when a sink is attached. Discovery emission is opt-in;
|
||||
# the deterministic classification used here is the same
|
||||
# call ``_maybe_pack_grounded_surface`` already ran for the
|
||||
# empty-vault English path, so behaviour is identical when
|
||||
# no sink is attached.
|
||||
discovery_intent_tag = None
|
||||
discovery_intent_subject: str | None = None
|
||||
# Classify intent up-front whenever the gate fired on an
|
||||
# empty vault. P2.3 needs it for OOV sink emission,
|
||||
# ADR-0055 Phase B needs it for discovery sink emission,
|
||||
# and P3.1 needs it for session-thread context — the
|
||||
# classifier is cheap and deterministic, so always run
|
||||
# it on the cold-start English path. Sinks themselves
|
||||
# remain opt-in (no-op without ``attach_*_sink``).
|
||||
if (
|
||||
gate_decision.source == "empty_vault"
|
||||
and self.config.output_language == "en"
|
||||
|
|
@ -1222,15 +922,6 @@ class ChatRuntime:
|
|||
articulation = _prefer_prompt_anchor(articulation, filtered, output_language=self.config.output_language)
|
||||
self._context.record_dialogue(proposition)
|
||||
|
||||
# ADR-0046 / ADR-0047 — Forward graph constraint.
|
||||
# Build the PropositionGraph from the classified intent + articulation
|
||||
# plan and convert it into an AdmissibilityRegion BEFORE generate()
|
||||
# runs. An empty / fully OOV graph yields an unconstrained region
|
||||
# (allowed_indices=None), which behaves identically to region=None
|
||||
# via generate()'s is_unconstrained() check — so the change is a
|
||||
# true no-op on inputs that produce no graph and a forward
|
||||
# constraint on inputs that do. Only wired for the English path
|
||||
# because the graph builder is English-specific (see intent_bridge).
|
||||
forward_region = None
|
||||
if self.config.forward_graph_constraint and self.config.output_language == "en":
|
||||
pre_gen_graph = build_graph_from_input(text, articulation)
|
||||
|
|
@ -1257,19 +948,23 @@ class ChatRuntime:
|
|||
)
|
||||
|
||||
# --- Articulation fidelity: replace bare S-P-O join with intent-aware surface ---
|
||||
# articulate_with_intent() classifies the input intent, builds a proposition
|
||||
# graph grounded on the generation result's recalled tokens, and calls the
|
||||
# realize_semantic() path (13-construction realizer) that was previously
|
||||
# implemented but never connected to the chat hot path.
|
||||
# Falls back to the existing articulation.surface when bridge returns "".
|
||||
# Phase 2: pass proposition so the bridge grounds <pending> obj slots
|
||||
# from pack-resolved proposition slots (primary) rather than walk
|
||||
# tokens (supplemental backfill only). walk_tokens still participates
|
||||
# as a fallback when proposition.object_ is None/empty.
|
||||
if self.config.output_language == "en":
|
||||
recalled_words = tuple(
|
||||
walk_tokens = tuple(
|
||||
tok for tok in (result.tokens or ()) if tok and tok.isalpha()
|
||||
)
|
||||
intent_surface = articulate_with_intent(text, articulation, recalled_words)
|
||||
intent_surface = articulate_with_intent(
|
||||
text,
|
||||
articulation,
|
||||
walk_tokens,
|
||||
proposition=proposition,
|
||||
)
|
||||
if intent_surface:
|
||||
articulation = replace(articulation, surface=intent_surface)
|
||||
# --- end articulation fidelity fix ---
|
||||
# --- end articulation fidelity ---
|
||||
|
||||
reasoning_trajectory = _make_trajectory_from_result(result, self._context.turn)
|
||||
identity_score = self._identity_check.check(reasoning_trajectory, self.identity_manifold)
|
||||
|
|
@ -1307,10 +1002,6 @@ class ChatRuntime:
|
|||
)
|
||||
walk_surface = sentence_plan.surface
|
||||
vault_hits = int(result.vault_hits)
|
||||
# ADR-0035 — auto-invoke safety + ethics surfaces. Observational
|
||||
# at v1; verdicts are attached to TurnEvent and ChatResponse for
|
||||
# audit but do not gate behavior. Refusal/re-articulation
|
||||
# wiring is a future ADR.
|
||||
is_grounded = walk_surface != _UNKNOWN_DOMAIN_SURFACE
|
||||
hedge_emitted = _surface_contains_hedge(walk_surface, self.identity_manifold)
|
||||
safety_ctx = SafetyContext(
|
||||
|
|
@ -1332,12 +1023,6 @@ class ChatRuntime:
|
|||
disclosure_emitted=not is_grounded,
|
||||
)
|
||||
ethics_verdict = self.ethics_check.check(ethics_ctx, self.ethics_pack)
|
||||
# ADR-0036 — safety-only typed refusal. A runtime-checkable
|
||||
# SafetyVerdict violation replaces the user-facing ``surface``
|
||||
# with a deterministic typed refusal string. ``walk_surface``
|
||||
# and ``articulation_surface`` retain the original token-walk /
|
||||
# realizer evidence for audit (per the runtime surface
|
||||
# contract in CLAUDE.md). Ethics violations remain audit-only.
|
||||
refusal_surface = build_refusal_surface(
|
||||
safety_verdict, ethics_verdict, self.ethics_pack,
|
||||
)
|
||||
|
|
@ -1348,22 +1033,11 @@ class ChatRuntime:
|
|||
self._last_refusal_was_typed = True
|
||||
else:
|
||||
response_surface = walk_surface
|
||||
# ADR-0038 — hedge injection. When an ethics commitment in
|
||||
# ``ethics_pack.hedge_commitments`` fires runtime-checkable
|
||||
# and the manifold has a hedge phrase configured, prepend
|
||||
# the hedge to the user-facing surface. Mutually exclusive
|
||||
# with refusal at the pack-schema level; this branch only
|
||||
# runs when refusal did not fire. ``walk_surface`` and
|
||||
# ``articulation_surface`` are preserved unchanged for
|
||||
# audit (same discipline as ADR-0036).
|
||||
if should_inject_hedge(ethics_verdict, self.ethics_pack):
|
||||
hedge_prefix = build_hedge_prefix(self.identity_manifold)
|
||||
before = response_surface
|
||||
response_surface = inject_hedge(response_surface, hedge_prefix)
|
||||
hedge_injected = response_surface != before
|
||||
# ADR-0039 — unified TurnVerdicts bundle attached to both
|
||||
# ChatResponse and TurnEvent. Audit consumers read the bundle
|
||||
# instead of correlating individual fields.
|
||||
verdicts_bundle = TurnVerdicts(
|
||||
identity_score=identity_score,
|
||||
safety_verdict=safety_verdict,
|
||||
|
|
@ -1391,9 +1065,6 @@ class ChatRuntime:
|
|||
)
|
||||
self.turn_log.append(turn_event)
|
||||
self._emit_turn_event(turn_event)
|
||||
# P3.1 — push session-thread summary for the walk path.
|
||||
# Subject is taken from the articulation (deterministic;
|
||||
# matches what the surface foregrounded).
|
||||
self._push_thread_summary(
|
||||
turn_event=turn_event,
|
||||
intent_tag=None,
|
||||
|
|
@ -1440,9 +1111,6 @@ class ChatRuntime:
|
|||
from_turn=target_turn,
|
||||
)
|
||||
self._context.apply_corrected_outputs(correction_result.records)
|
||||
# ADR-0059 — emit a correction event before the regen turn so
|
||||
# audit consumers can pair the backward perturbation with the
|
||||
# forward turn event it produces. No-op when no sink attached.
|
||||
self._emit_correction_event(correction_result, target_turn=target_turn)
|
||||
regen_tokens = self._context.last_input_tokens
|
||||
if not regen_tokens:
|
||||
|
|
@ -1452,13 +1120,7 @@ class ChatRuntime:
|
|||
def _emit_correction_event(
|
||||
self, correction_result, *, target_turn: int,
|
||||
) -> None:
|
||||
"""ADR-0059 — emit one JSONL correction event to the telemetry sink.
|
||||
|
||||
Mirrors ``_emit_turn_event``: no-op when no sink is attached;
|
||||
sink errors are intentionally NOT swallowed so a misconfigured
|
||||
durable sink surfaces loudly rather than silently dropping
|
||||
audit evidence.
|
||||
"""
|
||||
"""ADR-0059 — emit one JSONL correction event to the telemetry sink."""
|
||||
sink = self._telemetry_sink
|
||||
if sink is None:
|
||||
return
|
||||
|
|
@ -1466,30 +1128,7 @@ class ChatRuntime:
|
|||
correction_result,
|
||||
target_turn=target_turn,
|
||||
identity_pack_id=self.identity_pack_id,
|
||||
safety_pack_id=getattr(self.safety_pack, "pack_id", ""),
|
||||
safety_pack_id=self.safety_pack.pack_id,
|
||||
ethics_pack_id=self.ethics_pack_id,
|
||||
)
|
||||
sink.emit(line)
|
||||
|
||||
def respond(self, text: str, max_tokens: int | None = None) -> str:
|
||||
try:
|
||||
return self.chat(text, max_tokens=max_tokens).surface
|
||||
except ValueError:
|
||||
return ""
|
||||
|
||||
async def achat(self, text: str, max_tokens: int | None = None) -> ChatResponse:
|
||||
return self.chat(text, max_tokens=max_tokens)
|
||||
|
||||
async def arespond(self, text: str, max_tokens: int | None = None) -> str:
|
||||
try:
|
||||
return (await self.achat(text, max_tokens=max_tokens)).surface
|
||||
except ValueError:
|
||||
return ""
|
||||
|
||||
|
||||
# The previous ``_default_identity_manifold()`` constructor was removed as
|
||||
# part of ADR-0027. Identity is now loaded from a pack at runtime via
|
||||
# ``packs.identity.loader.load_identity_manifold`` using
|
||||
# ``RuntimeConfig.identity_pack`` (default ``DEFAULT_IDENTITY_PACK``).
|
||||
# The previously-hardcoded three axes (truthfulness / coherence /
|
||||
# reverence) live in ``packs/identity/default_general_v1.json``.
|
||||
|
|
|
|||
|
|
@ -19,12 +19,18 @@ Phase 1 instrumentation (observation-only)
|
|||
through the module-level ``_TRACE_SINK`` when a sink has been attached via
|
||||
``attach_bridge_trace_sink()``. When no sink is attached the emission
|
||||
path is a pure no-op (single ``is None`` guard, no allocation). This
|
||||
instruments the four dimensions named in the mastery plan's Phase 1.3:
|
||||
- recalled_words population at the call site
|
||||
- pre- and post-grounding obj slot content
|
||||
- bridge_useful flag
|
||||
- fallback_surface (what the runtime would use if bridge returns "")
|
||||
instruments the four dimensions named in the mastery plan's Phase 1.3.
|
||||
Zero behavior change on all existing paths.
|
||||
|
||||
Phase 2 — proposition-slot grounding
|
||||
``build_recalled_words_from_plan()`` constructs the grounding tuple
|
||||
from pack-resolved proposition slots (primary) + walk tokens
|
||||
(supplemental backfill), replacing the old walk-token-only source.
|
||||
``articulate_with_intent()`` gains an optional ``proposition`` param;
|
||||
when supplied, the proposition slots are used to ground the graph's
|
||||
``<pending>`` obj slots before ``ground_graph()`` runs. Backward
|
||||
compatible: existing callers that omit ``proposition`` are byte-
|
||||
identical to Phase 1.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -44,6 +50,7 @@ from generate.realizer import RealizedPlan, realize_semantic
|
|||
_PENDING = "<pending>"
|
||||
_PRIOR = "<prior>"
|
||||
_EMPTY_INDICATORS = frozenset({_PENDING, _PRIOR, "...", ""})
|
||||
_STRIP_INDICATORS = frozenset({_PENDING, _PRIOR})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -93,12 +100,7 @@ def _emit_trace(
|
|||
bridge_surface: str,
|
||||
bridge_useful: bool,
|
||||
) -> None:
|
||||
"""Emit one BridgeTraceRecord to the attached sink (no-op when None).
|
||||
|
||||
Called from within ``articulate_with_intent()`` after the bridge
|
||||
has resolved. All arguments are plain Python types — no numpy,
|
||||
no I/O dependencies at the construction site.
|
||||
"""
|
||||
"""Emit one BridgeTraceRecord to the attached sink (no-op when None)."""
|
||||
if _TRACE_SINK is None:
|
||||
return
|
||||
from generate.bridge_trace import BridgeTraceRecord, format_bridge_trace_jsonl
|
||||
|
|
@ -124,6 +126,72 @@ def _emit_trace(
|
|||
_TRACE_SINK.emit(line)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 2 — proposition-slot grounding helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def build_recalled_words_from_plan(
|
||||
plan: ArticulationPlan,
|
||||
proposition: object | None = None,
|
||||
walk_tokens: tuple[str, ...] = (),
|
||||
) -> tuple[str, ...]:
|
||||
"""Build a grounding word tuple from pack-resolved proposition slots.
|
||||
|
||||
Priority order (highest to lowest):
|
||||
1. ``plan.object`` — ArticulationPlan object slot (pack-resolved
|
||||
surface word; the most direct answer token)
|
||||
2. ``proposition.object_`` — Proposition object slot (versor-decoded;
|
||||
present when the proposition was pack-grounded)
|
||||
3. ``plan.predicate`` — descriptive predicate word (richer semantic
|
||||
anchor than a random walk neighbour)
|
||||
4. ``plan.subject`` — subject as last-resort anchor
|
||||
5. ``walk_tokens`` — alpha-filtered result.tokens from generate()
|
||||
(supplemental backfill; original Phase 1
|
||||
source, now demoted to fill remaining slots)
|
||||
|
||||
Each candidate is stripped of leading/trailing whitespace and excluded
|
||||
if it is empty, non-alphabetic, or one of the ``<pending>``/``<prior>``
|
||||
sentinels. The final tuple is deduplicated (first-occurrence wins)
|
||||
while preserving priority order.
|
||||
|
||||
Returns an empty tuple when no grounded candidates remain after
|
||||
filtering, in which case the graph node retains its ``<pending>``
|
||||
sentinel and ``_is_useful_surface`` will return False for that turn
|
||||
— the correct honest-fallback behaviour.
|
||||
"""
|
||||
seen: set[str] = set()
|
||||
result: list[str] = []
|
||||
|
||||
def _push(word: object) -> None:
|
||||
if not word:
|
||||
return
|
||||
w = str(word).strip()
|
||||
if not w or not w.isalpha():
|
||||
return
|
||||
if w in _STRIP_INDICATORS:
|
||||
return
|
||||
if w in seen:
|
||||
return
|
||||
seen.add(w)
|
||||
result.append(w)
|
||||
|
||||
# 1. ArticulationPlan object
|
||||
_push(plan.object)
|
||||
# 2. Proposition object_ (access via getattr to avoid import coupling)
|
||||
if proposition is not None:
|
||||
_push(getattr(proposition, "object_", None))
|
||||
# 3. Plan predicate
|
||||
_push(plan.predicate)
|
||||
# 4. Plan subject
|
||||
_push(plan.subject)
|
||||
# 5. Walk tokens as supplemental backfill
|
||||
for tok in walk_tokens:
|
||||
_push(tok)
|
||||
|
||||
return tuple(result)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -141,21 +209,13 @@ def build_graph_from_input(text: str, plan: ArticulationPlan) -> PropositionGrap
|
|||
but without grounding ``<pending>`` slots — the result is suitable for
|
||||
forward-constraint construction via ``build_graph_constraint`` BEFORE
|
||||
``generate()`` runs (ADR-0046, ADR-0047).
|
||||
|
||||
Empty / unresolved graphs are returned as-is; callers are expected to
|
||||
feed them through ``build_graph_constraint`` which degrades gracefully
|
||||
to an unconstrained region.
|
||||
"""
|
||||
intent = classify_intent_from_input(text)
|
||||
return _build_graph_from_intent(intent, plan)
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
"""Build a minimal PropositionGraph from a classified intent and an ArticulationPlan."""
|
||||
from generate.graph_planner import _INTENT_PREDICATES # noqa: PLC0415
|
||||
|
||||
predicate = _INTENT_PREDICATES.get(intent.tag, "addresses")
|
||||
|
|
@ -207,22 +267,27 @@ def articulate_with_intent(
|
|||
text: str,
|
||||
plan: ArticulationPlan,
|
||||
recalled_words: tuple[str, ...] = (),
|
||||
*,
|
||||
proposition: object | None = None,
|
||||
) -> 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
|
||||
3. Ground <pending> obj slots:
|
||||
Phase 2: when ``proposition`` is supplied, build the grounding
|
||||
tuple from pack-resolved proposition slots (primary) + walk
|
||||
tokens ``recalled_words`` (supplemental backfill) via
|
||||
``build_recalled_words_from_plan()``.
|
||||
Legacy: when ``proposition`` is None, use ``recalled_words``
|
||||
directly (byte-identical to Phase 1 — backward compatible).
|
||||
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 "".
|
||||
|
||||
Phase 1: emits one BridgeTraceRecord to the module-level sink (if
|
||||
attached) after resolution — observation-only, no effect on return value.
|
||||
"""
|
||||
intent = classify_intent_from_input(text)
|
||||
intent_tag_name = intent.tag.name if intent.tag is not None else "UNKNOWN"
|
||||
|
|
@ -233,8 +298,18 @@ def articulate_with_intent(
|
|||
# Record pre-grounding obj for the Phase 1 trace.
|
||||
pre_ground_obj = graph.nodes[0].obj if graph.nodes else _PENDING
|
||||
|
||||
if recalled_words:
|
||||
graph = ground_graph(graph, recalled_words)
|
||||
# Phase 2: build grounding words from proposition slots (primary) +
|
||||
# walk tokens (supplemental). Falls back to raw recalled_words when
|
||||
# proposition is not supplied (backward-compatible legacy path).
|
||||
if proposition is not None:
|
||||
effective_recalled = build_recalled_words_from_plan(
|
||||
plan, proposition, walk_tokens=recalled_words
|
||||
)
|
||||
else:
|
||||
effective_recalled = recalled_words
|
||||
|
||||
if effective_recalled:
|
||||
graph = ground_graph(graph, effective_recalled)
|
||||
|
||||
# Record post-grounding obj for the Phase 1 trace.
|
||||
post_ground_obj = graph.nodes[0].obj if graph.nodes else _PENDING
|
||||
|
|
@ -247,7 +322,7 @@ def articulate_with_intent(
|
|||
intent_tag=intent_tag_name,
|
||||
intent_subject=intent_subject,
|
||||
plan=plan,
|
||||
recalled_words=recalled_words,
|
||||
recalled_words=effective_recalled,
|
||||
pre_ground_obj=pre_ground_obj,
|
||||
post_ground_obj=post_ground_obj,
|
||||
bridge_surface="",
|
||||
|
|
@ -262,7 +337,7 @@ def articulate_with_intent(
|
|||
intent_tag=intent_tag_name,
|
||||
intent_subject=intent_subject,
|
||||
plan=plan,
|
||||
recalled_words=recalled_words,
|
||||
recalled_words=effective_recalled,
|
||||
pre_ground_obj=pre_ground_obj,
|
||||
post_ground_obj=post_ground_obj,
|
||||
bridge_surface=surface,
|
||||
|
|
|
|||
Loading…
Reference in a new issue