Contracts (the shared language): - percept / state / context / utterance / render / audio / capabilities - Capability model instead of vendor schemas (see below) Providers (capability-based abstraction): - LLM + Embedding: OpenAI-compatible shape IS the standard, adopted directly - ASR: internal contract is an event stream (partial -> final), because /v1/audio/transcriptions has no partial and dialogue needs it for endpointing. OpenAI adapter degrades to supports_partial=False. - TTS: internal contract carries tags/emotion/style, because /v1/audio/speech has no paralinguistics and [laughter]/[breath] are the core of feeling alive. Unsupported providers are stripped AND reported via Degradation -- silent degradation is forbidden. - Avatar: no de-facto standard, custom RenderCmd protocol driven by State. Core: - mood: dynamical system, slow variables, negativity bias (grudge), numbing after prolonged negativity - bazi: four pillars -> numeric vector; 'ming' is data, not prose - projection: phi, system event -> in-world statement; uncovered events are counted as projection gaps Storage: - SQLite schema, three memory layers, dyad ledger - accessibility floor 0.03 (not 0): forgettable but still there Config split: - models.yaml is the ONLY place model versions live - character.yaml holds world rules / emotion params, no version numbers 58 tests pass; ruff clean.
111 lines
3.5 KiB
Python
111 lines
3.5 KiB
Python
"""存储层测试。
|
||
|
||
守的是遗忘的**下限**:accessibility 会衰减,但永不归零。
|
||
这是 availability / accessibility 双阈值的实现——
|
||
想不起来,但还在,情绪线索能唤起。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from cybergril.memory import Store
|
||
|
||
|
||
def test_schema_initialises(store: Store) -> None:
|
||
rows = store.query(
|
||
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
||
)
|
||
names = {r["name"] for r in rows}
|
||
for expected in (
|
||
"bazi_core",
|
||
"identity_change_log",
|
||
"state_snapshot",
|
||
"life_event",
|
||
"mem_episodic",
|
||
"mem_semantic",
|
||
"mem_relational",
|
||
"dyad_state",
|
||
"dyad_event",
|
||
"projection_gap",
|
||
):
|
||
assert expected in names, f"缺表 {expected}"
|
||
|
||
|
||
def test_save_and_read_state(store: Store) -> None:
|
||
store.save_state(v=-0.4, a=0.6, d=0.3, e=0.5, mood_word="闷闷的")
|
||
row = store.last_state()
|
||
assert row is not None
|
||
assert abs(row["valence"] - (-0.4)) < 1e-9
|
||
assert row["mood_word"] == "闷闷的"
|
||
|
||
|
||
def test_add_episodic_and_fts(store: Store) -> None:
|
||
eid = store.add_episodic(
|
||
role="user",
|
||
raw="我今天很难过",
|
||
salience=0.85,
|
||
affect={"v": -0.7, "a": 0.6, "d": 0.3},
|
||
)
|
||
assert eid > 0
|
||
|
||
row = store.query_one("SELECT * FROM mem_episodic WHERE id=?", (eid,))
|
||
assert row is not None
|
||
assert row["salience"] == 0.85
|
||
assert row["accessibility"] == 1.0
|
||
|
||
|
||
def test_decay_never_reaches_zero(store: Store) -> None:
|
||
"""最关键的一条:遗忘有下限。"""
|
||
store.add_episodic(role="user", raw="一件小事", salience=0.1)
|
||
store.decay_accessibility(dt_hours=24 * 365, floor=0.03)
|
||
|
||
row = store.query_one("SELECT accessibility FROM mem_episodic LIMIT 1")
|
||
assert row is not None
|
||
assert row["accessibility"] >= 0.03
|
||
assert row["accessibility"] < 0.1 # 确实衰减了
|
||
|
||
|
||
def test_high_salience_decays_slower(store: Store) -> None:
|
||
store.add_episodic(role="user", raw="重要的事", salience=1.0)
|
||
store.add_episodic(role="user", raw="不重要的事", salience=0.0)
|
||
store.decay_accessibility(dt_hours=48, floor=0.03)
|
||
|
||
rows = store.query(
|
||
"SELECT salience, accessibility FROM mem_episodic ORDER BY salience DESC"
|
||
)
|
||
high, low = rows[0], rows[1]
|
||
assert high["accessibility"] > low["accessibility"]
|
||
|
||
|
||
def test_reinforce_raises_accessibility(store: Store) -> None:
|
||
eid = store.add_episodic(role="user", raw="会被想起的事", salience=0.5)
|
||
store.decay_accessibility(dt_hours=100, floor=0.03)
|
||
before = store.query_one(
|
||
"SELECT accessibility, recall_count FROM mem_episodic WHERE id=?", (eid,)
|
||
)
|
||
|
||
store.reinforce(eid, boost=0.25)
|
||
after = store.query_one(
|
||
"SELECT accessibility, recall_count FROM mem_episodic WHERE id=?", (eid,)
|
||
)
|
||
|
||
assert after["accessibility"] > before["accessibility"]
|
||
assert after["recall_count"] == before["recall_count"] + 1
|
||
|
||
|
||
def test_dyad_persistence(store: Store) -> None:
|
||
eid = store.add_dyad_event(kind="hurt", summary="他忘了约定", severity=-0.6)
|
||
store.save_dyad(trust=0.4, intimacy=0.5, debt=-0.6, security=0.4, unresolved=[eid])
|
||
|
||
row = store.last_dyad()
|
||
assert row is not None
|
||
assert abs(row["debt"] - (-0.6)) < 1e-9
|
||
|
||
ev = store.query_one("SELECT * FROM dyad_event WHERE id=?", (eid,))
|
||
assert ev is not None
|
||
assert ev["severity"] < 0
|
||
|
||
|
||
def test_projection_gap_recorded(store: Store) -> None:
|
||
store.record_projection_gap("mystery.event", {"detail": "x"})
|
||
assert store.projection_gap_count() == 1
|