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,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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user