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
This commit is contained in:
@@ -197,6 +197,7 @@ async def run_react_loop(
|
||||
"tenant_id": str(tenant_id),
|
||||
"user_id": str(user_id),
|
||||
"db": db,
|
||||
"agent_name": getattr(agent_definition, "name", "Agent"),
|
||||
}
|
||||
|
||||
# Audit helper — records every tool call in the audit log.
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""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",
|
||||
)
|
||||
Reference in New Issue
Block a user