Closes the user-flagged scope gap: every previous fluency lane (Phase 5.1 + 5.4-5.7 + grammatical_coverage) operates on 3-word SVO probes. These three pieces stress paragraph-scale generation, give per-stage latency visibility, and expose the realizer's word-choice geometry — all on top of the existing deterministic infrastructure. # discourse_paragraph lane (paragraph-scale fluency) Forces the realizer to emit multi-sentence paragraphs from a multi-step ArticulationTarget with rhetorical moves (ASSERT, SEQUENCE, ELABORATE, CONTRAST). Same realizer, much richer input — every case is 3-5 sentences with deterministic discourse markers. Public 12 cases / holdouts 5 / dev 1 across 12 + 5 topic chains (epistemic, scientific method, creation arc, logical dependency, ethical grounding, linguistic layers, mathematical chain, narrative, biology, physics, two contrast-shaped, musical, social, computational, psychological, economic). Sub-metrics per case: - sentence count (within min..max window) - subject coverage rate - discourse marker presence (next / furthermore / in contrast) - sentence-initial capitalization - replay determinism (run twice, surfaces match) Result: 12/12 public + 5/5 holdouts at 100%, replay rate 100%, mean sentence count 4. # Realizer capitalization (G4, addresses user-flagged concern) generate/realizer.py gains `_capitalize_sentence` + `_join_as_paragraph` helpers. Sentence-initial alphabetic characters are now uppercased (skipping leading whitespace/punctuation). Surfaces went from "wisdom grounds knowledge. next, knowledge requires evidence." to "Wisdom grounds knowledge. Next, knowledge requires evidence." The discourse_paragraph runner ships a strict per-sentence capitalization check so future regressions get caught. # Pipeline-stage profiler (benchmarks/pipeline_profiler.py) External monkey-patch wrapper around CognitiveTurnPipeline.run() that records per-stage ns budgets without editing any pipeline source. Stages: intent, graph_planner, realize_semantic, runtime_chat, maybe_transitive_walk, fold_walk_into_surface, run_teaching, trace_hash. API: `profile_turn(pipeline, text) -> ProfileReport` with `.stages: dict`, `.total_ns: int`, `.as_dict()`. Empirical: runtime_chat dominates >99% on the runtime hot path (which is correct — that's where ingest + propagate + recall + articulate all happen). Future optimisation work has a clear per-stage signal. # Word-selection tracer (benchmarks/word_selection_tracer.py) External wrapper around generate.articulation._resolve_slot that records every nearest-neighbor lookup as a WordSelectionStep: - slot (subject/predicate/object) - input versor (32-d copy) - top-K candidate words by CGA inner product - chosen word + morphology - output language Top-K scoring uses the diagonal Cl(4,1) metric kernel from algebra.backend (same vectorised path vault_recall uses), not a per-word Python loop over cga_inner. No approximation, exact deterministic ranking, bit-identical to a scalar scan. API: `trace_realization(pipeline, text) -> RealizationTrace` with `.steps`, `.realization_steps`, `.surface`, `.as_dict()`. # CLI lane registration Cognition suite now sweeps the benchmark profiler/tracer tests (test_benchmarks_profiler.py) so any future regression in the instrumentation surfaces immediately. # Constraints honoured - Zero edits to core/, chat/, vault/, teaching/, language_packs/, or the algebra hot path. All instrumentation is external monkey-patch with originals restored in finally. - discourse_paragraph runner bypasses ChatRuntime grounding (named v2 gap) so paragraph capability is isolated to the realizer. - No semantic changes; no hidden normalisation; no approximate recall. # Lane health smoke 55, runtime 19, teaching 17, packs 6, cognition 105 (was 103), algebra 132. All Phase 5 fluency lanes still 100% with the capitalised surfaces (rubric is case-insensitive). discourse_paragraph 100%. # What ships next (named v2) - Round-trip: discourse_paragraph through ChatRuntime end-to-end, not just realize_target. - Per-sentence grammatical_coverage rubric on each emitted sentence. - Longer chains (10/20/50 sentences) with per-sentence determinism scaling curves. - compose_relations operator to lift compositionality recall from 68.8% toward 100%.
266 lines
9.9 KiB
Python
266 lines
9.9 KiB
Python
"""Word-selection tracer for the articulation/realization path.
|
|
|
|
Captures every nearest-neighbor vocabulary lookup performed during a turn:
|
|
- slot name (subject / predicate / object)
|
|
- input versor (32-d float vector, copied)
|
|
- top-K candidate words by CGA inner product score
|
|
- chosen word
|
|
- any morphology applied
|
|
|
|
Also records each realization step (subject, predicate, object, tense,
|
|
aspect, plural, negation) emitted by ``realize_semantic`` / ``realize_target``.
|
|
|
|
External instrumentation only — instruments via module-level function
|
|
swaps that are reverted in ``finally``. No edits to generate/, vocab/,
|
|
or algebra/ source files.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
import numpy as np
|
|
|
|
from algebra.backend import _CGA_INNER_METRIC # diagonal Cl(4,1) metric (±1 per blade)
|
|
from chat.runtime import ChatRuntime
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WordSelectionStep:
|
|
"""A single nearest-neighbor lookup observed during articulation."""
|
|
|
|
slot: str # 'subject' | 'predicate' | 'object'
|
|
input_versor: np.ndarray # shape (32,), copy — safe to retain
|
|
top_candidates: tuple[tuple[str, float], ...] # (word, cga_inner_score)
|
|
chosen: str
|
|
morphology: dict[str, Any] # tense/aspect/plural/negation/lemma/surface, if any
|
|
output_language: str
|
|
|
|
def as_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"slot": self.slot,
|
|
"top_candidates": [list(c) for c in self.top_candidates],
|
|
"chosen": self.chosen,
|
|
"morphology": dict(self.morphology),
|
|
"output_language": self.output_language,
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RealizationStep:
|
|
"""A semantic realization step (subject/predicate/object + morphology)."""
|
|
|
|
subject: str
|
|
predicate: str
|
|
obj: str | None
|
|
tense: str | None
|
|
aspect: str | None
|
|
negated: bool
|
|
quantifier: str | None
|
|
move: str
|
|
|
|
def as_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"subject": self.subject,
|
|
"predicate": self.predicate,
|
|
"obj": self.obj,
|
|
"tense": self.tense,
|
|
"aspect": self.aspect,
|
|
"negated": self.negated,
|
|
"quantifier": self.quantifier,
|
|
"move": self.move,
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class RealizationTrace:
|
|
"""Full trace from one turn: word selections + realization steps."""
|
|
|
|
steps: list[WordSelectionStep] = field(default_factory=list)
|
|
realization_steps: list[RealizationStep] = field(default_factory=list)
|
|
surface: str = ""
|
|
|
|
def as_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"steps": [s.as_dict() for s in self.steps],
|
|
"realization_steps": [r.as_dict() for r in self.realization_steps],
|
|
"surface": self.surface,
|
|
}
|
|
|
|
|
|
def _morphology_summary(vocab: Any, word: str) -> dict[str, Any]:
|
|
"""Extract morphology fields for a word, returning an empty dict if none."""
|
|
entry = vocab.morphology_for_word(word)
|
|
if entry is None:
|
|
return {}
|
|
summary: dict[str, Any] = {}
|
|
# MorphologyEntry fields vary; collect any present attributes.
|
|
for attr in ("lemma", "surface", "tense", "aspect", "plural", "number", "negation", "person", "gender", "pos"):
|
|
value = getattr(entry, attr, None)
|
|
if value is not None:
|
|
summary[attr] = value
|
|
return summary
|
|
|
|
|
|
def _topk_candidates(
|
|
vocab: Any,
|
|
versor: np.ndarray,
|
|
candidate_indices: np.ndarray,
|
|
k: int = 5,
|
|
) -> tuple[tuple[str, float], ...]:
|
|
"""Compute top-K candidates by CGA inner product over the candidate set.
|
|
|
|
Vectorised via the diagonal Cl(4,1) metric — same kernel as
|
|
``algebra.backend.vault_recall``. Exact, deterministic, no approximation.
|
|
Used only for tracing; never fed back into the realizer's surface.
|
|
"""
|
|
if len(candidate_indices) == 0:
|
|
return ()
|
|
idx = np.asarray(candidate_indices, dtype=np.int64)
|
|
# Stack candidate versors into one (N, 32) matrix; the vocab stores
|
|
# them as a list of 32-vectors.
|
|
versors_list = [vocab._versors[int(i)] for i in idx]
|
|
M = np.asarray(versors_list, dtype=np.float32)
|
|
q = np.asarray(versor, dtype=np.float32).reshape(-1)
|
|
# Diagonal weighted dot-product, vectorised serial fold (same
|
|
# component order as scalar cga_inner so scores are bit-identical
|
|
# to the per-versor scan we replaced).
|
|
scores = np.zeros(M.shape[0], dtype=np.float32)
|
|
for c in range(M.shape[1]):
|
|
scores += (_CGA_INNER_METRIC[c] * M[:, c]) * q[c]
|
|
k_eff = max(1, min(int(k), scores.shape[0]))
|
|
if k_eff < scores.shape[0]:
|
|
cand = np.argpartition(-scores, k_eff - 1)[:k_eff]
|
|
else:
|
|
cand = np.arange(scores.shape[0])
|
|
order = np.lexsort((cand, -scores[cand]))
|
|
cand = cand[order]
|
|
return tuple(
|
|
(vocab._words[int(idx[int(c)])], float(scores[int(c)]))
|
|
for c in cand
|
|
)
|
|
|
|
|
|
def trace_realization(
|
|
runtime_or_pipeline: Any,
|
|
text: str,
|
|
*,
|
|
top_k: int = 5,
|
|
max_tokens: int | None = None,
|
|
) -> RealizationTrace:
|
|
"""Run one chat turn (or pipeline turn) while tracing every word lookup.
|
|
|
|
Accepts either a ``ChatRuntime`` (calls ``.chat``) or a
|
|
``CognitiveTurnPipeline`` (calls ``.run``). A pipeline is preferred
|
|
because the pipeline path invokes ``realize_semantic`` even when the
|
|
runtime's unknown-domain gate fires, so realization steps are captured
|
|
regardless of grounding.
|
|
|
|
Instruments ``generate.articulation._resolve_slot`` and
|
|
``generate.realizer.realize_semantic`` for the duration of this call,
|
|
then restores them. Does NOT modify the realizer/articulation source.
|
|
"""
|
|
trace = RealizationTrace()
|
|
|
|
from generate import articulation as articulation_mod
|
|
from generate import realizer as realizer_mod
|
|
|
|
orig_resolve_slot = articulation_mod._resolve_slot
|
|
orig_candidate_indices = articulation_mod._candidate_indices
|
|
orig_surface_for_word = articulation_mod._surface_for_word
|
|
orig_realize_semantic = realizer_mod.realize_semantic
|
|
orig_resolve_obj = realizer_mod._resolve_obj
|
|
|
|
# Track slot order within a single realize() call. Reset on every
|
|
# articulation.realize() entry; resolve_slot has no slot label itself,
|
|
# so we synthesize it from invocation order: subject, predicate, object.
|
|
slot_state: dict[str, int] = {"counter": 0}
|
|
_SLOT_ORDER = ("subject", "predicate", "object")
|
|
|
|
def traced_resolve_slot(
|
|
versor: np.ndarray | None,
|
|
vocab: Any,
|
|
output_language: str,
|
|
) -> str | None:
|
|
slot_idx = slot_state["counter"]
|
|
slot_state["counter"] = slot_idx + 1
|
|
slot_name = _SLOT_ORDER[slot_idx] if slot_idx < len(_SLOT_ORDER) else f"slot_{slot_idx}"
|
|
if versor is None:
|
|
return None
|
|
cand = orig_candidate_indices(vocab, output_language)
|
|
chosen_word, _chosen_idx = vocab.nearest(versor, candidate_indices=cand)
|
|
top = _topk_candidates(vocab, versor, cand, k=top_k)
|
|
morph = _morphology_summary(vocab, chosen_word)
|
|
trace.steps.append(
|
|
WordSelectionStep(
|
|
slot=slot_name,
|
|
input_versor=np.asarray(versor, dtype=float).copy(),
|
|
top_candidates=top,
|
|
chosen=chosen_word,
|
|
morphology=morph,
|
|
output_language=output_language,
|
|
)
|
|
)
|
|
return orig_surface_for_word(vocab, chosen_word)
|
|
|
|
# Reset slot counter at each realize() entry. Patch articulation.realize
|
|
# via a wrapper that resets the slot_state counter before delegating.
|
|
orig_realize = articulation_mod.realize
|
|
|
|
def traced_realize(*args: Any, **kwargs: Any) -> Any:
|
|
slot_state["counter"] = 0
|
|
return orig_realize(*args, **kwargs)
|
|
|
|
def traced_realize_semantic(target: Any, graph: Any = None) -> Any:
|
|
plan = orig_realize_semantic(target, graph)
|
|
# Record the realization steps directly from the target/graph
|
|
# without re-running the realizer.
|
|
if target is not None and target.steps:
|
|
for step in target.steps:
|
|
obj = orig_resolve_obj(step, graph) if graph is not None else None
|
|
trace.realization_steps.append(
|
|
RealizationStep(
|
|
subject=step.subject,
|
|
predicate=step.predicate,
|
|
obj=obj,
|
|
tense=step.tense,
|
|
aspect=step.aspect,
|
|
negated=step.negated,
|
|
quantifier=step.quantifier,
|
|
move=step.move.value,
|
|
)
|
|
)
|
|
return plan
|
|
|
|
articulation_mod._resolve_slot = traced_resolve_slot
|
|
articulation_mod.realize = traced_realize
|
|
realizer_mod.realize_semantic = traced_realize_semantic
|
|
|
|
# Also patch the symbol referenced by the pipeline module, since it
|
|
# was imported by name at module load time.
|
|
try:
|
|
from core.cognition import pipeline as pipeline_mod
|
|
orig_pipeline_realize_semantic = pipeline_mod.realize_semantic
|
|
pipeline_mod.realize_semantic = traced_realize_semantic
|
|
except ImportError:
|
|
pipeline_mod = None
|
|
orig_pipeline_realize_semantic = None
|
|
|
|
try:
|
|
if hasattr(runtime_or_pipeline, "run") and hasattr(runtime_or_pipeline, "runtime"):
|
|
# CognitiveTurnPipeline
|
|
result = runtime_or_pipeline.run(text, max_tokens=max_tokens)
|
|
trace.surface = result.articulation_surface or result.surface or ""
|
|
else:
|
|
# ChatRuntime
|
|
response = runtime_or_pipeline.chat(text, max_tokens=max_tokens)
|
|
trace.surface = response.articulation_surface or response.surface or ""
|
|
finally:
|
|
articulation_mod._resolve_slot = orig_resolve_slot
|
|
articulation_mod.realize = orig_realize
|
|
realizer_mod.realize_semantic = orig_realize_semantic
|
|
if pipeline_mod is not None and orig_pipeline_realize_semantic is not None:
|
|
pipeline_mod.realize_semantic = orig_pipeline_realize_semantic
|
|
|
|
return trace
|