Initial commit
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
"""Kanban task service shared by Office UI and CLI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from opc.core.models import TaskStatus
|
||||
from opc.layer2_organization.work_item_transition import apply_task_status_transition
|
||||
from opc.presentation.kanban import column_to_task_status
|
||||
|
||||
from .context import OfficeServiceContext
|
||||
from .models import ServiceError, ServiceEvent, ServiceResult
|
||||
from .session import SessionService
|
||||
|
||||
|
||||
class KanbanService:
|
||||
TERMINAL_STATUSES: set[str] = {"done", "failed", "cancelled"}
|
||||
|
||||
def __init__(self, context: OfficeServiceContext, session_service: SessionService) -> None:
|
||||
self.context = context
|
||||
self.session_service = session_service
|
||||
|
||||
def _reject_system_driven(self) -> None:
|
||||
if self.context.mode_state.exec_mode in {"company", "org", "custom"}:
|
||||
raise ServiceError("company_mode_kanban_is_system_driven", "company_mode_kanban_is_system_driven")
|
||||
|
||||
async def create_task(self, *, project_id: str, title: str, description: str = "", task_id: str | None = None, board_id: str | None = None, assignee_ids: list[str] | None = None) -> ServiceResult:
|
||||
self._reject_system_driven()
|
||||
return await self.session_service.create(
|
||||
project_id=project_id,
|
||||
title=title or "Untitled",
|
||||
description=description,
|
||||
task_id=task_id or str(uuid.uuid4()),
|
||||
exec_mode=self.context.mode_state.exec_mode,
|
||||
company_profile=self.context.mode_state.company_profile,
|
||||
preferred_agent=self.context.mode_state.task_preferred_agent,
|
||||
interface="office_ui",
|
||||
board_id=board_id,
|
||||
assignee_ids=assignee_ids or [],
|
||||
)
|
||||
|
||||
async def update_task(self, *, project_id: str, task_id: str, updates: dict[str, Any]) -> ServiceResult:
|
||||
self._reject_system_driven()
|
||||
engine = await self.context.engine_for_project(project_id)
|
||||
if task_id and getattr(engine, "store", None):
|
||||
task = await engine.store.get_task(task_id)
|
||||
if task:
|
||||
if "title" in updates:
|
||||
task.title = updates["title"]
|
||||
if "description" in updates:
|
||||
task.description = updates["description"]
|
||||
if "tags" in updates:
|
||||
task.tags = updates["tags"]
|
||||
await engine.store.save_task(task)
|
||||
payload = {"project_id": self.context.normalize_project_id(project_id), "task_id": task_id}
|
||||
return ServiceResult(payload, [ServiceEvent("kanban_updated", payload)])
|
||||
|
||||
async def move_task(self, *, project_id: str, task_id: str, column_id: str) -> ServiceResult:
|
||||
self._reject_system_driven()
|
||||
engine = await self.context.engine_for_project(project_id)
|
||||
if task_id and column_id and getattr(engine, "store", None):
|
||||
task = await engine.store.get_task(task_id)
|
||||
if task:
|
||||
current = task.status.value if hasattr(task.status, "value") else str(task.status)
|
||||
new_status = column_to_task_status(column_id)
|
||||
if not new_status:
|
||||
raise ServiceError("invalid_column", f"Invalid column/status: {column_id}")
|
||||
target = new_status.value if hasattr(new_status, "value") else str(new_status)
|
||||
if current in self.TERMINAL_STATUSES and target not in self.TERMINAL_STATUSES:
|
||||
raise ServiceError("terminal_task", f"Cannot move {current} task back to {column_id}")
|
||||
if current != target:
|
||||
task.status = new_status
|
||||
await engine.store.save_task(task)
|
||||
payload = {"project_id": self.context.normalize_project_id(project_id), "task_id": task_id, "display_id": "", "column_name": column_id}
|
||||
return ServiceResult(payload, [ServiceEvent("board_task_moved", payload)])
|
||||
|
||||
async def delete_task(self, *, project_id: str, task_id: str) -> ServiceResult:
|
||||
self._reject_system_driven()
|
||||
engine = await self.context.engine_for_project(project_id)
|
||||
if task_id and getattr(engine, "store", None):
|
||||
task = await engine.store.get_task(task_id)
|
||||
if task:
|
||||
await apply_task_status_transition(
|
||||
engine.store,
|
||||
task,
|
||||
target_status_or_phase=TaskStatus.CANCELLED,
|
||||
reason="kanban_delete_task",
|
||||
release_claim=True,
|
||||
)
|
||||
payload = {"project_id": self.context.normalize_project_id(project_id), "task_id": task_id}
|
||||
return ServiceResult(payload, [ServiceEvent("kanban_updated", payload)])
|
||||
|
||||
async def assign(self, *, project_id: str, task_id: str, agent_id: str) -> ServiceResult:
|
||||
self._reject_system_driven()
|
||||
engine = await self.context.engine_for_project(project_id)
|
||||
if task_id and agent_id and getattr(engine, "store", None):
|
||||
task = await engine.store.get_task(task_id)
|
||||
if task:
|
||||
agent = await self.context.agent_store._get_one(agent_id)
|
||||
role_id = agent.get("opc_role_id", agent_id) if agent else agent_id
|
||||
task.assigned_to = role_id
|
||||
await engine.store.save_task(task)
|
||||
return ServiceResult({"project_id": self.context.normalize_project_id(project_id), "task_id": task_id, "agent_id": agent_id})
|
||||
|
||||
async def status(self, *, project_id: str, task_id: str, status: str) -> ServiceResult:
|
||||
return await self.move_task(project_id=project_id, task_id=task_id, column_id=status)
|
||||
Reference in New Issue
Block a user