Files
leocrm/app/ai/miniapp_tools.py
Agent Zero 335762dd3d feat(M6): Weitere Hosts — MiniApps in Fenstern + AI-Agenten-Ausgabe-Bloecke (#364)
- Windows-Host: openMiniAppWindow-Helper + MiniAppWindowContent (windowStore);
  Oeffnen-Buttons im Chat-Block (MiniAppBlock) und Dashboard-Widget
- AI-Agenten-Host: Core-Tool send_miniapp (app/ai/miniapp_tools.py) —
  miniapp-Block in Agent-Chat (approval_request-Praezedenz), Permission
  fail-closed gegen aufrufenden User pro App; Registrierung im lifespan
- agent_loop: tool_context + agent_name (Raum-Aufloesung)
- Fix: MiniAppBlock nutzt useMiniapps (component-Feld) statt Legacy /comm/miniapps
- Tests: M6 7/7 (TDD rot->gruen), Backend-Regression 57/57, Vitest 26/26
  (4 neue Window-Tests), tsc clean, build OK
2026-08-31 01:04:33 +02:00

128 lines
4.2 KiB
Python

"""Core AI agent tools for MiniApp output (Phase M6).
``send_miniapp`` lets an agent embed a MiniApp as an interactive output
block in its chat room (block_type "miniapp", approval_request precedent
from agent_loop). Permission is checked fail-closed against the CALLING
user for the target app's own permission — the tool never widens access.
"""
from __future__ import annotations
import logging
import uuid
from typing import Any
from app.ai.tool_registry import get_tool_registry
from app.core.permissions import check_permission, resolve_permissions
from app.plugins.miniapp_registry import get_miniapp_registry
logger = logging.getLogger(__name__)
def _get_komm_contract() -> Any | None:
"""Resolve the kommunikation contract (None when plugin inactive)."""
from app.plugins.builtins.contracts import get_contract_registry
return get_contract_registry().get("kommunikation")
async def _send_miniapp_handler(arguments: dict[str, Any], context: dict[str, Any]) -> str:
"""Send a MiniApp as an output block to the agent's chat room."""
app_id = str(arguments.get("app_id") or "")
settings = arguments.get("settings") or {}
if not isinstance(settings, dict):
settings = {}
app = get_miniapp_registry().get_app(app_id)
if app is None:
return f"Error: MiniApp '{app_id}' not found"
db = context.get("db")
tenant_id = context.get("tenant_id")
user_id = context.get("user_id")
if not tenant_id or not user_id or db is None:
return "Error: Missing tenant/user context"
try:
tenant_uuid = uuid.UUID(str(tenant_id))
user_uuid = uuid.UUID(str(user_id))
except (ValueError, TypeError):
return "Error: Invalid tenant/user context"
# Fail-closed permission check against the calling user
resolved = await resolve_permissions(db, user_uuid, tenant_uuid)
if app.permission and not check_permission(resolved, app.permission):
return f"Error: Permission '{app.permission}' required for MiniApp '{app.app_id}'"
komm = _get_komm_contract()
if komm is None:
return "Error: Communication plugin not available"
agent_name = str(context.get("agent_name") or "Agent")
conv_id = await komm.find_locked_room_id(
db=db,
tenant_id=tenant_uuid,
plugin_name="automation",
title=f"Agent: {agent_name}",
)
if conv_id is None:
return f"Info: No agent chat room found for '{agent_name}' — MiniApp not posted"
agent_id_raw = context.get("agent_id")
try:
sender_id = uuid.UUID(str(agent_id_raw)) if agent_id_raw else None
except (ValueError, TypeError):
sender_id = None
await komm.send_message(
db=db,
tenant_id=tenant_uuid,
conversation_id=conv_id,
sender_id=sender_id,
sender_type="agent",
content=f"MiniApp: {app.name}",
content_format="text",
blocks=[
{
"block_type": "miniapp",
"block_data": {"app_id": app_id, "config": settings},
"sort_order": 0,
}
],
)
return f"MiniApp '{app_id}' sent to chat"
# ─── Registration ───
def register_miniapp_tools() -> None:
"""Register the MiniApp agent tools in the global tool registry."""
registry = get_tool_registry()
registry.register(
name="send_miniapp",
description=(
"Eine MiniApp als interaktiven Ausgabe-Block in den Agent-Chat senden "
"(z.B. ein Widget mit Einstellungen anzeigen). Verfügbare App-IDs "
"stehen in /api/v1/miniapps."
),
parameters={
"type": "object",
"properties": {
"app_id": {
"type": "string",
"description": "ID der MiniApp (z.B. recent_contacts, tasks_summary)",
},
"settings": {
"type": "object",
"description": "Optionale Einstellungen für die MiniApp-Instanz",
},
},
"required": ["app_id"],
},
handler=_send_miniapp_handler,
plugin_name="system",
required_permission=None, # per-app check inside the handler (fail-closed)
category="ui",
)