545 lines
19 KiB
Python
545 lines
19 KiB
Python
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
|
||
|
||
|
||
from workflows.history_manager import init_history, build_history_str
|
||
|
||
|
||
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:
|
||
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
|
||
|
||
|
||
class HistoryQAAnalysisState(TypedDict):
|
||
"""历史问答分析工作流状态"""
|
||
|
||
extracted_text: str
|
||
combined_query: str
|
||
|
||
history_message: str
|
||
history_messages: List[Dict[str, Any]]
|
||
|
||
extracted_items: List[Dict[str, Any]]
|
||
extracted_images: List[Dict[str, str]]
|
||
report_title: str
|
||
report_text: str
|
||
|
||
response: str
|
||
actions: List[Dict[str, Any]]
|
||
route_flag: Literal["", "report"]
|
||
suggestedReplies: List[Dict[str, Any]]
|
||
|
||
error_message: str
|
||
|
||
|
||
async def extract_analysis_items(state: HistoryQAAnalysisState) -> 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", "")
|
||
|
||
pre_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:报告标题(根据历史对话内容生成一个简洁的标题,格式为"XXX故障处理报告",不超过20个字)
|
||
- items:抽取记录数组,每条记录包含:
|
||
- topic:问题主题(如 设备、系统、管理、流程、保障、训练 等)
|
||
- key_entity:关键对象或名词
|
||
- conclusion:结论 / 判断 / 回答要点
|
||
- action_suggestion:建议或行动(若无可为空)
|
||
- date:时间(若无法判断可为空)
|
||
- images:图片信息数组(仅当有用户上传图片时提取,每条记录包含):
|
||
- url:图片URL(直接从【用户上传图片信息】中提取)
|
||
- summary:图片内容摘要(根据图片识别结果提炼关键信息,如设备类型、故障现象、异常情况等,不超过100字)
|
||
|
||
【输出要求】
|
||
1. 仅输出 JSON 对象
|
||
2. 不要附加任何解释
|
||
3. 如果没有图片信息,images字段返回空数组
|
||
"""
|
||
|
||
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):
|
||
extracted_items = parsed_result.get("items", [])
|
||
report_title = parsed_result.get("report_title", "历史问答分析报告")
|
||
model_images = parsed_result.get("images", [])
|
||
|
||
|
||
else:
|
||
extracted_items = parsed_result if isinstance(parsed_result, list) else []
|
||
report_title = "历史问答分析报告"
|
||
model_images = []
|
||
except Exception as e:
|
||
extracted_items = []
|
||
report_title = "历史问答分析报告"
|
||
model_images = []
|
||
|
||
final_images = pre_extracted_images if pre_extracted_images else model_images
|
||
|
||
return {"extracted_items": extracted_items, "report_title": report_title, "extracted_images": final_images}
|
||
|
||
|
||
# @track_function_calls
|
||
async def generate_analysis_report_text(state: HistoryQAAnalysisState) -> Dict[str, Any]:
|
||
from utils.function_tracker import get_all_callbacks
|
||
import time
|
||
|
||
extracted_items = state.get("extracted_items", [])
|
||
report_title = state.get("report_title", "历史问答分析报告")
|
||
combined_query = state.get("combined_query", "") or state.get("extracted_text", "") or ""
|
||
history_messages = state.get("history_messages", [])
|
||
extracted_images = state.get("extracted_images", [])
|
||
history_message = state.get("history_message", "")
|
||
|
||
def summarize_extracted_items(items) -> str:
|
||
"""将抽取结果转为中文描述,兼容 list / dict"""
|
||
|
||
if not items:
|
||
return "未提取到有效分析数据"
|
||
|
||
# 如果是单个 dict,转为 list
|
||
if isinstance(items, dict):
|
||
items = [items]
|
||
|
||
# 如果不是 list,直接返回
|
||
if not isinstance(items, list):
|
||
return "分析数据格式异常"
|
||
|
||
topics = set()
|
||
entities = set()
|
||
|
||
for item in items:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
|
||
topic = item.get("topic")
|
||
entity = item.get("key_entity")
|
||
|
||
if topic:
|
||
topics.add(str(topic))
|
||
if entity:
|
||
entities.add(str(entity))
|
||
|
||
topic_text = "、".join(list(topics)[:5]) if topics else "未识别"
|
||
entity_text = "、".join(list(entities)[:5]) if entities else "未识别"
|
||
|
||
return f"识别主题 {len(items)} 条,涉及主题:{topic_text};关键对象:{entity_text}"
|
||
|
||
details_text = summarize_extracted_items(extracted_items)
|
||
loading_prompts = [
|
||
{"title": "📄 生成分析报告文本", "details": details_text},
|
||
{"title": "✍️ 正在撰写分析报告", "details": details_text},
|
||
{"title": "📋 生成历史问答分析报告", "details": details_text},
|
||
{"title": "📝 构建分析报告结构", "details": details_text},
|
||
{"title": "🎯 汇总历史问答数据生成报告", "details": details_text},
|
||
]
|
||
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 history_message:
|
||
history_context = history_message
|
||
|
||
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>
|
||
|
||
<extracted_items>
|
||
{extracted_items}
|
||
</extracted_items>
|
||
|
||
<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. **使用段落形式,如果有表格生成,生成表格必须arkdown表格,不要html形式**
|
||
3. **如果给定结果在历史对话信息中没有,可以进行删除**
|
||
4. **输出内容必须严格按照历史对话信息内容进行生成,不得编造不实信息和不相干信息**
|
||
5. 不得输出解释说明
|
||
6. 不要使用列表符号
|
||
7. 所有内容使用完整中文句子。
|
||
8. **每个段落首行必须缩进,使用两个全角空格( )实现首行缩进两个中文字符的效果**
|
||
{"9. **报告中必须包含用户上传的图片信息,在故障信息部分引用图片URL**" if extracted_images else ""}
|
||
|
||
</output_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>
|
||
只输出完整报告正文,不要任何解释说明。
|
||
</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)
|
||
|
||
safe_topic = re.sub(r'[\\/:*?"<>|]', '_', report_title)
|
||
file_name = f"{safe_topic}.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("report")
|
||
download_url = f"http://192.168.1.164:9088/api/download/{file_name}"
|
||
|
||
actions.append({
|
||
"type": "download",
|
||
"label": f"{report_title}.docx",
|
||
"url": download_url,
|
||
"fileName": file_name,
|
||
"content": report_text
|
||
})
|
||
else:
|
||
error_msg = f"\n\n[注意] Word 文档生成失败"
|
||
report_text += error_msg
|
||
|
||
# actions.append({
|
||
# "type": "regenerate",
|
||
# "label": "重新生成",
|
||
# "icon": "report"
|
||
# })
|
||
|
||
suggested_replies = await generate_suggested_replies_qa_report(report_text)
|
||
|
||
return {
|
||
"report_text": report_text,
|
||
"response": report_text,
|
||
"actions": actions,
|
||
"route_flag": "report",
|
||
"suggestedReplies": [],
|
||
# "suggestedReplies": suggested_replies,
|
||
"__details__": "分析报告生成完成"
|
||
}
|
||
except Exception as e:
|
||
# error_event = {
|
||
# "type": "function_execution",
|
||
# "title": "步骤二失败:生成分析报告",
|
||
# "details": f"错误: {str(e)}",
|
||
# "has_result": False
|
||
# }
|
||
# for callback in get_all_callbacks():
|
||
# try:
|
||
# callback(error_event)
|
||
# except Exception:
|
||
# pass
|
||
|
||
return {
|
||
"report_text": f"生成分析报告失败: {str(e)}",
|
||
"response": f"生成分析报告失败: {str(e)}",
|
||
"actions": [],
|
||
"route_flag": "report",
|
||
"suggestedReplies": [],
|
||
"error_message": str(e),
|
||
"__details__": f"生成分析报告失败: {str(e)}"
|
||
}
|
||
|
||
|
||
async def generate_suggested_replies_qa_report(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_history_qa_analysis_workflow():
|
||
graph = StateGraph(HistoryQAAnalysisState)
|
||
|
||
graph.add_node("extract_items", extract_analysis_items)
|
||
graph.add_node("generate_text", generate_analysis_report_text)
|
||
|
||
graph.add_edge(START, "extract_items")
|
||
graph.add_edge("extract_items", "generate_text")
|
||
graph.add_edge("generate_text", END)
|
||
|
||
return graph.compile()
|
||
|
||
|
||
# @track_function_calls
|
||
async def run_qa_report_agent(
|
||
extracted_text: str,
|
||
combined_query: str,
|
||
history_message: str = "",
|
||
route_flag: str = "",
|
||
extracted_images: List[Dict[str, str]] = None
|
||
) -> Dict[str, Any]:
|
||
app = create_history_qa_analysis_workflow()
|
||
|
||
initial_state: HistoryQAAnalysisState = {
|
||
"extracted_text": extracted_text,
|
||
"combined_query": combined_query,
|
||
**init_history(history_message),
|
||
"extracted_items": [],
|
||
"extracted_images": extracted_images or [],
|
||
"report_title": "",
|
||
"report_text": "",
|
||
"response": "",
|
||
"actions": [],
|
||
"route_flag": "",
|
||
"suggestedReplies": [],
|
||
"error_message": ""
|
||
}
|
||
|
||
return await app.ainvoke(initial_state)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
import asyncio
|
||
|
||
|
||
async def test():
|
||
history_message = json.dumps(
|
||
[
|
||
{"role": "user", "content": "我需要分析最近的历史问答数据"},
|
||
{"role": "assistant", "content": "好的,我可以帮您生成历史问答分析报告,请提供相关数据。"},
|
||
{"role": "user", "content": "请重点关注设备故障和维护相关的内容"}
|
||
],
|
||
ensure_ascii=False,
|
||
)
|
||
|
||
result = await run_qa_report_agent(
|
||
extracted_text="请重点关注设备故障和维护相关的内容",
|
||
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())
|
||
|
||
|
||
|