from datetime import datetime, timedelta from types import SimpleNamespace import pytest from fastapi import HTTPException 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_intent import CanonicalIntent from app.services.ai_pmo.draft_confirm_handler import DraftConfirmHandler 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.utils.thread_context import build_feishu_conversation_scope_key, build_feishu_thread_key @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 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", "finalize"] begin_kwargs = captured["calls"][0][1] record_kwargs = captured["calls"][1][1] finalize_kwargs = captured["calls"][2][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 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_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" @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_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 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 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