"""Adaptive RAG answer mainline orchestration helpers.""" from __future__ import annotations from .shared import * class AdaptiveRAGAnswerMainlineMixin: _EXTERNAL_FACT_FRESHNESS_MARKERS = ( "最新", "目前", "当前", "截至现在", "截至当前", "截止当前", "今天", "现在", "最近", "出到什么", "发布了吗", "发布到", "版本", "最高模型", ) _MODEL_ENTITY_MARKERS = ( "codex", "gpt", "chatgpt", "claude", "deepseek", "qwen", "gemini", "copilot", "grok", "kimi", "llama", "openai", "anthropic", ) _INTERNAL_MODEL_CONFIG_MARKERS = ( "ai-config", "/ai-config", "模型配置", "配置项", "配置值", "路由规则", "初始化参数", "llm服务", "llm 服务", "运行时", "当前系统", "我们系统", "pmflow", "本系统", "接入的模型", "接的是", "用的是哪个模型配置", ) _PROJECT_CONTEXT_POSITIVE_MARKERS = ( "这个项目", "当前项目", "本项目", "我们项目", "项目所在行业", "对项目的影响", "对这个项目的影响", "对当前项目的影响", "对我们项目的影响", "结合项目", "结合当前项目", "落到项目", ) _PROJECT_CONTEXT_NEGATIVE_MARKERS = ( "不要提项目", "别提项目", "不要结合项目", "不用结合项目", "不需要结合项目", "先别结合项目", "不要绑定项目", "别绑定项目", ) @classmethod def _extract_prompt_variant_from_policy_version(cls, policy_version: Optional[str]) -> Optional[str]: raw = str(policy_version or "").strip() if not raw: return None for part in raw.split("|"): normalized = str(part or "").strip() if not normalized.startswith("prompt:"): continue value = normalized.split(":", 1)[1].strip().lower() return value or None return None @classmethod def _question_explicitly_requests_project_context( cls, *, question: str, project_id: Optional[int], ) -> bool: if project_id is None: return False normalized = re.sub(r"\s+", "", str(question or "")).strip().lower() if not normalized: return False if any(marker in normalized for marker in cls._PROJECT_CONTEXT_NEGATIVE_MARKERS): return False return any(marker in normalized for marker in cls._PROJECT_CONTEXT_POSITIVE_MARKERS) @classmethod def _looks_like_external_freshness_question(cls, question: str) -> bool: normalized = re.sub(r"\s+", "", str(question or "")).strip().lower() if not normalized: return False return any(marker in normalized for marker in cls._EXTERNAL_FACT_FRESHNESS_MARKERS) @classmethod def _looks_like_internal_model_config_question(cls, question: str) -> bool: normalized = re.sub(r"\s+", "", str(question or "")).strip().lower() if not normalized: return False return any(marker in normalized for marker in cls._INTERNAL_MODEL_CONFIG_MARKERS) @classmethod def _looks_like_model_fact_question(cls, question: str) -> bool: normalized = re.sub(r"\s+", "", str(question or "")).strip().lower() if not normalized: return False has_entity = any(marker in normalized for marker in cls._MODEL_ENTITY_MARKERS) if not has_entity: return False return "模型" in normalized or cls._looks_like_external_freshness_question(question) async def _resolve_anchor_prompt_variant( self, *, anchor_resolution: Optional[FollowupAnchorResolution], conversation_scope_key: Optional[str], ) -> Optional[str]: resolver = getattr(self, "_resolve_anchor_qa_record", None) if not callable(resolver): return None try: anchor_row = await resolver( anchor_resolution=anchor_resolution, conversation_scope_key=conversation_scope_key, ) except Exception: return None if anchor_row is None: return None return self._extract_prompt_variant_from_policy_version(getattr(anchor_row, "policy_version", None)) def _apply_chat_external_fact_scene_policy( self, *, question: str, project_id: Optional[int], scene_semantics: Optional[dict[str, Any]], anchor_prompt_variant: Optional[str] = None, ) -> dict[str, Any]: normalized = dict(scene_semantics or {}) scene_key = str(normalized.get("scene_key") or "default").strip().lower() or "default" anchor_variant = str(anchor_prompt_variant or "").strip().lower() explicit_project_context = self._question_explicitly_requests_project_context( question=question, project_id=project_id, ) requires_freshness = self._looks_like_external_freshness_question(question) internal_model_config_question = self._looks_like_internal_model_config_question(question) model_fact_question = self._looks_like_model_fact_question(question) anchored_trend_followup = anchor_variant in {"external_trend", "industry_trend"} if scene_key == "ai_pmo" and model_fact_question and requires_freshness and not internal_model_config_question: scene_key = "external_trend" if explicit_project_context else "industry_trend" normalized["snapshot_focus"] = "none" normalized["deep_intent"] = "none" normalized["external_fact_chat"] = True normalized["requires_freshness"] = True if scene_key == "ai_pmo_correction" and anchored_trend_followup: scene_key = "external_trend" if explicit_project_context else "industry_trend" normalized["snapshot_focus"] = "none" normalized["deep_intent"] = "none" normalized["external_fact_chat"] = True if scene_key == "external_trend" and not explicit_project_context: scene_key = "industry_trend" if scene_key in {"external_trend", "industry_trend"}: normalized["external_fact_chat"] = True normalized["requires_freshness"] = bool(requires_freshness) if requires_freshness: normalized["deep_intent"] = "none" normalized["scene_key"] = scene_key return normalized def _plan_query_runtime( self, *, question: str, effective_question: str, query_type: str, scene_semantics: Optional[dict[str, Any]] = None, requested_thinking_mode: str, enforce_scene_prompt_only: bool, consultant_mode_preference: Optional[str] = None, ) -> QueryPlannerDecision: normalize_thinking_mode = getattr( self, "_normalize_thinking_mode", lambda value: THINKING_MODE_DEEP if str(value or "").strip().lower() == THINKING_MODE_DEEP else THINKING_MODE_NORMAL, ) normalized_thinking_mode = normalize_thinking_mode(requested_thinking_mode) normalized_query_type = str(query_type or QUERY_TYPE_UNKNOWN).strip().lower() normalized_consultant_mode = str(consultant_mode_preference or "").strip().lower() fast_path_candidate = normalized_query_type in {QUERY_TYPE_COUNT, QUERY_TYPE_LIST, QUERY_TYPE_STATUS} normalized_question = str(question or "").strip() normalized_effective = str(effective_question or "").strip() normalized_scene_semantics = scene_semantics if isinstance(scene_semantics, dict) else {} deep_intent = str(normalized_scene_semantics.get("deep_intent") or "none").strip().lower() force_deep_by_semantics = deep_intent in { "correction_turn", "correction_escalation", "engine_process_query", "runtime_state_query", } if enforce_scene_prompt_only: retrieval_mode = ANSWER_MODE_RETRIEVAL_DEEP if normalized_thinking_mode == THINKING_MODE_DEEP else ANSWER_MODE_RETRIEVAL_NORMAL return QueryPlannerDecision( answer_mode=retrieval_mode, thinking_mode=THINKING_MODE_DEEP if retrieval_mode == ANSWER_MODE_RETRIEVAL_DEEP else THINKING_MODE_NORMAL, allow_fast_path=False, retrieval_answer_mode=retrieval_mode, budget=RetrievalBudget( max_steps=DEEP_RETRIEVAL_STEPS if retrieval_mode == ANSWER_MODE_RETRIEVAL_DEEP else AGENT_MAX_RETRIEVAL_STEPS, vector_top_k=DEEP_VECTOR_TOP_K if retrieval_mode == ANSWER_MODE_RETRIEVAL_DEEP else DEFAULT_TOP_K, ), reason="scene_prompt_only", ) if fast_path_candidate and not force_deep_by_semantics: if normalized_consultant_mode: return QueryPlannerDecision( answer_mode=ANSWER_MODE_RETRIEVAL_NORMAL, thinking_mode=THINKING_MODE_NORMAL, allow_fast_path=False, retrieval_answer_mode=ANSWER_MODE_RETRIEVAL_NORMAL, budget=RetrievalBudget(max_steps=AGENT_MAX_RETRIEVAL_STEPS, vector_top_k=DEFAULT_TOP_K), reason=f"consultant_mode:{normalized_consultant_mode}", ) return QueryPlannerDecision( answer_mode=ANSWER_MODE_FAST_PATH, thinking_mode=THINKING_MODE_NORMAL, allow_fast_path=True, retrieval_answer_mode=ANSWER_MODE_RETRIEVAL_NORMAL, budget=RetrievalBudget(max_steps=AGENT_MAX_RETRIEVAL_STEPS, vector_top_k=DEFAULT_TOP_K), reason="deterministic_query_type", ) retrieval_mode = ANSWER_MODE_RETRIEVAL_DEEP if (normalized_thinking_mode == THINKING_MODE_DEEP or force_deep_by_semantics) else ANSWER_MODE_RETRIEVAL_NORMAL return QueryPlannerDecision( answer_mode=retrieval_mode, thinking_mode=THINKING_MODE_DEEP if retrieval_mode == ANSWER_MODE_RETRIEVAL_DEEP else THINKING_MODE_NORMAL, allow_fast_path=False, retrieval_answer_mode=retrieval_mode, budget=RetrievalBudget( max_steps=DEEP_RETRIEVAL_STEPS if retrieval_mode == ANSWER_MODE_RETRIEVAL_DEEP else AGENT_MAX_RETRIEVAL_STEPS, vector_top_k=DEEP_VECTOR_TOP_K if retrieval_mode == ANSWER_MODE_RETRIEVAL_DEEP else DEFAULT_TOP_K, ), reason="requested_thinking_mode" if retrieval_mode == ANSWER_MODE_RETRIEVAL_DEEP else "default_retrieval", ) async def _prepare_answer_runtime( self, *, clean_q: str, asked_by: Optional[int], asked_via: str, project_id: Optional[int], source_chat_id: Optional[str], session_id: Optional[int], conversation_key: Optional[str], force_new_session: bool, action_mode: str, source_message_id: Optional[str] = None, source_sender_open_id: Optional[str] = None, thread_context: Optional[dict[str, Any]] = None, ) -> dict[str, Any]: external_conversation_key = self._build_external_conversation_key( asked_via=asked_via, source_chat_id=source_chat_id, conversation_key=conversation_key, source_sender_open_id=source_sender_open_id, sender=None, thread_context=thread_context, ) self._set_active_external_qa_context( source_chat_id=source_chat_id, source_message_id=source_message_id, source_sender_open_id=source_sender_open_id, conversation_scope_key=external_conversation_key, thread_context=thread_context, ) session_id, turn_index = await self._touch_session_for_new_record( asked_by=asked_by, asked_via=asked_via, project_id=project_id, question=clean_q, preferred_session_id=session_id, force_new_session=bool(force_new_session), external_conversation_key=external_conversation_key, ) context_session_id: Optional[int] if session_id is not None: context_session_id = int(session_id) elif asked_by is None: context_session_id = self._external_session_id_from_key(external_conversation_key) else: context_session_id = None conversation = await self._load_recent_conversation( asked_by=asked_by, asked_via=asked_via, project_id=project_id, session_id=session_id, external_session_id=context_session_id, limit=10, ) state_session_id = int(session_id) if session_id is not None else ( int(context_session_id) if context_session_id is not None else None ) state_row = await self._ensure_session_state(session_id=state_session_id) pending_selection = self._consume_pending_clarification_selection( question=clean_q, session_state=state_row, ) resolved_question = str(pending_selection.get("effective_question") or clean_q).strip() if isinstance( pending_selection, dict ) else clean_q anchor_resolution = await self._resolve_followup_anchor( question=resolved_question, session_id=state_session_id, context_session_id=context_session_id, session_state=state_row, conversation_scope_key=external_conversation_key, source_chat_id=source_chat_id, source_sender_open_id=source_sender_open_id, source_message_id=source_message_id, thread_context=thread_context, ) effective_q, rewrite_meta = self._rewrite_question_with_state( question=resolved_question, conversation=conversation, session_state=state_row, anchor_resolution=anchor_resolution, ) query_semantics = await self._resolve_query_semantics( message=effective_q, source_system=asked_via, metadata={ "project_id": project_id, "source_chat_id": source_chat_id, "source_message_id": source_message_id, "conversation_scope_key": external_conversation_key, "reply_to_message_id": str((thread_context or {}).get("reply_to_message_id") or "").strip() or None, "root_message_id": str((thread_context or {}).get("root_message_id") or "").strip() or None, }, ) query_semantics = dict(query_semantics) if isinstance(query_semantics, dict) else {} query_type = str(query_semantics.get("query_type") or QUERY_TYPE_UNKNOWN).strip().lower() or QUERY_TYPE_UNKNOWN conversation_text = "\n".join( [ f"Q: {str(row.get('question') or '').strip()}\nA: {str(row.get('answer') or '').strip()}" for row in conversation[-4:] if isinstance(row, dict) ] ) or "无" scene_semantics = await self._resolve_answer_scene_semantics( question=clean_q, effective_question=effective_q, asked_via=asked_via, message_intent=str(query_semantics.get("message_intent") or query_semantics.get("intent") or "").strip().lower() or None, query_type=query_type, conversation_text=conversation_text, ) anchor_prompt_variant = await self._resolve_anchor_prompt_variant( anchor_resolution=anchor_resolution, conversation_scope_key=external_conversation_key, ) scene_semantics = self._apply_chat_external_fact_scene_policy( question=resolved_question, project_id=project_id, scene_semantics=scene_semantics, anchor_prompt_variant=anchor_prompt_variant, ) if bool(scene_semantics.get("requires_freshness")): query_semantics["external_freshness_only"] = True query_semantics["retrieval_domains"] = ["url"] requested_action_mode = self._normalize_action_mode(action_mode) effective_action_mode = await self._resolve_action_mode_async( requested_mode=requested_action_mode, question=resolved_question, effective_question=effective_q, asked_via=asked_via, ) return { "session_id": session_id, "turn_index": turn_index, "context_session_id": context_session_id, "conversation": conversation, "state_row": state_row, "resolved_question": resolved_question, "effective_q": effective_q, "rewrite_meta": rewrite_meta, "query_type": query_type, "query_semantics": query_semantics, "retrieval_domains": list(query_semantics.get("retrieval_domains") or []), "message_intent": str(query_semantics.get("message_intent") or query_semantics.get("intent") or "").strip().lower() or None, "scene_semantics": scene_semantics if isinstance(scene_semantics, dict) else {}, "effective_action_mode": effective_action_mode, "anchor_resolution": anchor_resolution, } async def _execute_base_answer_mainline( self, *, clean_q: str, asked_by: Optional[int], asked_via: str, project_id: Optional[int], source_chat_id: Optional[str], session_id: Optional[int], turn_index: int, context_session_id: Optional[int], conversation: list[dict[str, str]], state_row: Optional[QASessionState], effective_q: str, rewrite_meta: dict[str, Any], query_type: str, query_semantics: dict[str, Any], retrieval_domains: list[str], scene_semantics: dict[str, Any], thinking_mode: str, active_thinking_mode: str, deep_thinking: bool, enforce_scene_prompt_only: bool, deep_retrieval_steps: int, deep_vector_top_k: int, started: float, message_intent: Optional[str], mixed_signal_part: Optional[str], anchor_resolution: Optional[FollowupAnchorResolution] = None, consultant_mode_preference: Optional[str] = None, ) -> dict[str, Any]: planner = self._plan_query_runtime( question=clean_q, effective_question=effective_q, query_type=query_type, scene_semantics=scene_semantics, requested_thinking_mode=active_thinking_mode, enforce_scene_prompt_only=enforce_scene_prompt_only, consultant_mode_preference=consultant_mode_preference, ) active_thinking_mode = planner.thinking_mode deep_thinking = planner.retrieval_answer_mode == ANSWER_MODE_RETRIEVAL_DEEP deep_retrieval_steps = int(planner.budget.max_steps or deep_retrieval_steps) deep_vector_top_k = int(planner.budget.vector_top_k or deep_vector_top_k) forced_deep_reason = "" if not deep_thinking and not planner.allow_fast_path: deep_intent = str((scene_semantics or {}).get("deep_intent") or "none").strip().lower() if deep_intent != "none": forced_deep_reason = deep_intent deep_thinking = True active_thinking_mode = THINKING_MODE_DEEP clarification_payload = self._build_followup_clarification_payload( question=clean_q, anchor_resolution=anchor_resolution, session_state=state_row, ) meta_followup_payload = await self._build_meta_followup_payload( question=clean_q, anchor_resolution=anchor_resolution, conversation_scope_key=self._get_active_external_qa_context().get("conversation_scope_key"), ) meta_followup_result = await self._maybe_return_meta_followup_response( meta_followup_payload=meta_followup_payload, started=started, clean_q=clean_q, effective_q=effective_q, query_type=query_type, asked_by=asked_by, asked_via=asked_via, project_id=project_id, session_id=session_id, turn_index=turn_index, state_row=state_row, rewrite_meta=rewrite_meta, context_session_id=context_session_id, active_thinking_mode=active_thinking_mode, ) if meta_followup_result is not None: return meta_followup_result clarification_result = await self._maybe_return_clarification_response( clarification_payload=clarification_payload, started=started, clean_q=clean_q, effective_q=effective_q, query_type=query_type, asked_by=asked_by, asked_via=asked_via, project_id=project_id, session_id=session_id, turn_index=turn_index, state_row=state_row, rewrite_meta=rewrite_meta, context_session_id=context_session_id, active_thinking_mode=active_thinking_mode, ) if clarification_result is not None: return clarification_result system_snapshot = await self.context_cache.get_system_snapshot( db=self.db, tenant_id=self.tenant_id, ) user_snapshot = await self.context_cache.get_user_snapshot( db=self.db, tenant_id=self.tenant_id, user_id=asked_by, ) if planner.allow_fast_path: deterministic_result = await self._run_deterministic_query( question=effective_q, project_id=project_id, query_type=query_type, query_semantics=query_semantics, ) if deterministic_result is not None: return await self._persist_fast_answer( question=clean_q, asked_by=asked_by, asked_via=asked_via, project_id=project_id, fast_result=deterministic_result, session_id=session_id, external_session_id=context_session_id, turn_index=turn_index, query_type=str(deterministic_result.get("query_type") or query_type), missing_fields=list(deterministic_result.get("missing_fields") or []), state_row=state_row, effective_question=effective_q, rewrite_meta=rewrite_meta, thinking_mode=active_thinking_mode, answer_mode=ANSWER_MODE_FAST_PATH, ) direct_answer = None if planner.allow_fast_path: direct_answer = self._build_snapshot_direct_answer( effective_q, system_snapshot=system_snapshot, user_snapshot=user_snapshot, snapshot_focus=str((scene_semantics or {}).get("snapshot_focus") or "none"), ) direct_result = await self._maybe_return_snapshot_direct_response( direct_answer=direct_answer, started=started, clean_q=clean_q, effective_q=effective_q, asked_by=asked_by, asked_via=asked_via, project_id=project_id, session_id=session_id, turn_index=turn_index, state_row=state_row, rewrite_meta=rewrite_meta, context_session_id=context_session_id, active_thinking_mode=active_thinking_mode, user_snapshot=user_snapshot, ) if direct_result is not None: return direct_result is_snapshot_direct_question = str((scene_semantics or {}).get("snapshot_focus") or "none").strip().lower() != "none" if planner.allow_fast_path and not is_snapshot_direct_question: glm_semantic_result = await self._try_glm_project_semantic_answer( clean_q=effective_q, project_id=project_id, query_semantics=query_semantics, ) if glm_semantic_result is not None: return await self._persist_fast_answer( question=clean_q, asked_by=asked_by, asked_via=asked_via, project_id=project_id, fast_result=glm_semantic_result, session_id=session_id, external_session_id=context_session_id, turn_index=turn_index, query_type=query_type if query_type != QUERY_TYPE_UNKNOWN else QUERY_TYPE_EXPLAIN, missing_fields=[], state_row=state_row, effective_question=effective_q, rewrite_meta=rewrite_meta, thinking_mode=active_thinking_mode, answer_mode=ANSWER_MODE_FAST_PATH, ) return await self._build_retrieval_response( clean_q=clean_q, effective_q=effective_q, asked_by=asked_by, asked_via=asked_via, project_id=project_id, source_chat_id=source_chat_id, session_id=session_id, turn_index=turn_index, context_session_id=context_session_id, conversation=conversation, state_row=state_row, rewrite_meta=rewrite_meta, query_type=query_type, query_semantics=query_semantics, retrieval_domains=retrieval_domains, scene_semantics=scene_semantics, thinking_mode=thinking_mode, active_thinking_mode=active_thinking_mode, deep_thinking=deep_thinking, forced_deep_reason=forced_deep_reason or str(planner.reason or ""), deep_retrieval_steps=deep_retrieval_steps, deep_vector_top_k=deep_vector_top_k, system_snapshot=system_snapshot, user_snapshot=user_snapshot, started=started, message_intent=message_intent, mixed_signal_part=mixed_signal_part, answer_mode=planner.retrieval_answer_mode, consultant_mode_preference=consultant_mode_preference, )