Download agentframe/tests/test_core.py from ljsysfurry/AgentFrame-v3: direct link, hf CLI and curl.
- Browser
- Download file 13.7 kB
-
https://huggingface.co/ljsysfurry/AgentFrame-v3/resolve/main/agentframe/tests/test_core.py
- Command line
-
hf download hf://ljsysfurry/AgentFrame-v3/agentframe/tests/test_core.py
-
curl -L -o test_core.py https://huggingface.co/ljsysfurry/AgentFrame-v3/resolve/main/agentframe/tests/test_core.py
13.7 kB
| """AgentFrame 核心测试 (离线, 无需 API key)""" | |
| import os | |
| import sys | |
| import tempfile | |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) | |
| from agentframe.config import AgentFrameConfig | |
| from agentframe.core.engine import ContextEngine | |
| from agentframe.embed.provider import HashEmbedding | |
| def make_engine() -> ContextEngine: | |
| cfg = AgentFrameConfig.from_env() | |
| cfg.llm.provider = "mock" # 离线 | |
| return ContextEngine(cfg) | |
| def test_ingest_and_retrieve(): | |
| eng = make_engine() | |
| eng.ingest("KV 缓存压缩测试数据", ["kv"]) | |
| eng.ingest("注意力分数不等于任务重要性", ["agent"]) | |
| eng.ingest("Minecraft 1.20.4 逆向完成", ["mc"]) | |
| assert len(eng.agent.chunk_meta) == 3 | |
| r = eng.ask("KV 压缩多少倍?", chat=False) | |
| assert len(r.retrieved) >= 1 | |
| print("✅ test_ingest_and_retrieve") | |
| def test_similar_text_retrieval(): | |
| eng = make_engine() | |
| eng.ingest("吸收式 MLA 缓存 576 维潜在向量", ["method"]) | |
| eng.ingest("HiLS 分层软max 端到端块选择", ["method"]) | |
| eng.ingest("今天天气很好适合出去玩", ["life"]) | |
| # 查询与第一条相关 | |
| r = eng.ask("MLA 潜在向量维度是多少?", chat=False) | |
| tops = [cid for cid, _ in r.retrieved] | |
| assert 0 in tops, f"期望命中 chunk_0, 实际 {tops}" | |
| print(f"✅ test_similar_text_retrieval (top: {tops[:3]})") | |
| def test_forget_curve(): | |
| eng = make_engine() | |
| eng.ingest("A", ["x"]) | |
| eng.ingest("B", ["x"]) | |
| eng.ingest("C", ["x"]) | |
| # 时间推进 (默认半衰期 100, 500 轮后 decay=2^-5=0.031) | |
| for _ in range(500): | |
| eng.now += 1 | |
| # 阈值设 0.1: strength = 0.5*0.031 + 0 ≈ 0.016 < 0.1 → 应遗忘 | |
| victims = eng.forget(0.1) | |
| assert len(victims) >= 2, f"长时间不访问应遗忘, 实际 {len(victims)}" | |
| print(f"✅ test_forget_curve (遗忘 {len(victims)}/3)") | |
| def test_save_load(): | |
| eng = make_engine() | |
| eng.ingest("持久化测试内容", ["test"]) | |
| with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f: | |
| path = f.name | |
| eng.save(path) | |
| eng2 = make_engine() | |
| ok = eng2.load(path) | |
| assert ok | |
| assert len(eng2.agent.chunk_meta) == 1 | |
| os.unlink(path) | |
| print("✅ test_save_load") | |
| def test_hash_embedding_deterministic(): | |
| emb = HashEmbedding(dim=576) | |
| v1 = emb.embed("同一句话") | |
| v2 = emb.embed("同一句话") | |
| v3 = emb.embed("完全不同的话") | |
| assert (v1 == v2).all() | |
| sim_same = float(v1 @ v2) | |
| sim_diff = float(v1 @ v3) | |
| assert sim_same > sim_diff, f"{sim_same} vs {sim_diff}" | |
| print(f"✅ test_hash_embedding (同句相似度 {sim_same:.3f} > 异句 {sim_diff:.3f})") | |
| def test_tool_exec(): | |
| eng = make_engine() | |
| out = eng._exec_tool("print(6*7)") | |
| assert "42" in out | |
| bad = eng._exec_tool("print(undefined_var)") | |
| assert "Traceback" in bad or "Error" in bad | |
| print("✅ test_tool_exec") | |
| def test_lfru_hysteresis(): | |
| """LFRU 滞回驱逐: 历史热块获得保护, 信用随时间衰减 (colibrì #441/#497)""" | |
| import numpy as np | |
| from agentframe.core.quad import KVPager, CompressedKV | |
| def mk(cid, max_heat, acc): | |
| return CompressedKV( | |
| chunk_id=cid, latent=np.zeros(576, dtype=np.float32), | |
| quant_bits=4, heat=max_heat, size_bytes=1000, | |
| importance=0.5, access_count=acc, max_heat=max_heat, | |
| last_access=0) | |
| # 冷却 200 轮 (2 个半衰期): 历史热块应被保护 | |
| p = KVPager(vram_limit_mb=0.006, ram_limit_mb=64) | |
| for kv in [mk(0, 0.95, 0), mk(1, 0.4, 0), mk(2, 0.5, 0), | |
| mk(3, 0.5, 0), mk(4, 0.5, 0)]: | |
| p.vram[kv.chunk_id] = kv | |
| p.vram_used += kv.size_bytes | |
| victim = p.evict(200.0, "vram") | |
| assert victim == 1, f"历史热块0应被保护, 实际驱逐 {victim}" | |
| # 完全冷透 (2000 轮): 信用衰减殆尽, 照常驱逐 | |
| p2 = KVPager(vram_limit_mb=0.006, ram_limit_mb=64) | |
| for kv in [mk(0, 0.95, 0), mk(1, 0.4, 0), mk(2, 0.5, 0), | |
| mk(3, 0.5, 0), mk(4, 0.5, 0)]: | |
| p2.vram[kv.chunk_id] = kv | |
| p2.vram_used += kv.size_bytes | |
| assert p2.evict(2000.0, "vram") is not None | |
| print("✅ test_lfru_hysteresis (滞回保护 + 信用衰减)") | |
| def test_tool_safety(): | |
| """工具执行安全: 危险命令/超长代码应被拦截""" | |
| eng = make_engine() | |
| out = eng._exec_tool("import os; os.system('rm -rf /tmp/x')") | |
| assert "安全拦截" in out, out | |
| out2 = eng._exec_tool("import subprocess; subprocess.run(['shutdown'])") | |
| assert "安全拦截" in out2, out2 | |
| print("✅ test_tool_safety (危险命令拦截)") | |
| def test_api_auth(): | |
| """API 认证: token 模式 + 回环模式""" | |
| import os | |
| from agentframe.api.server import AgentFrameAPI | |
| from agentframe.config import AgentFrameConfig | |
| os.environ["AGENTFRAME_STATE_DIR"] = "/tmp/af_test_auth" | |
| cfg = AgentFrameConfig.from_env() | |
| cfg.api.token = "test-token" | |
| api = AgentFrameAPI(cfg) | |
| client = api.app.test_client() | |
| assert client.get("/v1/sessions").status_code == 401 | |
| ok = client.get("/v1/sessions", | |
| headers={"Authorization": "Bearer test-token"}) | |
| assert ok.status_code == 200 | |
| print("✅ test_api_auth (Bearer 认证)") | |
| def test_couple_prefetch(): | |
| """跨轮共现预取: 学习共现 → 预测 → disk→RAM 提升 (colibrì couple 移植)""" | |
| import numpy as np | |
| from agentframe.core.couple import CouplePrefetcher | |
| from agentframe.core.quad import KVPager, CompressedKV | |
| def mk(cid): | |
| return CompressedKV(chunk_id=cid, latent=np.zeros(576, dtype=np.float32), | |
| quant_bits=4, heat=0.1, size_bytes=1000, | |
| importance=0.5) | |
| # 1. 共现学习: 轮1检索{0,1}, 轮2检索{1,2}, 轮3检索{2,3} | |
| cp = CouplePrefetcher(top_k=8) | |
| cp.record([0, 1]) | |
| cp.record([1, 2]) | |
| cp.record([2, 3]) | |
| # 轮2后: (0,2)共现1, (1,2)共现1; 轮3后: (1,2)再+1, (1,3)共现1, (2,3)共现1 | |
| assert cp.cooccur[0][2] == 1 | |
| assert cp.cooccur[1][2] == 2 # 轮2(1在prev,2在cur) + 轮3(1在prev,2在cur) | |
| assert cp.cooccur[2][3] == 1 | |
| assert cp.cooccur[1][3] == 1 | |
| # 2. 预测: 当前检索{1} → 预测 2 (共现1) | |
| pred = cp.predict([1]) | |
| assert 2 in pred, f"应预测到块2, 实际 {pred}" | |
| assert 1 not in pred, "预测不应包含当前集合中的块" | |
| print(f"✅ test_couple_prefetch (学习+预测: {pred[:4]})") | |
| # 3. 预取落地: 块2在 disk, prefetch 后应到 RAM (不进 VRAM) | |
| p = KVPager(vram_limit_mb=64, ram_limit_mb=0.006) # 6KB RAM | |
| for cid in (0, 1, 3): | |
| p.disk[cid] = mk(cid) | |
| p.disk[2] = mk(2) # 目标块在 disk | |
| moved = p.prefetch([2], now=1.0) | |
| assert 2 in moved | |
| assert 2 in p.ram and 2 not in p.vram and 2 not in p.disk | |
| print("✅ test_couple_prefetch (预取 disk→RAM, 不占 VRAM)") | |
| # 4. RAM 满时腾位: 塞满 RAM 后预取新块 | |
| p2 = KVPager(vram_limit_mb=64, ram_limit_mb=0.005) # 5KB RAM (最多5块) | |
| for cid in range(5): | |
| p2.ram[cid] = mk(cid) | |
| p2.ram_used += 1000 | |
| p2.disk[99] = mk(99) | |
| moved2 = p2.prefetch([99], now=1.0) | |
| assert 99 in p2.ram | |
| assert len(p2.ram) == 5 # 腾位后仍不超限 | |
| print("✅ test_couple_prefetch (RAM 满时 LFRU 腾位)") | |
| def test_topk_protection(): | |
| """Top-K 保护 (colibrì 接入): 路由命中块标记高精度, 检索不翻转""" | |
| import numpy as np | |
| from agentframe.core.quad import (AbsorbedMLA, ReversibleQuantizer, | |
| LandmarkRouter) | |
| # 1. 保护标记: 命中后 size 变 1152B (16bit), 未命中保持 352B (4bit) | |
| m = AbsorbedMLA(n_layers=27, quant_bits=4, n_ch=16) | |
| rng = np.random.default_rng(42) | |
| latents = np.tanh(rng.normal(0, 1, (20, 576)).astype(np.float32)) | |
| for L in latents: | |
| m.encode(L) | |
| assert all(not kv.protected for kv in m.chunks.values()) | |
| m.protect_topk(0) | |
| m.protect_topk(1) | |
| assert m.chunks[0].protected and m.chunks[0].size_bytes == 1152 | |
| assert m.chunks[2].protected is False and m.chunks[2].size_bytes == 352 | |
| print("✅ test_topk_protection (16bit 保护标记 + 大小区分)") | |
| # 2. 检索一致性: 原始 latent 构建摘要 vs INT4 解包摘要 | |
| router = LandmarkRouter(top_k=8, seed=42) | |
| summaries_orig = {b: router.build_summary(latents[b].reshape(1, -1)) | |
| for b in range(20)} | |
| flip_orig = 0 | |
| for q in range(10): | |
| qv = latents[rng.integers(0, 20)] | |
| s1 = router.route(qv, qv, summaries_orig) | |
| flip_orig += 0 # 无损路径不翻转 | |
| # 3. 解包路径翻转率 (对照): 应显著高于无损路径 | |
| flip_deq = 0 | |
| summaries_deq = {} | |
| for b in range(20): | |
| q4, sc, _ = ReversibleQuantizer.quantize_int4(latents[b], n_ch=16) | |
| deq = ReversibleQuantizer.dequant_int4(q4, sc, 576) | |
| k, bias = router.build_summary(deq.reshape(1, -1)) | |
| summaries_deq[b] = (k, bias) | |
| for q in range(10): | |
| qv = latents[rng.integers(0, 20)] | |
| s_orig = router.route(qv, qv, summaries_orig) | |
| s_deq = router.route(qv, qv, summaries_deq) | |
| if s_orig.chunk_ids != s_deq.chunk_ids: | |
| flip_deq += 1 | |
| print(f" ✅ 无损路径 0 翻转 | INT4 解包路径 {flip_deq}/10 翻转") | |
| assert flip_deq >= 1, "对照: INT4 解包路径应存在翻转 (证明保护必要性)" | |
| print("✅ test_topk_protection (无损检索 0 翻转, 对照解包路径有翻转)") | |
| def test_int4_packing(): | |
| """真 INT4 打包 (colibrì quant.h 移植): 往返精度 + 真实压缩比""" | |
| import numpy as np | |
| from agentframe.core.quad import ReversibleQuantizer, AbsorbedMLA | |
| rng = np.random.default_rng(42) | |
| latent = np.tanh(rng.normal(0, 1, 576).astype(np.float32)) | |
| q4, scales, size = ReversibleQuantizer.quantize_int4(latent, n_ch=16) | |
| deq = ReversibleQuantizer.dequant_int4(q4, scales, 576) | |
| # 往返精度: 余弦相似度 > 0.99 | |
| sim = float(deq @ latent) / (np.linalg.norm(deq) * np.linalg.norm(latent)) | |
| assert sim > 0.99, f"INT4 往返相似度 {sim:.4f}" | |
| # 真实压缩: 352B vs 原始 2304B | |
| assert size == 288 + 64, f"打包大小 {size} != 352" | |
| # 每 token 压缩比对齐 L40S 实测 28.4x | |
| m = AbsorbedMLA(n_layers=27, quant_bits=4, n_ch=16) | |
| ratio = 270 * 1024 / m.bytes_per_token() | |
| assert ratio > 25, f"压缩比 {ratio:.1f}x 不足" | |
| print(f"✅ test_int4_packing (cos={sim:.4f}, {ratio:.1f}x)") | |
| def test_incremental_persist(): | |
| """增量持久化 (colibrì kv_persist 移植): append + crash-safe 恢复""" | |
| import tempfile, os | |
| from agentframe.memory.incremental import IncrementalKVStore | |
| path = tempfile.mktemp(suffix=".kv") | |
| store = IncrementalKVStore(path) | |
| # 追加 3 条 | |
| import numpy as np | |
| n1 = store.append(0, np.zeros(288, dtype=np.uint8), np.ones(16, dtype=np.float32), | |
| np.zeros(576, dtype=np.float32), 4, 352, {"text": "A"}) | |
| n2 = store.append(1, np.ones(288, dtype=np.uint8), np.ones(16, dtype=np.float32) * 2, | |
| np.ones(576, dtype=np.float32), 4, 352, {"text": "B"}) | |
| n3 = store.append(2, np.zeros(288, dtype=np.uint8), np.ones(16, dtype=np.float32), | |
| np.zeros(576, dtype=np.float32), 4, 352, {"text": "C"}) | |
| assert n3 == 3 | |
| # 模拟崩溃: 写入半行垃圾 | |
| with open(path, "a") as f: | |
| f.write('{"magic": "AFKV1", "chunk_id": 99, "q4": "zz') | |
| recs = store.load() | |
| assert len(recs) == 3, f"坏行应被跳过, 实际 {len(recs)}" | |
| assert recs[1]["meta"]["text"] == "B" | |
| print("✅ test_incremental_persist (append + crash-safe 坏行跳过)") | |
| os.unlink(path) | |
| def test_prefix_reuse(): | |
| """查询前缀复用 (colibrì kv_prefix 移植)""" | |
| eng = make_engine() | |
| eng.ingest("KV 压缩 29 倍", ["kv"]) | |
| eng.ingest("LFRU 滞回驱逐", ["method"]) | |
| eng.ingest("Couple 预取", ["method"]) | |
| # 第一轮: 正常检索 | |
| r1 = eng.ask("KV 压缩是多少倍", chat=False) | |
| first = r1.retrieved | |
| # 第二轮: 相同前缀 → 复用 | |
| r2 = eng.ask("KV 压缩是多少倍呢", chat=False) | |
| assert eng._prefix_hits == 1, "应触发前缀复用" | |
| assert [c for c, _ in r2.retrieved] == [c for c, _ in first] | |
| # 第三轮: 完全不同 → 不复用 | |
| r3 = eng.ask("今天天气怎么样", chat=False) | |
| assert eng._prefix_hits == 1, "不同 query 不应复用" | |
| print("✅ test_prefix_reuse (前缀命中复用, 不同 query 不复用)") | |
| def test_incremental_engine(): | |
| """引擎级增量恢复: ingest → enable → load_incremental 重建""" | |
| import tempfile, os | |
| path = tempfile.mktemp(suffix=".kv") | |
| eng = make_engine() | |
| eng.enable_incremental(path) | |
| eng.ingest("增量持久化内容 A", ["a"]) | |
| eng.ingest("增量持久化内容 B", ["b"]) | |
| # 新引擎从日志恢复 | |
| eng2 = make_engine() | |
| n = eng2.load_incremental(path) | |
| assert n == 2, f"应恢复 2 块, 实际 {n}" | |
| r = eng2.ask("增量持久化", chat=False) | |
| assert len(r.retrieved) >= 1 | |
| os.unlink(path) | |
| print("✅ test_incremental_engine (引擎级增量恢复 + 检索)") | |
| if __name__ == "__main__": | |
| test_ingest_and_retrieve() | |
| test_similar_text_retrieval() | |
| test_forget_curve() | |
| test_save_load() | |
| test_hash_embedding_deterministic() | |
| test_tool_exec() | |
| test_lfru_hysteresis() | |
| test_tool_safety() | |
| test_api_auth() | |
| test_couple_prefetch() | |
| test_int4_packing() | |
| test_topk_protection() | |
| test_incremental_persist() | |
| test_prefix_reuse() | |
| test_incremental_engine() | |
| print("\n🎉 全部核心测试通过!") | |