Refactor API router imports and remove unused task imports
This commit is contained in:
1 parent
129b713057
commit
44ad86d455
6 files changed
+1
-458
No files matched your search
@@ -2,16 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1.routes.film import extract, tasks_entities, generated_video, generated_image, tasks_images, task_status
|
||||
from app.api.v1.routes.film import generated_video, generated_image, tasks_images, task_status
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(extract.router)
|
||||
router.include_router(tasks_entities.router)
|
||||
router.include_router(generated_video.router)
|
||||
router.include_router(generated_image.router)
|
||||
router.include_router(tasks_images.router)
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from langchain_core.runnables import Runnable
|
||||
|
||||
from app.chains.agents import FilmEntityExtractorAgent, FilmShotlistStoryboarderAgent
|
||||
from app.schemas.skills.film import FilmEntityExtractionResult, FilmShotlistResult
|
||||
from app.dependencies import get_llm
|
||||
from app.schemas.common import ApiResponse, success_response
|
||||
|
||||
from .common import EntityExtractRequest, ShotlistExtractRequest
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/extract/entities",
|
||||
response_model=ApiResponse[FilmEntityExtractionResult],
|
||||
summary="关键信息抽取",
|
||||
description="从小说文本中抽取人物、地点、道具,忠实原文、可追溯证据。",
|
||||
)
|
||||
def extract_entities(
|
||||
body: EntityExtractRequest,
|
||||
llm: Runnable = Depends(get_llm),
|
||||
) -> ApiResponse[FilmEntityExtractionResult]:
|
||||
"""FilmEntityExtractorAgent:人物/地点/道具抽取。"""
|
||||
extractor = FilmEntityExtractorAgent(llm)
|
||||
chunks_json = json.dumps(
|
||||
[{"chunk_id": c.chunk_id, "text": c.text} for c in body.chunks],
|
||||
ensure_ascii=False,
|
||||
)
|
||||
result = extractor.extract(
|
||||
source_id=body.source_id,
|
||||
language=body.language or "zh",
|
||||
chunks_json=chunks_json,
|
||||
)
|
||||
return success_response(result)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/extract/shotlist",
|
||||
response_model=ApiResponse[FilmShotlistResult],
|
||||
summary="分镜抽取",
|
||||
description="将小说片段转为可拍镜头表(景别/机位/运镜/转场/VFX)。",
|
||||
)
|
||||
def extract_shotlist(
|
||||
body: ShotlistExtractRequest,
|
||||
llm: Runnable = Depends(get_llm),
|
||||
) -> ApiResponse[FilmShotlistResult]:
|
||||
"""FilmShotlistStoryboarderAgent:场景/镜头/转场抽取。"""
|
||||
storyboarder = FilmShotlistStoryboarderAgent(llm)
|
||||
chunks_json = json.dumps(
|
||||
[{"chunk_id": c.chunk_id, "text": c.text} for c in body.chunks],
|
||||
ensure_ascii=False,
|
||||
)
|
||||
result = storyboarder.extract(
|
||||
source_id=body.source_id,
|
||||
source_title=body.source_title or "",
|
||||
language=body.language or "zh",
|
||||
chunks_json=chunks_json,
|
||||
)
|
||||
return success_response(result)
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from langchain_core.runnables import Runnable
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.chains.agents import FilmEntityExtractorAgent, FilmShotlistStoryboarderAgent
|
||||
from app.core.db import async_session_maker
|
||||
from app.core.task_manager import DeliveryMode, SqlAlchemyTaskStore, TaskManager
|
||||
from app.core.task_manager.types import TaskStatus
|
||||
from app.dependencies import get_db, get_llm
|
||||
from app.schemas.common import ApiResponse, success_response
|
||||
|
||||
from .common import (
|
||||
EntityExtractTaskRequest,
|
||||
ShotlistExtractTaskRequest,
|
||||
TaskCreated,
|
||||
_CreateOnlyTask,
|
||||
bind_task,
|
||||
ensure_single_bind_target,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/tasks/entities",
|
||||
response_model=ApiResponse[TaskCreated],
|
||||
status_code=201,
|
||||
summary="关键信息抽取(任务版)",
|
||||
)
|
||||
async def create_entity_extract_task(
|
||||
body: EntityExtractTaskRequest,
|
||||
llm: Runnable = Depends(get_llm),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiResponse[TaskCreated]:
|
||||
target_type, target_id = ensure_single_bind_target(body)
|
||||
|
||||
store = SqlAlchemyTaskStore(db)
|
||||
tm = TaskManager(store=store, strategies={})
|
||||
|
||||
chunks_json = json.dumps([{"chunk_id": c.chunk_id, "text": c.text} for c in body.chunks], ensure_ascii=False)
|
||||
task_record = await tm.create(
|
||||
task=_CreateOnlyTask(),
|
||||
mode=DeliveryMode.async_polling,
|
||||
run_args={
|
||||
"source_id": body.source_id,
|
||||
"language": body.language or "zh",
|
||||
"chunks_json": chunks_json,
|
||||
},
|
||||
)
|
||||
await bind_task(
|
||||
db,
|
||||
task_id=task_record.id,
|
||||
target_type=target_type,
|
||||
target_id=target_id,
|
||||
relation_type="entities",
|
||||
)
|
||||
|
||||
async def _runner(task_id: str, input_dict: dict) -> None:
|
||||
async with async_session_maker() as session:
|
||||
try:
|
||||
store2 = SqlAlchemyTaskStore(session)
|
||||
await store2.set_status(task_id, TaskStatus.running)
|
||||
await store2.set_progress(task_id, 10)
|
||||
extractor = FilmEntityExtractorAgent(llm)
|
||||
result = await extractor.aextract(**input_dict)
|
||||
await store2.set_result(task_id, result.model_dump())
|
||||
await store2.set_progress(task_id, 100)
|
||||
await store2.set_status(task_id, TaskStatus.succeeded)
|
||||
await session.commit()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
await session.rollback()
|
||||
async with async_session_maker() as s2:
|
||||
store3 = SqlAlchemyTaskStore(s2)
|
||||
await store3.set_error(task_id, str(exc))
|
||||
await store3.set_status(task_id, TaskStatus.failed)
|
||||
await s2.commit()
|
||||
|
||||
import asyncio
|
||||
|
||||
asyncio.create_task(
|
||||
_runner(
|
||||
task_record.id,
|
||||
{"source_id": body.source_id, "language": body.language or "zh", "chunks_json": chunks_json},
|
||||
)
|
||||
)
|
||||
return success_response(TaskCreated(task_id=task_record.id), code=201)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/tasks/shotlist",
|
||||
response_model=ApiResponse[TaskCreated],
|
||||
status_code=201,
|
||||
summary="分镜抽取(任务版)",
|
||||
)
|
||||
async def create_shotlist_task(
|
||||
body: ShotlistExtractTaskRequest,
|
||||
llm: Runnable = Depends(get_llm),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiResponse[TaskCreated]:
|
||||
target_type, target_id = ensure_single_bind_target(body)
|
||||
|
||||
store = SqlAlchemyTaskStore(db)
|
||||
tm = TaskManager(store=store, strategies={})
|
||||
|
||||
chunks_json = json.dumps([{"chunk_id": c.chunk_id, "text": c.text} for c in body.chunks], ensure_ascii=False)
|
||||
task_record = await tm.create(
|
||||
task=_CreateOnlyTask(),
|
||||
mode=DeliveryMode.async_polling,
|
||||
run_args={
|
||||
"source_id": body.source_id,
|
||||
"source_title": body.source_title or "",
|
||||
"language": body.language or "zh",
|
||||
"chunks_json": chunks_json,
|
||||
},
|
||||
)
|
||||
await bind_task(
|
||||
db,
|
||||
task_id=task_record.id,
|
||||
target_type=target_type,
|
||||
target_id=target_id,
|
||||
relation_type="shotlist",
|
||||
)
|
||||
|
||||
async def _runner(task_id: str, input_dict: dict) -> None:
|
||||
async with async_session_maker() as session:
|
||||
try:
|
||||
store2 = SqlAlchemyTaskStore(session)
|
||||
await store2.set_status(task_id, TaskStatus.running)
|
||||
await store2.set_progress(task_id, 10)
|
||||
storyboarder = FilmShotlistStoryboarderAgent(llm)
|
||||
result = await storyboarder.aextract(**input_dict)
|
||||
await store2.set_result(task_id, result.model_dump())
|
||||
await store2.set_progress(task_id, 100)
|
||||
await store2.set_status(task_id, TaskStatus.succeeded)
|
||||
await session.commit()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
await session.rollback()
|
||||
async with async_session_maker() as s2:
|
||||
store3 = SqlAlchemyTaskStore(s2)
|
||||
await store3.set_error(task_id, str(exc))
|
||||
await store3.set_status(task_id, TaskStatus.failed)
|
||||
await s2.commit()
|
||||
|
||||
import asyncio
|
||||
|
||||
asyncio.create_task(
|
||||
_runner(
|
||||
task_record.id,
|
||||
{
|
||||
"source_id": body.source_id,
|
||||
"source_title": body.source_title or "",
|
||||
"language": body.language or "zh",
|
||||
"chunks_json": chunks_json,
|
||||
},
|
||||
)
|
||||
)
|
||||
return success_response(TaskCreated(task_id=task_record.id), code=201)
|
||||
|
||||
@@ -11,11 +11,6 @@ from app.core.tasks.image_generation_tasks import (
|
||||
ImageGenerationResult,
|
||||
ImageGenerationTask,
|
||||
)
|
||||
from app.core.tasks.agent_tasks import (
|
||||
FilmEntityExtractionTask2,
|
||||
FilmShotlistTask2,
|
||||
ShotFramePromptTask,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ProviderConfig",
|
||||
@@ -25,7 +20,4 @@ __all__ = [
|
||||
"ImageGenerationInput",
|
||||
"ImageGenerationResult",
|
||||
"ImageGenerationTask",
|
||||
"FilmEntityExtractionTask2",
|
||||
"FilmShotlistTask2",
|
||||
"ShotFramePromptTask",
|
||||
]
|
||||
@@ -1,126 +0,0 @@
|
||||
"""将 chains/agents 的能力封装为 BaseTask 任务。
|
||||
|
||||
说明:
|
||||
- 这些 Task 采用 async_result 模式:run() 返回 None,结果通过 get_result() 获取。
|
||||
- 目的:让上层可以用统一的 TaskManager/存储模型编排生成任务。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, AsyncIterator, Optional
|
||||
|
||||
from app.chains.agents import (
|
||||
FilmEntityExtractorAgent,
|
||||
FilmShotlistStoryboarderAgent,
|
||||
ShotFirstFramePromptAgent,
|
||||
ShotKeyFramePromptAgent,
|
||||
ShotLastFramePromptAgent,
|
||||
)
|
||||
from app.schemas.skills.film import FilmEntityExtractionResult, FilmShotlistResult
|
||||
from app.schemas.skills.shot_frame_prompt import ShotFramePromptResult
|
||||
from app.core.task_manager.types import BaseTask
|
||||
|
||||
|
||||
class _BaseAsyncResultTask(BaseTask):
|
||||
"""通用基类:保存 _result/_error,并提供通用 status/is_done/get_result。"""
|
||||
|
||||
TASK_KEY: str = ""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._error: str = ""
|
||||
|
||||
async def status(self) -> dict[str, Any]: # type: ignore[override]
|
||||
return {
|
||||
"task": self.TASK_KEY,
|
||||
"done": await self.is_done(),
|
||||
"has_result": (await self.get_result()) is not None,
|
||||
"error": self._error,
|
||||
}
|
||||
|
||||
async def is_done(self) -> bool: # type: ignore[override]
|
||||
return (await self.get_result()) is not None or bool(self._error)
|
||||
|
||||
|
||||
class FilmEntityExtractionTask2(_BaseAsyncResultTask):
|
||||
"""人物/地点/道具抽取任务(基于 FilmEntityExtractorAgent)。"""
|
||||
|
||||
TASK_KEY = "film_entity_extraction"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
extractor: FilmEntityExtractorAgent,
|
||||
*,
|
||||
input_dict: dict[str, Any],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._extractor = extractor
|
||||
self._input_dict = input_dict
|
||||
self._result: FilmEntityExtractionResult | None = None
|
||||
|
||||
async def run(self, *args: Any, **kwargs: Any) -> AsyncIterator[Any] | None: # type: ignore[override]
|
||||
try:
|
||||
self._result = await self._extractor.aextract(**self._input_dict)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._error = str(exc)
|
||||
self._result = None
|
||||
return None
|
||||
|
||||
async def get_result(self) -> FilmEntityExtractionResult | None: # type: ignore[override]
|
||||
return self._result
|
||||
|
||||
|
||||
class FilmShotlistTask2(_BaseAsyncResultTask):
|
||||
"""分镜/镜头表抽取任务(基于 FilmShotlistStoryboarderAgent)。"""
|
||||
|
||||
TASK_KEY = "film_shotlist"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
storyboarder: FilmShotlistStoryboarderAgent,
|
||||
*,
|
||||
input_dict: dict[str, Any],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._storyboarder = storyboarder
|
||||
self._input_dict = input_dict
|
||||
self._result: FilmShotlistResult | None = None
|
||||
|
||||
async def run(self, *args: Any, **kwargs: Any) -> AsyncIterator[Any] | None: # type: ignore[override]
|
||||
try:
|
||||
self._result = await self._storyboarder.aextract(**self._input_dict)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._error = str(exc)
|
||||
self._result = None
|
||||
return None
|
||||
|
||||
async def get_result(self) -> FilmShotlistResult | None: # type: ignore[override]
|
||||
return self._result
|
||||
|
||||
|
||||
class ShotFramePromptTask(_BaseAsyncResultTask):
|
||||
"""镜头分镜帧提示词生成任务(首帧/尾帧/关键帧共用)。"""
|
||||
|
||||
TASK_KEY = "shot_frame_prompt"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
agent: ShotFirstFramePromptAgent | ShotLastFramePromptAgent | ShotKeyFramePromptAgent,
|
||||
*,
|
||||
input_dict: dict[str, Any],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._agent = agent
|
||||
self._input_dict = input_dict
|
||||
self._result: ShotFramePromptResult | None = None
|
||||
|
||||
async def run(self, *args: Any, **kwargs: Any) -> AsyncIterator[Any] | None: # type: ignore[override]
|
||||
try:
|
||||
self._result = await self._agent.aextract(**self._input_dict)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._error = str(exc)
|
||||
self._result = None
|
||||
return None
|
||||
|
||||
async def get_result(self) -> ShotFramePromptResult | None: # type: ignore[override]
|
||||
return self._result
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from app.core.tasks.agent_tasks import FilmEntityExtractionTask2, FilmShotlistTask2
|
||||
from app.schemas.skills.film import FilmEntityExtractionResult, FilmShotlistResult
|
||||
|
||||
|
||||
class _StubEntityAgent:
|
||||
async def aextract(self, **kwargs): # noqa: ANN003
|
||||
return FilmEntityExtractionResult.model_validate(
|
||||
{
|
||||
"source_id": kwargs.get("source_id", "novel_ch01"),
|
||||
"language": kwargs.get("language", "zh"),
|
||||
"chunks": [],
|
||||
"characters": [],
|
||||
"locations": [],
|
||||
"props": [],
|
||||
"notes": [],
|
||||
"uncertainties": [],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class _StubShotlistAgent:
|
||||
async def aextract(self, **kwargs): # noqa: ANN003
|
||||
return FilmShotlistResult.model_validate(
|
||||
{
|
||||
"breakdown": {
|
||||
"source_id": kwargs.get("source_id", "novel_ch01"),
|
||||
"chunks": [],
|
||||
"characters": [],
|
||||
"locations": [],
|
||||
"props": [],
|
||||
"scenes": [],
|
||||
"shots": [],
|
||||
"transitions": [],
|
||||
"notes": [],
|
||||
"uncertainties": [],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_film_entity_extraction_task_async_result() -> None:
|
||||
extractor = _StubEntityAgent()
|
||||
task = FilmEntityExtractionTask2(
|
||||
extractor,
|
||||
input_dict={"source_id": "novel_ch01", "language": "zh", "chunks_json": "[]"},
|
||||
)
|
||||
|
||||
assert await task.is_done() is False
|
||||
assert await task.get_result() is None
|
||||
|
||||
await task.run()
|
||||
assert await task.is_done() is True
|
||||
result = await task.get_result()
|
||||
assert result is not None
|
||||
assert result.source_id == "novel_ch01"
|
||||
|
||||
st = await task.status()
|
||||
assert st["done"] is True
|
||||
assert st["has_result"] is True
|
||||
assert st["error"] == ""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_film_shotlist_task_async_result() -> None:
|
||||
storyboarder = _StubShotlistAgent()
|
||||
task = FilmShotlistTask2(
|
||||
storyboarder,
|
||||
input_dict={"source_id": "novel_ch01", "source_title": "", "language": "zh", "chunks_json": "[]"},
|
||||
)
|
||||
|
||||
await task.run()
|
||||
result = await task.get_result()
|
||||
assert result is not None
|
||||
assert result.breakdown.source_id == "novel_ch01"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_task_records_error_when_skill_invalid() -> None:
|
||||
extractor = _StubEntityAgent()
|
||||
task = FilmEntityExtractionTask2(
|
||||
extractor,
|
||||
input_dict={"source_id": "novel_ch01", "language": "zh", "chunks_json": "[]"},
|
||||
)
|
||||
|
||||
# 这里不再测试“无效 skill_id”,因为动态 skill 机制已被移除。
|
||||
await task.run()
|
||||
assert await task.get_result() is not None
|
||||
|
||||
Reference in new issue
Block a user