Files
yawan-workbench/server/tests/test_memory_retrieval.py

239 lines
12 KiB
Python

import copy
import pytest
from sqlmodel import SQLModel, Session, create_engine, select
from app.agents.memory import (content_hash, frozen_candidates, memories_text,
memory_candidates, recall_report, record_injected_memories,
select_memories)
from app.agents.runner import (_generation_memories, _memory_model_call,
_runtime_segment_tools, human_review)
from app.agents.runtime import run_agent
from app.agents.snapshot import freeze_agent_snapshot
from app.models import Agent, AgentSession, AppUser, Campaign, Memory, MemoryGrant, PendingMemory, Segment
@pytest.fixture
def db():
engine = create_engine("sqlite://")
SQLModel.metadata.create_all(engine)
with Session(engine) as session:
yield session
engine.dispose()
def _item(identifier, content, layer="L2", **extra):
return {"id": identifier, "content": content, "layer": layer, **extra}
def _scope(db):
owner = AppUser(username="owner", role="editor", organization_id=1)
user = AppUser(username="learner", role="editor", organization_id=1)
other = AppUser(username="other", role="editor", organization_id=1)
db.add_all([owner, user, other])
db.commit()
agent = Agent(name="训练营主笔", owner_user_id=owner.id, organization_id=1)
db.add(agent)
db.commit()
return agent, owner, user, other
def test_chinese_relevance_keeps_foundations_but_not_unrelated_experiences():
rows = [_item(1, "不要编造事实", "L1"), _item(2, "用口语表达", "L3"),
_item(3, "先介绍分数的分母再讲约分", "L2"), _item(4, "售后沟通要先核对订单", "L4")]
selected = select_memories(rows, "数学分数约分练习")
assert {item["id"] for item in selected} == {1, 2, 3}
related = next(item for item in selected if item["id"] == 3)
assert "约分" in related["recall_reason"]
assert related["content_hash"] == content_hash(rows[2]["content"])
assert [item["id"] for item in select_memories(rows, "")] == [1, 2]
def test_whole_memory_budget_preserves_negation_and_deduplicates():
oversized = "示例说明" * 100 + ",以上案例不能当作真实用户经历。"
rows = [_item(1, oversized, "L1"), _item(2, "禁止编造事实", "L1"),
_item(3, " 禁止编造事实 ", "L1"), _item(4, "讲解使用大白话", "L3")]
selected = select_memories(rows, "", max_chars=130, max_items=2)
assert len(selected) == 2
assert all(item["content"] != oversized for item in selected)
assert len({item["content"] for item in selected}) == 2
assert any(item["content"] == "禁止编造事实" for item in selected)
assert len(memories_text(selected).split("\n", 3)[-1]) <= 130
assert select_memories(rows, "", max_items=0) == []
def test_candidates_enforce_user_tenant_grants_and_canvas_agent_owner(db):
agent, owner, user, other = _scope(db)
rows = [Memory(agent_id=agent.id, owner_user_id=person.id, organization_id=1,
content=content, visibility=visibility)
for person, content, visibility in [(user, "本人偏好", "private"),
(owner, "主笔公共经验", "private"), (other, "他人私有偏好", "private"),
(other, "明确授权", "shared"), (other, "同组织全局", "global")]]
foreign = Memory(agent_id=agent.id, owner_user_id=other.id, organization_id=2,
content="其他组织", visibility="global")
db.add_all(rows + [foreign])
db.commit()
db.add(MemoryGrant(memory_id=rows[3].id, user_id=user.id, permission="use"))
db.commit()
direct = memory_candidates(db, agent, user)
assert {item["content"] for item in direct} == {"本人偏好", "明确授权", "同组织全局"}
shared = memory_candidates(db, agent, user, allow_agent_scope=True, allowed_agent_ids={agent.id})
assert {item["content"] for item in shared} == {"本人偏好", "明确授权", "同组织全局", "主笔公共经验"}
user.role = "admin"
assert "其他组织" not in {item["content"] for item in memory_candidates(db, agent, user)}
assert "他人私有偏好" not in {item["content"] for item in memory_candidates(db, agent, user)}
def test_read_only_grant_does_not_authorize_generation(db):
agent, owner, user, _ = _scope(db)
memory = Memory(agent_id=agent.id, owner_user_id=owner.id, content="只能查看", visibility="shared")
db.add(memory)
db.commit()
db.add(MemoryGrant(memory_id=memory.id, user_id=user.id, permission="read"))
db.commit()
assert memory_candidates(db, agent, user) == []
def test_old_index_state_is_readable_and_freezing_does_not_count_usage(db):
agent, owner, _, _ = _scope(db)
memory = Memory(agent_id=agent.id, owner_user_id=owner.id, content="不要夸大效果", index_status="needs_reindex")
db.add(memory)
db.commit()
frozen = freeze_agent_snapshot(db, agent, user=owner)
assert [item["id"] for item in frozen["memories"]] == [memory.id]
assert memory.usage_count == 0
assert memory.last_used_at == ""
def test_frozen_content_unchanged_after_edit_and_legacy_private_memory_filtered(db):
agent, owner, user, other = _scope(db)
original = Memory(agent_id=agent.id, owner_user_id=owner.id, content="原版本:不得报低价")
private = Memory(agent_id=agent.id, owner_user_id=other.id, content="其他用户的秘密")
db.add_all([original, private])
db.commit()
frozen = {"memories": [{"id": original.id, "layer": "L3", "content": original.content},
{"id": private.id, "layer": "L3", "content": private.content}]}
unchanged = copy.deepcopy(frozen)
original.content = "新版本:只说已核实的价格"
db.add(original)
db.commit()
result = frozen_candidates(db, agent, frozen, user, allow_agent_scope=True, allowed_agent_ids={agent.id})
assert [item["content"] for item in result] == ["原版本:不得报低价"]
assert frozen == unchanged
def test_usage_counts_actual_injection_once_per_session(db):
agent, owner, _, _ = _scope(db)
first = Memory(agent_id=agent.id, owner_user_id=owner.id, content="先解释分数")
unused = Memory(agent_id=agent.id, owner_user_id=owner.id, content="不要夸大效果")
db.add_all([first, unused])
db.commit()
selected = select_memories(memory_candidates(db, agent, owner), "分数")
row = AgentSession(agent_id=agent.id)
db.add(row)
db.commit()
rendered = memories_text([item for item in selected if item["id"] == first.id])
record_injected_memories(db, row, selected, [{"content": rendered}])
record_injected_memories(db, row, selected, [{"content": rendered}])
db.refresh(first)
db.refresh(unused)
assert first.usage_count == 1 and first.last_used_at
assert unused.usage_count == 0
new_row = AgentSession(agent_id=agent.id)
db.add(new_row)
db.commit()
record_injected_memories(db, new_row, selected, [{"content": rendered}])
db.refresh(first)
assert first.usage_count == 2
def test_generation_primary_fusion_tools_share_selected_memories_without_mutating_snapshot(db):
agent, owner, user, _ = _scope(db)
fused = Agent(name="融合讲师", owner_user_id=owner.id)
db.add(fused)
db.commit()
agent.fusion_agent_ids = [fused.id]
primary_mem = Memory(agent_id=agent.id, owner_user_id=owner.id, layer="L2", content="分数讲清分母")
fused_mem = Memory(agent_id=fused.id, owner_user_id=owner.id, layer="L2", content="分数先做图示")
wrong = Memory(agent_id=fused.id, owner_user_id=owner.id, layer="L2", content="售后先核对订单")
db.add_all([agent, primary_mem, fused_mem, wrong])
db.commit()
campaign = Campaign(name="数学", owner_user_id=user.id, canvas_snapshot={"nodes": [
{"agent_id": agent.id}, {"agent_id": fused.id}]})
db.add(campaign)
db.commit()
segment = Segment(campaign_id=campaign.id, agent_id=agent.id, title="分数入门")
db.add(segment)
db.commit()
frozen = freeze_agent_snapshot(db, agent, user=user, allow_agent_scope=True,
allowed_agent_ids={agent.id, fused.id})
unchanged = copy.deepcopy(frozen)
selected, reports = _generation_memories(db, agent, segment, campaign, frozen)
assert [item["id"] for item in selected["memories"]] == [primary_mem.id]
assert [item["id"] for item in selected["fusions"][0]["memories"]] == [fused_mem.id]
assert reports[1]["items"][0]["reason"]
assert frozen == unchanged
tools, _ = _runtime_segment_tools(db, segment, agent, selected, [], [], [], [])
read = next(tool for tool in tools if tool.name == "read_agent_memory")
payload = read.handler(None, {})
assert payload["memories"][0]["content"] == selected["memories"][0]["content"]
assert len(payload["memories"]) == 1
def test_model_call_records_usage_and_trace_report_without_additional_ai_call(db, monkeypatch):
agent, owner, _, _ = _scope(db)
memory = Memory(agent_id=agent.id, owner_user_id=owner.id, content="讲解用大白话")
db.add(memory)
db.commit()
selected = select_memories(memory_candidates(db, agent, owner), "课程")
calls = []
def model_call(*args, **kwargs):
calls.append(1)
return {"status": "ok", "text": "正文", "tool_calls": []}
monkeypatch.setattr("app.agents.runtime.chat", model_call)
result = run_agent(db, run_id=0, campaign_id=0, segment_id=0, node_id="", agent=agent,
provider_id=1, model="test", phase="production",
messages=[{"role": "user", "content": memories_text(selected)}],
model_call=_memory_model_call(selected, "production"),
prompt_meta={"memory_recall": [recall_report(agent, selected)]})
db.refresh(memory)
assert len(calls) == 1 and memory.usage_count == 1
row = db.get(AgentSession, result["session_id"])
assert row.result_meta["prompt"]["memory_recall"][0]["items"][0]["id"] == memory.id
def test_existing_snapshot_does_not_acquire_new_fusion_memories(db):
agent, owner, _, _ = _scope(db)
fused = Agent(name="后新增融合 Agent", owner_user_id=owner.id)
db.add(fused)
db.commit()
agent.fusion_agent_ids = [fused.id]
db.add_all([agent, Memory(agent_id=fused.id, owner_user_id=owner.id, content="新的稳定偏好")])
db.commit()
campaign = Campaign(name="课程", owner_user_id=owner.id)
segment = Segment(campaign_id=1, agent_id=agent.id, title="段落")
selected, _ = _generation_memories(db, agent, segment, campaign, {"id": agent.id, "memories": []})
assert selected["fusions"][0]["memories"] == []
def test_rejection_pending_memory_dedup_is_user_scoped(db):
agent, owner, user, _ = _scope(db)
for person in [owner, user]:
campaign = Campaign(name="课程", owner_user_id=person.id)
db.add(campaign)
db.commit()
segment = Segment(campaign_id=campaign.id, agent_id=agent.id, title="段落", status="human_review")
db.add(segment)
db.commit()
human_review(db, segment.id, "reject", "不要用术语")
pending = db.exec(select(PendingMemory)).all()
assert {item.owner_user_id for item in pending} == {owner.id, user.id}
def test_relevant_correction_is_not_crowded_out_by_many_unrelated_preferences():
rows = [_item(i, f"口播习惯偏好编号{i}", "L3") for i in range(1, 80)]
rows.append(_item(100, "分数约分要先检查分母", "L2"))
selected = select_memories(rows, "数学分数约分", max_items=12)
assert len(selected) <= 12
assert selected[0]["id"] == 100