"""Task → Workstream integration (F-TASK-WORK). Posts task_card and goal_card blocks to the communication system so tasks and goals appear in the workstream with live status and progress. """ from __future__ import annotations import logging import uuid from typing import Any from sqlalchemy.ext.asyncio import AsyncSession from app.plugins.builtins.tasks.models import Task logger = logging.getLogger(__name__) def _task_card_data(task: Task) -> dict[str, Any]: """Build a task_card block payload for a task.""" return { "task_id": str(task.id), "title": task.title, "status": task.status, "priority": task.priority, "task_type": task.task_type, "assignee_type": task.assignee_type, "assignee_id": str(task.assignee_id) if task.assignee_id else None, "entity_type": task.entity_type, "entity_id": str(task.entity_id) if task.entity_id else None, "parent_task_id": str(task.parent_task_id) if task.parent_task_id else None, "due_date": task.due_date.isoformat() if task.due_date else None, "progress": task.progress or 0, } def _goal_card_data(task: Task) -> dict[str, Any]: """Build a goal_card block payload for a goal/milestone.""" return { "goal_id": str(task.id), "title": task.title, "status": task.status, "task_type": task.task_type, "progress": task.progress or 0, "target_date": task.target_date.isoformat() if task.target_date else None, "success_criteria": task.success_criteria, "parent_task_id": str(task.parent_task_id) if task.parent_task_id else None, } async def post_task_to_workstream( db: AsyncSession, tenant_id: uuid.UUID, task: Task, *, actor_id: uuid.UUID | None = None, actor_type: str = "user", conversation_id: uuid.UUID | None = None, content: str | None = None, ) -> uuid.UUID | None: """Post a task_card block to the workstream. Returns the created message ID, or None if the communication system is unavailable. """ try: from app.plugins.builtins.contracts import get_contract _komm = get_contract("kommunikation") if not _komm or not hasattr(_komm, "send_message"): logger.warning("kommunikation contract unavailable - skipping workstream post for task %s", task.id) return None send_message = _komm.send_message block_type = "goal_card" if task.task_type in ("goal", "milestone") else "task_card" block_data = _goal_card_data(task) if block_type == "goal_card" else _task_card_data(task) message_id = await send_message( db=db, tenant_id=tenant_id, sender_id=actor_id or task.created_by or task.id, sender_type=actor_type, conversation_id=conversation_id, content=content or f"{task.title} ({task.status})", blocks=[{"type": block_type, "data": block_data}], metadata={"source": "tasks", "task_id": str(task.id)}, ) return message_id except Exception: logger.exception("Failed to post task %s to workstream", task.id) return None async def post_task_status_update( db: AsyncSession, tenant_id: uuid.UUID, task: Task, *, old_status: str | None = None, actor_id: uuid.UUID | None = None, actor_type: str = "user", conversation_id: uuid.UUID | None = None, ) -> uuid.UUID | None: """Post a status-change update to the workstream.""" content = f"Status geändert: {task.title} → {task.status}" if old_status and old_status != task.status: content = f"Status geändert: {task.title} ({old_status} → {task.status})" return await post_task_to_workstream( db, tenant_id, task, actor_id=actor_id, actor_type=actor_type, conversation_id=conversation_id, content=content, )