2026-08-17 18:51:22 +02:00
|
|
|
"""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:
|
2026-08-22 07:25:48 +02:00
|
|
|
from app.plugins.builtins.contracts import get_contract
|
|
|
|
|
_komm = get_contract("kommunikation")
|
2026-08-23 20:29:09 +02:00
|
|
|
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
|
2026-08-17 18:51:22 +02:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
)
|