"""Adaptive RAG mixin module.""" from __future__ import annotations from app.schemas.project_operating_context import ExecutionRecipe, ProjectFitBrief, ProjectOperatingContext from .shared import * from .shared_utils import _env_bool from .internal_query_context import ( internal_context_block_reason, internal_context_operation_contract, internal_context_query_plan, ) class AdaptiveRAGAnswerPayloadMixin: _SOURCE_PRIORITY_MAP: dict[str, int] = { "pmflow": 100, "db": 90, "graph": 80, "doc": 70, "mcp_search": 68, "url": 65, "feishu": 60, "system": 10, } _INTERNAL_SOURCE_TYPES = {"db", "graph", "doc", "pmflow", "feishu"} _EXTERNAL_RETRIEVAL_STATUSES = { "evidence_found", "verifier_blocked", "no_relevant_results", "retrieval_failed", } _EXTERNAL_IMPACT_STATUSES = { "direct_match", "indirect_only", "no_direct_impact", } _ENV_ENABLE_DEFAULT_PLAN_HANDOFF_RENDERING = "PMFLOW_ENABLE_DEFAULT_PLAN_HANDOFF_RENDERING" @staticmethod def _normalize_project_operating_context(payload: Optional[dict[str, Any]]) -> dict[str, Any]: if not isinstance(payload, dict): return {} try: return ProjectOperatingContext.model_validate(payload).model_dump() except Exception: return {} @staticmethod def _normalize_execution_recipe(payload: Optional[dict[str, Any]]) -> dict[str, Any]: if not isinstance(payload, dict): return {} try: return ExecutionRecipe.model_validate(payload).model_dump() except Exception: return {} @staticmethod def _normalize_fit_brief(payload: Optional[dict[str, Any]]) -> dict[str, Any]: if not isinstance(payload, dict) or not payload: return {} try: return ProjectFitBrief.model_validate(payload).model_dump() except Exception: return {} @classmethod def _normalize_external_recommendation(cls, payload: Optional[dict[str, Any]]) -> dict[str, Any]: if not isinstance(payload, dict) or not payload: return {} retrieval_status = str(payload.get("retrieval_status") or "").strip().lower() impact_status = str(payload.get("impact_status") or "").strip().lower() if retrieval_status not in cls._EXTERNAL_RETRIEVAL_STATUSES: retrieval_status = "retrieval_failed" if impact_status not in cls._EXTERNAL_IMPACT_STATUSES: impact_status = "no_direct_impact" return { "retrieval_status": retrieval_status, "impact_status": impact_status, "external_summary": str(payload.get("external_summary") or "").strip(), "external_impact": str(payload.get("external_impact") or "").strip(), "affected_signal_sources": [ str(item).strip() for item in list(payload.get("affected_signal_sources") or []) if str(item).strip() ][:6], "external_action": str(payload.get("external_action") or "").strip(), } @classmethod def _normalize_external_anchor_text(cls, value: Any) -> str: raw = str(value or "").strip().lower() if not raw: return "" chars: list[str] = [] for char in raw: if char.isalnum() or "\u4e00" <= char <= "\u9fff": chars.append(char) return "".join(chars) @classmethod def _derive_external_retrieval_status(cls, packet: Optional[dict[str, Any]]) -> str: normalized = packet if isinstance(packet, dict) else {} explicit = str(normalized.get("retrieval_status") or "").strip().lower() if explicit in cls._EXTERNAL_RETRIEVAL_STATUSES: return explicit verifier = normalized.get("verifier") if isinstance(normalized.get("verifier"), dict) else {} verified_evidence = list(normalized.get("verified_evidence") or []) if verified_evidence and bool(verifier.get("ok")): return "evidence_found" if verified_evidence and not bool(verifier.get("ok")): return "verifier_blocked" attempts = [item for item in list(normalized.get("adapter_attempts") or []) if isinstance(item, dict)] if any(int(item.get("evidence_count") or 0) > 0 and not bool(item.get("verifier_ok")) for item in attempts): return "verifier_blocked" statuses = { str(item.get("status") or "").strip().lower() for item in attempts if str(item.get("status") or "").strip() } if any(status in {"success", "empty"} for status in statuses): return "no_relevant_results" return "retrieval_failed" @classmethod def _collect_fit_brief_signal_source_anchors(cls, fit_brief: Optional[dict[str, Any]]) -> list[str]: normalized_fit_brief = cls._normalize_fit_brief(fit_brief) anchors: list[str] = [] for item in list(normalized_fit_brief.get("signal_source_anchors") or []): normalized = str(item or "").strip() compact = cls._normalize_external_anchor_text(normalized) if len(compact) >= 2 and normalized not in anchors: anchors.append(normalized) return anchors[:6] @classmethod def _collect_external_evidence_texts(cls, packet: Optional[dict[str, Any]]) -> list[str]: normalized = packet if isinstance(packet, dict) else {} texts: list[str] = [] for item in list(normalized.get("verified_evidence") or []): if not isinstance(item, dict): continue for value in ( item.get("source_label"), item.get("title"), item.get("snippet"), item.get("target_url"), ): text = str(value or "").strip() if text: texts.append(text) return texts @classmethod def _build_external_recommendation( cls, *, external_context_packet: Optional[dict[str, Any]], fit_brief: Optional[dict[str, Any]], ) -> dict[str, Any]: packet = dict(external_context_packet or {}) if isinstance(external_context_packet, dict) else {} retrieval_status = cls._derive_external_retrieval_status(packet) if retrieval_status != "evidence_found": return { "retrieval_status": retrieval_status, "impact_status": "no_direct_impact", "external_summary": "", "external_impact": "", "affected_signal_sources": [], "external_action": "", } anchors = cls._collect_fit_brief_signal_source_anchors(fit_brief) evidence_texts = [cls._normalize_external_anchor_text(item) for item in cls._collect_external_evidence_texts(packet)] affected_signal_sources: list[str] = [] for anchor in anchors: compact_anchor = cls._normalize_external_anchor_text(anchor) if len(compact_anchor) < 2: continue if any(compact_anchor in text for text in evidence_texts if text): affected_signal_sources.append(anchor) first_evidence = next( (item for item in list(packet.get("verified_evidence") or []) if isinstance(item, dict)), {}, ) external_summary = str( first_evidence.get("snippet") or first_evidence.get("title") or first_evidence.get("source_label") or "" ).strip()[:240] if affected_signal_sources: return { "retrieval_status": "evidence_found", "impact_status": "direct_match", "external_summary": external_summary, "external_impact": f"外部证据已和当前项目对象建立直接映射:{'、'.join(affected_signal_sources[:3])}", "affected_signal_sources": affected_signal_sources[:6], "external_action": "先做映射验证,再决定是否把这条外部变化正式纳入当前项目计划。", } return { "retrieval_status": "evidence_found", "impact_status": "indirect_only", "external_summary": external_summary, "external_impact": "当前外部证据只支持背景相关性,暂不能直接映射到当前项目对象。", "affected_signal_sources": [], "external_action": "先做映射验证,确认这些外部变化是否真的影响当前项目。", } def _build_plan_handoff_fields( self, *, execution_recipe: Optional[dict[str, Any]], project_operating_context: Optional[dict[str, Any]], ) -> dict[str, Any]: normalized_recipe = self._normalize_execution_recipe(execution_recipe) normalized_context = self._normalize_project_operating_context(project_operating_context) handoff_mode = str(normalized_recipe.get("plan_handoff_mode") or "").strip().lower() or None if ( handoff_mode == "offer_after_answer" and not _env_bool(self._ENV_ENABLE_DEFAULT_PLAN_HANDOFF_RENDERING, True) ): handoff_mode = None can_enter_plan = handoff_mode in {"offer_after_answer", "explicit_plan_now"} recommended_next_step = None if handoff_mode == "offer_after_answer": if normalized_context.get("current_project_id") is not None: recommended_next_step = "如果你要,我可以基于当前结论进入 plan,继续把需求范围和下一步收清楚。" else: recommended_next_step = "如果你要,我可以基于当前结论继续进入 plan。" elif handoff_mode == "explicit_plan_now": recommended_next_step = "当前已命中明确的计划上下文,可以直接进入 plan 继续推进。" return { "handoff_mode": handoff_mode, "can_enter_plan": can_enter_plan, "recommended_next_step": recommended_next_step, } @staticmethod def _safe_source_metadata(raw: Any) -> dict[str, Any]: return raw if isinstance(raw, dict) else {} @staticmethod def _source_ref_value(value: Any) -> Any: if value is None: return None if isinstance(value, (str, int, float, bool)): text = str(value).strip() return text or None if isinstance(value, list): rows = [str(item).strip() for item in value if str(item).strip()] return rows or None if isinstance(value, dict): try: return json.loads(json.dumps(value, ensure_ascii=False)) except Exception: return dict(value) text = str(value).strip() return text or None @staticmethod def _isoformat_or_none(value: Any) -> Optional[str]: if value is None: return None if isinstance(value, datetime): try: return value.isoformat() except Exception: return None text = str(value).strip() return text or None def _normalize_source_type(self, value: Any) -> str: raw = str(self._source_ref_value(value) or "").strip().lower() return raw or "unknown" def _resolve_source_id( self, *, source_type: str, metadata: dict[str, Any], explicit_source_id: Any = None, ) -> Any: resolved = self._source_ref_value(explicit_source_id or metadata.get("source_id")) if resolved is not None: return resolved if source_type == "pmflow": record_type = self._source_ref_value(metadata.get("record_type")) record_id = self._source_ref_value(metadata.get("record_id")) if record_type and record_id is not None: return f"{record_type}:{record_id}" return record_id if source_type in {"doc", "url", "mcp_search"}: return self._source_ref_value( metadata.get("doc_id") or metadata.get("knowledge_doc_id") or metadata.get("doc_url") or metadata.get("file_url") or metadata.get("url") ) if source_type == "feishu": return self._source_ref_value( metadata.get("message_id") or metadata.get("attachment_id") or metadata.get("file_key") or metadata.get("key") or metadata.get("chat_id") ) if source_type == "graph": relation_id = self._source_ref_value(metadata.get("relation_id")) if relation_id is not None: return f"relation:{relation_id}" entity_id = self._source_ref_value(metadata.get("entity_id")) if entity_id is not None: return f"entity:{entity_id}" return self._source_ref_value( metadata.get("path") or metadata.get("source_ref") or metadata.get("subject_entity_id") ) if source_type == "db": table = self._source_ref_value(metadata.get("table")) if table is None: return None project_id = self._source_ref_value(metadata.get("project_id")) if project_id is not None: return f"{table}:{project_id}" return table return self._source_ref_value( metadata.get("doc_id") or metadata.get("knowledge_doc_id") or metadata.get("message_id") or metadata.get("url") or metadata.get("table") or metadata.get("matched_project_ids") ) def _resolve_chunk_id(self, metadata: dict[str, Any], *, explicit_chunk_id: Any = None) -> Any: return self._source_ref_value(explicit_chunk_id or metadata.get("chunk_id") or metadata.get("chunk_index")) def _resolve_target_url( self, *, source_type: str, metadata: dict[str, Any], explicit_target_url: Any = None, project_id: Any = None, ) -> Any: target_url = self._source_ref_value( explicit_target_url or metadata.get("target_url") or metadata.get("doc_url") or metadata.get("file_url") or metadata.get("url") or metadata.get("permalink") ) if target_url is not None: return target_url if source_type != "pmflow" or not hasattr(self, "_build_pmflow_target_url"): return None try: return self._source_ref_value( self._build_pmflow_target_url( record_type=str(metadata.get("record_type") or ""), record_id=metadata.get("record_id"), project_id=project_id if project_id is not None else metadata.get("project_id"), ) ) except Exception: return None def _resolve_updated_at(self, metadata: dict[str, Any], *, explicit_updated_at: Any = None) -> Any: return self._source_ref_value( self._isoformat_or_none(explicit_updated_at) or metadata.get("updated_at") or metadata.get("doc_updated_at") or metadata.get("source_updated_at") or metadata.get("captured_at") or metadata.get("last_seen_at") or metadata.get("last_fetched_at") ) def _resolve_permission_scope(self, metadata: dict[str, Any], *, project_id: Any = None) -> str: explicit_scope = str(self._source_ref_value(metadata.get("permission_scope")) or "").strip().lower() if explicit_scope in {"project", "tenant"}: return explicit_scope resolved_project_id = self._source_ref_value(project_id if project_id is not None else metadata.get("project_id")) return "project" if resolved_project_id is not None else "tenant" def _resolve_source_priority(self, metadata: dict[str, Any], *, source_type: str) -> int: explicit_priority = metadata.get("source_priority") if explicit_priority is not None: try: return int(explicit_priority) except Exception: pass return int(self._SOURCE_PRIORITY_MAP.get(source_type, 50)) def _normalize_source_registry_metadata( self, *, source_type: Any, metadata: Any, explicit_source_id: Any = None, explicit_chunk_id: Any = None, explicit_target_url: Any = None, explicit_project_id: Any = None, explicit_updated_at: Any = None, ) -> dict[str, Any]: normalized = self._safe_source_metadata(metadata).copy() resolved_source_type = self._normalize_source_type(source_type or normalized.get("source_type")) resolved_project_id = self._source_ref_value( explicit_project_id if explicit_project_id is not None else normalized.get("project_id") ) normalized["source_type"] = resolved_source_type normalized["source_id"] = self._resolve_source_id( source_type=resolved_source_type, metadata=normalized, explicit_source_id=explicit_source_id, ) normalized["chunk_id"] = self._resolve_chunk_id(normalized, explicit_chunk_id=explicit_chunk_id) normalized["target_url"] = self._resolve_target_url( source_type=resolved_source_type, metadata=normalized, explicit_target_url=explicit_target_url, project_id=resolved_project_id, ) normalized["project_id"] = resolved_project_id normalized["updated_at"] = self._resolve_updated_at(normalized, explicit_updated_at=explicit_updated_at) normalized["permission_scope"] = self._resolve_permission_scope(normalized, project_id=resolved_project_id) normalized["source_priority"] = self._resolve_source_priority(normalized, source_type=resolved_source_type) return normalized def _source_priority_sort_value(self, metadata: Any) -> int: normalized = self._safe_source_metadata(metadata) source_type = self._normalize_source_type(normalized.get("source_type")) return self._resolve_source_priority(normalized, source_type=source_type) @staticmethod def _normalize_answer_mode(value: Optional[str]) -> str: raw = str(value or "").strip().lower() if raw in { ANSWER_MODE_FAST_PATH, ANSWER_MODE_RETRIEVAL_NORMAL, ANSWER_MODE_RETRIEVAL_DEEP, ANSWER_MODE_CLARIFY, ANSWER_MODE_BLOCKED, }: return raw return ANSWER_MODE_RETRIEVAL_NORMAL def _normalize_grounding_status( self, value: Optional[str], *, sources: Optional[list[dict[str, Any]]] = None, missing_fields: Optional[list[str]] = None, answer_mode: Optional[str] = None, reason_code: Optional[str] = None, ) -> str: raw = str(value or "").strip().lower() if raw in { GROUNDING_STATUS_GROUNDED, GROUNDING_STATUS_PARTIALLY_GROUNDED, GROUNDING_STATUS_INSUFFICIENT_EVIDENCE, GROUNDING_STATUS_BLOCKED, }: return raw normalized_mode = self._normalize_answer_mode(answer_mode) if normalized_mode == ANSWER_MODE_BLOCKED: return GROUNDING_STATUS_BLOCKED if normalized_mode == ANSWER_MODE_CLARIFY: return GROUNDING_STATUS_INSUFFICIENT_EVIDENCE source_rows = sources if isinstance(sources, list) else [] snapshot_only = False if hasattr(self, "_is_snapshot_only_sources"): try: snapshot_only = bool(self._is_snapshot_only_sources(source_rows)) except Exception: snapshot_only = False if not snapshot_only and source_rows: snapshot_only = all(str(item.get("source") or "").strip().lower() == "system" for item in source_rows if isinstance(item, dict)) missing = [str(item).strip() for item in (missing_fields or []) if str(item).strip()] normalized_reason = str(reason_code or "").strip().lower() if normalized_reason == "insufficient_evidence": return GROUNDING_STATUS_INSUFFICIENT_EVIDENCE if snapshot_only or missing: return GROUNDING_STATUS_PARTIALLY_GROUNDED if source_rows: return GROUNDING_STATUS_GROUNDED return GROUNDING_STATUS_PARTIALLY_GROUNDED def _build_citations_from_sources(self, sources: list[dict[str, Any]]) -> list[dict[str, Any]]: normalized: list[dict[str, Any]] = [] seen: set[tuple[str, str, str, str]] = set() for item in (sources or [])[:8]: if not isinstance(item, dict): continue metadata = self._normalize_source_registry_metadata( source_type=item.get("source"), metadata=item.get("metadata"), ) source_type = self._source_ref_value(metadata.get("source_type")) source_label = self._source_ref_value(item.get("label") or metadata.get("source_label")) source_id = self._source_ref_value(metadata.get("source_id")) chunk_id = self._source_ref_value(metadata.get("chunk_id")) target_url = self._source_ref_value(metadata.get("target_url")) project_id = self._source_ref_value(metadata.get("project_id")) updated_at = self._source_ref_value(metadata.get("updated_at")) citation = { "source_type": source_type, "source_label": source_label, "source_id": source_id, "chunk_id": chunk_id, "target_url": target_url, "project_id": project_id, "updated_at": updated_at, "permission_scope": self._source_ref_value(metadata.get("permission_scope")), "source_priority": metadata.get("source_priority"), "confidence": round(self._to_float(item.get("score"), 0.0), 4), } key = ( str(source_type or ""), str(source_id or ""), str(chunk_id or ""), str(target_url or ""), ) if key in seen: continue seen.add(key) normalized.append(citation) return normalized @staticmethod def _build_sources_used_from_citations(citations: list[dict[str, Any]]) -> list[dict[str, Any]]: rows: list[dict[str, Any]] = [] for item in citations: if not isinstance(item, dict): continue rows.append( { "source_type": item.get("source_type"), "source_label": item.get("source_label"), "source_id": item.get("source_id"), "chunk_id": item.get("chunk_id"), "target_url": item.get("target_url"), "project_id": item.get("project_id"), "updated_at": item.get("updated_at"), "permission_scope": item.get("permission_scope"), "source_priority": item.get("source_priority"), } ) return rows def _build_evidence_pack( self, *, answer: str, citations: list[dict[str, Any]], tool_trace: list[dict[str, Any]], missing_fields: Optional[list[str]] = None, grounding_status: Optional[str] = None, reason_code: Optional[str] = None, ) -> dict[str, Any]: evidence_labels: list[str] = [] for item in citations[:5]: if not isinstance(item, dict): continue label = str(item.get("source_label") or item.get("source_id") or item.get("source_type") or "").strip() if label: evidence_labels.append(label) uncertainties = [str(item).strip() for item in (missing_fields or []) if str(item).strip()] normalized_grounding = str(grounding_status or "").strip().lower() normalized_reason = str(reason_code or "").strip() if normalized_grounding in {GROUNDING_STATUS_INSUFFICIENT_EVIDENCE, GROUNDING_STATUS_BLOCKED} and normalized_reason: if normalized_reason not in uncertainties: uncertainties.append(normalized_reason) return { "summary": { "conclusion": str(answer or "").strip(), "evidence": evidence_labels, "uncertainties": uncertainties, }, "tool_trace": tool_trace if isinstance(tool_trace, list) else [], } def _is_internal_answer_contract_route( self, *, route_decision: Optional[dict[str, Any]] = None, sources: Optional[list[dict[str, Any]]] = None, ) -> bool: normalized_route = ( self._normalize_query_route_decision(route_decision) if hasattr(self, "_normalize_query_route_decision") else {} ) if str(normalized_route.get("route_mode") or "").strip().lower() == "internal": return True if str(normalized_route.get("effective_source_policy") or "").strip().lower() == "internal": return True for item in sources or []: if not isinstance(item, dict): continue if str(item.get("source") or "").strip().lower() in self._INTERNAL_SOURCE_TYPES: return True return False def _verify_external_answer_contract( self, *, answer: str, sources: Optional[list[dict[str, Any]]] = None, route_decision: Optional[dict[str, Any]] = None, ) -> dict[str, Any]: normalized_route = ( self._normalize_query_route_decision(route_decision) if hasattr(self, "_normalize_query_route_decision") else {} ) if str(normalized_route.get("effective_source_policy") or "").strip().lower() != "external": return {"ok": True, "reason_code": None, "missing_fields": []} source_rows = sources if isinstance(sources, list) else [] if not source_rows: return { "ok": False, "reason_code": "missing_external_evidence", "missing_fields": ["external_evidence"], } external_source_seen = False for item in source_rows: if not isinstance(item, dict): continue source = str(item.get("source") or "").strip().lower() metadata = item.get("metadata") if isinstance(item.get("metadata"), dict) else {} if source == "system": continue if source not in {"url", "mcp_search"}: return { "ok": False, "reason_code": "external_source_contract_violation", "missing_fields": ["external_source_type"], } target_url = str( metadata.get("target_url") or metadata.get("file_url") or metadata.get("url") or "" ).strip().lower() if not target_url.startswith("http://") and not target_url.startswith("https://"): return { "ok": False, "reason_code": "external_target_url_missing", "missing_fields": ["target_url"], } external_source_seen = True if not external_source_seen: return { "ok": False, "reason_code": "missing_external_grounding", "missing_fields": ["external_evidence"], } _ = answer return {"ok": True, "reason_code": None, "missing_fields": []} def _build_external_answer_contract_guard_payload( self, *, reason_code: Optional[str], missing_fields: Optional[list[str]] = None, ) -> dict[str, Any]: normalized_reason = str(reason_code or "").strip().lower() or "external_answer_contract_violation" normalized_missing = [str(item).strip() for item in (missing_fields or []) if str(item).strip()] return { "answer": "当前没有拿到可核验的外部实时证据,已阻止直接返回外部结论。请稍后重试,或把要核验的公开对象说得更具体一些。", "sources": [ { "source": "system", "label": "External Query Contract Guard", "score": 0.99, "metadata": { "mode": "guard", "reason": normalized_reason, }, } ], "missing_fields": normalized_missing or ["external_answer_contract"], "reason_code": normalized_reason, "answer_mode": ANSWER_MODE_CLARIFY, "grounding_status": GROUNDING_STATUS_INSUFFICIENT_EVIDENCE, } def _verify_internal_answer_contract( self, *, query_type: str, answer: str, sources: Optional[list[dict[str, Any]]] = None, route_decision: Optional[dict[str, Any]] = None, internal_context_packet: Any = None, ) -> dict[str, Any]: source_rows = sources if isinstance(sources, list) else [] if not self._is_internal_answer_contract_route(route_decision=route_decision, sources=source_rows): return {"ok": True, "reason_code": None, "missing_fields": []} packet_block = internal_context_block_reason( internal_context_packet, route_decision=route_decision, ) if packet_block is not None: reason_code, missing_fields = packet_block return { "ok": False, "reason_code": reason_code, "missing_fields": missing_fields, } operation_contract = internal_context_operation_contract(packet=internal_context_packet) query_plan = internal_context_query_plan(packet=internal_context_packet) normalized_query_type = str( operation_contract.get("query_type") or query_plan.get("query_type") or query_type or QUERY_TYPE_UNKNOWN ).strip().lower() normalized_answer = str(answer or "").strip() compact_answer = re.sub(r"\s+", "", normalized_answer.lower()) if normalized_query_type == QUERY_TYPE_EXISTENCE: if "项目总数" in normalized_answer or "总数为" in normalized_answer: return { "ok": False, "reason_code": "no_total_count_substitution", "missing_fields": ["matched_evidence", "existence_contract"], } if ( "总数" in compact_answer and "匹配到" not in normalized_answer and "未检索到" not in normalized_answer and "任务总数" not in normalized_answer and "信号总数" not in normalized_answer ): return { "ok": False, "reason_code": "no_total_count_substitution", "missing_fields": ["matched_evidence", "existence_contract"], } if not source_rows: return { "ok": False, "reason_code": "missing_internal_evidence", "missing_fields": ["internal_evidence"], } return {"ok": True, "reason_code": None, "missing_fields": []} def _build_internal_answer_contract_guard_payload( self, *, query_type: str, reason_code: Optional[str], missing_fields: Optional[list[str]] = None, ) -> dict[str, Any]: normalized_query_type = str(query_type or QUERY_TYPE_UNKNOWN).strip().lower() normalized_reason = str(reason_code or "").strip().lower() or "internal_answer_contract_violation" normalized_missing = [str(item).strip() for item in (missing_fields or []) if str(item).strip()] if normalized_reason == "no_total_count_substitution" and normalized_query_type == QUERY_TYPE_EXISTENCE: answer = "当前内部查询结果与问题类型不一致,已阻止直接返回。你问的是“有没有”,请给我更明确的对象条件后我按存在性重新查询。" elif normalized_reason == "missing_internal_evidence": answer = "当前内部查询证据不足,已阻止直接返回结论。请补充更明确的对象或范围后我再查。" elif normalized_reason == "missing_internal_context_packet": answer = "当前内部查询上下文未完成,已停止直接回答。请稍后重试,或补充更明确的对象或范围后我再查。" elif normalized_reason == "missing_internal_operation_contract": answer = "当前内部查询合同不完整,已停止直接回答。请补充更明确的对象或范围后我再查。" elif normalized_reason == "target_resolution_clarify": answer = "当前内部查询对象仍有歧义,已停止直接回答。请先明确你要查的项目、配置或对象后我再继续。" elif normalized_reason == "missing_internal_query_plan_contract": answer = "当前内部查询计划不完整,已停止直接回答。请补充更明确的对象或范围后我再查。" else: answer = "当前内部查询结果未通过回答合同校验,已阻止直接返回。请补充更明确的对象或范围后我再查。" answer_mode = ANSWER_MODE_BLOCKED grounding_status = GROUNDING_STATUS_BLOCKED if normalized_reason in { "missing_internal_context_packet", "missing_internal_operation_contract", "target_resolution_clarify", "missing_internal_query_plan_contract", }: answer_mode = ANSWER_MODE_CLARIFY grounding_status = GROUNDING_STATUS_INSUFFICIENT_EVIDENCE return { "answer": answer, "sources": [ { "source": "system", "label": "Internal Query Contract Guard", "score": 0.99, "metadata": { "mode": "guard", "reason": normalized_reason, "query_type": normalized_query_type, }, } ], "missing_fields": normalized_missing or ["internal_answer_contract"], "reason_code": normalized_reason, "answer_mode": answer_mode, "grounding_status": grounding_status, } def _build_response_payload( self, *, answer: str, sources: list[dict[str, Any]], domains_used: list[str], response_time_ms: int, db_query_ms: int, vector_search_ms: int, graph_query_ms: int = 0, db_timeout: bool = False, vector_timeout: bool = False, graph_timeout: bool = False, qa_id: Optional[int] = None, session_id: Optional[int] = None, turn_index: int = 1, relevance_max: float = 0.0, query_type: str = QUERY_TYPE_UNKNOWN, confidence: float = 0.0, missing_fields: Optional[list[str]] = None, tool_trace: Optional[list[dict[str, Any]]] = None, thinking_mode: str = THINKING_MODE_NORMAL, action_taken: Optional[str] = None, signal_id: Optional[int] = None, reply_required: bool = False, reply_text: Optional[str] = None, ack_latency_ms: Optional[int] = None, trace_id: Optional[str] = None, processing_stage: Optional[str] = None, clarification_code: Optional[str] = None, clarification_candidates: Optional[list[dict[str, Any]]] = None, anchor_mode: Optional[str] = None, anchor_kind: Optional[str] = None, meta_followup_kind: Optional[str] = None, reason_code: Optional[str] = None, answer_mode: Optional[str] = None, grounding_status: Optional[str] = None, policy_version: Optional[str] = None, citations: Optional[list[dict[str, Any]]] = None, sources_used: Optional[list[dict[str, Any]]] = None, evidence_pack: Optional[dict[str, Any]] = None, prompt_variant_id: Optional[str] = None, route_decision: Optional[dict[str, Any]] = None, project_operating_context: Optional[dict[str, Any]] = None, execution_recipe: Optional[dict[str, Any]] = None, fit_brief: Optional[dict[str, Any]] = None, external_recommendation: Optional[dict[str, Any]] = None, ) -> dict[str, Any]: evidence: list[dict[str, Any]] = [] for item in sources[:8]: if not isinstance(item, dict): continue evidence.append( { "source": str(item.get("source") or "unknown"), "label": str(item.get("label") or "unknown"), "score": round(self._to_float(item.get("score"), 0.0), 4), "metadata": item.get("metadata") if isinstance(item.get("metadata"), dict) else {}, } ) normalized_tool_trace = tool_trace if isinstance(tool_trace, list) else [] normalized_citations = citations if isinstance(citations, list) else self._build_citations_from_sources(sources) normalized_sources_used = ( sources_used if isinstance(sources_used, list) else self._build_sources_used_from_citations(normalized_citations) ) normalized_answer_mode = self._normalize_answer_mode(answer_mode) normalized_grounding_status = self._normalize_grounding_status( grounding_status, sources=sources, missing_fields=missing_fields, answer_mode=normalized_answer_mode, reason_code=reason_code, ) normalized_policy_version = str( policy_version or self._build_policy_version(prompt_variant_id=prompt_variant_id) ).strip() or self._build_policy_version(prompt_variant_id=prompt_variant_id) normalized_evidence_pack = ( evidence_pack if isinstance(evidence_pack, dict) else self._build_evidence_pack( answer=answer, citations=normalized_citations, tool_trace=normalized_tool_trace, missing_fields=missing_fields, grounding_status=normalized_grounding_status, reason_code=reason_code, ) ) normalized_route_decision = ( self._normalize_query_route_decision(route_decision) if hasattr(self, "_normalize_query_route_decision") else {} ) normalized_project_operating_context = self._normalize_project_operating_context( project_operating_context ) normalized_execution_recipe = self._normalize_execution_recipe(execution_recipe) normalized_fit_brief = self._normalize_fit_brief(fit_brief) normalized_external_recommendation = self._normalize_external_recommendation(external_recommendation) handoff_fields = self._build_plan_handoff_fields( execution_recipe=normalized_execution_recipe, project_operating_context=normalized_project_operating_context, ) return { "answer": str(answer or "").strip(), "evidence": evidence, "confidence": round(max(0.0, min(1.0, self._to_float(confidence, 0.0))), 4), "thinking_mode": self._normalize_thinking_mode(thinking_mode), "missing_fields": [str(x) for x in (missing_fields or []) if str(x).strip()], "query_type": str(query_type or QUERY_TYPE_UNKNOWN), "sources": sources, "domains_used": domains_used, "response_time_ms": int(response_time_ms or 0), "retrieval_metrics": { "db_query_ms": int(db_query_ms or 0), "vector_search_ms": int(vector_search_ms or 0), "graph_query_ms": int(graph_query_ms or 0), "db_timeout": bool(db_timeout), "vector_timeout": bool(vector_timeout), "graph_timeout": bool(graph_timeout), }, "tool_trace": normalized_tool_trace, "qa_id": int(qa_id) if qa_id is not None else None, "session_id": int(session_id) if session_id is not None else None, "turn_index": int(turn_index or 1), "relevance_max": round(max(0.0, self._to_float(relevance_max, 0.0)), 4), "action_taken": str(action_taken).strip() if action_taken else None, "signal_id": int(signal_id) if signal_id is not None else None, "reply_required": bool(reply_required), "reply_text": str(reply_text or "").strip() if reply_text else None, "ack_latency_ms": int(ack_latency_ms) if ack_latency_ms is not None else None, "trace_id": str(trace_id or "").strip() or None, "processing_stage": str(processing_stage or "").strip() or None, "clarification_code": str(clarification_code or "").strip() or None, "clarification_candidates": ( clarification_candidates[:3] if isinstance(clarification_candidates, list) else [] ), "anchor_mode": str(anchor_mode or "").strip() or None, "anchor_kind": str(anchor_kind or "").strip() or None, "meta_followup_kind": str(meta_followup_kind or "").strip() or None, "reason_code": str(reason_code or "").strip() or None, "answer_mode": normalized_answer_mode, "grounding_status": normalized_grounding_status, "policy_version": normalized_policy_version, "citations": normalized_citations, "sources_used": normalized_sources_used, "evidence_pack": normalized_evidence_pack, "route_decision": normalized_route_decision, "project_operating_context": normalized_project_operating_context, "execution_recipe": normalized_execution_recipe, "fit_brief": normalized_fit_brief, "external_recommendation": normalized_external_recommendation, "handoff_mode": handoff_fields["handoff_mode"], "can_enter_plan": handoff_fields["can_enter_plan"], "recommended_next_step": handoff_fields["recommended_next_step"], } @staticmethod def _extract_ack_metadata_from_sources( sources: Any, *, answer: str = "", ) -> dict[str, Any]: rows = sources if isinstance(sources, list) else [] action_taken = "" signal_id: Optional[int] = None reply_required = False ack_latency_ms: Optional[int] = None trace_id = "" processing_stage = "" for source in rows: if not isinstance(source, dict): continue meta = source.get("metadata") if isinstance(source.get("metadata"), dict) else {} candidate_action = str(meta.get("action_taken") or "").strip() if not candidate_action: continue action_taken = candidate_action try: signal_id = int(meta.get("signal_id")) if meta.get("signal_id") is not None else None except Exception: signal_id = None trace_id = str(meta.get("trace_id") or "").strip() processing_stage = str(meta.get("processing_stage") or meta.get("action_mode") or "").strip() try: ack_latency_ms = ( int(meta.get("ack_latency_ms")) if meta.get("ack_latency_ms") is not None else None ) except Exception: ack_latency_ms = None reply_required = True break reply_text = str(answer or "").strip() if reply_required else "" normalized_stage = processing_stage if normalized_stage == "write": normalized_stage = "ack_only" return { "action_taken": action_taken or None, "signal_id": signal_id, "reply_required": bool(reply_required), "reply_text": reply_text or None, "ack_latency_ms": ack_latency_ms, "trace_id": trace_id or None, "processing_stage": normalized_stage or None, }