Files
JARVIS/backend/app/schemas/task.py

298 lines
9.1 KiB
Python

from __future__ import annotations
import json
from datetime import datetime
from pydantic import BaseModel, Field
from sqlalchemy import inspect
from sqlalchemy.orm.attributes import NO_VALUE
from app.models.task import (
Task,
TaskAssigneeType,
TaskDispatchStatus,
TaskHistory,
TaskPriority,
TaskQuadrant,
TaskSource,
TaskStatus,
TaskSubTask,
)
def _coerce_enum(value, enum_cls, default=None):
if value is None:
return default
if isinstance(value, enum_cls):
return value
if isinstance(value, str):
raw = value.strip()
if not raw:
return default
for item in enum_cls:
if raw == item.value or raw.lower() == item.value:
return item
if raw.upper() == item.name:
return item
return default
def parse_tags(raw_tags: str | None) -> list[str]:
if not raw_tags:
return []
try:
parsed = json.loads(raw_tags)
except json.JSONDecodeError:
return []
if not isinstance(parsed, list):
return []
return [str(item) for item in parsed]
def serialize_tags(tags: list[str] | None) -> str | None:
if not tags:
return None
return json.dumps([str(item) for item in tags], ensure_ascii=False)
class TaskSubTaskCreate(BaseModel):
title: str
description: str | None = None
status: TaskStatus = TaskStatus.TODO
order_index: int | None = None
assignee_type: TaskAssigneeType | None = None
assignee_id: str | None = None
class TaskSubTaskUpdate(BaseModel):
title: str | None = None
description: str | None = None
status: TaskStatus | None = None
order_index: int | None = None
assignee_type: TaskAssigneeType | None = None
assignee_id: str | None = None
dispatch_status: TaskDispatchStatus | None = None
dispatch_run_id: str | None = None
result_summary: str | None = None
class TaskSubTaskReorderItem(BaseModel):
id: str
order_index: int
class TaskSubTaskReorderRequest(BaseModel):
items: list[TaskSubTaskReorderItem] = Field(default_factory=list)
class TaskSubTaskOut(BaseModel):
id: str
task_id: str
title: str
description: str | None
status: TaskStatus
order_index: int
assignee_type: TaskAssigneeType | None
assignee_id: str | None
dispatch_status: TaskDispatchStatus
dispatch_run_id: str | None
result_summary: str | None = None
completed_at: datetime | None
created_at: datetime
updated_at: datetime
model_config = {"from_attributes": True}
class TaskHistoryOut(BaseModel):
id: str
task_id: str
action: str
old_value: str | None
new_value: str | None
created_at: datetime
updated_at: datetime
model_config = {"from_attributes": True}
class TaskDispatchSummary(BaseModel):
status: TaskDispatchStatus
run_id: str | None = None
result_summary: str | None = None
started_at: datetime | None = None
last_synced_at: datetime | None = None
total_subtasks: int = 0
dispatched_subtasks: int = 0
subtask_dispatch_statuses: dict[str, int] = Field(default_factory=dict)
class TaskCreate(BaseModel):
title: str
description: str | None = None
status: TaskStatus = TaskStatus.TODO
priority: TaskPriority = TaskPriority.MEDIUM
due_date: datetime | None = None
tags: list[str] | None = None
source: TaskSource = TaskSource.MANUAL
conversation_id: str | None = None
quadrant: TaskQuadrant | None = None
assignee_type: TaskAssigneeType | None = None
assignee_id: str | None = None
subtasks: list[TaskSubTaskCreate] = Field(default_factory=list)
dispatch_to_commander: bool = False
class TaskUpdate(BaseModel):
title: str | None = None
description: str | None = None
status: TaskStatus | None = None
priority: TaskPriority | None = None
due_date: datetime | None = None
tags: list[str] | None = None
source: TaskSource | None = None
conversation_id: str | None = None
quadrant: TaskQuadrant | None = None
assignee_type: TaskAssigneeType | None = None
assignee_id: str | None = None
dispatch_status: TaskDispatchStatus | None = None
dispatch_run_id: str | None = None
result_summary: str | None = None
started_at: datetime | None = None
last_synced_at: datetime | None = None
class TaskOut(BaseModel):
id: str
title: str
description: str | None
status: TaskStatus
priority: TaskPriority
due_date: datetime | None
completed_at: datetime | None
tags: list[str] = Field(default_factory=list)
source: TaskSource
conversation_id: str | None
quadrant: TaskQuadrant | None
assignee_type: TaskAssigneeType | None
assignee_id: str | None
dispatch_status: TaskDispatchStatus
dispatch_run_id: str | None
result_summary: str | None
started_at: datetime | None
last_synced_at: datetime | None
subtask_count: int = 0
created_at: datetime
updated_at: datetime
class TaskDetailOut(TaskOut):
subtasks: list[TaskSubTaskOut] = Field(default_factory=list)
history: list[TaskHistoryOut] = Field(default_factory=list)
dispatch: TaskDispatchSummary
dispatch_summary: TaskDispatchSummary
class TaskDispatchRequest(BaseModel):
target: str = "commander"
conversation_id: str | None = None
assignee_type: TaskAssigneeType | None = None
assignee_id: str | None = None
class TaskDispatchResponse(BaseModel):
status: TaskDispatchStatus
run_id: str | None = None
task: TaskDetailOut
payload: dict[str, object] = Field(default_factory=dict)
class DailyPlanRequest(BaseModel):
user_id: str
def build_task_out(task: Task) -> TaskOut:
subtasks_attr = inspect(task).attrs.subtasks.loaded_value
return TaskOut(
id=task.id,
title=task.title,
description=task.description,
status=_coerce_enum(task.status, TaskStatus, TaskStatus.TODO),
priority=_coerce_enum(task.priority, TaskPriority, TaskPriority.MEDIUM),
due_date=task.due_date,
completed_at=task.completed_at,
tags=parse_tags(task.tags),
source=_coerce_enum(task.source, TaskSource, TaskSource.MANUAL),
conversation_id=task.conversation_id,
quadrant=_coerce_enum(task.quadrant, TaskQuadrant, None),
assignee_type=_coerce_enum(task.assignee_type, TaskAssigneeType, None),
assignee_id=task.assignee_id,
dispatch_status=_coerce_enum(task.dispatch_status, TaskDispatchStatus, TaskDispatchStatus.IDLE),
dispatch_run_id=task.dispatch_run_id,
result_summary=task.result_summary,
started_at=task.started_at,
last_synced_at=task.last_synced_at,
subtask_count=0 if subtasks_attr is NO_VALUE else len(subtasks_attr or []),
created_at=task.created_at,
updated_at=task.updated_at,
)
def build_task_detail_out(task: Task) -> TaskDetailOut:
normalized_task_dispatch = _coerce_enum(task.dispatch_status, TaskDispatchStatus, TaskDispatchStatus.IDLE)
normalized_subtasks = [
TaskSubTaskOut(
id=item.id,
task_id=item.task_id,
title=item.title,
description=item.description,
status=_coerce_enum(item.status, TaskStatus, TaskStatus.TODO),
order_index=item.order_index,
assignee_type=_coerce_enum(item.assignee_type, TaskAssigneeType, None),
assignee_id=item.assignee_id,
dispatch_status=_coerce_enum(item.dispatch_status, TaskDispatchStatus, TaskDispatchStatus.IDLE),
dispatch_run_id=item.dispatch_run_id,
result_summary=item.result_summary,
completed_at=item.completed_at,
created_at=item.created_at,
updated_at=item.updated_at,
)
for item in task.subtasks
]
subtask_dispatch_statuses: dict[str, int] = {}
for item in normalized_subtasks:
key = item.dispatch_status.value
subtask_dispatch_statuses[key] = subtask_dispatch_statuses.get(key, 0) + 1
dispatched_subtasks = sum(1 for item in normalized_subtasks if item.dispatch_status != TaskDispatchStatus.IDLE)
return TaskDetailOut(
**build_task_out(task).model_dump(),
subtasks=normalized_subtasks,
history=[TaskHistoryOut.model_validate(item) for item in task.history],
dispatch=TaskDispatchSummary(
status=normalized_task_dispatch,
run_id=task.dispatch_run_id,
result_summary=task.result_summary,
started_at=task.started_at,
last_synced_at=task.last_synced_at,
total_subtasks=len(normalized_subtasks),
dispatched_subtasks=dispatched_subtasks,
subtask_dispatch_statuses=subtask_dispatch_statuses,
),
dispatch_summary=TaskDispatchSummary(
status=normalized_task_dispatch,
run_id=task.dispatch_run_id,
result_summary=task.result_summary,
started_at=task.started_at,
last_synced_at=task.last_synced_at,
total_subtasks=len(normalized_subtasks),
dispatched_subtasks=dispatched_subtasks,
subtask_dispatch_statuses=subtask_dispatch_statuses,
),
)