288 lines
12 KiB
Python
288 lines
12 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 _normalize_plan_status(value: Any) -> Optional[str]:
|
|
status = str(value or "").strip()
|
|
return status or None
|
|
|
|
|
|
def _normalize_requirement_plan_context_summary(value: Any) -> Optional[dict[str, Any]]:
|
|
context = value if isinstance(value, dict) else {}
|
|
if not context:
|
|
return None
|
|
missing_info = []
|
|
for item in context.get("missing_info") if isinstance(context.get("missing_info"), list) else []:
|
|
compact = _compact_text(item, 48)
|
|
if compact:
|
|
missing_info.append(compact)
|
|
last_round_options = context.get("last_round_options") if isinstance(context.get("last_round_options"), dict) else {}
|
|
last_round_field = str(last_round_options.get("field") or "").strip() or None
|
|
summary = {
|
|
"title": _compact_text(context.get("title"), 80) or None,
|
|
"goal": _compact_text(context.get("goal"), 160) or None,
|
|
"missing_info": missing_info[:3],
|
|
"question_round": _safe_int(context.get("question_round")),
|
|
"last_round_field": last_round_field,
|
|
}
|
|
if not any(
|
|
(
|
|
summary["title"],
|
|
summary["goal"],
|
|
summary["missing_info"],
|
|
summary["question_round"],
|
|
summary["last_round_field"],
|
|
)
|
|
):
|
|
return None
|
|
return summary
|
|
|
|
|
|
def _build_plan_mode_projection(
|
|
*,
|
|
reply_template_key: Optional[str],
|
|
state: Optional[str],
|
|
payload: Any,
|
|
context_snapshot: Any,
|
|
execution_result: Any,
|
|
) -> dict[str, Any]:
|
|
payload_dict = payload if isinstance(payload, dict) else {}
|
|
snapshot = context_snapshot if isinstance(context_snapshot, dict) else {}
|
|
execution = execution_result if isinstance(execution_result, dict) else {}
|
|
explicit_plan_mode = next(
|
|
(
|
|
candidate
|
|
for candidate in (
|
|
execution.get("plan_mode"),
|
|
snapshot.get("plan_mode"),
|
|
payload_dict.get("plan_mode"),
|
|
)
|
|
if isinstance(candidate, bool)
|
|
),
|
|
None,
|
|
)
|
|
requirement_plan_context = next(
|
|
(
|
|
dict(candidate)
|
|
for candidate in (
|
|
execution.get("requirement_plan_context"),
|
|
snapshot.get("requirement_plan_context"),
|
|
payload_dict.get("requirement_plan_context"),
|
|
)
|
|
if isinstance(candidate, dict)
|
|
),
|
|
None,
|
|
)
|
|
normalized_reply_template = str(reply_template_key or "").strip().lower()
|
|
normalized_state = str(state or "").strip().lower()
|
|
fallback_plan_mode = normalized_reply_template == "requirement_plan_followup" or normalized_state == "draft_pending_confirm"
|
|
plan_mode = explicit_plan_mode if explicit_plan_mode is not None else fallback_plan_mode
|
|
plan_status = next(
|
|
(
|
|
normalized
|
|
for normalized in (
|
|
_normalize_plan_status(execution.get("plan_status")),
|
|
_normalize_plan_status(snapshot.get("plan_status")),
|
|
_normalize_plan_status(payload_dict.get("plan_status")),
|
|
)
|
|
if normalized
|
|
),
|
|
None,
|
|
)
|
|
if plan_status is None:
|
|
if normalized_reply_template == "requirement_mode_suspended":
|
|
plan_status = "suspended"
|
|
elif plan_mode and normalized_state:
|
|
plan_status = normalized_state
|
|
return {
|
|
"plan_mode": bool(plan_mode),
|
|
"plan_status": plan_status,
|
|
"requirement_plan_context_summary": _normalize_requirement_plan_context_summary(requirement_plan_context),
|
|
}
|
|
|
|
|
|
def _serialize_process_text_result(result: dict[str, Any]) -> dict[str, Any]:
|
|
payload = dict(result) if isinstance(result, dict) else {}
|
|
plan_projection = _build_plan_mode_projection(
|
|
reply_template_key=payload.get("reply_template_key"),
|
|
state=payload.get("status"),
|
|
payload=payload,
|
|
context_snapshot=payload.get("context_snapshot"),
|
|
execution_result=payload.get("execution_result"),
|
|
)
|
|
payload.update(plan_projection)
|
|
return payload
|
|
|
|
|
|
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 len(compact_message) <= 8 and closure_summary)
|
|
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"))
|
|
plan_projection = _build_plan_mode_projection(
|
|
reply_template_key=getattr(row, "reply_template_key", None),
|
|
state=getattr(row, "state", None),
|
|
payload=payload,
|
|
context_snapshot=getattr(row, "context_snapshot", None),
|
|
execution_result=getattr(row, "execution_result", None),
|
|
)
|
|
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,
|
|
"plan_mode": plan_projection["plan_mode"],
|
|
"plan_status": plan_projection["plan_status"],
|
|
"requirement_plan_context_summary": plan_projection["requirement_plan_context_summary"],
|
|
"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,
|
|
}
|