feat: Add AI report generation with DIFY integration

- Add Users table for display name resolution from AD authentication
- Integrate DIFY AI service for report content generation
- Create docx assembly service with image embedding from MinIO
- Add REST API endpoints for report generation and download
- Add WebSocket notifications for generation progress
- Add frontend UI with progress modal and download functionality
- Add integration tests for report generation flow

Report sections (Traditional Chinese):
- 事件摘要 (Summary)
- 時間軸 (Timeline)
- 參與人員 (Participants)
- 處理過程 (Resolution Process)
- 目前狀態 (Current Status)
- 最終處置結果 (Final Resolution)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
egg
2025-12-04 18:32:40 +08:00
parent 77091eefb5
commit 3927441103
32 changed files with 4374 additions and 8 deletions

View File

@@ -0,0 +1,199 @@
"""Prompt templates for DIFY AI report generation
Contains the prompt construction logic for building the user query
sent to DIFY Chat API.
"""
from typing import List, Dict, Any
from datetime import datetime
INCIDENT_TYPE_MAP = {
"equipment_failure": "設備故障",
"material_shortage": "物料短缺",
"quality_issue": "品質問題",
"other": "其他",
}
SEVERITY_MAP = {
"low": "",
"medium": "",
"high": "",
"critical": "緊急",
}
STATUS_MAP = {
"active": "處理中",
"resolved": "已解決",
"archived": "已封存",
}
MEMBER_ROLE_MAP = {
"owner": "建立者",
"editor": "編輯者",
"viewer": "檢視者",
}
def build_report_prompt(
room_data: Dict[str, Any],
messages: List[Dict[str, Any]],
members: List[Dict[str, Any]],
files: List[Dict[str, Any]],
) -> str:
"""Build the complete prompt for DIFY report generation
Args:
room_data: Room metadata (title, type, severity, status, etc.)
messages: List of messages with sender_name, content, created_at
members: List of members with display_name, role
files: List of files with filename, file_type, uploaded_at, uploader_name
Returns:
Formatted prompt string for DIFY API
"""
sections = []
# Section 1: Event Information
sections.append(_format_room_info(room_data))
# Section 2: Participants
sections.append(_format_members(members))
# Section 3: Message Timeline
sections.append(_format_messages(messages))
# Section 4: File Attachments
sections.append(_format_files(files))
# Section 5: Instructions
sections.append(_format_instructions())
return "\n\n".join(sections)
def _format_room_info(room_data: Dict[str, Any]) -> str:
"""Format room metadata section"""
incident_type = INCIDENT_TYPE_MAP.get(
room_data.get("incident_type"), room_data.get("incident_type")
)
severity = SEVERITY_MAP.get(room_data.get("severity"), room_data.get("severity"))
status = STATUS_MAP.get(room_data.get("status"), room_data.get("status"))
created_at = room_data.get("created_at")
if isinstance(created_at, datetime):
created_at = created_at.strftime("%Y-%m-%d %H:%M")
resolved_at = room_data.get("resolved_at")
if isinstance(resolved_at, datetime):
resolved_at = resolved_at.strftime("%Y-%m-%d %H:%M")
elif resolved_at is None:
resolved_at = "尚未解決"
lines = [
"## 事件資訊",
f"- 標題: {room_data.get('title', '未命名')}",
f"- 類型: {incident_type}",
f"- 嚴重程度: {severity}",
f"- 目前狀態: {status}",
f"- 發生地點: {room_data.get('location', '未指定')}",
f"- 建立時間: {created_at}",
f"- 解決時間: {resolved_at}",
]
if room_data.get("description"):
lines.append(f"- 事件描述: {room_data['description']}")
if room_data.get("resolution_notes"):
lines.append(f"- 解決備註: {room_data['resolution_notes']}")
return "\n".join(lines)
def _format_members(members: List[Dict[str, Any]]) -> str:
"""Format participants section"""
lines = ["## 參與人員"]
if not members:
lines.append("無參與人員記錄")
return "\n".join(lines)
for member in members:
display_name = member.get("display_name") or member.get("user_id", "未知")
role = MEMBER_ROLE_MAP.get(member.get("role"), member.get("role", "成員"))
lines.append(f"- {display_name} ({role})")
return "\n".join(lines)
def _format_messages(messages: List[Dict[str, Any]]) -> str:
"""Format message timeline section"""
lines = ["## 對話記錄"]
if not messages:
lines.append("無對話記錄")
return "\n".join(lines)
for msg in messages:
sender = msg.get("sender_name") or msg.get("sender_id", "未知")
content = msg.get("content", "")
msg_type = msg.get("message_type", "text")
created_at = msg.get("created_at")
if isinstance(created_at, datetime):
time_str = created_at.strftime("%Y-%m-%d %H:%M")
else:
time_str = str(created_at) if created_at else "未知時間"
# Handle different message types
if msg_type == "file":
file_name = msg.get("file_name", "附件")
lines.append(f"[{time_str}] {sender}: [上傳檔案: {file_name}]")
elif msg_type == "image":
lines.append(f"[{time_str}] {sender}: [上傳圖片]")
elif msg_type == "system":
lines.append(f"[{time_str}] [系統]: {content}")
else:
lines.append(f"[{time_str}] {sender}: {content}")
return "\n".join(lines)
def _format_files(files: List[Dict[str, Any]]) -> str:
"""Format file attachments section"""
lines = ["## 附件清單"]
if not files:
lines.append("無附件")
return "\n".join(lines)
for f in files:
filename = f.get("filename", "未命名檔案")
file_type = f.get("file_type", "file")
uploader = f.get("uploader_name") or f.get("uploaded_by", "未知")
uploaded_at = f.get("uploaded_at")
if isinstance(uploaded_at, datetime):
time_str = uploaded_at.strftime("%Y-%m-%d %H:%M")
else:
time_str = str(uploaded_at) if uploaded_at else ""
type_label = "圖片" if file_type == "image" else "檔案"
lines.append(f"- [{type_label}] {filename} (由 {uploader}{time_str} 上傳)")
return "\n".join(lines)
def _format_instructions() -> str:
"""Format generation instructions"""
return """## 報告生成指示
請根據以上資料,生成一份結構化的事件報告。報告必須為 JSON 格式,包含以下欄位:
1. **summary**: 事件摘要 (50-100字)
2. **timeline**: 按時間順序的事件時間軸
3. **participants**: 參與人員及其角色
4. **resolution_process**: 詳細的處理過程描述
5. **current_status**: 目前狀態說明
6. **final_resolution**: 最終處置結果(若已解決)
請直接輸出 JSON不要包含其他說明文字。"""