146 lines
7.3 KiB
Python
146 lines
7.3 KiB
Python
"""HEL-835 correction proof v2: full 21 PAIRS in a fresh isolated DB.
|
|
|
|
Zero real model calls (ControlledLLM only). Fixes the two evidence defects the
|
|
reviewer found in the first probe:
|
|
- old seed had no companion 'sent' rows, so fillers never entered the
|
|
production recent-turn window (JOIN companion x user, both 'sent');
|
|
- its fingerprint was sha256(b'null') because bazi_core was empty.
|
|
|
|
This proof: creates a real profile through the production endpoint (program
|
|
bazi computation, no persona model call with R2 off), seeds ONE target pair
|
|
plus TWENTY filler pairs (both roles 'sent'), then verifies through the real
|
|
create_app + ControlledLLM that the production recent-turn SQL excludes the
|
|
twenty filler turns, the target sits outside, the final prompt contains the
|
|
target memory block, the recent history does NOT contain the target's unique
|
|
answer, and the birth fingerprint (non-null source) is unchanged.
|
|
"""
|
|
import asyncio
|
|
import json
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
from uuid import uuid4
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tests"))
|
|
|
|
from fastapi.testclient import TestClient # noqa: E402
|
|
|
|
from test_api import CREATE, ControlledLLM, events # noqa: E402
|
|
|
|
from cybergril.api.app import create_app # noqa: E402
|
|
from cybergril.life.persona import fingerprint # noqa: E402
|
|
from cybergril.life.runtime import LifeConfig # noqa: E402
|
|
from cybergril.think.router import build_router # noqa: E402
|
|
from cybergril.think.scenes import Tier # noqa: E402
|
|
|
|
PASS, FAIL = [], []
|
|
TARGET_TURN = "00000000-c0rr-0000-4000-8000-000000000000"
|
|
UNIQUE = "蓝灰色封面诗集"
|
|
|
|
|
|
def check(name, cond, detail=""):
|
|
(PASS if cond else FAIL).append(name)
|
|
print(("PASS" if cond else "FAIL"), name, detail)
|
|
|
|
|
|
class CaptureLLM(ControlledLLM):
|
|
"""ControlledLLM that records the full prompt of every call."""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.captured = []
|
|
|
|
async def stream(self, messages, **kwargs):
|
|
self.calls.append(messages)
|
|
self.captured.append(list(messages))
|
|
from cybergril.contracts import LLMDelta
|
|
yield LLMDelta(text="我在听。")
|
|
yield LLMDelta(done=True)
|
|
|
|
|
|
async def main_async():
|
|
tmp = Path(tempfile.mkdtemp(prefix="hel835-corr-"))
|
|
|
|
provider = CaptureLLM()
|
|
router = build_router(providers={t: provider for t in (Tier.HIGH, Tier.STANDARD, Tier.LOW)})
|
|
app = create_app(db_path=tmp / "corr.db", router=router,
|
|
life_config=LifeConfig(enabled=False))
|
|
with TestClient(app) as c:
|
|
r = c.post("/api/companion", json={"name": "补证", "birth": "2001-05-21", "hour": "午时", "relation": "朋友"})
|
|
check("profile created via production endpoint", r.status_code == 201)
|
|
s = app.state.store
|
|
fp_before = fingerprint(s)
|
|
check("fingerprint source non-null", fp_before != __import__("hashlib").sha256(b"null").hexdigest())
|
|
check("bazi_core populated", s.query_one("SELECT count(*) n FROM bazi_core")["n"] == 1)
|
|
|
|
# seed: 1 target pair (oldest) + 20 filler pairs, all both-roles sent
|
|
pairs = [(TARGET_TURN, f"我今天在巷口旧书店买了一本{UNIQUE}。", "谢谢你告诉我,听起来很安静。")]
|
|
for i in range(1, 21):
|
|
pairs.append((f"00000000-c0rr-0000-4000-8000-{i:012d}",
|
|
f"合成第{i}轮:雨天通勤和晚饭。", f"合成回复第{i}轮。"))
|
|
with s.conn:
|
|
for turn, user_text, comp_text in pairs:
|
|
uid = s.execute("INSERT INTO chat_message(ts,role,text,turn_id,status) VALUES(1,'user',?,?,'sent')",
|
|
(user_text, turn)).lastrowid
|
|
cid = s.execute("INSERT INTO chat_message(ts,role,text,turn_id,status) VALUES(2,'companion',?,?,'sent')",
|
|
(comp_text, turn)).lastrowid
|
|
s.execute("INSERT INTO mem_episodic(ts,role,raw,gist,salience,affect,source,accessibility,turn_id,message_id)"
|
|
" VALUES(1,'user',?,?,0.5,NULL,'chat',1,?,?)",
|
|
(user_text, f"用户在巷口旧书店买了{UNIQUE}。", turn, uid))
|
|
if turn == TARGET_TURN:
|
|
s.execute("INSERT INTO mem_episodic(ts,role,raw,gist,salience,affect,source,accessibility,turn_id,message_id)"
|
|
" VALUES(2,'companion',?,?,0.5,NULL,'chat',1,?,?)",
|
|
(comp_text, "她谢谢用户分享诗集。", turn, cid))
|
|
|
|
# (a) production recent-turn SQL over the seeded db
|
|
rows = s.query("""SELECT a.turn_id FROM chat_message a JOIN chat_message u
|
|
ON u.turn_id=a.turn_id AND u.role='user'
|
|
WHERE a.role='companion' AND a.status='sent' AND u.status='sent'
|
|
ORDER BY a.id DESC LIMIT 20""")
|
|
window = [r["turn_id"] for r in rows]
|
|
check("production window has exactly 20 pairs", len(window) == 20, str(len(window)))
|
|
check("target outside the window", TARGET_TURN not in window)
|
|
check("all fillers inside the window", all(t in window for t in window if t != TARGET_TURN))
|
|
|
|
# (b)(c) real chat turn -> captured prompt + retrieval log
|
|
import logging
|
|
caplog_stream = []
|
|
handler = logging.Handler()
|
|
handler.emit = lambda record: caplog_stream.append(record.getMessage())
|
|
log = logging.getLogger("cybergril.memory.episodic")
|
|
log.setLevel(logging.INFO)
|
|
log.addHandler(handler)
|
|
epoch = c.get("/api/health").json()["epoch"]
|
|
response = c.post("/api/chat", json={"text": f"还记得我在旧书店买的那本{UNIQUE[:-2]}吗",
|
|
"turn_id": str(uuid4()), "epoch": epoch})
|
|
final = events(response)[-1]
|
|
check("chat turn completed", final["t"] == "chat.final")
|
|
retrieval = next((line for line in caplog_stream if "mode" in line), None)
|
|
check("retrieval fired", retrieval is not None, retrieval or "")
|
|
if retrieval:
|
|
parsed = json.loads(retrieval)
|
|
check("retrieval hit target", any(h["turn_id"] == TARGET_TURN for h in parsed["hits"]),
|
|
str(parsed["hits"]))
|
|
check("mode phrase/trigram", parsed["mode"] in ("phrase", "trigram", "like"), parsed["mode"])
|
|
prompt = provider.captured[-1]
|
|
system_blob = "".join(m.content for m in prompt if m.role == "system")
|
|
history_msgs = [m for m in prompt[:-1] if m.role in ("user", "assistant")]
|
|
recent_blob = "".join(m.content for m in history_msgs)
|
|
check("memory block in system messages contains target gist", UNIQUE in system_blob)
|
|
check("recent user/assistant history does NOT contain the target answer", UNIQUE not in recent_blob)
|
|
kept_recent = sum(1 for m in prompt[1:] if m.role in ("user", "assistant"))
|
|
check("recent history actually delivered (non-empty)", kept_recent > 0, str(kept_recent))
|
|
check("filler recent turns present in prompt", "合成第20轮" in recent_blob or "合成第" in recent_blob)
|
|
fp_after = fingerprint(s)
|
|
check("birth fingerprint unchanged after chat+retrieval", fp_after == fp_before)
|
|
|
|
print(f"\n{len(PASS)} passed, {len(FAIL)} failed")
|
|
if FAIL:
|
|
print("FAILED:", FAIL)
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main_async())
|