PMFlow/backend/tests/test_pmflow_ai_object_ledge...

441 lines
18 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from app.schemas.rag import RAGAskRequest
from app.schemas.pmflow_ai_turn_runtime import (
PMFlowAIAnswerState,
PMFlowAIGoalFrame,
PMFlowAIRuntimeAction,
PMFlowAITargetScope,
PMFlowAITurnEvidenceState,
PMFlowAITurnObjectRef,
PMFlowAITurnObjectState,
PMFlowAITurnState,
)
from app.services.pmflow_ai_turn_object_ledger import (
build_object_ledger_from_payload,
merge_object_ledger_into_context_packet,
object_ledger_from_context_capsules,
upsert_object_ledger_capsule,
)
from app.services.pmflow_ai_turn_state_reviser import PMFlowAITurnStateReviser
from app.services.pmflow_ai_turn_runtime_service import PMFlowAITurnRuntimeService
def _service() -> PMFlowAITurnRuntimeService:
return PMFlowAITurnRuntimeService(object(), tenant_id=1)
def test_packet_i_builds_customer_safe_object_ledger_from_turn_payload() -> None:
payload = {
"answer_mode": "turn_runtime_state_answer",
"pmflow_ai_turn_state": {
"customer_packet_draft": {
"items": [
{
"object_type": "project",
"object_ref": "project:141",
"title": "童析",
"status": "进行中",
}
]
}
},
"customer_interaction_context_packet": {"visible_objects": []},
}
ledger = build_object_ledger_from_payload(payload=payload)
assert ledger["current_focus_object"]["object_ref"] == "project:141"
assert ledger["current_focus_object"]["title"] == "童析"
assert ledger["last_shown_objects"] == [
{"object_type": "project", "object_ref": "project:141", "title": "童析", "object_id": 141, "status": "进行中"}
]
merged = merge_object_ledger_into_context_packet(context_packet={}, object_ledger=ledger)
assert merged["current_focus_object"]["object_ref"] == "project:141"
assert merged["last_shown_objects"][0]["title"] == "童析"
def test_packet_i_upserts_ledger_into_session_context_capsules() -> None:
ledger = build_object_ledger_from_payload(
payload={
"pmflow_ai_turn_state": {
"customer_packet_draft": {
"items": [{"object_type": "project", "object_ref": "project:141", "title": "童析"}]
}
}
}
)
capsules = upsert_object_ledger_capsule(capsules=[{"capsule_type": "other", "value": 1}], object_ledger=ledger)
loaded = object_ledger_from_context_capsules(capsules)
assert capsules[0]["capsule_type"] == "pmflow_ai_object_ledger"
assert loaded["current_focus_object"]["object_ref"] == "project:141"
assert capsules[1]["capsule_type"] == "other"
def test_packet_i_session_object_ledger_binds_weak_followup_primary_object() -> None:
ledger = {
"packet_version": "pmflow_ai_object_ledger.v1",
"current_focus_object": {
"object_type": "project",
"object_ref": "project:141",
"title": "童析",
},
"last_shown_objects": [],
"recent_objects": [],
}
state = _service().build_initial_turn_state(
request=RAGAskRequest(question="这个项目你觉得发展怎么样"),
route_plan={},
conversation_context={"object_ledger": ledger},
turn_hypothesis={
"normalized_goal": "判断当前项目发展状态",
"answer_shape": "strategic_judgement",
"primary_object": {"binding_strength": "none"},
"goal_frames": [
{
"frame_id": "followup-project-judgement",
"operation_kind": "inspect_object",
"target_scope": {"scope_type": "current_project", "binding_strength": "workspace"},
"target_object_requirement": "single",
"answer_shape": "strategic_judgement",
}
],
"operation_gaps": [
{
"key": "project_status_snapshot",
"gap_type": "system_evidence_gap",
"blocking": False,
"user_actionable": False,
"system_action_hint": "需要读取项目状态快照",
"reason": "需要先读取绑定项目证据。",
}
],
"can_answer_with_assumptions": False,
},
)
assert state.object_state.primary_object is not None
assert state.object_state.primary_object.object_ref == "project:141"
assert state.object_state.primary_object.binding_strength == "session_object_ledger"
def test_packet_i_system_probe_wins_before_user_clarification_when_object_is_bound() -> None:
ledger = {
"packet_version": "pmflow_ai_object_ledger.v1",
"current_focus_object": {
"object_type": "project",
"object_ref": "project:141",
"title": "童析",
},
}
state = _service().build_initial_turn_state(
request=RAGAskRequest(question="这个项目你觉得发展怎么样"),
route_plan={},
conversation_context={"object_ledger": ledger},
turn_hypothesis={
"normalized_goal": "判断当前项目发展状态",
"answer_shape": "strategic_judgement",
"primary_object": {"binding_strength": "none"},
"goal_frames": [
{
"frame_id": "project-development-judgement",
"operation_kind": "inspect_object",
"target_scope": {"scope_type": "current_project", "binding_strength": "workspace"},
"target_object_requirement": "single",
"answer_shape": "strategic_judgement",
}
],
"operation_gaps": [
{
"key": "project_status_snapshot",
"gap_type": "system_evidence_gap",
"blocking": False,
"user_actionable": False,
"system_action_hint": "需要读取项目状态快照",
"reason": "需要读取绑定项目状态证据。",
},
{
"key": "user_metric_preference",
"gap_type": "user_clarification_gap",
"blocking": True,
"user_actionable": True,
"customer_question": "你更关心增长、交付还是风险?",
"reason": "补充维度可提升判断质量。",
},
],
"needs_user_input": True,
"can_answer_with_assumptions": False,
},
)
decision = _service().assess_sufficiency(state)
assert decision.must_ask_user is False
assert decision.next_action_type == "inspect_project_status"
def test_packet_x_bound_project_blocks_external_search_until_project_operating_probe() -> None:
ledger = {
"packet_version": "pmflow_ai_object_ledger.v1",
"current_focus_object": {
"object_type": "project",
"object_ref": "project:141",
"title": "童析",
},
}
state = _service().build_initial_turn_state(
request=RAGAskRequest(question="现在这个项目怎么样了,详细跟我说下"),
route_plan={},
conversation_context={"object_ledger": ledger},
turn_hypothesis={
"normalized_goal": "获取当前项目整体状态与关键进展细节",
"answer_shape": "composite_answer",
"primary_object": {"binding_strength": "none"},
"object_binding_decision": {"decision": "accept_previous_focus"},
"goal_frames": [
{
"frame_id": "wrong-external-composite",
"operation_kind": "judge_direction",
"target_scope": {"scope_type": "external_public", "binding_strength": "surface"},
"target_object_requirement": "none",
"source_policy": "external",
"answer_shape": "composite_answer",
}
],
"operation_gaps": [
{
"key": "public_project_evidence",
"gap_type": "freshness_gap",
"blocking": False,
"user_actionable": False,
"system_action_hint": "external_search",
"reason": "错误地要求外部公开证据。",
}
],
"can_answer_with_assumptions": True,
},
)
decision = _service().assess_sufficiency(state)
action = _service().select_next_minimal_action(state)
assert state.object_state.primary_object is not None
assert state.object_state.primary_object.object_ref == "project:141"
assert decision.next_action_type == "inspect_project_status"
assert action is not None
assert action.action_type == "inspect_project_status"
assert all(item.action_type != "external_search" for item in state.action_state.candidate_actions[:1])
def test_packet_z_final_answer_wrong_source_verifier_forces_project_probe() -> None:
ledger = {
"packet_version": "pmflow_ai_object_ledger.v1",
"current_focus_object": {
"object_type": "project",
"object_ref": "project:141",
"title": "童析",
},
}
state = _service().build_initial_turn_state(
request=RAGAskRequest(question="现在这个项目怎么样了,详细跟我说下"),
route_plan={},
conversation_context={"object_ledger": ledger},
turn_hypothesis={
"normalized_goal": "获取当前项目整体状态与关键进展细节",
"answer_shape": "composite_answer",
"primary_object": {"binding_strength": "none"},
"object_binding_decision": {"decision": "accept_previous_focus"},
"goal_frames": [
{
"frame_id": "wrong-final-answer-frame",
"operation_kind": "judge_direction",
"target_scope": {"scope_type": "external_public", "binding_strength": "surface"},
"source_policy": "external",
"answer_shape": "composite_answer",
}
],
"operation_gaps": [],
"can_answer_with_assumptions": True,
},
)
failure = _service()._verify_runtime_payload(
turn_state=state,
payload={
"_runtime_payload_kind": "final_answer",
"answer": "当前无法提供该项目具体状态。",
"source_policy": "external",
"sources": [{"source_type": "external_public", "label": "外部公开来源"}],
},
)
assert failure is not None
assert failure.failure_type == "wrong_source_or_missing_project_operating_state"
assert failure.next_action_hint == "inspect_project_status"
assert failure.state_patch["needs_internal_evidence"] is True
def test_packet_aa_verifier_recovery_keeps_required_project_probe_executable() -> None:
frame = PMFlowAIGoalFrame(
frame_id="wrong-source-frame",
operation_kind="judge_direction",
target_scope=PMFlowAITargetScope(scope_type="external_public", binding_strength="surface"),
answer_shape="composite_answer",
)
state = PMFlowAITurnState(
selected_frame=frame,
candidate_frames=[frame],
answer_state=PMFlowAIAnswerState(answer_shape="composite_answer"),
object_state=PMFlowAITurnObjectState(
primary_object=PMFlowAITurnObjectRef(
object_type="project",
object_ref="project:141",
title="童析",
binding_strength="session_object_ledger",
)
),
evidence_state=PMFlowAITurnEvidenceState(),
)
failure = _service()._verify_runtime_payload(
turn_state=state,
payload={"_runtime_payload_kind": "final_answer", "answer": "当前无法提供项目具体状态。"},
)
PMFlowAITurnStateReviser().revise_from_verification(turn_state=state, failure=failure)
decision = _service().assess_sufficiency(state)
action = _service().select_next_minimal_action(state)
assert failure is not None
assert decision.next_action_type == "inspect_project_status"
assert action is not None
assert action.action_type == "inspect_project_status"
assert state.completed is False
def test_packet_j_new_project_topic_does_not_inherit_previous_project_focus() -> None:
ledger = {
"packet_version": "pmflow_ai_object_ledger.v1",
"current_focus_object": {
"object_type": "project",
"object_ref": "project:141",
"title": "童析",
},
}
state = _service().build_initial_turn_state(
request=RAGAskRequest(question="解决信息投流公司大量员工混剪视频的问题,通过AI自动化混剪降低人工成本"),
route_plan={},
conversation_context={"object_ledger": ledger},
turn_hypothesis={
"normalized_goal": "评估一个新的AI视频混剪自动化项目方向",
"answer_shape": "strategic_judgement",
"primary_object": {"binding_strength": "none"},
"object_binding_decision": {
"decision": "create_new_focus",
"rejected_object_ref": "project:141",
"new_focus_title": "信息投流公司AI自动化混剪",
},
"goal_frames": [
{
"frame_id": "new-video-automation-direction",
"operation_kind": "guide_execution",
"target_scope": {"scope_type": "unspecified", "binding_strength": "none"},
"target_object_requirement": "none",
"answer_shape": "strategic_judgement",
}
],
"operation_gaps": [
{
"key": "company_context",
"gap_type": "user_clarification_gap",
"blocking": True,
"user_actionable": True,
"customer_question": "请补充典型混剪工作流。",
"reason": "缺少业务上下文。",
},
{
"key": "old_project_scope_rejected",
"gap_type": "scope_gap",
"blocking": False,
"user_actionable": False,
"system_action_hint": "previous focus is unrelated to this new idea",
"reason": "上一轮项目与本轮新想法无关。",
},
],
"needs_user_input": True,
"can_answer_with_assumptions": False,
},
)
decision = _service().assess_sufficiency(state)
action_types = [item.action_type for item in state.action_state.candidate_actions]
assert state.object_state.primary_object is not None
assert state.object_state.primary_object.object_ref == ""
assert state.object_state.primary_object.object_type == "new_idea"
assert state.object_state.primary_object.binding_strength == "active_workspace_strong"
assert "inspect_project_status" not in action_types
assert decision.next_action_type == "ask_clarification"
def test_packet_j_off_topic_project_evidence_is_not_absorbed_into_new_idea_goal() -> None:
state = _service().build_initial_turn_state(
request=RAGAskRequest(question="解决信息投流公司大量员工混剪视频的问题,通过AI自动化混剪降低人工成本"),
route_plan={},
turn_hypothesis={
"normalized_goal": "评估一个新的AI视频混剪自动化项目方向",
"answer_shape": "strategic_judgement",
"primary_object": {"binding_strength": "none"},
"object_binding_decision": {"decision": "create_new_focus", "rejected_object_ref": "project:141"},
"goal_frames": [
{
"frame_id": "new-video-automation-direction",
"operation_kind": "guide_execution",
"target_scope": {"scope_type": "unspecified", "binding_strength": "none"},
"target_object_requirement": "none",
"answer_shape": "strategic_judgement",
}
],
"can_answer_with_assumptions": True,
},
)
updated = _service().absorb_evidence(
turn_state=state,
action=PMFlowAIRuntimeAction(
action_id="inspect_project_status",
action_type="inspect_project_status",
reason="读取项目状态",
tool_name="project_status_evidence_tool",
),
payload={
"_runtime_payload_kind": "evidence",
"runtime_evidence_tool_result": {
"tool_name": "project_status_evidence_tool",
"action_type": "inspect_project_status",
"findings": [
{
"fact": "童析 当前状态证据:项目状态 active",
"source": "PMFlow 项目状态证据",
"confidence": "high",
}
],
"sources": [{"source": "project_status", "display_label": "PMFlow 项目状态证据"}],
"remaining_gaps": [],
"confidence": "high",
"summary": "读取项目状态证据,得到 1 条事实。",
},
},
started_at=0,
)
assert updated.evidence_state.known_facts == []
assert "off_topic_evidence_excluded" in updated.evidence_state.stale_or_missing