""" 通用报告生成工作流 统一接口说明: - 输入: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""" 你是一名专业的报告编写专家。 你的任务是根据用户需求和历史对话生成一份正式的报告。 {combined_query} {report_theme if report_theme else "根据内容自动确定"} {history_context if history_context else "(无历史对话上下文)"} {f''' {image_section} ''' if extracted_images else ''} 必须严格遵守以下规则: 1. 输出为 **正式报告文本** 2. **使用段落形式,如果需要表格,必须使用 Markdown 表格格式** 3. **输出内容必须基于历史对话信息,不得编造不实信息** 4. 不得输出解释说明 5. 所有内容使用完整中文句子 6. **【重要】每个段落首行必须缩进两个中文字符,使用两个全角空格"  "开头,例如:"  这是段落内容。"** 7. 根据报告主题自动选择合适的报告结构,可以是分析报告、总结报告、调研报告等 8. **报告内容必须紧扣报告主题,确保内容相关性和连贯性** {"9. **报告中必须包含用户上传的图片信息,在相关部分引用图片URL**" if extracted_images else ""} **标题格式规范(必须严格遵守):** 1. **报告主标题**:使用二级标题格式,即 `## **标题内容**`,例如:`## **智能运维系统项目总结报告**` 2. **章节大标题**:使用三级标题格式,即 `### **标题内容**`,例如:`### **一、项目概述**` 3. **小节标题**:使用四级标题格式,即 `#### **标题内容**`,例如:`#### **1. 项目背景**` 4. 所有标题必须加粗,使用 `**标题内容**` 格式 5. 标题前后各空一行 **段落格式规范(必须严格遵守):** 1. 每个段落必须以两个全角空格"  "开头 2. 正确示例:   本项目旨在开发一套智能运维系统,用于提升企业IT运维效率。   系统采用微服务架构设计,支持高并发访问。 3. 错误示例(缺少缩进): 本项目旨在开发一套智能运维系统。 4. 段落之间空一行 ## **{report_title}** 报告编号:{report_number} 编制时间:{compile_time} ### **一、概述**   介绍背景与目的。 ### **二、主要内容**   阐述核心问题与关键分析过程。 ### **三、备品备件**   列出本次维修或操作涉及的备品备件的名称(如有机油滤芯、密封圈、螺栓、传感器等),不要输出表格。如果没有涉及备件,请写"无"。 ### **四、结论**   总结结果并提出结论性观点。 {"- **附件图片:**" + chr(10) + chr(10).join([f"![图片{idx}]({img['url']})" for idx, img in enumerate(extracted_images, 1)]) if extracted_images else ""} 只输出完整报告正文,不要任何解释说明。每个段落首行必须使用"  "(两个全角空格)缩进。所有标题必须使用对应级别的Markdown格式并加粗。 """ 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())