wiki/b-reference/gateway/automation-data-model.md

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).