Files
ai-video-fullstack/backend/services/background_tasks/records.py
T

89 lines
3.6 KiB
Python
Raw Normal View History

"""Persistent projections of native jobs; this is not another scheduler."""
from dataclasses import asdict, dataclass, field
from datetime import UTC, datetime
from uuid import uuid4
from sqlalchemy import select
from pydantic.alias_generators import to_camel
from db.models import BackgroundTask, BackgroundTaskEvent
from db.session import SessionLocal
TERMINAL = {"succeeded", "failed", "cancelled"}
ACTIVE = {"queued", "submitted", "running", "waiting_input", "cancel_requested", "unknown"}
@dataclass
class TaskRecord:
id: str
assistant_id: str
conversation_id: str
tool_id: str
instruction: str
origin_key: str
connection: dict
secrets: dict
backend_id: str = ""
backend_type: str = "opencode"
deadline_at: str | None = None
status: str = "queued"
summary: str = "任务已排队"
result: str = ""
remote_handle: dict = field(default_factory=dict)
input_request: dict = field(default_factory=dict)
job_id: str = ""
sequence: int = 0
created_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
updated_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
def public(self):
hidden = {"connection", "secrets", "origin_key", "remote_handle"}
return {to_camel(key): value for key, value in asdict(self).items() if key not in hidden}
def from_row(row) -> TaskRecord:
return TaskRecord(**{key: getattr(row, key) for key in TaskRecord.__dataclass_fields__})
class TaskRepository:
async def create(self, record: TaskRecord) -> TaskRecord:
async with SessionLocal() as db:
# All calls are serialized by the application entry point; unique
# origin_key also protects against duplicate tool delivery.
existing = (await db.execute(select(BackgroundTask).where(BackgroundTask.origin_key == record.origin_key))).scalar_one_or_none()
if existing:
return from_row(existing)
db.add(BackgroundTask(**asdict(record)))
await db.commit()
return record
async def get(self, task_id: str) -> TaskRecord | None:
async with SessionLocal() as db:
row = await db.get(BackgroundTask, task_id)
return from_row(row) if row else None
async def list(self, assistant_id: str | None = None, *, active=False) -> list[TaskRecord]:
statement = select(BackgroundTask).order_by(BackgroundTask.status.in_(ACTIVE).desc(), BackgroundTask.created_at.desc())
if not active:
statement = statement.limit(100)
if assistant_id:
statement = statement.where(BackgroundTask.assistant_id == assistant_id)
if active:
statement = statement.where(BackgroundTask.status.in_(ACTIVE))
async with SessionLocal() as db:
return [from_row(row) for row in (await db.execute(statement)).scalars().all()]
async def patch(self, task_id: str, **changes) -> TaskRecord:
async with SessionLocal() as db:
row = (await db.execute(select(BackgroundTask).where(BackgroundTask.id == task_id).with_for_update())).scalar_one()
if row.status in TERMINAL:
return from_row(row)
for key, value in changes.items():
setattr(row, key, value)
row.sequence += 1
row.updated_at = datetime.now(UTC).isoformat()
record = from_row(row)
db.add(BackgroundTaskEvent(id="te_" + uuid4().hex, task_id=task_id, sequence=row.sequence, data=record.public()))
await db.commit()
return record