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:
Agent Zero
2026-07-23 20:00:37 +02:00
parent fc96a2f86c
commit 5dc6f29ac1
28 changed files with 7397 additions and 0 deletions
+325
View File
@@ -0,0 +1,325 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
useAutomationSettings,
useUpdateAutomationSettings,
useMiniApps,
useCreateMiniApp,
useDeleteMiniApp,
} from '@/api/automation';
import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card';
import { Select } from '@/components/ui/Select';
import { Skeleton } from '@/components/ui/Skeleton';
import { useToast } from '@/components/ui/Toast';
import { Modal } from '@/components/ui/Modal';
import type { AutomationSettings as AutomationSettingsType, MiniAppDef } from '@/types/automation';
import { Save, AlertCircle, Plus, Trash2, AppWindow } from 'lucide-react';
const logLevelOptions = [
{ value: 'debug', label: 'Debug' },
{ value: 'info', label: 'Info' },
{ value: 'warning', label: 'Warning' },
{ value: 'error', label: 'Error' },
];
export function AutomationSettingsPage() {
const { t } = useTranslation();
const toast = useToast();
const { data: settings, isLoading, isError, refetch } = useAutomationSettings();
const updateMutation = useUpdateAutomationSettings();
const [form, setForm] = useState<AutomationSettingsType>({
default_llm_model: '',
heartbeat_default_interval: 0,
max_concurrent_agents: 0,
log_level: 'info',
});
useEffect(() => {
if (settings) {
setForm({
default_llm_model: settings.default_llm_model || '',
heartbeat_default_interval: settings.heartbeat_default_interval || 0,
max_concurrent_agents: settings.max_concurrent_agents || 0,
log_level: settings.log_level || 'info',
});
}
}, [settings]);
const updateField = <K extends keyof AutomationSettingsType>(key: K, value: AutomationSettingsType[K]) => {
setForm((prev) => ({ ...prev, [key]: value }));
};
const handleSave = async () => {
try {
await updateMutation.mutateAsync(form);
toast.success(t('automation.settingsSaved'));
} catch (err: any) {
toast.error(err.message || t('common.error'));
}
};
const { data: miniapps, isLoading: miniappsLoading, refetch: refetchMiniapps } = useMiniApps();
const createMiniAppMutation = useCreateMiniApp();
const deleteMiniAppMutation = useDeleteMiniApp();
const [showMiniAppForm, setShowMiniAppForm] = useState(false);
const [miniAppForm, setMiniAppForm] = useState({
app_id: '',
name: '',
icon: 'AppWindow',
description: '',
render_schema: '{}',
});
const handleCreateMiniApp = async () => {
try {
let render_schema = {};
try {
render_schema = JSON.parse(miniAppForm.render_schema);
} catch {
toast.error(t('common.invalidJson'));
return;
}
await createMiniAppMutation.mutateAsync({
app_id: miniAppForm.app_id,
name: miniAppForm.name,
icon: miniAppForm.icon,
description: miniAppForm.description,
render_schema,
});
toast.success(t('automation.miniAppCreated'));
setShowMiniAppForm(false);
setMiniAppForm({ app_id: '', name: '', icon: 'AppWindow', description: '', render_schema: '{}' });
refetchMiniapps();
} catch (err: any) {
toast.error(err.message || t('common.error'));
}
};
const handleDeleteMiniApp = async (appId: string) => {
try {
await deleteMiniAppMutation.mutateAsync(appId);
toast.success(t('automation.miniAppDeleted'));
refetchMiniapps();
} catch (err: any) {
toast.error(err.message || t('common.error'));
}
};
return (
<div className="max-w-3xl mx-auto" data-testid="automation-settings">
<div className="mb-6">
<h1 className="text-2xl font-bold text-secondary-900">{t('automation.settingsTitle')}</h1>
<p className="text-sm text-secondary-500 mt-1">{t('automation.settingsSubtitle')}</p>
</div>
{isLoading && (
<div className="space-y-4">
{[1, 2, 3, 4].map((i) => (
<Skeleton key={i} className="h-16 w-full" />
))}
</div>
)}
{isError && (
<Card className="p-6">
<div className="flex items-center gap-3 text-danger-600">
<AlertCircle className="h-5 w-5" />
<span>{t('common.errorLoading')}</span>
<Button size="sm" variant="secondary" onClick={() => refetch()}>
{t('common.retry')}
</Button>
</div>
</Card>
)}
{!isLoading && !isError && (
<Card className="p-6">
<div className="space-y-6">
{/* Default LLM Model */}
<div>
<label className="block text-sm font-medium text-secondary-700 mb-1">
{t('automation.defaultLlmModel')}
</label>
<input
type="text"
value={form.default_llm_model}
onChange={(e) => updateField('default_llm_model', e.target.value)}
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder="gpt-4"
/>
<p className="text-xs text-secondary-400 mt-1">{t('automation.defaultLlmModelHint')}</p>
</div>
{/* Heartbeat Default Interval */}
<div>
<label className="block text-sm font-medium text-secondary-700 mb-1">
{t('automation.heartbeatDefaultInterval')}
</label>
<input
type="number"
value={form.heartbeat_default_interval}
onChange={(e) => updateField('heartbeat_default_interval', parseInt(e.target.value) || 0)}
min={0}
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
/>
<p className="text-xs text-secondary-400 mt-1">{t('automation.heartbeatDefaultIntervalHint')}</p>
</div>
{/* Max Concurrent Agents */}
<div>
<label className="block text-sm font-medium text-secondary-700 mb-1">
{t('automation.maxConcurrentAgents')}
</label>
<input
type="number"
value={form.max_concurrent_agents}
onChange={(e) => updateField('max_concurrent_agents', parseInt(e.target.value) || 0)}
min={0}
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
/>
<p className="text-xs text-secondary-400 mt-1">{t('automation.maxConcurrentAgentsHint')}</p>
</div>
{/* Log Level */}
<div>
<label className="block text-sm font-medium text-secondary-700 mb-1">
{t('automation.logLevel')}
</label>
<Select
options={logLevelOptions}
value={form.log_level}
onChange={(e) => updateField('log_level', e.target.value as 'debug' | 'info' | 'warning' | 'error')}
/>
</div>
{/* Save Button */}
<div className="pt-4 border-t border-secondary-200">
<Button
onClick={handleSave}
isLoading={updateMutation.isPending}
icon={<Save className="h-4 w-4" />}
>
{t('common.save')}
</Button>
</div>
</div>
</Card>
)}
{/* MiniApp Builder Section */}
<div className="mt-8">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-xl font-bold text-secondary-900">{t('automation.miniAppsTitle')}</h2>
<p className="text-sm text-secondary-500 mt-1">{t('automation.miniAppsSubtitle')}</p>
</div>
<Button onClick={() => setShowMiniAppForm(true)} icon={<Plus className="h-4 w-4" />}>
{t('automation.createMiniApp')}
</Button>
</div>
{miniappsLoading ? (
<div className="space-y-2">
{[1, 2].map((i) => (
<Skeleton key={i} className="h-16 w-full" />
))}
</div>
) : !miniapps || miniapps.length === 0 ? (
<Card className="p-6 text-center">
<AppWindow className="h-8 w-8 mx-auto text-secondary-400 mb-2" />
<p className="text-sm text-secondary-500">{t('automation.noMiniApps')}</p>
</Card>
) : (
<div className="space-y-2">
{miniapps.map((app: MiniAppDef) => (
<Card key={app.app_id} className="p-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<AppWindow className="h-5 w-5 text-primary-500" />
<div>
<p className="font-medium text-secondary-900">{app.name}</p>
<p className="text-xs text-secondary-400">{app.app_id} {app.description || t('common.noDescription')}</p>
</div>
</div>
<Button
size="sm"
variant="ghost"
onClick={() => handleDeleteMiniApp(app.app_id)}
isLoading={deleteMiniAppMutation.isPending}
>
<Trash2 className="h-4 w-4 text-danger-500" />
</Button>
</Card>
))}
</div>
)}
</div>
{/* Create MiniApp Modal */}
<Modal
open={showMiniAppForm}
onClose={() => setShowMiniAppForm(false)}
title={t('automation.createMiniApp')}
size="md"
>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-secondary-700 mb-1">{t('automation.miniAppId')}</label>
<input
type="text"
value={miniAppForm.app_id}
onChange={(e) => setMiniAppForm({ ...miniAppForm, app_id: e.target.value })}
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm"
placeholder="my_custom_app"
/>
</div>
<div>
<label className="block text-sm font-medium text-secondary-700 mb-1">{t('automation.miniAppName')}</label>
<input
type="text"
value={miniAppForm.name}
onChange={(e) => setMiniAppForm({ ...miniAppForm, name: e.target.value })}
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm"
placeholder="My Custom App"
/>
</div>
<div>
<label className="block text-sm font-medium text-secondary-700 mb-1">{t('automation.miniAppIcon')}</label>
<input
type="text"
value={miniAppForm.icon}
onChange={(e) => setMiniAppForm({ ...miniAppForm, icon: e.target.value })}
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm"
placeholder="AppWindow"
/>
</div>
<div>
<label className="block text-sm font-medium text-secondary-700 mb-1">{t('automation.miniAppDescription')}</label>
<textarea
value={miniAppForm.description}
onChange={(e) => setMiniAppForm({ ...miniAppForm, description: e.target.value })}
rows={2}
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm"
placeholder="Optional description"
/>
</div>
<div>
<label className="block text-sm font-medium text-secondary-700 mb-1">{t('automation.miniAppSchema')}</label>
<textarea
value={miniAppForm.render_schema}
onChange={(e) => setMiniAppForm({ ...miniAppForm, render_schema: e.target.value })}
rows={4}
className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm font-mono"
placeholder='{"type": "form", "fields": []}'
/>
</div>
<div className="flex justify-end gap-3 pt-4 border-t border-secondary-200">
<Button variant="secondary" onClick={() => setShowMiniAppForm(false)}>{t('common.cancel')}</Button>
<Button onClick={handleCreateMiniApp} isLoading={createMiniAppMutation.isPending}>{t('common.create')}</Button>
</div>
</div>
</Modal>
</div>
);
}