mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 01:06:59 +08:00
feat: chat view
This commit is contained in:
@@ -135,7 +135,7 @@ async def stream_sql(session: SessionDep, current_user: CurrentUser, request_que
|
||||
# execute sql
|
||||
result = llm_service.execute_sql(sql=sql)
|
||||
llm_service.save_sql_data(session=session, data_obj=result)
|
||||
yield json.dumps({'content': result, 'type': 'sql-data'}, ensure_ascii=False) + '\n\n'
|
||||
yield json.dumps({'content': json.dumps(result, ensure_ascii=False), 'type': 'sql-data'}) + '\n\n'
|
||||
|
||||
# generate chart
|
||||
chart_res = llm_service.generate_chart(session=session)
|
||||
@@ -149,7 +149,8 @@ async def stream_sql(session: SessionDep, current_user: CurrentUser, request_que
|
||||
print(full_chart_text)
|
||||
chart = llm_service.check_save_chart(session=session, res=full_chart_text)
|
||||
print(chart)
|
||||
yield json.dumps({'content': chart, 'type': 'chart'}, ensure_ascii=False) + '\n\n'
|
||||
yield json.dumps({'content': json.dumps(chart, ensure_ascii=False), 'type': 'chart'},
|
||||
ensure_ascii=False) + '\n\n'
|
||||
|
||||
llm_service.finish(session=session)
|
||||
yield json.dumps({'type': 'finish'})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from sqlmodel import SQLModel, Field
|
||||
from sqlalchemy import Column, Text, BigInteger, DateTime, Integer, Identity, Boolean
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional, Any
|
||||
from sqlalchemy import Column, Text, BigInteger, DateTime, Integer, Identity, Boolean
|
||||
from sqlmodel import SQLModel, Field
|
||||
|
||||
from apps.template.generate_chart.generator import get_chart_template
|
||||
from apps.template.generate_sql.generator import get_sql_template
|
||||
@@ -70,19 +71,14 @@ class AiModelQuestion(BaseModel):
|
||||
engine: str = ""
|
||||
db_schema: str = ""
|
||||
sql: str = ""
|
||||
rule: str = """
|
||||
请逐步推理后给出答案:
|
||||
推理过程中不需要输出JSON,仅在最终结果内输出符合要求的JSON
|
||||
步骤1: [思考内容]
|
||||
步骤2: [思考内容]
|
||||
最终答案: [结果]
|
||||
"""
|
||||
rule: str = ""
|
||||
|
||||
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, rule=self.rule)
|
||||
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)
|
||||
|
||||
+19
-1
@@ -18,6 +18,14 @@ template:
|
||||
- 如果问题是图表展示相关且与生成SQL查询无关时,请参考上一次回答的SQL来生成SQL
|
||||
- 如果问题是图表展示相关,可参考的图表类型为表格(table)、条形图(bar)、折线图(line)或饼图(pie),返回的JSON:
|
||||
{{"success":true,"sql":"生成的SQL语句","chart-type":"选择的图表类型(table/bar/line/pie)"}}
|
||||
|
||||
### 请遵守以下输出要求:
|
||||
请逐步推理后给出答案
|
||||
推理过程中不需要输出JSON,仅在最终结果内输出符合要求的JSON
|
||||
步骤1: [思考内容]
|
||||
步骤2: [思考内容]
|
||||
……
|
||||
最终答案: [结果]
|
||||
user: |
|
||||
### 表结构:
|
||||
{schema}
|
||||
@@ -26,6 +34,7 @@ template:
|
||||
{question}
|
||||
|
||||
### 其他规则:
|
||||
请使用英语(English)回答
|
||||
{rule}
|
||||
chart:
|
||||
system: |
|
||||
@@ -55,7 +64,15 @@ template:
|
||||
### 响应:
|
||||
根据您的指示,这是我生成的与 问题 和 sql 匹配的 JSON:
|
||||
```json
|
||||
user: |
|
||||
|
||||
### 请遵守以下输出要求:
|
||||
请逐步推理后给出答案
|
||||
推理过程中不需要输出JSON,仅在最终结果内输出符合要求的JSON
|
||||
步骤1: [思考内容]
|
||||
步骤2: [思考内容]
|
||||
……
|
||||
最终答案: [结果]
|
||||
user: |
|
||||
### SQL:
|
||||
{sql}
|
||||
|
||||
@@ -63,4 +80,5 @@ template:
|
||||
{question}
|
||||
|
||||
### 其他规则:
|
||||
请使用英语(English)回答
|
||||
{rule}
|
||||
@@ -12,6 +12,7 @@
|
||||
"@npkg/tinymce-plugins": "^0.0.7",
|
||||
"@tinymce/tinymce-vue": "^5.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"element-plus": "^2.9.11",
|
||||
"lodash": "^4.17.21",
|
||||
"snowflake-id": "^1.1.0",
|
||||
"tinymce": "^5.8.2",
|
||||
|
||||
@@ -10,7 +10,7 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const settings = ref<{
|
||||
type: "chart" | "sql" | "data"
|
||||
type: "chart" | "sql"
|
||||
showAnswer: boolean
|
||||
}>({
|
||||
type: "chart",
|
||||
@@ -74,11 +74,11 @@ const chartType = computed<"table" | "bar" | "line" | "pie">({
|
||||
<div v-if="message">
|
||||
<div>
|
||||
<div v-if="message.isTyping">Thinking ...</div>
|
||||
<div v-if="chartObject.title">{{ chartObject.title }}</div>
|
||||
<div v-if="chartObject.title && !message.isTyping">{{ chartObject.title }}</div>
|
||||
<el-tabs v-model="settings.type" class="demo-tabs" @tab-click="handleClick" tab-position="top">
|
||||
<el-tab-pane label="Chart" name="chart">
|
||||
<template #label>
|
||||
<el-select v-model="chartType" style="width: 80px">
|
||||
<el-select v-model="chartType" style="width: 80px" :disabled="settings.type!== 'chart'">
|
||||
<el-option value="table">table</el-option>
|
||||
<el-option value="bar">bar</el-option>
|
||||
<el-option value="line">line</el-option>
|
||||
@@ -87,7 +87,6 @@ const chartType = computed<"table" | "bar" | "line" | "pie">({
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="SQL" name="sql"></el-tab-pane>
|
||||
<el-tab-pane label="Data" name="data"></el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<template v-if="message.record">
|
||||
@@ -95,7 +94,7 @@ const chartType = computed<"table" | "bar" | "line" | "pie">({
|
||||
<el-collapse-item name="1">
|
||||
<template #title>
|
||||
Inference process
|
||||
<el-icon v-if="props.message.isTyping">
|
||||
<el-icon v-if="props.message?.isTyping">
|
||||
<Loading/>
|
||||
</el-icon>
|
||||
</template>
|
||||
@@ -112,7 +111,7 @@ const chartType = computed<"table" | "bar" | "line" | "pie">({
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<div>
|
||||
<div class="answer-content">
|
||||
<template v-if="settings.type === 'sql'">
|
||||
<div>
|
||||
<div v-if="message.record.sql">
|
||||
@@ -149,4 +148,8 @@ const chartType = computed<"table" | "bar" | "line" | "pie">({
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.answer-content {
|
||||
padding: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -12,7 +12,9 @@ defineProps<{
|
||||
<div class="chat-block-outer">
|
||||
<div class="chat-block">
|
||||
<slot>
|
||||
<div v-html="msg?.content"></div>
|
||||
<div class="chat-block-content">
|
||||
<div v-html="msg?.content"></div>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="chat-block-footer">
|
||||
@@ -26,9 +28,13 @@ defineProps<{
|
||||
.chat-block {
|
||||
border-radius: 2px;
|
||||
background-color: white;
|
||||
padding: 12px;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
|
||||
.chat-block-content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.chat-block-footer {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import {MoreFilled, EditPen, Delete} from '@element-plus/icons-vue'
|
||||
import {Delete, EditPen, MoreFilled} from '@element-plus/icons-vue'
|
||||
import {type Chat, chatApi} from "@/api/chat.ts";
|
||||
import {computed} from "vue";
|
||||
|
||||
@@ -105,10 +105,11 @@ function handleCommand(command: string | number | object, chat: Chat) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-scrollbar>
|
||||
<el-scrollbar ref="chatListRef">
|
||||
<div class="chat-list-inner">
|
||||
<template v-for="chat in chatList" :key="chat.id">
|
||||
<div class="chat-list-item" :class="{'active': currentChatId === chat.id}" @click="onClickHistory(chat)">
|
||||
|
||||
@@ -3,6 +3,7 @@ import ChatBlock from './ChatBlock.vue'
|
||||
import WelcomeBlock from './WelcomeBlock.vue'
|
||||
import {ChatInfo, type ChatMessage} from "@/api/chat.ts";
|
||||
import {computed} from "vue";
|
||||
import {UserFilled} from "@element-plus/icons-vue"
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
msg: ChatMessage
|
||||
@@ -27,8 +28,12 @@ const _datasource = computed({
|
||||
|
||||
<template>
|
||||
<div class="chat-row" :class="{'right-to-left': msg.role === 'user'}">
|
||||
<el-avatar shape="square" v-if="msg.role === 'assistant'">SQLBot</el-avatar>
|
||||
<el-avatar shape="square" v-if="msg.role === 'user'"/>
|
||||
<el-avatar class="ai-avatar" shape="square" v-if="msg.role === 'assistant'">SQLBot</el-avatar>
|
||||
<el-avatar class="user-avatar" shape="square" v-if="msg.role === 'user'">
|
||||
<el-icon>
|
||||
<UserFilled/>
|
||||
</el-icon>
|
||||
</el-avatar>
|
||||
<ChatBlock v-if="!msg.isWelcome" :msg="msg" :class="{'row-full': msg.role === 'assistant'}">
|
||||
<slot></slot>
|
||||
<template #footer>
|
||||
@@ -58,5 +63,13 @@ const _datasource = computed({
|
||||
}
|
||||
}
|
||||
|
||||
.ai-avatar {
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
background: var(--ed-color-primary)
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -63,37 +63,43 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<ChatBlock>
|
||||
<div>你好,我是SQLBot,很高兴为你服务</div>
|
||||
<div class="sub">我可以帮忙查询数据、生成图表、检测数据异常、预测数据等,请选择一个数据源,开启智能问数吧~</div>
|
||||
<template v-if="editable">
|
||||
<template v-if="dsList.length>0">
|
||||
<div class="ds-select-row">
|
||||
<div>选择数据源</div>
|
||||
<el-button @click="showDs" link type="primary">查看更多</el-button>
|
||||
</div>
|
||||
<div class="ds-row-container">
|
||||
<template v-for="(item, _index) in dsList" :key="_index">
|
||||
<DatasourceItemCard :ds="item" @click="selectDs(item)" v-if="_index<3 || item?.id===modelValue"
|
||||
class="ds-card" :class="[item?.id===modelValue? 'ds-card-selected': '']"/>
|
||||
</template>
|
||||
<div class="welcome-content">
|
||||
<div>你好,我是SQLBot,很高兴为你服务</div>
|
||||
<div class="sub">我可以帮忙查询数据、生成图表、检测数据异常、预测数据等,请选择一个数据源,开启智能问数吧~</div>
|
||||
<template v-if="editable">
|
||||
<template v-if="dsList.length>0">
|
||||
<div class="ds-select-row">
|
||||
<div>选择数据源</div>
|
||||
<el-button @click="showDs" link type="primary">查看更多</el-button>
|
||||
</div>
|
||||
<div class="ds-row-container">
|
||||
<template v-for="(item, _index) in dsList" :key="_index">
|
||||
<DatasourceItemCard :ds="item" @click="selectDs(item)" v-if="_index<3 || item?.id===modelValue"
|
||||
class="ds-card" :class="[item?.id===modelValue? 'ds-card-selected': '']"/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else>
|
||||
数据源为空,请新建后再开启智能问数!
|
||||
</div>
|
||||
</template>
|
||||
<div v-else>
|
||||
数据源为空,请新建后再开启智能问数!
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="ds-select-row">
|
||||
<div>已选择数据源</div>
|
||||
</div>
|
||||
<div class="ds-row-container">
|
||||
<DatasourceItemCard :ds="ds"/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="ds-select-row">
|
||||
<div>已选择数据源</div>
|
||||
</div>
|
||||
<div class="ds-row-container">
|
||||
<DatasourceItemCard :ds="ds"/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</ChatBlock>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.welcome-content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
color: grey;
|
||||
font-size: 0.8em;
|
||||
@@ -118,7 +124,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.ds-card-selected {
|
||||
border-color: var(--ed-color-primary-light-5);
|
||||
box-shadow: 0 1px 3px var(--ed-color-primary-light-5);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</el-aside>
|
||||
<el-container :loading="loading">
|
||||
<el-main class="chat-record-list">
|
||||
<el-scrollbar>
|
||||
<el-scrollbar ref="chatListRef">
|
||||
<template v-for="(message, _index) in computedMessages" :key="_index">
|
||||
<ChatRow
|
||||
:current-chat="currentChat"
|
||||
@@ -46,7 +46,7 @@
|
||||
<template
|
||||
v-if="currentChat.datasource && currentChat.datasource_name"
|
||||
>
|
||||
使用数据源:{{ currentChat.datasource_name }}
|
||||
Datasource:{{ currentChat.datasource_name }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
@@ -81,20 +81,24 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, nextTick, onMounted, ref} from "vue";
|
||||
import {Plus, Position} from "@element-plus/icons-vue";
|
||||
import {
|
||||
Chat,
|
||||
chatApi,
|
||||
ChatInfo,
|
||||
type ChatMessage,
|
||||
ChatRecord,
|
||||
questionApi,
|
||||
} from "@/api/chat";
|
||||
import {Chat, chatApi, ChatInfo, type ChatMessage, ChatRecord, questionApi,} from "@/api/chat";
|
||||
import ChatList from "./ChatList.vue";
|
||||
import ChatRow from "./ChatRow.vue";
|
||||
import ChatAnswer from "./ChatAnswer.vue";
|
||||
|
||||
const inputMessage = ref("");
|
||||
|
||||
const chatListRef = ref()
|
||||
|
||||
function scrollToBottom() {
|
||||
nextTick(() => {
|
||||
chatListRef.value?.scrollTo({
|
||||
top: chatListRef.value.wrapRef.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
const chatList = ref<Array<ChatInfo>>([]);
|
||||
|
||||
@@ -163,6 +167,8 @@ function onClickHistory(chat: Chat) {
|
||||
const info = chatApi.toChatInfo(res);
|
||||
if (info) {
|
||||
currentChat.value = info;
|
||||
|
||||
scrollToBottom()
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -264,39 +270,61 @@ const sendMessage = async () => {
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
console.log(chunk);
|
||||
const data = JSON.parse(chunk);
|
||||
|
||||
switch (data.type) {
|
||||
case "info":
|
||||
console.log(data.msg);
|
||||
break;
|
||||
case "error":
|
||||
currentRecord.error = data.content;
|
||||
isTyping.value = false;
|
||||
break;
|
||||
case "sql-result":
|
||||
sql_answer += data.content;
|
||||
currentChat.value.records[currentChat.value.records.length - 1].sql_answer = sql_answer;
|
||||
break;
|
||||
case "sql":
|
||||
currentChat.value.records[currentChat.value.records.length - 1].sql = data.content;
|
||||
break;
|
||||
case "sql-data":
|
||||
currentChat.value.records[currentChat.value.records.length - 1].data = data.content;
|
||||
break;
|
||||
case "chart-result":
|
||||
chart_answer += data.content;
|
||||
currentChat.value.records[currentChat.value.records.length - 1].chart_answer = chart_answer;
|
||||
break;
|
||||
case "chart":
|
||||
currentChat.value.records[currentChat.value.records.length - 1].chart = data.content;
|
||||
break;
|
||||
case "finish":
|
||||
isTyping.value = false;
|
||||
break;
|
||||
let _list = [chunk]
|
||||
|
||||
const lines = chunk.trim().split('}\n\n{')
|
||||
if (lines.length > 1) {
|
||||
_list = []
|
||||
for (let line of lines) {
|
||||
if (!line.trim().startsWith('{')) {
|
||||
line = '{' + line.trim()
|
||||
}
|
||||
if (!line.trim().endsWith('}')) {
|
||||
line = line.trim() + '}'
|
||||
}
|
||||
_list.push(line)
|
||||
}
|
||||
}
|
||||
await nextTick()
|
||||
|
||||
console.log(_list);
|
||||
|
||||
for (const str of _list) {
|
||||
|
||||
const data = JSON.parse(str);
|
||||
|
||||
switch (data.type) {
|
||||
case "info":
|
||||
console.log(data.msg);
|
||||
break;
|
||||
case "error":
|
||||
currentRecord.error = data.content;
|
||||
isTyping.value = false;
|
||||
break;
|
||||
case "sql-result":
|
||||
sql_answer += data.content;
|
||||
currentChat.value.records[currentChat.value.records.length - 1].sql_answer = sql_answer;
|
||||
break;
|
||||
case "sql":
|
||||
currentChat.value.records[currentChat.value.records.length - 1].sql = data.content;
|
||||
break;
|
||||
case "sql-data":
|
||||
currentChat.value.records[currentChat.value.records.length - 1].data = data.content;
|
||||
break;
|
||||
case "chart-result":
|
||||
chart_answer += data.content;
|
||||
currentChat.value.records[currentChat.value.records.length - 1].chart_answer = chart_answer;
|
||||
break;
|
||||
case "chart":
|
||||
currentChat.value.records[currentChat.value.records.length - 1].chart = data.content;
|
||||
break;
|
||||
case "finish":
|
||||
isTyping.value = false;
|
||||
break;
|
||||
}
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
if (!currentRecord.error) {
|
||||
|
||||
Reference in New Issue
Block a user