"""Adaptive RAG mixin module.""" from __future__ import annotations from .shared import * class AdaptiveRAGAnswerRetrievalMixin: async def _build_retrieval_response( self, *, clean_q: str, effective_q: str, asked_by: Optional[int], asked_via: str, project_id: Optional[int], source_chat_id: Optional[str], session_id: Optional[int], turn_index: int, context_session_id: Optional[int], conversation: list[dict[str, Any]], state_row: Optional[QASessionState], rewrite_meta: dict[str, Any], query_type: str, thinking_mode: str, active_thinking_mode: str, deep_thinking: bool, forced_deep_reason: str, deep_retrieval_steps: int, deep_vector_top_k: int, system_snapshot: dict[str, Any], user_snapshot: Optional[dict[str, Any]], started: float, message_intent: Optional[str] = None, mixed_signal_part: Optional[str] = None, answer_mode: str = ANSWER_MODE_RETRIEVAL_NORMAL, consultant_mode_preference: Optional[str] = None, ) -> dict[str, Any]: domains = self._route_domains(effective_q) if str(asked_via or "").strip().lower() == "feishu" and source_chat_id and "feishu" not in domains: domains.append("feishu") retrieved, search_meta = await self._multi_step_search( question=effective_q, domains=domains, project_id=project_id, source_chat_id=source_chat_id, session_state=state_row, max_steps_override=deep_retrieval_steps if deep_thinking else None, vector_top_k=deep_vector_top_k if deep_thinking else None, ) db_query_ms = int(search_meta.get("db_query_ms") or 0) vector_search_ms = int(search_meta.get("vector_search_ms") or 0) graph_query_ms = int(search_meta.get("graph_query_ms") or 0) db_timeout = bool(search_meta.get("db_timeout")) vector_timeout = bool(search_meta.get("vector_timeout")) graph_timeout = bool(search_meta.get("graph_timeout")) tool_trace = list(search_meta.get("tool_trace") or []) tool_trace.insert( 0, { "step": 0, "tool": "thinking_mode", "mode": active_thinking_mode, "fast_path_bypassed": bool(deep_thinking), "reason": forced_deep_reason or ( "manual_deep" if self._normalize_thinking_mode(thinking_mode) == THINKING_MODE_DEEP else "normal" ), }, ) final_domains = [str(x) for x in (search_meta.get("domains_used") or domains)] prompt_variant_id = self._peek_answer_prompt_variant() max_score = retrieved[0].score if retrieved else 0.0 answer_text = await self._generate_answer( effective_q, retrieved, conversation, project_id, system_snapshot=system_snapshot, user_snapshot=user_snapshot, relevance_max=max_score, thinking_mode=active_thinking_mode, context_block_limit=DEEP_CONTEXT_BLOCKS if deep_thinking else None, answer_max_tokens=DEEP_ANSWER_MAX_TOKENS if deep_thinking else None, asked_via=asked_via, message_intent=message_intent, mixed_signal_part=mixed_signal_part, query_type=query_type, consultant_mode_preference=consultant_mode_preference, ) prompt_variant_id = self._peek_answer_prompt_variant() routing_snapshot = self._peek_last_answer_routing_snapshot() used_sources = self._dedupe_sources(retrieved) if not used_sources: used_sources = [ { "source": "system", "label": "System Snapshot", "score": round(max(max_score, 0.6), 4), "metadata": {"mode": "snapshot_only", "relevance_max": round(max_score, 4)}, } ] if user_snapshot: used_sources.append( { "source": "system", "label": "User Snapshot", "score": round(max(max_score, 0.58), 4), "metadata": {"mode": "snapshot_only", "relevance_max": round(max_score, 4)}, } ) auto_retry_reason = "" if not deep_thinking: auto_retry_reason = self._should_auto_deep_retry( question=clean_q, effective_question=effective_q, sources=used_sources, relevance_max=max_score, ) if auto_retry_reason and not self._allow_auto_deep_retry_for_channel( asked_via=asked_via, question=clean_q, effective_question=effective_q, reason=auto_retry_reason, ): auto_retry_reason = "" if auto_retry_reason: deep_retrieved, deep_meta = await self._multi_step_search( question=effective_q, domains=domains, project_id=project_id, source_chat_id=source_chat_id, session_state=state_row, max_steps_override=deep_retrieval_steps, vector_top_k=deep_vector_top_k, ) deep_max_score = deep_retrieved[0].score if deep_retrieved else 0.0 deep_answer_text = await self._generate_answer( effective_q, deep_retrieved, conversation, project_id, system_snapshot=system_snapshot, user_snapshot=user_snapshot, relevance_max=deep_max_score, thinking_mode=THINKING_MODE_DEEP, context_block_limit=DEEP_CONTEXT_BLOCKS, answer_max_tokens=DEEP_ANSWER_MAX_TOKENS, asked_via=asked_via, message_intent=message_intent, mixed_signal_part=mixed_signal_part, query_type=query_type, consultant_mode_preference=consultant_mode_preference, ) deep_sources = self._dedupe_sources(deep_retrieved) if not deep_sources: deep_sources = [ { "source": "system", "label": "System Snapshot", "score": round(max(deep_max_score, 0.6), 4), "metadata": {"mode": "snapshot_only", "relevance_max": round(deep_max_score, 4)}, } ] if user_snapshot: deep_sources.append( { "source": "system", "label": "User Snapshot", "score": round(max(deep_max_score, 0.58), 4), "metadata": {"mode": "snapshot_only", "relevance_max": round(deep_max_score, 4)}, } ) merged_metrics = self._merge_search_metrics( { "db_query_ms": db_query_ms, "vector_search_ms": vector_search_ms, "graph_query_ms": graph_query_ms, "db_timeout": db_timeout, "vector_timeout": vector_timeout, "graph_timeout": graph_timeout, }, deep_meta, ) db_query_ms = int(merged_metrics.get("db_query_ms") or 0) vector_search_ms = int(merged_metrics.get("vector_search_ms") or 0) graph_query_ms = int(merged_metrics.get("graph_query_ms") or 0) db_timeout = bool(merged_metrics.get("db_timeout")) vector_timeout = bool(merged_metrics.get("vector_timeout")) graph_timeout = bool(merged_metrics.get("graph_timeout")) deep_trace = list(deep_meta.get("tool_trace") or []) tool_trace.append( { "step": 98, "tool": "auto_deep_retry", "reason": auto_retry_reason, "prev_relevance_max": round(float(max_score or 0.0), 4), "prev_snapshot_only": self._is_snapshot_only_sources(used_sources), } ) for row in deep_trace: tool_trace.append(row) final_domains = [str(x) for x in (deep_meta.get("domains_used") or final_domains or domains)] max_score = deep_max_score answer_text = deep_answer_text used_sources = deep_sources deep_thinking = True active_thinking_mode = THINKING_MODE_DEEP answer_mode = ANSWER_MODE_RETRIEVAL_DEEP prompt_variant_id = self._peek_answer_prompt_variant() routing_snapshot = self._peek_last_answer_routing_snapshot() evidence_guard_triggered = False clarification_due_to_evidence = False if not evidence_guard_triggered and self._is_snapshot_only_sources(used_sources) and float(max_score or 0.0) < 0.45: clarification_due_to_evidence = True answer_text = ( f"当前关于“{clean_q[:80]}”的证据不足,现阶段只命中系统快照。" "请补充更明确的对象、范围或文档入口,我再给你可核验的结论。" ) used_sources = [ { "source": "system", "label": "Evidence Guard", "score": 0.91, "metadata": {"mode": "clarification", "reason": "insufficient_evidence"}, } ] max_score = min(float(max_score or 0.0), 0.4) if "system_context" not in final_domains: final_domains.append("system_context") tool_trace.append( { "step": 97, "tool": "clarification_guard", "reason": "snapshot_only_low_relevance", } ) answer_mode = ANSWER_MODE_CLARIFY evidence_guard_triggered = self._should_block_answer_with_guard( question=clean_q, effective_question=effective_q, sources=used_sources, relevance_max=max_score, ) if evidence_guard_triggered: answer_text = self._build_evidence_guard_answer(clean_q) used_sources = [ { "source": "system", "label": "Evidence Guard", "score": 0.99, "metadata": {"mode": "guard", "reason": "snapshot_only_process_query"}, } ] max_score = min(float(max_score or 0.0), 0.35) if "system_context" not in final_domains: final_domains.append("system_context") tool_trace.append( { "step": 99, "tool": "answer_guard", "reason": "insufficient_call_chain_evidence", } ) answer_mode = ANSWER_MODE_BLOCKED elapsed = int((time.perf_counter() - started) * 1000) grounding_verdict = GroundingVerdict( status=GROUNDING_STATUS_GROUNDED, missing_fields=[], reason_code=None, ) if evidence_guard_triggered: grounding_verdict = GroundingVerdict( status=GROUNDING_STATUS_BLOCKED, missing_fields=["call_chain_evidence", "target_scope"], reason_code="insufficient_evidence", ) elif clarification_due_to_evidence: grounding_verdict = GroundingVerdict( status=GROUNDING_STATUS_INSUFFICIENT_EVIDENCE, missing_fields=["high_relevance_evidence"], reason_code="insufficient_evidence", ) elif self._is_snapshot_only_sources(used_sources) or float(max_score or 0.0) < 0.45: grounding_verdict = GroundingVerdict( status=GROUNDING_STATUS_PARTIALLY_GROUNDED, missing_fields=["high_relevance_evidence"] if float(max_score or 0.0) < 0.45 else [], reason_code=None, ) retrieval_metrics = { "db_query_ms": int(db_query_ms or 0), "vector_search_ms": int(vector_search_ms or 0), "graph_query_ms": int(graph_query_ms or 0), "db_timeout": bool(db_timeout), "vector_timeout": bool(vector_timeout), "graph_timeout": bool(graph_timeout), } runtime_fields = self._build_qa_record_runtime_fields( answer=answer_text, sources=used_sources, missing_fields=grounding_verdict.missing_fields, answer_mode=answer_mode, grounding_status=grounding_verdict.status, prompt_variant_id=prompt_variant_id, reason_code=grounding_verdict.reason_code, routing_snapshot=routing_snapshot, tool_trace=tool_trace, retrieval_metrics=retrieval_metrics, ) qa = QARecord( tenant_id=self.tenant_id, question=clean_q, answer=answer_text, sources=used_sources, intent="question", domains_used=final_domains, response_time_ms=elapsed, asked_by=asked_by, asked_via=(asked_via or "pmflow")[:20], project_id=project_id, session_id=session_id, turn_index=turn_index, **self._build_qa_record_external_fields( anchor_kind=None, anchor_ref=None, routing_snapshot=routing_snapshot, ), **runtime_fields, ) self.db.add(qa) confidence = round(max(0.2, min(1.0, max_score if max_score > 0 else 0.55)), 4) if evidence_guard_triggered: confidence = min(confidence, 0.35) missing_fields = list(grounding_verdict.missing_fields) await self._update_session_state_after_turn( session_state=state_row, question=clean_q, effective_question=effective_q, answer=answer_text, query_type=query_type if query_type != QUERY_TYPE_UNKNOWN else QUERY_TYPE_EXPLAIN, ambiguity="clear" if not missing_fields else "low_evidence", rewrite_meta=rewrite_meta, sources=used_sources, ) await self.db.commit() await self.db.refresh(qa) await self.context_cache.append_session_messages( tenant_id=self.tenant_id, session_id=context_session_id, question=clean_q, answer=answer_text, ) return self._build_response_payload( answer=answer_text, sources=used_sources, domains_used=final_domains, response_time_ms=elapsed, db_query_ms=db_query_ms, vector_search_ms=vector_search_ms, graph_query_ms=graph_query_ms, db_timeout=db_timeout, vector_timeout=vector_timeout, graph_timeout=graph_timeout, tool_trace=runtime_fields["tool_trace"], qa_id=int(qa.id), session_id=int(session_id) if session_id is not None else None, turn_index=int(turn_index), relevance_max=round(max_score, 4), query_type=query_type if query_type != QUERY_TYPE_UNKNOWN else QUERY_TYPE_EXPLAIN, confidence=confidence, missing_fields=missing_fields, thinking_mode=active_thinking_mode, reason_code=grounding_verdict.reason_code or ("answer_generated" if answer_mode not in {ANSWER_MODE_CLARIFY, ANSWER_MODE_BLOCKED} else "insufficient_evidence"), answer_mode=answer_mode, grounding_status=grounding_verdict.status, policy_version=runtime_fields["policy_version"], citations=runtime_fields["citations"], sources_used=runtime_fields["sources_used"], evidence_pack=runtime_fields["evidence_pack"], prompt_variant_id=prompt_variant_id, )