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,252 @@
|
||||
"""SQLAlchemy models for the Automation & Agents plugin."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
DateTime,
|
||||
Float,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
String,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
|
||||
|
||||
class AgentDefinition(Base, TenantMixin):
|
||||
"""An AI agent definition — configures an LLM-powered agent with tools and behavior."""
|
||||
|
||||
__tablename__ = "automation_agent_definitions"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("tenant_id", "name", name="uq_agent_def_name"),
|
||||
Index("ix_agent_def_tenant_active", "tenant_id", "is_active"),
|
||||
Index("ix_agent_def_tenant_mode", "tenant_id", "mode"),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(120), nullable=False)
|
||||
description: Mapped[str] = mapped_column(String(500), nullable=False, default="")
|
||||
llm_model: Mapped[str] = mapped_column(
|
||||
String(100), nullable=False, default="ollama/deepseek-v4-flash"
|
||||
)
|
||||
system_prompt: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
||||
tool_ids: Mapped[list[Any]] = mapped_column(JSONB, nullable=False, default=list)
|
||||
heartbeat_interval_seconds: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, default=0
|
||||
)
|
||||
mode: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="reactive"
|
||||
)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
max_executions_per_hour: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, default=10
|
||||
)
|
||||
max_duration_seconds: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, default=300
|
||||
)
|
||||
budget_limit_usd: Mapped[float] = mapped_column(
|
||||
Float, nullable=False, default=1.0
|
||||
)
|
||||
created_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||
PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
|
||||
|
||||
class AgentVersion(Base, TenantMixin):
|
||||
"""Versioned snapshots of agent definitions."""
|
||||
|
||||
__tablename__ = "automation_agent_versions"
|
||||
__table_args__ = (
|
||||
Index("ix_agent_versions_agent", "tenant_id", "agent_id"),
|
||||
UniqueConstraint("agent_id", "version_number", name="uq_agent_version"),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
agent_id: Mapped[uuid.UUID] = mapped_column(
|
||||
PGUUID(as_uuid=True),
|
||||
ForeignKey("automation_agent_definitions.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
version_number: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
snapshot: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
|
||||
changed_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||
PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
|
||||
|
||||
class AutomationDefinition(Base, TenantMixin):
|
||||
"""An automation workflow definition — event/schedule/manual triggered with conditions and actions."""
|
||||
|
||||
__tablename__ = "automation_definitions"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("tenant_id", "name", name="uq_automation_def_name"),
|
||||
Index("ix_automation_def_tenant_active", "tenant_id", "is_active"),
|
||||
Index("ix_automation_def_tenant_trigger", "tenant_id", "trigger_type"),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(120), nullable=False)
|
||||
description: Mapped[str] = mapped_column(String(500), nullable=False, default="")
|
||||
trigger_type: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="manual"
|
||||
)
|
||||
trigger_config: Mapped[dict[str, Any]] = mapped_column(
|
||||
JSONB, nullable=False, default=dict
|
||||
)
|
||||
conditions: Mapped[list[Any]] = mapped_column(JSONB, nullable=False, default=list)
|
||||
actions: Mapped[list[Any]] = mapped_column(JSONB, nullable=False, default=list)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
dry_run: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
created_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||
PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
|
||||
|
||||
class AutomationVersion(Base, TenantMixin):
|
||||
"""Versioned snapshots of automation definitions."""
|
||||
|
||||
__tablename__ = "automation_versions"
|
||||
__table_args__ = (
|
||||
Index("ix_automation_versions_def", "tenant_id", "automation_id"),
|
||||
UniqueConstraint("automation_id", "version_number", name="uq_automation_version"),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
automation_id: Mapped[uuid.UUID] = mapped_column(
|
||||
PGUUID(as_uuid=True),
|
||||
ForeignKey("automation_definitions.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
version_number: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
snapshot: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
|
||||
changed_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||
PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
|
||||
|
||||
class AutomationCronJob(Base, TenantMixin):
|
||||
"""Cron job schedule entries for agent heartbeats, automation triggers, or custom jobs."""
|
||||
|
||||
__tablename__ = "automation_cron_jobs"
|
||||
__table_args__ = (
|
||||
Index("ix_cron_jobs_tenant_active", "tenant_id", "is_active"),
|
||||
Index("ix_cron_jobs_next_run", "next_run_at"),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(120), nullable=False)
|
||||
cron_expression: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
job_type: Mapped[str] = mapped_column(
|
||||
String(50), nullable=False, default="automation_trigger"
|
||||
)
|
||||
target_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
PGUUID(as_uuid=True), nullable=True
|
||||
)
|
||||
plugin_name: Mapped[str] = mapped_column(String(80), nullable=False, default="")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
last_run_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
next_run_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, index=True
|
||||
)
|
||||
|
||||
|
||||
class AgentRun(Base, TenantMixin):
|
||||
"""Execution log for agent runs."""
|
||||
|
||||
__tablename__ = "automation_agent_runs"
|
||||
__table_args__ = (
|
||||
Index("ix_agent_runs_agent_status", "tenant_id", "agent_id", "status"),
|
||||
Index("ix_agent_runs_started", "tenant_id", "started_at"),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
agent_id: Mapped[uuid.UUID] = mapped_column(
|
||||
PGUUID(as_uuid=True),
|
||||
ForeignKey("automation_agent_definitions.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="pending"
|
||||
)
|
||||
started_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False
|
||||
)
|
||||
completed_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
duration_seconds: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
result: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
cost_usd: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
trigger_type: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="manual"
|
||||
)
|
||||
trigger_data: Mapped[dict[str, Any]] = mapped_column(
|
||||
JSONB, nullable=False, default=dict
|
||||
)
|
||||
|
||||
|
||||
class AutomationRun(Base, TenantMixin):
|
||||
"""Execution log for automation runs."""
|
||||
|
||||
__tablename__ = "automation_runs"
|
||||
__table_args__ = (
|
||||
Index("ix_automation_runs_def_status", "tenant_id", "automation_id", "status"),
|
||||
Index("ix_automation_runs_started", "tenant_id", "started_at"),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
automation_id: Mapped[uuid.UUID] = mapped_column(
|
||||
PGUUID(as_uuid=True),
|
||||
ForeignKey("automation_definitions.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="pending"
|
||||
)
|
||||
started_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False
|
||||
)
|
||||
completed_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
duration_seconds: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
result: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
trigger_type: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="manual"
|
||||
)
|
||||
trigger_data: Mapped[dict[str, Any]] = mapped_column(
|
||||
JSONB, nullable=False, default=dict
|
||||
)
|
||||
dry_run: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
Reference in New Issue
Block a user