195 lines
8.4 KiB
Python
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
|