PMFlow/backend/app/services/ai_pmo_kernel_mixins_closur...

195 lines
8.4 KiB
Python

"""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 = re.sub(r"\s+", "", str(text or "")).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)
if cls._is_generic_signal_source_message(str(intent.signal_title or "")):
if modules:
intent.signal_title = f"信号源接入 · {str(modules[0])[:60]}"[:120]
else:
intent.signal_title = "信号源接入请求"
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