Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ cover/

# Django stuff:
*.log
backend/logs/
local_settings.py
db.sqlite3
db.sqlite3-journal
Expand Down
23 changes: 23 additions & 0 deletions backend/alembic/versions/066_add_custom_prompt_is_full_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""066_add_custom_prompt_is_full_template

Revision ID: a1b2c3d4e5f6
Revises: 8ff90df7871d
Create Date: 2026-03-09

"""
from alembic import op
import sqlalchemy as sa

revision = 'a1b2c3d4e5f6'
down_revision = '8ff90df7871d'
branch_labels = None
depends_on = None


def upgrade():
op.add_column('custom_prompt', sa.Column('is_full_template', sa.Boolean(), nullable=True))
op.execute("UPDATE custom_prompt SET is_full_template = false WHERE is_full_template IS NULL")


def downgrade():
op.drop_column('custom_prompt', 'is_full_template')
17 changes: 16 additions & 1 deletion backend/apps/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,19 @@
from apps.datasource.api import datasource, table_relation, recommended_problem
from apps.mcp import mcp
from apps.openapi.demo import demo
from apps.system.api import login, user, aimodel, workspace, assistant, parameter, apikey, variable_api
from apps.system.api import (
login,
user,
aimodel,
workspace,
assistant,
parameter,
apikey,
variable_api,
template_prompt,
custom_prompt,
statistics,
)
from apps.terminology.api import terminology
from apps.settings.api import base
#from audit.api import audit_api
Expand Down Expand Up @@ -35,5 +47,8 @@
api_router.include_router(recommended_problem.router)

api_router.include_router(variable_api.router)
api_router.include_router(template_prompt.router)
api_router.include_router(custom_prompt.router)
api_router.include_router(statistics.router)

#api_router.include_router(audit_api.router)
26 changes: 16 additions & 10 deletions backend/apps/chat/models/chat_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,16 +294,22 @@ def sql_sys_question(self, db_type: Union[str, DB], enable_query_limit: bool = T
'example_answer_2']
_example_answer_3 = _sql_template['example_answer_3_with_limit'] if enable_query_limit else _sql_template[
'example_answer_3']
return _base_template['system'].format(engine=self.engine, schema=self.db_schema, question=self.question,
lang=self.lang, terminologies=self.terminologies,
data_training=self.data_training, custom_prompt=self.custom_prompt,
process_check=_process_check,
base_sql_rules=_base_sql_rules,
basic_sql_examples=_sql_examples,
example_engine=_example_engine,
example_answer_1=_example_answer_1,
example_answer_2=_example_answer_2,
example_answer_3=_example_answer_3)
return _base_template['system'].format(
engine=self.engine,
schema=self.db_schema,
question=self.question,
lang=self.lang,
terminologies=self.terminologies,
data_training=self.data_training,
custom_prompt=self.custom_prompt or "",
process_check=_process_check,
base_sql_rules=_base_sql_rules,
basic_sql_examples=_sql_examples,
example_engine=_example_engine,
example_answer_1=_example_answer_1,
example_answer_2=_example_answer_2,
example_answer_3=_example_answer_3,
)

def sql_user_question(self, current_time: str, change_title: bool):
_question = self.question
Expand Down
76 changes: 59 additions & 17 deletions backend/apps/chat/task/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
from apps.datasource.embedding.ds_embedding import get_ds_embedding
from apps.datasource.models.datasource import CoreDatasource
from apps.db.db import exec_sql, get_version, check_connection
from apps.system.crud import custom_prompt as custom_prompt_crud
from apps.system.crud.assistant import AssistantOutDs, AssistantOutDsFactory, get_assistant_ds
from apps.system.crud import custom_prompt as system_custom_prompt_crud
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate unused import of custom_prompt module

Low Severity

The module apps.system.crud.custom_prompt is imported twice under two different aliases: custom_prompt_crud on line 43 and system_custom_prompt_crud on line 45. Only system_custom_prompt_crud is actually used (at line 333 in build_rule_snippets). The custom_prompt_crud alias is dead code that adds confusion about which reference is canonical.

Fix in Cursor Fix in Web

from apps.system.crud.parameter_manage import get_groups
from apps.system.schemas.system_schema import AssistantOutDsSchema
from apps.terminology.curd.terminology import get_terminology_template
Expand Down Expand Up @@ -295,24 +297,64 @@ def filter_terminology_template(self, _session: Session, oid: int = None, ds_id:

def filter_custom_prompts(self, _session: Session, custom_prompt_type: CustomPromptTypeEnum, oid: int = None,
ds_id: int = None):
calculate_oid = oid
calculate_ds_id = ds_id
if self.current_assistant:
calculate_oid = self.current_assistant.oid if self.current_assistant.type != 4 else self.current_user.oid
if self.current_assistant.type == 1:
calculate_ds_id = None

self.current_logs[OperationEnum.FILTER_CUSTOM_PROMPT] = start_log(
session=_session,
operate=OperationEnum.FILTER_CUSTOM_PROMPT,
record_id=self.record.id,
local_operation=True,
)

# 1. 兼容 xpack 自定义提示词(如有 License)
xpack_prompt: str = ""
xpack_list: list = []
if SQLBotLicenseUtil.valid():
calculate_oid = oid
calculate_ds_id = ds_id
if self.current_assistant:
calculate_oid = self.current_assistant.oid if self.current_assistant.type != 4 else self.current_user.oid
if self.current_assistant.type == 1:
calculate_ds_id = None
self.current_logs[OperationEnum.FILTER_CUSTOM_PROMPT] = start_log(session=_session,
operate=OperationEnum.FILTER_CUSTOM_PROMPT,
record_id=self.record.id,
local_operation=True)
self.chat_question.custom_prompt, prompt_list = find_custom_prompts(_session, custom_prompt_type,
calculate_oid,
calculate_ds_id)
self.current_logs[OperationEnum.FILTER_CUSTOM_PROMPT] = end_log(session=_session,
log=self.current_logs[
OperationEnum.FILTER_CUSTOM_PROMPT],
full_message=prompt_list)
xpack_prompt, xpack_list = find_custom_prompts(
_session,
custom_prompt_type,
calculate_oid,
calculate_ds_id,
)

# 2. 系统内置 custom_prompt 作为补充规则,统一包成 <rule> 片段
system_rules_str: str = ""
system_rows = []
if custom_prompt_type in (
CustomPromptTypeEnum.GENERATE_SQL,
CustomPromptTypeEnum.ANALYSIS,
CustomPromptTypeEnum.PREDICT_DATA,
):
system_rules_str, system_rows = system_custom_prompt_crud.build_rule_snippets(
_session,
calculate_oid or 1,
custom_prompt_type.value,
calculate_ds_id,
)

# 3. 合并结果:xpack 在前,系统规则在后
pieces: list[str] = []
if xpack_prompt:
pieces.append(xpack_prompt)
if system_rules_str:
pieces.append(system_rules_str)
self.chat_question.custom_prompt = "\n".join(pieces) if pieces else ""

# 4. 记录日志,包含 xpack 与系统规则明细,便于排查
full_message = {
"xpack": xpack_list,
"system": [r.prompt for r in system_rows],
}
self.current_logs[OperationEnum.FILTER_CUSTOM_PROMPT] = end_log(
session=_session,
log=self.current_logs[OperationEnum.FILTER_CUSTOM_PROMPT],
full_message=full_message,
)

def filter_training_template(self, _session: Session, oid: int = None, ds_id: int = None):
self.current_logs[OperationEnum.FILTER_SQL_EXAMPLE] = start_log(session=_session,
Expand Down
109 changes: 109 additions & 0 deletions backend/apps/system/api/custom_prompt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""
自定义提示词 CRUD API:分页、增删改查、导出。
与前端 /system/custom_prompt/* 约定一致,不依赖 xpack。
"""
from typing import Optional, List
import io

from fastapi import APIRouter, Query, Body
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field

from apps.system.schemas.permission import SqlbotPermission, require_permissions
from apps.system.crud import custom_prompt as crud
from common.core.deps import SessionDep, CurrentUser

router = APIRouter(tags=["System"], prefix="/system/custom_prompt")


class CustomPromptBody(BaseModel):
id: Optional[int] = None
type: Optional[str] = None
name: Optional[str] = None
prompt: Optional[str] = None
specific_ds: Optional[bool] = False
datasource_ids: Optional[List[int]] = Field(default_factory=list)
is_full_template: Optional[bool] = False


def _row_to_dict(row) -> dict:
return {
"id": row.id,
"oid": row.oid,
"type": row.type,
"create_time": row.create_time.isoformat() if row.create_time else None,
"name": row.name,
"prompt": row.prompt,
"specific_ds": row.specific_ds,
"datasource_ids": row.datasource_ids or [],
"is_full_template": getattr(row, "is_full_template", False) or False,
}


@router.get("/{type}/page/{page_num}/{page_size}", summary="分页查询自定义提示词")
@require_permissions(permission=SqlbotPermission(role=["ws_admin"]))
async def page(
session: SessionDep,
current_user: CurrentUser,
type: str,
page_num: int,
page_size: int,
name: Optional[str] = Query(None, description="名称筛选"),
):
data, total_count = crud.page_list(
session, current_user.oid, type, page_num, page_size, name=name
)
return {"data": [_row_to_dict(d) for d in data], "total_count": total_count}


@router.get("/{id}", summary="获取单条自定义提示词")
@require_permissions(permission=SqlbotPermission(role=["ws_admin"]))
async def get_one(session: SessionDep, current_user: CurrentUser, id: int):
row = crud.get_one(session, id, current_user.oid)
if not row:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Not Found")
return _row_to_dict(row)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FastAPI route /{id} may shadow type-based paths

Medium Severity

The GET /{id} route (line 59, parameter typed as int) is declared after GET /{type}/page/... but before GET /{type}/export. In FastAPI, if a single-segment request like GET /system/custom_prompt/GENERATE_SQL is made, it matches /{id} first. Since GENERATE_SQL cannot be parsed as int, FastAPI returns a 422 validation error rather than a meaningful 404. This could confuse API consumers and may interfere if future single-segment string routes are added.

Additional Locations (1)

Fix in Cursor Fix in Web



@router.put("", summary="创建或更新自定义提示词")
@require_permissions(permission=SqlbotPermission(role=["ws_admin"]))
async def create_or_update(session: SessionDep, current_user: CurrentUser, body: CustomPromptBody):
data = body.model_dump()
row = crud.create_or_update(session, current_user.oid, data)
return _row_to_dict(row)


@router.delete("", summary="批量删除自定义提示词")
@require_permissions(permission=SqlbotPermission(role=["ws_admin"]))
async def delete_ids(session: SessionDep, current_user: CurrentUser, id_list: List[int] = Body(..., embed=False)):
crud.delete_by_ids(session, current_user.oid, id_list)


@router.get("/{type}/export", summary="导出自定义提示词")
@require_permissions(permission=SqlbotPermission(role=["ws_admin"]))
async def export_excel(
session: SessionDep,
current_user: CurrentUser,
type: str,
name: Optional[str] = Query(None),
):
rows = crud.list_for_export(session, current_user.oid, type, name=name)
try:
import pandas as pd
data_list = [
{"name": r.name, "prompt": r.prompt or "", "specific_ds": r.specific_ds, "datasource_ids": r.datasource_ids or []}
for r in rows
]
df = pd.DataFrame(data_list)
buf = io.BytesIO()
df.to_excel(buf, index=False)
buf.seek(0)
return StreamingResponse(
buf,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename=custom_prompt_{type}.xlsx"},
)
except Exception:
from fastapi.responses import JSONResponse
return JSONResponse(content={"detail": "Export failed"}, status_code=500)
Loading
Loading