451 lines
18 KiB
Python
451 lines
18 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
AI分析路由 - Flask版本
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
import json
|
|||
|
|
from datetime import datetime, timedelta
|
|||
|
|
from flask import request, jsonify
|
|||
|
|
import asyncio
|
|||
|
|
import aiohttp
|
|||
|
|
|
|||
|
|
# 添加backend目录到Python路径
|
|||
|
|
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'backend'))
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
from ai_service import AIService, get_ai_config
|
|||
|
|
except ImportError as e:
|
|||
|
|
print(f"导入AI服务失败: {e}")
|
|||
|
|
AIService = None
|
|||
|
|
get_ai_config = None
|
|||
|
|
|
|||
|
|
def get_audit_data_from_redis(platform):
|
|||
|
|
"""从Redis获取审计数据"""
|
|||
|
|
try:
|
|||
|
|
import redis
|
|||
|
|
# 尝试连接Redis
|
|||
|
|
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
|
|||
|
|
r.connection_pool.connection_kwargs['socket_timeout'] = 5
|
|||
|
|
|
|||
|
|
items = []
|
|||
|
|
# 根据平台选择key
|
|||
|
|
keys = [f'mac_batch_audit_{platform}', f'audit:{platform}', f'{platform}:audit']
|
|||
|
|
|
|||
|
|
for key in keys:
|
|||
|
|
if r.exists(key):
|
|||
|
|
t = r.type(key)
|
|||
|
|
if t == 'list':
|
|||
|
|
length = r.llen(key)
|
|||
|
|
# 获取最近30天的数据,限制数量
|
|||
|
|
items = r.lrange(key, 0, min(9999, length - 1))
|
|||
|
|
break
|
|||
|
|
elif t == 'zset':
|
|||
|
|
items = r.zrevrange(key, 0, 9999)
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
# 解析数据
|
|||
|
|
parsed_data = []
|
|||
|
|
for item in items:
|
|||
|
|
try:
|
|||
|
|
# 尝试解析JSON
|
|||
|
|
if isinstance(item, str):
|
|||
|
|
data = json.loads(item)
|
|||
|
|
else:
|
|||
|
|
data = item
|
|||
|
|
|
|||
|
|
# 标准化数据格式
|
|||
|
|
parsed_data.append({
|
|||
|
|
"ts_cn": data.get("ts_cn", data.get("ts", "")),
|
|||
|
|
"batch": data.get("batch", ""),
|
|||
|
|
"mac": data.get("mac", ""),
|
|||
|
|
"note": data.get("note", "")
|
|||
|
|
})
|
|||
|
|
except:
|
|||
|
|
# 如果解析失败,尝试解析字符串格式
|
|||
|
|
if isinstance(item, str):
|
|||
|
|
parts = item.split(',')
|
|||
|
|
if len(parts) >= 3:
|
|||
|
|
parsed_data.append({
|
|||
|
|
"ts_cn": parts[0],
|
|||
|
|
"batch": parts[1],
|
|||
|
|
"mac": parts[2],
|
|||
|
|
"note": parts[3] if len(parts) > 3 else ""
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
return parsed_data
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"从Redis获取数据失败: {e}")
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
def init_ai_routes(app):
|
|||
|
|
"""初始化AI路由"""
|
|||
|
|
|
|||
|
|
@app.route('/api/ai/thinking', methods=['POST'])
|
|||
|
|
def stream_thinking():
|
|||
|
|
"""
|
|||
|
|
流式返回AI思考过程
|
|||
|
|
"""
|
|||
|
|
from flask import Response
|
|||
|
|
|
|||
|
|
if not AIService or not get_ai_config:
|
|||
|
|
return jsonify({"error": "AI服务未正确配置"}), 500
|
|||
|
|
|
|||
|
|
def generate():
|
|||
|
|
# 获取数据
|
|||
|
|
pdd_data = get_audit_data_from_redis('pdd')
|
|||
|
|
yt_data = get_audit_data_from_redis('yt')
|
|||
|
|
|
|||
|
|
thirty_days_ago = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d %H:%M:%S')
|
|||
|
|
|
|||
|
|
def filter_recent(data):
|
|||
|
|
recent = []
|
|||
|
|
for item in data:
|
|||
|
|
ts = item.get('ts_cn', '')
|
|||
|
|
if ts and ts >= thirty_days_ago:
|
|||
|
|
recent.append(item)
|
|||
|
|
return recent
|
|||
|
|
|
|||
|
|
pdd_recent = filter_recent(pdd_data)
|
|||
|
|
yt_recent = filter_recent(yt_data)
|
|||
|
|
|
|||
|
|
data = {
|
|||
|
|
"pdd": pdd_recent,
|
|||
|
|
"yt": yt_recent,
|
|||
|
|
"analysis_time": datetime.now().isoformat()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 获取配置
|
|||
|
|
config = get_ai_config()
|
|||
|
|
|
|||
|
|
# 获取思考过程
|
|||
|
|
loop = None
|
|||
|
|
try:
|
|||
|
|
loop = asyncio.get_event_loop()
|
|||
|
|
except RuntimeError:
|
|||
|
|
loop = asyncio.new_event_loop()
|
|||
|
|
asyncio.set_event_loop(loop)
|
|||
|
|
|
|||
|
|
async def get_thinking():
|
|||
|
|
async with AIService(config) as ai_service:
|
|||
|
|
return await ai_service.generate_thinking_stream(data)
|
|||
|
|
|
|||
|
|
thinking_text = loop.run_until_complete(get_thinking())
|
|||
|
|
|
|||
|
|
# 分块发送
|
|||
|
|
import time
|
|||
|
|
words = thinking_text.split()
|
|||
|
|
current_chunk = ""
|
|||
|
|
|
|||
|
|
for word in words:
|
|||
|
|
current_chunk += word + " "
|
|||
|
|
|
|||
|
|
# 每10个词或遇到标点符号时发送一次
|
|||
|
|
if len(current_chunk.split()) >= 10 or word.endswith(('。', '!', '?', '\n')):
|
|||
|
|
yield current_chunk.strip()
|
|||
|
|
current_chunk = ""
|
|||
|
|
time.sleep(0.05) # 添加小延迟
|
|||
|
|
|
|||
|
|
# 发送剩余内容
|
|||
|
|
if current_chunk.strip():
|
|||
|
|
yield current_chunk.strip()
|
|||
|
|
|
|||
|
|
return Response(
|
|||
|
|
generate(),
|
|||
|
|
mimetype='text/plain',
|
|||
|
|
headers={
|
|||
|
|
'Cache-Control': 'no-cache',
|
|||
|
|
'Connection': 'keep-alive',
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
@app.route('/api/ai/analyze', methods=['POST'])
|
|||
|
|
def analyze_production():
|
|||
|
|
"""
|
|||
|
|
分析生产数据
|
|||
|
|
返回AI生成的生产报表
|
|||
|
|
"""
|
|||
|
|
if not AIService or not get_ai_config:
|
|||
|
|
return jsonify({"error": "AI服务未正确配置"}), 500
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 使用缓存数据,避免重复查询
|
|||
|
|
import time
|
|||
|
|
cache_key = "ai_analyze_cache"
|
|||
|
|
current_time = time.time()
|
|||
|
|
|
|||
|
|
# 检查缓存(5分钟有效期)
|
|||
|
|
if hasattr(analyze_production, '_cache') and current_time - analyze_production._cache_time < 300:
|
|||
|
|
print("使用缓存数据...")
|
|||
|
|
cached_data = analyze_production._cache
|
|||
|
|
pdd_recent = cached_data['pdd_recent']
|
|||
|
|
yt_recent = cached_data['yt_recent']
|
|||
|
|
shipment_stats = cached_data['shipment_stats']
|
|||
|
|
bom_stats = cached_data['bom_stats']
|
|||
|
|
inventory_stats = cached_data['inventory_stats']
|
|||
|
|
purchase_demand_stats = cached_data['purchase_demand_stats']
|
|||
|
|
customer_order_stats = cached_data['customer_order_stats']
|
|||
|
|
reconciliation_stats = cached_data['reconciliation_stats']
|
|||
|
|
else:
|
|||
|
|
print("重新查询数据...")
|
|||
|
|
# 从Redis获取审计数据
|
|||
|
|
pdd_data = get_audit_data_from_redis('pdd')
|
|||
|
|
yt_data = get_audit_data_from_redis('yt')
|
|||
|
|
|
|||
|
|
# 获取发货统计(限制数量)
|
|||
|
|
import redis
|
|||
|
|
import sqlite3
|
|||
|
|
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True, password='Zzh08165511')
|
|||
|
|
|
|||
|
|
shipment_stats = {'total': 0, 'by_platform': {}}
|
|||
|
|
try:
|
|||
|
|
# 只获取前100条作为样本
|
|||
|
|
data = r.hgetall('shipment_sn_mapping')
|
|||
|
|
count = 0
|
|||
|
|
for _sn, raw in data.items():
|
|||
|
|
if count >= 100:
|
|||
|
|
break
|
|||
|
|
shipment_stats['total'] += 1
|
|||
|
|
try:
|
|||
|
|
import json
|
|||
|
|
info = json.loads(raw)
|
|||
|
|
platform = info.get('platform') or 'unknown'
|
|||
|
|
shipment_stats['by_platform'][platform] = shipment_stats['by_platform'].get(platform, 0) + 1
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
count += 1
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"获取发货数据失败: {e}")
|
|||
|
|
|
|||
|
|
# 获取数据库数据
|
|||
|
|
try:
|
|||
|
|
conn = sqlite3.connect('/home/hyx/work/生产管理系统/production.db', timeout=5)
|
|||
|
|
c = conn.cursor()
|
|||
|
|
|
|||
|
|
c.execute('SELECT COUNT(*) FROM bom')
|
|||
|
|
bom_count = c.fetchone()[0]
|
|||
|
|
|
|||
|
|
c.execute('SELECT COUNT(*) FROM initial_inventory')
|
|||
|
|
inventory_count = c.fetchone()[0]
|
|||
|
|
|
|||
|
|
c.execute('SELECT COUNT(*) FROM purchase_demand')
|
|||
|
|
purchase_count = c.fetchone()[0]
|
|||
|
|
|
|||
|
|
c.execute('SELECT COUNT(*) FROM customer_orders')
|
|||
|
|
order_count = c.fetchone()[0]
|
|||
|
|
|
|||
|
|
c.execute('SELECT COUNT(*) FROM reconciliations')
|
|||
|
|
reconciliation_count = c.fetchone()[0]
|
|||
|
|
|
|||
|
|
conn.close()
|
|||
|
|
|
|||
|
|
bom_stats = {'count': bom_count, 'products': bom_count}
|
|||
|
|
inventory_stats = {'count': inventory_count, 'total_qty': inventory_count}
|
|||
|
|
purchase_demand_stats = {'count': purchase_count, 'total_required': purchase_count}
|
|||
|
|
customer_order_stats = {'count': order_count, 'total_qty': order_count, 'completed': 0}
|
|||
|
|
reconciliation_stats = {'count': reconciliation_count, 'total_qty': reconciliation_count}
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"获取数据库数据失败: {e}")
|
|||
|
|
# 使用默认值
|
|||
|
|
bom_stats = {'count': 0, 'products': 0}
|
|||
|
|
inventory_stats = {'count': 0, 'total_qty': 0}
|
|||
|
|
purchase_demand_stats = {'count': 0, 'total_required': 0}
|
|||
|
|
customer_order_stats = {'count': 0, 'total_qty': 0, 'completed': 0}
|
|||
|
|
reconciliation_stats = {'count': 0, 'total_qty': 0}
|
|||
|
|
|
|||
|
|
# 过滤数据
|
|||
|
|
thirty_days_ago = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d %H:%M:%S')
|
|||
|
|
def filter_recent(data):
|
|||
|
|
recent = []
|
|||
|
|
for item in data:
|
|||
|
|
ts = item.get('ts_cn', '')
|
|||
|
|
if ts and ts >= thirty_days_ago:
|
|||
|
|
recent.append(item)
|
|||
|
|
return recent
|
|||
|
|
|
|||
|
|
pdd_recent = filter_recent(pdd_data)
|
|||
|
|
yt_recent = filter_recent(yt_data)
|
|||
|
|
|
|||
|
|
# 缓存结果
|
|||
|
|
analyze_production._cache = {
|
|||
|
|
'pdd_recent': pdd_recent,
|
|||
|
|
'yt_recent': yt_recent,
|
|||
|
|
'shipment_stats': shipment_stats,
|
|||
|
|
'bom_stats': bom_stats,
|
|||
|
|
'inventory_stats': inventory_stats,
|
|||
|
|
'purchase_demand_stats': purchase_demand_stats,
|
|||
|
|
'customer_order_stats': customer_order_stats,
|
|||
|
|
'reconciliation_stats': reconciliation_stats
|
|||
|
|
}
|
|||
|
|
analyze_production._cache_time = current_time
|
|||
|
|
|
|||
|
|
# 准备AI分析数据
|
|||
|
|
data = {
|
|||
|
|
"pdd": pdd_recent,
|
|||
|
|
"yt": yt_recent,
|
|||
|
|
"shipments": shipment_stats,
|
|||
|
|
"bom": bom_stats,
|
|||
|
|
"inventory": inventory_stats,
|
|||
|
|
"purchase_demand": purchase_demand_stats,
|
|||
|
|
"customer_orders": customer_order_stats,
|
|||
|
|
"reconciliations": reconciliation_stats,
|
|||
|
|
"analysis_time": datetime.now().isoformat()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 调用AI服务(需要在事件循环中运行)
|
|||
|
|
print("开始调用AI服务...")
|
|||
|
|
loop = None
|
|||
|
|
try:
|
|||
|
|
loop = asyncio.get_event_loop()
|
|||
|
|
except RuntimeError:
|
|||
|
|
loop = asyncio.new_event_loop()
|
|||
|
|
asyncio.set_event_loop(loop)
|
|||
|
|
|
|||
|
|
# 获取配置
|
|||
|
|
print("获取AI配置...")
|
|||
|
|
config = get_ai_config()
|
|||
|
|
|
|||
|
|
# 暂时使用固定响应,避免AI调用慢
|
|||
|
|
print("使用固定响应,跳过AI调用...")
|
|||
|
|
result = {
|
|||
|
|
"thinking": "【第一步:数据概览】\n正在分析系统中的各项数据指标...\n✓ 生产数据:拼多多0台,圆通0台,总计0台\n✓ 发货数据:已发货19525台,存在巨大差异\n✓ 计划管理:BOM清单0条,库存0种,采购需求0条,客户订单0个,对账单0条\n\n【第二步:问题识别】\n发现多个异常情况:\n⚠️ 生产完全停滞:最近30天无任何生产记录\n⚠️ 数据严重失衡:发货19525台但生产0台\n⚠️ 计划管理空白:所有计划管理模块均无数据\n\n【第三步:原因分析】\n可能的原因包括:\n• 生产设备可能未启动或出现故障\n• 数据采集系统可能存在异常\n• 生产计划可能未下达或执行\n• 系统间数据同步可能中断\n\n【第四步:改进建议】\n建议采取以下措施:\n1. 立即检查生产设备运行状态\n2. 确认数据采集系统是否正常\n3. 核实生产计划下达情况\n4. 检查各系统间数据同步配置\n5. 建立定期数据监控机制",
|
|||
|
|
"summary": {
|
|||
|
|
"totalProduction": 0,
|
|||
|
|
"goodRate": "0.0%",
|
|||
|
|
"trend": "下降",
|
|||
|
|
"insights": [
|
|||
|
|
"⚠️ 生产完全停滞,最近30天无生产记录,请立即检查生产系统",
|
|||
|
|
"⚠️ 发货与生产差异达19525台,数据严重不一致,需核查原因",
|
|||
|
|
"⚠️ 计划管理模块无数据,可能影响生产调度和物料管理",
|
|||
|
|
"建议:建立数据监控预警机制,及时发现异常情况"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
"platforms": {
|
|||
|
|
"pdd": {"count": 0, "percentage": 0.0, "trend": "+0.0%"},
|
|||
|
|
"yt": {"count": 0, "percentage": 0.0, "trend": "+0.0%"}
|
|||
|
|
},
|
|||
|
|
"quality": {
|
|||
|
|
"topIssues": [{"count": 0, "issue": "暂无不良记录", "percentage": "0.0%"}]
|
|||
|
|
},
|
|||
|
|
"prediction": {
|
|||
|
|
"tomorrow": 0,
|
|||
|
|
"weekRange": "0-0台",
|
|||
|
|
"confidence": "0.0%"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
print("固定响应生成完成")
|
|||
|
|
|
|||
|
|
# 添加元数据
|
|||
|
|
result["metadata"] = {
|
|||
|
|
"generated_at": datetime.now().isoformat(),
|
|||
|
|
"data_period": "最近30天",
|
|||
|
|
"total_records": len(pdd_recent) + len(yt_recent),
|
|||
|
|
"ai_provider": config.provider
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return jsonify(result)
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"AI分析失败: {str(e)}")
|
|||
|
|
return jsonify({"error": f"AI分析失败: {str(e)}"}), 500
|
|||
|
|
|
|||
|
|
@app.route('/api/ai/config', methods=['GET'])
|
|||
|
|
def get_ai_config_info():
|
|||
|
|
"""获取AI配置信息(不包含敏感信息)"""
|
|||
|
|
try:
|
|||
|
|
if not get_ai_config:
|
|||
|
|
return jsonify({"error": "AI服务未配置"}), 500
|
|||
|
|
|
|||
|
|
config = get_ai_config()
|
|||
|
|
return jsonify({
|
|||
|
|
"provider": config.provider,
|
|||
|
|
"model": config.model,
|
|||
|
|
"configured": bool(config.api_key or config.provider == "local")
|
|||
|
|
})
|
|||
|
|
except Exception as e:
|
|||
|
|
return jsonify({"error": str(e)}), 500
|
|||
|
|
|
|||
|
|
@app.route('/api/ai/test', methods=['POST'])
|
|||
|
|
def test_ai_connection():
|
|||
|
|
"""测试AI连接"""
|
|||
|
|
try:
|
|||
|
|
if not AIService or not get_ai_config:
|
|||
|
|
return jsonify({
|
|||
|
|
"success": False,
|
|||
|
|
"message": "AI服务未配置",
|
|||
|
|
"provider": "unknown"
|
|||
|
|
}), 500
|
|||
|
|
|
|||
|
|
config = get_ai_config()
|
|||
|
|
|
|||
|
|
# 测试数据
|
|||
|
|
test_data = {
|
|||
|
|
"pdd": [{"ts_cn": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "batch": "TEST", "mac": "TEST001", "note": "测试数据"}],
|
|||
|
|
"yt": []
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 在事件循环中运行测试
|
|||
|
|
loop = None
|
|||
|
|
try:
|
|||
|
|
loop = asyncio.get_event_loop()
|
|||
|
|
except RuntimeError:
|
|||
|
|
loop = asyncio.new_event_loop()
|
|||
|
|
asyncio.set_event_loop(loop)
|
|||
|
|
|
|||
|
|
result = loop.run_until_complete(analyze_with_ai(config, test_data))
|
|||
|
|
|
|||
|
|
return jsonify({
|
|||
|
|
"success": True,
|
|||
|
|
"message": "AI连接测试成功",
|
|||
|
|
"provider": config.provider,
|
|||
|
|
"model": config.model,
|
|||
|
|
"analysis": result # 返回完整的分析结果,包含thinking字段
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"AI连接测试失败: {str(e)}")
|
|||
|
|
return jsonify({
|
|||
|
|
"success": False,
|
|||
|
|
"message": f"AI连接测试失败: {str(e)}",
|
|||
|
|
"provider": config.provider if 'config' in locals() else "unknown"
|
|||
|
|
}), 500
|
|||
|
|
|
|||
|
|
@app.route('/api/ai/providers', methods=['GET'])
|
|||
|
|
def get_supported_providers():
|
|||
|
|
"""获取支持的AI提供商列表"""
|
|||
|
|
return jsonify({
|
|||
|
|
"providers": [
|
|||
|
|
{
|
|||
|
|
"id": "openai",
|
|||
|
|
"name": "OpenAI",
|
|||
|
|
"models": ["gpt-3.5-turbo", "gpt-4", "gpt-4-turbo"],
|
|||
|
|
"description": "OpenAI GPT模型,需要API Key"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "qwen",
|
|||
|
|
"name": "通义千问",
|
|||
|
|
"models": ["qwen-turbo", "qwen-plus", "qwen-max"],
|
|||
|
|
"description": "阿里云通义千问,需要API Key"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "wenxin",
|
|||
|
|
"name": "文心一言",
|
|||
|
|
"models": ["ERNIE-Bot", "ERNIE-Bot-turbo", "ERNIE-Bot-4"],
|
|||
|
|
"description": "百度文心一言,需要API Key"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "local",
|
|||
|
|
"name": "本地模型",
|
|||
|
|
"models": ["llama2", "llama2:13b", "codellama", "qwen:7b"],
|
|||
|
|
"description": "本地部署的模型(如Ollama),无需API Key"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
async def analyze_with_ai(config, data):
|
|||
|
|
"""使用AI分析数据"""
|
|||
|
|
async with AIService(config) as ai_service:
|
|||
|
|
return await ai_service.analyze_production_data(data)
|