573 lines
20 KiB
Python
573 lines
20 KiB
Python
"""
|
||
通用报告生成工作流
|
||
|
||
统一接口说明:
|
||
- 输入:extracted_text(用户输入文本)、history_message(历史消息)
|
||
- 输出:{"response": str, "actions": List[Dict], "route_flag": str}
|
||
|
||
功能:
|
||
1. 从历史对话中提取关键信息
|
||
2. 根据用户需求生成通用报告
|
||
3. 支持图片插入到报告对应位置
|
||
"""
|
||
import asyncio
|
||
import re
|
||
import random
|
||
from typing import TypedDict, List, Dict, Any, Literal
|
||
from langgraph.graph import StateGraph, START, END
|
||
import os
|
||
import sys
|
||
|
||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||
parent_dir = os.path.dirname(current_dir)
|
||
if parent_dir not in sys.path:
|
||
sys.path.append(parent_dir)
|
||
|
||
from modelsAPI.model_api import OpenaiAPI
|
||
from utils.function_tracker import track_function_calls
|
||
from utils.word_generator import generate_report_word
|
||
from config import SERVER_CONFIG
|
||
import json
|
||
|
||
|
||
def _extract_images_from_text(text: str) -> List[Dict[str, str]]:
|
||
"""从文本中提取图片URL和描述信息"""
|
||
if not text:
|
||
return []
|
||
|
||
images = []
|
||
url_pattern = r'https?://[^\s<>"{}|\\^`\[\]]+\.(?:png|jpg|jpeg|gif|bmp|webp)'
|
||
urls = re.findall(url_pattern, text, re.IGNORECASE)
|
||
|
||
for url in urls:
|
||
url = url.strip()
|
||
if url.startswith('`') and url.endswith('`'):
|
||
url = url[1:-1]
|
||
|
||
summary = ""
|
||
url_index = text.find(url)
|
||
if url_index != -1:
|
||
after_url = text[url_index + len(url):]
|
||
result_match = re.search(
|
||
r'(?:\*\*图片识别结果:\*\*|图片识别结果[::]|识别结果[::])\s*(.*?)(?=\n\n\n|\n\n[A-Za-z\u4e00-\u9fa5]|$)',
|
||
after_url,
|
||
re.DOTALL
|
||
)
|
||
if result_match:
|
||
summary = result_match.group(1).strip()[:300]
|
||
|
||
images.append({
|
||
"url": url,
|
||
"summary": summary or "用户上传的图片"
|
||
})
|
||
|
||
return images
|
||
|
||
|
||
from workflows.history_manager import init_history, build_history_str
|
||
|
||
|
||
class OtherReportState(TypedDict):
|
||
"""通用报告工作流状态"""
|
||
|
||
extracted_text: str
|
||
combined_query: str
|
||
|
||
history_message: str
|
||
history_messages: List[Dict[str, Any]]
|
||
|
||
report_title: str
|
||
report_theme: str
|
||
report_text: str
|
||
extracted_images: List[Dict[str, str]]
|
||
|
||
response: str
|
||
actions: List[Dict[str, Any]]
|
||
route_flag: Literal["", "report"]
|
||
suggestedReplies: List[Dict[str, Any]]
|
||
|
||
error_message: str
|
||
|
||
|
||
async def extract_report_info(state: OtherReportState) -> Dict[str, Any]:
|
||
"""
|
||
步骤一:提取报告相关信息
|
||
"""
|
||
from utils.function_tracker import get_all_callbacks
|
||
|
||
combined_query = state.get("combined_query", "") or state.get("extracted_text", "") or ""
|
||
history_messages = state.get("history_messages", [])
|
||
history_message = state.get("history_message", "")
|
||
|
||
extracted_images = _extract_images_from_text(history_message)
|
||
|
||
history_context = build_history_str(history_messages, recent_count=10, assistant_truncate=200)
|
||
|
||
if not history_context and history_message:
|
||
history_context = history_message
|
||
|
||
loading_prompts = [
|
||
{"title": "提取报告相关信息中...", "details": f"历史消息数: {len(history_messages)}条"},
|
||
{"title": "🔍 正在分析历史对话内容...", "details": f"共处理 {len(history_messages)} 条消息"},
|
||
{"title": "📊 解析报告信息中...", "details": f"已加载 {len(history_messages)} 条历史记录"},
|
||
{"title": "🔑 整理对话数据...", "details": f"当前历史消息: {len(history_messages)} 条"},
|
||
{"title": "💬 提取关键信息...", "details": f"历史对话总数: {len(history_messages)}条"},
|
||
]
|
||
selected_prompt = random.choice(loading_prompts)
|
||
parent_event = {
|
||
"type": "function_execution",
|
||
"title": selected_prompt["title"],
|
||
"details": selected_prompt["details"],
|
||
}
|
||
for callback in get_all_callbacks():
|
||
try:
|
||
callback(parent_event)
|
||
except Exception:
|
||
pass
|
||
|
||
prompt = f"""
|
||
你是一名专业的报告编写专家。
|
||
请根据以下信息生成报告标题和主题。
|
||
|
||
【用户输入】
|
||
{combined_query}
|
||
|
||
【历史对话上下文】
|
||
{history_context if history_context else "(无)"}
|
||
|
||
【输出要求】
|
||
输出一个JSON对象,包含以下字段:
|
||
- report_title:报告标题(简洁明了,不超过20个字,格式根据内容自动判断,如"XXX分析报告"、"XXX总结报告"等)
|
||
- report_theme:报告主题(用一句话概括报告的核心主题,用于指导后续报告内容生成,不超过50个字)
|
||
|
||
仅输出 JSON 对象,不要附加任何解释。
|
||
"""
|
||
|
||
try:
|
||
result = await OpenaiAPI.open_api_chat_without_thinking(
|
||
query=prompt,
|
||
model=None,
|
||
json_output=True
|
||
)
|
||
parsed_result = json.loads(result)
|
||
if isinstance(parsed_result, dict):
|
||
report_title = parsed_result.get("report_title", "通用报告")
|
||
report_theme = parsed_result.get("report_theme", "")
|
||
else:
|
||
report_title = "通用报告"
|
||
report_theme = ""
|
||
except Exception as e:
|
||
report_title = "通用报告"
|
||
report_theme = ""
|
||
|
||
return {"report_title": report_title, "report_theme": report_theme, "extracted_images": extracted_images}
|
||
|
||
|
||
async def generate_report_text(state: OtherReportState) -> Dict[str, Any]:
|
||
"""
|
||
步骤二:生成报告文本
|
||
"""
|
||
from utils.function_tracker import get_all_callbacks
|
||
import time
|
||
|
||
report_title = state.get("report_title", "通用报告")
|
||
report_theme = state.get("report_theme", "")
|
||
combined_query = state.get("combined_query", "") or state.get("extracted_text", "") or ""
|
||
history_messages = state.get("history_messages", [])
|
||
extracted_images = state.get("extracted_images", []) or []
|
||
|
||
loading_prompts = [
|
||
{"title": "✍️ 生成报告文本中...", "details": f"报告标题: {report_title}"},
|
||
{"title": "🎯 正在撰写报告...", "details": f"报告标题: {report_title}"},
|
||
{"title": "📋 构建报告中...", "details": f"报告标题: {report_title}"},
|
||
{"title": "📝 汇总生成报告...", "details": f"报告标题: {report_title}"},
|
||
]
|
||
selected_prompt = random.choice(loading_prompts)
|
||
start_event = {
|
||
"type": "function_execution",
|
||
"title": selected_prompt["title"],
|
||
"details": selected_prompt["details"]
|
||
}
|
||
for callback in get_all_callbacks():
|
||
try:
|
||
callback(start_event)
|
||
except Exception:
|
||
pass
|
||
|
||
history_context = build_history_str(history_messages, recent_count=10, assistant_truncate=300)
|
||
|
||
if not history_context and not combined_query:
|
||
return {
|
||
"report_text": "",
|
||
"response": "【无法生成报告】\n\n缺少历史对话信息和用户输入,请先进行相关对话。",
|
||
"actions": [],
|
||
"route_flag": "report",
|
||
"suggestedReplies": [],
|
||
"error_message": "缺少历史对话信息和用户输入"
|
||
}
|
||
|
||
random_letters = ''.join(random.choices('ABCDEFGHIJKLMNOPQRSTUVWXYZ', k=2))
|
||
report_number = time.strftime(f"{random_letters}-%Y-%m-%d-%H%M")
|
||
compile_time = time.strftime("%Y-%m-%d %H:%M")
|
||
|
||
image_section = ""
|
||
if extracted_images:
|
||
image_parts = []
|
||
for idx, img in enumerate(extracted_images, 1):
|
||
image_parts.append(f"图片{idx}:{img['url']}")
|
||
summary = img.get('summary') or img.get('description', '')
|
||
if summary:
|
||
image_parts.append(f"识别结果:{summary}")
|
||
image_parts.append("")
|
||
image_section = "\n".join(image_parts)
|
||
|
||
prompt = f"""
|
||
<role>
|
||
你是一名专业的报告编写专家。
|
||
你的任务是根据用户需求和历史对话生成一份正式的报告。
|
||
</role>
|
||
|
||
<user_requirement>
|
||
{combined_query}
|
||
</user_requirement>
|
||
|
||
<report_theme>
|
||
{report_theme if report_theme else "根据内容自动确定"}
|
||
</report_theme>
|
||
|
||
<history_context>
|
||
{history_context if history_context else "(无历史对话上下文)"}
|
||
</history_context>
|
||
|
||
{f'''<user_images>
|
||
{image_section}
|
||
</user_images>''' if extracted_images else ''}
|
||
|
||
<output_rules>
|
||
|
||
必须严格遵守以下规则:
|
||
|
||
1. 输出为 **正式报告文本**
|
||
2. **使用段落形式,如果需要表格,必须使用 Markdown 表格格式**
|
||
3. **输出内容必须基于历史对话信息,不得编造不实信息**
|
||
4. 不得输出解释说明
|
||
5. 所有内容使用完整中文句子
|
||
6. **【重要】每个段落首行必须缩进两个中文字符,使用两个全角空格" "开头,例如:" 这是段落内容。"**
|
||
7. 根据报告主题自动选择合适的报告结构,可以是分析报告、总结报告、调研报告等
|
||
8. **报告内容必须紧扣报告主题,确保内容相关性和连贯性**
|
||
{"9. **报告中必须包含用户上传的图片信息,在相关部分引用图片URL**" if extracted_images else ""}
|
||
|
||
</output_rules>
|
||
|
||
<title_format_rules>
|
||
|
||
**标题格式规范(必须严格遵守):**
|
||
|
||
1. **报告主标题**:使用二级标题格式,即 `## **标题内容**`,例如:`## **智能运维系统项目总结报告**`
|
||
2. **章节大标题**:使用三级标题格式,即 `### **标题内容**`,例如:`### **一、项目概述**`
|
||
3. **小节标题**:使用四级标题格式,即 `#### **标题内容**`,例如:`#### **1. 项目背景**`
|
||
4. 所有标题必须加粗,使用 `**标题内容**` 格式
|
||
5. 标题前后各空一行
|
||
|
||
</title_format_rules>
|
||
|
||
<paragraph_format_rules>
|
||
|
||
**段落格式规范(必须严格遵守):**
|
||
|
||
1. 每个段落必须以两个全角空格" "开头
|
||
2. 正确示例:
|
||
本项目旨在开发一套智能运维系统,用于提升企业IT运维效率。
|
||
系统采用微服务架构设计,支持高并发访问。
|
||
3. 错误示例(缺少缩进):
|
||
本项目旨在开发一套智能运维系统。
|
||
4. 段落之间空一行
|
||
|
||
</paragraph_format_rules>
|
||
|
||
<report_template>
|
||
|
||
## **{report_title}**
|
||
|
||
报告编号:{report_number}
|
||
编制时间:{compile_time}
|
||
|
||
### **一、概述**
|
||
|
||
介绍背景与目的。
|
||
|
||
### **二、主要内容**
|
||
|
||
阐述核心问题与关键分析过程。
|
||
|
||
### **三、备品备件**
|
||
|
||
列出本次维修或操作涉及的备品备件的名称(如有机油滤芯、密封圈、螺栓、传感器等),不要输出表格。如果没有涉及备件,请写"无"。
|
||
|
||
### **四、结论**
|
||
|
||
总结结果并提出结论性观点。
|
||
{"- **附件图片:**" + chr(10) + chr(10).join([f"" for idx, img in enumerate(extracted_images, 1)]) if extracted_images else ""}
|
||
|
||
</report_template>
|
||
|
||
<final_rule>
|
||
只输出完整报告正文,不要任何解释说明。每个段落首行必须使用" "(两个全角空格)缩进。所有标题必须使用对应级别的Markdown格式并加粗。
|
||
</final_rule>
|
||
"""
|
||
|
||
try:
|
||
report_text = await OpenaiAPI.open_api_chat_without_thinking(
|
||
query=prompt,
|
||
model=None,
|
||
messages=history_messages
|
||
)
|
||
if not report_text:
|
||
report_text = "抱歉,当前无法生成报告,请稍后重试。"
|
||
|
||
word_dir = os.path.join(parent_dir, "created_word")
|
||
os.makedirs(word_dir, exist_ok=True)
|
||
|
||
import time
|
||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||
safe_topic = re.sub(r'[\\/:*?"<>|]', '_', report_title)
|
||
file_name = f"{safe_topic}_{timestamp}.docx"
|
||
output_path = os.path.join(word_dir, file_name)
|
||
|
||
word_result = generate_report_word(
|
||
markdown=report_text,
|
||
title=report_title,
|
||
output=output_path
|
||
)
|
||
|
||
actions: List[Dict[str, Any]] = []
|
||
|
||
if word_result and os.path.exists(word_result):
|
||
base_url = SERVER_CONFIG.get("base_url")
|
||
download_url = f"{base_url}/api/download/{file_name}"
|
||
|
||
actions.append({
|
||
"type": "download",
|
||
"label": f"{report_title}.docx",
|
||
"url": download_url,
|
||
"fileName": file_name,
|
||
"content": report_text
|
||
})
|
||
|
||
try:
|
||
from tools.fault_statistics import save_spare_parts_metadata, extract_spare_parts_from_text, extract_info_from_filename
|
||
|
||
spare_parts = await extract_spare_parts_from_text(report_text)
|
||
|
||
print(f"[备件提取] 提取到 {len(spare_parts)} 个备件: {spare_parts}")
|
||
|
||
# 无论有没有备件都保存元数据
|
||
device = ""
|
||
ship_number = ""
|
||
fault_name = ""
|
||
# 优先从文件名中提取(最准确)
|
||
file_info = await extract_info_from_filename(file_name)
|
||
if file_info:
|
||
if file_info.get("device"):
|
||
device = file_info["device"]
|
||
if file_info.get("ship_number"):
|
||
ship_number = file_info["ship_number"]
|
||
if file_info.get("fault_name"):
|
||
fault_name = file_info["fault_name"]
|
||
# 如果从文件名没提取全,再尝试从历史消息中提取
|
||
if not device or not ship_number or not fault_name:
|
||
if history_messages:
|
||
for msg in reversed(history_messages):
|
||
if isinstance(msg, dict) and msg.get("role") == "user":
|
||
content = msg.get("content", "")
|
||
if not device:
|
||
device_match = re.search(r'设备[名称]*[::]\s*([^\n,,]+)', content)
|
||
if device_match:
|
||
device = device_match.group(1).strip()
|
||
if not ship_number:
|
||
ship_match = re.search(r'舷号[::]\s*(\d+)', content)
|
||
if ship_match:
|
||
ship_number = ship_match.group(1).strip()
|
||
|
||
print(f"[备件保存] 设备: {device}, 舷号: {ship_number}, 故障: {fault_name}, 文件: {output_path}")
|
||
save_spare_parts_metadata(output_path, spare_parts, device, ship_number, fault_name)
|
||
except Exception as e:
|
||
print(f"[备件提取错误] {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
else:
|
||
error_msg = f"\n\n[注意] Word 文档生成失败"
|
||
report_text += error_msg
|
||
|
||
suggested_replies = await generate_suggested_replies(report_text)
|
||
|
||
return {
|
||
"report_text": report_text,
|
||
"response": report_text,
|
||
"actions": actions,
|
||
"route_flag": "report",
|
||
"suggestedReplies": suggested_replies,
|
||
"__details__": "报告生成完成"
|
||
}
|
||
except Exception as e:
|
||
return {
|
||
"report_text": f"生成报告失败: {str(e)}",
|
||
"response": f"生成报告失败: {str(e)}",
|
||
"actions": [],
|
||
"route_flag": "report",
|
||
"suggestedReplies": [
|
||
{"title": "如何优化报告内容?", "content": "如何优化报告内容?"},
|
||
{"title": "有哪些改进建议?", "content": "有哪些改进建议?"}
|
||
],
|
||
"error_message": str(e),
|
||
"__details__": f"生成报告失败: {str(e)}"
|
||
}
|
||
|
||
|
||
async def generate_suggested_replies(report: str) -> List[Dict[str, Any]]:
|
||
"""
|
||
生成针对报告的建议回复问题
|
||
"""
|
||
prompt = f"""
|
||
你是一个专业的报告助手。根据以下报告内容,生成两个用户可能关心的后续问题。
|
||
|
||
【报告内容】
|
||
{report[:500]}
|
||
|
||
请生成两个简洁、实用的问题,这些问题应该:
|
||
1. 不得出现需要归档和下载等操作相关的问题
|
||
2. 与报告内容相关,能够帮助用户更好地理解或使用报告
|
||
3. 问题要具体、可操作
|
||
4. 每个问题不超过20个字
|
||
|
||
请以JSON格式输出,格式如下:
|
||
[
|
||
{{"title": "问题1的标题", "content": "问题1的完整内容"}},
|
||
{{"title": "问题2的标题", "content": "问题2的完整内容"}}
|
||
]
|
||
|
||
只输出JSON,不要有其他文字说明。
|
||
"""
|
||
try:
|
||
answer = await OpenaiAPI.open_api_chat_without_thinking(
|
||
query=prompt,
|
||
model=None
|
||
)
|
||
json_match = re.search(r'\[.*\]', answer, re.DOTALL)
|
||
if json_match:
|
||
json_str = json_match.group(0)
|
||
suggested_replies = json.loads(json_str)
|
||
if isinstance(suggested_replies, list) and len(suggested_replies) >= 2:
|
||
formatted_replies = []
|
||
for reply in suggested_replies[:2]:
|
||
if isinstance(reply, dict) and "title" in reply and "content" in reply:
|
||
formatted_replies.append({
|
||
"title": str(reply["title"]),
|
||
"content": str(reply["content"])
|
||
})
|
||
if len(formatted_replies) == 2:
|
||
return formatted_replies
|
||
|
||
return [
|
||
{"title": "如何优化报告内容?", "content": "如何优化报告内容?"},
|
||
{"title": "有哪些改进建议?", "content": "有哪些改进建议?"}
|
||
]
|
||
except Exception as e:
|
||
return [
|
||
{"title": "如何优化报告内容?", "content": "如何优化报告内容?"},
|
||
{"title": "有哪些改进建议?", "content": "有哪些改进建议?"}
|
||
]
|
||
|
||
|
||
def create_other_report_workflow():
|
||
"""创建通用报告工作流"""
|
||
graph = StateGraph(OtherReportState)
|
||
|
||
graph.add_node("extract_info", extract_report_info)
|
||
graph.add_node("generate_text", generate_report_text)
|
||
|
||
graph.add_edge(START, "extract_info")
|
||
graph.add_edge("extract_info", "generate_text")
|
||
graph.add_edge("generate_text", END)
|
||
|
||
return graph.compile()
|
||
|
||
|
||
async def run_other_report_workflow(
|
||
extracted_text: str,
|
||
combined_query: str,
|
||
history_message: str = "",
|
||
route_flag: str = ""
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
执行通用报告工作流(供主Agent / app.py 调用)
|
||
|
||
Args:
|
||
extracted_text: 主Agent合并后的文本描述
|
||
combined_query: 用户问题
|
||
history_message: 历史消息(JSON格式字符串)
|
||
route_flag: 路由标识
|
||
|
||
Returns:
|
||
{
|
||
"response": str,
|
||
"actions": List[Dict[str, Any]],
|
||
"route_flag": str,
|
||
"suggestedReplies": List[Dict[str, Any]]
|
||
}
|
||
"""
|
||
app = create_other_report_workflow()
|
||
|
||
initial_state: OtherReportState = {
|
||
"extracted_text": extracted_text,
|
||
"combined_query": combined_query,
|
||
**init_history(history_message),
|
||
"report_title": "",
|
||
"report_theme": "",
|
||
"report_text": "",
|
||
"extracted_images": [],
|
||
"response": "",
|
||
"actions": [],
|
||
"route_flag": "",
|
||
"suggestedReplies": [],
|
||
"error_message": ""
|
||
}
|
||
|
||
return await app.ainvoke(initial_state)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
async def test():
|
||
history_message = json.dumps(
|
||
[
|
||
{"role": "user", "content": "我需要生成一份项目总结报告"},
|
||
{"role": "assistant", "content": "好的,我可以帮您生成项目总结报告,请提供相关项目信息。"},
|
||
{"role": "user",
|
||
"content": "项目名称:智能运维系统,项目周期:2024年1月-2024年6月,主要成果:完成了系统架构设计和核心模块开发"}
|
||
],
|
||
ensure_ascii=False,
|
||
)
|
||
|
||
result = await run_other_report_workflow(
|
||
extracted_text="项目名称:智能运维系统,项目周期:2024年1月-2024年6月,主要成果:完成了系统架构设计和核心模块开发",
|
||
combined_query="用户问题:请生成项目总结报告",
|
||
history_message=history_message
|
||
)
|
||
|
||
print("====== 报告文本 ======")
|
||
print(result["response"])
|
||
print("\n====== 推荐问题 ======")
|
||
for reply in result.get("suggestedReplies", []):
|
||
print(f"- {reply['title']}: {reply['content']}")
|
||
print("\n====== 操作按钮 ======")
|
||
for action in result.get("actions", []):
|
||
print(f"- {action.get('label', 'Unknown')}: {action.get('type', 'Unknown')}")
|
||
|
||
|
||
asyncio.run(test())
|
||
|
||
|
||
|
||
|
||
|