feat:LLM 分析文件并生成结构化建议
This commit is contained in:
+45
-4
@@ -3,13 +3,17 @@
|
||||
from typing import Annotated, Literal, get_args
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form, Query, Request, UploadFile
|
||||
from fastapi.responses import JSONResponse
|
||||
from starlette.types import ASGIApp, Receive, Scope, Send
|
||||
|
||||
from app.common.response import ApiResponse, success_response
|
||||
from app.common.response import ApiResponse, error_content, success_response
|
||||
from app.core.auth import get_current_user
|
||||
from app.datafiles.base import MAX_TABLE_ROWS
|
||||
from app.datafiles.service import (
|
||||
MAX_FILES_PER_CONVERSATION,
|
||||
PARSE_CONCURRENCY,
|
||||
DataFileService,
|
||||
file_too_large,
|
||||
)
|
||||
from app.schemas.base import NonBlankString
|
||||
from app.schemas.data_file import (
|
||||
@@ -31,18 +35,55 @@ from app.schemas.user import CurrentUser
|
||||
limits_router = APIRouter(prefix="/files", tags=["DataFile"])
|
||||
router = APIRouter(prefix="/files", tags=["DataFile"])
|
||||
|
||||
# 文件选择框的过滤条件;后端按文件内容识别格式,不看扩展名
|
||||
# 翻页取表数据:单页上限,和一张表最多的行数(100 万)
|
||||
# 翻页取表数据:单页上限,offset 不超过一张表最多的行数
|
||||
MAX_PAGE_ROWS = 500
|
||||
MAX_OFFSET = 1_000_000
|
||||
MAX_OFFSET = MAX_TABLE_ROWS
|
||||
|
||||
# 文件选择框的过滤条件;后端按文件内容识别格式,不看扩展名
|
||||
ACCEPT = ".csv,.tsv,.txt,.xlsx,.xls,.xlsb,.json,.jsonl,.ndjson,.parquet"
|
||||
|
||||
# 上传的请求体比文件本身多出 multipart 的分隔行、字段名和会话 ID
|
||||
UPLOAD_OVERHEAD_BYTES = 64 * 1024
|
||||
|
||||
|
||||
def _service(request: Request) -> DataFileService:
|
||||
return request.app.state.data_file_service
|
||||
|
||||
|
||||
class UploadSizeLimit:
|
||||
"""上传接口在读取请求体之前按 Content-Length 拒绝过大的请求
|
||||
|
||||
FastAPI 会先把整个 multipart 请求体收完、写进临时文件,再调用接口函数;
|
||||
只在 DataFileService 里边写边查大小的话,超大的上传已经占满了带宽和磁盘
|
||||
"""
|
||||
|
||||
def __init__(self, app: ASGIApp, path: str):
|
||||
self.app = app
|
||||
self.path = path
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
if (
|
||||
scope["type"] == "http"
|
||||
and scope["method"] == "POST"
|
||||
and scope["path"] == self.path
|
||||
and (rejection := self._reject(scope)) is not None
|
||||
):
|
||||
await rejection(scope, receive, send)
|
||||
return
|
||||
await self.app(scope, receive, send)
|
||||
|
||||
@staticmethod
|
||||
def _reject(scope: Scope) -> JSONResponse | None:
|
||||
max_mb = scope["app"].state.data_file_service.max_mb
|
||||
length = dict(scope["headers"]).get(b"content-length")
|
||||
if length is None or not length.isdigit():
|
||||
return JSONResponse(error_content(411, "缺少 Content-Length"), 411)
|
||||
if int(length) > max_mb * 1024 * 1024 + UPLOAD_OVERHEAD_BYTES:
|
||||
error = file_too_large(max_mb)
|
||||
return JSONResponse(error_content(error.code, error.msg), error.code)
|
||||
return None
|
||||
|
||||
|
||||
def _detail(record: DataFileRecord) -> DataFileDetail:
|
||||
# 按接口模型重新校验,去掉 owner_id、sha256、列概况等只在后端用的字段
|
||||
return DataFileDetail.model_validate(record.model_dump())
|
||||
|
||||
Reference in New Issue
Block a user