"""Job model — persistent asynchronous job state. RabbitMQ delivers work messages. MongoDB is the source of truth for job lifecycle. Worker processes update Job records as they progress through queued → running → completed|failed|cancelled. Do not use RabbitMQ queues as the permanent job database. """ from __future__ import annotations from typing import Any, Optional, Dict from pydantic import Field from asf_domain.base import ( ASFBaseModel, TenantScopedModel, ASFFieldID, UTCDatetime, JobStatus, JobPriority, JobType, generate_id, ) class JobUsage(ASFBaseModel): """Optional resource / cost usage tracked for a job.""" input_tokens: Optional[int] = Field(None, ge=0) output_tokens: Optional[int] = Field(None, ge=0) gpu_seconds: Optional[float] = Field(None, ge=0) cpu_seconds: Optional[float] = Field(None, ge=0) estimated_cost: Optional[float] = Field( None, ge=0, description="Estimated cost in tenant currency (EUR/USD). Approximate, used for metering only.", ) extra: Dict[str, Any] = Field(default_factory=dict) class JobBase(ASFBaseModel): type: JobType = Field( ..., description="Logical job class. Used for routing to the correct RabbitMQ queue.", ) priority: JobPriority = JobPriority.NORMAL payload: Dict[str, Any] = Field( default_factory=dict, description="The actual work payload (e.g. {artifact_id, prompt, model_preference}).", ) correlation_id: Optional[str] = Field( None, max_length=128, description="Optional correlation id propagated across logs, events, child jobs.", ) class JobCreate(JobBase): """Create payload. tenant_id/project_id/requested_by are injected server-side.""" project_id: Optional[ASFFieldID] = None model: Optional[str] = Field( None, max_length=128, description="Preferred model / engine, only meaningful for AI job types. " "Ultimate routing decision belongs to the AI Gateway.", ) class JobUpdate(ASFBaseModel): """Updates issued by workers as the job progresses.""" status: Optional[JobStatus] = None worker: Optional[str] = Field( None, max_length=255, description="Identifier of the worker process claiming this job." ) started_at: Optional[UTCDatetime] = None completed_at: Optional[UTCDatetime] = None result: Optional[Dict[str, Any]] = None error: Optional[str] = Field(None, max_length=2000) error_detail: Optional[Dict[str, Any]] = None usage: Optional[JobUsage] = None progress_percent: Optional[int] = Field(None, ge=0, le=100) class Job(TenantScopedModel, JobBase): """Persistent job record — the authoritative source of truth.""" id: ASFFieldID = Field(default_factory=generate_id) project_id: Optional[ASFFieldID] = None status: JobStatus = JobStatus.QUEUED requested_by: Optional[ASFFieldID] = Field( None, description="User that requested the job, or null for system jobs." ) worker: Optional[str] = None model: Optional[str] = None created_at: UTCDatetime = ... started_at: Optional[UTCDatetime] = None completed_at: Optional[UTCDatetime] = None result: Optional[Dict[str, Any]] = None error: Optional[str] = None error_detail: Optional[Dict[str, Any]] = None usage: JobUsage = Field(default_factory=JobUsage) progress_percent: Optional[int] = Field(None, ge=0, le=100) retries: int = Field(0, ge=0, description="How many times this job has been retried.")