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

93 lines
3.2 KiB
Python

"""AI PMO kernel closure helpers 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 KernelClosureHelpersMixin:
@staticmethod
def _normalize_dialogue_text(value: Any, *, max_len: int) -> str:
text = re.sub(r"\s+", " ", str(value or "")).strip()
if not text:
return ""
return text[:max_len]
@staticmethod
def _parse_datetime_like(value: Any) -> Optional[datetime]:
if isinstance(value, datetime):
dt = value
else:
raw = str(value or "").strip()
if not raw:
return None
try:
dt = datetime.fromisoformat(raw.replace("Z", "+00:00"))
except Exception:
return None
if dt.tzinfo is not None:
return dt.astimezone(timezone.utc).replace(tzinfo=None)
return dt
@staticmethod
def _normalize_feishu_user_text(value: Any, *, max_len: int) -> str:
text = str(value or "")
text = re.sub(r"^@_user_\d+\s*", "", text)
text = re.sub(r"</?at[^>]*>", " ", text, flags=re.IGNORECASE)
text = re.sub(r"\s+", " ", text).strip()
if not text:
return ""
return text[:max_len]
@classmethod
def _extract_assistant_text_from_log_payload(cls, payload: Any) -> str:
if not isinstance(payload, dict):
return ""
candidates = (
payload.get("followup_reply"),
payload.get("direct_reply_text"),
payload.get("clarification_message"),
payload.get("answer"),
payload.get("reason"),
payload.get("result_summary"),
payload.get("error_message"),
payload.get("message"),
)
for item in candidates:
text = cls._normalize_dialogue_text(item, max_len=420)
if not text:
continue
if any(token in text for token in ("chat_id", "message_type", "sender_name")) and len(text) < 48:
continue
return text
return ""
@staticmethod
def _merge_closure_turns(
base_turns: list[dict[str, str]],
extra_turns: list[dict[str, str]],
*,
prepend: bool = False,
) -> list[dict[str, str]]:
if not extra_turns:
return list(base_turns)
merged = list(extra_turns) + list(base_turns) if prepend else list(base_turns) + list(extra_turns)
compact: list[dict[str, str]] = []
for row in merged:
role = str(row.get("role") or "").strip().lower()
text = str(row.get("text") or "").strip()
if role not in {"user", "assistant"} or not text:
continue
if compact and compact[-1].get("role") == role and compact[-1].get("text") == text:
continue
compact.append({"role": role, "text": text})
return compact[-8:]
@staticmethod
def _turns_have_assistant(turns: list[dict[str, str]]) -> bool:
return any(str(turn.get("role") or "").strip().lower() == "assistant" and str(turn.get("text") or "").strip() for turn in turns)