PMFlow/backend/tests/test_ai_pmo_task_intent_con...

234 lines
8.0 KiB
Python

import pytest
from app.services.ai_pmo.ai_pmo_intent import (
INTENT_FOLLOWUP_CONTINUE,
INTENT_QUERY,
INTENT_TASK_CREATE,
ROUTE_ACTION_ANSWER,
ROUTE_ACTION_CANCEL,
ROUTE_ACTION_CONTINUE,
ROUTE_ACTION_CREATE,
ROUTE_FAMILY_QUERY,
ROUTE_FAMILY_TASK,
ROUTE_QUERY_ANSWER,
ROUTE_TASK_CREATE,
CanonicalIntent,
IntentResolver,
RouteResolver,
)
from app.services.ai_pmo.ai_pmo_kernel_common_core.imports import WORKFLOW_ROUTE_KEYS
from app.services.ai_pmo.ai_pmo_typed_executor_execute.service import TypedExecutorExecuteMixin
from app.services.ai_pmo.semantic_context.decision_schema import SemanticDecision
from app.services.ai_pmo.workflow_semantic_planner import WorkflowSemanticPlanner
def _planner_decision(*, workflows: list[dict]) -> SemanticDecision:
return SemanticDecision(
topic_relation="new_topic",
mode_decision="answer_only",
workflow_plans=workflows,
scene_metadata={},
requested_actions=["workflow.bundle.compose"],
confidence=0.92,
)
def test_intent_resolver_recognizes_task_create(monkeypatch: pytest.MonkeyPatch) -> None:
async def _fake_decide(cls, *, message_text: str, source_system: str, metadata=None):
_ = (message_text, source_system, metadata)
return _planner_decision(
workflows=[
{
"workflow_type": "task_flow",
"workflow_action": "create",
"target_scope": "project",
"operation_codes": ["task.create"],
"confidence": 0.92,
}
]
)
monkeypatch.setattr(WorkflowSemanticPlanner, "_decide_cold_start_semantics", classmethod(_fake_decide))
intent = IntentResolver.resolve(
text="请在项目#12 新建任务:修复登录超时",
project_id=12,
)
assert intent.name == INTENT_TASK_CREATE
assert intent.project_id == 12
assert str((intent.metadata or {}).get("task_title") or "").startswith("修复登录超时")
def test_route_resolver_builds_task_create_route() -> None:
route = RouteResolver.from_canonical(
canonical_intent=CanonicalIntent(
name=INTENT_TASK_CREATE,
confidence=0.9,
project_id=3,
signal_title="修复登录超时",
signal_summary="修复线上登录超时问题",
metadata={},
),
message_text="新增任务:修复登录超时",
)
assert route.family == ROUTE_FAMILY_TASK
assert route.action == ROUTE_ACTION_CREATE
assert route.key == ROUTE_TASK_CREATE
def test_intent_resolver_negated_write_statement_follows_semantic_query_bundle(monkeypatch: pytest.MonkeyPatch) -> None:
async def _fake_decide(cls, *, message_text: str, source_system: str, metadata=None):
_ = (message_text, source_system, metadata)
return _planner_decision(
workflows=[
{
"workflow_type": "query_flow",
"workflow_action": "answer",
"target_scope": "global",
"operation_codes": ["query.answer"],
"confidence": 0.86,
}
]
)
monkeypatch.setattr(WorkflowSemanticPlanner, "_decide_cold_start_semantics", classmethod(_fake_decide))
intent = IntentResolver.resolve(
text="先不要创建任务,告诉我当前进展和风险",
project_id=7,
)
assert intent.name == INTENT_QUERY
assert str((intent.metadata or {}).get("reason") or "") != "negated_write_statement"
def test_intent_resolver_followup_tokens_no_longer_override_semantic_query(monkeypatch: pytest.MonkeyPatch) -> None:
async def _fake_decide(cls, *, message_text: str, source_system: str, metadata=None):
_ = (cls, message_text, source_system, metadata)
return _planner_decision(
workflows=[
{
"workflow_type": "query_flow",
"workflow_action": "answer",
"target_scope": "global",
"operation_codes": ["query.answer"],
"confidence": 0.86,
}
]
)
monkeypatch.setattr(WorkflowSemanticPlanner, "_decide_cold_start_semantics", classmethod(_fake_decide))
intent = IntentResolver.resolve(
text="确认这条",
project_id=9,
)
assert intent.name == INTENT_QUERY
def test_intent_resolver_requirement_initiation_query_bundle_does_not_upgrade_to_project_create(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def _fake_decide(cls, *, message_text: str, source_system: str, metadata=None):
_ = (cls, message_text, source_system, metadata)
return _planner_decision(
workflows=[
{
"workflow_type": "query_flow",
"workflow_action": "answer",
"target_scope": "global",
"operation_codes": ["query.answer"],
"confidence": 0.91,
}
]
)
monkeypatch.setattr(WorkflowSemanticPlanner, "_decide_cold_start_semantics", classmethod(_fake_decide))
intent = IntentResolver.resolve(
text="想做个新需求,关于给运营公司打造主播推流的需求。",
project_id=None,
)
assert intent.name == INTENT_QUERY
assert intent.signal_title == ""
def test_route_resolver_builds_query_answer_route() -> None:
route = RouteResolver.from_canonical(
canonical_intent=CanonicalIntent(
name=INTENT_QUERY,
confidence=0.92,
project_id=7,
signal_title="",
signal_summary="当前进展和风险",
metadata={},
),
message_text="当前进展和风险是什么?",
)
assert route.family == ROUTE_FAMILY_QUERY
assert route.action == ROUTE_ACTION_ANSWER
assert route.key == ROUTE_QUERY_ANSWER
def test_route_resolver_followup_action_requires_structured_metadata() -> None:
action = RouteResolver.resolve_followup_route_action(
canonical_intent=INTENT_FOLLOWUP_CONTINUE,
message_text="先取消这个工作项",
)
assert action == ROUTE_ACTION_CONTINUE
def test_route_resolver_followup_action_no_longer_parses_message_text_with_context() -> None:
action = RouteResolver.resolve_followup_route_action(
canonical_intent=INTENT_FOLLOWUP_CONTINUE,
message_text="先取消这个工作项",
metadata={"followup_context_present": True, "target_work_item_id": 8},
)
assert action == ROUTE_ACTION_CONTINUE
def test_kernel_common_imports_exposes_workflow_route_keys() -> None:
assert "query.answer" in WORKFLOW_ROUTE_KEYS
assert "task.create" in WORKFLOW_ROUTE_KEYS
class _DummyTypedExecutor(TypedExecutorExecuteMixin):
def __init__(self) -> None:
self.called: dict[str, object] = {}
async def _execute_task_create_intent(self, *, intent, message_text: str, operator_user_id):
self.called = {
"intent": intent.name,
"message_text": message_text,
"operator_user_id": operator_user_id,
}
return {"status": "succeeded", "task_id": 77}
@pytest.mark.asyncio
async def test_typed_executor_dispatches_task_create() -> None:
executor = _DummyTypedExecutor()
intent = CanonicalIntent(
name=INTENT_TASK_CREATE,
confidence=0.88,
project_id=6,
signal_title="补充埋点",
signal_summary="补充行为埋点以支持漏斗分析",
metadata={"task_title": "补充埋点"},
)
result = await executor.execute(
intent=intent,
message_text="请创建任务:补充埋点",
sender="alice",
operator_user_id=101,
source_system="feishu",
source_message_id="msg-1",
)
assert result["status"] == "succeeded"
assert result["task_id"] == 77
assert executor.called["intent"] == INTENT_TASK_CREATE
assert executor.called["operator_user_id"] == 101