570 lines
17 KiB
Python
570 lines
17 KiB
Python
"""
|
|
数据合成模块 API 接口层
|
|
|
|
该模块提供数据合成任务的 RESTful API 接口。
|
|
每个接口负责:参数解析 → 调用服务 → 返回响应
|
|
"""
|
|
from typing import List, Optional
|
|
|
|
from fastapi import APIRouter, Depends, BackgroundTasks
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.exception import BusinessError, ErrorCodes, SuccessResponse
|
|
from app.core.logging import get_logger
|
|
from app.db.session import get_db
|
|
from app.module.generation.schema.generation import (
|
|
CreateSynthesisTaskRequest,
|
|
DataSynthesisTaskItem,
|
|
PagedDataSynthesisTaskResponse,
|
|
SynthesisType,
|
|
PagedDataSynthesisFileTaskResponse,
|
|
PagedDataSynthesisChunkResponse,
|
|
SynthesisDataItem,
|
|
SynthesisDataUpdateRequest,
|
|
BatchDeleteSynthesisDataRequest,
|
|
BatchDeleteChunkInstancesRequest,
|
|
BatchDeleteChunkInstancesByFileRequest,
|
|
BatchDeleteChunkInstancesByTaskRequest,
|
|
SynthesisDataPatchItem,
|
|
ExportSynthesisDataRequest,
|
|
ExportSynthesisDataResponse,
|
|
)
|
|
from app.module.generation.service.chunk_query_service import ChunkQueryService
|
|
from app.module.generation.service.export_service import SynthesisDatasetExporter, SynthesisExportError
|
|
from app.module.generation.service.file_query_service import FileQueryService
|
|
from app.module.generation.service.generation_service import GenerationService
|
|
from app.module.generation.service.prompt import get_prompt
|
|
from app.module.generation.service.synthesis_data_query_service import SynthesisDataQueryService
|
|
from app.module.generation.service.task_create_service import TaskCreateService
|
|
from app.module.generation.service.task_delete_service import TaskDeleteService
|
|
from app.module.generation.service.task_query_service import TaskQueryService
|
|
from app.module.generation.service.task_executor import execute_generation_task
|
|
from app.module.shared.schema import StandardResponse
|
|
|
|
router = APIRouter(
|
|
prefix="/gen",
|
|
tags=["gen"]
|
|
)
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
# ==================== 任务接口 ====================
|
|
|
|
@router.post("/task", response_model=StandardResponse[DataSynthesisTaskItem])
|
|
async def create_synthesis_task(
|
|
request: CreateSynthesisTaskRequest,
|
|
background_tasks: BackgroundTasks,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""
|
|
创建数据合成任务
|
|
|
|
使用改进的后台任务执行器,将任务提交到独立的线程池中执行,
|
|
避免阻塞主事件循环,确保前端接口的响应性。
|
|
|
|
Args:
|
|
request: 创建任务请求
|
|
background_tasks: 后台任务
|
|
db: 数据库会话
|
|
|
|
Returns:
|
|
创建的任务
|
|
"""
|
|
create_service = TaskCreateService(db)
|
|
task = await create_service.create(request)
|
|
|
|
# 使用新的执行器提交任务到线程池
|
|
# 这样可以真正隔离后台任务,避免阻塞主事件循环
|
|
async def run_task_in_executor(task_id: str):
|
|
"""在线程池中执行任务"""
|
|
# 定义一个包装函数来正确调用process_task
|
|
async def process_task_wrapper(tid: str):
|
|
# 创建新的GenerationService实例,使用独立的会话
|
|
from app.db.session import AsyncSessionLocal
|
|
async with AsyncSessionLocal() as session:
|
|
service = GenerationService(session)
|
|
await service.process_task(tid)
|
|
|
|
await execute_generation_task(task_id, process_task_wrapper)
|
|
|
|
background_tasks.add_task(run_task_in_executor, task.id)
|
|
logger.info(f"Submitted generation task {task.id} to background executor")
|
|
|
|
return SuccessResponse(data=DataSynthesisTaskItem.from_orm_with_config(task))
|
|
|
|
|
|
@router.get("/task/{task_id}", response_model=StandardResponse[DataSynthesisTaskItem])
|
|
async def get_synthesis_task(task_id: str, db: AsyncSession = Depends(get_db)):
|
|
"""
|
|
获取数据合成任务详情
|
|
|
|
Args:
|
|
task_id: 任务 ID
|
|
db: 数据库会话
|
|
|
|
Returns:
|
|
任务详情
|
|
"""
|
|
query_service = TaskQueryService(db)
|
|
task = await query_service.get_task(task_id)
|
|
if not task:
|
|
raise BusinessError(ErrorCodes.GENERATION_TASK_NOT_FOUND, data={"task_id": task_id})
|
|
return SuccessResponse(data=DataSynthesisTaskItem.from_orm_with_config(task))
|
|
|
|
|
|
@router.get("/tasks", response_model=StandardResponse[PagedDataSynthesisTaskResponse])
|
|
async def list_synthesis_tasks(
|
|
page: int = 1,
|
|
page_size: int = 10,
|
|
synthesis_type: str | None = None,
|
|
status: str | None = None,
|
|
name: str | None = None,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
分页列出所有数据合成任务
|
|
|
|
Args:
|
|
page: 页码
|
|
page_size: 每页数量
|
|
synthesis_type: 合成类型过滤
|
|
status: 状态过滤
|
|
name: 名称模糊搜索
|
|
db: 数据库会话
|
|
|
|
Returns:
|
|
分页任务列表
|
|
"""
|
|
query_service = TaskQueryService(db)
|
|
result = await query_service.list_tasks(
|
|
page=page,
|
|
page_size=page_size,
|
|
synthesis_type=synthesis_type,
|
|
status=status,
|
|
name=name,
|
|
)
|
|
return SuccessResponse(data=result)
|
|
|
|
|
|
@router.delete("/task/{task_id}", response_model=StandardResponse)
|
|
async def delete_synthesis_task(task_id: str, db: AsyncSession = Depends(get_db)):
|
|
"""
|
|
删除数据合成任务
|
|
|
|
Args:
|
|
task_id: 任务 ID
|
|
db: 数据库会话
|
|
"""
|
|
delete_service = TaskDeleteService(db)
|
|
await delete_service.delete_task(task_id)
|
|
return SuccessResponse(data=None)
|
|
|
|
|
|
@router.delete("/task/{task_id}/{file_id}", response_model=StandardResponse)
|
|
async def delete_synthesis_file_task(
|
|
task_id: str,
|
|
file_id: str,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
删除数据合成任务中的文件任务
|
|
|
|
Args:
|
|
task_id: 任务 ID
|
|
file_id: 文件任务 ID
|
|
db: 数据库会话
|
|
"""
|
|
delete_service = TaskDeleteService(db)
|
|
await delete_service.delete_file_task(task_id, file_id)
|
|
return SuccessResponse(data=None)
|
|
|
|
|
|
# ==================== 提示词接口 ====================
|
|
|
|
@router.get("/prompt", response_model=StandardResponse[str])
|
|
async def get_prompt_by_type(synth_type: SynthesisType):
|
|
"""
|
|
获取提示词
|
|
|
|
Args:
|
|
synth_type: 合成类型
|
|
|
|
Returns:
|
|
提示词内容
|
|
"""
|
|
prompt = get_prompt(synth_type)
|
|
return SuccessResponse(data=prompt)
|
|
|
|
|
|
# ==================== 文件任务接口 ====================
|
|
|
|
@router.get(
|
|
"/task/{task_id}/files",
|
|
response_model=StandardResponse[PagedDataSynthesisFileTaskResponse]
|
|
)
|
|
async def list_synthesis_file_tasks(
|
|
task_id: str,
|
|
page: int = 1,
|
|
page_size: int = 10,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""
|
|
分页获取任务下的文件任务列表
|
|
|
|
Args:
|
|
task_id: 任务 ID
|
|
page: 页码
|
|
page_size: 每页数量
|
|
db: 数据库会话
|
|
|
|
Returns:
|
|
分页文件任务列表
|
|
"""
|
|
query_service = TaskQueryService(db)
|
|
task = await query_service.get_task(task_id)
|
|
if not task:
|
|
raise BusinessError(ErrorCodes.GENERATION_TASK_NOT_FOUND, data={"task_id": task_id})
|
|
|
|
file_query = FileQueryService(db)
|
|
items, total = await file_query.list_by_task(task_id, page, page_size)
|
|
|
|
return SuccessResponse(data=PagedDataSynthesisFileTaskResponse(
|
|
content=items,
|
|
totalElements=total,
|
|
totalPages=(total + page_size - 1) // page_size,
|
|
page=page,
|
|
size=page_size,
|
|
))
|
|
|
|
|
|
# ==================== 切片接口 ====================
|
|
|
|
@router.get(
|
|
"/file/{file_id}/chunks",
|
|
response_model=StandardResponse[PagedDataSynthesisChunkResponse]
|
|
)
|
|
async def list_chunks_by_file(
|
|
file_id: str,
|
|
page: int = 1,
|
|
page_size: int = 10,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""
|
|
根据文件任务 ID 分页查询切片列表
|
|
|
|
Args:
|
|
file_id: 文件任务 ID
|
|
page: 页码
|
|
page_size: 每页数量
|
|
db: 数据库会话
|
|
|
|
Returns:
|
|
分页切片列表
|
|
"""
|
|
file_query = FileQueryService(db)
|
|
file_task = await file_query.get_by_id(file_id)
|
|
if not file_task:
|
|
raise BusinessError(ErrorCodes.GENERATION_FILE_NOT_FOUND, data={"file_id": file_id})
|
|
|
|
chunk_query = ChunkQueryService(db)
|
|
items, total = await chunk_query.list_by_file(file_id, page, page_size)
|
|
|
|
return SuccessResponse(data=PagedDataSynthesisChunkResponse(
|
|
content=items,
|
|
totalElements=total,
|
|
totalPages=(total + page_size - 1) // page_size,
|
|
page=page,
|
|
size=page_size,
|
|
))
|
|
|
|
|
|
@router.get(
|
|
"/chunk/{chunk_id}/data",
|
|
response_model=StandardResponse[List[SynthesisDataItem]]
|
|
)
|
|
async def list_synthesis_data_by_chunk(chunk_id: str, db: AsyncSession = Depends(get_db)):
|
|
"""
|
|
根据切片 ID 查询所有合成结果数据
|
|
|
|
Args:
|
|
chunk_id: 切片 ID
|
|
db: 数据库会话
|
|
|
|
Returns:
|
|
合成数据列表
|
|
"""
|
|
chunk_query = ChunkQueryService(db)
|
|
chunk = await chunk_query.get_by_id(chunk_id)
|
|
if not chunk:
|
|
raise BusinessError(ErrorCodes.GENERATION_CHUNK_NOT_FOUND, data={"chunk_id": chunk_id})
|
|
|
|
data_query = SynthesisDataQueryService(db)
|
|
items = await data_query.list_by_chunk(chunk_id)
|
|
return SuccessResponse(data=items)
|
|
|
|
|
|
# ==================== 导出接口 ====================
|
|
|
|
@router.post(
|
|
"/task/{task_id}/export-dataset/{dataset_id}",
|
|
response_model=StandardResponse[str]
|
|
)
|
|
async def export_synthesis_task_to_dataset(
|
|
task_id: str,
|
|
dataset_id: str,
|
|
format: str = "alpaca",
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""
|
|
将合成任务的数据归档到已有数据集
|
|
|
|
Args:
|
|
task_id: 任务 ID
|
|
dataset_id: 目标数据集 ID
|
|
format: 导出格式
|
|
db: 数据库会话
|
|
|
|
Returns:
|
|
数据集 ID
|
|
"""
|
|
exporter = SynthesisDatasetExporter(db, format=format)
|
|
generation = GenerationService(db)
|
|
try:
|
|
dataset = await exporter.export_task_to_dataset(task_id, dataset_id)
|
|
await generation.add_synthesis_to_graph(db, task_id, dataset_id)
|
|
except SynthesisExportError as e:
|
|
logger.error(
|
|
"Failed to export synthesis task %s to dataset %s: %s",
|
|
task_id, dataset_id, e
|
|
)
|
|
raise BusinessError(ErrorCodes.OPERATION_FAILED, data={"error": str(e)})
|
|
|
|
return SuccessResponse(data=dataset.id)
|
|
|
|
|
|
@router.post(
|
|
"/data/export",
|
|
response_model=StandardResponse[ExportSynthesisDataResponse],
|
|
)
|
|
async def export_synthesis_data(
|
|
request: ExportSynthesisDataRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""
|
|
将合成数据导出为指定格式的 JSONL 文件
|
|
|
|
Args:
|
|
request: 导出请求
|
|
db: 数据库会话
|
|
|
|
Returns:
|
|
导出结果
|
|
"""
|
|
exporter = SynthesisDatasetExporter(
|
|
db,
|
|
format=request.format.value,
|
|
output_path=request.output_path,
|
|
)
|
|
result = await exporter.export_data(
|
|
task_id=request.task_id,
|
|
file_instance_ids=request.file_instance_ids,
|
|
)
|
|
return SuccessResponse(data=result)
|
|
|
|
|
|
# ==================== 删除接口 ====================
|
|
|
|
@router.delete("/chunk/{chunk_id}", response_model=StandardResponse)
|
|
async def delete_chunk_with_data(chunk_id: str, db: AsyncSession = Depends(get_db)):
|
|
"""
|
|
删除切片及其关联的合成数据
|
|
|
|
Args:
|
|
chunk_id: 切片 ID
|
|
db: 数据库会话
|
|
"""
|
|
delete_service = TaskDeleteService(db)
|
|
await delete_service.delete_chunk(chunk_id)
|
|
return SuccessResponse(data=None)
|
|
|
|
|
|
@router.delete("/chunk/{chunk_id}/data", response_model=StandardResponse)
|
|
async def delete_synthesis_data_by_chunk(chunk_id: str, db: AsyncSession = Depends(get_db)):
|
|
"""
|
|
删除切片下的所有合成数据
|
|
|
|
Args:
|
|
chunk_id: 切片 ID
|
|
db: 数据库会话
|
|
|
|
Returns:
|
|
删除的记录数
|
|
"""
|
|
delete_service = TaskDeleteService(db)
|
|
deleted = await delete_service.delete_synthesis_data_by_chunk(chunk_id)
|
|
return SuccessResponse(data=deleted)
|
|
|
|
|
|
@router.delete("/data/batch", response_model=StandardResponse)
|
|
async def batch_delete_synthesis_data(
|
|
request: BatchDeleteSynthesisDataRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""
|
|
批量删除合成数据
|
|
|
|
Args:
|
|
request: 批量删除请求
|
|
db: 数据库会话
|
|
|
|
Returns:
|
|
删除的记录数
|
|
"""
|
|
delete_service = TaskDeleteService(db)
|
|
deleted = await delete_service.batch_delete_synthesis_data(request.ids)
|
|
return SuccessResponse(data=deleted)
|
|
|
|
|
|
@router.patch("/data/{data_id}", response_model=StandardResponse)
|
|
async def update_synthesis_data_field(
|
|
data_id: str,
|
|
body: SynthesisDataUpdateRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""
|
|
修改合成数据
|
|
|
|
Args:
|
|
data_id: 数据 ID
|
|
body: 更新请求
|
|
db: 数据库会话
|
|
|
|
Returns:
|
|
更新后的数据
|
|
"""
|
|
data_query = SynthesisDataQueryService(db)
|
|
record = await data_query.update(data_id, body.data)
|
|
if not record:
|
|
raise BusinessError(ErrorCodes.GENERATION_DATA_NOT_FOUND, data={"data_id": data_id})
|
|
return SuccessResponse(data=record)
|
|
|
|
|
|
@router.post(
|
|
"/batch-delete/chunks",
|
|
response_model=StandardResponse,
|
|
)
|
|
async def batch_delete_chunk_instances(
|
|
request: BatchDeleteChunkInstancesRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""
|
|
批量删除切片
|
|
|
|
Args:
|
|
request: 批量删除请求
|
|
db: 数据库会话
|
|
"""
|
|
delete_service = TaskDeleteService(db)
|
|
await delete_service.batch_delete_chunks(request.chunk_ids)
|
|
return SuccessResponse(data=None)
|
|
|
|
|
|
@router.post(
|
|
"/batch-delete/chunks-by-file",
|
|
response_model=StandardResponse,
|
|
)
|
|
async def batch_delete_chunk_instances_by_file(
|
|
request: BatchDeleteChunkInstancesByFileRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""
|
|
按文件任务维度删除切片
|
|
|
|
Args:
|
|
request: 删除请求
|
|
db: 数据库会话
|
|
"""
|
|
file_query = FileQueryService(db)
|
|
file_task = await file_query.get_by_id(request.file_id)
|
|
if not file_task:
|
|
raise BusinessError(ErrorCodes.GENERATION_FILE_NOT_FOUND)
|
|
|
|
delete_service = TaskDeleteService(db)
|
|
await delete_service.delete_chunks_by_file(request.file_id)
|
|
return SuccessResponse(data=None)
|
|
|
|
|
|
@router.post(
|
|
"/batch-delete/chunks-by-task",
|
|
response_model=StandardResponse,
|
|
)
|
|
async def batch_delete_chunk_instances_by_task(
|
|
request: BatchDeleteChunkInstancesByTaskRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""
|
|
按任务维度删除切片
|
|
|
|
Args:
|
|
request: 删除请求
|
|
db: 数据库会话
|
|
"""
|
|
query_service = TaskQueryService(db)
|
|
task = await query_service.get_task(request.task_id)
|
|
if not task:
|
|
raise BusinessError(ErrorCodes.GENERATION_TASK_NOT_FOUND)
|
|
|
|
delete_service = TaskDeleteService(db)
|
|
await delete_service.delete_chunks_by_task(request.task_id)
|
|
return SuccessResponse(data=None)
|
|
|
|
|
|
# ==================== 批量查询接口 ====================
|
|
|
|
@router.post(
|
|
"/data/patch",
|
|
response_model=StandardResponse[List[SynthesisDataPatchItem]],
|
|
)
|
|
async def get_synthesis_data_patch(
|
|
task_id: Optional[str] = None,
|
|
file_instance_id: Optional[str] = None,
|
|
chunk_instance_id: Optional[str] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""
|
|
批量查询合成数据
|
|
|
|
Args:
|
|
task_id: 任务 ID
|
|
file_instance_id: 文件任务 ID
|
|
chunk_instance_id: 切片 ID
|
|
db: 数据库会话
|
|
|
|
Returns:
|
|
合成数据列表
|
|
"""
|
|
data_query = SynthesisDataQueryService(db)
|
|
|
|
if chunk_instance_id:
|
|
items = await data_query.list_by_chunk(chunk_instance_id)
|
|
return SuccessResponse(data=[SynthesisDataPatchItem.model_validate(item) for item in items])
|
|
|
|
if file_instance_id:
|
|
file_query = FileQueryService(db)
|
|
file_task = await file_query.get_by_id(file_instance_id)
|
|
if not file_task:
|
|
raise BusinessError(ErrorCodes.GENERATION_FILE_NOT_FOUND)
|
|
items = await data_query.list_by_file(file_instance_id)
|
|
return SuccessResponse(data=items)
|
|
|
|
if task_id:
|
|
query_service = TaskQueryService(db)
|
|
task = await query_service.get_task(task_id)
|
|
if not task:
|
|
raise BusinessError(ErrorCodes.GENERATION_TASK_NOT_FOUND)
|
|
items = await data_query.list_by_task(task_id)
|
|
return SuccessResponse(data=items)
|
|
|
|
return SuccessResponse(data=[])
|