PMFlow/backend/app/services/adaptive_rag_service/mixin_answer_precheck.py

330 lines
12 KiB
Python

"""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,
) -> 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,
},
}
]
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=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,
),
)
self.db.add(qa)
await self._update_session_state_after_turn(
session_state=state_row,
question=clean_q,
effective_question=effective_q,
answer=answer,
query_type=query_type,
ambiguity="clear",
rewrite_meta=rewrite_meta,
sources=direct_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,
)
return self._build_response_payload(
answer=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,
)
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,
) -> 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},
}
]
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=clarification_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(),
)
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=clarification_answer,
query_type=query_type,
ambiguity="needs_clarification",
rewrite_meta=rewrite_meta,
sources=clarify_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=clarification_answer,
)
return self._build_response_payload(
answer=clarification_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,
)
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]],
) -> 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"},
}
)
qa = QARecord(
tenant_id=self.tenant_id,
question=clean_q,
answer=direct_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(),
)
self.db.add(qa)
await self._update_session_state_after_turn(
session_state=state_row,
question=clean_q,
effective_question=effective_q,
answer=direct_answer,
query_type=QUERY_TYPE_EXPLAIN,
ambiguity="clear",
rewrite_meta=rewrite_meta,
sources=direct_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=direct_answer,
)
return self._build_response_payload(
answer=direct_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,
)