PMFlow/backend/app/services/ai_pmo_reply_renderer.py

217 lines
9.6 KiB
Python

"""AI PMO reply rendering runtime."""
from __future__ import annotations
from typing import Any, Optional
from .ai_pmo_intent_routing import (
INTENT_FOLLOWUP_CONFIRM,
INTENT_FOLLOWUP_CONTINUE,
INTENT_PROJECT_CREATE,
INTENT_QUERY,
INTENT_SIGNAL_CREATE,
INTENT_SIGNAL_SOURCE_ONBOARD,
ROUTE_PROJECT_SIGNAL_CANCEL,
ROUTE_PROJECT_SIGNAL_COMMIT,
ROUTE_PROJECT_SIGNAL_DISCOVER,
ROUTE_PROJECT_SIGNAL_REFRESH,
CanonicalIntent,
)
def _safe_int(value: Any) -> Optional[int]:
try:
if value is None or value == "":
return None
return int(value)
except (TypeError, ValueError):
return None
def _normalize_signal_id_list(value: Any) -> list[int]:
ids: list[int] = []
seen: set[int] = set()
if isinstance(value, list):
items = value
else:
items = [value]
for item in items:
signal_id = _safe_int(item)
if signal_id is None or signal_id <= 0:
continue
if signal_id in seen:
continue
seen.add(signal_id)
ids.append(signal_id)
return ids
class ReplyRenderer:
"""唯一回执出口。"""
def __init__(
self,
*,
state_waiting_manual: str,
state_failed: str,
receipt_phase_action: str,
receipt_phase_pending: str,
receipt_phase_error: str,
) -> None:
self.state_waiting_manual = state_waiting_manual
self.state_failed = state_failed
self.receipt_phase_action = receipt_phase_action
self.receipt_phase_pending = receipt_phase_pending
self.receipt_phase_error = receipt_phase_error
def render(
self,
*,
work_state: str,
intent: CanonicalIntent,
decision_record: dict[str, Any],
execution_result: dict[str, Any],
) -> dict[str, Any]:
if work_state == self.state_waiting_manual:
if intent.name in {INTENT_FOLLOWUP_CONFIRM, INTENT_FOLLOWUP_CONTINUE}:
followup_text = str(execution_result.get("followup_reply") or "").strip()
if followup_text:
return {
"receipt_phase": self.receipt_phase_pending,
"template_key": "pending_manual",
"payload": {"intent": intent.name, "reason": followup_text},
"text": followup_text,
}
if intent.name == INTENT_SIGNAL_CREATE:
pending_text = str(execution_result.get("followup_reply") or "").strip()
if pending_text:
return {
"receipt_phase": self.receipt_phase_pending,
"template_key": "pending_manual",
"payload": {
"intent": intent.name,
"workflow_route": execution_result.get("workflow_route"),
"reason": pending_text,
},
"text": pending_text,
}
gate_message = str(((decision_record.get("approval_requirement") or {}).get("message")) or "").strip()
if not gate_message:
gate_message = "已进入待人工确认队列。"
return {
"receipt_phase": self.receipt_phase_pending,
"template_key": "pending_manual",
"payload": {"intent": intent.name, "reason": gate_message},
"text": gate_message,
}
if work_state == self.state_failed:
if intent.name in {INTENT_FOLLOWUP_CONFIRM, INTENT_FOLLOWUP_CONTINUE}:
followup_text = str(execution_result.get("followup_reply") or "").strip()
if followup_text:
return {
"receipt_phase": self.receipt_phase_error,
"template_key": "safe_error",
"payload": {"intent": intent.name, "reason": followup_text},
"text": followup_text,
}
reason = str(execution_result.get("error_message") or "系统处理中出现异常,本次未执行。").strip()
return {
"receipt_phase": self.receipt_phase_error,
"template_key": "safe_error",
"payload": {"intent": intent.name, "reason": reason},
"text": reason,
}
if intent.name in {INTENT_FOLLOWUP_CONFIRM, INTENT_FOLLOWUP_CONTINUE}:
followup_text = str(execution_result.get("followup_reply") or "已继续处理。").strip()
return {
"receipt_phase": self.receipt_phase_action,
"template_key": "action_succeeded",
"payload": {
"intent": intent.name,
"target_work_item_id": _safe_int(execution_result.get("target_work_item_id")),
"target_state": execution_result.get("target_state"),
},
"text": followup_text,
}
if intent.name == INTENT_QUERY:
answer = str(execution_result.get("answer") or "当前暂无可用回答。").strip()
return {
"receipt_phase": self.receipt_phase_action,
"template_key": "action_succeeded",
"payload": {"intent": intent.name, "answer": answer},
"text": answer,
}
if intent.name == INTENT_PROJECT_CREATE:
project = execution_result.get("project") if isinstance(execution_result.get("project"), dict) else {}
project_id = _safe_int(project.get("id"))
project_title = str(project.get("title") or project.get("name") or "新项目").strip()
return {
"receipt_phase": self.receipt_phase_action,
"template_key": "action_succeeded",
"payload": {"intent": intent.name, "project_id": project_id, "project_title": project_title},
"text": f"已立项:项目#{project_id}《{project_title}》已创建完成。" if project_id else f"已立项:《{project_title}》已创建完成。",
}
if intent.name == INTENT_SIGNAL_SOURCE_ONBOARD:
signal_id = _safe_int(execution_result.get("signal_id"))
workflow = execution_result.get("signal_source_workflow") if isinstance(execution_result.get("signal_source_workflow"), dict) else {}
status = str(workflow.get("status") or (execution_result.get("signal_source_orchestration") or {}).get("status") or "").strip()
if status == "pending_manual":
return {
"receipt_phase": self.receipt_phase_pending,
"template_key": "pending_manual",
"payload": {"intent": intent.name, "signal_id": signal_id, "status": status},
"text": f"已记录信号源接入请求,当前为待人工确认。信号#{signal_id} 已入库。" if signal_id else "已记录信号源接入请求,当前为待人工确认。",
}
return {
"receipt_phase": self.receipt_phase_action,
"template_key": "action_succeeded",
"payload": {"intent": intent.name, "signal_id": signal_id, "status": status},
"text": f"已接收入库并启动信号源接入流程。信号#{signal_id},状态:{status or 'running'}。",
}
workflow_route = str(execution_result.get("workflow_route") or "").strip().lower()
followup_text = str(execution_result.get("followup_reply") or "").strip()
if followup_text and workflow_route in {
ROUTE_PROJECT_SIGNAL_DISCOVER,
ROUTE_PROJECT_SIGNAL_COMMIT,
ROUTE_PROJECT_SIGNAL_CANCEL,
ROUTE_PROJECT_SIGNAL_REFRESH,
}:
return {
"receipt_phase": self.receipt_phase_action,
"template_key": "action_succeeded",
"payload": {
"intent": intent.name,
"workflow_route": workflow_route,
},
"text": followup_text,
}
signal_ids = _normalize_signal_id_list(execution_result.get("signal_ids"))
signal_id = _safe_int(execution_result.get("signal_id"))
if signal_id is not None and signal_id not in signal_ids:
signal_ids = [signal_id, *signal_ids]
primary_signal_id = signal_ids[0] if signal_ids else signal_id
project_id = _safe_int(execution_result.get("project_id"))
if len(signal_ids) > 1 and project_id is not None:
signal_labels = "、".join(f"#{sid}" for sid in signal_ids[:5])
text = f"已入库:已为项目#{project_id}创建{len(signal_ids)}条信号({signal_labels})。"
elif len(signal_ids) > 1:
signal_labels = "、".join(f"#{sid}" for sid in signal_ids[:5])
text = f"已入库:已创建{len(signal_ids)}条信号({signal_labels})。"
elif primary_signal_id is not None and project_id is not None:
text = f"已入库:已为项目#{project_id}创建信号#{primary_signal_id}。"
elif primary_signal_id is not None:
text = f"已入库:已创建信号#{primary_signal_id}。"
else:
text = "已收到并入库。"
return {
"receipt_phase": self.receipt_phase_action,
"template_key": "action_succeeded",
"payload": {
"intent": intent.name,
"signal_id": primary_signal_id,
"signal_ids": signal_ids,
"project_id": project_id,
},
"text": text,
}