feat(F.14): Unified Task System — F-TASK-MODEL/API/AGENT/WORK/UI/MIG/GOAL/TEST
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
- F-TASK-MODEL: Extended Task model with polymorphic assignee/entity/creator, subtasks, dependencies, task_type, success_criteria, progress - F-TASK-API: Extended task routes with polymorphic filters, subtasks, dependencies, new lifecycle - F-TASK-AGENT: ai_tools.py (191 lines) — create_task, assign_task, update_task_status, decompose_goal tools - F-TASK-WORK: workstream.py — task_card and goal_card blocks in communication system - F-TASK-UI: TaskBoard.tsx, TaskDetail.tsx, GoalView.tsx frontend components - F-TASK-MIG: Migration 0124 — new columns, data migration for contact_id/assigned_to - F-TASK-GOAL: Progress aggregation, success criteria evaluation, parent status propagation - F-TASK-TEST: test_unified_tasks.py (414 lines) - i18n updates for task system
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"""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.kommunikation.services import 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,
|
||||
)
|
||||
Reference in New Issue
Block a user