PMFlow/backend/tests/test_ai_pmo_ingress_mainlin...

3173 lines
115 KiB
Python
Raw 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.

import asyncio
from datetime import datetime, timedelta
from types import SimpleNamespace
import pytest
import pytest_asyncio
from fastapi import HTTPException
from sqlalchemy import JSON, select
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from app.api.ai_pmo_api import routes_core
from app.api.ai_pmo_api import helpers_inbox_filters
from app.api.ai_pmo_api import helpers_inbox_payload
from app.api.ai_pmo_api.schemas import PMOProcessTextRequest
from app.services.ai_pmo.consultant_dispatch import build_consultant_dispatch_contract
from app.services.ai_pmo.ai_pmo_kernel.mixins.intent.ai_pmo_kernel_mixins_intent_resolve import (
KernelIntentResolveMixin,
)
from app.services.ai_pmo.ai_pmo_kernel.mixins.intent.ai_pmo_kernel_mixins_intent_llm import (
KernelIntentLLMMixin,
)
from app.services.ai_pmo.ai_pmo_kernel.mixins.intent.ai_pmo_kernel_mixins_intent_rules import (
KernelIntentRulesMixin,
)
from app.services.ai_pmo.ai_pmo_kernel.ai_agent_kernel import gate_execution
from app.services.ai_pmo.ai_pmo_kernel.ai_pmo_reply_renderer import ReplyRenderer
from app.services.ai_pmo.ai_pmo_kernel import ai_pmo_typed_executor_mixins_parse
from app.services.ai_pmo.ai_pmo_kernel_ingress_flow import mixin_entry
from app.services.ai_pmo.ai_pmo_kernel.mixins.ingress import ai_pmo_kernel_mixins_ingress_render
from app.services.ai_pmo.ai_pmo_kernel_ingress_flow import mixin_prepare_manual
from app.services.ai_pmo.ai_pmo_kernel_world_model_core import mixin_project
from app.services.ai_pmo.ai_pmo_typed_executor_execute import mixin_execute_query_project
from app.services.ai_pmo.ai_pmo_typed_executor_execute import mixin_execute_mutations
from app.services.ai_pmo.ai_pmo_intent import CanonicalIntent
from app.services.ai_pmo.draft_confirm_handler import DraftConfirmHandler
from app.services.adaptive_rag_service import mixin_deterministic_query_core
from app.services.adaptive_rag_service import mixin_fast_answer_project_main
from app.services.llm_service import ChatCompletionResult, StructuredExtractionResult
from app.services.llm_service import mixin_tier_escalation
from app.services.llm_service.mixin_tier_escalation import LLMTierEscalationMixin
from app.models.base import Base
from app.models.mounted_ai_runtime import MountedDraftWorkspace, MountedTopicWorkspace
from app.utils.thread_context import build_feishu_conversation_scope_key, build_feishu_thread_key
def _use_sqlite_requirement_plan_json_types() -> None:
for table in (
MountedTopicWorkspace.__table__,
MountedDraftWorkspace.__table__,
):
for column in table.columns:
if column.name in {
"facts_json",
"goals_json",
"constraints_json",
"options_json",
"open_questions",
"pending_actions",
"bound_entities",
"metadata_json",
"requirement_card",
"draft_payload",
"required_confirmations",
}:
column.type = JSON()
@pytest_asyncio.fixture
async def requirement_plan_db_session():
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
_use_sqlite_requirement_plan_json_types()
async with engine.begin() as conn:
await conn.run_sync(
lambda sync_conn: Base.metadata.create_all(
sync_conn,
tables=[
MountedTopicWorkspace.__table__,
MountedDraftWorkspace.__table__,
],
)
)
maker = async_sessionmaker(engine, expire_on_commit=False)
async with maker() as session:
yield session
await engine.dispose()
@pytest.mark.asyncio
async def test_process_text_route_uses_pmflow_manual_ingress(monkeypatch) -> None:
captured: dict[str, object] = {}
class FakeKernel:
def __init__(self, db, tenant_id: int):
captured["init_db"] = db
captured["init_tenant_id"] = tenant_id
async def process_ingress(self, **kwargs):
captured["ingress_kwargs"] = kwargs
return {
"handled": True,
"status": "answered",
"classification": "question",
"direct_reply_required": True,
"direct_reply_text": "当前主链正常。",
}
monkeypatch.setattr(routes_core, "AIPMOKernelService", FakeKernel)
request = PMOProcessTextRequest(
message_text="现在进展怎么样?",
sender="产品经理A",
source_message_id="manual-1",
project_id=11,
)
current_user = SimpleNamespace(name="ignored", username="ignored")
response = await routes_core.process_text(
request=request,
current_user=current_user,
db=object(),
tenant_id=5,
)
assert response["code"] == 0
data = response["data"]
assert data["status"] == "answered"
kwargs = captured["ingress_kwargs"]
assert isinstance(kwargs, dict)
assert kwargs["source_system"] == "pmflow_manual"
assert kwargs["source_message_id"] == "manual-1"
assert kwargs["message_text"] == "现在进展怎么样?"
assert kwargs["sender"] == "产品经理A"
assert kwargs["channel_ref"] == "pmflow_manual"
assert kwargs["chat_type"] == "manual"
assert kwargs["project_hint_id"] == 11
@pytest.mark.asyncio
async def test_process_text_route_rejects_unhandled_result(monkeypatch) -> None:
class FakeKernel:
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def process_ingress(self, **kwargs):
_ = kwargs
return {"handled": False, "reason": "kernel_not_handled"}
monkeypatch.setattr(routes_core, "AIPMOKernelService", FakeKernel)
request = PMOProcessTextRequest(message_text="hello")
current_user = SimpleNamespace(name="u", username="u")
with pytest.raises(HTTPException) as exc_info:
await routes_core.process_text(
request=request,
current_user=current_user,
db=object(),
tenant_id=1,
)
assert exc_info.value.status_code == 400
assert str(exc_info.value.detail) == "kernel_not_handled"
@pytest.mark.asyncio
async def test_prepare_mixed_sidecar_context_passes_mixed_contract(monkeypatch) -> None:
captured: dict[str, object] = {}
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
captured["tenant_id"] = tenant_id
_ = db
async def classify_intent(self, text: str):
captured["classified_text"] = text
return {
"intent": "mixed",
"question_part": "现在进展如何",
"statement_part": "新增任务确认流",
}
async def answer_question(self, **kwargs):
captured["answer_kwargs"] = kwargs
return {"answer": "[回答] 进展稳定\n[已收到需求] 新增任务确认流"}
class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 9
monkeypatch.setattr(mixin_entry, "AdaptiveRAGService", FakeAdaptiveRAGService)
kernel = FakeKernel()
clean_text, mixed_meta = await kernel._prepare_mixed_sidecar_context(
source_system="pmflow_manual",
source_message_id="manual-msg-2",
channel_ref="pmflow-chat",
source_sender_open_id="ou-pmflow-1",
thread_context={"reply_to_message_id": "reply-1"},
message_text="顺便问下现在进展如何,并且新增任务确认流",
project_hint_id=22,
)
assert clean_text == "新增任务确认流"
assert mixed_meta["mixed_intent"] is True
assert mixed_meta["mixed_question_part"] == "现在进展如何"
assert mixed_meta["mixed_signal_part"] == "新增任务确认流"
assert "[回答] 进展稳定" in str(mixed_meta.get("mixed_sidecar_reply") or "")
answer_kwargs = captured["answer_kwargs"]
assert isinstance(answer_kwargs, dict)
assert answer_kwargs["asked_via"] == "pmflow_manual"
assert answer_kwargs["project_id"] is None
assert answer_kwargs["message_intent"] == "mixed"
assert answer_kwargs["source_sender_open_id"] == "ou-pmflow-1"
assert answer_kwargs["thread_context"] == {"reply_to_message_id": "reply-1"}
assert answer_kwargs["mixed_signal_part"] == "新增任务确认流"
assert answer_kwargs["conversation_key"] == "manual-msg-2"
@pytest.mark.asyncio
async def test_process_ingress_prioritizes_draft_pending_confirm(monkeypatch) -> None:
class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 1
self.draft_called = False
async def _prepare_ingress_envelope(self, **kwargs):
_ = kwargs
return {
"early_result": None,
"clean_text": "确认",
"synthetic_message_id": "s-msg-1",
"closure_payload": {},
"ingest_event": SimpleNamespace(id=100),
"operator": SimpleNamespace(id=8),
"work_item": SimpleNamespace(id=55),
"session": SimpleNamespace(last_intent=mixin_entry.STATE_DRAFT_PENDING_CONFIRM),
}
async def _maybe_handle_draft_confirmation_ingress(self, **kwargs):
self.draft_called = True
_ = kwargs
return {
"handled": True,
"status": "answered",
"classification": "command",
"direct_reply_required": True,
"direct_reply_text": "已确认草稿并继续执行。",
}
def _is_short_confirmation_message(self, **kwargs):
_ = kwargs
return False
async def _prepare_mixed_sidecar_context(self, **kwargs):
_ = kwargs
raise AssertionError("draft_pending_confirm flow should bypass mixed sidecar precheck")
kernel = FakeKernel()
result = await kernel.process_ingress(
source_system="feishu",
source_message_id="feishu-1",
message_text="确认",
sender="tester",
channel_ref="chat-1",
source_sender_open_id="ou_xxx",
ingress_options={"thread_context": {"reply_to_message_id": "msg-root-1"}},
)
assert kernel.draft_called is True
assert result["status"] == "answered"
assert "草稿" in result["direct_reply_text"]
@pytest.mark.asyncio
async def test_process_ingress_records_mounted_runtime_for_draft_confirmation(monkeypatch) -> None:
captured: dict[str, object] = {"calls": []}
class FakeMountedAIRuntimeWriteService:
def __init__(self, db) -> None:
captured["db"] = db
async def begin_ingress_run(self, **kwargs):
captured["calls"].append(("begin", kwargs))
return SimpleNamespace(
topic_workspace=SimpleNamespace(id=91, tenant_id=1),
conversation_run=SimpleNamespace(id=92),
context_layers_hit={"current_message": True, "thread": True, "topic": False},
)
async def record_orchestrator_decision(self, **kwargs):
captured["calls"].append(("record", kwargs))
return "plan"
async def record_agent_trace(self, **kwargs):
captured["calls"].append(("agent_trace", kwargs))
async def finalize_run(self, **kwargs):
captured["calls"].append(("finalize", kwargs))
class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 1
async def _prepare_ingress_envelope(self, **kwargs):
_ = kwargs
return {
"early_result": None,
"clean_text": "确认",
"synthetic_message_id": "s-msg-mounted-1",
"closure_payload": {},
"ingest_event": SimpleNamespace(id=100),
"operator": SimpleNamespace(id=8),
"work_item": SimpleNamespace(id=55),
"session": SimpleNamespace(last_intent=mixin_entry.STATE_DRAFT_PENDING_CONFIRM),
}
async def _maybe_handle_draft_confirmation_ingress(self, **kwargs):
_ = kwargs
return {
"handled": True,
"status": "answered",
"classification": "command",
"workflow_route": "followup.confirm",
"reply_template_key": "action_succeeded",
"work_item_id": 55,
"direct_reply_required": True,
"direct_reply_text": "已确认草稿并继续执行。",
}
def _is_short_confirmation_message(self, **kwargs):
_ = kwargs
return False
async def _prepare_mixed_sidecar_context(self, **kwargs):
_ = kwargs
raise AssertionError("draft confirmation flow should bypass mixed sidecar precheck")
monkeypatch.setattr(mixin_entry, "MountedAIRuntimeWriteService", FakeMountedAIRuntimeWriteService)
kernel = FakeKernel()
result = await kernel.process_ingress(
source_system="feishu",
source_message_id="feishu-mounted-1",
message_text="确认",
sender="tester",
channel_ref="chat-1",
source_sender_open_id="ou_xxx",
ingress_options={"thread_context": {"reply_to_message_id": "msg-root-1"}},
)
assert result["status"] == "answered"
assert [name for name, _ in captured["calls"]] == ["begin", "record", "agent_trace", "finalize"]
begin_kwargs = captured["calls"][0][1]
record_kwargs = captured["calls"][1][1]
agent_trace_kwargs = captured["calls"][2][1]
finalize_kwargs = captured["calls"][3][1]
assert begin_kwargs["source_system"] == "feishu"
assert begin_kwargs["source_message_id"] == "s-msg-mounted-1"
assert record_kwargs["workflow_route"] == "followup.confirm"
assert agent_trace_kwargs["agent_name"] not in {"ConversationOrchestrator", "ReplyComposer"}
assert agent_trace_kwargs["output_summary"]["workflow_route"] == "followup.confirm"
assert finalize_kwargs["result"]["direct_reply_text"] == "已确认草稿并继续执行。"
def test_should_route_to_draft_confirmation_requires_anchor_or_explicit_id() -> None:
session = SimpleNamespace(last_intent=mixin_entry.STATE_DRAFT_PENDING_CONFIRM)
assert mixin_entry.KernelIngressFlowEntryMixin._should_route_to_draft_confirmation(
session=session,
message_text="确认",
ingress_options=None,
) is False
assert mixin_entry.KernelIngressFlowEntryMixin._should_route_to_draft_confirmation(
session=session,
message_text="确认",
ingress_options={"thread_context": {"thread_id": "th_123"}},
) is False
assert mixin_entry.KernelIngressFlowEntryMixin._should_route_to_draft_confirmation(
session=session,
message_text="确认",
ingress_options={"thread_context": {"root_message_id": "root_123"}},
) is False
assert mixin_entry.KernelIngressFlowEntryMixin._should_route_to_draft_confirmation(
session=session,
message_text="创建时间是多少?",
ingress_options={"thread_context": {"reply_to_message_id": "msg-123"}},
) is False
assert mixin_entry.KernelIngressFlowEntryMixin._should_route_to_draft_confirmation(
session=session,
message_text="确认",
ingress_options={"thread_context": {"reply_to_message_id": "msg-123"}},
) is True
assert mixin_entry.KernelIngressFlowEntryMixin._should_route_to_draft_confirmation(
session=session,
message_text="工作项ID: 88 创建时间是多少?",
ingress_options=None,
) is True
def test_short_confirmation_requires_reply_anchor() -> None:
session = SimpleNamespace(
last_intent=mixin_entry.STATE_DRAFT_PENDING_CONFIRM,
last_active_at=datetime.now(),
)
sent_at = datetime.now() + timedelta(seconds=1)
assert (
KernelIntentRulesMixin._is_short_confirmation_message(
message_text="确认",
session=session,
message_sent_at=sent_at,
session_snapshot=None,
reply_anchor_present=False,
)
is False
)
assert (
KernelIntentRulesMixin._is_short_confirmation_message(
message_text="确认",
session=session,
message_sent_at=sent_at,
session_snapshot=None,
reply_anchor_present=True,
)
is True
)
def test_natural_revision_routes_to_draft_confirmation_without_reply_anchor() -> None:
session = SimpleNamespace(last_intent=mixin_entry.STATE_DRAFT_PENDING_CONFIRM)
assert mixin_entry.KernelIngressFlowEntryMixin._should_route_to_draft_confirmation(
session=session,
message_text="第一版先做直播录屏和评论回放切片,再加亮点和掉点总结,最后给一份复盘清单。",
ingress_options=None,
) is True
def test_scope_narrowing_revision_routes_to_draft_confirmation_without_reply_anchor() -> None:
session = SimpleNamespace(last_intent=mixin_entry.STATE_DRAFT_PENDING_CONFIRM)
assert mixin_entry.KernelIngressFlowEntryMixin._should_route_to_draft_confirmation(
session=session,
message_text="第一版先别做太大,先把三件事做好就行:直播录屏和评论回放切片、亮点和掉点自动总结、给团队一份能直接执行的复盘清单。",
ingress_options=None,
) is True
def test_smalltalk_does_not_route_to_draft_confirmation_without_reply_anchor() -> None:
session = SimpleNamespace(last_intent=mixin_entry.STATE_DRAFT_PENDING_CONFIRM)
assert mixin_entry.KernelIngressFlowEntryMixin._should_route_to_draft_confirmation(
session=session,
message_text="收到,辛苦了",
ingress_options=None,
) is False
def test_new_project_statement_does_not_route_to_existing_draft_without_anchor() -> None:
session = SimpleNamespace(last_intent=mixin_entry.STATE_DRAFT_PENDING_CONFIRM)
assert mixin_entry.KernelIngressFlowEntryMixin._should_route_to_draft_confirmation(
session=session,
message_text="最近直播复盘太慢了,想做个给运营团队用的小工具,这个新需求先叫 FeishuWebFlow-abc。",
ingress_options=None,
) is False
def test_mounted_runtime_query_business_review_uses_plan_mode_and_non_executor_agent() -> None:
mode = mixin_entry.KernelIngressFlowEntryMixin._resolve_mounted_runtime_mode_override(
workflow_route="query.answer",
route_metadata={
"message_intent": "business_review",
"consultant_mode_preference": "clarify",
},
reply_template_key=None,
)
agent = mixin_entry.KernelIngressFlowEntryMixin._resolve_mounted_runtime_agent_name(
workflow_route="query.answer",
auto_execute=True,
route_metadata={
"message_intent": "business_review",
"consultant_mode_preference": "clarify",
},
reply_template_key=None,
mode_hint=mode,
)
assert mode == "plan"
assert agent == "PlanCoach"
def test_mounted_runtime_query_advise_uses_assignee_recommender() -> None:
mode = mixin_entry.KernelIngressFlowEntryMixin._resolve_mounted_runtime_mode_override(
workflow_route="query.answer",
route_metadata={
"message_intent": "business_review",
"consultant_mode_preference": "advise",
"advise_subtype": "assignee",
},
reply_template_key=None,
)
agent = mixin_entry.KernelIngressFlowEntryMixin._resolve_mounted_runtime_agent_name(
workflow_route="query.answer",
auto_execute=True,
route_metadata={
"message_intent": "business_review",
"consultant_mode_preference": "advise",
"advise_subtype": "assignee",
},
reply_template_key=None,
mode_hint=mode,
)
assert mode == "plan"
assert agent == "AssigneeRecommender"
def test_mounted_runtime_query_requirement_plan_uses_plan_coach() -> None:
mode = mixin_entry.KernelIngressFlowEntryMixin._resolve_mounted_runtime_mode_override(
workflow_route="query.answer",
route_metadata={
"message_intent": "requirement_plan",
"consultant_mode_preference": "clarify",
},
reply_template_key=None,
)
agent = mixin_entry.KernelIngressFlowEntryMixin._resolve_mounted_runtime_agent_name(
workflow_route="query.answer",
auto_execute=True,
route_metadata={
"message_intent": "requirement_plan",
"consultant_mode_preference": "clarify",
},
reply_template_key=None,
mode_hint=mode,
)
assert mode == "plan"
assert agent == "PlanCoach"
def test_mounted_runtime_query_requirement_plan_uses_semantic_bundle_metadata_for_plan_mode() -> None:
mode = mixin_entry.KernelIngressFlowEntryMixin._resolve_mounted_runtime_mode_override(
workflow_route="query.answer",
route_metadata={
"semantic_bundle": {
"diagnostics": {
"message_intent": "requirement_plan",
"consultant_mode_preference": "clarify",
}
}
},
reply_template_key=None,
)
assert mode == "plan"
def test_build_canonical_intent_keeps_project_update_instead_of_dropping_to_ignore() -> None:
intent = KernelIntentRulesMixin._build_canonical_intent(
intent_name="project.update",
message_text="把项目2的 success criteria 改成 48 小时内必须上线。",
project_id=2,
confidence=0.91,
metadata={"field": "success_criteria"},
)
assert intent.name == "project.update"
assert intent.project_id == 2
assert intent.metadata["field"] == "success_criteria"
def test_explicit_named_project_create_skips_existing_project_name_binding() -> None:
assert mixin_project.KernelWorldModelCoreProjectMixin._should_skip_implicit_project_binding(
message_text=(
"最近我们做跨境直播复盘太慢了,想做个给运营团队用的小工具,"
"这个需求先叫 FeishuWebFlow-1775301894322。"
),
) is True
def test_project_lookup_question_keeps_existing_project_name_binding() -> None:
assert mixin_project.KernelWorldModelCoreProjectMixin._should_skip_implicit_project_binding(
message_text="帮我看下跨境直播复盘这个项目现在怎么样?",
) is False
@pytest.mark.asyncio
async def test_draft_confirm_handler_prefers_latest_sender_scope_candidate_without_anchor() -> None:
latest_work_item = SimpleNamespace(
id=202,
state="draft_pending_confirm",
source_message_id="msg-latest",
)
old_work_item = SimpleNamespace(
id=101,
state="draft_pending_confirm",
source_message_id="msg-old",
)
ingest_payload = {
"sender": "tester",
"channel_ref": "chat-1",
"source_sender_open_id": "ou_123",
}
rows = [
(latest_work_item, SimpleNamespace(payload=dict(ingest_payload))),
(old_work_item, SimpleNamespace(payload=dict(ingest_payload))),
]
class FakeResult:
def __init__(self, value):
self._value = value
def all(self):
return list(self._value)
class FakeDB:
async def execute(self, _stmt):
return FakeResult(rows)
handler = DraftConfirmHandler(SimpleNamespace(db=FakeDB(), tenant_id=1))
target = await handler._resolve_pending_draft(
sender="tester",
source_system="feishu",
channel_ref="chat-1",
source_sender_open_id="ou_123",
explicit_work_item_id=None,
thread_context=None,
)
assert target == rows[0]
@pytest.mark.asyncio
async def test_draft_confirm_handler_skips_followup_wrapper_when_resolving_latest_draft() -> None:
followup_wrapper = SimpleNamespace(
id=303,
state="draft_pending_confirm",
source_message_id="msg-followup",
intent="followup.confirm",
)
latest_project_draft = SimpleNamespace(
id=202,
state="draft_pending_confirm",
source_message_id="msg-project",
intent="project_create.create",
)
ingest_payload = {
"sender": "tester",
"channel_ref": "chat-1",
"source_sender_open_id": "ou_123",
}
rows = [
(followup_wrapper, SimpleNamespace(payload=dict(ingest_payload))),
(latest_project_draft, SimpleNamespace(payload=dict(ingest_payload))),
]
class FakeResult:
def __init__(self, value):
self._value = value
def all(self):
return list(self._value)
class FakeDB:
async def execute(self, _stmt):
return FakeResult(rows)
handler = DraftConfirmHandler(SimpleNamespace(db=FakeDB(), tenant_id=1))
target = await handler._resolve_pending_draft(
sender="tester",
source_system="feishu",
channel_ref="chat-1",
source_sender_open_id="ou_123",
explicit_work_item_id=None,
thread_context=None,
)
assert target == rows[1]
def test_build_feishu_thread_key_prefers_actor_scope_without_anchor() -> None:
assert build_feishu_thread_key(
source_chat_id="chat-1",
source_sender_open_id="ou_dm_1",
sender="产品经理A",
source_message_id="om_current_message",
thread_context=None,
) == "feishu:chat-1:ou_dm_1"
def test_low_value_closure_payload_helper_handles_summary_text() -> None:
assert helpers_inbox_filters._is_low_value_closure_payload(
{"closure_summary": "待补充核心痛点"}
) is False
def test_serialize_work_item_summary_handles_message_text_without_nameerror() -> None:
row = SimpleNamespace(
id=9,
source_system="pmflow_manual",
source_message_id="msg-9",
draft_content={},
state="received",
review_summary={},
action_bundle={},
intent="query.read_only",
project_id=None,
signal_id=None,
reply_template_key=None,
execution_result={},
draft_expires_at=None,
draft_version=0,
confirm_message_id=None,
created_at=None,
updated_at=None,
decision_level="portfolio",
)
ingest = SimpleNamespace(payload={"message_text": " 最近 状态 怎么样 "})
payload = helpers_inbox_payload._serialize_work_item_summary(
row=row,
ingest=ingest,
project=None,
)
assert payload["source_message_text"] == "最近 状态 怎么样"
def test_coerce_rag_business_review_scene_keeps_rag_ask_on_query_chain() -> None:
class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin):
def __init__(self) -> None:
self.tenant_id = 1
kernel = FakeKernel()
original_intent = mixin_entry.CanonicalIntent(
name="signal.create",
confidence=0.91,
project_id=None,
signal_title="大模型 API 聚合平台",
signal_summary="我现在要做一个大模型api聚合平台,你帮我分析下要怎么做,给出mvp需求",
metadata={"llm_function": "classify_intent_ai_pmo_kernel"},
)
coerced = kernel._coerce_intent_for_query_scene(
intent=original_intent,
source_system="pmflow_manual",
message_text="我现在要做一个大模型api聚合平台,你帮我分析下要怎么做,给出mvp需求",
rag_request_context={
"asked_via": "pmflow_manual",
"action_mode": "auto",
},
semantic_bundle=None,
world_model={},
)
assert coerced.name == "query.read_only"
assert coerced.metadata["message_intent"] == "business_review"
assert coerced.metadata["consultant_mode_preference"] == "clarify"
assert coerced.metadata["workflow_route"] == "query.answer"
semantic_bundle = coerced.metadata["semantic_bundle"]
assert semantic_bundle["diagnostics"]["message_intent"] == "business_review"
assert semantic_bundle["diagnostics"]["workflow_route"] == "query.answer"
def test_coerce_feishu_semantic_business_review_bundle_keeps_query_chain() -> None:
class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin):
def __init__(self) -> None:
self.tenant_id = 1
kernel = FakeKernel()
original_intent = mixin_entry.CanonicalIntent(
name="signal.create",
confidence=0.93,
project_id=None,
signal_title="大模型 API 聚合平台",
signal_summary="我现在要创建需求为一个大模型api聚合平台,你帮我分析下要怎么做,",
metadata={"llm_function": "classify_intent_ai_pmo_kernel"},
)
coerced = kernel._coerce_intent_for_query_scene(
intent=original_intent,
source_system="feishu",
message_text="我现在要创建需求为一个大模型api聚合平台,你帮我分析下要怎么做,",
rag_request_context=None,
semantic_bundle={
"primary_workflow": "query_flow",
"diagnostics": {
"message_intent": "business_review",
"review_framework": "five_lenses",
"consultant_mode_preference": "clarify",
},
},
world_model={},
)
assert coerced.name == "query.read_only"
assert coerced.metadata["message_intent"] == "business_review"
assert coerced.metadata["workflow_route"] == "query.answer"
def test_coerce_feishu_deferred_requirement_discussion_to_query_chain_from_llm_bundle() -> None:
class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin):
def __init__(self) -> None:
self.tenant_id = 1
kernel = FakeKernel()
original_intent = mixin_entry.CanonicalIntent(
name="project.create",
confidence=0.94,
project_id=None,
signal_title="直播复盘工具",
signal_summary="最近直播复盘太慢了,想做个给运营团队用的小工具,先不要直接创建,我再补充几轮,最后确认再入库。",
metadata={
"semantic_bundle": mixin_entry.WorkflowSemanticPlanner.plan_from_intent(
intent_name="project.create",
message_text="最近直播复盘太慢了,想做个给运营团队用的小工具,先不要直接创建,我再补充几轮,最后确认再入库。",
source_system="ai_pmo_ingress",
workflow_route="project_create.create",
metadata={"confidence": 0.94, "requires_confirmation": True},
).to_dict()
},
)
coerced = kernel._coerce_intent_for_query_scene(
intent=original_intent,
source_system="feishu",
message_text="最近直播复盘太慢了,想做个给运营团队用的小工具,先不要直接创建,我再补充几轮,最后确认再入库。",
rag_request_context=None,
semantic_bundle=None,
world_model={},
)
assert coerced.name == "query.read_only"
assert coerced.metadata["message_intent"] == "requirement_plan"
assert coerced.metadata["consultant_mode_preference"] == "clarify"
assert coerced.metadata["workflow_route"] == "query.answer"
def test_coerce_feishu_requirement_plan_without_semantic_bundle_keeps_query_chain() -> None:
class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin):
def __init__(self) -> None:
self.tenant_id = 1
kernel = FakeKernel()
original_intent = mixin_entry.CanonicalIntent(
name="project.create",
confidence=0.95,
project_id=None,
signal_title="A股趋势日评打分工具",
signal_summary="做一个自动给每日的大A趋势打分的小工具,先不创建,先收敛第一版。",
metadata={"llm_function": "classify_intent_ai_pmo_kernel"},
)
coerced = kernel._coerce_intent_for_query_scene(
intent=original_intent,
source_system="feishu",
message_text="想跟你聊聊需求,做一个自动给每日的大A趋势打分的小工具,先不创建,先收敛第一版。",
rag_request_context=None,
semantic_bundle=None,
world_model={},
)
assert coerced.name == "query.read_only"
assert coerced.metadata["message_intent"] == "requirement_plan"
assert coerced.metadata["consultant_mode_preference"] == "clarify"
assert coerced.metadata["workflow_route"] == "query.answer"
def test_coerce_feishu_assignee_recommendation_to_query_chain_from_llm_bundle() -> None:
class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin):
def __init__(self) -> None:
self.tenant_id = 1
kernel = FakeKernel()
original_intent = mixin_entry.CanonicalIntent(
name="project.create",
confidence=0.88,
project_id=None,
signal_title="直播复盘工具",
signal_summary="那你觉得分配给谁做比较合适?",
metadata={
"semantic_bundle": mixin_entry.WorkflowSemanticPlanner.plan_from_intent(
intent_name="project.create",
message_text="那你觉得分配给谁做比较合适?",
source_system="ai_pmo_ingress",
workflow_route="project_create.create",
metadata={"confidence": 0.88, "requires_confirmation": True},
).to_dict()
},
)
coerced = kernel._coerce_intent_for_query_scene(
intent=original_intent,
source_system="feishu",
message_text="那你觉得分配给谁做比较合适?",
rag_request_context=None,
semantic_bundle=None,
world_model={},
)
assert coerced.name == "query.read_only"
assert coerced.metadata["message_intent"] == "business_review"
assert coerced.metadata["consultant_mode_preference"] == "advise"
assert coerced.metadata["workflow_route"] == "query.answer"
@pytest.mark.asyncio
async def test_kernel_intent_resolve_inherits_recent_feishu_session_context_without_reply_anchor() -> None:
captured: dict[str, object] = {}
class FakeResolver(KernelIntentResolveMixin):
def __init__(self) -> None:
self.tenant_id = 1
def _is_short_confirmation_message(self, **kwargs):
_ = kwargs
return False
@staticmethod
def _intent_llm_enabled() -> bool:
return True
async def _resolve_intent_with_llm(self, **kwargs):
captured.update(kwargs)
return CanonicalIntent(
name="query.read_only",
confidence=0.93,
project_id=21,
signal_title="",
signal_summary="",
metadata={},
)
resolver = FakeResolver()
session = SimpleNamespace(
sender="feishu:chat-1:ou_dm_1",
last_intent="query.read_only",
current_project_id=21,
last_active_at=datetime.now(),
)
_, source = await resolver._resolve_intent(
message_text="我需要对应的需求mvp",
session=session,
world_model={},
thread_context=None,
)
assert source == "llm_only"
assert captured["session_last_intent_override"] == "query.read_only"
assert captured["session_project_id_override"] == 21
@pytest.mark.asyncio
async def test_kernel_intent_resolve_locks_signal_source_request_before_llm() -> None:
class FakeResolver(KernelIntentResolveMixin, KernelIntentRulesMixin):
def __init__(self) -> None:
self.tenant_id = 1
def _is_short_confirmation_message(self, **kwargs):
_ = kwargs
return False
@staticmethod
def _intent_llm_enabled() -> bool:
return True
async def _resolve_intent_with_llm(self, **kwargs):
_ = kwargs
raise AssertionError("explicit signal source request should not hit llm classifier")
resolver = FakeResolver()
intent, source = await resolver._resolve_intent(
message_text="给项目ID: 2 接一个 YouTube 录屏加评论回放的信号源,先不要执行,先出草稿。",
session=None,
world_model={"resolved_project_id": 2},
thread_context=None,
)
assert source == "rule_signal_source_onboard"
assert intent.name == "signal_source.onboard"
assert intent.project_id == 2
assert intent.metadata["source_request"] is True
@pytest.mark.asyncio
async def test_kernel_intent_llm_prompt_guides_natural_problem_statement_to_project_create(monkeypatch) -> None:
captured: dict[str, object] = {}
class FakeResolver(KernelIntentLLMMixin):
def __init__(self) -> None:
self.tenant_id = 1
self.db = object()
def _build_canonical_intent(self, *, intent_name, message_text, project_id, confidence, metadata): # noqa: ANN001
return CanonicalIntent(
name=intent_name,
confidence=confidence,
project_id=project_id,
signal_title="",
signal_summary=message_text[:300],
metadata=metadata,
)
async def fake_generate_structured_object(prompt: str, **kwargs): # noqa: ANN001
captured["prompt"] = prompt
captured["kwargs"] = kwargs
return StructuredExtractionResult(
payload={
"family": "project_create",
"action": "create",
"intent": "project.create",
"confidence": 0.93,
"project_id": None,
"requested_count": 1,
"requires_confirmation": False,
"parameters": {},
"reasoning": "用户在描述真实业务痛点,并明确表达想做一个工具解决。",
},
source_field="generated_text",
provider="mock",
model="mock-model",
raw_text='{"intent":"project.create"}',
validation="ok",
)
monkeypatch.setattr(
"app.services.ai_pmo.ai_pmo_kernel.mixins.intent.ai_pmo_kernel_mixins_intent_llm.llm_service.generate_structured_object",
fake_generate_structured_object,
)
resolver = FakeResolver()
intent = await resolver._resolve_intent_with_llm(
message_text="最近直播复盘太慢了,我想做个给运营用的小工具,能自动切片、总结亮点掉点。",
session=None,
world_model={},
rule_intent=None,
)
assert intent is not None
assert intent.name == "project.create"
prompt = str(captured["prompt"] or "")
assert "自然语言" in prompt
assert "想做个工具" in prompt
assert "先帮我分析" in prompt
def test_reply_renderer_formats_project_delete_pending_and_success() -> None:
renderer = ReplyRenderer(
state_waiting_manual="waiting_manual",
state_failed="failed",
receipt_phase_action="action",
receipt_phase_pending="pending",
receipt_phase_error="error",
)
intent = CanonicalIntent(
name="project.delete",
confidence=0.96,
project_id=None,
signal_title="",
signal_summary="",
metadata={"llm_parameters": {"project_id": 12, "project_title": "重复项目"}},
)
pending = renderer.render(
work_state="waiting_manual",
intent=intent,
decision_record={"approval_requirement": {"message": "检测到删除或归档类写操作,必须进入显式审批/人工确认。"}},
execution_result={
"status": "waiting_manual",
"action_contract": {
"operation_code": "project.delete",
"params": {"project_id": 12, "project_title": "重复项目"},
},
},
)
assert pending["template_key"] == "pending_manual"
assert "重复项目" in pending["text"]
assert "人工确认" in pending["text"]
success = renderer.render(
work_state="succeeded",
intent=intent,
decision_record={"approval_requirement": {}},
execution_result={"status": "succeeded", "operation_code": "project.delete", "project_id": 12},
)
assert success["template_key"] == "action_succeeded"
assert "已删除项目#12《重复项目》" in success["text"]
@pytest.mark.asyncio
async def test_finalize_waiting_manual_passes_execution_result_to_renderer(monkeypatch) -> None:
captured: dict[str, object] = {}
class FakeReviewService:
async def review_work_item(self, **kwargs):
_ = kwargs
return {"verified": False, "issues": ["waiting_manual"], "next_step": "请确认"}
class FakeRenderer:
def render(self, **kwargs):
captured["render_kwargs"] = kwargs
return {
"receipt_phase": "pending",
"template_key": "pending_manual",
"payload": {"intent": kwargs["intent"].name},
"text": "ok",
}
class FakeKernel(mixin_prepare_manual.KernelIngressFlowPrepareManualMixin):
def __init__(self) -> None:
class _FakeDB:
async def commit(self):
return None
self.db = _FakeDB()
self.tenant_id = 7
self.review_service = FakeReviewService()
self.reply_renderer = FakeRenderer()
async def _maybe_emit_risk_cadence_snapshot(self, **kwargs):
_ = kwargs
async def _claim_reply_receipt(self, **kwargs):
_ = kwargs
return True
async def _write_processing_log(self, **kwargs):
_ = kwargs
async def _update_session(self, **kwargs):
_ = kwargs
async def _upsert_operation_execution_ledger(self, **kwargs):
_ = kwargs
return None
def _extract_execution_operation_code(self, execution_result):
if not isinstance(execution_result, dict):
return None
action_contract = execution_result.get("action_contract") if isinstance(execution_result.get("action_contract"), dict) else {}
if isinstance(action_contract, dict):
operation_code = str(action_contract.get("operation_code") or "").strip().lower()
if operation_code:
return operation_code
operation_code = str(execution_result.get("operation_code") or "").strip().lower()
return operation_code or None
def _normalize_execution_ledger_status(self, *, work_item_state, execution_status):
_ = execution_status
return "pending_manual" if str(work_item_state or "").strip().lower() == "waiting_manual" else "processing"
async def _project_waiting_manual(self, **kwargs):
captured["project_waiting_manual"] = kwargs
def _apply_execution_action_contract(self, **kwargs):
execution_result = dict(kwargs["execution_result"])
execution_result["action_contract"] = {
"operation_code": "project.delete",
"params": {"project_id": 12, "project_title": "重复项目"},
}
return execution_result
def _build_capture_result(self, *, work_item, intent, execution_result, render, direct_reply_required, status, classification):
_ = (work_item, intent, execution_result, render, direct_reply_required, classification)
return {"status": status, "render": render}
kernel = FakeKernel()
work_item = SimpleNamespace(
state=None,
action_bundle=None,
execution_result=None,
review_summary=None,
reply_template_key=None,
id=99,
project_id=12,
signal_id=None,
intent="project.delete",
source_message_id="msg-1",
draft_version=None,
draft_expires_at=None,
)
session = SimpleNamespace()
ingest_event = SimpleNamespace()
decision_payload = {
"policy": {"approval_requirement": {"message": "检测到删除或归档类写操作,必须进入显式审批/人工确认。"}},
"execution_plan": {"actions": []},
"simulation": {},
}
workflow_route = SimpleNamespace(key="project.delete", action="delete")
intent = CanonicalIntent(
name="project.delete",
confidence=0.94,
project_id=12,
signal_title="",
signal_summary="",
metadata={"llm_parameters": {"project_id": 12, "project_title": "重复项目"}},
)
result = await kernel._finalize_waiting_manual_ingress(
work_item=work_item,
session=session,
intent=intent,
world_model={},
decision_payload=decision_payload,
workflow_route=workflow_route,
decision_trace={"reason": "test"},
source_system="feishu",
source_message_id="msg-1",
message_text="删除重复项目",
sender="tester",
ingest_event=ingest_event,
started=0.0,
)
assert result["status"] == "captured"
render_kwargs = captured["render_kwargs"]
assert isinstance(render_kwargs, dict)
assert render_kwargs["execution_result"]["action_contract"]["operation_code"] == "project.delete"
assert render_kwargs["execution_result"]["action_contract"]["params"]["project_title"] == "重复项目"
def test_gate_execution_ignores_low_confidence_but_keeps_risk_review() -> None:
low_confidence = gate_execution(confidence=0.02, risk_level="medium")
assert low_confidence.action == "auto_execute"
assert low_confidence.auto_execute is True
assert low_confidence.gate_code == "auto_execute_allowed"
assert "置信度" not in low_confidence.reason
high_risk = gate_execution(confidence=0.02, risk_level="high")
assert high_risk.action == "auto_execute"
assert high_risk.auto_execute is True
assert high_risk.gate_code == "auto_execute_allowed"
assert "人工确认" not in high_risk.reason
@pytest.mark.asyncio
async def test_execute_query_intent_honors_rag_request_context(monkeypatch) -> None:
captured: dict[str, object] = {}
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = db
captured["tenant_id"] = tenant_id
async def answer_question(self, **kwargs):
captured["answer_kwargs"] = kwargs
return {"answer": "来自统一 ingress 的回答", "session_id": 7}
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 3
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
intent = SimpleNamespace(
project_id=12,
metadata={
"rag_request_context": {
"asked_by": 101,
"asked_via": "pmflow_manual",
"project_id": 19,
"source_chat_id": "chat-abc",
"session_id": 55,
"force_new_session": True,
"thinking_mode": "deep",
"action_mode": "read_write",
"sync_requirement": True,
"message_intent": "mixed",
"mixed_signal_part": "新增任务确认流",
"consultant_mode_preference": "challenge",
}
},
)
result = await executor._execute_query_intent(
intent=intent,
message_text="请同步更新状态",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-1",
)
assert result["status"] == "succeeded"
assert "统一 ingress" in result["answer"]
answer_kwargs = captured["answer_kwargs"]
assert isinstance(answer_kwargs, dict)
assert answer_kwargs["asked_by"] == 101
assert answer_kwargs["asked_via"] == "pmflow_manual"
assert answer_kwargs["project_id"] == 19
assert answer_kwargs["source_chat_id"] == "chat-abc"
assert answer_kwargs["session_id"] == 55
assert answer_kwargs["force_new_session"] is True
assert answer_kwargs["thinking_mode"] == "deep"
assert answer_kwargs["action_mode"] == "read_write"
assert answer_kwargs["sync_requirement"] is True
assert answer_kwargs["message_intent"] == "mixed"
assert answer_kwargs["mixed_signal_part"] == "新增任务确认流"
assert answer_kwargs["consultant_mode_preference"] == "challenge"
@pytest.mark.asyncio
async def test_execute_query_intent_feishu_fallback_uses_thread_key(monkeypatch) -> None:
captured: dict[str, object] = {}
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = db
captured["tenant_id"] = tenant_id
async def answer_question(self, **kwargs):
captured["answer_kwargs"] = kwargs
return {"answer": "fallback-answer", "session_id": 7}
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 3
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
intent = SimpleNamespace(
project_id=None,
metadata={
"thread_context": {"root_message_id": "root-1"},
"source_sender_open_id": "ou-feishu-99",
},
)
result = await executor._execute_query_intent(
intent=intent,
message_text="最近AI行业有什么动态",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-feishu-1",
)
assert result["status"] == "succeeded"
answer_kwargs = captured["answer_kwargs"]
assert isinstance(answer_kwargs, dict)
assert answer_kwargs["asked_by"] is None
assert answer_kwargs["asked_via"] == "feishu"
assert answer_kwargs["source_chat_id"] is None
assert answer_kwargs["source_sender_open_id"] == "ou-feishu-99"
assert answer_kwargs["thread_context"] == {"root_message_id": "root-1"}
assert answer_kwargs["conversation_key"] == build_feishu_conversation_scope_key(
source_chat_id=None,
source_sender_open_id="ou-feishu-99",
sender=None,
)
@pytest.mark.asyncio
async def test_execute_query_intent_uses_semantic_bundle_rag_context_when_no_explicit_rag_request(monkeypatch) -> None:
captured: dict[str, object] = {}
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = db
captured["tenant_id"] = tenant_id
async def answer_question(self, **kwargs):
captured["answer_kwargs"] = kwargs
return {"answer": "corrected-answer", "session_id": 9}
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 3
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
intent = SimpleNamespace(
project_id=None,
metadata={
"source_chat_id": "chat-dm-9",
"source_sender_open_id": "ou-feishu-9",
"semantic_bundle": {
"diagnostics": {
"message_intent": "correction_escalation",
"consultant_mode_preference": "challenge",
"force_thinking_mode": "deep",
}
},
},
)
result = await executor._execute_query_intent(
intent=intent,
message_text="不对,调用高级模型来回答我的问题",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-feishu-9",
)
assert result["status"] == "succeeded"
answer_kwargs = captured["answer_kwargs"]
assert isinstance(answer_kwargs, dict)
assert answer_kwargs["asked_by"] is None
assert answer_kwargs["asked_via"] == "feishu"
assert answer_kwargs["source_chat_id"] == "chat-dm-9"
assert answer_kwargs["source_sender_open_id"] == "ou-feishu-9"
assert answer_kwargs["message_intent"] == "correction_escalation"
assert answer_kwargs["consultant_mode_preference"] == "challenge"
assert answer_kwargs["thinking_mode"] == "deep"
assert answer_kwargs["conversation_key"] == build_feishu_conversation_scope_key(
source_chat_id="chat-dm-9",
source_sender_open_id="ou-feishu-9",
sender=None,
)
@pytest.mark.asyncio
async def test_execute_query_intent_rolls_back_failed_rag_transaction(monkeypatch) -> None:
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def answer_question(self, **kwargs):
_ = kwargs
raise RuntimeError("rag failed before response")
class FakeDB:
def __init__(self) -> None:
self.rollback_calls = 0
self.sync_session = SimpleNamespace(is_active=False)
async def rollback(self) -> None:
self.rollback_calls += 1
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = FakeDB()
self.tenant_id = 3
@staticmethod
def _build_query_error_message(exc: Exception) -> str:
return f"query failed: {exc}"
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
intent = SimpleNamespace(project_id=12, metadata={})
result = await executor._execute_query_intent(
intent=intent,
message_text="现在完整调用链是什么?",
operator_user_id=999,
source_system="pmflow_manual",
source_message_id="msg-rollback-1",
)
assert result["status"] == "failed_safe"
assert "rag failed" in str(result["error_message"] or "")
assert executor.db.rollback_calls == 1
@pytest.mark.asyncio
async def test_execute_query_intent_does_not_rollback_healthy_session_on_rag_failure(monkeypatch) -> None:
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def answer_question(self, **kwargs):
_ = kwargs
raise RuntimeError("rag failed before response")
class FakeDB:
def __init__(self) -> None:
self.rollback_calls = 0
self.sync_session = SimpleNamespace(is_active=True)
async def rollback(self) -> None:
self.rollback_calls += 1
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = FakeDB()
self.tenant_id = 3
@staticmethod
def _build_query_error_message(exc: Exception) -> str:
return f"query failed: {exc}"
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
intent = SimpleNamespace(project_id=12, metadata={})
result = await executor._execute_query_intent(
intent=intent,
message_text="先分析,不要直接创建。",
operator_user_id=999,
source_system="pmflow_manual",
source_message_id="msg-rollback-2",
)
assert result["status"] == "succeeded"
assert "先不创建项目" in str(result["answer"] or "")
assert executor.db.rollback_calls == 0
@pytest.mark.asyncio
async def test_execute_query_intent_short_circuits_casual_chat_without_rag(monkeypatch) -> None:
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def answer_question(self, **kwargs):
_ = kwargs
raise AssertionError("casual chat should not enter heavy RAG path")
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 3
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
intent = SimpleNamespace(
project_id=None,
metadata={
"semantic_bundle": {
"diagnostics": {
"message_intent": "casual_chat",
"consultant_mode_preference": "brainstorm",
}
}
},
)
result = await executor._execute_query_intent(
intent=intent,
message_text="早上好,今天你在线吗",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-chat-1",
)
assert result["status"] == "succeeded"
assert "我在线" in str(result["answer"] or "")
assert str(result["rag_result"]["query_type"]) == "chat"
@pytest.mark.asyncio
async def test_execute_query_intent_short_circuits_requirement_followup_clarify_without_rag(monkeypatch) -> None:
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def answer_question(self, **kwargs):
_ = kwargs
raise AssertionError("requirement clarify follow-up should not enter heavy RAG path")
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 3
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
intent = SimpleNamespace(
project_id=None,
metadata={
"thread_context": {"root_message_id": "root-1", "reply_to_message_id": "root-1"},
"semantic_bundle": {
"diagnostics": {
"message_intent": "business_review",
"consultant_mode_preference": "clarify",
}
},
},
)
result = await executor._execute_query_intent(
intent=intent,
message_text="那就先按上面的方向继续,但是别太重",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-chat-2",
)
assert result["status"] == "succeeded"
assert "轻量版继续理解" in str(result["answer"] or "")
assert str(result["rag_result"]["query_type"]) == "clarify"
@pytest.mark.asyncio
async def test_execute_query_intent_short_circuits_requirement_planning_without_rag(monkeypatch) -> None:
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def answer_question(self, **kwargs):
_ = kwargs
raise AssertionError("requirement planning should not enter heavy RAG path")
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 3
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
intent = SimpleNamespace(project_id=None, metadata={})
result = await executor._execute_query_intent(
intent=intent,
message_text="我们想做一个给电动车品牌用的直播复盘工具,先别创建项目,我还在补范围。",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-chat-2b",
)
answer = str(result["answer"] or "")
assert result["status"] == "succeeded"
assert "不创建" in answer
assert "哪一步" in answer
assert answer.count("?") <= 1
assert str(result["rag_result"]["query_type"]) == "plan"
@pytest.mark.asyncio
async def test_execute_query_intent_requirement_plan_scene_short_circuits_without_rag(monkeypatch) -> None:
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def answer_question(self, **kwargs):
_ = kwargs
raise AssertionError("requirement plan scene should not enter heavy RAG path")
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 3
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
intent = SimpleNamespace(
project_id=None,
metadata={
"semantic_bundle": {
"diagnostics": {
"message_intent": "requirement_plan",
"consultant_mode_preference": "clarify",
}
}
},
)
result = await executor._execute_query_intent(
intent=intent,
message_text="想跟你聊聊需求,做一个自动给每日的大A趋势打分的小工具,先不创建,先收敛第一版。",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-chat-plan-1",
)
answer = str(result["answer"] or "")
assert result["status"] == "succeeded"
assert "主要想根据什么来打" in answer
assert "三件事" not in answer
assert answer.count("?") <= 1
assert str(result["rag_result"]["query_type"]) == "plan"
@pytest.mark.asyncio
async def test_execute_query_intent_requirement_plan_followup_persists_progress_and_asks_next_question(
monkeypatch,
requirement_plan_db_session,
) -> None:
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def answer_question(self, **kwargs):
_ = kwargs
raise AssertionError("requirement plan follow-up should not enter heavy RAG path")
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = requirement_plan_db_session
self.tenant_id = 3
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
initial_intent = SimpleNamespace(
project_id=None,
metadata={
"source_chat_id": "chat-plan-1",
"source_sender_open_id": "ou-plan-1",
"semantic_bundle": {
"diagnostics": {
"message_intent": "requirement_plan",
"consultant_mode_preference": "clarify",
}
},
},
)
followup_intent = SimpleNamespace(
project_id=None,
metadata={
"source_chat_id": "chat-plan-1",
"source_sender_open_id": "ou-plan-1",
"thread_context": {
"root_message_id": "msg-plan-root",
"reply_to_message_id": "msg-plan-root",
},
"semantic_bundle": {
"diagnostics": {
"message_intent": "requirement_plan",
"consultant_mode_preference": "clarify",
}
},
},
)
first = await executor._execute_query_intent(
intent=initial_intent,
message_text="想跟你聊聊需求,做一个自动给每日的大A趋势打分的小工具,先不创建,先收敛第一版。",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-root",
)
second = await executor._execute_query_intent(
intent=followup_intent,
message_text="先按技术指标来,主要给投顾看。",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-reply-1",
)
assert first["status"] == "succeeded"
assert second["status"] == "succeeded"
assert "根据什么来打" in str(first["answer"] or "")
assert "我先记下" in str(second["answer"] or "")
assert "技术指标" in str(second["answer"] or "")
assert "主要想根据什么来打" not in str(second["answer"] or "")
assert "怎么接收" in str(second["answer"] or "") or "输出" in str(second["answer"] or "")
topic = (
await requirement_plan_db_session.execute(
select(MountedTopicWorkspace).where(MountedTopicWorkspace.tenant_id == 3)
)
).scalar_one()
draft = (
await requirement_plan_db_session.execute(
select(MountedDraftWorkspace).where(MountedDraftWorkspace.tenant_id == 3)
)
).scalar_one()
card = draft.requirement_card or {}
assert topic.mode == "plan"
assert draft.state == "collecting"
assert card["scoring_basis"]
assert "技术指标" in str(card["scoring_basis"])
assert "投顾" in str(card["target_user"])
@pytest.mark.asyncio
async def test_execute_query_intent_requirement_plan_builds_feishu_friendly_draft_after_enough_answers(
monkeypatch,
requirement_plan_db_session,
) -> None:
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def answer_question(self, **kwargs):
_ = kwargs
raise AssertionError("requirement plan draft should not enter heavy RAG path")
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = requirement_plan_db_session
self.tenant_id = 3
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
intent_root = SimpleNamespace(
project_id=None,
metadata={
"source_chat_id": "chat-plan-2",
"source_sender_open_id": "ou-plan-2",
"semantic_bundle": {
"diagnostics": {
"message_intent": "requirement_plan",
"consultant_mode_preference": "clarify",
}
},
},
)
intent_followup = SimpleNamespace(
project_id=None,
metadata={
"source_chat_id": "chat-plan-2",
"source_sender_open_id": "ou-plan-2",
"thread_context": {
"root_message_id": "msg-plan-root-2",
"reply_to_message_id": "msg-plan-root-2",
},
"semantic_bundle": {
"diagnostics": {
"message_intent": "requirement_plan",
"consultant_mode_preference": "clarify",
}
},
},
)
await executor._execute_query_intent(
intent=intent_root,
message_text="想跟你聊聊需求,做一个自动给每日的大A趋势打分的小工具,先不创建,先收敛第一版。",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-root-2",
)
await executor._execute_query_intent(
intent=intent_followup,
message_text="先按技术指标来,主要给投顾看。",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-reply-2a",
)
await executor._execute_query_intent(
intent=intent_followup,
message_text="先做成飞书日报,每天收盘后给一个分数和一句解释。",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-reply-2b",
)
final_result = await executor._execute_query_intent(
intent=intent_followup,
message_text="第一版就先保留分数、方向判断和一句解释,别做太重。",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-reply-2c",
)
answer = str(final_result["answer"] or "")
assert final_result["status"] == "succeeded"
assert "需求草稿" in answer
assert "【需求名称】" in answer
assert "【打分依据】" in answer
assert "【输出形式】" in answer
assert "【更新频率】" in answer
assert str(final_result["rag_result"]["query_type"]) == "draft"
draft = (
await requirement_plan_db_session.execute(
select(MountedDraftWorkspace).where(MountedDraftWorkspace.tenant_id == 3)
)
).scalar_one()
topic = (
await requirement_plan_db_session.execute(
select(MountedTopicWorkspace).where(MountedTopicWorkspace.tenant_id == 3)
)
).scalar_one()
assert draft.state == "ready"
assert "每日 A 股趋势打分工具" in str((draft.draft_payload or {}).get("title") or "")
assert topic.plan_status == "awaiting_execute_confirm"
assert topic.pending_actions == ["confirm_requirement_draft"]
@pytest.mark.asyncio
async def test_execute_query_intent_explicit_new_requirement_topic_suspends_previous_plan(
monkeypatch,
requirement_plan_db_session,
) -> None:
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def answer_question(self, **kwargs):
_ = kwargs
raise AssertionError("explicit new topic switching should not enter heavy RAG path")
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = requirement_plan_db_session
self.tenant_id = 3
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
active_plan_intent = SimpleNamespace(
project_id=None,
metadata={
"source_chat_id": "chat-plan-switch",
"source_sender_open_id": "ou-plan-switch",
"semantic_bundle": {
"diagnostics": {
"message_intent": "requirement_plan",
"consultant_mode_preference": "clarify",
}
},
},
)
explicit_switch_intent = SimpleNamespace(
project_id=None,
metadata={
"source_chat_id": "chat-plan-switch",
"source_sender_open_id": "ou-plan-switch",
"semantic_bundle": {
"diagnostics": {
"message_intent": "requirement_plan",
"consultant_mode_preference": "clarify",
"topic_switch": "new",
}
},
},
)
await executor._execute_query_intent(
intent=active_plan_intent,
message_text="想跟你聊聊需求,做一个自动给每日的大A趋势打分的小工具,先不创建,先收敛第一版。",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-switch-root",
)
switch_result = await executor._execute_query_intent(
intent=explicit_switch_intent,
message_text="不对啊,现在是另一个需求,先别沿用刚才那版。",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-switch-new",
)
rows = (
await requirement_plan_db_session.execute(
select(MountedTopicWorkspace)
.where(MountedTopicWorkspace.tenant_id == 3)
.order_by(MountedTopicWorkspace.id.asc())
)
).scalars().all()
assert len(rows) == 2
assert "切到新需求" in str(switch_result["answer"] or "")
assert rows[0].status == "suspended"
assert rows[0].plan_status == "suspended"
assert rows[1].status == "active"
assert rows[1].plan_status == "collecting"
@pytest.mark.asyncio
async def test_execute_query_intent_low_overlap_requirement_seed_switches_new_topic(
monkeypatch,
requirement_plan_db_session,
) -> None:
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def answer_question(self, **kwargs):
_ = kwargs
raise AssertionError("low-overlap requirement switching should not enter heavy RAG path")
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = requirement_plan_db_session
self.tenant_id = 3
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
current_topic = MountedTopicWorkspace(
tenant_id=3,
topic_key="feishu:chat-plan-overlap:root:msg-plan-overlap-root",
source_system="feishu",
channel_ref="chat-plan-overlap",
conversation_scope_key=build_feishu_conversation_scope_key(
source_chat_id="chat-plan-overlap",
source_sender_open_id="ou-plan-overlap",
sender=None,
),
root_message_id="msg-plan-overlap-root",
mode="plan",
status="active",
plan_status="awaiting_execute_confirm",
facts_json={
"title": "每日 A 股趋势打分工具",
"target_user": "投顾",
"scoring_basis": "技术指标(均线、成交量、涨跌比等)",
"output_form": "飞书日报",
},
goals_json=["每天产出一个可解释的 A 股趋势分数,辅助判断和记录。"],
constraints_json={"first_scope": "先保留一个分数、方向判断和一句解释"},
pending_actions=["confirm_requirement_draft"],
metadata_json={"plan_mode": True},
)
requirement_plan_db_session.add(current_topic)
await requirement_plan_db_session.flush()
requirement_plan_db_session.add(
MountedDraftWorkspace(
tenant_id=3,
topic_workspace_id=int(current_topic.id),
state="ready",
draft_version=1,
requirement_card={
"title": "每日 A 股趋势打分工具",
"goal": "每天产出一个可解释的 A 股趋势分数,辅助判断和记录。",
"target_user": "投顾",
"scoring_basis": "技术指标(均线、成交量、涨跌比等)",
"output_form": "飞书日报",
"cadence": "每日",
"first_scope": "先保留一个分数、方向判断和一句解释",
"plan_profile": "scoring",
},
draft_payload={"title": "每日 A 股趋势打分工具"},
required_confirmations=["需求草稿需要人工确认后才允许正式创建"],
)
)
await requirement_plan_db_session.flush()
implicit_switch_intent = SimpleNamespace(
project_id=None,
metadata={
"source_chat_id": "chat-plan-overlap",
"source_sender_open_id": "ou-plan-overlap",
"semantic_bundle": {
"diagnostics": {
"message_intent": "requirement_plan",
"consultant_mode_preference": "clarify",
}
},
},
)
switch_result = await executor._execute_query_intent(
intent=implicit_switch_intent,
message_text="想跟你聊需求,关于批量自动化部署openclaw,先不创建,先收第一版。",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-overlap-new",
)
rows = (
await requirement_plan_db_session.execute(
select(MountedTopicWorkspace)
.where(MountedTopicWorkspace.tenant_id == 3)
.order_by(MountedTopicWorkspace.id.asc())
)
).scalars().all()
assert len(rows) == 2
assert "每日 A 股趋势打分工具" not in str(switch_result["answer"] or "")
assert rows[0].status == "suspended"
assert rows[0].plan_status == "suspended"
assert rows[1].status == "active"
assert rows[1].plan_status == "collecting"
@pytest.mark.asyncio
async def test_execute_query_intent_requirement_plan_accepts_fragmented_short_answers_and_only_drafts_when_scope_arrives(
monkeypatch,
requirement_plan_db_session,
) -> None:
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def answer_question(self, **kwargs):
_ = kwargs
raise AssertionError("fragmented requirement plan should not enter heavy RAG path")
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = requirement_plan_db_session
self.tenant_id = 3
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
root_intent = SimpleNamespace(
project_id=None,
metadata={
"source_chat_id": "chat-plan-3",
"source_sender_open_id": "ou-plan-3",
"semantic_bundle": {
"diagnostics": {
"message_intent": "requirement_plan",
"consultant_mode_preference": "clarify",
}
},
},
)
fragmented_followup_intent = SimpleNamespace(
project_id=None,
metadata={
"source_chat_id": "chat-plan-3",
"source_sender_open_id": "ou-plan-3",
"thread_context": {
"root_message_id": "msg-plan-root-3",
"reply_to_message_id": "msg-plan-root-3",
},
},
)
first = await executor._execute_query_intent(
intent=root_intent,
message_text="想跟你聊聊需求,做一个自动给每日的大A趋势打分的小工具,先不创建,先收敛第一版。",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-root-3",
)
second = await executor._execute_query_intent(
intent=fragmented_followup_intent,
message_text="技术指标吧",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-reply-3a",
)
third = await executor._execute_query_intent(
intent=fragmented_followup_intent,
message_text="主要给投顾看",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-reply-3b",
)
fourth = await executor._execute_query_intent(
intent=fragmented_followup_intent,
message_text="飞书日报就行",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-reply-3c",
)
fifth = await executor._execute_query_intent(
intent=fragmented_followup_intent,
message_text="每天收盘后发",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-reply-3d",
)
final_result = await executor._execute_query_intent(
intent=fragmented_followup_intent,
message_text="第一版先保留分数、方向判断和一句解释就够了",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-reply-3e",
)
assert "根据什么来打" in str(first["answer"] or "")
assert "给谁看" in str(second["answer"] or "")
assert str(second["rag_result"]["query_type"]) == "plan"
assert "怎么接收" in str(third["answer"] or "")
assert str(third["rag_result"]["query_type"]) == "plan"
assert "什么时候更新" in str(fourth["answer"] or "")
assert str(fourth["rag_result"]["query_type"]) == "plan"
assert "先保留哪些输出" in str(fifth["answer"] or "")
assert str(fifth["rag_result"]["query_type"]) == "plan"
final_answer = str(final_result["answer"] or "")
assert "需求草稿" in final_answer
assert "【需求名称】" in final_answer
assert "【第一版范围】" in final_answer
assert str(final_result["rag_result"]["query_type"]) == "draft"
draft = (
await requirement_plan_db_session.execute(
select(MountedDraftWorkspace).where(MountedDraftWorkspace.tenant_id == 3)
)
).scalar_one()
card = draft.requirement_card or {}
assert card["scoring_basis"]
assert "投顾" in str(card["target_user"] or "")
assert "飞书日报" in str(card["output_form"] or "")
assert "收盘后" in str(card["cadence"] or "")
assert "方向判断" in str(card["first_scope"] or "")
@pytest.mark.asyncio
async def test_execute_query_intent_requirement_plan_generalizes_for_meeting_minutes_tool(
monkeypatch,
requirement_plan_db_session,
) -> None:
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def answer_question(self, **kwargs):
_ = kwargs
raise AssertionError("generic requirement plan should not enter heavy RAG path")
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = requirement_plan_db_session
self.tenant_id = 3
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
root_intent = SimpleNamespace(
project_id=None,
metadata={
"source_chat_id": "chat-plan-meeting",
"source_sender_open_id": "ou-plan-meeting",
"semantic_bundle": {
"diagnostics": {
"message_intent": "requirement_plan",
"consultant_mode_preference": "clarify",
}
},
},
)
followup_intent = SimpleNamespace(
project_id=None,
metadata={
"source_chat_id": "chat-plan-meeting",
"source_sender_open_id": "ou-plan-meeting",
"thread_context": {
"root_message_id": "msg-plan-meeting-root",
"reply_to_message_id": "msg-plan-meeting-root",
},
},
)
first = await executor._execute_query_intent(
intent=root_intent,
message_text="想跟你聊个需求,我们开完会以后待办经常散掉,想做个小工具自动从会议纪要里提待办,先别创建,先收第一版。",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-meeting-root",
)
second = await executor._execute_query_intent(
intent=followup_intent,
message_text="先给内部 PM 和研发负责人用。",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-meeting-2",
)
third = await executor._execute_query_intent(
intent=followup_intent,
message_text="飞书总结就行,不用单独页面。",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-meeting-3",
)
fourth = await executor._execute_query_intent(
intent=followup_intent,
message_text="每次会后半小时内整理出来。",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-meeting-4",
)
final_result = await executor._execute_query_intent(
intent=followup_intent,
message_text="第一版先抽待办、责任人和截止时间,不做复杂优先级。#meeting-plan-debug-tag",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-plan-meeting-5",
)
assert "主要给谁用" in str(first["answer"] or "") or "主要给谁看" in str(first["answer"] or "")
assert "怎么给结果" in str(second["answer"] or "") or "怎么接收" in str(second["answer"] or "")
assert "多久整理出来" in str(third["answer"] or "") or "时效" in str(third["answer"] or "")
assert "先保留哪些字段" in str(fourth["answer"] or "") or "第一版先保留哪些字段" in str(fourth["answer"] or "")
final_answer = str(final_result["answer"] or "")
assert "需求草稿" in final_answer
assert "【输入材料】会议纪要" in final_answer
assert "【目标用户】内部 PM 和研发负责人" in final_answer
assert "【输出形式】飞书总结" in final_answer
assert "【响应时效】每次会后半小时内" in final_answer
assert "【第一版范围】第一版先抽待办、责任人和截止时间,不做复杂优先级。" in final_answer
assert "#meeting-plan-debug-tag" not in final_answer
assert str(final_result["rag_result"]["query_type"]) == "draft"
draft = (
await requirement_plan_db_session.execute(
select(MountedDraftWorkspace).where(MountedDraftWorkspace.tenant_id == 3)
)
).scalar_one()
card = draft.requirement_card or {}
assert card["source_material"] == "会议纪要"
assert "内部 PM 和研发负责人" in str(card["target_user"] or "")
assert card["output_form"] == "飞书总结"
assert card["cadence"] == "每次会后半小时内"
assert "待办" in str(card["first_scope"] or "")
@pytest.mark.asyncio
async def test_execute_query_intent_short_circuits_requirement_advise_without_rag(monkeypatch) -> None:
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def answer_question(self, **kwargs):
_ = kwargs
raise AssertionError("requirement advice should not enter heavy RAG path")
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 3
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
intent = SimpleNamespace(
project_id=None,
metadata={
"semantic_bundle": {
"diagnostics": {
"message_intent": "business_review",
"consultant_mode_preference": "advise",
}
},
},
)
result = await executor._execute_query_intent(
intent=intent,
message_text="如果这个政策雷达项目先做 MVP,你觉得范围怎么收才比较稳?",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-chat-3",
)
assert result["status"] == "succeeded"
assert "只保留一个核心用户对象" in str(result["answer"] or "")
assert str(result["rag_result"]["query_type"]) == "advice"
@pytest.mark.asyncio
async def test_execute_query_intent_short_circuits_requirement_advise_by_text_without_metadata(monkeypatch) -> None:
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def answer_question(self, **kwargs):
_ = kwargs
raise AssertionError("scope advice should not enter heavy RAG path")
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 3
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
intent = SimpleNamespace(project_id=None, metadata={})
result = await executor._execute_query_intent(
intent=intent,
message_text="如果第一版先做 MVP,这个需求范围怎么收才比较稳?",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-chat-3b",
)
assert result["status"] == "succeeded"
assert "只保留一个核心用户对象" in str(result["answer"] or "")
assert str(result["rag_result"]["query_type"]) == "advice"
@pytest.mark.asyncio
async def test_execute_query_intent_short_circuits_assignee_recommendation_without_rag(monkeypatch) -> None:
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def answer_question(self, **kwargs):
_ = kwargs
raise AssertionError("assignee recommendation should not enter heavy RAG path")
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 3
async def _build_assignee_recommendation_answer(self, *, message_text: str, project_id):
_ = (message_text, project_id)
return "如果现在先给一个保守负责人,我更倾向 AI工程师。"
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
intent = SimpleNamespace(project_id=None, metadata={})
result = await executor._execute_query_intent(
intent=intent,
message_text="那你觉得分配给谁做比较合适?",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-chat-3c",
)
assert result["status"] == "succeeded"
assert "AI工程师" in str(result["answer"] or "")
assert str(result["rag_result"]["query_type"]) == "recommendation"
@pytest.mark.asyncio
async def test_execute_query_intent_short_circuits_project_existence_lookup_without_rag(monkeypatch) -> None:
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def _try_fast_project_existence_answer(self, *, clean_q: str, project_id):
_ = (clean_q, project_id)
return {
"answer": "有,当前匹配到 1 个相关项目:\n#101 AI客服(状态:active)",
"sources": [{"source": "db", "label": "Project database", "score": 0.99, "metadata": {"table": "projects"}}],
"db_query_ms": 8,
}
async def answer_question(self, **kwargs):
_ = kwargs
raise AssertionError("project existence lookup should not enter heavy RAG path")
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 3
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
intent = SimpleNamespace(project_id=None, metadata={})
result = await executor._execute_query_intent(
intent=intent,
message_text="一个 AI 客服工具,这个项目有吗",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-chat-project-exists-1",
)
assert result["status"] == "succeeded"
assert "当前匹配到 1 个相关项目" in str(result["answer"] or "")
assert str(result["rag_result"]["query_type"]) == "status"
@pytest.mark.asyncio
async def test_draft_confirmation_route_does_not_hijack_explicit_signal_source_request() -> None:
class FakeSession:
last_intent = mixin_entry.STATE_DRAFT_PENDING_CONFIRM
should_route = mixin_entry.KernelIngressFlowEntryMixin._should_route_to_draft_confirmation(
session=FakeSession(),
message_text="给项目ID: 2 接一个 YouTube 录屏加评论回放的信号源,先不要执行,先出草稿。",
ingress_options={},
)
assert should_route is False
def test_mounted_runtime_agent_name_uses_plan_coach_for_scope_advise() -> None:
agent_name = mixin_entry.KernelIngressFlowEntryMixin._resolve_mounted_runtime_agent_name(
workflow_route="query.answer",
auto_execute=False,
route_metadata={"consultant_mode_preference": "advise", "advise_subtype": "scope"},
reply_template_key=None,
mode_hint="plan",
)
assert agent_name == "PlanCoach"
@pytest.mark.asyncio
async def test_execute_project_update_intent_resolves_string_project_id_without_name_error() -> None:
class FakeDB:
def __init__(self) -> None:
self.commits = 0
self.refreshed: list[object] = []
async def commit(self) -> None:
self.commits += 1
async def refresh(self, row) -> None:
self.refreshed.append(row)
class FakeExecutor(mixin_execute_mutations.ExecuteMutationsMixin):
def __init__(self) -> None:
self.db = FakeDB()
self.tenant_id = 1
async def _resolve_project(self, *, project_id, project_title, project_name):
_ = (project_title, project_name)
return (
SimpleNamespace(
id=project_id,
title="旧标题",
name="旧标题",
description=None,
status="active",
domain_id=None,
assigned_role=None,
owner_id=None,
signal_count=0,
created_at=None,
updated_at=None,
),
[],
)
async def _log_operation(self, **kwargs):
_ = kwargs
async def _invalidate_project_cache(self, *, project_id: int) -> None:
_ = project_id
executor = FakeExecutor()
intent = SimpleNamespace(name="project.update", project_id=None, metadata={})
result = await executor._execute_project_update_intent(
intent=intent,
params={"project_id": "104", "title": "新标题"},
operator_user_id=9,
)
assert result["status"] == "succeeded"
assert result["project"]["id"] == 104
assert result["project"]["title"] == "新标题"
assert executor.db.commits == 1
def test_deterministic_query_type_treats_project_risk_question_as_status() -> None:
class FakeRAG(mixin_deterministic_query_core.AdaptiveRAGDeterministicQueryCoreMixin):
pass
rag = FakeRAG()
assert rag._classify_query_type("帮我看看跨境直播项目最近有哪些风险和阻塞") == "status"
@pytest.mark.asyncio
async def test_fast_project_progress_answer_handles_risk_blocker_questions() -> None:
project = SimpleNamespace(id=104, title="跨境直播项目", name="跨境直播项目", status="active")
tasks = [
SimpleNamespace(status="todo", updated_at=datetime(2026, 4, 5, 10, 0), created_at=datetime(2026, 4, 5, 9, 0)),
SimpleNamespace(status="in_progress", updated_at=datetime(2026, 4, 5, 11, 0), created_at=datetime(2026, 4, 5, 8, 0)),
SimpleNamespace(status="done", updated_at=datetime(2026, 4, 5, 7, 0), created_at=datetime(2026, 4, 5, 6, 0)),
]
class FakeScalarResult:
def __init__(self, rows):
self._rows = rows
def scalars(self):
return SimpleNamespace(all=lambda: list(self._rows))
class FakeDB:
async def execute(self, stmt):
_ = stmt
return FakeScalarResult(tasks)
class FakeRAG(mixin_fast_answer_project_main.AdaptiveRAGFastAnswerProjectMainMixin):
def __init__(self) -> None:
self.db = FakeDB()
self.tenant_id = 1
@staticmethod
def _extract_project_lookup_keyword(question: str) -> str:
_ = question
return "跨境直播"
@staticmethod
def _extract_query_terms(question: str, preferred: str):
_ = question
return [preferred]
async def _search_projects_by_keyword(self, *, keyword: str, project_id, limit: int):
_ = (keyword, project_id, limit)
return [project]
rag = FakeRAG()
result = await rag._try_fast_project_progress_answer(
clean_q="帮我看看跨境直播项目最近有哪些风险和阻塞",
project_id=None,
)
assert result is not None
assert "风险/阻塞台账字段" in str(result["answer"] or "")
assert "待处理任务数 1" in str(result["answer"] or "")
@pytest.mark.asyncio
async def test_fast_project_progress_answer_handles_risk_blocker_questions_without_tasks() -> None:
project = SimpleNamespace(id=101, title="AI客服", name="AI客服", status="active")
class FakeScalarResult:
def __init__(self, rows):
self._rows = rows
def scalars(self):
return SimpleNamespace(all=lambda: list(self._rows))
class FakeDB:
async def execute(self, stmt):
_ = stmt
return FakeScalarResult([])
class FakeRAG(mixin_fast_answer_project_main.AdaptiveRAGFastAnswerProjectMainMixin):
def __init__(self) -> None:
self.db = FakeDB()
self.tenant_id = 1
@staticmethod
def _extract_project_lookup_keyword(question: str) -> str:
_ = question
return "AI客服"
@staticmethod
def _extract_query_terms(question: str, preferred: str):
_ = question
return [preferred]
async def _search_projects_by_keyword(self, *, keyword: str, project_id, limit: int):
_ = (keyword, project_id, limit)
return [project]
rag = FakeRAG()
result = await rag._try_fast_project_progress_answer(
clean_q="帮我看看 AI 客服工具项目最近有哪些风险和阻塞",
project_id=None,
)
assert result is not None
assert "风险/阻塞台账字段" in str(result["answer"] or "")
assert "没有可用的任务拆解记录" in str(result["answer"] or "")
def test_typed_executor_common_exports_regex_helpers_to_query_mixins() -> None:
assert mixin_execute_query_project.ExecuteQueryProjectMixin._extract_task_priority("p0 紧急任务") == "high"
assert (
ai_pmo_typed_executor_mixins_parse.TypedExecutorParseMixin._extract_requested_signal_count(
message_text="请帮我分析 3 个信号",
metadata={},
)
== 3
)
assert (
ai_pmo_typed_executor_mixins_parse.TypedExecutorParseMixin._requires_strict_signal_analysis(
message_text="先分析后入库 2 个信号",
requested_count=2,
)
is True
)
def test_consultant_dispatch_uses_short_next_action_for_query_answers() -> None:
dispatch = build_consultant_dispatch_contract(
intent_name="query.answer",
message_text="PMFlow 现在完整调用链是什么?",
source_system="pmflow_manual",
status="answered",
classification="question",
primary_operation_code="query.answer",
risk_level="low",
missing_fields=[],
direct_reply_text=(
"结论:PMFlow 当前完整调用链无法完整还原。\n\n"
"依据:前端 `/ai-steward` 页面通过 `/rag/ask` 发起请求,"
"并继续进入 `AdaptiveRAGService.upsert_pmflow_record_async`。"
),
)
assert dispatch["allow_execute"] is False
assert dispatch["mode"] == "advise"
assert dispatch["next_action"] == "继续追问具体模块、入口函数或文件路径,我会把调用链收窄到那一段。"
assert "结论:" not in dispatch["next_action"]
assert "/rag/ask" not in dispatch["next_action"]
def test_build_capture_result_no_longer_exposes_consultant_dispatch() -> None:
class _FakeKernel(ai_pmo_kernel_mixins_ingress_render.KernelIngressRenderMixin):
tenant_id = 1
work_item = SimpleNamespace(
id=18,
project_id=7,
signal_id=None,
state="succeeded",
intent="query.answer",
reply_template_key="action_succeeded",
decision_level="team",
review_summary={"verified": True},
raw_message="现在进展怎么样?",
source_system="pmflow_manual",
draft_version=0,
)
intent = CanonicalIntent(
name="query.answer",
confidence=0.93,
project_id=7,
signal_title="",
signal_summary="当前主链正常。",
metadata={},
)
result = _FakeKernel()._build_capture_result(
work_item=work_item,
intent=intent,
execution_result={
"workflow_route": "query.answer",
"rag_result": {"answer": "当前主链正常。"},
},
render={
"template_key": "action_succeeded",
"text": "当前主链正常。",
},
direct_reply_required=True,
status="answered",
classification="question",
)
assert "consultant_dispatch" not in result
assert "consultant_dispatch" not in result["rag_result"]
@pytest.mark.asyncio
async def test_low_tier_rag_failure_retries_high_tier(monkeypatch) -> None:
captured: dict[str, object] = {"logs": []}
monkeypatch.setattr(
mixin_tier_escalation,
"_runtime_log",
lambda scope, message: captured["logs"].append((scope, message)),
)
low_engine = {
"id": "low-1",
"provider": "glm",
"model": "qwen-plus",
"base_url": "https://open.bigmodel.cn/api/paas/v4",
"api_key": "low-key",
}
high_engine = {
"id": "high-1",
"provider": "openai",
"model": "GLM-5",
"base_url": "https://api.openai.com/v1",
"api_key": "high-key",
}
bundle = {
"settings": {},
"active_engine_id": "low-1",
"engines": [low_engine, high_engine],
}
openclaw_bundle = {
"rules": [
{
"name": "engine_high_reasoning_glm5",
"enabled": True,
"priority": 220,
"tier": "high",
"provider": "openai",
"model": "GLM-5",
"api_env": "OPENAI_API_KEY",
"match_any": ["adaptive_rag_answer"],
}
]
}
class FakeLLM(LLMTierEscalationMixin):
def _low_to_high_escalation_enabled(self) -> bool:
return True
def _is_low_tier_function(self, function_name: str) -> bool:
return function_name == "adaptive_rag_answer_normal"
@staticmethod
def _rule_matches_function(rule, function_name: str) -> bool:
fn = str(function_name or "").strip().lower()
if not fn:
return False
if bool(rule.get("match_all", False)):
return True
exact = str(rule.get("function_exact") or "").strip().lower()
if exact and fn == exact:
return True
prefix = str(rule.get("function_prefix") or "").strip().lower()
if prefix and fn.startswith(prefix):
return True
match_any = rule.get("match_any")
if isinstance(match_any, list):
for hint in match_any:
token = str(hint or "").strip().lower()
if token and token in fn:
return True
return False
async def _get_or_init_bundle(self, db, *, tenant_id: int):
_ = (db, tenant_id)
return SimpleNamespace(), bundle
def _resolve_active_engine_from_bundle(self, bundle_value):
_ = bundle_value
return dict(low_engine)
def _load_openclaw_routing_bundle(self):
return dict(openclaw_bundle)
def _resolve_engine_from_rule(self, *, rule, engines, active_engine):
_ = active_engine
for item in engines:
if (
str(item.get("provider") or "").strip().lower() == str(rule.get("provider") or "").strip().lower()
and str(item.get("model") or "").strip() == str(rule.get("model") or "").strip()
):
return dict(item)
return None
def _ensure_openclaw_control_plane_engine(self, engine):
return dict(engine)
def _normalize_engine_settings(self, raw):
_ = raw
return {}
def _ensure_engine_allowed_for_function(self, *, engine, function_name, settings, engine_label):
_ = (function_name, settings, engine_label)
return dict(engine)
def _is_invalid_structured_output(self, *, function_name, response_text):
_ = (function_name, response_text)
return False
async def _chat_completion(self, **kwargs):
captured["chat_kwargs"] = kwargs
return "GLM-5 answer"
llm = FakeLLM()
text, resolved_engine = await llm._retry_with_high_tier_after_low_model_failure(
db=object(),
tenant_id=1,
function_name="adaptive_rag_answer_normal",
failed_engine=dict(low_engine),
prompt="请回答一个问题",
system_prompt="system prompt",
temperature=0.2,
max_tokens=512,
response_mode="complete",
error=ValueError("LLM 返回空内容"),
)
assert text == "GLM-5 answer"
assert isinstance(resolved_engine, dict)
resolved_meta = resolved_engine["_routing_meta"]
assert resolved_meta["planned_tier"] == "low"
assert resolved_meta["executed_tier"] == "high"
assert resolved_meta["routing_decision"] == "low_to_high_escalated"
assert captured["chat_kwargs"]["provider"] == "openai"
assert captured["chat_kwargs"]["model"] == "GLM-5"
log_text = "\n".join(message for _, message in captured["logs"])
assert "高阶升级成功" in log_text
@pytest.mark.asyncio
async def test_low_tier_intent_failure_retries_high_tier(monkeypatch) -> None:
captured: dict[str, object] = {"logs": []}
monkeypatch.setattr(
mixin_tier_escalation,
"_runtime_log",
lambda scope, message: captured["logs"].append((scope, message)),
)
low_engine = {
"id": "low-1",
"provider": "glm",
"model": "qwen-plus",
"base_url": "https://open.bigmodel.cn/api/paas/v4",
"api_key": "low-key",
}
alternate_high_engine = {
"id": "high-deepseek-1",
"provider": "deepseek",
"model": "deepseek-chat",
"base_url": "https://api.deepseek.com/v1",
"api_key": "deepseek-key",
}
glm5_engine = {
"id": "high-glm5-1",
"provider": "openai",
"model": "GLM-5",
"base_url": "https://api.openai.com/v1",
"api_key": "glm5-key",
}
bundle = {
"settings": {},
"active_engine_id": "low-1",
"engines": [low_engine, alternate_high_engine, glm5_engine],
}
openclaw_bundle = {
"rules": [
{
"name": "engine_low_intent_kernel_exact",
"enabled": True,
"priority": 310,
"tier": "low",
"provider": "glm",
"model": "qwen-plus",
"api_env": "DASHSCOPE_API_KEY",
"function_exact": "classify_intent_ai_pmo_kernel",
}
]
}
class FakeLLM(LLMTierEscalationMixin):
def _low_to_high_escalation_enabled(self) -> bool:
return True
def _is_low_tier_function(self, function_name: str) -> bool:
return function_name == "classify_intent_ai_pmo_kernel"
@staticmethod
def _rule_matches_function(rule, function_name: str) -> bool:
fn = str(function_name or "").strip().lower()
if not fn:
return False
if bool(rule.get("match_all", False)):
return True
exact = str(rule.get("function_exact") or "").strip().lower()
if exact and fn == exact:
return True
prefix = str(rule.get("function_prefix") or "").strip().lower()
if prefix and fn.startswith(prefix):
return True
match_any = rule.get("match_any")
if isinstance(match_any, list):
for hint in match_any:
token = str(hint or "").strip().lower()
if token and token in fn:
return True
return False
async def _get_or_init_bundle(self, db, *, tenant_id: int):
_ = (db, tenant_id)
return SimpleNamespace(), bundle
def _resolve_active_engine_from_bundle(self, bundle_value):
_ = bundle_value
return dict(low_engine)
def _load_openclaw_routing_bundle(self):
return dict(openclaw_bundle)
def _resolve_engine_from_rule(self, *, rule, engines, active_engine):
_ = active_engine
for item in engines:
if (
str(item.get("provider") or "").strip().lower() == str(rule.get("provider") or "").strip().lower()
and str(item.get("model") or "").strip() == str(rule.get("model") or "").strip()
):
return dict(item)
return None
def _resolve_tier_engine(self, *, tier, engines, active_engine):
_ = (engines, active_engine)
if str(tier).strip().lower() == "high":
return dict(alternate_high_engine)
return dict(low_engine)
def _ensure_openclaw_control_plane_engine(self, engine):
return dict(engine)
def _normalize_engine_settings(self, raw):
_ = raw
return {}
def _ensure_engine_allowed_for_function(self, *, engine, function_name, settings, engine_label):
_ = (function_name, settings, engine_label)
return dict(engine)
def _is_invalid_structured_output(self, *, function_name, response_text):
_ = (function_name, response_text)
return False
async def _chat_completion(self, **kwargs):
captured["chat_kwargs"] = kwargs
return "GLM-5 answer"
llm = FakeLLM()
text, resolved_engine = await llm._retry_with_high_tier_after_low_model_failure(
db=object(),
tenant_id=1,
function_name="classify_intent_ai_pmo_kernel",
failed_engine=dict(low_engine),
prompt="请返回结构化意图",
system_prompt="system prompt",
temperature=0.2,
max_tokens=512,
response_mode="complete",
error=ValueError("LLM 返回空内容"),
)
assert text == "GLM-5 answer"
assert isinstance(resolved_engine, dict)
resolved_meta = resolved_engine["_routing_meta"]
assert resolved_meta["planned_tier"] == "low"
assert resolved_meta["executed_tier"] == "high"
assert resolved_meta["routing_decision"] == "low_to_high_escalated"
assert captured["chat_kwargs"]["provider"] == "openai"
assert captured["chat_kwargs"]["model"] == "GLM-5"
log_text = "\n".join(message for _, message in captured["logs"])
assert "高阶升级成功" in log_text
@pytest.mark.asyncio
async def test_low_tier_structured_failure_retries_high_tier(monkeypatch) -> None:
captured: dict[str, object] = {"logs": []}
monkeypatch.setattr(
mixin_tier_escalation,
"_runtime_log",
lambda scope, message: captured["logs"].append((scope, message)),
)
low_engine = {
"id": "low-1",
"provider": "glm",
"model": "qwen-plus",
"base_url": "https://open.bigmodel.cn/api/paas/v4",
"api_key": "low-key",
}
alternate_high_engine = {
"id": "high-deepseek-1",
"provider": "deepseek",
"model": "deepseek-chat",
"base_url": "https://api.deepseek.com/v1",
"api_key": "deepseek-key",
}
glm5_engine = {
"id": "high-glm5-1",
"provider": "openai",
"model": "GLM-5",
"base_url": "https://api.openai.com/v1",
"api_key": "glm5-key",
}
bundle = {
"settings": {},
"active_engine_id": "low-1",
"engines": [low_engine, alternate_high_engine, glm5_engine],
}
openclaw_bundle = {
"rules": [
{
"name": "engine_low_intent_kernel_exact",
"enabled": True,
"priority": 310,
"tier": "low",
"provider": "glm",
"model": "qwen-plus",
"api_env": "DASHSCOPE_API_KEY",
"function_exact": "classify_intent_ai_pmo_kernel",
}
]
}
class FakeLLM(LLMTierEscalationMixin):
def _low_to_high_escalation_enabled(self) -> bool:
return True
def _is_low_tier_function(self, function_name: str) -> bool:
return function_name == "classify_intent_ai_pmo_kernel"
@staticmethod
def _rule_matches_function(rule, function_name: str) -> bool:
fn = str(function_name or "").strip().lower()
if not fn:
return False
if bool(rule.get("match_all", False)):
return True
exact = str(rule.get("function_exact") or "").strip().lower()
if exact and fn == exact:
return True
prefix = str(rule.get("function_prefix") or "").strip().lower()
if prefix and fn.startswith(prefix):
return True
match_any = rule.get("match_any")
if isinstance(match_any, list):
for hint in match_any:
token = str(hint or "").strip().lower()
if token and token in fn:
return True
return False
async def _get_or_init_bundle(self, db, *, tenant_id: int):
_ = (db, tenant_id)
return SimpleNamespace(), bundle
def _resolve_active_engine_from_bundle(self, bundle_value):
_ = bundle_value
return dict(low_engine)
def _load_openclaw_routing_bundle(self):
return dict(openclaw_bundle)
def _resolve_engine_from_rule(self, *, rule, engines, active_engine):
_ = active_engine
for item in engines:
if (
str(item.get("provider") or "").strip().lower() == str(rule.get("provider") or "").strip().lower()
and str(item.get("model") or "").strip() == str(rule.get("model") or "").strip()
):
return dict(item)
return None
def _resolve_tier_engine(self, *, tier, engines, active_engine):
_ = (engines, active_engine)
if str(tier).strip().lower() == "high":
return dict(alternate_high_engine)
return dict(low_engine)
def _ensure_openclaw_control_plane_engine(self, engine):
return dict(engine)
def _normalize_engine_settings(self, raw):
_ = raw
return {}
def _ensure_engine_allowed_for_function(self, *, engine, function_name, settings, engine_label):
_ = (function_name, settings, engine_label)
return dict(engine)
def _is_invalid_structured_output(self, *, function_name, response_text):
_ = (function_name, response_text)
return False
def _extract_structured_object_from_response(
self,
*,
function_name,
response,
fallback_text,
provider,
model,
):
_ = (function_name, response, fallback_text)
return StructuredExtractionResult(
payload={"confidence": 0.91, "reasoning": "ok", "intent": "query.read_only"},
source_field="generated_text",
provider=provider,
model=model,
raw_text='{"confidence": 0.91, "reasoning": "ok", "intent": "query.read_only"}',
validation="ok",
)
async def _chat_completion_raw(self, **kwargs):
captured["chat_kwargs"] = kwargs
raw_text = '{"confidence": 0.91, "reasoning": "ok", "intent": "query.read_only"}'
return ChatCompletionResult(text=raw_text, raw_response=raw_text)
llm = FakeLLM()
result, resolved_engine = await llm._retry_structured_with_high_tier_after_low_model_failure(
db=object(),
tenant_id=1,
function_name="classify_intent_ai_pmo_kernel",
failed_engine=dict(low_engine),
prompt="请返回结构化意图",
system_prompt="system prompt",
temperature=0.2,
max_tokens=512,
response_mode="complete",
error=ValueError("LLM 返回空内容"),
)
assert result is not None
assert result.payload["intent"] == "query.read_only"
assert isinstance(resolved_engine, dict)
resolved_meta = resolved_engine["_routing_meta"]
assert resolved_meta["planned_tier"] == "low"
assert resolved_meta["executed_tier"] == "high"
assert resolved_meta["routing_decision"] == "low_to_high_escalated"
assert captured["chat_kwargs"]["provider"] == "openai"
assert captured["chat_kwargs"]["model"] == "GLM-5"
log_text = "\n".join(message for _, message in captured["logs"])
assert "高阶结构化升级成功" in log_text