From 62793a001ce68d64dc0d0fe3f789d8d8d5b702bd Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Fri, 21 Aug 2026 00:05:13 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20I-WORK-HANDOFF=20+=20I-WORK-PROACTIVE?= =?UTF-8?q?=20=E2=80=94=20approval=20requests=20posted=20to=20communicatio?= =?UTF-8?q?n=20with=20approval=5Frequest=20block,=20proactive=20suggestion?= =?UTF-8?q?s=20posted=20to=20communication=20with=20action=5Fcard=20block?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/ai/agent_loop.py | 53 +++++++++++--- app/plugins/builtins/ai_proactive/services.py | 69 ++++++++++++++++++- 2 files changed, 112 insertions(+), 10 deletions(-) diff --git a/app/ai/agent_loop.py b/app/ai/agent_loop.py index b818cec..1170f8a 100644 --- a/app/ai/agent_loop.py +++ b/app/ai/agent_loop.py @@ -390,16 +390,51 @@ async def run_react_loop( requested_by_type="agent", ) - # Post approval request to workstream + # Post approval request to Communication (I-WORK-HANDOFF) if agent_run_id: - await post_approval_request( - db=db, - tenant_id=tenant_id, - agent_id=getattr(agent_definition, "id", uuid.uuid4()), - approval_id=approval.id, - action=f"Tool '{tool_name}' requires approval", - details={"tool_name": tool_name, "arguments": args}, - ) + try: + from app.plugins.builtins.contracts import get_contract_registry + from app.plugins.builtins.kommunikation.models import CommConversation + from sqlalchemy import select as sa_select + komm = get_contract_registry().get("kommunikation") + if komm: + agent_id = getattr(agent_definition, "id", uuid.uuid4()) + room_title = f"Agent: {getattr(agent_definition, 'name', 'Agent')}" + existing = await db.execute( + sa_select(CommConversation).where( + CommConversation.tenant_id == tenant_id, + CommConversation.title == room_title, + CommConversation.is_locked.is_(True), + CommConversation.locked_by == "automation", + CommConversation.deleted_at.is_(None), + ) + ) + conv = existing.scalar_one_or_none() + if conv: + await komm.send_message( + db=db, + tenant_id=tenant_id, + conversation_id=conv.id, + sender_id=agent_id, + sender_type="agent", + content=f"Approval required for tool '{tool_name}'", + content_format="text", + blocks=[ + { + "block_type": "approval_request", + "block_data": { +n "title": f"Approval: {tool_name}", + "description": f"Agent wants to execute tool '{tool_name}' with arguments: {json.dumps(args)[:300]}", + "approval_id": str(approval.id), + "status": "pending", + }, + "sort_order": 0, + } + ], + metadata={"approval_id": str(approval.id), "agent_run_id": str(agent_run_id)}, + ) + except Exception: + logger.warning("Failed to post approval request to communication", exc_info=True) # Pause the loop — return with waiting_for_approval status result.status = "waiting_for_approval" diff --git a/app/plugins/builtins/ai_proactive/services.py b/app/plugins/builtins/ai_proactive/services.py index eac73a9..ea315f4 100644 --- a/app/plugins/builtins/ai_proactive/services.py +++ b/app/plugins/builtins/ai_proactive/services.py @@ -62,10 +62,77 @@ def get_sse_queue(user_id: str) -> asyncio.Queue[dict[str, Any]]: async def push_suggestion(user_id: str, suggestion: dict[str, Any]) -> None: - """Push suggestion to user's SSE queue.""" + """Push suggestion to user's SSE queue and post to Communication.""" queue = get_sse_queue(user_id) await queue.put(suggestion) + # Post suggestion to Communication (I-WORK-PROACTIVE) + try: + import uuid as uuid_mod + from app.plugins.builtins.contracts import get_contract_registry + from app.plugins.builtins.kommunikation.models import CommConversation + from sqlalchemy import select as sa_select + from app.core.db import get_worker_session_factory + komm = get_contract_registry().get("kommunikation") + if komm: + factory = get_worker_session_factory() + async with factory() as db: + # Find or create AI suggestions room + room_title = "KI Vorschläge" + # Get tenant_id from suggestion or user + tenant_id = suggestion.get("tenant_id") + if not tenant_id: + return + existing = await db.execute( + sa_select(CommConversation).where( + CommConversation.tenant_id == uuid_mod.UUID(str(tenant_id)), + CommConversation.title == room_title, + CommConversation.is_locked.is_(True), + CommConversation.locked_by == "ai_proactive", + CommConversation.deleted_at.is_(None), + ) + ) + conv = existing.scalar_one_or_none() + if not conv: + room = await komm.create_plugin_room( + db=db, + tenant_id=uuid_mod.UUID(str(tenant_id)), + user_id=uuid_mod.UUID(str(user_id)), + plugin_name="ai_proactive", + title=room_title, + participant_type="ai", + ) + conv_id = uuid_mod.UUID(room["conversation_id"]) + else: + conv_id = conv.id + await komm.send_message( + db=db, + tenant_id=uuid_mod.UUID(str(tenant_id)), + conversation_id=conv_id, + sender_id=None, + sender_type="ai", + content=suggestion.get("title", "KI Vorschlag"), + content_format="text", + blocks=[ + { + "block_type": "action_card", + "block_data": { + "title": suggestion.get("title", "Vorschlag"), + "description": suggestion.get("description", ""), + "actions": [ + {"label": "Annehmen", "action": "accept_suggestion", "data": {"suggestion_id": suggestion.get("id", "")}}, + {"label": "Ablehnen", "action": "dismiss_suggestion", "data": {"suggestion_id": suggestion.get("id", "")}}, + ], + }, + "sort_order": 0, + } + ], + metadata={"suggestion_id": suggestion.get("id", ""), "type": "proactive_suggestion"}, + ) + await db.commit() + except Exception: + logger.warning("Failed to post suggestion to communication", exc_info=True) + # ─── Rate Limiting ───