From a4e456ce2ee26cd4ebf94bec054260c7a031080a Mon Sep 17 00:00:00 2001 From: ulleo Date: Thu, 5 Jun 2025 19:12:02 +0800 Subject: [PATCH] feat: chat view --- backend/apps/chat/models/chat_model.py | 11 +- backend/apps/chat/task/llm.py | 43 +++-- backend/template.yaml | 27 ++- frontend/components.d.ts | 62 ------ frontend/package.json | 6 +- frontend/src/views/chat/ChatAnswer.vue | 152 +++++++++++++++ frontend/src/views/chat/ChatBlock.vue | 31 ++- frontend/src/views/chat/ChatRow.vue | 4 + frontend/src/views/chat/index.vue | 256 +++++++++++-------------- 9 files changed, 349 insertions(+), 243 deletions(-) delete mode 100644 frontend/components.d.ts create mode 100644 frontend/src/views/chat/ChatAnswer.vue diff --git a/backend/apps/chat/models/chat_model.py b/backend/apps/chat/models/chat_model.py index 7687a375..61180272 100644 --- a/backend/apps/chat/models/chat_model.py +++ b/backend/apps/chat/models/chat_model.py @@ -70,18 +70,25 @@ class AiModelQuestion(BaseModel): engine: str = "" db_schema: str = "" sql: str = "" + rule: str = """ + 请逐步推理后给出答案: + 推理过程中不需要输出JSON,仅在最终结果内输出符合要求的JSON + 步骤1: [思考内容] + 步骤2: [思考内容] + 最终答案: [结果] + """ def sql_sys_question(self): return get_sql_template()['system'].format(engine=self.engine, schema=self.db_schema, question=self.question) def sql_user_question(self): - return get_sql_template()['user'].format(engine=self.engine, schema=self.db_schema, question=self.question) + return get_sql_template()['user'].format(engine=self.engine, schema=self.db_schema, question=self.question, rule=self.rule) def chart_sys_question(self): return get_chart_template()['system'].format(sql=self.sql, question=self.question) def chart_user_question(self): - return get_chart_template()['user'].format(sql=self.sql, question=self.question) + return get_chart_template()['user'].format(sql=self.sql, question=self.question, rule=self.rule) class ChatQuestion(AiModelQuestion): diff --git a/backend/apps/chat/task/llm.py b/backend/apps/chat/task/llm.py index b40478a5..d2de5a62 100644 --- a/backend/apps/chat/task/llm.py +++ b/backend/apps/chat/task/llm.py @@ -1,9 +1,7 @@ -import asyncio import json import logging -import re import warnings -from typing import Any, List, Union, AsyncGenerator, Dict +from typing import Any, List, Union, Dict from langchain_community.utilities import SQLDatabase from langchain_core.language_models import BaseLLM @@ -21,6 +19,8 @@ from common.core.deps import SessionDep, CurrentUser warnings.filterwarnings("ignore") +base_message_count_limit = 5 + class LLMService: ds: CoreDatasource @@ -55,34 +55,51 @@ class LLMService: last_chart_messages[-1].full_chart_message last_sql_messages: List[dict[str, Any]] = json.loads(last_sql_message_str) + + # todo maybe can configure + count_limit = 0 - base_message_count_limit + + self.sql_message = [] if last_sql_messages is None or len(last_sql_messages) == 0: # add sys prompt self.sql_message.append(SystemMessage(content=self.chat_question.sql_sys_question())) else: + # limit count for last_sql_message in last_sql_messages: + if last_sql_message['type'] == 'system': + _msg = SystemMessage(content=last_sql_message['content']) + self.sql_message.append(_msg) + break + for last_sql_message in last_sql_messages[count_limit:]: _msg: BaseMessage if last_sql_message['type'] == 'human': _msg = HumanMessage(content=last_sql_message['content']) + self.sql_message.append(_msg) elif last_sql_message['type'] == 'ai': _msg = AIMessage(content=last_sql_message['content']) - else: - _msg = SystemMessage(content=last_sql_message['content']) - self.sql_message.append(_msg) + self.sql_message.append(_msg) last_chart_messages: List[dict[str, Any]] = json.loads(last_chart_message_str) + + self.chart_message = [] if last_chart_messages is None or len(last_chart_messages) == 0: # add sys prompt self.chart_message.append(SystemMessage(content=self.chat_question.chart_sys_question())) else: + # limit count + for last_chart_message in last_chart_messages: + if last_chart_message['type'] == 'system': + _msg = SystemMessage(content=last_chart_message['content']) + self.chart_message.append(_msg) + break for last_chart_message in last_chart_messages: _msg: BaseMessage if last_chart_message['type'] == 'human': _msg = HumanMessage(content=last_chart_message['content']) + self.chart_message.append(_msg) elif last_chart_message['type'] == 'ai': _msg = AIMessage(content=last_chart_message['content']) - else: - _msg = SystemMessage(content=last_chart_message['content']) - self.chart_message.append(_msg) + self.chart_message.append(_msg) def init_record(self, session: SessionDep, current_user: CurrentUser) -> ChatRecord: self.record = save_question(session=session, current_user=current_user, question=self.chat_question) @@ -98,6 +115,7 @@ class LLMService: full_sql_text = '' res = self.llm.stream(self.sql_message) for chunk in res: + print(chunk) if isinstance(chunk, dict): full_sql_text += chunk['content'] yield chunk['content'] @@ -196,9 +214,10 @@ class LLMService: return save_error_message(session=session, record_id=self.record.id, message=message) def save_sql_data(self, session: SessionDep, data_obj: Dict[str, Any]): - return save_sql_exec_data(session=session, record_id=self.record.id, data=json.dumps(data_obj, ensure_ascii=False)) + return save_sql_exec_data(session=session, record_id=self.record.id, + data=json.dumps(data_obj, ensure_ascii=False)) - def finish(self,session: SessionDep): + def finish(self, session: SessionDep): return finish_record(session=session, record_id=self.record.id) def execute_sql(self, sql: str): @@ -237,7 +256,7 @@ def extract_nested_json(text): pass else: stack = [] # 括号不匹配则重置 - if results[0]: + if len(results) > 0 and results[0]: return results[0] return None diff --git a/backend/template.yaml b/backend/template.yaml index 2a38f9ce..927f6590 100644 --- a/backend/template.yaml +++ b/backend/template.yaml @@ -15,17 +15,23 @@ template: {{"success":false,"message":"抱歉,我无法回答您的问题。"}} - 如果根据提供的表结构不能生成符合问题与条件的SQL,回答: {{"success":false,"message":"无法生成SQL的原因"}} - user: | - 表结构: + - 如果问题是图表展示相关且与生成SQL查询无关时,请参考上一次回答的SQL来生成SQL + - 如果问题是图表展示相关,可参考的图表类型为表格(table)、条形图(bar)、折线图(line)或饼图(pie),返回的JSON: + {{"success":true,"sql":"生成的SQL语句","chart-type":"选择的图表类型(table/bar/line/pie)"}} + user: | + ### 表结构: {schema} - 问题: + ### 问题: {question} + + ### 其他规则: + {rule} chart: system: | ### 说明: 您的任务是通过给定的问题和SQL生成 JSON 以进行数据可视化。 - 请遵守以下规则: + 请遵守以下规则: - 如果需要表格,则生成的 JSON 格式应为: {{"type":"table", "title": "标题", "columns": [{{"name":"中文字段名1", "value": "SQL 查询列 1(有别名用别名)"}}, {{"name": "中文字段名 2", "value": "SQL 查询列 2(有别名用别名)"}}]}} 必须从 SQL 查询列中提取“columns”。 @@ -36,22 +42,25 @@ template: {{"type":"line", "title": "标题", "axis": {{"x": {{"name":"x轴的中文名称","value": "x轴的SQL查询列(有别名用别名)"}}, "y": {{"name":"y轴的中文名称","value": "y轴的SQL查询列(有别名用别名)"}}}} 其中“x”和“y”必须从SQL查询列中提取。 - 如果需要饼图,则生成的 JSON 格式应为: - {{"type":"pie", "title": "标题", "column": {{"name":"中文字段名","value":"SQL查询列1(有别名用别名)"}}}} + {{"type":"pie", "title": "标题", "column": [{{"name":"中文字段名1","value":"SQL查询列1(有别名用别名)"}},{{"name":"中文字段名2","value":"SQL查询列2(有别名用别名)"}}]}} 其中“column”必须从SQL查询列中提取。 - 如果答案未知或者与生成JSON无关,则生成的 JSON 格式应为: {{"type":"error", "reason": "抱歉,我无法回答您的问题。"}} - ### 示例 + ### 示例: 如果 SQL 为: SELECT products_sales_data.category, AVG(products_sales_data.price) AS average_price FROM products_sales_data GROUP BY products_sales_data.category; 问题是:每个商品分类的平均价格 则生成的 JSON 可以是: {{"type":"table", "title": "每个商品分类的平均价格", "columns": [{{"name":"商品分类","value":"category"}}, {{"name":"平均价格","value":"average_price"}}]}} - ### 响应: + ### 响应: 根据您的指示,这是我生成的与 问题 和 sql 匹配的 JSON: ```json user: | ### SQL: {sql} - ### 问题: - {question} \ No newline at end of file + ### 问题: + {question} + + ### 其他规则: + {rule} \ No newline at end of file diff --git a/frontend/components.d.ts b/frontend/components.d.ts deleted file mode 100644 index 3b04c291..00000000 --- a/frontend/components.d.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* eslint-disable */ -/* prettier-ignore */ -// @ts-nocheck -// Generated by unplugin-vue-components -// Read more: https://github.com/vuejs/core/pull/3399 -import '@vue/runtime-core' - -export {} - -declare module '@vue/runtime-core' { - export interface GlobalComponents { - ElAside: typeof import('element-plus-secondary/es')['ElAside'] - ElAvatar: typeof import('element-plus-secondary/es')['ElAvatar'] - ElButton: typeof import('element-plus-secondary/es')['ElButton'] - ElCheckbox: typeof import('element-plus-secondary/es')['ElCheckbox'] - ElCheckboxGroup: typeof import('element-plus-secondary/es')['ElCheckboxGroup'] - ElCol: typeof import('element-plus-secondary/es')['ElCol'] - ElContainer: typeof import('element-plus-secondary/es')['ElContainer'] - ElDialog: typeof import('element-plus-secondary/es')['ElDialog'] - ElDivider: typeof import('element-plus-secondary/es')['ElDivider'] - ElDropdown: typeof import('element-plus-secondary/es')['ElDropdown'] - ElDropdownItem: typeof import('element-plus-secondary/es')['ElDropdownItem'] - ElDropdownMenu: typeof import('element-plus-secondary/es')['ElDropdownMenu'] - ElEmpty: typeof import('element-plus-secondary/es')['ElEmpty'] - ElFooter: typeof import('element-plus-secondary/es')['ElFooter'] - ElForm: typeof import('element-plus-secondary/es')['ElForm'] - ElFormItem: typeof import('element-plus-secondary/es')['ElFormItem'] - ElHeader: typeof import('element-plus-secondary/es')['ElHeader'] - ElIcon: typeof import('element-plus-secondary/es')['ElIcon'] - ElInput: typeof import('element-plus-secondary/es')['ElInput'] - ElMain: typeof import('element-plus-secondary/es')['ElMain'] - ElMenu: typeof import('element-plus-secondary/es')['ElMenu'] - ElMenuItem: typeof import('element-plus-secondary/es')['ElMenuItem'] - ElOption: typeof import('element-plus-secondary/es')['ElOption'] - ElPagination: typeof import('element-plus-secondary/es')['ElPagination'] - ElPopover: typeof import('element-plus-secondary/es')['ElPopover'] - ElRadio: typeof import('element-plus-secondary/es')['ElRadio'] - ElRadioGroup: typeof import('element-plus-secondary/es')['ElRadioGroup'] - ElRow: typeof import('element-plus-secondary/es')['ElRow'] - ElScrollbar: typeof import('element-plus-secondary/es')['ElScrollbar'] - ElSelect: typeof import('element-plus-secondary/es')['ElSelect'] - ElStep: typeof import('element-plus-secondary/es')['ElStep'] - ElSteps: typeof import('element-plus-secondary/es')['ElSteps'] - ElSwitch: typeof import('element-plus-secondary/es')['ElSwitch'] - ElTable: typeof import('element-plus-secondary/es')['ElTable'] - ElTableColumn: typeof import('element-plus-secondary/es')['ElTableColumn'] - ElTabPane: typeof import('element-plus-secondary/es')['ElTabPane'] - ElTabs: typeof import('element-plus-secondary/es')['ElTabs'] - ElTooltip: typeof import('element-plus-secondary/es')['ElTooltip'] - ElTree: typeof import('element-plus-secondary/es')['ElTree'] - ElTreeSelect: typeof import('element-plus-secondary/es')['ElTreeSelect'] - ElUpload: typeof import('element-plus-secondary/es')['ElUpload'] - Icon: typeof import('./src/components/icon-custom/src/Icon.vue')['default'] - Layout: typeof import('./src/components/layout/index.vue')['default'] - RouterLink: typeof import('vue-router')['RouterLink'] - RouterView: typeof import('vue-router')['RouterView'] - SinglePage: typeof import('./src/components/layout/SinglePage.vue')['default'] - } - export interface ComponentCustomProperties { - vLoading: typeof import('element-plus-secondary/es')['ElLoadingDirective'] - } -} diff --git a/frontend/package.json b/frontend/package.json index 9eae6874..dec585b5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,13 +11,13 @@ "dependencies": { "@npkg/tinymce-plugins": "^0.0.7", "@tinymce/tinymce-vue": "^5.1.0", - "tinymce": "^5.8.2", + "dayjs": "^1.11.13", "lodash": "^4.17.21", "snowflake-id": "^1.1.0", + "tinymce": "^5.8.2", "vue": "^3.5.13", "vue-router": "^4.5.0", - "web-storage-cache": "^1.1.1", - "dayjs": "^1.11.13" + "web-storage-cache": "^1.1.1" }, "devDependencies": { "@element-plus/icons-vue": "^2.3.1", diff --git a/frontend/src/views/chat/ChatAnswer.vue b/frontend/src/views/chat/ChatAnswer.vue new file mode 100644 index 00000000..d861ece9 --- /dev/null +++ b/frontend/src/views/chat/ChatAnswer.vue @@ -0,0 +1,152 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/chat/ChatBlock.vue b/frontend/src/views/chat/ChatBlock.vue index 32d88ea6..a752f1cf 100644 --- a/frontend/src/views/chat/ChatBlock.vue +++ b/frontend/src/views/chat/ChatBlock.vue @@ -9,19 +9,30 @@ defineProps<{ \ No newline at end of file diff --git a/frontend/src/views/chat/ChatRow.vue b/frontend/src/views/chat/ChatRow.vue index 2230a3ca..c6f45581 100644 --- a/frontend/src/views/chat/ChatRow.vue +++ b/frontend/src/views/chat/ChatRow.vue @@ -31,6 +31,9 @@ const _datasource = computed({ + @@ -51,6 +54,7 @@ const _datasource = computed({ .row-full { flex: 1; + width: 0; } } diff --git a/frontend/src/views/chat/index.vue b/frontend/src/views/chat/index.vue index cf80f458..cf53519d 100644 --- a/frontend/src/views/chat/index.vue +++ b/frontend/src/views/chat/index.vue @@ -1,24 +1,23 @@