"""Adaptive RAG mixin module.""" from __future__ import annotations from .shared import * class AdaptiveRAGAnswerPrecheckMixin: async def _maybe_return_meta_followup_response( self, *, meta_followup_payload: Optional[dict[str, Any]], started: float, clean_q: str, effective_q: str, query_type: str, asked_by: Optional[int], asked_via: str, project_id: Optional[int], session_id: Optional[int], turn_index: int, state_row: Optional[QASessionState], rewrite_meta: dict[str, Any], context_session_id: Optional[int], active_thinking_mode: str, route_decision: Optional[dict[str, Any]] = None, ) -> Optional[dict[str, Any]]: if not isinstance(meta_followup_payload, dict): return None answer = str(meta_followup_payload.get("answer") or "").strip() if not answer: return None reason_code = str(meta_followup_payload.get("reason_code") or "").strip() or "meta_followup_answered" anchor_kind = str(meta_followup_payload.get("anchor_kind") or "").strip() or None anchor_mode = str(meta_followup_payload.get("anchor_mode") or "").strip() or None meta_followup_kind = str(meta_followup_payload.get("meta_followup_kind") or "").strip() or None routing_snapshot = ( meta_followup_payload.get("routing_snapshot") if isinstance(meta_followup_payload.get("routing_snapshot"), dict) else {} ) elapsed = int((time.perf_counter() - started) * 1000) direct_sources = [ { "source": "system", "label": "Followup Anchor", "score": 0.97, "metadata": { "mode": "meta_followup", "reason_code": reason_code, "anchor_kind": anchor_kind, "anchor_mode": anchor_mode, "meta_followup_kind": meta_followup_kind, }, } ] render_with_contract = getattr(self, "_render_answer_with_route_contract", None) rendered_answer = answer if callable(render_with_contract): rendered_answer = render_with_contract( answer=answer, sources=direct_sources, route_decision=route_decision, ) runtime_fields = self._build_qa_record_runtime_fields( answer=rendered_answer, sources=direct_sources, missing_fields=[], answer_mode=ANSWER_MODE_FAST_PATH, grounding_status=GROUNDING_STATUS_GROUNDED, prompt_variant_id="meta_followup", reason_code=reason_code, routing_snapshot=routing_snapshot, retrieval_metrics={ "db_query_ms": 0, "vector_search_ms": 0, "graph_query_ms": 0, "db_timeout": False, "vector_timeout": False, "graph_timeout": False, }, ) active_session_id = int(session_id) if session_id is not None else ( int(context_session_id) if context_session_id is not None else None ) qa = QARecord( tenant_id=self.tenant_id, question=clean_q, answer=rendered_answer, sources=direct_sources, intent="question", domains_used=["system_context"], response_time_ms=elapsed, asked_by=asked_by, asked_via=(asked_via or "pmflow")[:20], project_id=project_id, session_id=active_session_id, turn_index=turn_index, **self._build_qa_record_external_fields( anchor_kind=anchor_kind, anchor_ref=str(meta_followup_payload.get("anchor_ref") or "").strip() or None, routing_snapshot=routing_snapshot, route_decision=route_decision, ), **runtime_fields, ) self.db.add(qa) await self._update_session_state_after_turn( session_state=state_row, question=clean_q, effective_question=effective_q, answer=rendered_answer, query_type=query_type, ambiguity="clear", rewrite_meta=rewrite_meta, sources=direct_sources, ) await self.db.flush() persist_route_memory = getattr(self, "_persist_query_route_memory", None) if callable(persist_route_memory): await persist_route_memory( qa_record_id=int(qa.id) if getattr(qa, "id", None) is not None else None, question_text=clean_q, project_id=project_id, route_decision=route_decision, ) 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=rendered_answer, ) return self._build_response_payload( answer=rendered_answer, sources=direct_sources, domains_used=["system_context"], response_time_ms=elapsed, db_query_ms=0, vector_search_ms=0, db_timeout=False, vector_timeout=False, qa_id=int(qa.id), session_id=active_session_id, turn_index=int(turn_index), relevance_max=0.97, query_type=query_type, confidence=0.94, missing_fields=[], thinking_mode=active_thinking_mode, anchor_mode=anchor_mode, anchor_kind=anchor_kind, meta_followup_kind=meta_followup_kind, reason_code=reason_code, answer_mode=ANSWER_MODE_FAST_PATH, grounding_status=GROUNDING_STATUS_GROUNDED, policy_version=runtime_fields["policy_version"], citations=runtime_fields["citations"], sources_used=runtime_fields["sources_used"], evidence_pack=runtime_fields["evidence_pack"], tool_trace=runtime_fields["tool_trace"], prompt_variant_id="meta_followup", route_decision=route_decision, ) async def _maybe_return_clarification_response( self, *, clarification_payload: Optional[dict[str, Any]], started: float, clean_q: str, effective_q: str, query_type: str, asked_by: Optional[int], asked_via: str, project_id: Optional[int], session_id: Optional[int], turn_index: int, state_row: Optional[QASessionState], rewrite_meta: dict[str, Any], context_session_id: Optional[int], active_thinking_mode: str, route_decision: Optional[dict[str, Any]] = None, ) -> Optional[dict[str, Any]]: if not isinstance(clarification_payload, dict): return None clarification_answer = str(clarification_payload.get("answer") or "").strip() if not clarification_answer: return None clarification_code = str(clarification_payload.get("code") or "no_anchor_found").strip() or "no_anchor_found" clarification_candidates = ( clarification_payload.get("candidates") if isinstance(clarification_payload.get("candidates"), list) else [] ) missing_fields = [ str(item) for item in ( clarification_payload.get("missing_fields") if isinstance(clarification_payload.get("missing_fields"), list) else ["reply_anchor"] ) if str(item).strip() ] elapsed = int((time.perf_counter() - started) * 1000) clarify_sources = [ { "source": "system", "label": "Session State", "score": 0.95, "metadata": {"mode": "clarification", "reason": clarification_code}, } ] render_with_contract = getattr(self, "_render_answer_with_route_contract", None) rendered_answer = clarification_answer if callable(render_with_contract): rendered_answer = render_with_contract( answer=clarification_answer, sources=clarify_sources, route_decision=route_decision, ) runtime_fields = self._build_qa_record_runtime_fields( answer=rendered_answer, sources=clarify_sources, missing_fields=missing_fields, answer_mode=ANSWER_MODE_CLARIFY, grounding_status=GROUNDING_STATUS_INSUFFICIENT_EVIDENCE, prompt_variant_id="clarification", reason_code=clarification_code, retrieval_metrics={ "db_query_ms": 0, "vector_search_ms": 0, "graph_query_ms": 0, "db_timeout": False, "vector_timeout": False, "graph_timeout": False, }, ) active_session_id = int(session_id) if session_id is not None else ( int(context_session_id) if context_session_id is not None else None ) qa = QARecord( tenant_id=self.tenant_id, question=clean_q, answer=rendered_answer, sources=clarify_sources, intent="question", domains_used=["system_context"], response_time_ms=elapsed, asked_by=asked_by, asked_via=(asked_via or "pmflow")[:20], project_id=project_id, session_id=active_session_id, turn_index=turn_index, **self._build_qa_record_external_fields(route_decision=route_decision), **runtime_fields, ) self.db.add(qa) if clarification_code == "ambiguous_recent_reply" and clarification_candidates: previous = self._get_pending_clarification(session_state=state_row) previous_attempts = int(previous.get("attempts") or 0) if isinstance(previous, dict) else 0 self._store_pending_clarification( session_state=state_row, original_question=clean_q, clarification_code=clarification_code, candidates=clarification_candidates, previous_attempts=previous_attempts, ) else: self._clear_pending_clarification(session_state=state_row) await self._update_session_state_after_turn( session_state=state_row, question=clean_q, effective_question=effective_q, answer=rendered_answer, query_type=query_type, ambiguity="needs_clarification", rewrite_meta=rewrite_meta, sources=clarify_sources, ) await self.db.flush() persist_route_memory = getattr(self, "_persist_query_route_memory", None) if callable(persist_route_memory): await persist_route_memory( qa_record_id=int(qa.id) if getattr(qa, "id", None) is not None else None, question_text=clean_q, project_id=project_id, route_decision=route_decision, ) 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=rendered_answer, ) return self._build_response_payload( answer=rendered_answer, sources=clarify_sources, domains_used=["system_context"], response_time_ms=elapsed, db_query_ms=0, vector_search_ms=0, db_timeout=False, vector_timeout=False, qa_id=int(qa.id), session_id=active_session_id, turn_index=int(turn_index), relevance_max=0.95, query_type=query_type, confidence=0.92, missing_fields=missing_fields, thinking_mode=active_thinking_mode, clarification_code=clarification_code, clarification_candidates=clarification_candidates, reason_code=clarification_code, answer_mode=ANSWER_MODE_CLARIFY, grounding_status=GROUNDING_STATUS_INSUFFICIENT_EVIDENCE, policy_version=runtime_fields["policy_version"], citations=runtime_fields["citations"], sources_used=runtime_fields["sources_used"], evidence_pack=runtime_fields["evidence_pack"], tool_trace=runtime_fields["tool_trace"], prompt_variant_id="clarification", route_decision=route_decision, ) async def _maybe_return_snapshot_direct_response( self, *, direct_answer: Optional[str], started: float, clean_q: str, effective_q: str, asked_by: Optional[int], asked_via: str, project_id: Optional[int], session_id: Optional[int], turn_index: int, state_row: Optional[QASessionState], rewrite_meta: dict[str, Any], context_session_id: Optional[int], active_thinking_mode: str, user_snapshot: Optional[dict[str, Any]], route_decision: Optional[dict[str, Any]] = None, ) -> Optional[dict[str, Any]]: if not direct_answer: return None elapsed = int((time.perf_counter() - started) * 1000) direct_sources = [ { "source": "system", "label": "System Snapshot", "score": 0.99, "metadata": {"mode": "snapshot_direct", "topic": "identity_or_architecture"}, } ] if user_snapshot: direct_sources.append( { "source": "system", "label": "User Snapshot", "score": 0.98, "metadata": {"mode": "snapshot_direct", "topic": "current_user"}, } ) render_with_contract = getattr(self, "_render_answer_with_route_contract", None) rendered_answer = direct_answer if callable(render_with_contract): rendered_answer = render_with_contract( answer=direct_answer, sources=direct_sources, route_decision=route_decision, ) runtime_fields = self._build_qa_record_runtime_fields( answer=rendered_answer, sources=direct_sources, missing_fields=[], answer_mode=ANSWER_MODE_FAST_PATH, grounding_status=GROUNDING_STATUS_PARTIALLY_GROUNDED, prompt_variant_id="snapshot_direct", retrieval_metrics={ "db_query_ms": 0, "vector_search_ms": 0, "graph_query_ms": 0, "db_timeout": False, "vector_timeout": False, "graph_timeout": False, }, ) qa = QARecord( tenant_id=self.tenant_id, question=clean_q, answer=rendered_answer, sources=direct_sources, intent="question", domains_used=["system_context"], response_time_ms=elapsed, asked_by=asked_by, asked_via=(asked_via or "pmflow")[:20], project_id=project_id, session_id=int(session_id) if session_id is not None else (int(context_session_id) if context_session_id is not None else None), turn_index=turn_index, **self._build_qa_record_external_fields(route_decision=route_decision), **runtime_fields, ) self.db.add(qa) await self._update_session_state_after_turn( session_state=state_row, question=clean_q, effective_question=effective_q, answer=rendered_answer, query_type=QUERY_TYPE_EXPLAIN, ambiguity="clear", rewrite_meta=rewrite_meta, sources=direct_sources, ) await self.db.flush() persist_route_memory = getattr(self, "_persist_query_route_memory", None) if callable(persist_route_memory): await persist_route_memory( qa_record_id=int(qa.id) if getattr(qa, "id", None) is not None else None, question_text=clean_q, project_id=project_id, route_decision=route_decision, ) 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=rendered_answer, ) return self._build_response_payload( answer=rendered_answer, sources=direct_sources, domains_used=["system_context"], response_time_ms=elapsed, db_query_ms=0, vector_search_ms=0, db_timeout=False, vector_timeout=False, 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=1.0, query_type=QUERY_TYPE_EXPLAIN, confidence=0.99, missing_fields=[], thinking_mode=active_thinking_mode, answer_mode=ANSWER_MODE_FAST_PATH, grounding_status=GROUNDING_STATUS_PARTIALLY_GROUNDED, policy_version=runtime_fields["policy_version"], citations=runtime_fields["citations"], sources_used=runtime_fields["sources_used"], evidence_pack=runtime_fields["evidence_pack"], tool_trace=runtime_fields["tool_trace"], prompt_variant_id="snapshot_direct", route_decision=route_decision, )