"""AI PMO kernel closure payload mixin.""" from __future__ import annotations from . import ai_pmo_kernel_common as _common for _name, _value in vars(_common).items(): if not _name.startswith("__"): globals()[_name] = _value class KernelClosurePayloadMixin: @staticmethod def _is_generic_signal_source_message(text: str) -> bool: compact = "".join(str(text or "").split()).strip().lower() if not compact: return False if bool(GENERIC_SIGNAL_SOURCE_MESSAGE_RE.match(compact)): return True return len(compact) <= 24 and "信号源" in compact and ("这个" in compact or "步骤" in compact) @classmethod def _extract_implementation_modules_from_text(cls, text: str) -> list[str]: normalized = str(text or "").lower() modules: list[str] = [] for module_name, keywords in MODULE_KEYWORD_RULES: if any(str(keyword).lower() in normalized for keyword in keywords): modules.append(module_name) return modules[:6] @staticmethod def _build_closure_dialogue_excerpt(turns: list[dict[str, str]]) -> str: if not turns: return "" lines: list[str] = [] for turn in turns[-4:]: role = str(turn.get("role") or "").strip().lower() text = str(turn.get("text") or "").strip() if not text: continue if role == "assistant": lines.append(f"机器人:{text[:380]}") else: lines.append(f"用户:{text[:260]}") return "\n".join(lines)[:1600] @classmethod def _derive_closure_summary( cls, *, turns: list[dict[str, str]], modules: list[str], fallback_text: str, ) -> str: latest_user = "" latest_assistant = "" for turn in reversed(turns): role = str(turn.get("role") or "").strip().lower() text = str(turn.get("text") or "").strip() if not text: continue if role == "user" and not latest_user: latest_user = text if role == "assistant" and not latest_assistant: latest_assistant = text if latest_user and latest_assistant: break if latest_assistant and any(token in latest_assistant for token in ("缺少项目归属", "不能创建", "无法创建", "先确认项目")): if latest_user: return f"机器人先提示条件不足,用户随后继续要求执行:{latest_user[:120]}" return "机器人提示条件不足,需补齐项目归属后继续执行信号源接入。" if latest_user and cls._is_generic_signal_source_message(latest_user) and modules: if len(modules) == 1 and str(modules[0]).strip() == "信号源接入": return "用户要求将需求按步骤接入信号源,需要先补齐项目归属再继续执行。" return f"同一消息闭环内,用户要求将需求按步骤接入信号源,重点模块:{'、'.join(modules[:4])}。" if modules: return f"用户在本次消息闭环中推进信号源接入,涉及模块:{'、'.join(modules[:4])}。" if latest_user: return latest_user[:200] return str(fallback_text or "").strip()[:200] async def _build_message_closure_payload( self, *, source_system: str, channel_ref: Optional[str], sender: str, source_sender_open_id: Optional[str], message_text: str, source_message_id: Optional[str] = None, owner_user_id: Optional[int] = None, project_id: Optional[int] = None, captured_at: Optional[Any] = None, ) -> dict[str, Any]: normalized_message = self._normalize_dialogue_text(message_text, max_len=600) turns: list[dict[str, str]] = [] normalized_source_system = str(source_system or "").strip().lower() normalized_channel = str(channel_ref or "").strip() if normalized_source_system == "feishu" and normalized_channel: conversation_key = self._build_feishu_rag_conversation_key( source_chat_id=normalized_channel, source_sender_open_id=source_sender_open_id, sender=sender, ) session_id = AdaptiveRAGService._external_session_id_from_key(conversation_key) if session_id is not None: cached_messages = await self.context_cache.get_session_messages( tenant_id=self.tenant_id, session_id=int(session_id), ) cached_turns = self.context_cache.messages_to_turns(cached_messages) for row in cached_turns[-2:]: question = self._normalize_dialogue_text(row.get("question"), max_len=260) answer = self._normalize_dialogue_text(row.get("answer"), max_len=420) if question: turns.append({"role": "user", "text": question}) if answer: turns.append({"role": "assistant", "text": answer}) if normalized_source_system == "feishu" and not self._turns_have_assistant(turns): log_turns = await self._load_closure_turns_from_processing_logs( source_message_id=source_message_id, message_text=normalized_message, ) turns = self._merge_closure_turns(turns, log_turns) needs_semantic_turns = ( normalized_source_system == "feishu" and ( (not self._turns_have_assistant(turns)) or (self._is_generic_signal_source_message(normalized_message) and len(turns) <= 2) ) ) if needs_semantic_turns: qa_turns = await self._load_closure_turns_from_qa_records( owner_user_id=owner_user_id, project_id=project_id, captured_at=self._parse_datetime_like(captured_at), message_text=normalized_message, ) turns = self._merge_closure_turns(turns, qa_turns, prepend=True) if normalized_message: if not turns or str(turns[-1].get("text") or "").strip() != normalized_message: turns.append({"role": "user", "text": normalized_message}) closure_dialogue = self._build_closure_dialogue_excerpt(turns) modules = self._extract_implementation_modules_from_text( " ".join(str(turn.get("text") or "") for turn in turns) ) closure_summary = self._derive_closure_summary( turns=turns, modules=modules, fallback_text=normalized_message, ) return { "closure_dialogue": closure_dialogue or None, "closure_summary": closure_summary or None, "closure_modules": modules, "closure_turns": turns[-4:], } @classmethod def _enrich_signal_source_intent_from_closure( cls, *, intent: CanonicalIntent, message_text: str, closure_payload: dict[str, Any], ) -> CanonicalIntent: if intent.name != INTENT_SIGNAL_SOURCE_ONBOARD: return intent modules = closure_payload.get("closure_modules") if isinstance(closure_payload.get("closure_modules"), list) else [] closure_summary = cls._normalize_dialogue_text(closure_payload.get("closure_summary"), max_len=300) closure_dialogue = cls._normalize_dialogue_text(closure_payload.get("closure_dialogue"), max_len=1600) if closure_summary: intent.signal_summary = closure_summary elif not str(intent.signal_summary or "").strip(): intent.signal_summary = cls._normalize_dialogue_text(message_text, max_len=300) metadata = dict(intent.metadata or {}) metadata["closure_summary"] = closure_summary or None metadata["closure_dialogue"] = closure_dialogue or None metadata["closure_modules"] = [str(item) for item in modules[:6] if str(item).strip()] intent.metadata = metadata return intent