Files
leocrm/app/plugins/builtins/tasks/workstream.py
T
2026-08-23 20:29:09 +02:00

112 lines
3.9 KiB
Python

"""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,
)