146 lines
5.9 KiB
Python
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
|