Files
egg 44822a561a feat: Improve file display, timezone handling, and LOT management
Changes:
- Fix datetime serialization with UTC 'Z' suffix for correct timezone display
- Add PDF upload support with extension fallback for MIME detection
- Fix LOT add/remove by creating new list for SQLAlchemy JSON change detection
- Add file message components (FileMessage, ImageLightbox, UploadPreview)
- Add multi-file upload support with progress tracking
- Link uploaded files to chat messages via message_id
- Include file attachments in AI report generation
- Update specs for file-storage, realtime-messaging, and ai-report-generation

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-08 12:39:15 +08:00

248 lines
7.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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, timezone, timedelta
# Taiwan timezone (GMT+8)
TZ_GMT8 = timezone(timedelta(hours=8))
def _to_gmt8(dt: datetime) -> datetime:
"""Convert datetime to GMT+8 timezone"""
if dt.tzinfo is None:
# Assume UTC if no timezone
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(TZ_GMT8)
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 = _to_gmt8(created_at).strftime("%Y-%m-%d %H:%M")
resolved_at = room_data.get("resolved_at")
if isinstance(resolved_at, datetime):
resolved_at = _to_gmt8(resolved_at).strftime("%Y-%m-%d %H:%M")
elif resolved_at is None:
resolved_at = "尚未解決"
# Format LOT batch numbers
lots = room_data.get('lots', [])
lots_str = ", ".join(lots) if lots else ""
lines = [
"## 事件資訊",
f"- 標題: {room_data.get('title', '未命名')}",
f"- 類型: {incident_type}",
f"- 嚴重程度: {severity}",
f"- 目前狀態: {status}",
f"- 發生地點: {room_data.get('location', '未指定')}",
f"- 影響批號 (LOT): {lots_str}",
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 = _to_gmt8(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 with context
Each file now includes:
- caption: User-provided description when uploading
- context_before: The message sent before this file
- context_after: The message sent after this file
This helps AI understand the context of each attachment.
"""
lines = ["## 附件清單"]
lines.append("每個附件包含上傳時的說明文字以及上下文訊息,幫助理解該附件的用途。")
lines.append("")
if not files:
lines.append("無附件")
return "\n".join(lines)
for i, f in enumerate(files, 1):
filename = f.get("filename", "未命名檔案")
file_type = f.get("file_type", "file")
uploader = f.get("uploader_name") or f.get("uploaded_by", "未知")
caption = f.get("caption") # User-provided description
context_before = f.get("context_before")
context_after = f.get("context_after")
uploaded_at = f.get("uploaded_at")
if isinstance(uploaded_at, datetime):
time_str = _to_gmt8(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 "檔案"
# Basic file info
lines.append(f"### 附件 {i}: {filename}")
lines.append(f"- 類型: {type_label}")
lines.append(f"- 上傳者: {uploader}")
lines.append(f"- 上傳時間: {time_str}")
# Caption/description if provided
if caption:
lines.append(f"- 說明: {caption}")
# Context messages to help AI understand when/why file was uploaded
if context_before or context_after:
lines.append("- 上下文:")
if context_before:
lines.append(f" - 前一則訊息: [{context_before['sender']}]: {context_before['content']}")
if context_after:
lines.append(f" - 後一則訊息: [{context_after['sender']}]: {context_after['content']}")
lines.append("") # Blank line between files
return "\n".join(lines)
def _format_instructions() -> str:
"""Format generation instructions"""
return """## 報告生成指示
請根據以上資料,生成一份結構化的事件報告。報告必須為 JSON 格式,包含以下欄位:
1. **summary**: 事件摘要 (50-300字)
2. **timeline**: 按時間順序的事件時間軸
3. **participants**: 參與人員及其角色
4. **resolution_process**: 詳細的處理過程描述
5. **current_status**: 目前狀態說明
6. **final_resolution**: 最終處置結果(若已解決)
請直接輸出 JSON不要包含其他說明文字。"""