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

226 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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=[],
)
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],
) -> tuple[str, dict[str, Any]]:
expanded = self._expand_followup_question(question, conversation)
current = str(expanded or question or "").strip()
if not current:
return current, {"mode": "empty"}
lower = current.lower()
has_referential = any(k in lower for k in FOLLOWUP_REFERENTIAL_KEYWORDS)
if not has_referential:
return current, {"mode": "direct"}
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]}
return current, {"mode": "referential_without_subject"}
def _build_clarification_question(
self,
*,
original_question: str,
effective_question: str,
session_state: Optional[QASessionState],
) -> Optional[str]:
text = str(original_question or "").strip().lower()
if not text:
return None
if self._is_correction_query(text):
if self._has_explicit_target_scope(text):
return None
return (
"收到,你在纠正上一条回答。请直接说你要的目标范围,例如:"
"`我问的是 AI 引擎驱动方式,第一层是否调用 openclaw?`"
)
has_referential = any(k in text for k in FOLLOWUP_REFERENTIAL_KEYWORDS)
if not has_referential:
return None
state_subject = str(getattr(session_state, "last_subject", "") or "").strip()
state_entity_ids = getattr(session_state, "last_entity_ids", None)
has_state_entities = isinstance(state_entity_ids, list) and any(str(x).strip() for x in state_entity_ids)
if state_subject or has_state_entities:
return None
if str(effective_question or "").strip() != text and "补充问题" in str(effective_question):
return None
return "你这句缺少明确主语。请先指明对象,例如:`分别是哪些项目?` 或 `请列出这些项目的名称和ID`。"
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
subject = self._extract_state_subject(question=question, answer=answer, sources=sources)
if subject:
session_state.last_subject = subject[:255]
session_state.last_intent = "question"
session_state.ambiguity = str(ambiguity or "clear")[:20]
session_state.last_entity_ids = self._collect_entity_ids_from_sources(sources)
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()
keyword = self._extract_project_lookup_keyword(text)
if len(keyword) >= 2:
return keyword[: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:],
"updated_at": state.updated_at.isoformat() if state and state.updated_at else None,
}