#!/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