994 lines
44 KiB
Markdown
994 lines
44 KiB
Markdown
# PowerOn Automation Unification — Datenmodell & Architektur
|
|
|
|
**Version:** 2.0
|
|
**Datum:** 2026-04-05
|
|
**Referenz:** [Automation Business Spec](./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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
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)
|
|
```
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
# 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)
|
|
|
|
```python
|
|
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:**
|
|
|
|
```python
|
|
{
|
|
"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
|
|
|
|
```python
|
|
# 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)
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```python
|
|
# 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)
|
|
|
|
```python
|
|
# 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). |
|