Phase 3.5: Automation & Agents Plugin
Neues automation Plugin (app/plugins/builtins/automation/): - 7 DB-Modelle: AgentDefinition, AgentVersion, AutomationDefinition, AutomationVersion, AutomationCronJob, AgentRun, AutomationRun - Migration 0001_initial.sql mit allen Tabellen + RLS - PluginManifest erweitert: agent_definitions, automation_templates, cron_jobs, heartbeat_configs, miniapps Contribution-Felder - 21 API-Endpoints: /api/v1/automation (CRUD, execute, dry-run, runs, versions, restore, settings, miniapps) + /api/v1/agents (CRUD, execute, test-run, runs, versions, restore, tools, send-message) Backend Features: - Cron-Scheduler (scheduler.py): ARQ-basiert, liest CronJob-Tabelle, enqueued run_agent/run_automation, croniter fuer next_run_at - Workflow-Timeout-Worker (workflow_timeout.py): prueft abgelaufene WorkflowInstances, setzt cancelled, sendet Notification - Agent Runner (agent_runner.py): LiteLLM + ToolRegistry, proactive/ reactive mode, Rate-Limiting, Budget-Limit, Infinite-Loop-Detection - Automation Execution Engine (execution_engine.py): Condition evaluation (eq/ne/gt/lt/contains/exists), Actions (api_call/ notification/workflow_start), Dry-Run mode - Agent-to-Agent Communication (agent_comm.py): send_agent_message tool, kommunikation plugin integration - Plugin-Beitraege: register/unregister on activate/deactivate, Konfliktloesung mit Plugin-Name als Prefix - Heartbeat-Migration: ai_proactive heartbeat als Cron-Job - Versionshistorie: Auto-Versioning bei Updates, Restore-Endpoint - Settings: GET/PATCH /api/v1/automation/settings - ARQ Worker: 11 functions, 2 cron_jobs (scheduler_tick 30s, check_workflow_timeouts 5min) Frontend: - AutomationDashboard.tsx: Automation Builder UI mit Trigger, Conditions, Actions, Execute, Dry-Run, Run-History, Versions - AgentDashboard.tsx: Agent Builder UI mit Model, Tools, Prompt, Heartbeat, Rate-Limits, Execute, Test-Run, Agent-Chat - AutomationSettings.tsx: Settings + MiniApp-Builder - automation.ts: 24 React Query Hooks - automation.ts types: TypeScript Interfaces - routes/index.tsx: /automation, /agents, /settings/automation Tests: - 22 Frontend-Tests (AutomationDashboard, AgentDashboard, API) — alle bestanden - Backend-Tests: test_automation.py (CRUD, versions, conditions, dry-run, rate-limiting) - TSC: keine neuen Errors (nur pre-existing Dms.tsx) - croniter dependency installiert
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
"""Agent-to-Agent communication router."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def send_agent_message(
|
||||
from_agent_id: str,
|
||||
to_agent_name: str,
|
||||
message: str,
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
) -> dict:
|
||||
"""Send a message from one agent to another.
|
||||
|
||||
1. Find target agent by name
|
||||
2. Create a kommunikation message in a dedicated agent room
|
||||
3. Enqueue run_agent for the target agent with the message as trigger_data
|
||||
4. Return delivery status
|
||||
"""
|
||||
from app.plugins.builtins.automation.models import AgentDefinition
|
||||
from app.plugins.builtins.automation.agent_runner import run_agent
|
||||
|
||||
# 1. Find target agent by name
|
||||
result = await db.execute(
|
||||
select(AgentDefinition)
|
||||
.where(AgentDefinition.name == to_agent_name)
|
||||
.where(AgentDefinition.tenant_id == tenant_id)
|
||||
.limit(1)
|
||||
)
|
||||
target_agent = result.scalar_one_or_none()
|
||||
|
||||
if target_agent is None:
|
||||
logger.warning(
|
||||
"Target agent '%s' not found for message from agent %s",
|
||||
to_agent_name, from_agent_id,
|
||||
)
|
||||
return {"status": "error", "error": f"Target agent '{to_agent_name}' not found"}
|
||||
|
||||
if not target_agent.is_active:
|
||||
logger.warning(
|
||||
"Target agent '%s' is inactive, cannot deliver message from %s",
|
||||
to_agent_name, from_agent_id,
|
||||
)
|
||||
return {"status": "error", "error": f"Target agent '{to_agent_name}' is inactive"}
|
||||
|
||||
# 2. Create a kommunikation message in a dedicated agent room
|
||||
try:
|
||||
from app.plugins.builtins.kommunikation.models import Message, Room
|
||||
from app.plugins.builtins.kommunikation.services import RoomService
|
||||
|
||||
# Find or create the agent-to-agent room
|
||||
room_name = f"agent:{from_agent_id}:{target_agent.id}"
|
||||
room_result = await db.execute(
|
||||
select(Room).where(Room.name == room_name).limit(1)
|
||||
)
|
||||
room = room_result.scalar_one_or_none()
|
||||
|
||||
if room is None:
|
||||
# Create a new room for agent communication
|
||||
room = Room(
|
||||
tenant_id=tenant_id,
|
||||
name=room_name,
|
||||
display_name=f"Agent Chat: {from_agent_id} -> {to_agent_name}",
|
||||
room_type="agent_comm",
|
||||
is_direct=True,
|
||||
)
|
||||
db.add(room)
|
||||
await db.flush()
|
||||
|
||||
# Create the message
|
||||
msg = Message(
|
||||
tenant_id=tenant_id,
|
||||
room_id=room.id,
|
||||
sender_id=from_agent_id,
|
||||
sender_type="agent",
|
||||
content=message,
|
||||
message_type="agent_comm",
|
||||
)
|
||||
db.add(msg)
|
||||
await db.flush()
|
||||
|
||||
logger.info(
|
||||
"Agent message created: %s -> %s in room %s",
|
||||
from_agent_id, to_agent_name, room_name,
|
||||
)
|
||||
except ImportError:
|
||||
logger.warning("Kommunikation plugin not available, skipping message storage")
|
||||
except Exception as e:
|
||||
logger.exception("Failed to create kommunikation message: %s", e)
|
||||
|
||||
# 3. Enqueue run_agent for the target agent with the message as trigger_data
|
||||
try:
|
||||
trigger_data = {
|
||||
"from_agent_id": from_agent_id,
|
||||
"message": message,
|
||||
"type": "agent_comm",
|
||||
}
|
||||
# Run the target agent with the message as trigger data
|
||||
result_data = await run_agent(
|
||||
ctx={},
|
||||
agent_id=str(target_agent.id),
|
||||
trigger_type="agent_comm",
|
||||
trigger_data=trigger_data,
|
||||
)
|
||||
logger.info(
|
||||
"Target agent %s executed with message from %s: status=%s",
|
||||
to_agent_name, from_agent_id, result_data.get("status"),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("Failed to run target agent %s: %s", to_agent_name, e)
|
||||
return {"status": "error", "error": f"Failed to activate target agent: {e}"}
|
||||
|
||||
return {
|
||||
"status": "sent",
|
||||
"target_agent": to_agent_name,
|
||||
"target_agent_id": str(target_agent.id),
|
||||
}
|
||||
|
||||
|
||||
def register_agent_comm_tool():
|
||||
"""Register the send_agent_message tool in the global tool registry."""
|
||||
from app.plugins.builtins.ai_assistant.tool_registry import get_tool_registry
|
||||
|
||||
registry = get_tool_registry()
|
||||
|
||||
async def handler(arguments: dict[str, Any], context: dict[str, Any]) -> str:
|
||||
"""Handle send_agent_message tool call from an AI agent."""
|
||||
from app.core.db import get_session_factory
|
||||
|
||||
to_agent_name = arguments.get("to_agent_name", "")
|
||||
message = arguments.get("message", "")
|
||||
from_agent_id = context.get("agent_id", "unknown")
|
||||
tenant_id_str = context.get("tenant_id", "")
|
||||
|
||||
if not to_agent_name or not message:
|
||||
return json.dumps({"status": "error", "error": "Missing to_agent_name or message"})
|
||||
|
||||
try:
|
||||
tenant_id = uuid.UUID(tenant_id_str) if tenant_id_str else uuid.uuid4()
|
||||
except (ValueError, TypeError):
|
||||
return json.dumps({"status": "error", "error": "Invalid tenant_id"})
|
||||
|
||||
factory = get_session_factory()
|
||||
async with factory() as db:
|
||||
result = await send_agent_message(
|
||||
from_agent_id=from_agent_id,
|
||||
to_agent_name=to_agent_name,
|
||||
message=message,
|
||||
db=db,
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
return json.dumps(result)
|
||||
|
||||
registry.register(
|
||||
name="send_agent_message",
|
||||
description="Send a message to another agent. The target agent will be activated with your message.",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"to_agent_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the target agent",
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Message to send",
|
||||
},
|
||||
},
|
||||
"required": ["to_agent_name", "message"],
|
||||
},
|
||||
handler=handler,
|
||||
plugin_name="automation",
|
||||
required_permission="agents:execute",
|
||||
category="communication",
|
||||
)
|
||||
logger.info("Agent communication tool 'send_agent_message' registered")
|
||||
|
||||
|
||||
def unregister_agent_comm_tool():
|
||||
"""Unregister the send_agent_message tool."""
|
||||
from app.plugins.builtins.ai_assistant.tool_registry import get_tool_registry
|
||||
|
||||
registry = get_tool_registry()
|
||||
registry.unregister("send_agent_message")
|
||||
logger.info("Agent communication tool 'send_agent_message' unregistered")
|
||||
Reference in New Issue
Block a user