"""Workstream contract — unified posting path for Human/System/Agent/Workflow (I-WORK-BASE, I-WORK-ACTOR, I-WORK-HANDOFF). Defines the contract for posting typed blocks to the central Communication system. All actors use the same posting path with typed blocks. """ from __future__ import annotations import logging import uuid from dataclasses import dataclass, field from typing import Any, Literal from sqlalchemy.ext.asyncio import AsyncSession logger = logging.getLogger(__name__) ActorType = Literal["human", "system", "agent", "workflow"] BlockType = Literal["text", "entity_card", "action_card", "evidence_card", "approval_card", "miniapp", "workflow_status", "workflow_handoff", "error"] @dataclass class WorkstreamBlock: type: BlockType content: str = "" metadata: dict[str, Any] = field(default_factory=dict) def to_dict(self) -> dict[str, Any]: return {"type": self.type, "content": self.content, "metadata": self.metadata} @dataclass class WorkstreamMessage: actor_type: ActorType actor_id: str | None = None content: str = "" blocks: list[WorkstreamBlock] = field(default_factory=list) conversation_id: str | None = None tenant_id: str | None = None def to_dict(self) -> dict[str, Any]: return { "actor_type": self.actor_type, "actor_id": self.actor_id, "content": self.content, "blocks": [b.to_dict() for b in self.blocks], "conversation_id": self.conversation_id, "tenant_id": self.tenant_id, } async def post_to_workstream(db: AsyncSession, tenant_id: uuid.UUID, message: WorkstreamMessage) -> dict[str, Any] | None: """Post a message to the central workstream (I-WORK-ACTOR).""" try: from app.plugins.builtins.kommunikation.contracts import KommunikationContract contract = KommunikationContract post_fn = contract.get_function("post_message") if post_fn is None: if message.actor_type == "human" and message.actor_id: from app.core.notifications import post_system_message await post_system_message(db, tenant_id, uuid.UUID(message.actor_id), "workstream", message.content[:200], message.content) return None return await post_fn(db=db, tenant_id=tenant_id, sender_id=uuid.UUID(message.actor_id) if message.actor_id else None, sender_type=message.actor_type, message_type=f"workstream_{message.blocks[0].type}" if message.blocks else "workstream_text", content=message.content, blocks=[b.to_dict() for b in message.blocks], conversation_id=message.conversation_id) except Exception as e: logger.warning("Failed to post to workstream: %s", e) return None async def create_handoff(db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, *, handoff_type: str, assignee_type: str = "user", assignee_id: str | None = None, entity_type: str | None = None, entity_id: str | None = None, description: str = "", agent_run_id: str | None = None, workflow_instance_id: str | None = None, conversation_id: str | None = None) -> dict[str, Any]: """Create a Human<->Agent handoff (I-WORK-HANDOFF). Creates a Task with task_type='handoff'.""" from app.plugins.builtins.tasks.services import create_task task_data: dict[str, Any] = {"title": f"Handoff: {handoff_type}", "description": description, "task_type": "handoff", "assignee_type": assignee_type, "assignee_id": assignee_id, "entity_type": entity_type, "entity_id": entity_id, "status": "open", "priority": "medium"} task = await create_task(db, tenant_id, user_id, task_data) handoff_block = WorkstreamBlock(type="workflow_handoff", content=description, metadata={"handoff_type": handoff_type, "task_id": task.get("id") if task else None, "assignee_type": assignee_type, "assignee_id": assignee_id, "entity_type": entity_type, "entity_id": entity_id, "agent_run_id": agent_run_id, "workflow_instance_id": workflow_instance_id}) message = WorkstreamMessage(actor_type="system", content=f"Handoff: {handoff_type} - {description}", blocks=[handoff_block], conversation_id=conversation_id, tenant_id=str(tenant_id)) post_result = await post_to_workstream(db, tenant_id, message) return {"task": task, "workstream_post": post_result, "handoff_type": handoff_type} def build_entity_card(entity_type: str, entity_id: str, title: str = "", subtitle: str = "", url: str = "") -> WorkstreamBlock: return WorkstreamBlock(type="entity_card", content=title, metadata={"entity_type": entity_type, "entity_id": entity_id, "title": title, "subtitle": subtitle, "url": url}) def build_action_card(title: str, actions: list[dict[str, str]], description: str = "") -> WorkstreamBlock: return WorkstreamBlock(type="action_card", content=title, metadata={"title": title, "description": description, "actions": actions}) def build_evidence_card(source_type: str, source_id: str, title: str, snippet: str = "", url: str = "", confidence: float = 0.0) -> WorkstreamBlock: return WorkstreamBlock(type="evidence_card", content=title, metadata={"source_type": source_type, "source_id": source_id, "title": title, "snippet": snippet[:200], "url": url, "confidence": confidence}) def build_approval_card(approval_id: str, action: str, description: str = "") -> WorkstreamBlock: return WorkstreamBlock(type="approval_card", content=f"Approval needed: {action}", metadata={"approval_id": approval_id, "action": action, "description": description}) def build_miniapp_block(app_id: str, title: str = "", render_schema: dict[str, Any] | None = None) -> WorkstreamBlock: return WorkstreamBlock(type="miniapp", content=title, metadata={"app_id": app_id, "title": title, "render_schema": render_schema or {}}) __all__ = ["ActorType", "BlockType", "WorkstreamBlock", "WorkstreamMessage", "post_to_workstream", "create_handoff", "build_entity_card", "build_action_card", "build_evidence_card", "build_approval_card", "build_miniapp_block"]