441 lines
18 KiB
Python
441 lines
18 KiB
Python
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
|
||
|