feat:LLM 分析文件并生成结构化建议
This commit is contained in:
+29
-30
@@ -1,4 +1,4 @@
|
||||
"""列概况和预览:上传时在 Parquet 上用 DuckDB 计算,存进文件记录,供 describe_data 和接口返回"""
|
||||
"""列概况和预览:上传时在 Parquet 上用 DuckDB 计算,存进文件记录,供 describe_data、上传后的分析和接口返回"""
|
||||
|
||||
import math
|
||||
from pathlib import Path
|
||||
@@ -6,8 +6,9 @@ from typing import Any
|
||||
|
||||
import duckdb
|
||||
|
||||
from app.datafiles.columns import EXACT_FLOAT_INT
|
||||
from app.datafiles.query import connect, json_value, quote_identifier
|
||||
from app.schemas.data_file import DataColumn
|
||||
from app.schemas.data_file import ColumnProfile, DataColumn, TableProfile, TopValue
|
||||
|
||||
PREVIEW_ROWS = 20
|
||||
SAMPLE_ROWS = 5
|
||||
@@ -23,11 +24,10 @@ PREVIEW_CELL_CHARS = 200
|
||||
|
||||
def profile_table(
|
||||
path: Path, columns: list[DataColumn], notes: dict[str, str]
|
||||
) -> tuple[dict[str, Any], list[list[Any]]]:
|
||||
) -> tuple[TableProfile, list[list[Any]]]:
|
||||
"""返回列概况和接口用的预览(前 PREVIEW_ROWS 行)"""
|
||||
connection = connect({"t": path})
|
||||
try:
|
||||
rows = connection.execute("SELECT count(*) FROM t").fetchone()[0]
|
||||
connection.execute("SUMMARIZE t")
|
||||
fields = [column[0] for column in connection.description]
|
||||
summary = {row[0]: dict(zip(fields, row)) for row in connection.fetchall()}
|
||||
@@ -48,13 +48,12 @@ def profile_table(
|
||||
).fetchall()
|
||||
finally:
|
||||
connection.close()
|
||||
profile = {
|
||||
"rows": rows,
|
||||
"columns": profiled,
|
||||
"sample": [
|
||||
profile = TableProfile(
|
||||
columns=profiled,
|
||||
sample=[
|
||||
[json_value(value, PROFILE_CELL_CHARS) for value in row] for row in sample
|
||||
],
|
||||
}
|
||||
)
|
||||
preview = [
|
||||
[json_value(value, PREVIEW_CELL_CHARS) for value in row]
|
||||
for row in head[:PREVIEW_ROWS]
|
||||
@@ -92,25 +91,19 @@ def _profile_column(
|
||||
head: list[tuple],
|
||||
position: int,
|
||||
note: str | None,
|
||||
) -> dict[str, Any]:
|
||||
item: dict[str, Any] = {"name": column.name, "type": column.type}
|
||||
if column.format:
|
||||
item["format"] = column.format
|
||||
null_ratio = float(stats["null_percentage"]) / 100
|
||||
item["null_ratio"] = round(null_ratio, 4)
|
||||
# approx_unique 是估算值,小表上可能比行数还大
|
||||
item["distinct"] = min(
|
||||
stats["approx_unique"], round(stats["count"] * (1 - null_ratio))
|
||||
)
|
||||
) -> ColumnProfile:
|
||||
item: dict[str, Any] = {}
|
||||
if column.type == "number":
|
||||
for key in ("min", "max", "avg", "q25", "q50", "q75"):
|
||||
item[key] = _number(stats[key])
|
||||
item = {
|
||||
key: _number(stats[key])
|
||||
for key in ("min", "max", "avg", "q25", "q50", "q75")
|
||||
}
|
||||
elif column.type in ("date", "datetime"):
|
||||
# SUMMARIZE 的时间用空格分隔,和预览里的 ISO 写法统一
|
||||
item["min"], item["max"] = (
|
||||
None if stats[key] is None else stats[key].replace(" ", "T", 1)
|
||||
item = {
|
||||
key: None if stats[key] is None else stats[key].replace(" ", "T", 1)
|
||||
for key in ("min", "max")
|
||||
)
|
||||
}
|
||||
if (
|
||||
column.type in ("text", "boolean")
|
||||
and stats["approx_unique"] <= TOP_VALUES_MAX_DISTINCT
|
||||
@@ -121,7 +114,7 @@ def _profile_column(
|
||||
f"GROUP BY 1 ORDER BY n DESC, 1 LIMIT {TOP_VALUES}"
|
||||
).fetchall()
|
||||
item["top_values"] = [
|
||||
{"value": json_value(value, PROFILE_CELL_CHARS), "count": count}
|
||||
TopValue(value=json_value(value, PROFILE_CELL_CHARS), count=count)
|
||||
for value, count in counts
|
||||
]
|
||||
examples: list[Any] = []
|
||||
@@ -131,10 +124,16 @@ def _profile_column(
|
||||
examples.append(value)
|
||||
if len(examples) == EXAMPLES:
|
||||
break
|
||||
item["examples"] = examples
|
||||
if note:
|
||||
item["note"] = note
|
||||
return item
|
||||
null_ratio = float(stats["null_percentage"]) / 100
|
||||
return ColumnProfile(
|
||||
**column.model_dump(),
|
||||
null_ratio=round(null_ratio, 4),
|
||||
# approx_unique 是估算值,小表上可能比行数还大
|
||||
distinct=min(stats["approx_unique"], round(stats["count"] * (1 - null_ratio))),
|
||||
examples=examples,
|
||||
note=note,
|
||||
**item,
|
||||
)
|
||||
|
||||
|
||||
def _number(text: str | None) -> int | float | None:
|
||||
@@ -144,6 +143,6 @@ def _number(text: str | None) -> int | float | None:
|
||||
value = float(text)
|
||||
if not math.isfinite(value):
|
||||
return None
|
||||
if value.is_integer() and abs(value) < 2**53:
|
||||
if value.is_integer() and abs(value) < EXACT_FLOAT_INT:
|
||||
return int(value)
|
||||
return round(value, 6)
|
||||
|
||||
Reference in New Issue
Block a user