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

222 lines
8.8 KiB
Python

"""Adaptive RAG mixin module."""
from __future__ import annotations
from .shared import *
class AdaptiveRAGSessionStateMixin:
async def _ensure_session_state(self, *, session_id: Optional[int]) -> Optional[QASessionState]:
if session_id is None:
return None
row = (
await self.db.execute(
select(QASessionState).where(
and_(
QASessionState.tenant_id == self.tenant_id,
QASessionState.session_id == int(session_id),
)
)
)
).scalar_one_or_none()
if row is None:
row = QASessionState(
tenant_id=self.tenant_id,
session_id=int(session_id),
last_subject=None,
last_entity_ids=[],
last_intent=None,
ambiguity="unknown",
rewrite_history=[],
pending_clarification=None,
pending_clarification_expires_at=None,
)
self.db.add(row)
await self.db.flush()
return row
def _rewrite_question_with_state(
self,
*,
question: str,
conversation: list[dict[str, str]],
session_state: Optional[QASessionState],
anchor_resolution: Optional[FollowupAnchorResolution] = None,
) -> tuple[str, dict[str, Any]]:
_ = conversation
current = str(question or "").strip()
if not current:
return current, {"mode": "empty"}
resolution = anchor_resolution or FollowupAnchorResolution()
if str(resolution.followup_mode or "").strip().lower() != "referential":
return current, {"mode": "direct"}
explicit_anchor_mode = str(resolution.anchor_mode or "").strip().lower()
if explicit_anchor_mode == "explicit":
anchored_question, anchor_meta = self._rewrite_question_with_followup_anchor(
question=current,
anchor_resolution=resolution,
)
if str(anchor_meta.get("mode") or "").strip() not in {"", "direct"}:
return anchored_question, anchor_meta
state_subject = ""
state_entity_ids: list[int] = []
if session_state is not None:
state_subject = str(session_state.last_subject or "").strip()
if isinstance(session_state.last_entity_ids, list):
for value in session_state.last_entity_ids:
try:
state_entity_ids.append(int(value))
except Exception:
continue
if state_subject:
rewritten = f"围绕“{state_subject}”回答:{current}"
return rewritten[:1200], {"mode": "state_subject", "subject": state_subject}
if state_entity_ids:
anchor = ",".join(str(x) for x in state_entity_ids[:6])
rewritten = f"围绕会话实体ID[{anchor}]回答:{current}"
return rewritten[:1200], {"mode": "state_entities", "entity_ids": state_entity_ids[:6]}
anchored_question, anchor_meta = self._rewrite_question_with_followup_anchor(
question=current,
anchor_resolution=resolution,
)
if str(anchor_meta.get("mode") or "").strip() not in {"", "direct"}:
return anchored_question, anchor_meta
return current, {"mode": "referential_without_subject"}
async def _update_session_state_after_turn(
self,
*,
session_state: Optional[QASessionState],
question: str,
effective_question: str,
answer: str,
query_type: str,
ambiguity: str,
rewrite_meta: dict[str, Any],
sources: list[dict[str, Any]],
) -> None:
if session_state is None:
return
session_state.last_intent = "question"
session_state.ambiguity = str(ambiguity or "clear")[:20]
if str(ambiguity or "").strip() != "needs_clarification":
subject = self._extract_state_subject(question=question, answer=answer, sources=sources)
if subject:
session_state.last_subject = subject[:255]
session_state.last_entity_ids = self._collect_entity_ids_from_sources(sources)
self._clear_pending_clarification(session_state=session_state)
history = session_state.rewrite_history if isinstance(session_state.rewrite_history, list) else []
history.append(
{
"at": now_cn_aware().isoformat(),
"question": question[:500],
"effective_question": effective_question[:500],
"query_type": str(query_type or QUERY_TYPE_UNKNOWN),
"ambiguity": str(ambiguity or "clear"),
"rewrite_meta": rewrite_meta if isinstance(rewrite_meta, dict) else {},
}
)
session_state.rewrite_history = history[-20:]
await self.db.flush()
def _collect_entity_ids_from_sources(self, sources: list[dict[str, Any]]) -> list[int]:
ids: list[int] = []
for source in sources:
if not isinstance(source, dict):
continue
meta = source.get("metadata") if isinstance(source.get("metadata"), dict) else {}
for key in (
"project_id",
"task_id",
"signal_id",
"user_id",
"entity_id",
"subject_entity_id",
"object_entity_id",
):
value = meta.get(key)
try:
if value is not None:
ids.append(int(value))
except Exception:
continue
matched = meta.get("matched_project_ids")
if isinstance(matched, list):
for value in matched:
try:
ids.append(int(value))
except Exception:
continue
uniq: list[int] = []
for value in ids:
if value not in uniq:
uniq.append(value)
return uniq[:20]
def _extract_state_subject(
self,
*,
question: str,
answer: str,
sources: list[dict[str, Any]],
) -> str:
for source in sources:
if not isinstance(source, dict):
continue
meta = source.get("metadata") if isinstance(source.get("metadata"), dict) else {}
project_keyword = str(meta.get("query_keyword") or "").strip()
if project_keyword:
return project_keyword[:255]
subject_name = str(meta.get("subject_name") or "").strip()
object_name = str(meta.get("object_name") or "").strip()
if subject_name and object_name:
return f"{subject_name}->{object_name}"[:255]
text = str(question or "").strip()
normalizer = getattr(self, "_normalize_lookup_text", None)
normalized_question = normalizer(text) if callable(normalizer) else text
if 2 <= len(normalized_question) <= 64:
return normalized_question[:255]
answer_text = str(answer or "").strip()
m = re.search(r"项目[「\"]?([^」\"\n]{2,40})", answer_text)
if m:
return str(m.group(1)).strip()[:255]
return ""
async def get_session_state_replay(self, *, session_id: int, user_id: int) -> dict[str, Any]:
session = (
await self.db.execute(
select(QASession).where(
and_(
QASession.tenant_id == self.tenant_id,
QASession.id == int(session_id),
QASession.user_id == int(user_id),
)
)
)
).scalar_one_or_none()
if session is None:
raise ValueError("qa session 不存在")
state = await self._ensure_session_state(session_id=int(session_id))
history = state.rewrite_history if isinstance(state.rewrite_history, list) else []
return {
"session_id": int(session_id),
"last_subject": str(state.last_subject or "") if state else "",
"last_entity_ids": state.last_entity_ids if state else [],
"last_intent": str(state.last_intent or "") if state else "",
"ambiguity": str(state.ambiguity or "") if state else "",
"rewrite_history": history[-20:],
"pending_clarification": state.pending_clarification if state else None,
"pending_clarification_expires_at": (
state.pending_clarification_expires_at.isoformat()
if state and state.pending_clarification_expires_at
else None
),
"updated_at": state.updated_at.isoformat() if state and state.updated_at else None,
}