mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 01:06:59 +08:00
feat: support configuring terminology for specified advanced applications
This commit is contained in:
@@ -189,3 +189,5 @@ test.py
|
||||
|
||||
|
||||
!/.venv/
|
||||
|
||||
sqlbot-xpack
|
||||
|
||||
@@ -25,8 +25,8 @@ config = context.config
|
||||
# from apps.system.models.user import SQLModel # noqa
|
||||
# from apps.settings.models.setting_models import SQLModel
|
||||
#from apps.chat.models.chat_model import SQLModel
|
||||
#from apps.terminology.models.terminology_model import SQLModel
|
||||
#from apps.custom_prompt.models.custom_prompt_model import SQLModel
|
||||
from apps.terminology.models.terminology_model import SQLModel
|
||||
from sqlbot_xpack.custom_prompt.models.custom_prompt_model import SQLModel
|
||||
#from apps.data_training.models.data_training_model import SQLModel
|
||||
# from apps.dashboard.models.dashboard_model import SQLModel
|
||||
from common.core.config import settings # noqa
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""069_term_custom_prompt
|
||||
|
||||
Revision ID: 1f82cad3546e
|
||||
Revises: a1b2c3d4e5f6
|
||||
Create Date: 2026-06-15 14:51:12.280391
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel.sql.sqltypes
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1f82cad3546e'
|
||||
down_revision = 'a1b2c3d4e5f6'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('custom_prompt', sa.Column('advanced_application', sa.BigInteger(), nullable=True))
|
||||
op.add_column('terminology', sa.Column('advanced_application', sa.BigInteger(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('terminology', 'advanced_application')
|
||||
op.drop_column('custom_prompt', 'advanced_application')
|
||||
# ### end Alembic commands ###
|
||||
@@ -12,9 +12,8 @@ from typing import Any, List, Optional, Union, Dict, Iterator
|
||||
import orjson
|
||||
import pandas as pd
|
||||
import requests
|
||||
import sqlparse
|
||||
import sqlglot
|
||||
from sqlglot import exp
|
||||
import sqlparse
|
||||
from langchain.chat_models.base import BaseChatModel
|
||||
from langchain_community.utilities import SQLDatabase
|
||||
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, BaseMessageChunk
|
||||
@@ -24,6 +23,7 @@ from sqlbot_xpack.config.model import SysArgModel
|
||||
from sqlbot_xpack.custom_prompt.curd.custom_prompt import find_custom_prompts
|
||||
from sqlbot_xpack.custom_prompt.models.custom_prompt_model import CustomPromptTypeEnum
|
||||
from sqlbot_xpack.license.license_manage import SQLBotLicenseUtil
|
||||
from sqlglot import exp
|
||||
from sqlmodel import Session
|
||||
|
||||
from apps.ai_model.model_factory import LLMConfig, LLMFactory, get_default_config
|
||||
@@ -68,7 +68,6 @@ session_maker = scoped_session(sessionmaker(bind=engine, class_=Session))
|
||||
i18n = I18n()
|
||||
|
||||
|
||||
|
||||
def extract_tables_from_sql(sql: str, ds_type: str = None) -> set:
|
||||
"""从 SQL 中提取表名(使用 sqlglot 解析,可信)"""
|
||||
tables = set()
|
||||
@@ -340,18 +339,26 @@ class LLMService:
|
||||
return format_chart_fields(chart_info)
|
||||
|
||||
def filter_terminology_template(self, _session: Session, oid: int = None, ds_id: int = None):
|
||||
self.current_logs[OperationEnum.FILTER_TERMS] = start_log(session=_session,
|
||||
operate=OperationEnum.FILTER_TERMS,
|
||||
record_id=self.record.id, local_operation=True)
|
||||
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_TERMS] = start_log(session=_session,
|
||||
operate=OperationEnum.FILTER_TERMS,
|
||||
record_id=self.record.id, local_operation=True)
|
||||
if self.current_assistant and self.current_assistant.type == 1:
|
||||
self.chat_question.terminologies, term_list = get_terminology_template(_session,
|
||||
self.chat_question.question,
|
||||
calculate_oid,
|
||||
None, self.current_assistant.id)
|
||||
else:
|
||||
self.chat_question.terminologies, term_list = get_terminology_template(_session,
|
||||
self.chat_question.question,
|
||||
calculate_oid,
|
||||
calculate_ds_id)
|
||||
|
||||
self.chat_question.terminologies, term_list = get_terminology_template(_session, self.chat_question.question,
|
||||
calculate_oid, calculate_ds_id)
|
||||
self.current_logs[OperationEnum.FILTER_TERMS] = end_log(session=_session,
|
||||
log=self.current_logs[OperationEnum.FILTER_TERMS],
|
||||
full_message=term_list)
|
||||
@@ -359,19 +366,26 @@ class LLMService:
|
||||
def filter_custom_prompts(self, _session: Session, custom_prompt_type: CustomPromptTypeEnum, oid: int = None,
|
||||
ds_id: int = None):
|
||||
if SQLBotLicenseUtil.valid():
|
||||
self.current_logs[OperationEnum.FILTER_CUSTOM_PROMPT] = start_log(session=_session,
|
||||
operate=OperationEnum.FILTER_CUSTOM_PROMPT,
|
||||
record_id=self.record.id,
|
||||
local_operation=True)
|
||||
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)
|
||||
if self.current_assistant and self.current_assistant.type == 1:
|
||||
self.chat_question.custom_prompt, prompt_list = find_custom_prompts(_session,
|
||||
custom_prompt_type,
|
||||
calculate_oid,
|
||||
None, self.current_assistant.id)
|
||||
else:
|
||||
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],
|
||||
|
||||
@@ -82,6 +82,7 @@ async def export_excel(session: SessionDep, trans: Trans, current_user: CurrentU
|
||||
"description": obj.description,
|
||||
"all_data_sources": 'N' if obj.specific_ds else 'Y',
|
||||
"datasource": ', '.join(obj.datasource_names) if obj.datasource_names and obj.specific_ds else '',
|
||||
"advanced_application_name": obj.advanced_application_name or '',
|
||||
}
|
||||
data_list.append(_data)
|
||||
|
||||
@@ -91,6 +92,7 @@ async def export_excel(session: SessionDep, trans: Trans, current_user: CurrentU
|
||||
fields.append(AxisObj(name=trans('i18n_terminology.term_description'), value='description'))
|
||||
fields.append(AxisObj(name=trans('i18n_terminology.effective_data_sources'), value='datasource'))
|
||||
fields.append(AxisObj(name=trans('i18n_terminology.all_data_sources'), value='all_data_sources'))
|
||||
fields.append(AxisObj(name=trans('i18n_data_training.advanced_application'), value='advanced_application_name'))
|
||||
|
||||
md_data, _fields_list = DataFormat.convert_object_array_for_pandas(fields, data_list)
|
||||
|
||||
@@ -119,6 +121,7 @@ async def excel_template(trans: Trans):
|
||||
"description": trans('i18n_terminology.term_description_template_example_1'),
|
||||
"all_data_sources": 'N',
|
||||
"datasource": trans('i18n_terminology.effective_data_sources_template_example_1'),
|
||||
"advanced_application_name": '',
|
||||
}
|
||||
data_list.append(_data1)
|
||||
_data2 = {
|
||||
@@ -127,6 +130,7 @@ async def excel_template(trans: Trans):
|
||||
"description": trans('i18n_terminology.term_description_template_example_2'),
|
||||
"all_data_sources": 'Y',
|
||||
"datasource": '',
|
||||
"advanced_application_name": '',
|
||||
}
|
||||
data_list.append(_data2)
|
||||
|
||||
@@ -136,6 +140,7 @@ async def excel_template(trans: Trans):
|
||||
fields.append(AxisObj(name=trans('i18n_terminology.term_description_template'), value='description'))
|
||||
fields.append(AxisObj(name=trans('i18n_terminology.effective_data_sources_template'), value='datasource'))
|
||||
fields.append(AxisObj(name=trans('i18n_terminology.all_data_sources_template'), value='all_data_sources'))
|
||||
fields.append(AxisObj(name=trans('i18n_data_training.advanced_application'), value='advanced_application_name'))
|
||||
|
||||
md_data, _fields_list = DataFormat.convert_object_array_for_pandas(fields, data_list)
|
||||
|
||||
@@ -180,7 +185,7 @@ async def upload_excel(trans: Trans, current_user: CurrentUser, file: UploadFile
|
||||
|
||||
oid = current_user.oid
|
||||
|
||||
use_cols = [0, 1, 2, 3, 4]
|
||||
use_cols = [0, 1, 2, 3, 4, 5]
|
||||
|
||||
def inner():
|
||||
|
||||
@@ -217,9 +222,11 @@ async def upload_excel(trans: Trans, current_user: CurrentUser, file: UploadFile
|
||||
3].strip() else []
|
||||
all_datasource = True if pd.notna(row[4]) and row[4].lower().strip() in ['y', 'yes', 'true'] else False
|
||||
specific_ds = False if all_datasource else True
|
||||
advanced_application_name = row[5].strip() if pd.notna(row[5]) and row[5].strip() else None
|
||||
|
||||
import_data.append(TerminologyInfo(word=word, description=description, other_words=other_words,
|
||||
datasource_names=datasource_names, specific_ds=specific_ds))
|
||||
datasource_names=datasource_names, specific_ds=specific_ds,
|
||||
advanced_application_name=advanced_application_name))
|
||||
|
||||
res = batch_create_terminology(session, import_data, oid, trans)
|
||||
|
||||
@@ -237,6 +244,7 @@ async def upload_excel(trans: Trans, current_user: CurrentUser, file: UploadFile
|
||||
"all_data_sources": 'N' if obj['data'].specific_ds else 'Y',
|
||||
"datasource": ', '.join(obj['data'].datasource_names) if obj['data'].datasource_names and obj[
|
||||
'data'].specific_ds else '',
|
||||
"advanced_application_name": obj['data'].advanced_application_name or '',
|
||||
"errors": obj['errors']
|
||||
}
|
||||
data_list.append(_data)
|
||||
@@ -247,6 +255,7 @@ async def upload_excel(trans: Trans, current_user: CurrentUser, file: UploadFile
|
||||
fields.append(AxisObj(name=trans('i18n_terminology.term_description'), value='description'))
|
||||
fields.append(AxisObj(name=trans('i18n_terminology.effective_data_sources'), value='datasource'))
|
||||
fields.append(AxisObj(name=trans('i18n_terminology.all_data_sources'), value='all_data_sources'))
|
||||
fields.append(AxisObj(name=trans('i18n_data_training.advanced_application'), value='advanced_application_name'))
|
||||
fields.append(AxisObj(name=trans('i18n_data_training.error_info'), value='errors'))
|
||||
|
||||
md_data, _fields_list = DataFormat.convert_object_array_for_pandas(fields, data_list)
|
||||
|
||||
@@ -10,6 +10,7 @@ from sqlalchemy.orm import aliased
|
||||
|
||||
from apps.ai_model.embedding import EmbeddingModelCache
|
||||
from apps.datasource.models.datasource import CoreDatasource
|
||||
from apps.system.models.system_model import AssistantModel
|
||||
from apps.template.generate_chart.generator import get_base_terminology_template
|
||||
from apps.terminology.models.terminology_model import Terminology, TerminologyInfo
|
||||
from common.core.config import settings
|
||||
@@ -141,7 +142,9 @@ def build_terminology_query(session: SessionDep, oid: int, name: Optional[str] =
|
||||
Terminology.datasource_ids,
|
||||
children_subquery.c.other_words,
|
||||
func.jsonb_agg(CoreDatasource.name).filter(CoreDatasource.id.isnot(None)).label('datasource_names'),
|
||||
Terminology.enabled
|
||||
Terminology.enabled,
|
||||
Terminology.advanced_application,
|
||||
AssistantModel.name.label('advanced_application_name'),
|
||||
)
|
||||
.outerjoin(
|
||||
children_subquery,
|
||||
@@ -155,6 +158,8 @@ def build_terminology_query(session: SessionDep, oid: int, name: Optional[str] =
|
||||
CoreDatasource,
|
||||
CoreDatasource.id == datasource_names_subquery.c.ds_id
|
||||
)
|
||||
.outerjoin(AssistantModel,
|
||||
and_(Terminology.advanced_application == AssistantModel.id, AssistantModel.type == 1))
|
||||
.where(and_(Terminology.id.in_(paginated_parent_ids), Terminology.oid == oid))
|
||||
.group_by(
|
||||
Terminology.id,
|
||||
@@ -163,6 +168,8 @@ def build_terminology_query(session: SessionDep, oid: int, name: Optional[str] =
|
||||
Terminology.description,
|
||||
Terminology.specific_ds,
|
||||
Terminology.datasource_ids,
|
||||
Terminology.advanced_application,
|
||||
AssistantModel.name,
|
||||
children_subquery.c.other_words,
|
||||
Terminology.enabled
|
||||
)
|
||||
@@ -190,6 +197,8 @@ def execute_terminology_query(session: SessionDep, stmt) -> List[TerminologyInfo
|
||||
datasource_ids=row.datasource_ids if row.datasource_ids is not None else [],
|
||||
datasource_names=row.datasource_names if row.datasource_names is not None else [],
|
||||
enabled=row.enabled if row.enabled is not None else False,
|
||||
advanced_application=str(row.advanced_application) if row.advanced_application else None,
|
||||
advanced_application_name=row.advanced_application_name,
|
||||
))
|
||||
|
||||
return _list
|
||||
@@ -240,8 +249,8 @@ def create_terminology(session: SessionDep, info: TerminologyInfo, oid: int, tra
|
||||
datasource_ids = info.datasource_ids if info.datasource_ids is not None else []
|
||||
|
||||
if specific_ds:
|
||||
if not datasource_ids:
|
||||
raise Exception(trans("i18n_terminology.datasource_cannot_be_none"))
|
||||
if not datasource_ids and info.advanced_application is None:
|
||||
raise Exception(trans("i18n_data_training.datasource_assistant_cannot_be_none"))
|
||||
|
||||
parent = Terminology(
|
||||
word=info.word.strip(),
|
||||
@@ -250,7 +259,8 @@ def create_terminology(session: SessionDep, info: TerminologyInfo, oid: int, tra
|
||||
oid=oid,
|
||||
specific_ds=specific_ds,
|
||||
enabled=info.enabled,
|
||||
datasource_ids=datasource_ids
|
||||
datasource_ids=datasource_ids,
|
||||
advanced_application=info.advanced_application,
|
||||
)
|
||||
|
||||
words = [info.word.strip()]
|
||||
@@ -267,30 +277,63 @@ def create_terminology(session: SessionDep, info: TerminologyInfo, oid: int, tra
|
||||
# 基础查询条件(word 和 oid 必须满足)
|
||||
base_query = and_(
|
||||
Terminology.word.in_(words),
|
||||
Terminology.oid == oid
|
||||
Terminology.oid == oid,
|
||||
)
|
||||
|
||||
# 构建查询
|
||||
query = session.query(Terminology).filter(base_query)
|
||||
|
||||
if specific_ds:
|
||||
# 仅当 specific_ds=False 时,检查数据源条件
|
||||
query = query.where(
|
||||
or_(
|
||||
or_(Terminology.specific_ds == False, Terminology.specific_ds.is_(None)),
|
||||
and_(
|
||||
Terminology.specific_ds == True,
|
||||
Terminology.datasource_ids.isnot(None),
|
||||
text("""
|
||||
EXISTS (
|
||||
SELECT 1 FROM jsonb_array_elements(datasource_ids) AS elem
|
||||
WHERE elem::text::int = ANY(:datasource_ids)
|
||||
)
|
||||
""")
|
||||
# 作用域重复检查
|
||||
scope_conditions = []
|
||||
|
||||
if not specific_ds:
|
||||
# 全部数据源:与有数据源的记录冲突,也与同为全部数据源的记录冲突
|
||||
scope_conditions.append(
|
||||
and_(
|
||||
Terminology.specific_ds == True,
|
||||
Terminology.datasource_ids.isnot(None),
|
||||
func.jsonb_array_length(Terminology.datasource_ids) > 0,
|
||||
)
|
||||
)
|
||||
scope_conditions.append(
|
||||
Terminology.specific_ds == False,
|
||||
)
|
||||
elif specific_ds and datasource_ids:
|
||||
# 指定数据源:与全部数据源冲突,也与有数据源重叠的记录冲突
|
||||
scope_conditions.append(
|
||||
Terminology.specific_ds == False,
|
||||
)
|
||||
ds_overlap_conditions = [
|
||||
Terminology.datasource_ids.contains([ds_id])
|
||||
for ds_id in datasource_ids
|
||||
]
|
||||
scope_conditions.append(
|
||||
and_(
|
||||
Terminology.specific_ds == True,
|
||||
Terminology.datasource_ids.isnot(None),
|
||||
or_(*ds_overlap_conditions)
|
||||
)
|
||||
)
|
||||
else:
|
||||
# 不选数据源:仅与同为不选数据源的记录冲突
|
||||
scope_conditions.append(
|
||||
and_(
|
||||
Terminology.specific_ds == True,
|
||||
or_(
|
||||
Terminology.datasource_ids.is_(None),
|
||||
func.jsonb_array_length(Terminology.datasource_ids) == 0,
|
||||
)
|
||||
)
|
||||
)
|
||||
query = query.params(datasource_ids=datasource_ids)
|
||||
|
||||
# 高级应用重复检查:advanced_application 相同时同名即重复
|
||||
if info.advanced_application is not None:
|
||||
scope_conditions.append(
|
||||
Terminology.advanced_application == info.advanced_application
|
||||
)
|
||||
|
||||
if scope_conditions:
|
||||
query = query.where(or_(*scope_conditions))
|
||||
|
||||
# 转换为 EXISTS 查询并获取结果
|
||||
exists = session.query(query.exists()).scalar()
|
||||
@@ -396,6 +439,14 @@ def batch_create_terminology(session: SessionDep, info_list: List[TerminologyInf
|
||||
for ds in datasource_result:
|
||||
datasource_name_to_id[ds.name.strip()] = ds.id
|
||||
|
||||
# 预加载高级应用名称到ID的映射
|
||||
assistant_name_to_id = {}
|
||||
assistant_stmt = select(AssistantModel.id, AssistantModel.name).where(
|
||||
and_(AssistantModel.oid == oid, AssistantModel.type == 1))
|
||||
assistant_result = session.execute(assistant_stmt).all()
|
||||
for a in assistant_result:
|
||||
assistant_name_to_id[a.name.strip()] = a.id
|
||||
|
||||
# 验证和转换数据源名称
|
||||
valid_records = []
|
||||
for info in deduplicated_list:
|
||||
@@ -411,6 +462,7 @@ def batch_create_terminology(session: SessionDep, info_list: List[TerminologyInf
|
||||
# 根据specific_ds决定是否验证数据源
|
||||
specific_ds = info.specific_ds if info.specific_ds is not None else False
|
||||
datasource_ids = []
|
||||
advanced_application = info.advanced_application
|
||||
|
||||
if specific_ds:
|
||||
# specific_ds为True时需要验证数据源
|
||||
@@ -424,12 +476,21 @@ def batch_create_terminology(session: SessionDep, info_list: List[TerminologyInf
|
||||
else:
|
||||
error_messages.append(trans("i18n_terminology.datasource_not_found").format(ds_name))
|
||||
|
||||
# 检查specific_ds为True时必须有数据源
|
||||
if not datasource_ids:
|
||||
error_messages.append(trans("i18n_terminology.datasource_cannot_be_none"))
|
||||
# 解析高级应用名称到ID
|
||||
if advanced_application is None and info.advanced_application_name:
|
||||
if info.advanced_application_name.strip() in assistant_name_to_id:
|
||||
advanced_application = assistant_name_to_id[info.advanced_application_name.strip()]
|
||||
else:
|
||||
error_messages.append(trans("i18n_data_training.advanced_application_not_found").format(
|
||||
info.advanced_application_name))
|
||||
|
||||
# 检查specific_ds为True时datasource_ids和advanced_application不能同时为空
|
||||
if specific_ds and not datasource_ids and advanced_application is None:
|
||||
error_messages.append(trans("i18n_data_training.datasource_assistant_cannot_be_none"))
|
||||
else:
|
||||
# specific_ds为False时忽略数据源名称
|
||||
datasource_ids = []
|
||||
advanced_application = None
|
||||
|
||||
# 检查主词和其他词是否重复(过滤空字符串)
|
||||
words = [info.word.strip().lower()]
|
||||
@@ -456,11 +517,13 @@ def batch_create_terminology(session: SessionDep, info_list: List[TerminologyInf
|
||||
processed_info = TerminologyInfo(
|
||||
word=info.word.strip(),
|
||||
description=info.description.strip(),
|
||||
other_words=[w for w in info.other_words if w and w.strip()], # 过滤空字符串
|
||||
other_words=[w for w in info.other_words if w and w.strip()],
|
||||
datasource_ids=datasource_ids,
|
||||
datasource_names=info.datasource_names,
|
||||
specific_ds=specific_ds,
|
||||
enabled=info.enabled if info.enabled is not None else True
|
||||
enabled=info.enabled if info.enabled is not None else True,
|
||||
advanced_application=advanced_application,
|
||||
advanced_application_name=info.advanced_application_name,
|
||||
)
|
||||
|
||||
valid_records.append(processed_info)
|
||||
@@ -512,8 +575,8 @@ def update_terminology(session: SessionDep, info: TerminologyInfo, oid: int, tra
|
||||
datasource_ids = info.datasource_ids if info.datasource_ids is not None else []
|
||||
|
||||
if specific_ds:
|
||||
if not datasource_ids:
|
||||
raise Exception(trans("i18n_terminology.datasource_cannot_be_none"))
|
||||
if not datasource_ids and info.advanced_application is None:
|
||||
raise Exception(trans("i18n_data_training.datasource_assistant_cannot_be_none"))
|
||||
|
||||
words = [info.word.strip()]
|
||||
for child in info.other_words:
|
||||
@@ -536,24 +599,57 @@ def update_terminology(session: SessionDep, info: TerminologyInfo, oid: int, tra
|
||||
# 构建查询
|
||||
query = session.query(Terminology).filter(base_query)
|
||||
|
||||
if specific_ds:
|
||||
# 仅当 specific_ds=False 时,检查数据源条件
|
||||
query = query.where(
|
||||
or_(
|
||||
or_(Terminology.specific_ds == False, Terminology.specific_ds.is_(None)),
|
||||
and_(
|
||||
Terminology.specific_ds == True,
|
||||
Terminology.datasource_ids.isnot(None),
|
||||
text("""
|
||||
EXISTS (
|
||||
SELECT 1 FROM jsonb_array_elements(datasource_ids) AS elem
|
||||
WHERE elem::text::int = ANY(:datasource_ids)
|
||||
)
|
||||
""") # 检查是否包含任意目标值
|
||||
# 作用域重复检查
|
||||
scope_conditions = []
|
||||
|
||||
if not specific_ds:
|
||||
# 全部数据源:与有数据源的记录冲突,也与同为全部数据源的记录冲突
|
||||
scope_conditions.append(
|
||||
and_(
|
||||
Terminology.specific_ds == True,
|
||||
Terminology.datasource_ids.isnot(None),
|
||||
func.jsonb_array_length(Terminology.datasource_ids) > 0,
|
||||
)
|
||||
)
|
||||
scope_conditions.append(
|
||||
Terminology.specific_ds == False,
|
||||
)
|
||||
elif specific_ds and datasource_ids:
|
||||
# 指定数据源:与全部数据源冲突,也与有数据源重叠的记录冲突
|
||||
scope_conditions.append(
|
||||
Terminology.specific_ds == False,
|
||||
)
|
||||
ds_overlap_conditions = [
|
||||
Terminology.datasource_ids.contains([ds_id])
|
||||
for ds_id in datasource_ids
|
||||
]
|
||||
scope_conditions.append(
|
||||
and_(
|
||||
Terminology.specific_ds == True,
|
||||
Terminology.datasource_ids.isnot(None),
|
||||
or_(*ds_overlap_conditions)
|
||||
)
|
||||
)
|
||||
else:
|
||||
# 不选数据源:仅与同为不选数据源的记录冲突
|
||||
scope_conditions.append(
|
||||
and_(
|
||||
Terminology.specific_ds == True,
|
||||
or_(
|
||||
Terminology.datasource_ids.is_(None),
|
||||
func.jsonb_array_length(Terminology.datasource_ids) == 0,
|
||||
)
|
||||
)
|
||||
)
|
||||
query = query.params(datasource_ids=datasource_ids)
|
||||
|
||||
# 高级应用重复检查:advanced_application 相同时同名即重复
|
||||
if info.advanced_application is not None:
|
||||
scope_conditions.append(
|
||||
Terminology.advanced_application == info.advanced_application
|
||||
)
|
||||
|
||||
if scope_conditions:
|
||||
query = query.where(or_(*scope_conditions))
|
||||
|
||||
# 转换为 EXISTS 查询并获取结果
|
||||
exists = session.query(query.exists()).scalar()
|
||||
@@ -567,6 +663,7 @@ def update_terminology(session: SessionDep, info: TerminologyInfo, oid: int, tra
|
||||
specific_ds=specific_ds,
|
||||
datasource_ids=datasource_ids,
|
||||
enabled=info.enabled,
|
||||
advanced_application=info.advanced_application,
|
||||
)
|
||||
session.execute(stmt)
|
||||
session.commit()
|
||||
@@ -590,7 +687,8 @@ def update_terminology(session: SessionDep, info: TerminologyInfo, oid: int, tra
|
||||
oid=oid,
|
||||
enabled=info.enabled,
|
||||
specific_ds=specific_ds,
|
||||
datasource_ids=datasource_ids
|
||||
datasource_ids=datasource_ids,
|
||||
advanced_application=info.advanced_application,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -711,8 +809,22 @@ ORDER BY similarity DESC
|
||||
LIMIT {settings.EMBEDDING_TERMINOLOGY_TOP_COUNT}
|
||||
"""
|
||||
|
||||
embedding_sql_with_advanced_application = f"""
|
||||
SELECT id, pid, word, similarity
|
||||
FROM
|
||||
(SELECT id, pid, word, oid, specific_ds, advanced_application, enabled,
|
||||
( 1 - (embedding <=> :embedding_array) ) AS similarity
|
||||
FROM terminology AS child
|
||||
) TEMP
|
||||
WHERE similarity > {settings.EMBEDDING_TERMINOLOGY_SIMILARITY} AND oid = :oid AND enabled = true
|
||||
AND advanced_application = :advanced_application_id
|
||||
ORDER BY similarity DESC
|
||||
LIMIT {settings.EMBEDDING_TERMINOLOGY_TOP_COUNT}
|
||||
"""
|
||||
|
||||
def select_terminology_by_word(session: SessionDep, word: str, oid: int, datasource: int = None):
|
||||
|
||||
def select_terminology_by_word(session: SessionDep, word: str, oid: int, datasource: int = None,
|
||||
advanced_application_id: Optional[int] = None):
|
||||
if word.strip() == "":
|
||||
return []
|
||||
|
||||
@@ -729,7 +841,9 @@ def select_terminology_by_word(session: SessionDep, word: str, oid: int, datasou
|
||||
)
|
||||
)
|
||||
|
||||
if datasource is not None:
|
||||
if advanced_application_id is not None:
|
||||
stmt = stmt.where(Terminology.advanced_application == advanced_application_id)
|
||||
elif datasource is not None:
|
||||
stmt = stmt.where(
|
||||
or_(
|
||||
or_(Terminology.specific_ds == False, Terminology.specific_ds.is_(None)),
|
||||
@@ -760,7 +874,11 @@ def select_terminology_by_word(session: SessionDep, word: str, oid: int, datasou
|
||||
|
||||
embedding = model.embed_query(word)
|
||||
|
||||
if datasource is not None:
|
||||
if advanced_application_id is not None:
|
||||
results = session.execute(text(embedding_sql_with_advanced_application),
|
||||
{'embedding_array': str(embedding), 'oid': oid,
|
||||
'advanced_application_id': advanced_application_id}).fetchall()
|
||||
elif datasource is not None:
|
||||
results = session.execute(text(embedding_sql_with_datasource),
|
||||
{'embedding_array': str(embedding), 'oid': oid,
|
||||
'datasource': datasource}).fetchall()
|
||||
@@ -846,10 +964,11 @@ def to_xml_string(_dict: list[dict] | dict, root: str = 'terminologies') -> str:
|
||||
|
||||
|
||||
def get_terminology_template(session: SessionDep, question: str, oid: Optional[int] = 1,
|
||||
datasource: Optional[int] = None) -> tuple[str, list[dict]]:
|
||||
datasource: Optional[int] = None,
|
||||
advanced_application_id: Optional[int] = None) -> tuple[str, list[dict]]:
|
||||
if not oid:
|
||||
oid = 1
|
||||
_results = select_terminology_by_word(session, question, oid, datasource)
|
||||
_results = select_terminology_by_word(session, question, oid, datasource, advanced_application_id)
|
||||
if _results and len(_results) > 0:
|
||||
terminology = to_xml_string(_results)
|
||||
template = get_base_terminology_template().format(terminologies=terminology)
|
||||
|
||||
@@ -20,6 +20,7 @@ class Terminology(SQLModel, table=True):
|
||||
specific_ds: Optional[bool] = Field(sa_column=Column(Boolean, default=False))
|
||||
datasource_ids: Optional[list[int]] = Field(sa_column=Column(JSONB), default=[])
|
||||
enabled: Optional[bool] = Field(sa_column=Column(Boolean, default=True))
|
||||
advanced_application: Optional[int] = Field(sa_column=Column(BigInteger, nullable=True))
|
||||
|
||||
|
||||
class TerminologyInfo(BaseModel):
|
||||
@@ -32,3 +33,5 @@ class TerminologyInfo(BaseModel):
|
||||
datasource_ids: Optional[list[int]] = []
|
||||
datasource_names: Optional[list[str]] = []
|
||||
enabled: Optional[bool] = True
|
||||
advanced_application: Optional[int] = None
|
||||
advanced_application_name: Optional[str] = None
|
||||
|
||||
@@ -39,7 +39,7 @@ dependencies = [
|
||||
"pyyaml (>=6.0.2,<7.0.0)",
|
||||
"fastapi-mcp (>=0.3.4,<0.4.0)",
|
||||
"tabulate>=0.9.0",
|
||||
"sqlbot-xpack>=0.0.5.22,<0.0.6.0",
|
||||
"sqlbot-xpack>=0.0.5.23,<0.0.6.0",
|
||||
"fastapi-cache2>=0.2.2",
|
||||
"sqlparse>=0.5.3",
|
||||
"redis>=6.2.0",
|
||||
|
||||
@@ -15,6 +15,7 @@ import { convertFilterText, FilterText } from '@/components/filter-text'
|
||||
import { DrawerMain } from '@/components/drawer-main'
|
||||
import iconFilter from '@/assets/svg/icon-filter_outlined.svg'
|
||||
import Uploader from '@/views/system/excel-upload/Uploader.vue'
|
||||
import { getAdvancedApplicationList } from '@/api/embedded.ts'
|
||||
|
||||
interface Form {
|
||||
id?: string | null
|
||||
@@ -23,6 +24,8 @@ interface Form {
|
||||
specific_ds: boolean
|
||||
datasource_ids: number[]
|
||||
datasource_names: string[]
|
||||
advanced_application: string | null
|
||||
advanced_application_name: string | null
|
||||
description: string | null
|
||||
}
|
||||
|
||||
@@ -33,7 +36,7 @@ const keywords = ref('')
|
||||
const oldKeywords = ref('')
|
||||
const searchLoading = ref(false)
|
||||
const drawerMainRef = ref()
|
||||
|
||||
const adv_options = ref<any[]>([])
|
||||
const selectable = () => {
|
||||
return true
|
||||
}
|
||||
@@ -41,6 +44,9 @@ onMounted(() => {
|
||||
datasourceApi.list().then((res) => {
|
||||
filterOption.value[0].option = [...res]
|
||||
})
|
||||
getAdvancedApplicationList().then((res: any) => {
|
||||
adv_options.value = res || []
|
||||
})
|
||||
search()
|
||||
})
|
||||
const dialogFormVisible = ref<boolean>(false)
|
||||
@@ -69,6 +75,8 @@ const defaultForm = {
|
||||
datasource_ids: [],
|
||||
other_words: [''],
|
||||
datasource_names: [],
|
||||
advanced_application: null,
|
||||
advanced_application_name: null,
|
||||
}
|
||||
const pageForm = ref<Form>(cloneDeep(defaultForm))
|
||||
|
||||
@@ -252,13 +260,13 @@ const search = ($event: any = {}) => {
|
||||
|
||||
const termFormRef = ref()
|
||||
|
||||
const validatePass = (_: any, value: any, callback: any) => {
|
||||
if (pageForm.value.specific_ds && !value.length) {
|
||||
callback(new Error(t('datasource.Please_select') + t('common.empty') + t('ds.title')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
// const validatePass = (_: any, value: any, callback: any) => {
|
||||
// if (pageForm.value.specific_ds && !value.length) {
|
||||
// callback(new Error(t('datasource.Please_select') + t('common.empty') + t('ds.title')))
|
||||
// } else {
|
||||
// callback()
|
||||
// }
|
||||
// }
|
||||
|
||||
const rules = {
|
||||
word: [
|
||||
@@ -274,12 +282,12 @@ const rules = {
|
||||
t('datasource.please_enter') + t('common.empty') + t('professional.term_description'),
|
||||
},
|
||||
],
|
||||
datasource_ids: [
|
||||
{
|
||||
validator: validatePass,
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
// datasource_ids: [
|
||||
// {
|
||||
// validator: validatePass,
|
||||
// trigger: 'blur',
|
||||
// },
|
||||
// ],
|
||||
}
|
||||
|
||||
const handleChange = () => {
|
||||
@@ -516,6 +524,11 @@ const changeStatus = (id: any, val: any) => {
|
||||
<div v-else>{{ t('training.all_data_sources') }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="advanced_application_name"
|
||||
:label="$t('embedded.advanced_application')"
|
||||
min-width="180"
|
||||
/>
|
||||
<el-table-column :label="t('ds.status')" width="180">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center" @click.stop>
|
||||
@@ -650,7 +663,6 @@ const changeStatus = (id: any, val: any) => {
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
class="is-required"
|
||||
:class="!pageForm.specific_ds && 'no-error'"
|
||||
prop="datasource_ids"
|
||||
:label="t('training.effective_data_sources')"
|
||||
@@ -671,7 +683,26 @@ const changeStatus = (id: any, val: any) => {
|
||||
<el-option v-for="item in allDsList" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="advanced_application" :label="t('embedded.advanced_application')">
|
||||
<el-select
|
||||
v-model="pageForm.advanced_application"
|
||||
filterable
|
||||
clearable
|
||||
:placeholder="
|
||||
$t('datasource.Please_select') +
|
||||
$t('common.empty') +
|
||||
$t('embedded.advanced_application')
|
||||
"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in adv_options"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div style="display: flex; align-items: center">
|
||||
@@ -753,6 +784,11 @@ const changeStatus = (id: any, val: any) => {
|
||||
}}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('embedded.advanced_application')">
|
||||
<div class="content">
|
||||
{{ pageForm.advanced_application_name }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('professional.term_description')">
|
||||
<div class="content">
|
||||
{{ pageForm.description }}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { convertFilterText, FilterText } from '@/components/filter-text'
|
||||
import { DrawerMain } from '@/components/drawer-main'
|
||||
import iconFilter from '@/assets/svg/icon-filter_outlined.svg'
|
||||
import Uploader from '@/views/system/excel-upload/Uploader.vue'
|
||||
import { getAdvancedApplicationList } from '@/api/embedded.ts'
|
||||
|
||||
interface Form {
|
||||
id?: string | null
|
||||
@@ -25,6 +26,8 @@ interface Form {
|
||||
specific_ds: boolean
|
||||
datasource_ids: number[]
|
||||
datasource_names: string[]
|
||||
advanced_application: string | null
|
||||
advanced_application_name: string | null
|
||||
name: string | null
|
||||
}
|
||||
const drawerMainRef = ref()
|
||||
@@ -35,7 +38,7 @@ const keywords = ref('')
|
||||
const oldKeywords = ref('')
|
||||
const searchLoading = ref(false)
|
||||
const currentType = ref('GENERATE_SQL')
|
||||
|
||||
const adv_options = ref<any[]>([])
|
||||
const options = ref<any[]>([])
|
||||
const selectable = () => {
|
||||
return true
|
||||
@@ -50,6 +53,9 @@ onMounted(() => {
|
||||
datasourceApi.list().then((res) => {
|
||||
filterOption.value[0].option = [...res]
|
||||
})
|
||||
getAdvancedApplicationList().then((res: any) => {
|
||||
adv_options.value = res || []
|
||||
})
|
||||
search()
|
||||
})
|
||||
|
||||
@@ -74,6 +80,8 @@ const defaultForm = {
|
||||
datasource_names: [],
|
||||
name: null,
|
||||
specific_ds: false,
|
||||
advanced_application: null,
|
||||
advanced_application_name: null,
|
||||
}
|
||||
const pageForm = ref<Form>(cloneDeep(defaultForm))
|
||||
const copyCode = () => {
|
||||
@@ -269,13 +277,13 @@ const search = ($event: any = {}) => {
|
||||
}
|
||||
|
||||
const termFormRef = ref()
|
||||
const validatePass = (_: any, value: any, callback: any) => {
|
||||
if (pageForm.value.specific_ds && !value.length) {
|
||||
callback(new Error(t('datasource.Please_select') + t('common.empty') + t('ds.title')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
// const validatePass = (_: any, value: any, callback: any) => {
|
||||
// if (pageForm.value.specific_ds && !value.length) {
|
||||
// callback(new Error(t('datasource.Please_select') + t('common.empty') + t('ds.title')))
|
||||
// } else {
|
||||
// callback()
|
||||
// }
|
||||
// }
|
||||
const rules = {
|
||||
name: [
|
||||
{
|
||||
@@ -283,12 +291,12 @@ const rules = {
|
||||
message: t('datasource.please_enter') + t('common.empty') + t('prompt.prompt_word_name'),
|
||||
},
|
||||
],
|
||||
datasource_ids: [
|
||||
{
|
||||
validator: validatePass,
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
// datasource_ids: [
|
||||
// {
|
||||
// validator: validatePass,
|
||||
// trigger: 'blur',
|
||||
// },
|
||||
// ],
|
||||
prompt: [
|
||||
{
|
||||
required: true,
|
||||
@@ -541,6 +549,11 @@ const drawerMainClose = () => {
|
||||
<div v-else>{{ t('training.all_data_sources') }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="advanced_application_name"
|
||||
:label="$t('embedded.advanced_application')"
|
||||
min-width="180"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="create_time"
|
||||
sortable
|
||||
@@ -678,7 +691,6 @@ const drawerMainClose = () => {
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
class="is-required"
|
||||
:class="!pageForm.specific_ds && 'no-error'"
|
||||
prop="datasource_ids"
|
||||
:label="t('training.effective_data_sources')"
|
||||
@@ -699,6 +711,26 @@ const drawerMainClose = () => {
|
||||
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="advanced_application" :label="t('embedded.advanced_application')">
|
||||
<el-select
|
||||
v-model="pageForm.advanced_application"
|
||||
filterable
|
||||
clearable
|
||||
:placeholder="
|
||||
$t('datasource.Please_select') +
|
||||
$t('common.empty') +
|
||||
$t('embedded.advanced_application')
|
||||
"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in adv_options"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div v-loading="updateLoading" class="dialog-footer">
|
||||
@@ -744,6 +776,11 @@ const drawerMainClose = () => {
|
||||
}}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('embedded.advanced_application')">
|
||||
<div class="content">
|
||||
{{ pageForm.advanced_application_name }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
<drawer-main
|
||||
|
||||
Reference in New Issue
Block a user