feat(backend): enhance task and schedule center APIs with expanded endpoints
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -1,14 +1,146 @@
|
||||
from pydantic import BaseModel
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from app.models.task import TaskStatus, TaskPriority
|
||||
|
||||
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):
|
||||
@@ -18,6 +150,16 @@ class TaskUpdate(BaseModel):
|
||||
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):
|
||||
@@ -28,12 +170,128 @@ class TaskOut(BaseModel):
|
||||
priority: TaskPriority
|
||||
due_date: datetime | None
|
||||
completed_at: datetime | None
|
||||
tags: str | 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
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
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,
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user