PMFlow/backend/app/api/rag_ask_contract.py

146 lines
5.9 KiB
Python

from __future__ import annotations
from typing import Any
from uuid import uuid4
from ..models import User
from ..schemas.rag import RAGAskRequest
from ..services.ai_pmo.project_operating_context_builder import ProjectOperatingContextBuilder
def resolve_effective_tenant_id(current_user: User, fallback_tenant_id: int) -> int:
try:
return int(getattr(current_user, "tenant_id") or fallback_tenant_id or 1)
except Exception:
return int(fallback_tenant_id or 1)
def build_answer_question_kwargs(request: RAGAskRequest, current_user: User) -> dict[str, Any]:
asked_by = int(getattr(current_user, "id"))
return {
"question": request.question,
"asked_by": asked_by,
"asked_via": request.asked_via,
"project_id": request.project_id,
"source_chat_id": request.source_chat_id,
"session_id": request.session_id,
"force_new_session": bool(request.new_session),
"thinking_mode": request.thinking_mode,
"action_mode": request.action_mode,
"sync_requirement": bool(request.sync_requirement),
"message_intent": request.message_intent,
"mixed_signal_part": request.mixed_signal_part,
"consultant_mode_preference": request.consultant_mode_preference,
}
def _safe_int(value: Any) -> int | None:
try:
if value is None:
return None
return int(value)
except Exception:
return None
def _resolve_sender_name(current_user: User) -> str:
sender = str(
getattr(current_user, "name", None)
or getattr(current_user, "username", None)
or "PMFlow用户"
).strip()
return sender or "PMFlow用户"
def _normalize_dict_list(value: Any) -> list[dict[str, Any]]:
if not isinstance(value, list):
return []
return [item for item in value if isinstance(item, dict)]
def _normalize_optional_dict(value: Any) -> dict[str, Any]:
return dict(value) if isinstance(value, dict) else {}
def _default_evidence_pack(*, answer: str, tool_trace: list[dict[str, Any]]) -> dict[str, Any]:
return {
"summary": {
"conclusion": str(answer or "").strip(),
"evidence": [],
"uncertainties": [],
},
"tool_trace": tool_trace,
}
def build_kernel_ingress_kwargs(request: RAGAskRequest, current_user: User) -> dict[str, Any]:
rag_request_context = build_answer_question_kwargs(request=request, current_user=current_user)
project_operating_context = ProjectOperatingContextBuilder().build_from_ingress(
project_hint_id=request.project_id,
rag_request_context=rag_request_context,
)
return {
"source_system": str(request.asked_via or "pmflow").strip() or "pmflow",
"source_message_id": f"rag-ask-{uuid4().hex}",
"message_text": request.question,
"sender": _resolve_sender_name(current_user),
"channel_ref": request.source_chat_id,
"source_sender_open_id": None,
"captured_at": None,
"chat_type": "manual",
"project_hint_id": request.project_id,
"ingress_options": {
"rag_request_context": rag_request_context,
"project_operating_context": project_operating_context.model_dump(),
},
}
def build_rag_response_payload(*, ingress_result: dict[str, Any]) -> dict[str, Any]:
result = ingress_result if isinstance(ingress_result, dict) else {}
rag_result = result.get("rag_result") if isinstance(result.get("rag_result"), dict) else {}
payload = dict(rag_result)
consultant_dispatch = (
payload.get("consultant_dispatch")
if isinstance(payload.get("consultant_dispatch"), dict)
else (result.get("consultant_dispatch") if isinstance(result.get("consultant_dispatch"), dict) else None)
)
if isinstance(consultant_dispatch, dict):
payload["consultant_dispatch"] = consultant_dispatch
answer = str(payload.get("answer") or result.get("direct_reply_text") or "").strip()
payload["answer"] = answer
if "reply_required" not in payload:
payload["reply_required"] = bool(result.get("direct_reply_required"))
if "reply_text" not in payload:
payload["reply_text"] = str(result.get("direct_reply_text") or "").strip() or None
if "trace_id" not in payload:
payload["trace_id"] = str(result.get("ai_trace_id") or "").strip() or None
if "processing_stage" not in payload:
payload["processing_stage"] = "qa"
if "tool_trace" not in payload:
payload["tool_trace"] = []
payload["tool_trace"] = _normalize_dict_list(payload.get("tool_trace"))
payload["route_decision"] = _normalize_optional_dict(payload.get("route_decision"))
payload["clarification_code"] = str(payload.get("clarification_code") or "").strip() or None
payload["clarification_candidates"] = _normalize_dict_list(payload.get("clarification_candidates"))
payload["citations"] = _normalize_dict_list(payload.get("citations"))
payload["sources_used"] = _normalize_dict_list(payload.get("sources_used"))
payload["answer_mode"] = str(payload.get("answer_mode") or "retrieval_normal").strip() or "retrieval_normal"
payload["grounding_status"] = str(payload.get("grounding_status") or "partially_grounded").strip() or "partially_grounded"
payload["policy_version"] = (
str(payload.get("policy_version") or "retrieval-v2.1|routing:unknown|prompt:unknown").strip()
or "retrieval-v2.1|routing:unknown|prompt:unknown"
)
evidence_pack = payload.get("evidence_pack") if isinstance(payload.get("evidence_pack"), dict) else None
payload["evidence_pack"] = evidence_pack or _default_evidence_pack(
answer=answer,
tool_trace=payload["tool_trace"],
)
payload["work_item_id"] = _safe_int(result.get("work_item_id"))
payload["ingress_status"] = str(result.get("status") or "").strip() or None
payload["intent"] = str(result.get("intent") or "").strip() or None
return payload