Skip to content

Commit d2558ce

Browse files
committed
feat: configure embedding retrieval strategies at runtime
1 parent 384dfe5 commit d2558ce

45 files changed

Lines changed: 4869 additions & 548 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎AGENTS.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,20 @@ SQLBot 让业务用户用自然语言提问,基于已配置的数据源生成
6262
- 按验收条件选择验证层级,不把构建或单元测试等同于产品验收。普通任务不默认构建镜像或启动完整运行栈;已要求真实接口、浏览器或部署验证时,可启动必要的隔离服务。环境范围、外部成本或数据用途不明确时先确认。
6363
- 除非用户明确要求,不要上传、发布或推送镜像 / wheel / 包。
6464
- 变更涉及本文件或 `docs/agents/` 描述的约定(目录职责、命令、流程)时,同步更新对应文档。
65+
66+
## Agent skills
67+
68+
### Issue tracker
69+
70+
处理 Issue 或发布规格、任务时,使用 dataease/SQLBot 的 GitHub Issues;
71+
操作约定见 `docs/agents/issue-tracker.md`。
72+
73+
### Triage labels
74+
75+
执行 triage 时,使用五个默认角色标签;
76+
映射见 `docs/agents/triage-labels.md`。
77+
78+
### Domain docs
79+
80+
工程技能采用 single-context 布局;探索相关领域前,
81+
按 `docs/agents/domain.md` 读取词汇表和架构决策。

‎CONTEXT.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,18 @@ _Avoid_: Terminology、SQL example
5454
术语、SQL 示例和自定义提示词生效的边界:工作空间、数据源或高级应用。高级应用是独立知识域,不继承工作空间级知识。
5555
_Avoid_: 权限——作用域决定向模型注入哪些知识,权限决定用户能访问哪些数据
5656

57+
**Knowledge Retrieval / 知识召回**:
58+
根据问题匹配相关术语和 SQL 示例,为问数提供业务解释与查询示例。字面匹配与向量匹配是补充关系。
59+
_Avoid_: 自定义提示词注入、全系统向量化
60+
61+
**Table Selection / 表筛选**:
62+
从当前可访问的候选表中选择与问题相关的表,为生成 SQL 提供表结构上下文。
63+
_Avoid_: 表权限——筛选不授予访问权限
64+
65+
**Datasource Preselection / 数据源预筛选**:
66+
在自动选择数据源之前,缩小与问题相关的数据源候选范围。
67+
_Avoid_: 数据源选择结果——预筛选结果仍是候选集合
68+
5769
**Row Permission / 行权限**(xpack):
5870
对表追加的行级过滤条件;同一表命中多条时取 AND,经改写 SQL 执行。
5971
_Avoid_: Column Permission

‎backend/apps/chat/task/llm.py‎

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from apps.terminology.curd.terminology import expand_with_terminology
5353
from apps.terminology.curd.terminology import get_terminology_template
5454
from common.core.config import settings
55+
from common.core.embedding_config import load_embedding_config
5556
from common.core.db import engine
5657
from common.core.deps import CurrentAssistant, CurrentUser
5758
from common.error import SingleMessageError, SQLBotDBError, ParseSQLResultError, SQLBotDBConnectionError
@@ -125,6 +126,7 @@ class LLMService:
125126
def __init__(self, session: Session, current_user: CurrentUser, chat_question: ChatQuestion,
126127
current_assistant: Optional[CurrentAssistant] = None, no_reasoning: bool = False,
127128
embedding: bool = False, config: LLMConfig = None):
129+
self.embedding_config = load_embedding_config(session)
128130
self.sql_message = []
129131
self.chart_message = []
130132
self.generate_sql_logs = []
@@ -155,6 +157,9 @@ def __init__(self, session: Session, current_user: CurrentUser, chat_question: C
155157
raise SingleMessageError("Current assistant user cannot not access this chat")
156158

157159
self.current_user.oid = chat.oid
160+
SQLBotLogUtil.info(
161+
f"[embedding] stage=config chat_id={chat_id} config={self.embedding_config}"
162+
)
158163
ds: CoreDatasource | AssistantOutDsSchema | None = None
159164
if not chat.datasource and chat_question.datasource_id:
160165
_ds = session.get(CoreDatasource, chat_question.datasource_id)
@@ -386,13 +391,19 @@ def filter_terminology_template(self, _session: Session, oid: int = None, ds_id:
386391
self.chat_question.terminologies, term_list = get_terminology_template(_session,
387392
match_text,
388393
calculate_oid,
389-
None, self.current_assistant.id)
394+
None, self.current_assistant.id, config=self.embedding_config)
390395
else:
391396
self.chat_question.terminologies, term_list = get_terminology_template(_session,
392397
match_text,
393398
calculate_oid,
394-
calculate_ds_id)
399+
calculate_ds_id, config=self.embedding_config)
395400

401+
SQLBotLogUtil.info(
402+
f"[embedding] stage=terminology chat_id={self.chat_question.chat_id} "
403+
f"record_id={self.record.id} "
404+
f"retrieval_mode={'literal_and_vector' if self.embedding_config.enabled else 'literal_only'} "
405+
f"result_count={len(term_list)}"
406+
)
396407
self.current_logs[OperationEnum.FILTER_TERMS] = end_log(session=_session,
397408
log=self.current_logs[OperationEnum.FILTER_TERMS],
398409
full_message=term_list)
@@ -440,12 +451,18 @@ def filter_training_template(self, _session: Session, oid: int = None, ds_id: in
440451
self.chat_question.data_training, example_list = get_training_template(_session,
441452
self.chat_question.question,
442453
calculate_oid,
443-
None, self.current_assistant.id)
454+
None, self.current_assistant.id, config=self.embedding_config)
444455
else:
445456
self.chat_question.data_training, example_list = get_training_template(_session,
446457
self.chat_question.question,
447458
calculate_oid,
448-
calculate_ds_id)
459+
calculate_ds_id, config=self.embedding_config)
460+
SQLBotLogUtil.info(
461+
f"[embedding] stage=sql_example chat_id={self.chat_question.chat_id} "
462+
f"record_id={self.record.id} "
463+
f"retrieval_mode={'literal_and_vector' if self.embedding_config.enabled else 'literal_only'} "
464+
f"result_count={len(example_list)}"
465+
)
449466
self.current_logs[OperationEnum.FILTER_SQL_EXAMPLE] = end_log(session=_session,
450467
log=self.current_logs[
451468
OperationEnum.FILTER_SQL_EXAMPLE],
@@ -552,7 +569,15 @@ def choose_table_schema(self, _session: Session):
552569
current_user=self.current_user,
553570
ds=self.ds,
554571
question=self.chat_question.question,
555-
keywords=keywords)
572+
keywords=keywords, config=self.embedding_config)
573+
574+
SQLBotLogUtil.info(
575+
f"[embedding] stage=table_context chat_id={self.chat_question.chat_id} "
576+
f"record_id={self.record.id} ds_id={self.ds.id} "
577+
f"source={'dynamic_datasource' if self.out_ds_instance else 'configured_datasource'} "
578+
f"vector_enabled={self.embedding_config.table_enabled and not self.out_ds_instance} "
579+
f"result_count={len(tables)}"
580+
)
556581

557582
# Get sample data for all tables
558583
if not self.out_ds_instance and settings.SAMPLE_DATA_ENABLED:
@@ -767,16 +792,28 @@ def select_datasource(self, _session: Session):
767792
if not _ds_list:
768793
raise SingleMessageError('No available datasource configuration found')
769794
ignore_auto_select = _ds_list and len(_ds_list) == 1
795+
if ignore_auto_select:
796+
SQLBotLogUtil.info(
797+
f"[embedding] stage=datasource chat_id={self.chat_question.chat_id} "
798+
f"record_id={self.record.id} action=skip reason=single_candidate candidate_count=1"
799+
)
770800
# ignore auto select ds
771801

772802
full_thinking_text = ''
773803
full_text = ''
774804
if not ignore_auto_select:
775-
if settings.TABLE_EMBEDDING_ENABLED and (
776-
not self.current_assistant or (self.current_assistant and self.current_assistant.type != 1)):
805+
if self.embedding_config.table_enabled and (
806+
not self.current_assistant or (self.current_assistant and self.current_assistant.type not in dynamic_ds_types)):
777807
_ds_list = get_ds_embedding(_session, _ds_list, self.out_ds_instance,
778-
self.chat_question.question, self.current_assistant)
808+
self.chat_question.question, self.current_assistant, config=self.embedding_config)
779809
# yield {'content': '{"id":' + str(ds.get('id')) + '}'}
810+
else:
811+
reason = 'disabled' if not self.embedding_config.table_enabled else 'dynamic_datasource'
812+
SQLBotLogUtil.info(
813+
f"[embedding] stage=datasource chat_id={self.chat_question.chat_id} "
814+
f"record_id={self.record.id} action=skip reason={reason} "
815+
f"candidate_count={len(_ds_list)} result_count={len(_ds_list)}"
816+
)
780817

781818
_ds_list_dict = []
782819
for _ds in _ds_list:
@@ -873,7 +910,7 @@ def select_datasource(self, _session: Session):
873910
except Exception as e:
874911
_error = e
875912

876-
if not ignore_auto_select and not settings.TABLE_EMBEDDING_ENABLED:
913+
if not ignore_auto_select and not self.embedding_config.table_enabled:
877914
self.record = save_select_datasource_answer(session=_session, record_id=self.record.id,
878915
answer=orjson.dumps({'content': full_text}).decode(),
879916
datasource=_datasource,
@@ -883,7 +920,7 @@ def select_datasource(self, _session: Session):
883920
ds_id = self.ds.id if isinstance(self.ds, CoreDatasource) else None
884921

885922
# 提取关键词(前置步骤,结果复用于术语匹配和表匹配)
886-
if settings.TABLE_EMBEDDING_KEYWORD_ENABLED:
923+
if self.embedding_config.keyword_enabled:
887924
is_error, kw_result = self.extract_keywords(_session)
888925
if is_error:
889926
raise SingleMessageError(kw_result)
@@ -1365,7 +1402,7 @@ def run_task(self, in_chat: bool = True, stream: bool = True,
13651402
ds_id = self.ds.id if isinstance(self.ds, CoreDatasource) else None
13661403

13671404
# 提取关键词(前置步骤,结果复用于术语匹配和表匹配)
1368-
if settings.TABLE_EMBEDDING_KEYWORD_ENABLED:
1405+
if self.embedding_config.keyword_enabled:
13691406
is_error, kw_result = self.extract_keywords(_session)
13701407
if is_error:
13711408
raise SingleMessageError(kw_result)

‎backend/apps/data_training/curd/data_training.py‎

Lines changed: 27 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@
33
from typing import List, Optional
44
from xml.dom.minidom import parseString
55

6-
from sqlalchemy import and_, select, func, delete, update, or_
6+
from sqlalchemy import case, and_, select, func, delete, update, or_
77
from sqlalchemy import text
88

99
from apps.ai_model.embedding import EmbeddingModelCache
10+
from apps.datasource.embedding.utils import parse_valid_embedding
1011
from apps.data_training.models.data_training_model import DataTrainingInfo, DataTraining, DataTrainingInfoResult
1112
from apps.datasource.models.datasource import CoreDatasource
1213
from apps.system.models.system_model import AssistantModel
1314
from apps.template.generate_chart.generator import get_base_data_training_template
14-
from common.core.config import settings
15+
from common.core.embedding_config import EmbeddingConfig, load_embedding_config
1516
from common.core.deps import SessionDep, Trans
1617
from common.utils.dict_to_xml import dict_to_xml
18+
from common.utils.embedding_lifecycle import generate_missing_embeddings
1719
from common.utils.embedding_threads import run_save_data_training_embeddings
1820

1921

@@ -266,6 +268,7 @@ def update_training(session: SessionDep, info: DataTrainingInfo, oid: int, trans
266268
raise Exception(trans("i18n_data_training.exists_in_db"))
267269

268270
stmt = update(DataTraining).where(and_(DataTraining.id == info.id)).values(
271+
embedding=case((DataTraining.question.is_distinct_from(info.question.strip()), None), else_=DataTraining.embedding),
269272
question=info.question.strip(),
270273
description=info.description.strip(),
271274
datasource=info.datasource,
@@ -442,84 +445,46 @@ def enable_training(session: SessionDep, id: int, enabled: bool, trans: Trans, o
442445
session.commit()
443446

444447

445-
# def run_save_embeddings(ids: List[int]):
446-
# executor.submit(save_embeddings, ids)
447-
#
448-
#
449-
# def fill_empty_embeddings():
450-
# executor.submit(run_fill_empty_embeddings)
451-
452-
453448
def run_fill_empty_embeddings(session_maker):
454-
try:
455-
if not settings.EMBEDDING_ENABLED:
456-
return
457-
458-
session = session_maker()
459-
stmt = select(DataTraining.id).where(and_(DataTraining.embedding.is_(None)))
460-
results = session.execute(stmt).scalars().all()
461-
462-
save_embeddings(session_maker, results)
463-
except Exception:
464-
traceback.print_exc()
465-
finally:
466-
session_maker.remove()
449+
return generate_missing_embeddings(session_maker, DataTraining, DataTraining.question)
467450

468451

469452
def save_embeddings(session_maker, ids: List[int]):
470-
if not settings.EMBEDDING_ENABLED:
471-
return
472-
473-
if not ids or len(ids) == 0:
453+
if not ids:
474454
return
475-
try:
476-
session = session_maker()
477-
_list = session.query(DataTraining).filter(and_(DataTraining.id.in_(ids))).all()
478-
479-
_question_list = [item.question for item in _list]
480-
481-
model = EmbeddingModelCache.get_model()
482-
483-
results = model.embed_documents(_question_list)
484-
485-
for index in range(len(results)):
486-
item = results[index]
487-
stmt = update(DataTraining).where(and_(DataTraining.id == _list[index].id)).values(embedding=item)
488-
session.execute(stmt)
489-
session.commit()
490-
491-
except Exception:
492-
traceback.print_exc()
493-
finally:
494-
session_maker.remove()
455+
return generate_missing_embeddings(
456+
session_maker, DataTraining, DataTraining.question,
457+
DataTraining.id.in_(ids),
458+
)
495459

496460

497-
embedding_sql = f"""
461+
embedding_sql = """
498462
SELECT id, datasource, question, similarity
499463
FROM
500464
(SELECT id, datasource, question, oid, enabled,
501465
( 1 - (embedding <=> :embedding_array) ) AS similarity
502466
FROM data_training AS child
503467
) TEMP
504-
WHERE similarity > {settings.EMBEDDING_DATA_TRAINING_SIMILARITY} and oid = :oid and datasource = :datasource and enabled = true
468+
WHERE similarity > :similarity and oid = :oid and datasource = :datasource and enabled = true
505469
ORDER BY similarity DESC
506-
LIMIT {settings.EMBEDDING_DATA_TRAINING_TOP_COUNT}
470+
LIMIT :top_count
507471
"""
508-
embedding_sql_in_advanced_application = f"""
472+
embedding_sql_in_advanced_application = """
509473
SELECT id, advanced_application, question, similarity
510474
FROM
511475
(SELECT id, advanced_application, question, oid, enabled,
512476
( 1 - (embedding <=> :embedding_array) ) AS similarity
513477
FROM data_training AS child
514478
) TEMP
515-
WHERE similarity > {settings.EMBEDDING_DATA_TRAINING_SIMILARITY} and oid = :oid and advanced_application = :advanced_application and enabled = true
479+
WHERE similarity > :similarity and oid = :oid and advanced_application = :advanced_application and enabled = true
516480
ORDER BY similarity DESC
517-
LIMIT {settings.EMBEDDING_DATA_TRAINING_TOP_COUNT}
481+
LIMIT :top_count
518482
"""
519483

520484

521485
def select_training_by_question(session: SessionDep, question: str, oid: int, datasource: Optional[int] = None,
522-
advanced_application_id: Optional[int] = None):
486+
advanced_application_id: Optional[int] = None, *, config: EmbeddingConfig | None = None):
487+
config = config or load_embedding_config(session)
523488
if question.strip() == "":
524489
return []
525490

@@ -547,20 +512,20 @@ def select_training_by_question(session: SessionDep, question: str, oid: int, da
547512
for row in results:
548513
_list.append(DataTraining(id=row.id, question=row.question))
549514

550-
if settings.EMBEDDING_ENABLED:
515+
if config.enabled:
551516
with session.begin_nested():
552517
try:
553518
model = EmbeddingModelCache.get_model()
554519

555-
embedding = model.embed_query(question)
520+
embedding = parse_valid_embedding(model.embed_query(question))
556521

557522
if advanced_application_id is not None:
558523
results = session.execute(text(embedding_sql_in_advanced_application),
559-
{'embedding_array': str(embedding), 'oid': oid,
524+
{'embedding_array': str(embedding), 'oid': oid, 'similarity': config.data_training_similarity, 'top_count': config.data_training_top_count,
560525
'advanced_application': advanced_application_id})
561526
else:
562527
results = session.execute(text(embedding_sql),
563-
{'embedding_array': str(embedding), 'oid': oid, 'datasource': datasource})
528+
{'embedding_array': str(embedding), 'oid': oid, 'similarity': config.data_training_similarity, 'top_count': config.data_training_top_count, 'datasource': datasource})
564529

565530
for row in results:
566531
_list.append(DataTraining(id=row.id, question=row.question))
@@ -617,12 +582,14 @@ def to_xml_string(_dict: list[dict] | dict, root: str = 'sql-examples') -> str:
617582

618583

619584
def get_training_template(session: SessionDep, question: str, oid: Optional[int] = 1, datasource: Optional[int] = None,
620-
advanced_application_id: Optional[int] = None) -> tuple[str, list[dict]]:
585+
advanced_application_id: Optional[int] = None, *,
586+
config: EmbeddingConfig | None = None) -> tuple[str, list[dict]]:
587+
config = config or load_embedding_config(session)
621588
if not oid:
622589
oid = 1
623590
if not datasource and not advanced_application_id:
624591
return '', []
625-
_results = select_training_by_question(session, question, oid, datasource, advanced_application_id)
592+
_results = select_training_by_question(session, question, oid, datasource, advanced_application_id, config=config)
626593
if _results and len(_results) > 0:
627594
data_training = to_xml_string(_results)
628595
template = get_base_data_training_template().format(data_training=data_training)

0 commit comments

Comments
 (0)