import pytest from types import SimpleNamespace 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_deterministic_query_core import ( AdaptiveRAGDeterministicQueryCoreMixin, ) 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, "effective_q": clean_q, "rewrite_meta": {"mode": "direct"}, "query_type": "explain", "effective_action_mode": action_mode, } 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_forces_deep_mode() -> None: service = _FakeAdaptiveRAGService() result = await service.answer_question( question="最近有哪些项目?", asked_by=1, asked_via="pmflow", 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"] == "deep" assert service.base_kwargs["deep_thinking"] is True 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_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"] == "deep" assert service.base_kwargs["deep_thinking"] is True @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"] == "deep" assert service.base_kwargs["deep_thinking"] is True 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_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_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 {} 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