Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
298 lines
9.1 KiB
Python
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,
|
|
),
|
|
)
|