225 lines
7.0 KiB
Python
225 lines
7.0 KiB
Python
"""资料导入 API(api-spec §8)。
|
||
|
||
JSON 提交与文件上传分两个端点(FastAPI 无法优雅地在单端点同时吃 JSON body
|
||
与 multipart);两者返回结构一致。路径挂在 ``/characters/{character_id}`` 下。
|
||
候选抽取为异步后台任务:触发即返回 job 状态,前端轮询 ``GET /imports/{id}``
|
||
(extracting_* → reviewing)与候选列表获取结果。
|
||
"""
|
||
|
||
from fastapi import APIRouter, Depends, File, Form, Query, UploadFile
|
||
|
||
from app.api.v1.deps import get_current_user
|
||
from app.schemas.auth import CurrentUser
|
||
from app.schemas.candidates import (
|
||
BatchReviewRequest,
|
||
BatchReviewResult,
|
||
MemoryCandidateResponse,
|
||
ProfileCandidateResponse,
|
||
)
|
||
from app.schemas.common import Page
|
||
from app.schemas.imports import (
|
||
CreateImportRequest,
|
||
ImportItemResponse,
|
||
ImportJobResponse,
|
||
)
|
||
from app.services.imports import (
|
||
batch_review_memory_candidates,
|
||
batch_review_profile_candidates,
|
||
create_import_from_json,
|
||
create_import_from_upload,
|
||
get_import,
|
||
list_import_items,
|
||
list_jobs_page,
|
||
list_memory_candidates,
|
||
list_profile_candidates,
|
||
trigger_memory_extraction,
|
||
trigger_personality_extraction,
|
||
trigger_profile_extraction,
|
||
)
|
||
|
||
router = APIRouter(tags=["imports"])
|
||
|
||
|
||
@router.post("/characters/{character_id}/imports", response_model=ImportJobResponse)
|
||
async def create_import_json(
|
||
character_id: str,
|
||
payload: CreateImportRequest,
|
||
current_user: CurrentUser = Depends(get_current_user),
|
||
) -> ImportJobResponse:
|
||
return await create_import_from_json(
|
||
character_id, payload, owner_scope=current_user.owner_scope
|
||
)
|
||
|
||
|
||
@router.post("/characters/{character_id}/imports/upload", response_model=ImportJobResponse)
|
||
async def create_import_upload(
|
||
character_id: str,
|
||
file: UploadFile = File(..., description="上传文件(文本或音频)。"),
|
||
title: str | None = Form(default=None),
|
||
source_description: str | None = Form(default=None),
|
||
speaker_label: str | None = Form(default=None, description="音频说话人标签。"),
|
||
consent_status: str = Form(default="unknown"),
|
||
usage_scope: str = Form(default="reference_only"),
|
||
current_user: CurrentUser = Depends(get_current_user),
|
||
) -> ImportJobResponse:
|
||
return await create_import_from_upload(
|
||
character_id,
|
||
file,
|
||
title=title,
|
||
source_description=source_description,
|
||
speaker_label=speaker_label,
|
||
consent_status=consent_status,
|
||
usage_scope=usage_scope,
|
||
owner_scope=current_user.owner_scope,
|
||
)
|
||
|
||
|
||
@router.get("/characters/{character_id}/imports", response_model=Page[ImportJobResponse])
|
||
async def list_imports(
|
||
character_id: str,
|
||
page: int = Query(default=1, ge=1),
|
||
page_size: int = Query(default=20, ge=1, le=100),
|
||
current_user: CurrentUser = Depends(get_current_user),
|
||
) -> Page[ImportJobResponse]:
|
||
return await list_jobs_page(
|
||
character_id, page=page, page_size=page_size, owner_scope=current_user.owner_scope
|
||
)
|
||
|
||
|
||
@router.get(
|
||
"/characters/{character_id}/imports/{import_id}",
|
||
response_model=ImportJobResponse,
|
||
)
|
||
async def read_import(
|
||
character_id: str,
|
||
import_id: str,
|
||
current_user: CurrentUser = Depends(get_current_user),
|
||
) -> ImportJobResponse:
|
||
return await get_import(character_id, import_id, owner_scope=current_user.owner_scope)
|
||
|
||
|
||
@router.get(
|
||
"/characters/{character_id}/imports/{import_id}/items",
|
||
response_model=Page[ImportItemResponse],
|
||
)
|
||
async def read_import_items(
|
||
character_id: str,
|
||
import_id: str,
|
||
page: int = Query(default=1, ge=1),
|
||
page_size: int = Query(default=20, ge=1, le=100),
|
||
current_user: CurrentUser = Depends(get_current_user),
|
||
) -> Page[ImportItemResponse]:
|
||
return await list_import_items(
|
||
character_id,
|
||
import_id,
|
||
page=page,
|
||
page_size=page_size,
|
||
owner_scope=current_user.owner_scope,
|
||
)
|
||
|
||
|
||
# —— 候选抽取(5b,异步后台任务)——
|
||
|
||
|
||
@router.post(
|
||
"/characters/{character_id}/imports/{import_id}/profile-candidates",
|
||
response_model=ImportJobResponse,
|
||
)
|
||
async def trigger_profile_candidates(
|
||
character_id: str,
|
||
import_id: str,
|
||
current_user: CurrentUser = Depends(get_current_user),
|
||
) -> ImportJobResponse:
|
||
return await trigger_profile_extraction(
|
||
character_id, import_id, owner_scope=current_user.owner_scope
|
||
)
|
||
|
||
|
||
@router.get(
|
||
"/characters/{character_id}/imports/{import_id}/profile-candidates",
|
||
response_model=list[ProfileCandidateResponse],
|
||
)
|
||
async def list_profile_candidates_api(
|
||
character_id: str,
|
||
import_id: str,
|
||
status: str | None = Query(default=None, description="按审核状态过滤。"),
|
||
current_user: CurrentUser = Depends(get_current_user),
|
||
) -> list[ProfileCandidateResponse]:
|
||
return await list_profile_candidates(
|
||
character_id, import_id, status=status, owner_scope=current_user.owner_scope
|
||
)
|
||
|
||
|
||
@router.post(
|
||
"/characters/{character_id}/imports/{import_id}/memory-candidates",
|
||
response_model=ImportJobResponse,
|
||
)
|
||
async def trigger_memory_candidates(
|
||
character_id: str,
|
||
import_id: str,
|
||
current_user: CurrentUser = Depends(get_current_user),
|
||
) -> ImportJobResponse:
|
||
return await trigger_memory_extraction(
|
||
character_id, import_id, owner_scope=current_user.owner_scope
|
||
)
|
||
|
||
|
||
@router.get(
|
||
"/characters/{character_id}/imports/{import_id}/memory-candidates",
|
||
response_model=list[MemoryCandidateResponse],
|
||
)
|
||
async def list_memory_candidates_api(
|
||
character_id: str,
|
||
import_id: str,
|
||
status: str | None = Query(default=None, description="按审核状态过滤。"),
|
||
current_user: CurrentUser = Depends(get_current_user),
|
||
) -> list[MemoryCandidateResponse]:
|
||
return await list_memory_candidates(
|
||
character_id, import_id, status=status, owner_scope=current_user.owner_scope
|
||
)
|
||
|
||
|
||
@router.post(
|
||
"/characters/{character_id}/imports/{import_id}/personality-proposals",
|
||
response_model=ImportJobResponse,
|
||
)
|
||
async def trigger_personality_proposals(
|
||
character_id: str,
|
||
import_id: str,
|
||
current_user: CurrentUser = Depends(get_current_user),
|
||
) -> ImportJobResponse:
|
||
return await trigger_personality_extraction(
|
||
character_id, import_id, owner_scope=current_user.owner_scope
|
||
)
|
||
|
||
|
||
# —— 候选审核应用(5c)——
|
||
|
||
|
||
@router.post(
|
||
"/characters/{character_id}/profile-candidates/batch-review",
|
||
response_model=BatchReviewResult,
|
||
)
|
||
async def batch_review_profile(
|
||
character_id: str,
|
||
payload: BatchReviewRequest,
|
||
current_user: CurrentUser = Depends(get_current_user),
|
||
) -> BatchReviewResult:
|
||
return await batch_review_profile_candidates(
|
||
character_id, payload, owner_scope=current_user.owner_scope
|
||
)
|
||
|
||
|
||
@router.post(
|
||
"/characters/{character_id}/memory-candidates/batch-review",
|
||
response_model=BatchReviewResult,
|
||
)
|
||
async def batch_review_memory(
|
||
character_id: str,
|
||
payload: BatchReviewRequest,
|
||
current_user: CurrentUser = Depends(get_current_user),
|
||
) -> BatchReviewResult:
|
||
return await batch_review_memory_candidates(
|
||
character_id, payload, owner_scope=current_user.owner_scope
|
||
)
|