Skip to content

Commit 546aa84

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents 386e207 + 682f558 commit 546aa84

File tree

9 files changed

+147
-118
lines changed

9 files changed

+147
-118
lines changed

.env.example

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
1-
2-
# Before running, please copy .env.example to .env
3-
DOMAIN=localhost
4-
FRONTEND_HOST=http://localhost:5173
5-
ENVIRONMENT=local
61
PROJECT_NAME="SQLBot"
7-
STACK_NAME=SQLBot
8-
92
# Backend
103
BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,https://localhost:5173"
114
SECRET_KEY=y5txe1mRmS_JpOrUzFzHEu-kIQn3lf7ll0AOv9DQh0s
12-
FIRST_SUPERUSER=[email protected]
13-
FIRST_SUPERUSER_PASSWORD=123456 # Change this to your pwd
145

156
TOKEN_KEY="X-SQLBOT-TOKEN"
167
DEFAULT_PWD="SQLBot@123456"
@@ -22,7 +13,6 @@ LOG_FORMAT="%(asctime)s - %(name)s - %(levelname)s:%(lineno)d - %(message)s"
2213
SQL_DEBUG=False
2314

2415
CACHE_TYPE="memory"
25-
CACHE_REDIS_URL="redis://127.0.0.1:6379"
2616

2717
# Postgres
2818
POSTGRES_SERVER=localhost
@@ -31,12 +21,6 @@ POSTGRES_DB=sqlbot
3121
POSTGRES_USER=root
3222
POSTGRES_PASSWORD=132456 # Change this to your pwd
3323

34-
SENTRY_DSN=
35-
36-
# Configure these with your own Docker registry images
37-
DOCKER_IMAGE_BACKEND=backend
38-
DOCKER_IMAGE_FRONTEND=frontend
39-
4024
MCP_IMAGE_PATH=/opt/sqlbot/images
4125
MCP_IMAGE_HOST=http://localhost:3000
4226
SERVER_IMAGE_HOST=https://sqlbot.fit2cloud.cn/images/

backend/apps/mcp/mcp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ async def mcp_question(session: SessionDep, chat: ChatMcp):
106106
raise HTTPException(status_code=400, detail="Inactive user")
107107

108108
# ask
109-
llm_service = LLMService(session, session_user, chat)
109+
llm_service = LLMService(session_user, chat)
110110
llm_service.init_record()
111111

112112
return StreamingResponse(llm_service.run_task(False), media_type="text/event-stream")

backend/apps/system/api/aimodel.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import json
22
from typing import List, Union
3+
4+
from fastapi.responses import StreamingResponse
35
from apps.ai_model.model_factory import LLMConfig, LLMFactory
46
from apps.system.schemas.ai_model_schema import AiModelConfigItem, AiModelCreator, AiModelEditor, AiModelGridItem
57
from fastapi import APIRouter, Query
@@ -14,21 +16,31 @@
1416

1517
@router.post("/status")
1618
async def check_llm(info: AiModelCreator, trans: Trans):
17-
try:
18-
additional_params = {item.key: item.val for item in info.config_list}
19-
config = LLMConfig(
20-
model_type="openai",
21-
model_name=info.base_model,
22-
api_key=info.api_key,
23-
api_base_url=info.api_domain,
24-
additional_params=additional_params,
25-
)
26-
llm_instance = LLMFactory.create_llm(config)
27-
result = llm_instance.llm.invoke("who are you?")
28-
SQLBotLogUtil.info(f"check_llm result: {result}")
29-
except Exception as e:
30-
SQLBotLogUtil.error(f"Error checking LLM: {e}")
31-
raise Exception(trans('i18n_llm.validate_error', msg = str(e)))
19+
async def generate():
20+
try:
21+
additional_params = {item.key: item.val for item in info.config_list}
22+
config = LLMConfig(
23+
model_type="openai",
24+
model_name=info.base_model,
25+
api_key=info.api_key,
26+
api_base_url=info.api_domain,
27+
additional_params=additional_params,
28+
)
29+
llm_instance = LLMFactory.create_llm(config)
30+
31+
res = llm_instance.llm.stream("who are you?")
32+
33+
for chunk in res:
34+
if chunk and chunk.content:
35+
SQLBotLogUtil.info(chunk)
36+
yield json.dumps({"content": chunk.content}) + "\n"
37+
38+
except Exception as e:
39+
SQLBotLogUtil.error(f"Error checking LLM: {e}")
40+
error_msg = trans('i18n_llm.validate_error', msg=str(e))
41+
yield json.dumps({"error": error_msg}) + "\n"
42+
43+
return StreamingResponse(generate(), media_type="application/x-ndjson")
3244

3345
@router.get("", response_model=list[AiModelGridItem])
3446
async def query(

backend/common/core/config.py

Lines changed: 1 addition & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
import secrets
2-
import warnings
32
from typing import Annotated, Any, Literal
43

54
from pydantic import (
65
AnyUrl,
76
BeforeValidator,
8-
EmailStr,
9-
HttpUrl,
107
PostgresDsn,
118
computed_field,
12-
model_validator,
139
)
1410
from pydantic_core import MultiHostUrl
1511
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -31,12 +27,12 @@ class Settings(BaseSettings):
3127
env_ignore_empty=True,
3228
extra="ignore",
3329
)
30+
PROJECT_NAME: str = "SQLBot"
3431
API_V1_STR: str = "/api/v1"
3532
SECRET_KEY: str = secrets.token_urlsafe(32)
3633
# 60 minutes * 24 hours * 8 days = 8 days
3734
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
3835
FRONTEND_HOST: str = "http://localhost:5173"
39-
ENVIRONMENT: Literal["local", "staging", "production"] = "local"
4036

4137
BACKEND_CORS_ORIGINS: Annotated[
4238
list[AnyUrl] | str, BeforeValidator(parse_cors)
@@ -49,8 +45,6 @@ def all_cors_origins(self) -> list[str]:
4945
self.FRONTEND_HOST
5046
]
5147

52-
PROJECT_NAME: str
53-
SENTRY_DSN: HttpUrl | None = None
5448
POSTGRES_SERVER: str
5549
POSTGRES_PORT: int = 5432
5650
POSTGRES_USER: str
@@ -81,55 +75,8 @@ def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
8175
path=self.POSTGRES_DB,
8276
)
8377

84-
SMTP_TLS: bool = True
85-
SMTP_SSL: bool = False
86-
SMTP_PORT: int = 587
87-
SMTP_HOST: str | None = None
88-
SMTP_USER: str | None = None
89-
SMTP_PASSWORD: str | None = None
90-
EMAILS_FROM_EMAIL: EmailStr | None = None
91-
EMAILS_FROM_NAME: EmailStr | None = None
9278
MCP_IMAGE_PATH: str
9379
MCP_IMAGE_HOST: str
9480
SERVER_IMAGE_HOST: str
9581

96-
@model_validator(mode="after")
97-
def _set_default_emails_from(self) -> Self:
98-
if not self.EMAILS_FROM_NAME:
99-
self.EMAILS_FROM_NAME = self.PROJECT_NAME
100-
return self
101-
102-
EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48
103-
104-
@computed_field # type: ignore[prop-decorator]
105-
@property
106-
def emails_enabled(self) -> bool:
107-
return bool(self.SMTP_HOST and self.EMAILS_FROM_EMAIL)
108-
109-
EMAIL_TEST_USER: EmailStr = "[email protected]"
110-
FIRST_SUPERUSER: EmailStr
111-
FIRST_SUPERUSER_PASSWORD: str
112-
113-
def _check_default_secret(self, var_name: str, value: str | None) -> None:
114-
if value == "changethis":
115-
message = (
116-
f'The value of {var_name} is "changethis", '
117-
"for security, please change it, at least for deployments."
118-
)
119-
if self.ENVIRONMENT == "local":
120-
warnings.warn(message, stacklevel=1)
121-
else:
122-
raise ValueError(message)
123-
124-
@model_validator(mode="after")
125-
def _enforce_non_default_secrets(self) -> Self:
126-
self._check_default_secret("SECRET_KEY", self.SECRET_KEY)
127-
self._check_default_secret("POSTGRES_PASSWORD", self.POSTGRES_PASSWORD)
128-
self._check_default_secret(
129-
"FIRST_SUPERUSER_PASSWORD", self.FIRST_SUPERUSER_PASSWORD
130-
)
131-
132-
return self
133-
134-
13582
settings = Settings() # type: ignore

backend/main.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from fastapi.concurrency import asynccontextmanager
2-
import sentry_sdk
32
from fastapi import FastAPI
43
from fastapi.routing import APIRoute
54
from starlette.middleware.cors import CORSMiddleware
@@ -38,9 +37,6 @@ def custom_generate_unique_id(route: APIRoute) -> str:
3837
return f"{tag}-{route.name}"
3938

4039

41-
if settings.SENTRY_DSN and settings.ENVIRONMENT != "local":
42-
sentry_sdk.init(dsn=str(settings.SENTRY_DSN), enable_tracing=True)
43-
4440
app = FastAPI(
4541
title=settings.PROJECT_NAME,
4642
openapi_url=f"{settings.API_V1_STR}/openapi.json",

backend/template.yaml

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,19 @@ template:
44
### 请使用语言:{lang} 回答,若有深度思考过程,则思考过程也需要使用 {lang} 输出
55
66
任务:
7-
根据表结构和问题生成符合{engine}数据库引擎规范的sql语句,以及sql中所用到的表名(不要包含schema和database,用数组返回)。
7+
根据给定的表结构(M-Schema)和用户问题生成符合{engine}数据库引擎规范的sql语句,以及sql中所用到的表名(不要包含schema和database,用数组返回)。
88
你必须遵守以下规则:
9+
- 不要编造没有提供给你的表结构
910
- 生成的SQL必须符合{engine}的规范。
11+
- 你的回答必须使用如下JSON格式返回:
12+
{{"success":true,"sql":"生成的SQL语句","tables":["表名1","表名2",...]}}
13+
- 问题与生成SQL无关时,直接回答:
14+
{{"success":false,"message":"抱歉,我无法回答您的问题。"}}
15+
- 如果根据提供的表结构不能生成符合问题与条件的SQL,回答:
16+
{{"success":false,"message":"无法生成SQL的原因"}}
17+
- 如果问题是图表展示相关且与生成SQL查询无关时,请参考上一次回答的SQL来生成SQL
18+
- 如果问题是图表展示相关,可参考的图表类型为表格(table)、柱状图(column)、条形图(bar)、折线图(line)或饼图(pie),返回的JSON:
19+
{{"success":true,"sql":"生成的SQL语句","chart-type":"选择的图表类型(table/column/bar/line/pie)","tables":["表名1","表名2",...]}}
1020
- 提问中如果有涉及数据源名称或数据源描述的内容,则忽略数据源的信息,直接根据剩余内容生成SQL
1121
- 根据表结构生成SQL语句,需给每个表名生成一个别名(不要加AS)。
1222
- SQL查询中不能使用星号(*),必须明确指定字段名.
@@ -18,33 +28,37 @@ template:
1828
- 如数据库引擎是 MySQL,则在表名、字段名、别名外层加反引号;
1929
- 如数据库引擎是 Microsoft SQL Server,则在schema、表名、字段名、别名外层加方括号。
2030
- 以PostgreSQL为例,查询Schema为TEST表TABLE下所有数据,则生成的SQL为:
21-
SELECT * FROM "TEST"."TABLE"
31+
SELECT "id" FROM "TEST"."TABLE"
2232
- 注意在表名外双引号的位置,千万不要生成为:
23-
SELECT * FROM "TEST.TABLE"
33+
SELECT "id" FROM "TEST.TABLE"
2434
- 如果生成SQL的字段内有时间格式的字段(重要):
2535
- 若提问中没有指定查询顺序,则默认按时间升序排序
2636
- 若提问是时间,且没有指定具体格式,则格式化为yyyy-MM-dd HH:mm:ss的格式
2737
- 若提问是日期,且没有指定具体格式,则格式化为yyyy-MM-dd的格式
2838
- 若提问是年月,且没有指定具体格式,则格式化为yyyy-MM的格式
2939
- 若提问是年,且没有指定具体格式,则格式化为yyyy的格式
3040
- 生成的格式化语法需要适配对应的数据库引擎。
31-
- 生成的SQL使用JSON格式返回:
32-
{{"success":true,"sql":"生成的SQL语句","tables":["表名1","表名2",...]}}
33-
- 问题与生成SQL无关时,直接回答:
34-
{{"success":false,"message":"抱歉,我无法回答您的问题。"}}
35-
- 如果根据提供的表结构不能生成符合问题与条件的SQL,回答:
36-
{{"success":false,"message":"无法生成SQL的原因"}}
37-
- 如果问题是图表展示相关且与生成SQL查询无关时,请参考上一次回答的SQL来生成SQL
38-
- 如果问题是图表展示相关,可参考的图表类型为表格(table)、柱状图(column)、条形图(bar)、折线图(line)或饼图(pie),返回的JSON:
39-
{{"success":true,"sql":"生成的SQL语句","chart-type":"选择的图表类型(table/column/bar/line/pie)","tables":["表名1","表名2",...]}}
4041
- 生成的SQL查询结果可以用来进行图表展示,需要注意排序字段的排序优先级,例如:
4142
- 柱状图或折线图:适合展示在横轴的字段优先排序,若SQL包含分类字段,则分类字段次一级排序
4243
44+
### M-Schema格式简单的解释如下:
45+
```
46+
【DB_ID】 [Database名]
47+
【Schema】
48+
# Table: [Database名].[Table名], [表描述(若没有则为空)]
49+
[
50+
([字段名1]:[字段1的类型], [字段1的描述(这一行的逗号后都是描述,若没有则为空)]),
51+
([字段名2]:[字段2的类型], [字段2的描述(这一行的逗号后都是描述,若没有则为空)]),
52+
([字段名3]:[字段3的类型], [字段3的描述(这一行的逗号后都是描述,若没有则为空)]),
53+
...
54+
]
55+
```
56+
4357
### 响应, 请直接返回JSON结果:
4458
```json
4559
4660
user: |
47-
### 表结构:
61+
### 提供表结构如下:
4862
{schema}
4963
5064
### 问题:

frontend/src/api/system.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,5 @@ export const modelApi = {
88
delete: (id: number) => request.delete(`/system/aimodel/${id}`),
99
query: (id: number) => request.get(`/system/aimodel/${id}`),
1010
setDefault: (id: number) => request.put(`/system/aimodel/default/${id}`),
11-
check: (data: any) =>
12-
request.post('/system/aimodel/status', data, { requestOptions: { silent: true } }),
11+
check: (data: any) => request.fetchStream('/system/aimodel/status', data),
1312
}

frontend/src/views/system/model/Card.vue

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import delIcon from '@/assets/svg/icon_delete.svg'
33
import edit from '@/assets/svg/icon_edit_outlined.svg'
44
import { get_supplier } from '@/entity/supplier'
5-
import { computed } from 'vue'
5+
import { computed, ref } from 'vue'
66
const props = withDefaults(
77
defineProps<{
88
name: string
@@ -21,12 +21,22 @@ const props = withDefaults(
2121
supplier: 0,
2222
}
2323
)
24+
const errorMsg = ref('')
2425
const current_supplier = computed(() => {
2526
if (!props.supplier) {
2627
return null
2728
}
2829
return get_supplier(props.supplier)
2930
})
31+
const showErrorMask = (msg?: string) => {
32+
if (!msg) {
33+
return
34+
}
35+
errorMsg.value = msg
36+
setTimeout(() => {
37+
errorMsg.value = ''
38+
}, 3000)
39+
}
3040
const emits = defineEmits(['edit', 'del'])
3141
3242
const handleEdit = () => {
@@ -36,10 +46,17 @@ const handleEdit = () => {
3646
const handleDel = () => {
3747
emits('del', { id: props.id, name: props.name, default_model: props.isDefault })
3848
}
49+
50+
defineExpose({ showErrorMask })
3951
</script>
4052

4153
<template>
42-
<div class="card">
54+
<div
55+
v-loading="!!errorMsg"
56+
class="card"
57+
:element-loading-text="errorMsg"
58+
element-loading-custom-class="model-card-loading"
59+
>
4360
<div class="name-icon">
4461
<img :src="current_supplier?.icon" width="32px" height="32px" />
4562
<span :title="name" class="name ellipsis">{{ name }}</span>
@@ -163,5 +180,29 @@ const handleDel = () => {
163180
display: flex;
164181
}
165182
}
183+
:deep(.model-card-loading) {
184+
border-radius: 12px;
185+
display: flex;
186+
flex-direction: column;
187+
justify-content: flex-end;
188+
align-items: end;
189+
background-color: rgb(122 122 122 / 87%);
190+
.ed-loading-spinner {
191+
top: auto;
192+
margin: 8px 4px;
193+
display: flex;
194+
position: relative;
195+
justify-content: flex-end;
196+
align-items: center;
197+
width: calc(100% - 8px);
198+
}
199+
svg {
200+
display: none;
201+
}
202+
p {
203+
text-align: left;
204+
color: var(--ed-color-danger);
205+
}
206+
}
166207
}
167208
</style>

0 commit comments

Comments
 (0)