394 lines
12 KiB
Python
394 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
AI分析模块 - 支持多模型切换
|
||
"""
|
||
import os
|
||
import json
|
||
import urllib.request
|
||
import urllib.error
|
||
import ssl
|
||
import logging
|
||
from datetime import datetime
|
||
from flask import current_app
|
||
from tag_names import get_tag_description
|
||
|
||
# 设置日志
|
||
logging.basicConfig(level=logging.INFO)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def call_ai_api(prompt, model_key=None, system_prompt=None):
|
||
"""
|
||
调用AI API进行分析
|
||
:param prompt: 用户提示词
|
||
:param model_key: 模型key (kimi/deepseek/qwen/zhipu)
|
||
:param system_prompt: 系统提示词
|
||
:return: {'status': str, 'content': str, 'model': str}
|
||
"""
|
||
if model_key is None:
|
||
model_key = current_app.config.get('DEFAULT_AI_MODEL', 'kimi')
|
||
|
||
models = current_app.config.get('AI_MODELS', {})
|
||
if model_key not in models:
|
||
return {'status': 'error', 'content': f'未找到AI模型: {model_key}', 'model': model_key}
|
||
|
||
model_config = models[model_key]
|
||
api_key = model_config.get('api_key', '')
|
||
|
||
if not api_key:
|
||
return {
|
||
'status': 'error',
|
||
'content': f'未配置AI模型 {model_config["name"]} 的API Key,请在 config.py 中配置',
|
||
'model': model_key
|
||
}
|
||
|
||
# 构建消息
|
||
messages = []
|
||
if system_prompt:
|
||
messages.append({"role": "system", "content": system_prompt})
|
||
messages.append({"role": "user", "content": prompt})
|
||
|
||
# 构建请求
|
||
payload = {
|
||
"model": model_config['model'],
|
||
"messages": messages
|
||
}
|
||
|
||
data = json.dumps(payload).encode('utf-8')
|
||
req = urllib.request.Request(
|
||
model_config['api_url'],
|
||
data=data,
|
||
headers={
|
||
'Content-Type': 'application/json',
|
||
'Authorization': f'Bearer {api_key}'
|
||
}
|
||
)
|
||
|
||
ctx = ssl.create_default_context()
|
||
ctx.check_hostname = False
|
||
ctx.verify_mode = ssl.CERT_NONE
|
||
|
||
import time
|
||
try:
|
||
logger.info(f"AI API请求开始 - 模型: {model_config['model']}")
|
||
|
||
# 从配置中获取超时时间,默认300秒(5分钟)
|
||
api_timeout = current_app.config.get('AI_API_TIMEOUT', 300)
|
||
logger.info(f"AI API超时设置: {api_timeout}秒")
|
||
|
||
start_time = time.time()
|
||
with urllib.request.urlopen(req, context=ctx, timeout=api_timeout) as response:
|
||
elapsed = time.time() - start_time
|
||
logger.info(f"AI API响应成功,耗时: {elapsed:.1f}秒")
|
||
raw_response = response.read().decode('utf-8')
|
||
result = json.loads(raw_response)
|
||
|
||
# 解析响应 - 兼容OpenAI格式
|
||
content = ''
|
||
if 'choices' in result and len(result['choices']) > 0:
|
||
content = result['choices'][0].get('message', {}).get('content', '')
|
||
|
||
return {
|
||
'status': 'success',
|
||
'content': content,
|
||
'model': model_key,
|
||
'model_name': model_config['name'],
|
||
'usage': result.get('usage', {})
|
||
}
|
||
except urllib.error.HTTPError as e:
|
||
error_body = e.read().decode('utf-8')
|
||
logger.error(f"HTTP错误: {e.code}, 响应: {error_body}")
|
||
return {
|
||
'status': 'error',
|
||
'content': f'API请求失败: HTTP {e.code}\n{error_body}',
|
||
'model': model_key
|
||
}
|
||
except urllib.error.URLError as e:
|
||
logger.error(f"URL错误: {str(e.reason)}")
|
||
return {
|
||
'status': 'error',
|
||
'content': f'网络连接失败: {str(e.reason)}',
|
||
'model': model_key
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"异常: {type(e).__name__}: {str(e)}")
|
||
return {
|
||
'status': 'error',
|
||
'content': f'AI分析异常: {type(e).__name__}: {str(e)}',
|
||
'model': model_key
|
||
}
|
||
|
||
|
||
def analyze_fault(data, model_key=None):
|
||
"""
|
||
故障诊断分析
|
||
:param data: 采集数据列表
|
||
:param model_key: AI模型
|
||
:return: AI分析结果(包含data_summary字段,方便前端展示)
|
||
"""
|
||
# 构建数据摘要(控制数量防止上下文超限)
|
||
data_summary = build_data_summary(data)
|
||
|
||
system_prompt = """你是一个专业的综采工作面设备故障诊断专家,具有丰富的煤矿机电设备维修经验。
|
||
请根据提供的实时监测数据,进行以下分析:
|
||
1. 识别存在的故障和异常
|
||
2. 分析故障原因
|
||
3. 提供维修建议和措施
|
||
4. 评估故障严重程度(正常/轻微/中等/严重/紧急)
|
||
请保持分析专业、简洁、实用。"""
|
||
|
||
prompt = f"""请对以下综采工作面实时监测数据进行故障诊断分析:
|
||
|
||
{data_summary}
|
||
|
||
请按以下格式输出分析结果:
|
||
|
||
## 故障诊断报告
|
||
|
||
### 1. 异常识别
|
||
(列出发现的异常项)
|
||
|
||
### 2. 故障原因分析
|
||
(分析可能的原因)
|
||
|
||
### 3. 维修建议
|
||
(提供具体的维修措施)
|
||
|
||
### 4. 严重程度评估
|
||
(整体评估)"""
|
||
|
||
result = call_ai_api(prompt, model_key, system_prompt)
|
||
# 附加数据摘要,让前端可以展示传入AI的数据
|
||
result['data_summary'] = data_summary
|
||
result['data_count'] = len(data) if data else 0
|
||
return result
|
||
|
||
|
||
def analyze_trend(data, model_key=None):
|
||
"""
|
||
趋势分析
|
||
:param data: 历史数据列表
|
||
:param model_key: AI模型
|
||
:return: AI分析结果(包含data_summary字段,方便前端展示)
|
||
"""
|
||
# 构建趋势摘要(控制数量防止上下文超限)
|
||
data_summary = build_trend_summary(data)
|
||
|
||
system_prompt = """你是一个专业的煤矿生产数据分析专家,擅长分析设备运行趋势,预测潜在问题。
|
||
请根据提供的历史数据,进行趋势分析,预测可能的设备状态变化。"""
|
||
|
||
prompt = f"""请对以下综采工作面历史数据进行趋势分析:
|
||
|
||
{data_summary}
|
||
|
||
请按以下格式输出分析结果:
|
||
|
||
## 趋势分析报告
|
||
|
||
### 1. 数据趋势概述
|
||
(描述整体趋势)
|
||
|
||
### 2. 关键参数变化
|
||
(分析重要参数的变化规律)
|
||
|
||
### 3. 预测与预警
|
||
(基于趋势的预测和预警)
|
||
|
||
### 4. 优化建议
|
||
(生产和设备维护建议)"""
|
||
|
||
result = call_ai_api(prompt, model_key, system_prompt)
|
||
# 附加数据摘要,让前端可以展示传入AI的数据
|
||
result['data_summary'] = data_summary
|
||
result['data_count'] = len(data) if data else 0
|
||
return result
|
||
|
||
|
||
def generate_report(data, unit_name, model_key=None):
|
||
"""
|
||
生成运维报告
|
||
:param data: 数据列表
|
||
:param unit_name: 单位名称
|
||
:param model_key: AI模型
|
||
:return: AI生成的报告(包含data_summary字段)
|
||
"""
|
||
# 构建数据摘要(控制数量防止上下文超限)
|
||
data_summary = build_data_summary(data)
|
||
|
||
system_prompt = """你是一个专业的煤矿运维报告撰写专家,能够根据设备运行数据生成专业的运维分析报告。
|
||
报告应该专业、规范,符合煤矿行业标准。"""
|
||
|
||
prompt = f"""请为{unit_name}生成一份设备运维分析报告:
|
||
|
||
采集时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||
|
||
监测数据:
|
||
{data_summary}
|
||
|
||
请按以下格式生成报告:
|
||
|
||
# {unit_name}设备运维分析报告
|
||
|
||
## 一、概述
|
||
(简要说明本次分析的范围和时间)
|
||
|
||
## 二、设备运行状态
|
||
(描述各主要设备的运行状态)
|
||
|
||
## 三、关键参数分析
|
||
(分析电流、电压、温度、压力等关键参数)
|
||
|
||
## 四、存在问题
|
||
(列出发现的问题)
|
||
|
||
## 五、改进建议
|
||
(提供针对性的改进措施)
|
||
|
||
## 六、总结
|
||
(整体评价和下一步工作建议)"""
|
||
|
||
result = call_ai_api(prompt, model_key, system_prompt)
|
||
result['data_summary'] = data_summary
|
||
result['data_count'] = len(data) if data else 0
|
||
return result
|
||
|
||
|
||
def build_data_summary(data, max_total=80, max_per_category=15):
|
||
"""
|
||
构建数据摘要(使用中文名称)
|
||
:param data: 数据列表
|
||
:param max_total: 最大总条目数,防止上下文超限
|
||
:param max_per_category: 每类最大条目数
|
||
:return: 数据摘要字符串
|
||
"""
|
||
if not data:
|
||
return "无数据"
|
||
|
||
lines = []
|
||
total = len(data)
|
||
lines.append(f"共采集 {total} 个监测点(以下展示关键数据):")
|
||
|
||
# 按设备分类
|
||
categories = {}
|
||
for item in data:
|
||
tag = item.get('tag_short_name', item.get('tag_name', ''))
|
||
value = item.get('tag_value', item.get('value'))
|
||
value_bool = item.get('tag_value_bool', item.get('value_bool'))
|
||
unit = item.get('tag_unit', '')
|
||
|
||
# 优先使用已有的中文名称,否则从映射表查找
|
||
chinese_name = item.get('chinese_name')
|
||
if not chinese_name:
|
||
chinese_name = get_tag_description(tag)
|
||
|
||
# 分类
|
||
if 'Shearer' in tag or 'shearer' in tag:
|
||
cat = '采煤机'
|
||
elif any(x in tag for x in ['AFC', 'LOA', 'CRU']):
|
||
cat = '三机'
|
||
elif 'Emulsion' in tag or 'Spray' in tag:
|
||
cat = '泵站'
|
||
elif 'Loop' in tag:
|
||
cat = '开关'
|
||
else:
|
||
cat = '其他'
|
||
|
||
if cat not in categories:
|
||
categories[cat] = []
|
||
|
||
display_value = value if value is not None else value_bool
|
||
# 中文名称与英文标签都显示,方便AI理解
|
||
categories[cat].append(f" - {chinese_name}({tag}): {display_value} {unit}")
|
||
|
||
# 控制总条目数,优先展示关键设备
|
||
cat_priority = ['采煤机', '三机', '泵站', '开关', '其他']
|
||
remaining = max_total
|
||
|
||
for cat in cat_priority:
|
||
if cat not in categories:
|
||
continue
|
||
items = categories[cat]
|
||
# 每类限制条目数,同时考虑剩余配额
|
||
limit = min(max_per_category, remaining)
|
||
if limit <= 0:
|
||
continue
|
||
|
||
lines.append(f"\n### {cat}")
|
||
lines.extend(items[:limit])
|
||
if len(items) > limit:
|
||
lines.append(f" ... 还有 {len(items)-limit} 条(省略)")
|
||
remaining -= limit
|
||
|
||
# 处理未在优先级中的类别
|
||
for cat, items in categories.items():
|
||
if cat not in cat_priority and remaining > 0:
|
||
limit = min(max_per_category, remaining)
|
||
lines.append(f"\n### {cat}")
|
||
lines.extend(items[:limit])
|
||
if len(items) > limit:
|
||
lines.append(f" ... 还有 {len(items)-limit} 条(省略)")
|
||
remaining -= limit
|
||
|
||
return '\n'.join(lines)
|
||
|
||
|
||
def build_trend_summary(data, max_tags=30):
|
||
"""
|
||
构建趋势数据摘要(使用中文名称)
|
||
:param data: 历史数据列表
|
||
:param max_tags: 最多展示的标签数量,防止上下文超限
|
||
:return: 趋势摘要字符串
|
||
"""
|
||
if not data:
|
||
return "无数据"
|
||
|
||
# 按标签分组
|
||
tag_data = {}
|
||
for item in data:
|
||
tag = item.get('tag_short_name', item.get('tag_name', ''))
|
||
time = item.get('snapshot_time', item.get('sample_time', ''))
|
||
value = item.get('tag_value', item.get('value', 0))
|
||
|
||
if tag not in tag_data:
|
||
tag_data[tag] = []
|
||
tag_data[tag].append({'time': str(time), 'value': value})
|
||
|
||
lines = []
|
||
count = 0
|
||
for tag, points in tag_data.items():
|
||
if count >= max_tags:
|
||
break
|
||
if len(points) >= 2:
|
||
values = [p['value'] for p in points if p['value'] is not None]
|
||
if values:
|
||
min_val = min(values)
|
||
max_val = max(values)
|
||
avg_val = sum(values) / len(values)
|
||
# 使用中文名称
|
||
chinese_name = get_tag_description(tag)
|
||
lines.append(f"- {chinese_name}({tag}): 最小={min_val}, 最大={max_val}, 平均={avg_val:.2f}, 采样点数={len(points)}")
|
||
count += 1
|
||
|
||
total_tags = len(tag_data)
|
||
result = '\n'.join(lines) if lines else "数据不足以进行趋势分析"
|
||
if total_tags > max_tags and count >= max_tags:
|
||
result += f"\n... 还有 {total_tags - max_tags} 个标签(省略)"
|
||
return result
|
||
|
||
|
||
def get_available_models():
|
||
"""获取可用的AI模型列表"""
|
||
models = current_app.config.get('AI_MODELS', {})
|
||
result = []
|
||
|
||
for key, config in models.items():
|
||
has_key = bool(config.get('api_key', ''))
|
||
result.append({
|
||
'key': key,
|
||
'name': config['name'],
|
||
'model': config['model'],
|
||
'available': has_key
|
||
})
|
||
|
||
return result |