44 KiB
44 KiB
PowerOn Automation Unification — Datenmodell & Architektur
Version: 2.0
Datum: 2026-04-05
Referenz: Automation Business Spec.md
1. Plattform-Architektur: Schichten
┌──────────────────────────────────────────────────────────────────┐
│ FRONTEND (React/TS) │
│ │
│ Shared Components: ChatBar, ChatStream, UnifiedDataBar, │
│ FlowEditor, FormGenerator, FolderTree │
│ │
│ Feature Pages: WorkspacePage, GraphicalEditorPage, │
│ CommcoachDossierView, ChatbotView, ... │
└──────────────────────────────┬────────────────────────────────────┘
│ HTTP/SSE/WebSocket
┌──────────────────────────────▼────────────────────────────────────┐
│ GATEWAY (FastAPI) │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Layer 1: FEATURES (Feature-Instanz-gebunden) │ │
│ │ │ │
│ │ features/workspace/ features/graphicalEditor/ │ │
│ │ features/commcoach/ features/chatbot/ │ │
│ │ features/trustee/ features/realEstate/ │ │
│ │ features/neutralization/ features/teamsbot/ │ │
│ └──────────────────────────┬──────────────────────────────────┘ │
│ │ nutzt │
│ ┌──────────────────────────▼──────────────────────────────────┐ │
│ │ Layer 2: SERVICES (Feature-unabhängig, pro Request) │ │
│ │ │ │
│ │ serviceAgent serviceAi serviceKnowledge serviceBilling │ │
│ │ serviceChat serviceExtraction serviceGeneration │ │
│ │ serviceMessaging serviceWeb serviceSubscription │ │
│ └──────────────────────────┬──────────────────────────────────┘ │
│ │ nutzt │
│ ┌──────────────────────────▼──────────────────────────────────┐ │
│ │ Layer 3: SHARED INFRASTRUCTURE │ │
│ │ │ │
│ │ workflows/methods/ (Unified Action Library / Toolboxes)│ │
│ │ workflows/processing/ (ActionExecutor, methodDiscovery) │ │
│ │ workflows/automation2/ (Graph Execution Engine) │ │
│ │ interfaces/ (DB-Abstraktion + RBAC) │ │
│ │ aicore/ (Provider Plugins + Model Selector) │ │
│ │ datamodels/ (Pydantic Models) │ │
│ │ security/ (RBAC Engine) │ │
│ │ shared/ (Utilities, eventManagement) │ │
│ └─────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
2. Feature-Container-Pattern (Ist-Zustand, bewährt)
2.1 Verzeichnisstruktur
features/<featureName>/
├── main<FeatureName>.py ◄── Feature-Definition + Registrierung
│ FEATURE_CODE: str Feature-Identifikator
│ UI_OBJECTS: List[Dict] RBAC-fähige UI-Objekte
│ RESOURCE_OBJECTS: List[Dict] RBAC-fähige Resource-Objekte
│ TEMPLATE_ROLES: List[Dict] Rollen-Templates mit AccessRules
│ REQUIRED_SERVICES: List[Dict] Service-Dependencies (optional)
│ registerFeature(catalog) → bool Registriert Objekte, synct Rollen in DB
│ getFeatureDefinition() → Dict Katalog-Metadaten (Label, Icon)
│ get<Feature>Services(user, ...) → Hub Service-Hub-Factory (optional)
│
├── routeFeature<FeatureName>.py ◄── HTTP API
│ router = APIRouter(prefix="/api/<featureName>")
│ [templateRouter] = APIRouter(...) Zusätzliche Router (optional)
│
├── interfaceFeature<FeatureName>.py ◄── Datenbankzugriff
│ <Feature>Objects CRUD + RBAC-Filterung
│ getInterface(user, mandateId, featureInstanceId) → Singleton
│
├── datamodelFeature<FeatureName>.py ◄── Pydantic-Datenmodelle
│ class <Model>(PowerOnModel): ... Feature-spezifische Entitäten
│
├── nodeDefinitions/ (optional) ◄── Graph-Node-Typen (für graphicalEditor)
│ triggers.py, flow.py, ai.py, ...
│
└── service<Name>/ (optional) ◄── Feature-interne Services
└── mainService<Name>.py
2.2 Automatische Discovery
# system/registry.py — Kein manuelles Wiring nötig
discoverFeatureContainers()
# Scannt features/*/routeFeature*.py → Liste von Feature-Dirs
loadFeatureRouters(app)
# Importiert routeFeature*.py, mountet router + *Router
loadFeatureMainModules()
# Importiert main*.py, cached
registerAllFeaturesInCatalog(catalogService)
# Für jedes main-Module:
# getFeatureDefinition() → catalogService.registerFeatureDefinition()
# registerFeature() → UI/Resource-Objekte + Template-Rollen sync
2.3 Service-Hub-Wiring
# serviceHub/__init__.py — Dynamisches Service-Wiring
class ServiceHub:
def __init__(self, user, workflow, mandateId, featureInstanceId):
# ServiceCenterContext erstellen
self._serviceCenterContext = ServiceCenterContext(...)
# Core Interfaces laden
self.interfaceDbApp = getAppInterface(user, mandateId)
self.interfaceDbChat = getChatInterface(user, mandateId, featureInstanceId)
# Feature-Interfaces dynamisch laden (glob features/*/interfaceFeature*.py)
self._loadFeatureInterfaces()
# Feature-Services dynamisch laden (glob features/*/service*/mainService*.py)
self._loadFeatureServices()
def __getattr__(self, name):
# Lazy: shared services via getService(name, context) bei Erstzugriff
3. RBAC-Architektur
3.1 Zwei getrennte Template-Rollen-Systeme
WICHTIG: Es gibt zwei getrennte Template-Systeme — diese sind nicht dieselben:
┌────────────────────────────────────────────────────────────────────┐
│ TEMPLATE-ROLLEN (Global) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ System Templates (für Mandanten) Priority 1 │ │
│ │ isSystemRole=true, mandateId=null, featureCode=null │ │
│ │ │ │
│ │ "admin" → Mandant-Administration │ │
│ │ "user" → Standard-User │ │
│ │ "viewer" → Nur-Lesen │ │
│ │ │ │
│ │ → kopiert bei Mandant-Erstellung (copySystemRolesToMandate)│ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Feature Templates (für Feature-Instanzen) Priority 1 │ │
│ │ isSystemRole=false, mandateId=null, featureCode="workspace"│ │
│ │ │ │
│ │ "workspace-admin" → alle UI/RESOURCE/DATA Rechte │ │
│ │ "workspace-user" → eingeschränkte DATA Rechte │ │
│ │ "workspace-viewer" → nur View │ │
│ │ │ │
│ │ → kopiert bei Feature-Instanz-Erstellung (_copyTemplateRoles)│ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────┐
│ MANDATE-ROLLEN Priority 2 │
│ mandateId=X, featureInstanceId=null, featureCode=null │
│ │
│ "admin" (kopiert von System Template) │
│ "user" (kopiert von System Template) │
│ "viewer" (kopiert von System Template) │
│ → zugewiesen via UserMandate → UserMandateRole │
└────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────┐
│ INSTANZ-ROLLEN Priority 3 │
│ mandateId=X, featureInstanceId=Y, featureCode="workspace" │
│ │
│ "workspace-admin" (kopiert von Feature Template) │
│ "workspace-user" (kopiert von Feature Template) │
│ "workspace-viewer" (kopiert von Feature Template) │
│ → zugewiesen via FeatureAccess → FeatureAccessRole │
└────────────────────────────────────────────────────────────────────┘
3.2 Datenmodell RBAC
class Role(PowerOnModel):
# Scope bestimmt die Priorität:
mandateId: Optional[str] # null = Global/Template
featureInstanceId: Optional[str] # null = nicht Instanz-spezifisch
featureCode: Optional[str] # z.B. "workspace", "graphicalEditor"
roleLabel: str # z.B. "workspace-admin"
isSystemRole: bool # System-Rollen nicht löschbar
class AccessRule(PowerOnModel):
roleId: str # FK → Role
context: AccessRuleContext # DATA | UI | RESOURCE
item: Optional[str] # null=generic, oder spezifisch
view: bool
read: Optional[AccessLevel] # "n" | "o" | "m" | "a"
create: Optional[AccessLevel]
update: Optional[AccessLevel]
delete: Optional[AccessLevel]
class AccessLevel(str, Enum):
NONE = "n" # kein Zugriff
OWN = "o" # nur eigene Records
MANDATE = "m" # alle Records im Mandant
ALL = "a" # alle Records (System-Admin)
3.3 User-Zuweisung
User ──► UserMandate ──► UserMandateRole ──► Role (Mandate-Scope)
User ──► FeatureAccess ──► FeatureAccessRole ──► Role (Instanz-Scope)
class UserMandate(PowerOnModel):
userId: str
mandateId: str
class UserMandateRole(PowerOnModel):
userMandateId: str # FK → UserMandate
roleId: str # FK → Role (mandateId=X, featureInstanceId=null)
class FeatureAccess(PowerOnModel):
userId: str
featureInstanceId: str
enabled: bool
class FeatureAccessRole(PowerOnModel):
featureAccessId: str # FK → FeatureAccess
roleId: str # FK → Role (mandateId=X, featureInstanceId=Y)
3.4 Resolution-Algorithmus
1. Sammle roleIds:
- Mandate-Rollen: UserMandate → UserMandateRole → roleId
- Instanz-Rollen: FeatureAccess → FeatureAccessRole → roleId
2. Lade AccessRules für alle roleIds (context + item filter)
3. Bestimme Priorität pro Rolle:
- Priority 3: role.featureInstanceId gesetzt (Instanz)
- Priority 2: role.mandateId gesetzt (Mandant)
- Priority 1: beides null (Global/Template)
4. DATA-Permissions: Nur Rules der HÖCHSTEN Priorität zählen
View: OR über alle Rules der höchsten Priorität
Item-Spezifität: exact > prefix > generic (innerhalb Priorität)
5. SQL WHERE Clause: buildRbacWhereClause(permissions, user, table)
4. Unified Workflow Datenmodell
4.1 Entitäten
# Ziel: datamodels/datamodelWorkflowUnified.py oder
# features/graphicalEditor/datamodelFeatureGraphicalEditor.py
class WorkflowStatus(str, Enum):
DRAFT = "draft"
PUBLISHED = "published"
ARCHIVED = "archived"
class Workflow(PowerOnModel):
"""Workflow-Metadaten und Ownership."""
id: str
mandateId: str
featureInstanceId: str
label: str
description: Optional[str]
tags: List[str] = []
isTemplate: bool = False
templateSourceId: Optional[str] # geklont von diesem Template
currentVersionId: Optional[str] # aktive/published Version
active: bool = True # Scheduler-enabled
eventId: Optional[str] # APScheduler Job-ID (v1-Pattern)
class WorkflowVersion(PowerOnModel):
"""Immutable Snapshot eines Workflow-Graphen."""
id: str
workflowId: str # FK → Workflow
versionNumber: int # auto-increment
status: WorkflowStatus # draft | published | archived
graph: Dict[str, Any] # { nodes: [...], connections: [...] }
invocations: List[Dict[str, Any]] # Entry-Points (manual, schedule, webhook, form)
publishedAt: Optional[float]
publishedBy: Optional[str]
4.2 State Machine: WorkflowVersion.status
┌───────┐
create ───►│ DRAFT │◄─── edit (graph mutation via API oder AI Tools)
└───┬───┘
│ publish
▼
┌───────────┐
unpublish─►│ PUBLISHED │
(→ DRAFT) └─────┬─────┘
│ archive
▼
┌───────────┐
│ ARCHIVED │──► re-publish (→ PUBLISHED)
└───────────┘
Invariante: Pro Workflow maximal 1 Version mit status=PUBLISHED.
Scheduler nutzt immer die PUBLISHED Version.
4.3 Run-Modell
class RunStatus(str, Enum):
PENDING = "pending"
RUNNING = "running"
PAUSED = "paused"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
class WorkflowRun(PowerOnModel):
"""Einzelne Ausführung einer WorkflowVersion."""
id: str
workflowId: str # FK → Workflow
versionId: str # FK → WorkflowVersion
status: RunStatus
trigger: Dict[str, Any] # { type: "manual"|"schedule"|"webhook"|..., metadata }
startedAt: float
completedAt: Optional[float]
nodeOutputs: Dict[str, Any] # Outputs pro Node-ID
currentNodeId: Optional[str] # Paused bei diesem Node
resumeContext: Dict[str, Any] # Kontext für Resume
error: Optional[str] # Top-Level Fehler
costTokens: Optional[int] # Aggregierte Token-Kosten
costCredits: Optional[float] # Aggregierte Credit-Kosten
4.4 State Machine: WorkflowRun.status
┌─────────┐
executeGraph──►│ RUNNING │
└────┬────┘
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌──────────┐ ┌────────┐
│ PAUSED │ │COMPLETED │ │ FAILED │
└───┬────┘ └──────────┘ └────────┘
│
│ resume(taskResult | emailReceived)
▼
┌─────────┐
│ RUNNING │───► COMPLETED | FAILED | PAUSED | CANCELLED
└─────────┘
Pause-Gründe:
- input.* Node → HumanTask erstellt
- email.checkEmail → EmailWait (Background Poller)
Cancel:
- Manuell durch User
- Timeout bei HumanTask (expiresAt)
4.5 RunStepLog und HumanTask
class StepStatus(str, Enum):
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
SKIPPED = "skipped"
class RunStepLog(PowerOnModel):
"""Detaillierter Log pro Node-Execution."""
id: str
runId: str # FK → WorkflowRun
nodeId: str # Node-ID im Graph
nodeType: str # z.B. "ai.prompt", "email.checkEmail"
status: StepStatus
inputSnapshot: Dict[str, Any] # Parameters + Upstream-Daten bei Execution
output: Optional[Dict[str, Any]] # Node-Output
error: Optional[str]
startedAt: float
completedAt: Optional[float]
durationMs: Optional[int]
tokensUsed: Optional[int] # AI-Calls in diesem Step
retryCount: int = 0
class TaskStatus(str, Enum):
PENDING = "pending"
COMPLETED = "completed"
CANCELLED = "cancelled"
EXPIRED = "expired"
class HumanTask(PowerOnModel):
"""Aufgabe für menschliche Eingabe bei Pause."""
id: str
runId: str # FK → WorkflowRun
workflowId: str # FK → Workflow (Convenience-FK)
nodeId: str
nodeType: str # input.approval, input.form, etc.
config: Dict[str, Any] # Node-Parameter (Formular-Felder, Approval-Text)
assigneeId: str
status: TaskStatus
result: Optional[Dict[str, Any]]
expiresAt: Optional[float] # Timeout
4.6 State Machine: HumanTask.status
┌─────────┐
│ PENDING │ (erstellt bei Run-Pause)
└────┬────┘
│
├── complete(result) → COMPLETED (Run wird resumed mit result)
├── cancel() → CANCELLED (Run wird cancelled)
└── expiresAt passed → EXPIRED (Run wird cancelled oder Default)
4.7 ER-Diagramm
┌──────────────────┐ ┌──────────────────────┐
│ Workflow │ 1───N │ WorkflowVersion │
├──────────────────┤ ├──────────────────────┤
│ id │ │ id │
│ mandateId │ │ workflowId (FK) │
│ featureInstanceId│ │ versionNumber │
│ label │ │ status │
│ description │ │ graph {} │
│ tags [] │ │ invocations [] │
│ isTemplate │ │ publishedAt │
│ currentVersionId ├───1──►│ publishedBy │
│ active │ └──────────┬───────────┘
│ eventId │ │ 1:N
└──────────────────┘ ┌──────────▼───────────┐
│ WorkflowRun │
├──────────────────────┤
│ id │
│ workflowId (FK) │
│ versionId (FK) │
│ status │
│ trigger {} │
│ startedAt │
│ completedAt │
│ nodeOutputs {} │
│ currentNodeId │
│ resumeContext {} │
│ error │
│ costTokens │
│ costCredits │
└──────────┬───────────┘
1:N │ │ 1:N
┌─────────────┘ └──────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ RunStepLog │ │ HumanTask │
├──────────────────┤ ├──────────────────┤
│ id │ │ id │
│ runId (FK) │ │ runId (FK) │
│ nodeId │ │ workflowId (FK) │
│ nodeType │ │ nodeId │
│ status │ │ nodeType │
│ inputSnapshot {} │ │ config {} │
│ output {} │ │ assigneeId │
│ error │ │ status │
│ startedAt │ │ result {} │
│ completedAt │ │ expiresAt │
│ durationMs │ └──────────────────┘
│ tokensUsed │
│ retryCount │
└──────────────────┘
5. AI-Tool-Architektur: Toolbox-Datenmodell
5.1 Toolbox-Definition
# Ziel: serviceCenter/services/serviceAgent/toolboxRegistry.py
class ToolboxDefinition(BaseModel):
"""Definition einer thematischen Tool-Gruppe."""
id: str # z.B. "core", "email", "sharepoint", "workflow"
label: Dict[str, str] # Multilingual: {"en": "Email", "de": "E-Mail"}
description: str
featureCode: Optional[str] # null = Feature-unabhängig, "trustee" = Feature-spezifisch
tools: List[str] # Tool-Namen in dieser Toolbox
isDefault: bool = False # Immer aktiv (z.B. "core")
requiresConnection: Optional[str] # "outlook", "sharepoint" → auto-aktiviert bei Connection
class ToolboxRegistry:
"""Verwaltet Toolboxes und deren Zuordnung zu Tools."""
_toolboxes: Dict[str, ToolboxDefinition]
_toolRegistry: ToolRegistry # Referenz auf die bestehende ToolRegistry
def registerToolbox(self, toolbox: ToolboxDefinition): ...
def getActiveToolboxes(
self,
featureCode: str,
userConnections: List[str], # Aktive Connection-Typen des Users
explicitToolboxes: List[str], # Explizit aktivierte Toolboxes
) -> List[str]: ...
def getToolsForToolboxes(
self,
activeToolboxIds: List[str],
) -> List[ToolDefinition]: ...
5.2 AgentConfig (erweitert mit Eskalation)
class AgentConfig(BaseModel):
maxRounds: int = 25
maxCostCHF: Optional[float] = None
initialToolboxes: List[str] = ["core"] # Tools bei Runde 1
availableToolboxes: List[str] = [] # Via requestToolbox anforderbar
temperature: Optional[float] = None
5.3 Dynamische Toolbox-Eskalation
Kern-Idee: Der Agent startet mit einem kompakten Initial-Set (initialToolboxes). Er kennt den Katalog aller availableToolboxes. Wenn er Spezial-Tools braucht, ruft er requestToolbox(id) auf — die Tools werden in der nächsten Runde bereitgestellt.
Runde 1:
Aktive Tools: core (readFile, webSearch, ...) + requestToolbox
System-Prompt: "Verfügbare Toolboxes: email, sharepoint, ai, ..."
User: "Lies meine letzten Emails und fasse sie zusammen"
LLM: → requestToolbox("email")
Runde 2:
Aktive Tools: core + email (outlook_readEmails, ...) + requestToolbox
LLM: → outlook_readEmails(connectionRef, folder="Inbox")
Runde 3:
LLM: → (Text-Antwort mit Zusammenfassung) → COMPLETED
requestToolbox Meta-Tool:
{
"name": "requestToolbox",
"description": "Request specialized tools for the current task.",
"parameters": {
"toolboxId": {
"type": "string",
"enum": [...] # Dynamisch: nur availableToolboxes
},
"reason": {"type": "string"}
}
}
5.4 Toolbox-Registrierung (Architektur)
App Startup
│
├── _registerCoreTools(registry) → 40 Core-Tools in ToolRegistry
├── ActionToolAdapter.registerAll(registry) → dynamicMode Actions als Tools
│
└── ToolboxRegistry.registerAll()
├── Toolbox "core" → readFile, listFiles, webSearch, ... (isDefault=true)
├── Toolbox "ai" → summarizeContent, generateImage, ...
├── Toolbox "datasources" → browseDataSource, downloadFromDataSource, ...
├── Toolbox "email" → sendMail, outlook_readEmails, ... (requiresConnection="outlook")
├── Toolbox "sharepoint" → sharepoint_findDocumentPath, ... (requiresConnection="sharepoint")
├── Toolbox "clickup" → clickup_searchTasks, ... (requiresConnection="clickup")
├── Toolbox "jira" → jira_connectJira, ... (requiresConnection="jira")
├── Toolbox "workflow" → readWorkflowGraph, addNode, connectNodes, ...
├── Toolbox "trustee" → trustee_extractFromFiles, ... (featureCode="trustee")
└── Toolbox "chatbot" → chatbot_queryDatabase, ... (featureCode="chatbot")
Per Agent-Run:
│
├── Feature-Instanz Config → initialToolboxes + availableToolboxes
├── User Connections → filtert availableToolboxes (nur was User nutzen kann)
│
├── Runde 1: getToolsForToolboxes(initialToolboxes) + requestToolbox
│
└── Nach requestToolbox("email"):
Runde 2+: getToolsForToolboxes(initialToolboxes + ["email"]) + requestToolbox
5.5 Sub-Agent-Architektur
# Bestehendes Pattern: featureDataAgent.py
# Aufruf-Kette:
# Main Agent → Tool "queryFeatureInstance" → _queryFeatureInstance()
# → runFeatureDataAgent(question, featureInstanceId, ...)
# → Eigene ToolRegistry (browseTable, queryTable)
# → runAgentLoop(prompt, registry, config=AgentConfig(maxRounds=5))
# → Antwort zurück → ToolResult → Main Agent
Sub-Agents sind komplementär zu Toolboxes:
- Toolbox-Eskalation: Agent bekommt Tools direkt und nutzt sie selbst (z.B. Email-Tools)
- Sub-Agent: Agent delegiert an spezialisierten Mini-Agent mit eigenem Wissen (z.B. Trustee DB-Schema)
Faustregel: Toolbox wenn die Tools generisch sind. Sub-Agent wenn Feature-spezifisches Kontext-/Schema-Wissen nötig ist.
5.6 Toolbox-Zuordnung pro Feature
| Feature | initialToolboxes |
availableToolboxes (anforderbar) |
|---|---|---|
| Workspace | ["core"] |
["ai", "datasources", "email", "sharepoint", "clickup", "jira"] — gefiltert nach User-Connections |
| Graphical Editor Chat | ["core", "workflow"] |
["ai"] |
| CommCoach | ["core"] |
[] (keine Eskalation) |
| Chatbot | (Eigenes Tool-System) | — |
6. Graph-Struktur und Node-Definitionen
6.1 Graph-Schema (innerhalb WorkflowVersion.graph)
{
"nodes": [
{
"id": "node-uuid",
"type": "trigger.schedule",
"position": { "x": 100, "y": 200 },
"parameters": {
"cronExpression": "0 8 * * 1-5"
}
},
{
"id": "node-uuid-2",
"type": "ai.prompt",
"position": { "x": 300, "y": 200 },
"parameters": {
"prompt": "Analysiere die Emails und erstelle eine Zusammenfassung"
}
}
],
"connections": [
{
"source": "node-uuid",
"target": "node-uuid-2",
"sourceOutput": 0,
"targetInput": 0
}
]
}
6.2 Node-Definition-Schema
# Jede Node-Definition ist ein Dict mit folgender Struktur:
{
"id": "ai.prompt", # Unique Node-Type-ID
"category": "ai", # Kategorie (für Palette-Gruppierung)
"label": {"en": "Prompt", "de": "Prompt"}, # Multilingual
"description": {"en": "...", "de": "..."},
"parameters": [
{
"name": "prompt",
"type": "string",
"required": True,
"description": {"en": "AI prompt", "de": "KI-Prompt"}
}
],
"inputs": 1, # Anzahl Eingänge
"outputs": 1, # Anzahl Ausgänge
"meta": {"icon": "mdi-robot", "color": "#9C27B0"},
# Interne Felder (nicht an Frontend exponiert):
"_method": "ai", # Method für ActionExecutor
"_action": "process", # Action für ActionExecutor
"_paramMap": {"prompt": "aiPrompt"}, # Parameter-Mapping Node → Action
}
6.3 Node-Type zu Method/Action Mapping
| Node Type | _method |
_action |
_paramMap |
|---|---|---|---|
trigger.manual |
— | — | — (TriggerExecutor) |
trigger.schedule |
— | — | — (TriggerExecutor) |
trigger.form |
— | — | — (TriggerExecutor) |
flow.ifElse |
— | — | — (FlowExecutor) |
flow.switch |
— | — | — (FlowExecutor) |
flow.loop |
— | — | — (FlowExecutor) |
input.* |
— | — | — (InputExecutor → HumanTask) |
ai.prompt |
ai |
process |
prompt→aiPrompt |
ai.webResearch |
ai |
webResearch |
query→prompt |
ai.summarizeDocument |
ai |
summarizeDocument |
— |
ai.translateDocument |
ai |
translateDocument |
targetLanguage→targetLanguage |
ai.generateDocument |
ai |
generateDocument |
prompt→prompt |
email.checkEmail |
outlook |
readEmails |
connectionId→connectionReference |
email.searchEmail |
outlook |
searchEmails |
connectionId→connectionReference |
email.draftEmail |
outlook |
composeAndDraft... |
connectionId→connectionReference |
sharepoint.* |
sharepoint |
entsprechend | connectionId→connectionReference |
clickup.* |
clickup |
entsprechend | connectionId→connectionReference |
file.create |
file |
create |
template→template |
6.4 Execution-Routing
executeGraph(graph, services, ...)
│
├── parseGraph() → nodes, connections, nodeIds
├── validateGraph() → Konsistenzprüfung
├── topoSort(nodes, connectionMap) → geordnete Node-Liste
│
└── Pro Node:
│
├── nodeType.startsWith("trigger.") → TriggerExecutor
│ → runEnvelope passthrough
│
├── nodeType.startsWith("flow.") → FlowExecutor
│ → ifElse: evaluiert Bedingung, setzt active path
│ → switch: evaluiert Match, setzt active path
│ → loop: iteriert mit _loopState
│
├── nodeType.startsWith("input.") → InputExecutor
│ → erstellt HumanTask
│ → setzt Run auf PAUSED
│ → raise PauseForHumanTaskError
│
└── nodeType.startsWith("ai." | "email." | "sharepoint." | ...) → ActionNodeExecutor
│
├── _getNodeDefinition(nodeType) → _method, _action, _paramMap
├── Parameter-Mapping (Node-Params → Action-Params)
├── Upstream-Daten mergen (documents von vorherigen Nodes)
└── ActionExecutor.executeAction(method, action, params)
7. Scheduler-Architektur (konsolidiert)
7.1 Konsolidierter Scheduler (Ziel)
# Ziel: workflows/scheduler/mainScheduler.py
# Übernimmt die besten Patterns aus v1 und v2
class WorkflowScheduler:
"""Konsolidierter Scheduler für Workflow-Automationen."""
def start(self, eventUser):
"""Startup: Initial Sync + Delayed Sync + Callback Registration."""
eventManager.start()
self._syncScheduledWorkflows(eventUser)
self._registerDelayedSync(eventUser, delaySeconds=5)
callbackRegistry.register("workflow.changed",
lambda _: self._syncScheduledWorkflows(eventUser))
def stop(self, eventUser):
"""Shutdown: Cleanup."""
pass # APScheduler handles job cleanup
def _syncScheduledWorkflows(self, eventUser):
"""Inkrementeller Sync (v1-Pattern)."""
workflows = self._getAllSchedulableWorkflows()
for wf in workflows:
jobId = f"workflow.{wf.id}"
if wf.active and wf.currentVersionId:
# Register/Replace Job
version = self._getPublishedVersion(wf.currentVersionId)
cronKwargs = self._extractSchedule(version)
if cronKwargs:
handler = self._createHandler(wf, version, eventUser)
eventManager.registerCron(
jobId=jobId,
func=handler,
cronKwargs=cronKwargs,
replaceExisting=True # v1-Pattern: atomic replace
)
# Persist eventId on Workflow (v1-Pattern: debugging)
if wf.eventId != jobId:
self._updateEventId(wf.id, jobId)
else:
# Deactivated: Remove Job + Clear eventId
self._removeJob(jobId)
if wf.eventId:
self._updateEventId(wf.id, None)
def _createHandler(self, wf, version, eventUser):
"""Handler: Reload + Active-Check + Execute (v1-Pattern)."""
async def handler():
# Reload Workflow (v1-Pattern: Zustand könnte sich geändert haben)
current = self._getWorkflow(wf.id)
if not current or not current.active:
return
# Execute published version
services = self._buildServices(current, eventUser)
result = await executeGraph(
graph=version.graph,
services=services,
workflowId=current.id,
...
)
# Execution Log (v1-Pattern: capped audit trail)
self._appendExecutionLog(current.id, result)
# Thread-Bridge (v2-Pattern)
return self._wrapAsync(handler)
7.2 Scheduler State Machine
Workflow.active=false ──► UNREGISTERED (kein APScheduler Job)
│
│ activate + published version mit schedule invocation
▼
Workflow.active=true ──► REGISTERED (eventId gesetzt, Job aktiv)
│
├── Cron fires → handler() → executeGraph → WorkflowRun
│
├── Workflow deactivated → remove job → UNREGISTERED (eventId=null)
│
├── Schedule geändert → replaceExisting=true → REGISTERED (neuer Trigger)
│
└── Workflow gelöscht → remove job → (Workflow gelöscht)
8. Backend Code-Struktur (Ziel)
gateway/modules/
│
├── features/
│ ├── graphicalEditor/ ◄── NEUES FEATURE (ersetzt automation2)
│ │ ├── mainGraphicalEditor.py
│ │ │ FEATURE_CODE = "graphicalEditor"
│ │ │ UI_OBJECTS = [
│ │ │ "ui.feature.graphicalEditor.editor",
│ │ │ "ui.feature.graphicalEditor.workflows",
│ │ │ "ui.feature.graphicalEditor.tasks",
│ │ │ ]
│ │ │ RESOURCE_OBJECTS = [
│ │ │ "resource.feature.graphicalEditor.execute",
│ │ │ "resource.feature.graphicalEditor.schedule",
│ │ │ ]
│ │ │ TEMPLATE_ROLES = [
│ │ │ "graphicalEditor-admin",
│ │ │ "graphicalEditor-user",
│ │ │ "graphicalEditor-viewer",
│ │ │ ]
│ │ │
│ │ ├── routeFeatureGraphicalEditor.py
│ │ │ router prefix: /api/workflows
│ │ │ Endpoints: CRUD Workflows, Versions, Execute, Runs, Tasks, NodeTypes, Chat
│ │ │
│ │ ├── interfaceFeatureGraphicalEditor.py
│ │ │ DB: poweron_workflows (oder poweron_automation2 renamed)
│ │ │ Tables: Workflow, WorkflowVersion, WorkflowRun, RunStepLog, HumanTask
│ │ │
│ │ ├── datamodelFeatureGraphicalEditor.py
│ │ │ Workflow, WorkflowVersion, WorkflowRun, RunStepLog, HumanTask
│ │ │ WorkflowStatus, RunStatus, StepStatus, TaskStatus (State Machines)
│ │ │
│ │ └── nodeDefinitions/
│ │ triggers.py, flow.py, input.py, ai.py, email.py, sharepoint.py, clickup.py, file.py
│ │
│ ├── workspace/ ◄── BLEIBT (AI Chat mit UDB)
│ ├── automation/ ◄── BLEIBT bis Ablösung durch graphicalEditor
│ ├── commcoach/ ◄── BLEIBT
│ ├── chatbot/ ◄── BLEIBT
│ ├── trustee/ ◄── BLEIBT
│ └── ...
│
├── serviceCenter/services/
│ ├── serviceAgent/
│ │ ├── mainServiceAgent.py ◄── Core Tools + Toolbox-Tagging
│ │ ├── agentLoop.py ◄── Toolbox-Filtering statt toolSet
│ │ ├── toolRegistry.py ◄── BLEIBT
│ │ ├── toolboxRegistry.py ◄── NEU: Toolbox-Verwaltung
│ │ ├── actionToolAdapter.py ◄── BLEIBT (+ Toolbox-Zuordnung)
│ │ ├── featureDataAgent.py ◄── BLEIBT (Sub-Agent Pattern)
│ │ └── datamodelAgent.py ◄── toolboxes: List[str] statt toolSet
│ └── ...
│
├── workflows/
│ ├── methods/ ◄── BLEIBT (Unified Action Library)
│ ├── processing/ ◄── BLEIBT (ActionExecutor, methodDiscovery)
│ ├── automation2/ ◄── Graph Engine BLEIBT (execution, executors, graphUtils)
│ ├── scheduler/ ◄── NEU (konsolidierter Scheduler)
│ │ └── mainScheduler.py
│ ├── automation/ ◄── BLEIBT bis Ablösung
│ └── workflowManager.py ◄── BLEIBT (für Workspace Dynamic Mode)
│
└── ...
9. Frontend Code-Struktur (Ziel)
frontend_nyla/src/
│
├── components/ ◄── SHARED COMPONENTS
│ ├── ChatBar/ ◄── NEU (extrahiert aus WorkspaceInput)
│ │ ├── ChatBar.tsx Props: onSend, onStop, onVoice,
│ │ │ fileAttachments, showProviderSelector,
│ │ │ showVoice, placeholder
│ │ ├── ChatBar.module.css
│ │ └── index.ts
│ │
│ ├── ChatStream/ ◄── NEU (extrahiert aus workspace/ChatStream)
│ │ ├── ChatStream.tsx SSE-driven message stream, wiederverwendbar
│ │ └── index.ts
│ │
│ ├── UnifiedDataBar/ ◄── BLEIBT
│ │
│ ├── FlowEditor/ ◄── RENAMED von Automation2FlowEditor
│ │ ├── editor/
│ │ │ ├── FlowEditor.tsx + AI Chat Panel (ChatBar + ChatStream)
│ │ │ ├── FlowCanvas.tsx + Visual Run Tracing
│ │ │ ├── EditorChatPanel.tsx ◄── NEU
│ │ │ └── ...
│ │ └── ...
│ │
│ └── ...
│
├── pages/views/
│ ├── workspace/
│ │ ├── WorkspacePage.tsx Nutzt ChatBar, ChatStream, UDB
│ │ └── ...
│ │
│ ├── graphicalEditor/ ◄── RENAMED von automation2
│ │ ├── GraphicalEditorPage.tsx FlowEditor + UDB + ChatPanel
│ │ ├── WorkflowsListPage.tsx
│ │ └── TasksPage.tsx
│ │
│ └── ...
│
├── api/
│ ├── workflowApi.ts ◄── Konsolidiert (ersetzt automationApi + automation2Api)
│ └── ...
│
└── ...
10. Zusammenfassung: Architektur-Prinzipien
| Prinzip | Umsetzung |
|---|---|
| Feature-Container-Pattern | Jedes Feature folgt dem gleichen Verzeichnis-Pattern mit automatischer Discovery. Kein manuelles Wiring. |
| RBAC auf zwei Ebenen | Mandate-Rollen (generell) + Instanz-Rollen (feature-spezifisch). Höchste Priorität gewinnt. |
| State Machines als Grundlage | WorkflowVersion, WorkflowRun, HumanTask und Scheduler haben klar definierte Zustände und Übergänge. |
| Unified Action Library | Eine Method/Action-Library für alle Consumers (Agent Tools, Graph Nodes, Workflow Processor). |
| Toolboxes statt flache Tool-Liste | Thematisch gruppierte Tools, kontextabhängig aktiviert. Skaliert auf 100+ Tools. |
| Sub-Agents für Feature-Daten | Main Agent bleibt schlank. Feature-spezifisches Wissen in dedizierten Sub-Agents. |
| Keine Migration | Direkte Implementierung auf Automation2-Basis. v1 bleibt bis Ablösung. |
| Scheduler konsolidiert | Beste Patterns aus v1 (inkrementell, eventId, reload+check) und v2 (interval, thread-bridge). |