492 lines
19 KiB
Python
492 lines
19 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,
|
|
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,
|
|
)
|
|
elif clarification_code == "query_route_clarify":
|
|
previous = self._get_pending_clarification(session_state=state_row)
|
|
previous_attempts = (
|
|
int(previous.get("attempts") or 0)
|
|
if isinstance(previous, dict) and str(previous.get("kind") or "").strip().lower() == "query_route_selection"
|
|
else 0
|
|
)
|
|
self._store_pending_query_route_clarification(
|
|
session_state=state_row,
|
|
original_question=clean_q,
|
|
previous_attempts=previous_attempts,
|
|
)
|
|
elif isinstance(clarification_payload.get("clarification_protocol"), dict):
|
|
protocol = dict(clarification_payload.get("clarification_protocol") or {})
|
|
previous = self._get_pending_clarification(session_state=state_row)
|
|
previous_attempts = (
|
|
int(previous.get("attempts") or 0)
|
|
if isinstance(previous, dict) and str(previous.get("kind") or "").strip().lower() == str(protocol.get("kind") or "").strip().lower()
|
|
else 0
|
|
)
|
|
self._store_pending_selection_protocol(
|
|
session_state=state_row,
|
|
original_question=clean_q,
|
|
protocol=protocol,
|
|
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,
|
|
)
|