Files
aiData/Controller/DegreeController.py
HuangHai 34501faafb 'commit'
2026-01-20 08:09:13 +08:00

292 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import calendar
import json
import logging
from datetime import datetime, date
from decimal import Decimal
from typing import List, Optional
from fastapi import APIRouter, Query
from fastapi.responses import StreamingResponse
from Model.DegreeModel import DegreeModel
from Util.LlmUtil import get_llm_response
router = APIRouter(prefix="/degree")
logger = logging.getLogger('DegreeController')
async def _query_month(year: int, month: int, station_names: Optional[List[str]]):
last_day = calendar.monthrange(year, month)[1]
start_time = f"{year:04d}-{month:02d}-01 00:00:00"
end_time = f"{year:04d}-{month:02d}-{last_day:02d} 23:59:59"
logger.info(
f"query_month year={year}, month={month}, start_time={start_time}, end_time={end_time}, stations={station_names}"
)
model = DegreeModel()
data = await model.get_degree(station_names=station_names, start_time=start_time, end_time=end_time)
return {"count": len(data), "list": data}
@router.get("/by-month")
async def by_month(year: int, month: int, station_names: Optional[List[str]] = Query(None)):
return await _query_month(year, month, station_names)
@router.get("/chat")
async def chat(q: str):
"""
SSE接口接收自然语言查询调用DeepSeek解析意图查询数据并流式返回结果
"""
async def event_generator():
yield f"data: {json.dumps({'step': 'start', 'message': '正在初始化查询...'}, ensure_ascii=False)}\n\n"
print(f"Starting SSE for query: {q}")
# 1. 解析用户意图
now = datetime.now()
system_prompt = f"""
你是一个数据查询助手。当前时间是: {now.strftime('%Y-%m-%d %H:%M:%S')}
用户想查询场站充电度数报表。
请从用户的输入中提取查询参数:
- query_type (str): 查询类型。可选值:
- "month": 查询特定月份的数据(默认)。
- "trend": 查询近三个月的趋势变化数据。
- "company": 查询按企业充电量排名的TOP 10数据。
- year (int): 年份。如果用户未指定且为"month""company"类型,根据当前时间推断。
- month (int): 月份。
- station_names (list[str] or null): 场站名称列表。如果查询所有场站则为null。
用户输入: "{q}"
请仅返回一个JSON字符串格式如下
{{"query_type": "month", "year": 2025, "month": 12, "station_names": null}}
或者:
{{"query_type": "trend", "station_names": null}}
或者:
{{"query_type": "company", "year": 2025, "month": 12}}
不要包含Markdown代码块或其他文本。
"""
# 获取意图解析结果(非流式读取)
intent_json_str = ""
try:
print("Requesting intent analysis from LLM...")
async for chunk in get_llm_response(system_prompt, stream=True):
intent_json_str += chunk
print(f"Intent analysis received: {intent_json_str}")
except Exception as e:
print(f"Error during intent analysis: {e}")
yield f"data: {json.dumps({'step': 'error', 'message': f'意图解析失败: {str(e)}'})}\n\n"
return
try:
# 清理可能的markdown标记
intent_json_str = intent_json_str.replace("```json", "").replace("```", "").strip()
params = json.loads(intent_json_str)
query_type = params.get("query_type", "month")
year = params.get("year")
month = params.get("month")
station_names = params.get("station_names")
model = DegreeModel()
# 1.1 如果用户指定了场站,进行场站名称匹配
if station_names:
yield f"data: {json.dumps({'step': 'analyzing', 'message': '正在匹配场站信息...'}, ensure_ascii=False)}\n\n"
all_stations = await model.getStationList()
all_station_names = [s['station_name'] for s in all_stations]
match_prompt = f"""
你是一个专业的场站名称匹配助手。
用户想查询以下场站的数据:{station_names}
数据库中存在的场站列表如下:
{all_station_names}
请从数据库场站列表中找出与用户描述最匹配的场站名称。
匹配规则:
1. 如果用户描述的名称在列表中能找到完全一致的,直接返回该名称。
2. 如果用户描述的是简称或部分名称(如“净月商贸城”匹配“驿来特充电站(净月商贸城)”),请返回列表中的完整名称。
3. 如果有多个匹配项,请全部返回。
4. 如果完全没有找到匹配的场站,请返回空列表 []。
请仅返回一个JSON数组字符串例如["场站1", "场站2"]
不要包含Markdown代码块或其他文本。
"""
match_result_str = ""
async for chunk in get_llm_response_async(match_prompt, stream=True):
match_result_str += chunk
# 清理并解析匹配结果
match_result_str = match_result_str.replace("```json", "").replace("```", "").strip()
matched_names = json.loads(match_result_str)
if not matched_names:
yield f"data: {json.dumps({'step': 'error', 'message': f'未找到与“{station_names}”相关的场站,请确认场站名称是否正确。'}, ensure_ascii=False)}\n\n"
return
station_names = matched_names
logger.info(f"Matched station names: {station_names}")
# 2. 执行数据查询
yield f"data: {json.dumps({'step': 'querying', 'message': '正在从数据库提取数据...'}, ensure_ascii=False)}\n\n"
if query_type == "trend":
from dateutil.relativedelta import relativedelta
m3_dt = now
m2_dt = now - relativedelta(months=1)
m1_dt = now - relativedelta(months=2)
month3 = m3_dt.strftime('%Y-%m')
month2 = m2_dt.strftime('%Y-%m')
month1 = m1_dt.strftime('%Y-%m')
data = await model.get_trend_change()
summary_prompt = f"""
你是一个专业的充电数据分析师。
用户查询了近三个月所有场站的充电趋势及变化。
数据库返回的数据如下JSON格式
{json.dumps(data, ensure_ascii=False)}
请根据这些数据:
1. 简要分析整体趋势。
2. 识别出变化(增长或减少)最显著的场站。
3. 使用Markdown表格展示数据列名应为场站名称, 第1月电量, 第2月电量, 第3月电量, 净变化, 绝对变化。
4. **重要**:提供一个 ECharts 6.0 柱状图或折线图来展示 TOP 10 变化最大的场站趋势。
- **配色要求**:请使用明亮且对比鲜明的配色方案,避免单调。
- 请确保图表在深色背景下清晰可见。
请使用以下格式输出 ECharts 配置JSON 格式):
```echarts
{{
"title": {{ "text": "充电量趋势变化 TOP 10", "left": "center", "textStyle": {{ "color": "#f8fafc" }} }},
"tooltip": {{ "trigger": "axis", "axisPointer": {{ "type": "shadow" }} }},
"legend": {{ "data": ["{month1}", "{month2}", "{month3}"], "bottom": 0, "textStyle": {{ "color": "#94a3b8" }} }},
"grid": {{ "left": "3%", "right": "4%", "bottom": "10%", "containLabel": true }},
"xAxis": {{ "type": "category", "data": ["场站A", "场站B", ...], "axisLabel": {{ "color": "#94a3b8" }} }},
"yAxis": {{ "type": "value", "axisLabel": {{ "color": "#94a3b8" }}, "splitLine": {{ "lineStyle": {{ "color": "rgba(148, 163, 184, 0.1)" }} }} }},
"series": [
{{ "name": "{month1}", "type": "bar", "data": [100, 200, ...], "itemStyle": {{ "color": "#6366f1", "borderRadius": [4, 4, 0, 0] }} }},
{{ "name": "{month2}", "type": "bar", "data": [120, 180, ...], "itemStyle": {{ "color": "#8b5cf6", "borderRadius": [4, 4, 0, 0] }} }},
{{ "name": "{month3}", "type": "bar", "data": [150, 210, ...], "itemStyle": {{ "color": "#ec4899", "borderRadius": [4, 4, 0, 0] }} }}
]
}}
```
5. 最后给出一个简单的总结。
直接输出分析内容,不要解释。
"""
elif query_type == "company":
if not year or not month:
from dateutil.relativedelta import relativedelta
prev = now - relativedelta(months=1)
year = year or prev.year
month = month or prev.month
last_day = calendar.monthrange(year, month)[1]
start_time = f"{year:04d}-{month:02d}-01 00:00:00"
end_time = f"{year:04d}-{month:02d}-{last_day:02d} 23:59:59"
data = await model.get_company_top10(start_time=start_time, end_time=end_time)
summary_prompt = f"""
你是一个专业的充电数据分析师。
用户查询了 {year}{month}月 企业充电量排名的 TOP 10 数据。
数据库返回的数据如下JSON格式
{json.dumps(data, ensure_ascii=False)}
请根据这些数据:
1. 简要分析该月企业充电量的整体情况。
2. 指出充电量排名前三的企业。
3. 使用Markdown表格展示数据列名应为企业名称, 总充电量, 峰电量, 平电量, 谷电量。
4. **重要**:提供一个 ECharts 6.0 柱状图展示 TOP 10 企业的充电量对比。
- **核心要求**:每个企业应展示 **4个柱子**,分别对应:**总量 (Total)**、**峰电量 (Peak)**、**平电量 (Flat)**、**谷电量 (Valley)**。
- **配色要求**:请使用丰富的配色方案,避免单调。建议:总量使用深蓝色 (#3b82f6),峰电量使用橙红色 (#ef4444),平电量使用绿色 (#10b981),谷电量使用黄色 (#f59e0b)。
- 请确保图例 (Legend) 清晰可见。
- 请确保图表在深色背景下清晰可见。
请使用以下格式输出 ECharts 配置JSON 格式):
```echarts
{{
"title": {{ "text": "{year}{month}月企业充电量 TOP 10", "left": "center", "textStyle": {{ "color": "#f8fafc" }} }},
"tooltip": {{ "trigger": "axis", "axisPointer": {{ "type": "shadow" }} }},
"legend": {{ "data": ["总量", "峰电量", "平电量", "谷电量"], "bottom": 0, "textStyle": {{ "color": "#94a3b8" }} }},
"grid": {{ "left": "3%", "right": "4%", "bottom": "10%", "containLabel": true }},
"xAxis": {{ "type": "category", "data": ["企业A", "企业B", ...], "axisLabel": {{ "color": "#94a3b8", "rotate": 30 }} }},
"yAxis": {{ "type": "value", "axisLabel": {{ "color": "#94a3b8" }}, "splitLine": {{ "lineStyle": {{ "color": "rgba(148, 163, 184, 0.1)" }} }} }},
"series": [
{{ "name": "总量", "type": "bar", "data": [100, 200, ...], "itemStyle": {{ "color": "#3b82f6", "borderRadius": [4, 4, 0, 0] }} }},
{{ "name": "峰电量", "type": "bar", "data": [40, 80, ...], "itemStyle": {{ "color": "#ef4444", "borderRadius": [4, 4, 0, 0] }} }},
{{ "name": "平电量", "type": "bar", "data": [30, 70, ...], "itemStyle": {{ "color": "#10b981", "borderRadius": [4, 4, 0, 0] }} }},
{{ "name": "谷电量", "type": "bar", "data": [30, 50, ...], "itemStyle": {{ "color": "#f59e0b", "borderRadius": [4, 4, 0, 0] }} }}
]
}}
```
5. 最后给出一个简单的分析总结。
直接输出分析内容,不要解释。
"""
else:
# 默认按月查询
if not year or not month:
from dateutil.relativedelta import relativedelta
prev = now - relativedelta(months=1)
year = year or prev.year
month = month or prev.month
last_day = calendar.monthrange(year, month)[1]
start_time = f"{year:04d}-{month:02d}-01 00:00:00"
end_time = f"{year:04d}-{month:02d}-{last_day:02d} 23:59:59"
data = await model.get_degree(station_names=station_names, start_time=start_time, end_time=end_time)
summary_prompt = f"""
你是一个专业的充电数据分析师。
用户查询了 {year}{month}月 的场站充电数据。
数据库返回的数据如下JSON格式
{json.dumps(data, ensure_ascii=False)}
请根据这些数据:
1. 简要总结该月的充电情况(总电量、峰平谷占比等)。
2. 如果是TOP 10查询指出电量最高的几个场站。
3. 使用Markdown表格展示数据。
4. **重要**:提供一个 ECharts 6.0 图表。
- 无论是单个场站还是多个场站,请使用 **柱状图 (Bar Chart)**。
- **核心要求**:每个场站应展示 **4个柱子**,分别对应:**总量 (Total)**、**峰电量 (Peak)**、**平电量 (Flat)**、**谷电量 (Valley)**。
- **配色要求**:请使用丰富的配色方案,避免单调。建议:总量使用深蓝色 (#1e40af),峰电量使用橙红色 (#ef4444),平电量使用绿色 (#10b981),谷电量使用黄色 (#f59e0b)。
- 请确保图例 (Legend) 清晰可见。
请使用以下格式输出 ECharts 配置JSON 格式):
```echarts
{{
"title": {{ "text": "{year}{month}月充电量对比", "left": "center", "textStyle": {{ "color": "#f8fafc" }} }},
"tooltip": {{ "trigger": "axis", "axisPointer": {{ "type": "shadow" }} }},
"legend": {{ "data": ["总量", "峰电量", "平电量", "谷电量"], "bottom": 0, "textStyle": {{ "color": "#94a3b8" }} }},
"grid": {{ "left": "3%", "right": "4%", "bottom": "10%", "containLabel": true }},
"xAxis": {{ "type": "category", "data": ["场站1", "场站2", ...], "axisLabel": {{ "color": "#94a3b8" }} }},
"yAxis": {{ "type": "value", "axisLabel": {{ "color": "#94a3b8" }}, "splitLine": {{ "lineStyle": {{ "color": "rgba(148, 163, 184, 0.1)" }} }} }},
"series": [
{{ "name": "总量", "type": "bar", "data": [100, 200, ...], "itemStyle": {{ "color": "#3b82f6", "borderRadius": [4, 4, 0, 0] }} }},
{{ "name": "峰电量", "type": "bar", "data": [40, 80, ...], "itemStyle": {{ "color": "#ef4444", "borderRadius": [4, 4, 0, 0] }} }},
{{ "name": "平电量", "type": "bar", "data": [30, 70, ...], "itemStyle": {{ "color": "#10b981", "borderRadius": [4, 4, 0, 0] }} }},
{{ "name": "谷电量", "type": "bar", "data": [30, 50, ...], "itemStyle": {{ "color": "#f59e0b", "borderRadius": [4, 4, 0, 0] }} }}
]
}}
```
5. 给出一个简单的分析结论。
直接输出分析内容,不要解释。
"""
# 3. 生成总结并返回结果
yield f"data: {json.dumps({'step': 'summarizing', 'message': '正在生成分析总结...'}, ensure_ascii=False)}\n\n"
try:
async for chunk in get_llm_response(summary_prompt, stream=True):
yield f"data: {json.dumps({'content': chunk}, ensure_ascii=False)}\n\n"
yield "data: [DONE]\n\n"
except Exception as e:
logger.error(f"Inner processing error: {e}", exc_info=True)
print(f"Inner processing error: {e}")
yield f"data: {json.dumps({'step': 'error', 'message': f'处理数据时发生错误: {str(e)}'})}\n\n"
except Exception as e:
logger.error(f"Chat processing error: {e}", exc_info=True)
print(f"Chat processing error: {e}") # Debug print
yield f"data: {json.dumps({'step': 'error', 'message': f'处理请求时发生错误: {str(e)}'})}\n\n"
return StreamingResponse(event_generator(), media_type="text/event-stream")