PMFlow/backend/app/api/ai_pmo_api/helpers_inbox_payload.py

165 lines
8.0 KiB
Python

from __future__ import annotations
import re
from .shared import *
from .helpers_inbox_filters import (
_blocking_reason,
_compact_text,
_extract_gate_projection_from_work_item,
_is_low_value_closure_payload,
_next_step_text,
_normalize_approval_requirement,
_normalize_module_list,
_priority_from_review,
_safe_int,
_stage_text,
)
async def _build_closure_payload_overrides(
*,
db: AsyncSession,
tenant_id: int,
rows: list[tuple[WorkItem, IngestEvent, Optional[Project]]],
) -> dict[int, dict[str, Any]]:
if not rows:
return {}
service = AIPMOKernelService(db, tenant_id=tenant_id)
overrides: dict[int, dict[str, Any]] = {}
for row, ingest, _project in rows:
payload = ingest.payload if isinstance(ingest.payload, dict) else {}
if not isinstance(payload, dict):
continue
has_closure = bool(
str(payload.get("closure_summary") or "").strip()
or str(payload.get("closure_dialogue") or "").strip()
or (isinstance(payload.get("closure_modules"), list) and payload.get("closure_modules"))
)
if has_closure and not _is_low_value_closure_payload(payload):
continue
message_text = str(payload.get("message_text") or "").strip()
if not message_text:
continue
source_system = str(row.source_system or "").strip() or "pmflow_manual"
captured_at_seed: Optional[str] = str(payload.get("captured_at") or "").strip() or None
if not captured_at_seed and isinstance(row.created_at, datetime):
captured_at_seed = row.created_at.isoformat()
closure_payload = await service._build_message_closure_payload(
source_system=source_system,
channel_ref=str(payload.get("channel_ref") or "").strip() or None,
sender=str(payload.get("sender") or "").strip(),
source_sender_open_id=str(payload.get("source_sender_open_id") or "").strip() or None,
message_text=message_text,
source_message_id=str(row.source_message_id or "").strip() or None,
owner_user_id=_safe_int(row.owner_user_id),
project_id=_safe_int(row.project_id),
captured_at=captured_at_seed,
)
if not any(
(
str(closure_payload.get("closure_summary") or "").strip(),
str(closure_payload.get("closure_dialogue") or "").strip(),
isinstance(closure_payload.get("closure_modules"), list) and closure_payload.get("closure_modules"),
)
):
continue
overrides[int(row.id)] = {
**payload,
"closure_summary": closure_payload.get("closure_summary"),
"closure_dialogue": closure_payload.get("closure_dialogue"),
"closure_modules": closure_payload.get("closure_modules"),
"closure_turns": closure_payload.get("closure_turns"),
}
return overrides
def _serialize_work_item_summary(
*,
row: WorkItem,
ingest: IngestEvent,
project: Optional[Project],
latest_decision_projection: Optional[dict[str, Any]] = None,
payload_override: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
payload = payload_override if isinstance(payload_override, dict) else (
ingest.payload if isinstance(ingest.payload, dict) else {}
)
message_text = str(payload.get("message_text") or "").strip()
closure_dialogue = str(payload.get("closure_dialogue") or "").strip() or None
closure_summary = str(payload.get("closure_summary") or "").strip() or None
closure_modules = _normalize_module_list(payload.get("closure_modules"))
closure_turns = payload.get("closure_turns") if isinstance(payload.get("closure_turns"), list) else []
source_captured_at = str(payload.get("captured_at") or "").strip() or None
source_sender = str(payload.get("sender") or "").strip() or None
source_channel_ref = str(payload.get("channel_ref") or "").strip() or None
draft_content = row.draft_content if isinstance(row.draft_content, dict) else {}
draft_title = _compact_text(str(draft_content.get("title") or "").strip(), 64)
draft_summary = _compact_text(str(draft_content.get("summary") or "").strip(), 180)
summary_seed = closure_summary or message_text or _next_step_text(row) or row.intent or "AI PMO 工作项"
summary = _compact_text(summary_seed, 180)
compact_message = re.sub(r"\s+", "", message_text).lower() if message_text else ""
prefer_closure_title = bool(compact_message and GENERIC_SOURCE_TEXT_RE.match(compact_message))
title_seed = (
(closure_summary or message_text)
if prefer_closure_title
else (message_text or closure_summary or f"AI PMO · {row.intent or 'work_item'}")
)
title = _compact_text(title_seed, 64)
if str(row.state or "").strip() == "draft_pending_confirm":
if draft_title:
title = draft_title
if draft_summary:
summary = draft_summary
priority, priority_rank = _priority_from_review(row.review_summary)
project_name = None
if project is not None:
project_name = str(project.title or project.name or "").strip() or None
project_id = _safe_int(row.project_id)
decision_projection = latest_decision_projection if isinstance(latest_decision_projection, dict) else {}
latest_policy_gate_code = str(decision_projection.get("latest_policy_gate_code") or "").strip() or None
approval_requirement = _normalize_approval_requirement(decision_projection.get("approval_requirement"))
if latest_policy_gate_code is None or approval_requirement is None:
fallback_projection = _extract_gate_projection_from_work_item(row)
if latest_policy_gate_code is None:
latest_policy_gate_code = str(fallback_projection.get("latest_policy_gate_code") or "").strip() or None
if approval_requirement is None:
approval_requirement = _normalize_approval_requirement(fallback_projection.get("approval_requirement"))
return {
"id": int(row.id),
"source_system": row.source_system,
"source_message_id": row.source_message_id,
"source_message_text": _compact_text(message_text, 600) or None,
"source_conversation_closure": _compact_text(closure_dialogue, 2000) or None,
"source_captured_at": source_captured_at,
"source_sender": source_sender,
"source_channel_ref": source_channel_ref,
"closure_summary": closure_summary,
"implementation_modules": closure_modules,
"source_closure_turns": closure_turns[:4],
"state": row.state,
"intent": row.intent,
"decision_level": row.decision_level,
"project_id": project_id,
"project_name": project_name,
"signal_id": _safe_int(row.signal_id),
"reply_template_key": row.reply_template_key,
"title": title or f"工作项#{int(row.id)}",
"summary": summary,
"ai_plan": _compact_text(((row.action_bundle or {}) if isinstance(row.action_bundle, dict) else {}).get("summary") or "", 180) or None,
"blocking_reason": _blocking_reason(row),
"next_step_text": _next_step_text(row),
"stage_text": _stage_text(str(row.state or "")),
"priority": priority,
"priority_rank": priority_rank,
"latest_policy_gate_code": latest_policy_gate_code,
"approval_requirement": approval_requirement,
"target_url": f"/projects/{project_id}/board" if project_id is not None else None,
"review_summary": row.review_summary,
"execution_result": row.execution_result,
"draft_content": draft_content or None,
"draft_expires_at": row.draft_expires_at.isoformat() if row.draft_expires_at else None,
"draft_version": int(row.draft_version or 0),
"confirm_message_id": str(row.confirm_message_id or "").strip() or None,
"created_at": row.created_at.isoformat() if row.created_at else None,
"updated_at": row.updated_at.isoformat() if row.updated_at else None,
}