feat:LLM 分析文件并生成结构化建议

This commit is contained in:
2026-10-10 04:56:28 -07:00
parent 7caf0d31da
commit b90842237f
11 changed files with 746 additions and 7 deletions
+121 -1
View File
@@ -7,7 +7,7 @@
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, JsonValue
from pydantic import BaseModel, ConfigDict, Field, JsonValue
DataFormat = Literal["csv", "tsv", "xlsx", "xls", "xlsb", "json", "jsonl", "parquet"]
@@ -66,6 +66,107 @@ class DataTable(DataTableDetail):
parquet: str
Aggregation = Literal["sum", "avg", "min", "max", "count", "count_distinct"]
# 日期列按粒度分组,结果是字符串:年 2025,季度 2025-Q1,月 2025-01,日 2025-01-31
Grain = Literal["year", "quarter", "month", "day"]
MAX_GROUP_BY = 2
MAX_METRICS = 5
MAX_AGGREGATE_ROWS = 1000
class GroupBy(BaseModel):
model_config = ConfigDict(extra="forbid")
column: str
# 只有日期、日期时间列可以指定
grain: Grain | None = None
class Metric(BaseModel):
model_config = ConfigDict(extra="forbid")
agg: Aggregation
# count 可以不写,数整张表的行数;其他聚合必填。sum、avg 只能用于数值列
column: str | None = None
# 结果列名,默认沿用列名(计数类加后缀),按需指定以避免重名
alias: str | None = Field(default=None, min_length=1, max_length=100)
class OrderBy(BaseModel):
model_config = ConfigDict(extra="forbid")
# 必须是结果里的列名(分组列或指标的别名)
by: str
direction: Literal["asc", "desc"] = "asc"
class AggregateRequest(BaseModel):
"""对一张表分组汇总:group_by 为空时整表汇总成一行"""
model_config = ConfigDict(extra="forbid")
group_by: list[GroupBy] = Field(default_factory=list, max_length=MAX_GROUP_BY)
metrics: list[Metric] = Field(min_length=1, max_length=MAX_METRICS)
# 不写时有分组按第一个分组列升序
order_by: OrderBy | None = None
limit: int = Field(default=100, ge=1, le=MAX_AGGREGATE_ROWS)
class AggregateResult(BaseModel):
# 结果列名,按分组列、指标的顺序
columns: list[str]
# 对象数组,键是列名,可直接作为图表物料的 dataset.source
rows: list[dict[str, JsonValue]]
# 结果超过 limit 行,只返回了前 limit 行
truncated: bool
# 前端图表物料支持绑定汇总数据的系列类型,推荐只会给出这几种
ChartKind = Literal["bar", "line", "pie", "funnel", "pictorialBar"]
ColumnRole = Literal["dimension", "measure", "time", "id", "other"]
MAX_SUGGESTIONS = 3
class ChartSuggestion(BaseModel):
"""一条图表推荐:一个可以直接回答的问题,加上能直接跑的汇总请求"""
# 给用户看的问题,如“各区域销售额对比”,也作为图表标题
title: str
# 针对哪张表,是文件里的表名(t1、t2……)
table: str
chart: ChartKind
request: AggregateRequest
class ColumnInsight(BaseModel):
table: str
# 原始列名,与 columns[].name 对应
name: str
# 中文含义,列名已经够清楚时与列名相同
label: str
role: ColumnRole
# 单位,如“元”“%”“件”;没有或不确定时为空
unit: str | None = None
class FileInsights(BaseModel):
"""上传后由 LLM 一次分析出的文件洞察:一句话说明、图表推荐、列的含义
后续要加的分析结果都往这里加带默认值的字段,version 随字段含义变化递增
"""
version: int = 1
# pending:正在分析;ready:已完成(推荐可能为空);failed:分析失败,文件本身可用
status: Literal["pending", "ready", "failed"]
summary: str = ""
suggestions: list[ChartSuggestion] = Field(default_factory=list)
columns: list[ColumnInsight] = Field(default_factory=list)
class DataFileInfo(BaseModel):
file_id: str
conversation_id: str
@@ -78,6 +179,8 @@ class DataFileInfo(BaseModel):
expires_at: datetime
tables: list[DataTableInfo]
warnings: list[ParseWarning]
# 未启用智能分析,或这是功能上线前上传的文件时为空
insights: FileInsights | None = None
class DataFileDetail(DataFileInfo):
@@ -96,12 +199,29 @@ class DataFileList(BaseModel):
files: list[DataFileInfo]
class DataTableRows(BaseModel):
"""一张表的一页数据,用于预览时翻页看全表"""
# 表的总行数(与文件记录里的 rows 一致),前端据此决定滚动长度
total: int
offset: int
# 这一页的行,顺序与 columns 一致;到末尾时可能少于请求的 limit
rows: list[list[JsonValue]]
class DataFileLimits(BaseModel):
max_size_mb: int
max_files_per_conversation: int
formats: list[DataFormat]
# 文件选择框的 accept,只用于前端过滤;后端按文件内容识别格式
accept: str
# 同时解析的文件数,其余排队;前端据此决定并发上传几个
parse_concurrency: int
# 翻页取表数据时单页行数上限
rows_max_page: int
# 分组汇总的指标个数上限、返回行数上限
aggregate_max_metrics: int
aggregate_max_rows: int
class RequestDataTable(DataTableInfo):