From 7ecf9718f2d5b0e95dc50d11bbf79d8230ef5efc Mon Sep 17 00:00:00 2001 From: HuangHai <10402852@qq.com> Date: Mon, 17 Mar 2025 10:51:07 +0800 Subject: [PATCH] 'commit' --- AI/Text2Sql/Model/biModel.py | 4 + AI/Text2Sql/Train.py | 111 ------------------ AI/Text2Sql/Util/EchartsUtil.py | 197 +++++++++++++------------------- AI/Text2Sql/app.py | 38 ++++++ 4 files changed, 124 insertions(+), 226 deletions(-) diff --git a/AI/Text2Sql/Model/biModel.py b/AI/Text2Sql/Model/biModel.py index 5850e323..2fc3e79b 100644 --- a/AI/Text2Sql/Model/biModel.py +++ b/AI/Text2Sql/Model/biModel.py @@ -48,6 +48,10 @@ async def get_question_by_id(db: asyncpg.Connection, question_id: str): _data = await db.fetch(select_sql, question_id) return _data +# 根据 SQL 查询数据 +async def get_data_by_sql(db: asyncpg.Connection, sql: str): + _data = await db.fetch(sql) + return _data # 保存系统推荐 async def set_system_recommend_questions(db: asyncpg.Connection, question_id: str, flag: str): diff --git a/AI/Text2Sql/Train.py b/AI/Text2Sql/Train.py index 6e251308..6ff89e0a 100644 --- a/AI/Text2Sql/Train.py +++ b/AI/Text2Sql/Train.py @@ -42,115 +42,4 @@ if __name__ == "__main__": for i, (comment, sql) in enumerate(sql_snippets, 1): vn.train(sql=comment.strip() + '\n' + sql.strip() + '\n') - # 自然语言提问 - # 整体情况 - # question = ''' - # 查询: - # 1、发布时间是2024年度 - # 2、每个行政区每个学校都上传了多少课程数量 - # 3、格式: 行政区划名,学段,排名,学校名称,课程数量 - # ''' - # 指定行政区域 - # question = ''' - # 查询: - # 1、发布时间是2024年度 - # 2、二道区每个学校都上传了多少课程数量 - # 3、格式: 行政区划名,学段,排名,学校名称,发布年份,课程数量 - # ''' - - # 指定学段 - question = ''' - 查询: - 1、发布时间是2024年度 - 2、每个学段,每个科目,上传课程数量,按由多到少排序 - 3、字段名: 学段,科目,排名,课程数量 - ''' - common_prompt = ''' - 返回的信息要求: - 1、行政区划为NULL 或者是空字符的不参加统计 - 2、目标数据库是Postgresql 16 - ''' - question = question + common_prompt - # 开始查询 - print("开始查询...") - # 获取完整 SQL - sql = vn.generate_sql(question) - print("生成的查询 SQL:\n", sql) - - # 执行SQL查询 - with PostgreSQLUtil() as db: - _data = db.execute_query(sql) - - # 获取字段名和数据示例 - field_names = list(_data[0].keys()) if _data else [] - sample_data = _data[:3] # 取前 3 行作为示例数据 - - # 1、生成柱状图 - generate_bar_chart( - _data=_data, - title="学段+科目课程数量柱状图", - x_columns=['学段', '科目'], # 动态指定 X 轴列 - y_columns=['课程数量'], # 动态指定 Y 轴列 - output_file="d:/lesson_bar_chart.html" - ) - # 2、生成饼状图 - generate_pie_chart( - _data=_data, - title="学段+科目分布", - category_columns=['学段', '科目'], # 多列组合参数 - value_column='课程数量', - output_file="d:/lesson_pie_chart.html" - ) - - # 3、生成excel - filename = "d:/导出信息.xlsx" - save_to_excel(_data, filename) - - # 4、生成word报告 - prompt = ''' - 请根据以下 JSON 数据,整理出2000字左右的话描述当前数据情况。要求: - 1、以Markdown格式返回,我将直接通过markdown格式生成Word。 - 2、标题统一为:长春云校数据分析报告 - 3、内容中不要提到JSON数据,统一称:数据 - 4、尽量以条目列出,这样更清晰 - 5、数据: - ''' - prompt = prompt + json.dumps(_data, ensure_ascii=False) - - # 初始化 OpenAI 客户端 - client = OpenAI( - api_key=MODEL_API_KEY, - base_url=MODEL_API_URL, - ) - - # 调用 OpenAI API 生成总结(流式输出) - response = client.chat.completions.create( - model=MODEL_NAME, - messages=[ - {"role": "system", "content": "你是一个数据分析助手,擅长从 JSON 数据中提取关键信息并生成详细的总结。"}, - {"role": "user", "content": prompt} - ], - max_tokens=3000, # 控制生成内容的长度 - temperature=0.7, # 控制生成内容的创造性 - stream=True # 启用流式输出 - ) - - # 初始化变量用于存储流式输出的内容 - summary = "" - - # 处理流式输出 - for chunk in response: - if chunk.choices[0].delta.content: # 检查是否有内容 - chunk_content = chunk.choices[0].delta.content - print(chunk_content, end="", flush=True) # 实时打印到控制台 - summary += chunk_content # 将内容拼接到 summary 中 - - # 保存markdown - with open("d:/report.md", "w", encoding="utf-8") as file: - file.write(summary) - - # 最终 summary 为完整的 Markdown 内容 - print("\n\n流式输出完成,summary 已拼接为完整字符串。") - # 生成 Word 文档 - markdown_to_docx(summary, output_file="d:/report.docx") diff --git a/AI/Text2Sql/Util/EchartsUtil.py b/AI/Text2Sql/Util/EchartsUtil.py index e9841348..b76cec8d 100644 --- a/AI/Text2Sql/Util/EchartsUtil.py +++ b/AI/Text2Sql/Util/EchartsUtil.py @@ -1,14 +1,26 @@ -from pyecharts.charts import Bar +from pyecharts.charts import Bar, Pie +from pyecharts import options as opts +from pyecharts.commons.utils import JsCode +import os -def generate_bar_chart(_data, title: str, x_columns: list, y_columns: list, output_file: str = "bar_chart.html"): +def generate_chart( + _data, + chart_type: str, # 图表类型:'bar' 或 'pie' + title: str, # 图表标题 + category_columns: list, # 分类数据列(X 轴或饼图标签) + value_column: str, # 数值数据列(Y 轴或饼图值) + output_file: str = "chart.html" # 输出文件路径 +): """ - 根据结果集生成柱状图并保存为 HTML 文件。 + 统一生成柱形图或饼形图。 参数: - result: 结果集,通常是一个列表(字典格式)。 - x_columns (list): 作为 X 轴的列名列表。 - y_columns (list): 作为 Y 轴的列名列表。 - output_file (str): 输出 HTML 文件名(默认 "bar_chart.html")。 + _data: 结果集,通常是一个列表(字典格式)。 + chart_type (str): 图表类型,'bar' 或 'pie'。 + title (str): 图表标题。 + category_columns (list): 分类数据列(X 轴或饼图标签)。 + value_column (str): 数值数据列(Y 轴或饼图值)。 + output_file (str): 输出 HTML 文件名(默认 "chart.html")。 """ try: # 如果结果集为空,直接返回 @@ -16,131 +28,86 @@ def generate_bar_chart(_data, title: str, x_columns: list, y_columns: list, outp raise ValueError("结果集为空,无法生成图表!") # 检查列名是否存在 - for col in x_columns + y_columns: + for col in category_columns + [value_column]: if col not in _data[0]: raise ValueError(f"列名 '{col}' 不存在!") - # 提取 X 轴数据(动态组合多列) - x_axis_data = [" - ".join(str(row[col]) for col in x_columns) for row in _data] - # 提取 Y 轴数据 - y_axis_data = [row[y_columns[0]] for row in _data] # 目前只支持一个 Y 轴列 + # 提取分类数据(动态组合多列) + categories = [" - ".join(str(row[col]) for col in category_columns) for row in _data] + # 提取数值数据 + values = [row[value_column] for row in _data] - # 为每个柱状图设置不同颜色 + # 颜色配置 colors = [ "#5470C6", "#91CC75", "#EE6666", "#73C0DE", "#3BA272", "#FC8452", "#9A60B4", "#EA7CCC" - ] # 自定义颜色列表 + ] + color_js = JsCode(f''' + function(params) {{ + var colorList = {colors}; + return colorList[params.dataIndex % colorList.length]; + }} + ''') + + # 生成柱形图 + if chart_type == 'bar': + chart = Bar() + chart.add_xaxis(categories) + chart.add_yaxis( + series_name=value_column, + y_axis=values, + bar_width="30%", + itemstyle_opts=opts.ItemStyleOpts(color=color_js) + ) + chart.set_global_opts( + title_opts=opts.TitleOpts(title=title), + xaxis_opts=opts.AxisOpts( + name=" - ".join(category_columns), + axislabel_opts=opts.LabelOpts(rotate=45), + boundary_gap=True, + splitline_opts=opts.SplitLineOpts(is_show=False) + ), + yaxis_opts=opts.AxisOpts(name=value_column), + legend_opts=opts.LegendOpts(is_show=False) + ) - # 创建柱状图并配置布局 - bar = Bar() - bar.add_xaxis(x_axis_data) - bar.add_yaxis( - series_name=y_columns[0], # 使用 y_columns 的第一个元素作为系列名称 - y_axis=y_axis_data, - bar_width="30%", # 控制柱宽 - itemstyle_opts=opts.ItemStyleOpts( - color=JsCode(f''' - function(params) {{ - var colorList = {colors}; - return colorList[params.dataIndex % colorList.length]; - }} - ''') + # 生成饼形图 + elif chart_type == 'pie': + data_pairs = list(zip(categories, values)) + chart = Pie() + chart.add( + series_name="占比", + data_pair=data_pairs, + radius=["30%", "55%"], + label_opts=opts.LabelOpts( + formatter="{b}\n{d}%", + position="outside", + font_size=14 + ), + itemstyle_opts=opts.ItemStyleOpts(color=color_js) + ) + chart.set_global_opts( + title_opts=opts.TitleOpts( + title=title, + subtitle="数据维度:" + " → ".join(category_columns) + ), + legend_opts=opts.LegendOpts(is_show=False) ) - ) - bar.set_global_opts( - title_opts=opts.TitleOpts(title=title), # 添加标题 - xaxis_opts=opts.AxisOpts( - name=" - ".join(x_columns), - axislabel_opts=opts.LabelOpts(rotate=45), - boundary_gap=True, # 开启分类间隙 - splitline_opts=opts.SplitLineOpts(is_show=False) - ), - yaxis_opts=opts.AxisOpts(name=y_columns[0]), # 使用 y_columns 的第一个元素作为 Y 轴名称 - legend_opts=opts.LegendOpts(is_show=False) # 隐藏图例 - ) + + else: + raise ValueError("不支持的图表类型!") # 确保目标目录存在 - if not os.path.exists(os.path.dirname(output_file)): - os.makedirs(os.path.dirname(output_file)) + os.makedirs(os.path.dirname(output_file), exist_ok=True) # 保存为 HTML 文件 - bar.render(output_file) - print(f"柱状图已保存为 {output_file}") + chart.render(output_file) + print(f"图表已保存为 {output_file}") # 检查文件是否存在 if os.path.exists(output_file): print("文件生成成功!") else: print("文件生成失败!") - except Exception as e: - print(f"生成柱状图时发生错误: {e}") - - -from pyecharts import options as opts -from pyecharts.charts import Pie -from pyecharts.commons.utils import JsCode -import os - - -def generate_pie_chart( - _data, - title: str, - category_columns: list, # 改为列表接收多列(如["学段","科目"]) - value_column: str, - output_file: str = "pie_chart.html" -): - """ - 支持多列组合的饼图生成器 - - 参数说明: - category_columns : 分类标签的多个字段名 - value_column : 数值字段名 - """ - try: - # 数据预处理:生成组合标签 - combined_data = {} - for item in _data: - # 生成组合键(例:"小学 - 语文") - key = " - ".join(str(item[col]) for col in category_columns) - combined_data[key] = combined_data.get(key, 0) + item[value_column] - - # 转换为饼图数据格式 - data_pairs = list(combined_data.items()) - - # 颜色配置(与柱状图一致) - color_js = JsCode(""" - function(params) { - var colors = ['#5470C6','#91CC75','#EE6666','#73C0DE']; - return colors[params.dataIndex % colors.length]; - } - """) - - # 构建饼图 - pie = Pie() - pie.add( - series_name="占比", - data_pair=data_pairs, - radius=["30%", "55%"], # 环形布局优化 - label_opts=opts.LabelOpts( - formatter="{b}\n{d}%", # 换行显示标签和比例 - position="outside", - font_size=14 - ), - itemstyle_opts=opts.ItemStyleOpts(color=color_js) - ) - - # 全局配置优化 - pie.set_global_opts( - title_opts=opts.TitleOpts( - title=title, - subtitle="数据维度:" + " → ".join(category_columns) - ), - legend_opts=opts.LegendOpts(is_show=False) # 不显示图例 - ) - - # 输出文件 - os.makedirs(os.path.dirname(output_file), exist_ok=True) - pie.render(output_file) - print(f"组合饼图已生成: {output_file}") except Exception as e: - print(f"生成失败: {str(e)}") + print(f"生成图表时发生错误: {e}") \ No newline at end of file diff --git a/AI/Text2Sql/app.py b/AI/Text2Sql/app.py index 865ab98c..4fc32552 100644 --- a/AI/Text2Sql/app.py +++ b/AI/Text2Sql/app.py @@ -11,6 +11,7 @@ from starlette.responses import StreamingResponse from Config import * from Model.biModel import * +from Text2Sql.Util.EchartsUtil import generate_chart from Text2Sql.Util.MarkdownToDocxUtil import markdown_to_docx from Text2Sql.Util.SaveToExcel import save_to_excel from Text2Sql.Util.VannaUtil import VannaUtil @@ -268,6 +269,43 @@ async def get_data_scheme_by_id( return {"success": True, "data": column_names} +# 生成图表 +@app.get("/questions/get_chart") +async def get_chart( + question_id: str = Query(None, description="问题ID(GET请求)"), + type_id: str = Query(1, description="图表类型ID(GET请求),1:柱图,2:饼图"), + db: asyncpg.Connection = Depends(get_db) +): + sql = (await get_question_by_id(db, question_id))[0]['sql'] + # 执行sql获取数据集 + _data = await get_data_by_sql(db, sql) + + # 图表文件名称 + uuid_str = str(uuid.uuid4()) + filename = f"static/{uuid_str}.html" + + if type_id == 1: # 1、生成柱状图 + generate_chart( + _data=_data, + chart_type='bar', + title="柱形统计图", + category_columns=["学段", "科目"], + value_column="课程数量", + output_file=filename + ) + elif type_id == 2: # 2、生成饼状图 + generate_chart( + _data=_data, + chart_type='pie', + title="饼形统计图", + category_columns=["学段", "科目"], + value_column="课程数量", + output_file=filename + ) + # 返回静态文件URL + return {"success": True, "message": "图表文件生成成功", "url": filename} + + # 启动 FastAPI if __name__ == "__main__": uvicorn.run("app:app", host="0.0.0.0", port=8000, workers=4)