add .gitattributes: python文件统一LF换行

This commit is contained in:
2026-09-14 13:00:15 +08:00
parent 67d5cfc2b8
commit bc7761d79e
6 changed files with 64 additions and 8 deletions
+1
View File
@@ -0,0 +1 @@
*.py text eol=lf
+2
View File
@@ -76,6 +76,7 @@ async def execute_advisor_data_query(
question: str,
trace_id: str,
session_id: str | None = None,
conversation_context: str = "",
data_scope: dict[str, Any] | None = None,
max_rows: int | None = None,
page: int = 1,
@@ -162,6 +163,7 @@ async def execute_advisor_data_query(
llm_client=llm_client,
summary_llm=llm_client,
masks=permission.get("masks"),
conversation_context=conversation_context,
)
except (EmbeddingError, LLMFailError) as exc:
raise QueryServiceError("投顾 Agent 依赖服务不可用,请检查 LLM/Embedding 服务连接") from exc
+9 -1
View File
@@ -49,6 +49,7 @@ _CLASSIFIER_PROMPT = """你是基金投顾工作台的意图分类器,只负
"""
_RECOMMENDATION_TERMS = ("推荐", "组合建议", "配置建议", "买什么", "适合配置", "筛选基金", "投资方案")
_SCOPE_ERROR_MESSAGE = "投顾范围查询仅支持客户数据查询"
def _rule_fallback(query: str | None) -> IntentClassification:
@@ -94,6 +95,13 @@ async def classify_advisor_intent(
return IntentClassification(explicit_intent, 1.0, "explicit", "客户端显式指定")
if not query or not query.strip():
return IntentClassification("", 0.0, "fallback", "空输入")
if re.sub(r"\s+", "", query) == _SCOPE_ERROR_MESSAGE:
return IntentClassification(
AGENT_INTENT_CASUAL_CHAT,
1.0,
"rule",
"识别为系统提示文本而非业务查询",
)
if llm_client is not None:
try:
raw = await asyncio.wait_for(
@@ -114,7 +122,7 @@ async def classify_advisor_intent(
rule_intent = recognize_advisor_intent(query)
if (
rule_intent == AGENT_INTENT_DATA_QUERY
and parsed.intent == AGENT_INTENT_RECOMMEND
and parsed.intent != AGENT_INTENT_DATA_QUERY
and not any(term in (query or "") for term in _RECOMMENDATION_TERMS)
):
return IntentClassification(
+11
View File
@@ -40,6 +40,8 @@ _REBALANCE_TERMS = ("调仓", "再平衡", "组合调整", "配置偏离", "偏
_FUND_ANALYSIS_TERMS = ("基金分析", "分析基金", "基金表现", "净值走势", "最大回撤", "夏普比率", "年化波动")
_DIALOGUE_TERMS = ("话术", "沟通", "怎么跟客户说", "如何向客户解释", "安抚客户", "投诉处理")
_RECOMMEND_TERMS = ("推荐", "组合建议", "配置建议", "买什么", "适合配置", "筛选基金", "投资方案")
_CUSTOMER_IDENTITY_TERMS = ("是谁", "姓名", "实名", "基本信息", "联系方式", "手机号")
_SCOPE_ERROR_MESSAGE = "投顾范围查询仅支持客户数据查询"
def _contains_any(text: str, terms: tuple[str, ...]) -> bool:
@@ -57,6 +59,8 @@ def recognize_advisor_intent(query: str | None, explicit_intent: str | None = No
text = re.sub(r"\s+", "", query or "")
if not text:
return None
if text == _SCOPE_ERROR_MESSAGE:
return None
# 先处理最明确的任务词,避免“查询基金收益”被误判成普通数据查询。
if _contains_any(text, _REBALANCE_TERMS):
@@ -68,6 +72,13 @@ def recognize_advisor_intent(query: str | None, explicit_intent: str | None = No
has_action = any(term in text for term in _QUERY_ACTIONS)
has_data = any(term in text for term in _DATA_TERMS)
has_customer_identity = (
"客户" in text
and _contains_any(text, _CUSTOMER_IDENTITY_TERMS)
and not _contains_any(text, _RECOMMEND_TERMS)
)
if has_customer_identity:
return AGENT_INTENT_DATA_QUERY
if has_data and (has_action or "客户" in text or "近一年" in text or "本月" in text):
return AGENT_INTENT_DATA_QUERY
if _contains_any(text, _RECOMMEND_TERMS):
+3 -2
View File
@@ -29,7 +29,7 @@ def apply_row_scope(sql: str, permission: dict, data_scope: dict | None) -> str:
statement = parse_one(sql, read="mysql")
except ParseError as exc:
raise RowScopeError("SQL 解析失败") from exc
for table in statement.find_all(exp.Table):
for table in list(statement.find_all(exp.Table)):
scope = scopes.get(table.name)
if not scope:
continue
@@ -38,5 +38,6 @@ def apply_row_scope(sql: str, permission: dict, data_scope: dict | None) -> str:
this=exp.column(scope["column"], table=table.alias_or_name),
expressions=[exp.Literal.number(value) for value in values],
)
statement = statement.where(condition)
owner = table.find_ancestor(exp.Select) or statement
owner.where(condition, copy=False)
return statement.sql(dialect="mysql")
+38 -5
View File
@@ -10,6 +10,7 @@ from nl2sql.executor import QueryExecutionError, execute_readonly_sql
from nl2sql.row_scope import RowScopeError, apply_row_scope
from nl2sql.result import build_chart_config, summarize_result
from nl2sql.query_experience import apply_query_options, build_query_explanation
from nl2sql.query_rewriter import rewrite_query
from nl2sql.supervisor import UnsupportedIntent, ensure_query_intent
from nl2sql.sql_agent import generate_sql
from nl2sql.sql_security import SqlSecurityError, validate_select_sql
@@ -19,6 +20,29 @@ class QueryServiceError(RuntimeError):
"""查询编排失败或当前用户没有查询权限。"""
def _filter_schema_columns(schema: dict[str, Any], permission: dict[str, Any]) -> dict[str, Any]:
"""只把当前用户有列权限的字段暴露给 SQL 生成模型。"""
authorized_columns = permission.get("columns")
if authorized_columns is None:
return schema
filtered = dict(schema)
filtered["columns"] = [
column
for column in schema.get("columns", [])
if column.get("field_name")
in set(authorized_columns.get(column.get("table_name"), set()))
]
return filtered
def _normalize_advisor_placeholder(sql: str) -> str:
"""统一 LLM 可能生成的投顾参数占位符,供 SQLAlchemy 命名绑定。"""
if "advisor_id" not in sql or "?" not in sql:
return sql
return sql.replace("?", ":advisor_id", 1)
async def query(
request: DataQueryRequest,
*,
@@ -30,8 +54,13 @@ async def query(
conversation_context: str = "",
):
"""执行权限、召回、权威 Schema、生成和安全校验,返回可执行 SQL。"""
effective_question = await rewrite_query(
request.question,
conversation_context,
llm_client=llm_client,
)
try:
ensure_query_intent(request.question)
ensure_query_intent(effective_question)
except UnsupportedIntent as exc:
raise QueryServiceError(str(exc)) from exc
permission = await permission_loader(request.user_id)
@@ -40,7 +69,7 @@ async def query(
if metadata_retriever is None or schema_loader is None:
raise QueryServiceError("NL2SQL 查询依赖未配置")
hits = await metadata_retriever(request.question)
hits = await metadata_retriever(effective_question)
candidate_tables = {
hit.get("table_name")
for hit in hits
@@ -48,23 +77,27 @@ async def query(
}
if not candidate_tables:
raise QueryServiceError("未找到有权限的业务表")
schema = await schema_loader(candidate_tables, permission)
schema = _filter_schema_columns(
await schema_loader(candidate_tables, permission),
permission,
)
if not schema.get("tables"):
raise QueryServiceError("候选表未通过权威 Schema 校验")
few_shot = []
if few_shot_retriever is not None:
try:
few_shot = await few_shot_retriever(request.question)
few_shot = await few_shot_retriever(effective_question)
except Exception: # noqa: BLE001 Few-shot 故障不阻断主查询
few_shot = []
generated = await generate_sql(
request.question,
effective_question,
schema,
llm_client=llm_client,
few_shot=few_shot,
conversation_context=conversation_context,
)
generated = replace(generated, sql=_normalize_advisor_placeholder(generated.sql))
max_rows = request.max_rows or permission.get("max_rows") or 1000
try:
validated = validate_select_sql(