193 lines
7.7 KiB
Python
193 lines
7.7 KiB
Python
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from app.services.pmflow_ai_context_memory_service import (
|
|
_merge_memory,
|
|
_memory_write_gate,
|
|
_resolve_memory_conflict,
|
|
_scope_refs,
|
|
)
|
|
from app.services.pmflow_ai_context_pack_service import (
|
|
PMFlowAIContextPackBuilder,
|
|
context_pack_memory_claim_rows,
|
|
context_pack_retrieval_critic_packet,
|
|
evaluate_context_replay_case,
|
|
)
|
|
|
|
|
|
def _row(**kwargs):
|
|
base = {
|
|
"id": 11,
|
|
"summary": "",
|
|
"durable_facts": [],
|
|
"open_loops": [],
|
|
"decisions": [],
|
|
"preferences": [],
|
|
"object_refs": [],
|
|
"confidence": "medium",
|
|
"confidence_score": 0.6,
|
|
"status": "active",
|
|
"invalidation_reason": "",
|
|
}
|
|
base.update(kwargs)
|
|
return SimpleNamespace(**base)
|
|
|
|
|
|
def test_memory_write_gate_skips_duplicate_and_merges_new_delta() -> None:
|
|
existing = _row(
|
|
summary="用户要求 PMFlow AI 方案不要靠硬编码关键词和场景枚举。",
|
|
durable_facts=[{"summary": "上下文管理要走统一 Context OS。"}],
|
|
)
|
|
duplicate = {
|
|
"summary": "用户要求 PMFlow AI 方案不要靠硬编码关键词和场景枚举。",
|
|
"durable_facts": [{"summary": "上下文管理要走统一 Context OS。"}],
|
|
"confidence": "medium",
|
|
}
|
|
duplicate_gate = _memory_write_gate(memory=duplicate, existing=existing)
|
|
assert duplicate_gate["action"] == "skip"
|
|
|
|
incoming = {
|
|
"summary": "新增要求:长期记忆需要写入门禁、合并策略和回放评测。",
|
|
"durable_facts": [{"summary": "新增 golden replay 覆盖记住、忘记、覆盖和预算。"}],
|
|
"confidence": "high",
|
|
}
|
|
gate = _memory_write_gate(memory=incoming, existing=existing)
|
|
assert gate["action"] == "update"
|
|
assert gate["status"] == "active"
|
|
|
|
merged = _merge_memory(existing=existing, incoming=incoming)
|
|
assert "硬编码关键词" in merged["summary"]
|
|
assert "长期记忆需要写入门禁" in merged["summary"]
|
|
assert any("golden replay" in str(item) for item in merged["durable_facts"])
|
|
|
|
|
|
def test_memory_write_gate_invalidates_existing_without_keyword_scene_rules() -> None:
|
|
existing = _row(summary="当前方向是 AI 采购平台,从发票自动比对切入。")
|
|
incoming = {
|
|
"summary": "用户明确推翻当前方向,要求旧方向不再作为后续判断依据。",
|
|
"status": "invalidated",
|
|
"invalidation_reason": "用户在最新对话中推翻了当前方向。",
|
|
"confidence": "high",
|
|
}
|
|
gate = _memory_write_gate(memory=incoming, existing=existing)
|
|
assert gate["action"] == "update"
|
|
assert gate["status"] == "invalidated"
|
|
assert gate["supersedes_memory_id"] == 11
|
|
assert "推翻" in gate["invalidation_reason"]
|
|
assert gate["conflict_resolution"]["resolution"] == "invalidate"
|
|
|
|
|
|
def test_memory_conflict_resolver_keeps_distinct_structured_delta_without_scene_keywords() -> None:
|
|
existing = _row(
|
|
public_memory_id="ctxmem_existing",
|
|
summary="用户已确认首版需要项目执行包。",
|
|
durable_facts=[{"summary": "首版需要项目执行包。"}],
|
|
)
|
|
incoming = {
|
|
"summary": "新增约束:执行包必须带验收清单和外部工具交接说明。",
|
|
"durable_facts": [{"summary": "执行包必须带验收清单。"}],
|
|
"confidence": "high",
|
|
}
|
|
|
|
resolution = _resolve_memory_conflict(memory=incoming, existing=existing, novelty_ratio=0.6, structured_delta=1)
|
|
|
|
assert resolution["resolution"] == "keep_both"
|
|
assert resolution["old_memory_id"] == "ctxmem_existing"
|
|
assert resolution["structured_delta"] == 1
|
|
|
|
|
|
def test_scope_refs_do_not_mix_visible_project_when_current_project_is_explicit() -> None:
|
|
refs = _scope_refs(
|
|
thread=SimpleNamespace(id=3, public_thread_id="thread_abc"),
|
|
current_user=SimpleNamespace(id=9),
|
|
conversation_context={"conversation_scope": "space", "current_project_id": 101},
|
|
tenant_project_context={
|
|
"tenant_id": 1,
|
|
"user_id": 9,
|
|
"surface_context": {
|
|
"visible_objects": [
|
|
{"object_type": "project", "object_id": 202, "display_name": "另一个项目"},
|
|
]
|
|
},
|
|
},
|
|
)
|
|
assert refs["thread"] == ["thread:thread_abc"]
|
|
assert refs["user"] == ["user:9"]
|
|
assert refs["project"] == ["project:101"]
|
|
assert "project:202" not in refs["project"]
|
|
|
|
|
|
def test_context_retrieval_critic_flags_low_overlap_memory_and_missing_thread_context() -> None:
|
|
context_pack = PMFlowAIContextPackBuilder(budget_chars=9000).build(
|
|
user_question="继续展开采购审批助手的 MVP 和执行包",
|
|
thread_context_pack={},
|
|
conversation_context={"conversation_scope": "space"},
|
|
tenant_project_context={"tenant_id": 1, "user_id": 2, "surface_context": {}},
|
|
memory_context_pack={
|
|
"packet_version": "pmflow_ai_layered_memory_pack.v1",
|
|
"scope_refs": {"user": ["user:2"]},
|
|
"memories": [
|
|
{
|
|
"memory_id": "ctxmem_weather",
|
|
"memory_scope": "user",
|
|
"scope_ref": "user:2",
|
|
"summary": "用户曾询问广州天气和线下安排。",
|
|
"confidence_score": 0.8,
|
|
}
|
|
],
|
|
},
|
|
)
|
|
critic = context_pack_retrieval_critic_packet(context_pack, user_question="继续展开采购审批助手的 MVP 和执行包")
|
|
assert any(item["kind"] == "no_same_thread_context" for item in critic["gaps"])
|
|
assert any(item["kind"] == "memory_present_but_low_question_overlap" for item in critic["risks"])
|
|
|
|
|
|
def test_context_budget_keeps_thread_and_memory_before_large_surface() -> None:
|
|
thread_pack = {
|
|
"packet_version": "pmflow_ai_thread_context_pack.v1",
|
|
"recent_messages": [
|
|
{"message_id": "m1", "role": "assistant", "message_index": 2, "content": "前文确认 MVP 切口是发票自动比对。"}
|
|
],
|
|
"relevant_messages": [
|
|
{"message_id": "m2", "role": "assistant", "message_index": 4, "content": "采购平台后续要补供应商匹配和合同分析。"}
|
|
],
|
|
}
|
|
memory_pack = {
|
|
"packet_version": "pmflow_ai_layered_memory_pack.v1",
|
|
"scope_refs": {"thread": ["thread:abc"]},
|
|
"memories": [
|
|
{
|
|
"memory_id": "ctxmem_1",
|
|
"memory_scope": "thread",
|
|
"scope_ref": "thread:abc",
|
|
"summary": "用户要求后续输出执行包,并避免硬编码关键词方案。",
|
|
"confidence_score": 0.9,
|
|
}
|
|
],
|
|
}
|
|
context_pack = PMFlowAIContextPackBuilder(budget_chars=8200).build(
|
|
user_question="继续上面采购平台并拆执行包",
|
|
thread_context_pack=thread_pack,
|
|
conversation_context={"conversation_scope": "space"},
|
|
tenant_project_context={
|
|
"tenant_id": 1,
|
|
"user_id": 2,
|
|
"surface_context": {
|
|
"space_runtime_context": {
|
|
"space_operating_packet": {"large": "无关可见面板 " * 2000},
|
|
}
|
|
},
|
|
},
|
|
memory_context_pack=memory_pack,
|
|
)
|
|
replay = evaluate_context_replay_case(
|
|
context_pack=context_pack,
|
|
expected_evidence=["发票自动比对", "避免硬编码关键词方案"],
|
|
)
|
|
assert replay["passed"] is True
|
|
assert context_pack_memory_claim_rows(context_pack)
|
|
statuses = {item["section"]: item["status"] for item in context_pack["budget_report"]["sections"]}
|
|
assert statuses["thread_context"] in {"included", "compressed"}
|
|
assert statuses["memory_context"] in {"included", "compressed"}
|