466 lines
17 KiB
Markdown
466 lines
17 KiB
Markdown
# Automation Templates – Gateway Implementation Concept
|
||
|
||
**Basis:** `doc_automation_templates_db_and_editor_concept.md`
|
||
|
||
---
|
||
|
||
## ⚠️ STATUS: BEREITS IMPLEMENTIERT
|
||
|
||
**Das Backend ist vollständig implementiert!** Folgende Komponenten existieren bereits:
|
||
|
||
| Komponente | Status | Datei |
|
||
|------------|--------|-------|
|
||
| AutomationTemplate Model | ✅ Vorhanden | `datamodelFeatureAutomation.py:49` |
|
||
| RBAC Namespace | ✅ Vorhanden | `interfaceRbac.py:69` |
|
||
| API Routes (CRUD) | ✅ Vorhanden | `routeFeatureAutomation.py:391-607` |
|
||
| Interface Methods | ✅ Vorhanden | `interfaceFeatureAutomation.py:411-575` |
|
||
| Navigation Entry | ✅ Vorhanden | `mainSystem.py:96-103` |
|
||
|
||
**Was noch fehlt:**
|
||
- Bootstrap-Funktion für initiale Template-Migration (optional)
|
||
|
||
**Actions-Katalog:** ✅ Bereits vorhanden unter `GET /api/automations/actions` (Zeile 293)
|
||
|
||
---
|
||
|
||
## 1. Bestehendes Datenmodell: AutomationTemplate
|
||
|
||
### 1.1 Datei: `gateway/modules/features/automation/datamodelFeatureAutomation.py`
|
||
|
||
Neues Modell **AutomationTemplate** hinzufügen (neben bestehendem `AutomationDefinition`):
|
||
|
||
```python
|
||
from modules.datamodels.datamodelUtils import TextMultilingual
|
||
|
||
class AutomationTemplate(BaseModel):
|
||
"""Automation-Vorlage ohne scharfe Placeholder-Werte."""
|
||
id: str = Field(
|
||
default_factory=lambda: str(uuid.uuid4()),
|
||
description="Primary key",
|
||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True}
|
||
)
|
||
label: TextMultilingual = Field(
|
||
description="Template name (multilingual)",
|
||
json_schema_extra={"frontend_type": "multilingual", "frontend_required": True}
|
||
)
|
||
overview: Optional[TextMultilingual] = Field(
|
||
None,
|
||
description="Short description (multilingual)",
|
||
json_schema_extra={"frontend_type": "multilingual", "frontend_required": False}
|
||
)
|
||
template: str = Field(
|
||
description="JSON workflow structure with {{KEY:...}} placeholders",
|
||
json_schema_extra={"frontend_type": "textarea", "frontend_required": True}
|
||
)
|
||
# System fields (_createdAt, _createdBy, etc.) werden automatisch vom DB-Connector gesetzt
|
||
|
||
|
||
registerModelLabels(
|
||
"AutomationTemplate",
|
||
{"en": "Automation Template", "de": "Automation-Vorlage", "fr": "Modèle d'automatisation"},
|
||
{
|
||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
||
"label": {"en": "Label", "de": "Bezeichnung", "fr": "Libellé"},
|
||
"overview": {"en": "Overview", "de": "Übersicht", "fr": "Aperçu"},
|
||
"template": {"en": "Template", "de": "Vorlage", "fr": "Modèle"},
|
||
},
|
||
)
|
||
```
|
||
|
||
### 1.2 Namespace & Tabelle
|
||
|
||
- **Namespace:** `data.automation` (wie AutomationDefinition)
|
||
- **Tabelle:** `AutomationTemplate`
|
||
- Registrierung in `interfaceRbac.py` (TABLE_NAMESPACE_MAP):
|
||
|
||
```python
|
||
"AutomationTemplate": "automation",
|
||
```
|
||
|
||
---
|
||
|
||
## 2. RBAC für AutomationTemplate
|
||
|
||
### 2.1 Bootstrap-Regeln (interfaceBootstrap.py)
|
||
|
||
In `_createTableSpecificRules()` hinzufügen:
|
||
|
||
```python
|
||
# AutomationTemplate: MY-level (user-owned), like AutomationDefinition
|
||
for roleId in [adminId, userId]:
|
||
if roleId:
|
||
tableRules.append(AccessRule(
|
||
roleId=roleId,
|
||
context=AccessRuleContext.DATA,
|
||
item="data.automation.AutomationTemplate",
|
||
view=True,
|
||
read=AccessLevel.MY,
|
||
create=AccessLevel.MY,
|
||
update=AccessLevel.MY,
|
||
delete=AccessLevel.MY,
|
||
))
|
||
if viewerId:
|
||
tableRules.append(AccessRule(
|
||
roleId=viewerId,
|
||
context=AccessRuleContext.DATA,
|
||
item="data.automation.AutomationTemplate",
|
||
view=True,
|
||
read=AccessLevel.MY,
|
||
create=AccessLevel.NONE,
|
||
update=AccessLevel.NONE,
|
||
delete=AccessLevel.NONE,
|
||
))
|
||
```
|
||
|
||
In `_ensureDataContextRules()` (Zeile ~845) ergänzen:
|
||
|
||
```python
|
||
"data.automation.AutomationTemplate",
|
||
```
|
||
|
||
### 2.2 UI-Regeln
|
||
|
||
In `mainAutomation.py` bereits vorhanden:
|
||
```python
|
||
{"objectKey": "ui.feature.automation.templates", ...}
|
||
```
|
||
Sichtbarkeit gemäss RBAC (alle mit Berechtigung; nicht nur SysAdmin).
|
||
|
||
### 2.3 Navigation (mainSystem.py)
|
||
|
||
In `NAVIGATION_SECTIONS` unter dem "workflows" Abschnitt hinzufügen:
|
||
|
||
```python
|
||
{
|
||
"id": "automation-templates",
|
||
"objectKey": "ui.system.automation-templates",
|
||
"label": {"en": "Templates", "de": "Vorlagen", "fr": "Modèles"},
|
||
"icon": "FaFileAlt",
|
||
"path": "/workflows/automation-templates",
|
||
"order": 35, # Nach automations (30)
|
||
"public": True,
|
||
},
|
||
```
|
||
|
||
**WICHTIG:** Das `objectKey` Format für Navigation ist `ui.system.xxx`, nicht `page.system.xxx`!
|
||
|
||
---
|
||
|
||
## 3. API Routes für AutomationTemplate
|
||
|
||
### 3.1 Neue Route-Datei oder erweitern: `routeFeatureAutomation.py`
|
||
|
||
Endpoints unter `/api/automation-templates` (oder `/api/automations/templates` erweitern):
|
||
|
||
```python
|
||
from modules.features.automation.datamodelFeatureAutomation import AutomationTemplate
|
||
|
||
# GET /api/automation-templates - Liste (RBAC-gefiltert)
|
||
@router.get("/templates", response_model=PaginatedResponse[AutomationTemplate])
|
||
async def get_templates(
|
||
request: Request,
|
||
pagination: Optional[str] = Query(None),
|
||
context: RequestContext = Depends(getRequestContext)
|
||
):
|
||
"""Get automation templates, filtered by RBAC (MY = own templates)."""
|
||
chatInterface = getChatInterface(context.user, ...)
|
||
result = chatInterface.getAllAutomationTemplates(pagination=paginationParams)
|
||
return JSONResponse(content=result)
|
||
|
||
# GET /api/automation-templates/{id}
|
||
@router.get("/templates/{templateId}", response_model=AutomationTemplate)
|
||
async def get_template(templateId: str, context: RequestContext = Depends(getRequestContext)):
|
||
chatInterface = getChatInterface(context.user, ...)
|
||
template = chatInterface.getAutomationTemplate(templateId)
|
||
if not template:
|
||
raise HTTPException(404, "Template not found")
|
||
return template
|
||
|
||
# POST /api/automation-templates
|
||
@router.post("/templates", response_model=AutomationTemplate)
|
||
async def create_template(
|
||
request: Request,
|
||
templateData: Dict[str, Any] = Body(...),
|
||
context: RequestContext = Depends(getRequestContext)
|
||
):
|
||
chatInterface = getChatInterface(context.user, ...)
|
||
return chatInterface.createAutomationTemplate(templateData)
|
||
|
||
# PUT /api/automation-templates/{id}
|
||
@router.put("/templates/{templateId}", response_model=AutomationTemplate)
|
||
async def update_template(
|
||
templateId: str,
|
||
templateData: Dict[str, Any] = Body(...),
|
||
context: RequestContext = Depends(getRequestContext)
|
||
):
|
||
chatInterface = getChatInterface(context.user, ...)
|
||
return chatInterface.updateAutomationTemplate(templateId, templateData)
|
||
|
||
# DELETE /api/automation-templates/{id}
|
||
@router.delete("/templates/{templateId}")
|
||
async def delete_template(templateId: str, context: RequestContext = Depends(getRequestContext)):
|
||
chatInterface = getChatInterface(context.user, ...)
|
||
success = chatInterface.deleteAutomationTemplate(templateId)
|
||
if not success:
|
||
raise HTTPException(404, "Template not found or no permission")
|
||
return {"success": True}
|
||
```
|
||
|
||
### 3.2 Interface-Methoden (interfaceDbChat.py)
|
||
|
||
Analog zu `getAllAutomationDefinitions`, `createAutomationDefinition`, etc.:
|
||
|
||
```python
|
||
def getAllAutomationTemplates(self, pagination=None) -> Union[List[Dict], PaginatedResult]:
|
||
"""Returns templates filtered by RBAC (MY = own templates)."""
|
||
filteredTemplates = getRecordsetWithRBAC(self.db, AutomationTemplate, self.currentUser)
|
||
# ... pagination, enrichment
|
||
return filteredTemplates
|
||
|
||
def getAutomationTemplate(self, templateId: str) -> Optional[AutomationTemplate]:
|
||
filtered = getRecordsetWithRBAC(self.db, AutomationTemplate, self.currentUser, recordFilter={"id": templateId})
|
||
return AutomationTemplate(**filtered[0]) if filtered else None
|
||
|
||
def createAutomationTemplate(self, templateData: Dict) -> AutomationTemplate:
|
||
if not self.checkRbacPermission(AutomationTemplate, "create"):
|
||
raise ValueError("No permission to create template")
|
||
simpleFields, _ = self._separateObjectFields(AutomationTemplate, templateData)
|
||
created = self.db.recordCreate(AutomationTemplate, simpleFields)
|
||
return AutomationTemplate(**created)
|
||
|
||
def updateAutomationTemplate(self, templateId: str, templateData: Dict) -> AutomationTemplate:
|
||
existing = self.getAutomationTemplate(templateId)
|
||
if not existing:
|
||
raise ValueError("Template not found")
|
||
if not self.checkRbacPermission(AutomationTemplate, "update", templateId):
|
||
raise ValueError("No permission to update")
|
||
simpleFields, _ = self._separateObjectFields(AutomationTemplate, templateData)
|
||
updated = self.db.recordModify(AutomationTemplate, templateId, simpleFields)
|
||
return AutomationTemplate(**updated)
|
||
|
||
def deleteAutomationTemplate(self, templateId: str) -> bool:
|
||
existing = self.getAutomationTemplate(templateId)
|
||
if not existing:
|
||
return False
|
||
if not self.checkRbacPermission(AutomationTemplate, "delete", templateId):
|
||
raise ValueError("No permission to delete")
|
||
self.db.recordDelete(AutomationTemplate, templateId)
|
||
return True
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Bootstrap: Template-Seed
|
||
|
||
### 4.1 Neue Funktion in interfaceBootstrap.py
|
||
|
||
```python
|
||
def initAutomationTemplates(db: DatabaseConnector) -> None:
|
||
"""
|
||
Seed initial automation templates from subAutomationTemplates.py.
|
||
Only runs if no templates exist yet (bootstrap).
|
||
Creates templates with _createdBy = admin user (SysAdmin privilege).
|
||
"""
|
||
from modules.features.automation.subAutomationTemplates import AUTOMATION_TEMPLATES
|
||
from modules.features.automation.datamodelFeatureAutomation import AutomationTemplate
|
||
|
||
# Check if templates already exist
|
||
existing = db.getRecordset(AutomationTemplate)
|
||
if existing:
|
||
logger.info(f"Automation templates already seeded ({len(existing)} templates)")
|
||
return
|
||
|
||
# Get admin user ID for _createdBy
|
||
adminUsers = db.getRecordset(UserInDB, {"email": APP_CONFIG.ADMIN_EMAIL})
|
||
adminUserId = adminUsers[0]["id"] if adminUsers else None
|
||
|
||
templates = AUTOMATION_TEMPLATES.get("sets", [])
|
||
for i, templateSet in enumerate(templates):
|
||
templateContent = templateSet.get("template", {})
|
||
overview = templateContent.get("overview", f"Template {i+1}")
|
||
|
||
# Create multilingual label from overview (German as primary since current templates are German)
|
||
label = {"en": overview, "de": overview}
|
||
|
||
# Create template WITHOUT parameters (no sharp values)
|
||
templateData = {
|
||
"label": label,
|
||
"overview": {"en": overview, "de": overview},
|
||
"template": json.dumps(templateContent), # Only template JSON with {{KEY:...}}
|
||
}
|
||
|
||
# Set _createdBy to admin for bootstrap
|
||
if adminUserId:
|
||
templateData["_createdBy"] = adminUserId
|
||
|
||
db.recordCreate(AutomationTemplate, templateData)
|
||
logger.info(f"Created automation template: {overview}")
|
||
|
||
logger.info(f"Seeded {len(templates)} automation templates")
|
||
```
|
||
|
||
### 4.2 In initBootstrap() aufrufen
|
||
|
||
```python
|
||
def initBootstrap(db: DatabaseConnector) -> None:
|
||
# ... existing code ...
|
||
|
||
# Seed automation templates (after admin user exists)
|
||
initAutomationTemplates(db)
|
||
|
||
logger.info("System bootstrap completed")
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Actions-Katalog Endpoint
|
||
|
||
### 5.1 Neuer Endpoint: GET /api/automations/actions
|
||
|
||
```python
|
||
from modules.workflows.processing.shared.methodDiscovery import discoverMethods, methods
|
||
|
||
@router.get("/actions")
|
||
async def get_available_actions(
|
||
request: Request,
|
||
context: RequestContext = Depends(getRequestContext)
|
||
):
|
||
"""
|
||
Get available workflow actions filtered by RBAC.
|
||
Returns action definitions with parameters and example JSON snippets.
|
||
"""
|
||
# Ensure methods are discovered
|
||
if not methods:
|
||
# Need a serviceCenter with current user for RBAC filtering
|
||
# This requires a lightweight serviceCenter or direct method iteration
|
||
pass
|
||
|
||
actionsList = []
|
||
for methodName, methodInfo in methods.items():
|
||
# Skip duplicate short names (e.g., "ai" and "AiMethod" are same)
|
||
if methodName != methodName.lower():
|
||
continue
|
||
|
||
methodInstance = methodInfo.get("instance")
|
||
if not methodInstance:
|
||
continue
|
||
|
||
for actionName, actionDef in methodInstance._actions.items():
|
||
actionId = actionDef.actionId
|
||
|
||
# RBAC check: user needs view permission on this action (RESOURCE context)
|
||
permissions = context.rbac.getUserPermissions(
|
||
user=context.user,
|
||
context=AccessRuleContext.RESOURCE,
|
||
item=actionId
|
||
)
|
||
if not permissions.view:
|
||
continue
|
||
|
||
# Build action info from WorkflowActionDefinition
|
||
actionInfo = {
|
||
"method": methodName,
|
||
"action": actionName,
|
||
"actionId": actionId,
|
||
"description": actionDef.description,
|
||
"category": actionDef.category,
|
||
"parameters": []
|
||
}
|
||
|
||
# Add parameters from WorkflowActionParameter
|
||
for paramName, paramDef in actionDef.parameters.items():
|
||
actionInfo["parameters"].append({
|
||
"name": paramName,
|
||
"type": paramDef.type,
|
||
"frontendType": paramDef.frontendType.value if paramDef.frontendType else "text",
|
||
"required": paramDef.required,
|
||
"default": paramDef.default,
|
||
"description": paramDef.description,
|
||
"frontendOptions": paramDef.frontendOptions,
|
||
})
|
||
|
||
# Build example JSON snippet for copy/paste
|
||
exampleParams = {}
|
||
for paramName, paramDef in actionDef.parameters.items():
|
||
if paramDef.required:
|
||
exampleParams[paramName] = f"{{{{KEY:{paramName}}}}}"
|
||
else:
|
||
exampleParams[paramName] = paramDef.default or f"{{{{KEY:{paramName}}}}}"
|
||
|
||
actionInfo["exampleJson"] = {
|
||
"execMethod": methodName,
|
||
"execAction": actionName,
|
||
"execParameters": exampleParams,
|
||
"execResultLabel": f"{methodName}_{actionName}_result"
|
||
}
|
||
|
||
actionsList.append(actionInfo)
|
||
|
||
return JSONResponse(content={"actions": actionsList})
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Label in User-Sprache bei Definition-Erstellung
|
||
|
||
### 6.1 Bei "Aus Template erstellen" (Frontend ruft Backend)
|
||
|
||
Wenn das Frontend eine neue AutomationDefinition aus einem Template erstellt, sendet es:
|
||
- `template` (JSON von AutomationTemplate.template)
|
||
- `label` (aus AutomationTemplate.label in User-Sprache extrahiert)
|
||
|
||
**Backend-seitig** (falls Backend den Label-Extract macht):
|
||
|
||
```python
|
||
def createAutomationFromTemplate(self, templateId: str, userLanguage: str = "en") -> AutomationDefinition:
|
||
"""Create a new AutomationDefinition from a template, label in user's language."""
|
||
template = self.getAutomationTemplate(templateId)
|
||
if not template:
|
||
raise ValueError("Template not found")
|
||
|
||
# Extract label in user's language
|
||
labelMulti = template.label # TextMultilingual object
|
||
if hasattr(labelMulti, 'get_text'):
|
||
label = labelMulti.get_text(userLanguage)
|
||
elif isinstance(labelMulti, dict):
|
||
label = labelMulti.get(userLanguage) or labelMulti.get("en", "New Automation")
|
||
else:
|
||
label = str(labelMulti)
|
||
|
||
# Create definition with template content
|
||
definitionData = {
|
||
"label": label,
|
||
"template": template.template, # Copy template JSON
|
||
"placeholders": {}, # Empty - user fills in later
|
||
"schedule": "0 22 * * *", # Default schedule
|
||
"active": False,
|
||
}
|
||
|
||
return self.createAutomationDefinition(definitionData)
|
||
```
|
||
|
||
**Alternative:** Frontend extrahiert Label selbst und sendet direkt an `createAutomationDefinition`.
|
||
|
||
---
|
||
|
||
## 7. Zusammenfassung der Änderungen
|
||
|
||
| Datei | Änderung |
|
||
|-------|----------|
|
||
| `datamodelFeatureAutomation.py` | Neues Modell `AutomationTemplate` mit `TextMultilingual` für label/overview |
|
||
| `interfaceRbac.py` | TABLE_NAMESPACE_MAP erweitern: `"AutomationTemplate": "automation"` |
|
||
| `interfaceBootstrap.py` | RBAC-Regeln für AutomationTemplate (MY); Bootstrap-Seed `initAutomationTemplates()` |
|
||
| `interfaceDbChat.py` | CRUD-Methoden für AutomationTemplate (analog AutomationDefinition) |
|
||
| `routeFeatureAutomation.py` | Endpoints: GET/POST/PUT/DELETE `/api/automation-templates/*`, GET `/api/automations/actions` |
|
||
| `mainAutomation.py` | UI-Object `ui.feature.automation.templates` bereits vorhanden |
|
||
|
||
---
|
||
|
||
## 8. Abhängigkeiten & Reihenfolge
|
||
|
||
1. **Datamodel** erstellen (AutomationTemplate)
|
||
2. **RBAC** in interfaceRbac.py und interfaceBootstrap.py
|
||
3. **Interface-Methoden** in interfaceDbChat.py
|
||
4. **Routes** in routeFeatureAutomation.py
|
||
5. **Bootstrap-Seed** in interfaceBootstrap.py (nach DB-Migration)
|
||
6. **Actions-Endpoint** als letzte Erweiterung
|