fractus-vorax / tests /test_answer_lock.py
thefinalboss's picture
Fractus-Vorax v1.0.0 — the takeover: sealed CTE brain + ingestion organs + mechanical speech (199 tests, honest floors)
1da7ac7 verified
Raw
History Blame Contribute Delete
13.2 kB
# tests/test_answer_lock.py
"""Plan 7 — le verrou de réponse : les organes articulent à travers le noyau.
Quatre angles :
- ``speak(..., answer_lock, answer_boost)`` : à l'étape i, +boost sur
answer_lock[i] (APRÈS z-norm/rep-pen, AVANT top-k/p) — boost par défaut
30, CALIBRÉ PAR MESURE (le 12 du plan perd le tirage quand le token
verrouillé vit à −1σ et le top à +4σ : z[' par']=−3.0 vs zmax=11.9 sur
le 1B — 2/4 capitales seulement ; le tiny core non-entraîné porte les
mêmes outliers froids, zmax 17-20) : le token verrouillé est choisi à
CHAQUE étape i, 10 seeds sur 10 ;
- ``Brain.speak_answer`` : la liste COMPLÈTE des tokens BPE de " "+réponse
(tokyo pour japan), ANALOGIE en repli, LACUNE -> [] ;
- ``:say`` sans noyau : la dégradation honnête (mingw-safe, jamais torch) ;
- intégration tiny : REPL + tiny CteCore -> les lignes [ORGANES]/[PAROLE],
la réponse verrouillée VISIBLE dans la génération.
"""
import pytest
from fractus_vorax.agent.repl import Repl
from fractus_vorax.brain import Brain
from fractus_vorax.compiler.atoms import Atom
CAPITALS = [
Atom("what is the capital of france", "paris", "cap.csv:2"),
Atom("what is the capital of spain", "madrid", "cap.csv:3"),
Atom("what is the capital of japan", "tokyo", "cap.csv:4"),
Atom("what is the capital of italy", "rome", "cap.csv:5"),
]
def _brain() -> Brain:
brain = Brain(D=2048)
brain.ingest_source("capitals", CAPITALS)
return brain
class FakeAnswerTok:
"""Tokenizer fake du protocole minimal (même table que test_steering) :
« paris » -> [7, 8] comme un vrai BPE qui fusionnerait — la liste
COMPLÈTE est observable, pas juste le premier geste."""
vocab_size = 128
eos_token_id = 127
_WORDS = {
" paris": [7, 8],
" madrid": [9],
" tokyo": [10],
" rome": [11],
}
def encode(self, text: str) -> list[int]:
return list(self._WORDS.get(text, [1]))
def decode(self, ids: list[int]) -> str:
rev = {v[0]: k.strip() for k, v in self._WORDS.items()}
return "".join(rev.get(int(i), "?") for i in ids)
class _TinyCharTok:
"""Tokenizer char-level sur le vocab tiny (128) — injectif sur les ids < 128."""
vocab_size = 128
def encode(self, text: str) -> list[int]:
return [ord(c) % 128 for c in text]
def decode(self, ids: list[int]) -> str:
return "".join(chr(int(i) % 128) for i in ids)
# ---------------------------------------------------------------------------
# speak(answer_lock=...) — le verrou domine chaque pas (torch, skip en mingw)
# ---------------------------------------------------------------------------
def _torch_tiny():
torch = pytest.importorskip(
"torch", reason="speak nécessite torch (substrat sans torch)"
)
from fractus_vorax.model.cte_core import CteCore, CteCoreConfig
torch.manual_seed(0)
return torch, CteCore(CteCoreConfig(vocab_size=128))
def test_answer_lock_locks_every_step_ten_seeds():
"""Boost par défaut (30, calibré par mesure sur le 1B — le 12 du plan
perdait contre les outliers zmax≈12-20 alors que z[locked] peut vivre à
−3σ) : à CHAQUE étape i le token émis EST answer_lock[i], 10 seeds sur
10 — la réponse des organes passe À TRAVERS la distribution du noyau,
pas à côté."""
_, core = _torch_tiny()
from fractus_vorax.model.speak import speak
tok = _TinyCharTok()
lock = [42, 84, 7] # ids distincts : le rep-pen ne touche jamais le verrou
for seed in range(10):
_, diags = speak(
core, tok, "hello", max_new_tokens=3,
answer_lock=lock, seed=seed,
)
assert [d["chosen"] for d in diags] == [tok.decode([i]) for i in lock], (
f"seed {seed}: le verrou n'a pas tenu à chaque pas"
)
def test_answer_lock_boost_twelve_is_honestly_weak():
"""Le 12 du plan, gardé comme MESURE honnête : sur le tiny core non
entraîné (outliers zmax≈18 froids), +12 sur un token à −1σ NE domine
PAS — c'est la mesure qui a motivé le défaut 30 (voir speak.py). Ce
test fige la frontière : le boost place le token, il ne le force pas
magiquement."""
_, core = _torch_tiny()
from fractus_vorax.model.speak import speak
tok = _TinyCharTok()
text12, _ = speak(
core, tok, "hello", max_new_tokens=1,
answer_lock=[42], answer_boost=12.0, seed=0,
)
text30, _ = speak(
core, tok, "hello", max_new_tokens=1,
answer_lock=[42], answer_boost=30.0, seed=0,
)
assert text30 == tok.decode([42]) # 30 : le verrou tient (même seed)
assert text12 != tok.decode([42]) # 12 : z[42]−1.2+12 < zmax 18.4 — perdu
def test_answer_lock_survives_top_k_mask():
"""Le verrou vit AVANT top-k/p : le token boosté est top-1, il survit au
masque — top_k=5 ne casse pas l'articulation."""
_, core = _torch_tiny()
from fractus_vorax.model.speak import speak
tok = _TinyCharTok()
for seed in range(5):
_, diags = speak(
core, tok, "hello", max_new_tokens=2,
answer_lock=[42, 84], top_k=5, seed=seed,
)
assert [d["chosen"] for d in diags] == [tok.decode([42]), tok.decode([84])]
def test_answer_lock_longer_than_generation_is_fine():
"""len(answer_lock) > max_new_tokens : le verrou s'arrête avec la boucle,
aucune erreur, les pas émis sont verrouillés."""
_, core = _torch_tiny()
from fractus_vorax.model.speak import speak
tok = _TinyCharTok()
text, diags = speak(
core, tok, "hello", max_new_tokens=2,
answer_lock=[42, 84, 7, 90, 55], seed=0,
)
assert len(diags) == 2
assert [d["chosen"] for d in diags] == [tok.decode([42]), tok.decode([84])]
assert isinstance(text, str)
def test_answer_lock_rejects_bad_arguments():
_, core = _torch_tiny()
from fractus_vorax.model.speak import speak
tok = _TinyCharTok()
with pytest.raises(ValueError): # id hors vocab
speak(core, tok, "hello", max_new_tokens=2, answer_lock=[999])
with pytest.raises(ValueError): # boost nul ou négatif
speak(core, tok, "hello", max_new_tokens=2, answer_lock=[42], answer_boost=0.0)
def test_answer_lock_visible_in_top3_diagnostics():
"""Le boost est posé AVANT les diagnostics top3 : le token verrouillé
y apparaît (observabilité du mécanisme — il est affiché, pas caché)."""
_, core = _torch_tiny()
from fractus_vorax.model.speak import speak
tok = _TinyCharTok()
_, diags = speak(core, tok, "hello", max_new_tokens=1, answer_lock=[42], seed=0)
top3_tokens = [s for s, _v in diags[0]["top3"]]
assert tok.decode([42]) in top3_tokens
assert top3_tokens[0] == tok.decode([42]) # top-1 après le boost
# ---------------------------------------------------------------------------
# Brain.speak_answer — substrat (les deux venvs)
# ---------------------------------------------------------------------------
def test_speak_answer_japan_is_full_tokyo_list():
"""japan -> la liste COMPLÈTE de " tokyo" (pas juste le premier token —
c'est la différence avec speak_bias : UN verrou, TOUS ses tokens)."""
assert _brain().speak_answer("what is the capital of japan", FakeAnswerTok()) == [10]
def test_speak_answer_france_is_full_paris_list():
"""france -> [7, 8] : les DEUX tokens de " paris" — le verrou couvre la
réponse entière, fragment après fragment."""
assert _brain().speak_answer("what is the capital of france", FakeAnswerTok()) == [7, 8]
def test_speak_answer_lacune_is_empty():
"""Cerveau vide -> seule carte LACUNE -> [] : on ne verrouille JAMAIS la
bouche sur une lacune."""
assert Brain(D=2048).speak_answer("anything", FakeAnswerTok()) == []
def test_speak_answer_rejects_bad_k():
with pytest.raises(ValueError):
_brain().speak_answer("q", FakeAnswerTok(), k=0)
def test_speak_answer_deterministic():
brain = _brain()
a = brain.speak_answer("what is the capital of japan", FakeAnswerTok())
b = brain.speak_answer("what is the capital of japan", FakeAnswerTok())
assert a == b == [10]
def test_speak_answer_with_real_gpt2_bpe():
"""Le VRAI BPE GPT-2 : japan -> exactement encode(" tokyo"), liste
complète (se skipe en substrat mingw sans lib tokenizers)."""
bpt = pytest.importorskip("fractus_vorax.model.bpe_tokenizer")
if not bpt.bpe_available():
pytest.skip("tokenizers indisponible")
tok = bpt.Gpt2BpeTokenizer()
assert _brain().speak_answer("what is the capital of japan", tok) == tok.encode(" tokyo")
assert _brain().speak_answer("what is the capital of france", tok) == tok.encode(" paris")
# ---------------------------------------------------------------------------
# :say — dégradation honnête (substrat, les deux venvs)
# ---------------------------------------------------------------------------
def _repl(tmp_path) -> Repl:
brain = Brain(D=2048)
brain.ingest_source("capitals", CAPITALS)
brain.save(tmp_path / "brain")
return Repl(tmp_path / "brain", D=2048)
def test_say_without_core_degrades_honestly(tmp_path):
"""Sans :core : UNE ligne honnête, jamais de traceback — le contrat
mingw (ce test ne touche jamais torch)."""
r = _repl(tmp_path)
assert r.core is None and r.tokenizer is None
assert r.feed(":say what is the capital of japan") == [
"[PAROLE] noyau non attaché (:core d'abord)"
]
# le REPL reste vivant après la dégradation
assert any("[CARTE]" in l for l in r.feed("what is the capital of japan"))
def test_say_usage_line_without_question(tmp_path):
r = _repl(tmp_path)
assert r.feed(":say") == ["[PAROLE] usage: :say <question>"]
# ---------------------------------------------------------------------------
# Intégration tiny — :say complet sur un tiny CteCore (torch)
# ---------------------------------------------------------------------------
def test_say_tiny_core_organes_and_locked_parole(tmp_path):
"""Noeud torch : tiny CteCore + tokenizer char -> les DEUX lignes.
[ORGANES] porte la carte (paris), [PAROLE] porte le prompt
d'articulation ET la génération où la réponse verrouillée est VISIBLE :
les 6 tokens char de " paris" passent À TRAVERS la boucle du noyau."""
torch = pytest.importorskip(
"torch", reason=":say mécanique nécessite torch (substrat sans torch)"
)
from fractus_vorax.model.cte_core import CteCore, CteCoreConfig
r = _repl(tmp_path)
torch.manual_seed(0)
r.core = CteCore(CteCoreConfig(vocab_size=128))
r.tokenizer = _TinyCharTok()
out = r.feed(":say what is the capital of france")
organes = [l for l in out if l.startswith("[ORGANES]")]
parole = [l for l in out if l.startswith("[PAROLE]")]
assert len(organes) == 1 and len(parole) == 1, f"lignes manquantes: {out}"
assert "paris" in organes[0] # la carte qui connaît : stmt => paris
assert parole[0].startswith(
"[PAROLE] what is the capital of france The answer is"
)
# LE point du Plan 7 : la réponse des organes apparaît dans la parole
# du noyau (verrou = " paris" char par char, puis suite libre)
assert " paris" in parole[0]
# déterminisme : même seed interne (7) -> même articulation
again = r.feed(":say what is the capital of france")
assert [l for l in again if l.startswith("[PAROLE]")] == parole
def test_say_tiny_core_lacune_says_no_lock(tmp_path):
"""Cerveau SANS connaissance (aucun atome -> carte LACUNE) : la parole
coule SANS verrou et le DIT — honnête, jamais de verrou sur rien.
(Un brain non vide répond toujours des FAIT bas-sim : la lacune vraie
est le cerveau vide.)"""
torch = pytest.importorskip(
"torch", reason=":say mécanique nécessite torch (substrat sans torch)"
)
from fractus_vorax.model.cte_core import CteCore, CteCoreConfig
r = Repl(tmp_path / "brain-vide", D=2048) # pas de meta.json -> brain vide
torch.manual_seed(0)
r.core = CteCore(CteCoreConfig(vocab_size=128))
r.tokenizer = _TinyCharTok()
out = r.feed(":say what is the capital of france")
parole = [l for l in out if l.startswith("[PAROLE]")]
assert parole and "sans verrou" in parole[0]
assert any("LACUNE" in l for l in out) # la lacune est dite comme lacune
def test_say_tiny_core_failure_is_honest(tmp_path):
"""Un noyau qui échoue (tokenizer incohérent avec le vocab) -> ligne
``[PAROLE] indisponible: ...``, jamais de traceback."""
torch = pytest.importorskip(
"torch", reason=":say mécanique nécessite torch (substrat sans torch)"
)
from fractus_vorax.model.cte_core import CteCore, CteCoreConfig
r = _repl(tmp_path)
torch.manual_seed(0)
r.core = CteCore(CteCoreConfig(vocab_size=8)) # vocab minuscule
r.tokenizer = _TinyCharTok() # ids jusqu'à 127 -> hors vocab
out = r.feed(":say what is the capital of france")
assert any(l.startswith("[PAROLE] indisponible:") for l in out)