import pytest from types import SimpleNamespace from app.config.ai_prompts import PROMPT_AI_PMO_QA from app.services.adaptive_rag_service.mixin_answer_entry import AdaptiveRAGAnswerEntryMixin from app.services.adaptive_rag_service.mixin_answer_mainline import AdaptiveRAGAnswerMainlineMixin from app.services.adaptive_rag_service.mixin_deep_guard_answer import AdaptiveRAGDeepGuardAnswerMixin from app.services.adaptive_rag_service.mixin_deep_guard_rules import AdaptiveRAGDeepGuardRulesMixin from app.services.adaptive_rag_service.mixin_deterministic_query_core import ( AdaptiveRAGDeterministicQueryCoreMixin, ) from app.services.adaptive_rag_service.mixin_retrieval_vector import AdaptiveRAGRetrievalVectorMixin from app.services.adaptive_rag_service.shared_part_02 import DEEP_RETRIEVAL_STEPS, DEEP_VECTOR_TOP_K class _FakeAdaptiveRAGService( AdaptiveRAGAnswerEntryMixin, AdaptiveRAGAnswerMainlineMixin, ): def __init__(self) -> None: self.db = object() self.tenant_id = 9 self.ack_calls = 0 self.base_calls = 0 self.base_kwargs: dict[str, object] = {} def _normalize_thinking_mode(self, thinking_mode: str) -> str: return "deep" if str(thinking_mode or "").strip().lower() == "deep" else "normal" async def _prepare_answer_runtime(self, **kwargs): clean_q = str(kwargs.get("clean_q") or "").strip() action_mode = str(kwargs.get("action_mode") or "").strip().lower() return { "session_id": 101, "turn_index": 2, "context_session_id": 101, "conversation": [], "state_row": None, "resolved_question": clean_q, "effective_q": clean_q, "rewrite_meta": {"mode": "direct"}, "query_type": "explain", "effective_action_mode": action_mode, "anchor_resolution": None, } async def _handle_ack_first_requirement(self, **kwargs): self.ack_calls += 1 _ = kwargs return {"answer": "ack-required", "processing_stage": "ack_only"} async def _execute_base_answer_mainline(self, **kwargs): self.base_calls += 1 self.base_kwargs = dict(kwargs) return {"answer": "base-answer", "processing_stage": "qa"} @pytest.mark.asyncio async def test_answer_entry_read_mainline_skips_requirement_ack() -> None: service = _FakeAdaptiveRAGService() result = await service.answer_question( question="现在有哪些阻塞?", asked_by=1, asked_via="pmflow_manual", action_mode="read", sync_requirement=True, ) assert result["processing_stage"] == "qa" assert service.ack_calls == 0 assert service.base_calls == 1 @pytest.mark.asyncio async def test_answer_entry_write_mode_uses_requirement_ack_gate() -> None: service = _FakeAdaptiveRAGService() result = await service.answer_question( question="请同步写入一条风险", asked_by=1, asked_via="pmflow_manual", action_mode="write", sync_requirement=True, ) assert result["processing_stage"] == "ack_only" assert service.ack_calls == 1 assert service.base_calls == 0 @pytest.mark.asyncio async def test_answer_entry_high_accuracy_channel_respects_requested_normal_mode() -> None: service = _FakeAdaptiveRAGService() result = await service.answer_question( question="最近有哪些项目?", asked_by=1, asked_via="pmflow", thinking_mode="normal", sync_requirement=True, ) assert result["processing_stage"] == "qa" assert service.ack_calls == 0 assert service.base_calls == 1 assert service.base_kwargs["active_thinking_mode"] == "normal" assert service.base_kwargs["deep_thinking"] is False assert int(service.base_kwargs["deep_retrieval_steps"]) == DEEP_RETRIEVAL_STEPS assert int(service.base_kwargs["deep_vector_top_k"]) == DEEP_VECTOR_TOP_K @pytest.mark.asyncio async def test_answer_entry_forwards_consultant_mode_preference_to_mainline() -> None: service = _FakeAdaptiveRAGService() result = await service.answer_question( question="这个方案怎么做更稳妥?", asked_by=1, asked_via="pmflow", thinking_mode="normal", consultant_mode_preference="advise", ) assert result["processing_stage"] == "qa" assert service.base_calls == 1 assert service.base_kwargs["consultant_mode_preference"] == "advise" @pytest.mark.asyncio async def test_answer_entry_mixed_request_skips_requirement_ack_gate() -> None: service = _FakeAdaptiveRAGService() result = await service.answer_question( question="现在进展如何,并且新增任务确认流", asked_by=1, asked_via="pmflow_manual", action_mode="read_write", sync_requirement=True, message_intent="mixed", ) assert result["processing_stage"] == "qa" assert service.ack_calls == 0 assert service.base_calls == 1 assert service.base_kwargs["active_thinking_mode"] == "normal" assert service.base_kwargs["deep_thinking"] is False @pytest.mark.asyncio async def test_answer_entry_feishu_write_mode_still_goes_base_mainline() -> None: service = _FakeAdaptiveRAGService() result = await service.answer_question( question="请在飞书里补一条风险", asked_by=1, asked_via="feishu", action_mode="write", sync_requirement=True, ) assert result["processing_stage"] == "qa" assert service.ack_calls == 0 assert service.base_calls == 1 assert service.base_kwargs["active_thinking_mode"] == "normal" assert service.base_kwargs["deep_thinking"] is False assert int(service.base_kwargs["deep_retrieval_steps"]) == DEEP_RETRIEVAL_STEPS assert int(service.base_kwargs["deep_vector_top_k"]) == DEEP_VECTOR_TOP_K def test_query_runtime_planner_prefers_fast_path_for_list_question_even_if_requested_deep() -> None: class _FakePlannerService(AdaptiveRAGAnswerMainlineMixin): pass service = _FakePlannerService() decision = service._plan_query_runtime( question="现在有哪些项目?", effective_question="现在有哪些项目?", query_type="list", requested_thinking_mode="deep", enforce_scene_prompt_only=False, ) assert decision.answer_mode == "fast_path" assert decision.thinking_mode == "normal" assert decision.allow_fast_path is True def test_query_runtime_planner_bypasses_fast_path_when_consultant_mode_preference_present() -> None: class _FakePlannerService(AdaptiveRAGAnswerMainlineMixin): pass service = _FakePlannerService() decision = service._plan_query_runtime( question="现在有哪些项目?", effective_question="现在有哪些项目?", query_type="list", requested_thinking_mode="normal", enforce_scene_prompt_only=False, consultant_mode_preference="advise", ) assert decision.answer_mode == "retrieval_normal" assert decision.thinking_mode == "normal" assert decision.allow_fast_path is False @pytest.mark.asyncio async def test_deterministic_query_prefers_project_created_at_answer() -> None: calls: list[str] = [] class _FakeDeterministicService(AdaptiveRAGDeterministicQueryCoreMixin): def __init__(self) -> None: self.db = object() self.tenant_id = 1 async def _try_fast_project_created_at_answer(self, *, clean_q: str, project_id): calls.append("created_at") _ = (clean_q, project_id) return {"answer": "创建于 2026-03-01 09:30。", "sources": [], "missing_fields": []} async def _try_fast_project_progress_answer(self, *, clean_q: str, project_id): calls.append("progress") _ = (clean_q, project_id) return {"answer": "进度回答", "sources": []} async def _try_fast_project_existence_answer(self, *, clean_q: str, project_id): calls.append("existence") _ = (clean_q, project_id) return {"answer": "存在性回答", "sources": []} service = _FakeDeterministicService() result = await service._run_deterministic_query( question="他是什么时候创建的?", project_id=7, query_type="status", ) assert result is not None assert "创建于 2026-03-01 09:30" in str(result["answer"]) assert calls[0] == "created_at" def test_query_type_classifies_colloquial_list_request_as_list() -> None: service = AdaptiveRAGDeterministicQueryCoreMixin() assert service._classify_query_type("我感觉很多重复的项目在系统,你能列一下吗?") == "list" @pytest.mark.asyncio async def test_vector_retrieval_skips_legacy_fallback_for_non_postgresql_session() -> None: class _FakeLLM: async def get_embedding_async(self, question: str, **kwargs): _ = (question, kwargs) return [0.1, 0.2, 0.3] class _FakeService(AdaptiveRAGRetrievalVectorMixin): def __init__(self) -> None: self.db = object() self.tenant_id = 1 self.llm = _FakeLLM() self.legacy_called = False def _map_domains_to_source_types(self, domains: list[str]) -> list[str]: return list(domains) def _is_postgresql_session(self) -> bool: return False async def _retrieve_vector_context_legacy(self, **kwargs): _ = kwargs self.legacy_called = True return ["legacy-hit"] service = _FakeService() result = await service._retrieve_vector_context( "查询项目文档", domains=["doc"], project_id=None, source_chat_id=None, ) assert result == [] assert service.legacy_called is False @pytest.mark.asyncio async def test_vector_retrieval_skips_legacy_fallback_when_pg_query_errors() -> None: class _FakeLLM: async def get_embedding_async(self, question: str, **kwargs): _ = (question, kwargs) return [0.1, 0.2, 0.3] class _FakeService(AdaptiveRAGRetrievalVectorMixin): def __init__(self) -> None: self.db = object() self.tenant_id = 1 self.llm = _FakeLLM() self.legacy_called = False def _map_domains_to_source_types(self, domains: list[str]) -> list[str]: return list(domains) def _is_postgresql_session(self) -> bool: return True async def _retrieve_vector_context_pg(self, **kwargs): _ = kwargs return None async def _retrieve_vector_context_legacy(self, **kwargs): _ = kwargs self.legacy_called = True return ["legacy-hit"] service = _FakeService() result = await service._retrieve_vector_context( "查询项目文档", domains=["doc"], project_id=None, source_chat_id=None, ) assert result == [] assert service.legacy_called is False @pytest.mark.asyncio async def test_deep_guard_answer_raises_on_empty_llm_response() -> None: class _FakeLLM: async def generate(self, prompt: str, **kwargs): _ = (prompt, kwargs) return "" class _FakeService(AdaptiveRAGDeepGuardAnswerMixin): def __init__(self) -> None: self.db = object() self.tenant_id = 1 self.llm = _FakeLLM() def _normalize_thinking_mode(self, thinking_mode: str) -> str: return "normal" if str(thinking_mode or "").strip().lower() != "deep" else "deep" async def get_project_manifest(self, project_id): _ = project_id return {} async def _select_scene_prompt(self, **kwargs): _ = kwargs return "external_trend", "请回答问题" def _enforce_feature_evidence_in_answer(self, *, question: str, answer_text: str, retrieved, system_snapshot): _ = (question, retrieved, system_snapshot) return answer_text service = _FakeService() with pytest.raises(ValueError, match="adaptive_rag_answer_empty_response"): await service._generate_answer( question="最近AI行业有什么动态", retrieved=[], conversation=[], project_id=None, asked_via="feishu", ) @pytest.mark.asyncio async def test_deep_guard_answer_prefers_member_workload_direct_answer() -> None: class _FakeResult: def __init__(self, rows=None, scalar=None): self._rows = rows or [] self._scalar = scalar def all(self): return self._rows def first(self): return self._rows[0] if self._rows else None def scalar_one_or_none(self): return self._scalar class _FakeDB: def __init__(self) -> None: self.calls = 0 async def execute(self, stmt, params=None): # noqa: ANN001 self.calls += 1 if self.calls == 1: return _FakeResult(rows=[SimpleNamespace(id=24, name="杨宇", username="yangyu")]) return _FakeResult(rows=[(265.56, 95.6, 60.0)]) class _FakeLLM: async def generate(self, prompt: str, **kwargs): # pragma: no cover - should not be called raise AssertionError("LLM should not be called for direct member workload answer") class _FakeService(AdaptiveRAGDeepGuardAnswerMixin): def __init__(self) -> None: self.db = _FakeDB() self.tenant_id = 1 self.llm = _FakeLLM() def _normalize_thinking_mode(self, thinking_mode: str) -> str: return "normal" async def get_project_manifest(self, project_id): _ = project_id return {} def _select_scene_prompt(self, **kwargs): _ = kwargs return "member_workload", "member workload prompt" def _enforce_feature_evidence_in_answer(self, *, question: str, answer_text: str, retrieved, system_snapshot): _ = (question, retrieved, system_snapshot) return answer_text service = _FakeService() result = await service._generate_answer( question="杨宇现在负载情况如何?", retrieved=[], conversation=[], project_id=None, asked_via="feishu", ) assert "杨宇当前负载265.56%" in result assert "严重超载" in result @pytest.mark.asyncio async def test_select_scene_prompt_applies_consultant_mode_preference_wrapper() -> None: class _FakeService(AdaptiveRAGDeepGuardAnswerMixin): def __init__(self) -> None: self.db = object() self.tenant_id = 1 async def _render_scene_prompt(self, *, prompt_key: str, fallback_template: str, **kwargs): _ = (fallback_template, kwargs) return f"PROMPT::{prompt_key}" service = _FakeService() scene_key, prompt = await service._select_scene_prompt( question="这个方案怎么做更稳妥?", project_id=7, asked_via="pmflow", message_intent=None, mixed_signal_part=None, conversation_text="无", context_blocks_text="无", system_snapshot={}, user_snapshot={}, system_snapshot_text="{}", user_snapshot_text="{}", relevance_max=0.61, query_type="explain", consultant_mode_preference="advise", ) assert scene_key == "default_consultant_advise" assert "PROMPT::PROMPT_RAG_ANSWER" in prompt assert "稳妥建议" in prompt def test_ai_pmo_qa_prompt_includes_five_lenses_and_runtime_state_guard() -> None: assert "事实源在哪" in PROMPT_AI_PMO_QA assert "输出模板在哪" in PROMPT_AI_PMO_QA assert "检索范围在哪" in PROMPT_AI_PMO_QA assert "执行器在哪" in PROMPT_AI_PMO_QA assert "反馈回流在哪" in PROMPT_AI_PMO_QA assert "静态资料" in PROMPT_AI_PMO_QA assert "运行态配置" in PROMPT_AI_PMO_QA @pytest.mark.asyncio async def test_select_scene_prompt_routes_business_review_to_ai_pmo_prompt() -> None: class _FakeService(AdaptiveRAGDeepGuardAnswerMixin): def __init__(self) -> None: self.db = object() self.tenant_id = 1 async def _render_scene_prompt(self, *, prompt_key: str, fallback_template: str, **kwargs): _ = fallback_template return ( f"PROMPT::{prompt_key}|" f"business_review={kwargs.get('business_review_mode')}|" f"framework={kwargs.get('review_framework')}" ) service = _FakeService() scene_key, prompt = await service._select_scene_prompt( question="我要做一个大模型 API 聚合平台,帮我分析下要怎么做", project_id=None, asked_via="feishu", message_intent="business_review", mixed_signal_part=None, conversation_text="用户上一轮提到要做 API 聚合平台", context_blocks_text="无", system_snapshot={}, user_snapshot={}, system_snapshot_text="{}", user_snapshot_text="{}", relevance_max=0.51, query_type="explain", consultant_mode_preference="clarify", ) assert scene_key == "ai_pmo_business_review_consultant_clarify" assert "PROMPT::PROMPT_AI_PMO_QA" in prompt assert "business_review=true" in prompt assert "framework=five_lenses" in prompt def test_runtime_state_query_guard_blocks_static_catalog_evidence() -> None: class _FakeService(AdaptiveRAGDeepGuardRulesMixin): pass service = _FakeService() blocked = service._should_block_answer_with_guard( question="高级模型路由是否启用?", effective_question="高级模型路由是否启用?", sources=[ {"source": "feature_catalog", "label": "Feature Catalog", "metadata": {}}, {"source": "graph", "label": "Knowledge Graph", "metadata": {"path": "1->uses->2"}}, ], relevance_max=0.96, ) assert blocked is True assert "无法仅凭静态资料确认" in service._build_evidence_guard_answer("高级模型路由是否启用?") def test_runtime_state_query_guard_allows_routing_config_evidence() -> None: class _FakeService(AdaptiveRAGDeepGuardRulesMixin): pass service = _FakeService() blocked = service._should_block_answer_with_guard( question="高级模型路由是否启用?", effective_question="高级模型路由是否启用?", sources=[ { "source": "doc", "label": "Routing Config", "metadata": {"source_id": "backend/.openclaw/pmflow-routing-rules.json"}, } ], relevance_max=0.74, ) assert blocked is False def test_correction_escalation_query_forces_deep_reasoning_in_feishu_channel() -> None: class _FakeService(AdaptiveRAGDeepGuardRulesMixin): pass service = _FakeService() reason = service._forced_deep_reason( question="不对,调用高级模型来回答我的问题", effective_question="不对,调用高级模型来回答我的问题", conversation=[{"question": "我要做一个大模型 API 聚合平台", "answer": "上一轮回答"}], ) assert reason == "correction_escalation" assert service._should_force_deep_for_channel( asked_via="feishu", question="不对,调用高级模型来回答我的问题", effective_question="不对,调用高级模型来回答我的问题", reason=reason, ) is True