diff --git a/appdoc/ai_plan_architecture.md b/appdoc/ai_plan_architecture.md index 6f0f08b..3b1ad9b 100644 --- a/appdoc/ai_plan_architecture.md +++ b/appdoc/ai_plan_architecture.md @@ -705,14 +705,14 @@ async def callAiPlanning( **Characteristics**: - Returns unified `AiResponse` model - Uses `parseJsonWithModel()` for structured parsing -- modelName, priceUsd, processingTime tracked separately (not in response) +- modelName, priceCHF, processingTime tracked separately (not in response) **Calls**: 1. `self._buildPromptWithPlaceholders(prompt, placeholdersDict)` → `str` 2. `AiCallRequest(prompt=fullPrompt, context="", options=options)` - Create request 3. `self.aiObjects.call(request)` → `AiCallResponse` 4. `AiResponse(content=response.content, metadata=None)` → `AiResponse` ✅ - - Note: modelName, priceUsd, processingTime tracked separately (e.g., ChatStat) + - Note: modelName, priceCHF, processingTime tracked separately (e.g., ChatStat) --- diff --git a/appdoc/analysis_ai_workflow.md b/appdoc/analysis_ai_workflow.md index 1e03f77..52f1d80 100644 --- a/appdoc/analysis_ai_workflow.md +++ b/appdoc/analysis_ai_workflow.md @@ -694,7 +694,7 @@ async def processContentPartsWithAi( return AiCallResponse( content=mergedContent, modelName="multiple", - priceUsd=sum(r.priceUsd for r in allResults), + priceCHF=sum(r.priceCHF for r in allResults), processingTime=sum(r.processingTime for r in allResults), bytesSent=sum(r.bytesSent for r in allResults), bytesReceived=sum(r.bytesReceived for r in allResults), diff --git a/appdoc/doc_dev_workflow.md b/appdoc/doc_dev_workflow.md index 62e0994..ecb32aa 100644 --- a/appdoc/doc_dev_workflow.md +++ b/appdoc/doc_dev_workflow.md @@ -901,7 +901,7 @@ AiCallRequest: AiCallResponse: - content: str - modelName: str - - priceUsd: float + - priceCHF: float - processingTime: float - bytesSent: int - bytesReceived: int diff --git a/archiv/implementation_content_handling_with_dynamic_ai.md b/archiv/implementation_content_handling_with_dynamic_ai.md index 64b4fee..2cd72a7 100644 --- a/archiv/implementation_content_handling_with_dynamic_ai.md +++ b/archiv/implementation_content_handling_with_dynamic_ai.md @@ -70,7 +70,7 @@ G: Idea │ │ │ ├── _callWithModel(model, prompt, context, temperature, maxTokens, inputBytes) │ │ │ ├── If fails → try next model in fallback list │ │ │ └── If all fail → return error -│ │ └── Returns: AiCallResponse with content, modelName, priceUsd, etc. +│ │ └── Returns: AiCallResponse with content, modelName, priceCHF, etc. @@ -106,7 +106,7 @@ The architecture for interfaceAiObjects.call + interfaceAiObjects.callImage chan 5.4. chunking: if chunkItem size bigger than models maxSize: to produce chunks for the chunkItem, then replace the chunkItem in pipelineContentPartChunks[] with the new chunks 5.5. processing: process chunkItem's in pipelineContentPartChunks and write each processed extraction into processedContentPartChunks and remove chunkItem in pipelineContentPartChunks. This to loop until model fails (to increment model fail counter for the model, to increment model index number, then next loop) or no items anymore (to exit LOOP). -6. Returns: AiCallResponse with content, modelName, priceUsd, etc. based on the delivered data in processedContentPartChunks +6. Returns: AiCallResponse with content, modelName, priceCHF, etc. based on the delivered data in processedContentPartChunks diff --git a/concepts/Billing-Konzept.md b/concepts/Billing-Konzept.md new file mode 100644 index 0000000..741e0c2 --- /dev/null +++ b/concepts/Billing-Konzept.md @@ -0,0 +1,1064 @@ +# Billing & Verrechnungssystem Konzept + +## Übersicht + +Dieses Dokument beschreibt das Konzept für ein Kosten- und Verrechnungssystem (Billing) für LLM-Nutzung in der PowerOn-Plattform. Das System ermöglicht die Abrechnung von AI-Services auf Mandanten- und Benutzerebene mit verschiedenen Abrechnungsmodellen. + +## Grundprinzipien + +### 1. Mandantenzentrierte Verrechnung +- **Alle Kosten** werden einem Mandanten zugeordnet +- Der **Mandant** ist die zentrale Abrechnungseinheit +- Benutzer können mehreren Mandanten angehören → Kosten werden pro Mandant getrennt erfasst + +### 2. Kostenstellen-Hierarchie +``` +Mandant (Kostenstelle) +├── Abrechnungsmodell (definiert durch SysAdmin) +├── Budget/Guthaben +├── Benutzer mit Zugriff +│ ├── User A → individuelle Nutzung +│ └── User B → individuelle Nutzung +└── Features mit LLM-Nutzung + ├── Chat Playground + ├── Chatbot (Feature-Instanzen) + └── Automation +``` + +### 3. LLM-Provider-Steuerung +- Benutzer können pro Workflow definieren, welche **AICore-Provider** erlaubt sind +- Zuordnung auf Level **AICore-Connector**: `anthropic`, `openai`, `perplexity`, `tavily`, `internal` +- Ermöglicht Kostenkontrolle und Compliance-Anforderungen + +### 4. Transparente Kostenerfassung +- Jede AI-Operation wird mit Kosten erfasst (`priceCHF`) +- Echtzeit-Sichtbarkeit für Benutzer +- Monatliche Abrechnungsberichte + +--- + +## Abrechnungsmodelle + +### Übersicht der Modelle + +| Modell | Beschreibung | Kostenstelle | Nutzung | +|--------|--------------|--------------|---------| +| `PREPAY_MANDATE` | Vorbezahltes Guthaben auf Mandantenebene | Mandant | Gemeinsames Budget für alle User | +| `PREPAY_USER` | Vorbezahltes Guthaben pro User | User (im Mandanten-Kontext) | Individuelles Budget | +| `CREDIT_POSTPAY` | Nutzung auf Kredit mit Nachzahlung | Mandant (Rechnungsadresse erforderlich) | Monatsrechnung | +| `UNLIMITED` | Keine Kostenlimitierung | - | Nur für interne Mandanten | + +### Modell: PREPAY_MANDATE + +``` +Mandant "SOHA Treuhand AG" +├── Abrechnungsmodell: PREPAY_MANDATE +├── Guthaben: 500.00 CHF +├── Warnschwelle: 50.00 CHF (10%) +└── Nutzer: + ├── User A: verbraucht 120.50 CHF → Mandanten-Konto -120.50 + └── User B: verbraucht 80.25 CHF → Mandanten-Konto -80.25 + + Restguthaben: 299.25 CHF +``` + +**Eigenschaften:** +- Mandant lädt Guthaben auf (via Admin oder Payment) +- Alle User des Mandanten teilen das Guthaben +- Bei Guthaben = 0 → AI-Features blockiert (mit Warnung) +- Warnschwelle konfigurierbar (Email-Benachrichtigung) + +### Modell: PREPAY_USER + +``` +Mandant "ROOT" (Bootstrap-Mandant) +├── Abrechnungsmodell: PREPAY_USER +├── Startguthaben für neue User: 10.00 CHF +└── Nutzer: + ├── User A: Guthaben 10.00 CHF → verbraucht 3.50 → Rest 6.50 CHF + └── User B: Guthaben 25.00 CHF → verbraucht 12.00 → Rest 13.00 CHF +``` + +**Eigenschaften:** +- Jeder User hat eigenes Guthaben im Mandanten-Kontext +- Neuer User erhält Startguthaben (konfigurierbar) +- User kann Guthaben aufladen +- Bei Guthaben = 0 → AI-Features blockiert für diesen User + +### Modell: CREDIT_POSTPAY + +``` +Mandant "Enterprise AG" +├── Abrechnungsmodell: CREDIT_POSTPAY +├── Rechnungsadresse: Enterprise AG, Musterstrasse 1, 8000 Zürich +├── Kreditlimit: 2000.00 CHF (optional) +├── Aktueller Saldo: -450.00 CHF (Schulden) +└── Monatsende: Rechnung über 450.00 CHF +``` + +**Eigenschaften:** +- Nutzung ohne Vorauszahlung +- Monatliche Abrechnung +- Optionales Kreditlimit +- Für verifizierte Geschäftskunden + +--- + +## Datenmodell + +### Neue Tabellen + +```python +# modules/datamodels/datamodelBilling.py + +class BillingAddress(BaseModel): + """Rechnungsadresse für CREDIT_POSTPAY Mandanten""" + + company: str # Firmenname + street: str # Strasse und Hausnummer + zip: str # PLZ + city: str # Ort + country: str # Land (Default: "CH") + vatNumber: Optional[str] # MwSt-Nummer (optional) + + +class BillingAccount(BaseModel): + """Abrechnungskonto pro Mandant oder User-Mandant-Kombination""" + + id: str # UUID + mandateId: str # FK → Mandate + userId: Optional[str] # FK → User (nur bei PREPAY_USER) + accountType: str # MANDATE | USER + balance: float # Aktuelles Guthaben/Saldo in CHF + creditLimit: Optional[float] # Kreditlimit in CHF (nur CREDIT_POSTPAY) + warningThreshold: float # Warnschwelle in CHF + lastWarningAt: Optional[datetime] # Letzte Warnung gesendet + enabled: bool # Account aktiv + + +class BillingTransaction(BaseModel): + """Einzelne Transaktion (Aufladung, Verbrauch, Gutschrift)""" + + id: str # UUID + accountId: str # FK → BillingAccount + transactionType: str # CREDIT | DEBIT | ADJUSTMENT + amount: float # Betrag in CHF (positiv) + description: str # Beschreibung + + # Referenz zur Quelle + referenceType: Optional[str] # WORKFLOW | PAYMENT | ADMIN | SYSTEM + referenceId: Optional[str] # ID des referenzierten Objekts + + # Kontext + workflowId: Optional[str] # Bei WORKFLOW-Verbrauch + featureInstanceId: Optional[str] # Bei Feature-Nutzung + aicoreProvider: Optional[str] # anthropic, openai, etc. + + +class BillingSettings(BaseModel): + """Abrechnungseinstellungen pro Mandant""" + + id: str # UUID + mandateId: str # FK → Mandate (UNIQUE) + billingModel: str # PREPAY_MANDATE | PREPAY_USER | CREDIT_POSTPAY | UNLIMITED + + # Konfiguration + defaultUserCredit: float # Startguthaben in CHF für neue User (bei PREPAY_USER) + warningThresholdPercent: float # Warnschwelle in % (z.B. 10) + blockOnZeroBalance: bool # AI blockieren bei Guthaben = 0 + + # Rechnungsadresse (erforderlich für CREDIT_POSTPAY) + billingAddress: Optional[BillingAddress] + + # Benachrichtigungen + notifyEmails: List[str] # Email-Adressen für Monatsberichte + notifyOnWarning: bool # Email bei Unterschreitung Warnschwelle + + +class UsageStatistics(BaseModel): + """Aggregierte Nutzungsstatistiken (für schnellen Abruf)""" + + id: str # UUID + accountId: str # FK → BillingAccount + periodType: str # DAY | MONTH | YEAR + periodStart: date # Beginn der Periode + + # Aggregierte Werte + totalCostCHF: float # Gesamtkosten in CHF + transactionCount: int # Anzahl Transaktionen + + # Aufschlüsselung nach Provider + costByProvider: Dict[str, float] # {'anthropic': 12.50, 'openai': 8.30} + + # Aufschlüsselung nach Feature + costByFeature: Dict[str, float] # {'playground': 15.00, 'automation': 5.80} +``` + +### Erweiterung bestehender Modelle + +```python +# Erweiterung: ChatWorkflow (datamodelChat.py) +class ChatWorkflow(BaseModel): + # ... bestehende Felder ... + + # NEU: Mandanten-Kontext für Billing + mandateId: Optional[str] # FK → Mandate +``` + +**Hinweis:** Die Provider-Steuerung erfolgt NICHT über Datenbank-Attribute, sondern über das RBAC-System (siehe Abschnitt "LLM-Provider als RBAC Resources"). + +### ER-Diagramm + +``` +┌─────────────────┐ ┌─────────────────────┐ +│ Mandate │────<│ BillingSettings │ +│ │ 1:1│ │ +│ id │ │ mandateId │ +│ name │ │ billingModel │ +└────────┬────────┘ │ billingAddress │ + │ └─────────────────────┘ + │ + │ 1:n + ▼ +┌─────────────────────┐ ┌─────────────────────┐ +│ BillingAccount │────<│ BillingTransaction │ +│ │ 1:n │ │ +│ mandateId │ │ accountId │ +│ userId (optional) │ │ amount (CHF) │ +│ balance (CHF) │ │ transactionType │ +│ accountType │ │ workflowId │ +└─────────────────────┘ └─────────────────────┘ + │ + │ 1:n RBAC Resources + ▼ ┌─────────────────────┐ +┌─────────────────────┐ │ resource.aicore.* │ +│ UsageStatistics │ │ │ +│ │ │ .anthropic │ +│ accountId │ │ .openai │ +│ periodType │ │ .perplexity │ +│ totalCostCHF │ │ .tavily │ +│ costByProvider │ │ .internal │ +└─────────────────────┘ └─────────────────────┘ +``` + +### LLM-Provider als RBAC Resources + +Die Steuerung, welche LLM-Provider ein Benutzer verwenden darf, erfolgt über das RBAC-System: + +**Registrierte Resources:** +- `resource.aicore.anthropic` +- `resource.aicore.openai` +- `resource.aicore.perplexity` +- `resource.aicore.tavily` +- `resource.aicore.internal` + +**Bootstrap-Berechtigungen (Mandate ROOT):** +- **SysAdmin:** Alle Provider (USE) +- **Admin:** Alle Provider (USE) +- **User:** Alle Provider (USE) + +**Vorteile des RBAC-Ansatzes:** +- Zentrale Verwaltung über bestehende Rollen-Struktur +- Granulare Steuerung pro Mandant möglich +- Keine zusätzlichen Datenbank-Attribute erforderlich +- Konsistent mit bestehendem Berechtigungssystem + +--- + +## Backend-Integration + +### 1. Billing Service + +```python +# modules/services/serviceBilling/mainServiceBilling.py + +class BillingService: + """Zentrale Service-Klasse für Billing-Operationen""" + + def __init__(self, currentUser: User, mandateId: str): + self.currentUser = currentUser + self.mandateId = mandateId + self._loadSettings() + + def _loadSettings(self): + """Lädt BillingSettings für den Mandanten""" + self.settings = getBillingSettings(self.mandateId) + + def getAccount(self) -> BillingAccount: + """Gibt das relevante Account zurück (Mandate oder User)""" + if self.settings.billingModel == 'PREPAY_USER': + return getOrCreateUserAccount(self.mandateId, self.currentUser.id) + else: + return getMandateAccount(self.mandateId) + + def checkBalance(self, estimatedCost: float) -> BillingCheckResult: + """Prüft ob genug Guthaben vorhanden ist""" + account = self.getAccount() + + if self.settings.billingModel == 'UNLIMITED': + return BillingCheckResult(allowed=True) + + if account.balance < estimatedCost: + if self.settings.blockOnZeroBalance: + return BillingCheckResult( + allowed=False, + reason='INSUFFICIENT_BALANCE', + currentBalance=account.balance, + requiredAmount=estimatedCost + ) + + return BillingCheckResult(allowed=True, currentBalance=account.balance) + + def recordUsage(self, priceCHF: float, context: UsageContext) -> BillingTransaction: + """Erfasst Nutzungskosten und erstellt Transaktion""" + account = self.getAccount() + + transaction = BillingTransaction( + id=generateUuid(), + accountId=account.id, + transactionType='DEBIT', + amount=priceCHF, + description=f"AI Usage: {context.process}", + referenceType='WORKFLOW', + referenceId=context.workflowId, + workflowId=context.workflowId, + featureInstanceId=context.featureInstanceId, + aicoreProvider=context.provider + ) + + # Account-Balance aktualisieren (atomar in DB) + account.balance -= priceCHF + + # Warnung prüfen + self._checkWarningThreshold(account) + + # Statistiken aktualisieren + self._updateStatistics(transaction) + + return transaction + + def isProviderAllowed(self, provider: str) -> bool: + """Prüft ob User Berechtigung für Provider hat via RBAC""" + resourceKey = f"resource.aicore.{provider}" + return self.rbacService.hasPermission( + user=self.currentUser, + mandateId=self.mandateId, + objectKey=resourceKey, + action='USE' + ) +``` + +### 2. Integration in AICore + +```python +# modules/aicore/aicoreModelSelector.py - Erweiterung + +class ModelSelector: + + def selectModel( + self, + operationType: str, + promptBytes: int, + processingMode: str = 'BASIC', + priority: str = 'BALANCED', + allowedProviders: Optional[List[str]] = None # NEU + ) -> Optional[AiModel]: + """ + Wählt das beste Model basierend auf Kriterien. + NEU: Filtert nach erlaubten Providern. + """ + availableModels = self._getAvailableModels() + + # NEU: Provider-Filter anwenden + if allowedProviders: + availableModels = [ + m for m in availableModels + if m.connectorType in allowedProviders + ] + + # ... bestehende Selektionslogik ... +``` + +### 3. Integration in Workflow-Verarbeitung + +```python +# modules/workflows/processing/workflowProcessor.py - Erweiterung + +class WorkflowProcessor: + + def __init__(self, ...): + # ... bestehende Initialisierung ... + + # NEU: Billing Service initialisieren + self.billingService = BillingService( + currentUser=self.currentUser, + mandateId=self.mandateId + ) + + async def _processAiCall(self, ...): + """Erweitert um Billing-Checks""" + + # Provider-Check + if not self.billingService.isProviderAllowed(selectedModel.connectorType): + raise ProviderNotAllowedException( + f"Provider '{selectedModel.connectorType}' not allowed for this mandate" + ) + + # Balance-Check (mit Kostenschätzung) + estimatedCost = self._estimateCost(selectedModel, inputBytes) + balanceCheck = self.billingService.checkBalance(estimatedCost) + + if not balanceCheck.allowed: + raise InsufficientBalanceException( + f"Insufficient balance. Current: {balanceCheck.currentBalance}, " + f"Required: {balanceCheck.requiredAmount}" + ) + + # AI-Aufruf durchführen + response = await self._callAi(...) + + # NEU: Kosten erfassen + if response.priceCHF: + self.billingService.recordUsage( + priceCHF=response.priceCHF, + context=UsageContext( + workflowId=self.workflowId, + featureInstanceId=self.featureInstanceId, + provider=selectedModel.connectorType, + process=response.process + ) + ) + + return response +``` + +### 4. Bootstrap: Root-Mandant Setup + +```python +# modules/interfaces/interfaceBootstrap.py - Erweiterung + +def _setupRootMandate(): + """Setup für Root-Mandant mit PREPAY_USER Modell""" + + rootMandate = getOrCreateMandate( + name="ROOT", + description="System Root Mandate" + ) + + # Billing Settings für Root + billingSettings = BillingSettings( + id=generateUuid(), + mandateId=rootMandate.id, + billingModel='PREPAY_USER', + defaultUserCredit=10.0, # 10 CHF Startguthaben + warningThresholdPercent=20, + blockOnZeroBalance=True, + notifyOnWarning=True + ) + + saveBillingSettings(billingSettings) + + # RBAC: LLM-Provider Berechtigungen für alle Rollen + _setupAicoreRbacPermissions(rootMandate.id) + + +def _setupAicoreRbacPermissions(mandateId: str): + """Erstellt RBAC-Berechtigungen für LLM-Provider""" + + providers = ['anthropic', 'openai', 'perplexity', 'tavily', 'internal'] + roles = ['sysadmin', 'admin', 'user'] + + for role in roles: + for provider in providers: + createAccessRule( + mandateId=mandateId, + roleLabel=role, + context='RESOURCE', + item=f'resource.aicore.{provider}', + use=True + ) + + +def _setupNewUserInRoot(user: User): + """Erstellt Billing-Account für neuen User im Root-Mandanten""" + + rootMandate = getRootMandate() + settings = getBillingSettings(rootMandate.id) + + # User-Account erstellen + account = BillingAccount( + id=generateUuid(), + mandateId=rootMandate.id, + userId=user.id, + accountType='USER', + balance=settings.defaultUserCredit, # 10 CHF + warningThreshold=settings.defaultUserCredit * 0.2, # 20% + enabled=True + ) + + # Initiale Gutschrift-Transaktion + transaction = BillingTransaction( + id=generateUuid(), + accountId=account.id, + transactionType='CREDIT', + amount=settings.defaultUserCredit, + description='Initial credit for new user registration', + referenceType='SYSTEM' + ) + + saveAccount(account) + saveTransaction(transaction) +``` + +--- + +## API-Endpoints + +### Billing Management (Admin) + +``` +# Mandanten-Billing-Einstellungen +GET /api/admin/billing/mandates/{mandateId}/settings +PUT /api/admin/billing/mandates/{mandateId}/settings +POST /api/admin/billing/mandates/{mandateId}/credit # Guthaben aufladen + +# Transaktionen einsehen +GET /api/admin/billing/mandates/{mandateId}/transactions +GET /api/admin/billing/mandates/{mandateId}/accounts +``` + +### User-Billing (Self-Service) + +``` +# Eigenes Guthaben +GET /api/billing/balance # Aktueller Stand pro Mandant +GET /api/billing/balance/{mandateId} # Stand für spezifischen Mandanten + +# Eigene Transaktionen +GET /api/billing/transactions # Alle eigenen Transaktionen +GET /api/billing/transactions/{mandateId} # Transaktionen pro Mandant + +# Statistiken +GET /api/billing/statistics # Aggregierte Stats +GET /api/billing/statistics/{mandateId} # Stats pro Mandant +GET /api/billing/statistics/{mandateId}/chart # Chart-Daten (Tag/Monat/Jahr) +``` + +### Provider-Konfiguration + +``` +# Erlaubte Provider für Workflows +GET /api/billing/providers # Alle verfügbaren Provider +GET /api/billing/providers/{mandateId} # Erlaubte Provider für Mandant + +# Workflow-spezifische Provider-Einstellung +PUT /api/chat/playground/{workflowId}/providers # Provider für Workflow setzen +PUT /api/automations/{automationId}/providers # Provider für Automation setzen +``` + +### Response-Beispiele + +```json +// GET /api/billing/balance +{ + "balances": [ + { + "mandateId": "root-mandate-id", + "mandateName": "ROOT", + "billingModel": "PREPAY_USER", + "balance": 6.50, + "currency": "CHF", + "warningThreshold": 2.00, + "isWarning": false + }, + { + "mandateId": "soha-mandate-id", + "mandateName": "SOHA Treuhand AG", + "billingModel": "PREPAY_MANDATE", + "balance": 299.25, + "currency": "CHF", + "warningThreshold": 50.00, + "isWarning": false + } + ] +} + +// GET /api/billing/statistics/root-mandate-id/chart?period=month&year=2026 +{ + "mandateId": "root-mandate-id", + "period": "month", + "year": 2026, + "currency": "CHF", + "data": [ + {"label": "Jan", "totalCost": 12.50, "byProvider": {"anthropic": 10.00, "openai": 2.50}}, + {"label": "Feb", "totalCost": 8.30, "byProvider": {"anthropic": 8.30}}, + // ... + ], + "totals": { + "totalCost": 45.80, + "byProvider": {"anthropic": 38.30, "openai": 7.50} + } +} +``` + +--- + +## UI-Seiten + +**Billing wird als separater Navigation-Container gerendert:** + +``` +Static Block: BILLING +├── page.billing.dashboard → /billing +├── page.billing.transactions → /billing/transactions +├── page.billing.statistics → /billing/statistics/{mandateId} +└── page.billing.admin → /billing/admin (nur SysAdmin) +``` + +### 1. Billing Dashboard (User) + +**Pfad:** `/billing` + +**Funktionen:** +- Übersicht aller Mandanten mit Guthaben +- Restbudget für ROOT-Mandant prominent anzeigen +- Quick-Stats: Ausgaben heute/diese Woche/diesen Monat + +``` +┌─────────────────────────────────────────────────────────┐ +│ MEIN GUTHABEN │ +├─────────────────────────────────────────────────────────┤ +│ ┌──────────────────┐ ┌──────────────────────────────┐ │ +│ │ ROOT │ │ Chart: Ausgaben pro Tag │ │ +│ │ ────────────────│ │ ┌─┐ ┌─┐ ┌─┐ │ │ +│ │ CHF 6.50 │ │ │█│ │█│ ┌─┐ │█│ ┌─┐ │ │ +│ │ [████████░░] 65% │ │ │█│ │█│ │█│ │█│ │█│ ┌─┐ │ │ +│ │ │ │ Mo Di Mi Do Fr Sa So │ │ +│ │ Warnschwelle: 2 │ └──────────────────────────────┘ │ +│ └──────────────────┘ │ +│ │ +│ MANDANTEN-ÜBERSICHT │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ SOHA Treuhand AG CHF 299.25 ████████████░░ │ │ +│ │ Partner AG CHF 150.00 ██████████░░░░ │ │ +│ └──────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2. Detailansicht pro Mandant + +**Pfad:** `/billing/statistics/{mandateId}` + +**Funktionen:** +- Detaillierte Kostengrafik (Tag/Monat/Jahr umschaltbar) +- Aufschlüsselung nach Provider +- Aufschlüsselung nach Feature +- Transaktionshistorie + +``` +┌─────────────────────────────────────────────────────────┐ +│ SOHA Treuhand AG - Kostenübersicht │ +├─────────────────────────────────────────────────────────┤ +│ Zeitraum: [Tag ▼] [Monat] [Jahr] Feb 2026 │ +│ │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ [Bar-Chart: Kosten pro Tag im Februar] │ │ +│ │ 25 ─┬─ (CHF) │ │ +│ │ │ ██ │ │ +│ │ 20 ─┤ ██ ██ │ │ +│ │ │ ██ ██ ██ ██ │ │ +│ │ 10 ─┤ ██ ██ ██ ██ ██ ██ │ │ +│ │ │──██──██───██───██───██──██─────────────│ │ +│ │ 1 5 10 15 20 25 │ │ +│ └─────────────────────────────────────────────────┘ │ +│ │ +│ AUFSCHLÜSSELUNG │ +│ ┌────────────────┐ ┌────────────────┐ │ +│ │ Nach Provider │ │ Nach Feature │ │ +│ │ Anthropic 70% │ │ Playground 60% │ │ +│ │ OpenAI 25% │ │ Automation 30% │ │ +│ │ Internal 5% │ │ Chatbot 10% │ │ +│ └────────────────┘ └────────────────┘ │ +│ │ +│ LETZTE TRANSAKTIONEN │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ 04.02.2026 14:32 AI Usage: Document Analysis │ │ +│ │ anthropic -CHF 0.15 │ │ +│ │ 04.02.2026 14:28 AI Usage: Chat Response │ │ +│ │ anthropic -CHF 0.08 │ │ +│ │ ... │ │ +│ └──────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 3. Provider-Auswahl im Chat Playground + +**Integration in:** `/workflows/playground` + +**Funktionen:** +- Dropdown/Checkboxen für erlaubte Provider +- Zeigt nur Provider an, für die der User RBAC-Berechtigung hat +- Auswahl gilt für den nächsten AI-Call + +``` +┌─────────────────────────────────────────────────────────┐ +│ Chat Playground │ +├─────────────────────────────────────────────────────────┤ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ EINSTELLUNGEN │ │ +│ │ │ │ +│ │ AI-Provider für nächsten Call: │ │ +│ │ ○ Anthropic (Claude) ~CHF 0.020/1k tokens │ │ +│ │ ● OpenAI (GPT-4) ~CHF 0.015/1k tokens │ │ +│ │ ○ Perplexity (Web) ~CHF 0.008/1k tokens │ │ +│ │ ○ Internal (Document) ~CHF 0.002/1k tokens │ │ +│ │ │ │ +│ │ Geschätzte Kosten: ~CHF 0.02 pro Nachricht │ │ +│ └─────────────────────────────────────────────────┘ │ +│ │ +│ [Chat-Interface...] │ +└─────────────────────────────────────────────────────────┘ +``` + +### 4. Automation Editor (Modal mit Tabs) + +**Integration in:** `/automations/{id}/edit` + +Das Automation-Editor Modal wird in drei Tabs aufgeteilt: + +**Tab-Struktur:** +1. **General** - Name, Beschreibung, Schedule, Status +2. **Modellierung** - Template, Placeholders, Preview +3. **LLM Modelle** - Provider-Auswahl basierend auf RBAC-Berechtigungen + +``` +┌─────────────────────────────────────────────────────────┐ +│ Automation bearbeiten: "Täglicher Report" │ +├─────────────────────────────────────────────────────────┤ +│ [General] [Modellierung] [LLM Modelle] │ +│ ───────────────────────────────────────────────────── │ +│ TAB: LLM Modelle │ +│ │ +│ Verfügbare Provider (basierend auf Ihren Berechtigungen): +│ ○ Anthropic (Claude) CHF 0.020/1k tokens │ +│ ● OpenAI (GPT-4) CHF 0.015/1k tokens │ +│ ○ Perplexity (Web) CHF 0.008/1k tokens │ +│ ○ Internal (Document) CHF 0.002/1k tokens │ +│ │ +│ Geschätzte Kosten pro Ausführung: ~CHF 0.05 │ +│ Bei täglicher Ausführung: ~CHF 1.50/Monat │ +│ │ +│ [Speichern] [Abbrechen] │ +└─────────────────────────────────────────────────────────┘ +``` + +### 5. Admin: Billing-Verwaltung (nur SysAdmin) + +**Pfad:** `/billing/admin` + +**Funktionen:** +- Übersicht aller Mandanten mit Billing-Status +- Abrechnungsmodell ändern +- Guthaben manuell aufladen (Stripe-Vorbereitung für Zukunft) +- Rechnungsadresse verwalten (für CREDIT_POSTPAY) +- Email-Empfänger verwalten + +``` +┌─────────────────────────────────────────────────────────┐ +│ BILLING ADMINISTRATION │ +├─────────────────────────────────────────────────────────┤ +│ MANDANTEN │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Mandant Modell Balance Status │ │ +│ │ ──────────────────────────────────────────────── │ │ +│ │ ROOT PREPAY_USER n/a ✓ │ │ +│ │ SOHA Treuhand PREPAY_MANDATE CHF 299 ✓ │ │ +│ │ Enterprise AG CREDIT_POSTPAY CHF -450 ✓ │ │ +│ │ Test GmbH PREPAY_MANDATE CHF 5 ⚠ │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ +│ [+ Neuer Mandant] [Export Monatsreport] │ +│ │ +│ MANDANT BEARBEITEN: SOHA Treuhand AG │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Abrechnungsmodell: [PREPAY_MANDATE ▼] │ │ +│ │ │ │ +│ │ Warnschwelle: [10] % │ │ +│ │ Blockieren bei 0: [✓] │ │ +│ │ │ │ +│ │ Rechnungsadresse (für CREDIT_POSTPAY): │ │ +│ │ [Firma: ________________] [Strasse: _________] │ │ +│ │ [PLZ: ____] [Ort: _______] [MwSt-Nr: ________] │ │ +│ │ │ │ +│ │ Benachrichtigungs-Emails: │ │ +│ │ [admin@soha.ch, billing@soha.ch ] │ │ +│ │ │ │ +│ │ [Speichern] [Guthaben aufladen] │ │ +│ └──────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +**Hinweis:** Provider-Berechtigungen werden über das RBAC-System verwaltet (Administration → Rollen). + +--- + +## Email-Reports + +### Monatsende-Report + +**Trigger:** Automatisch am 1. jeden Monats (Cronjob) + +**Empfänger:** `BillingSettings.notifyEmails` pro Mandant + +**Template:** + +``` +Betreff: PowerOn Kostenreport Januar 2026 - SOHA Treuhand AG + +Sehr geehrte Damen und Herren, + +anbei erhalten Sie den monatlichen Kostenreport für Ihren Mandanten +"SOHA Treuhand AG" für den Zeitraum Januar 2026. + +ZUSAMMENFASSUNG +─────────────────────────────────────────────────────── +Gesamtkosten: $ 156.80 USD +Anzahl AI-Operationen: 423 +Durchschnitt pro Operation: $ 0.37 USD + +AUFSCHLÜSSELUNG NACH PROVIDER +─────────────────────────────────────────────────────── +Anthropic (Claude) $ 112.50 USD (72%) +OpenAI (GPT-4) $ 38.30 USD (24%) +Internal $ 6.00 USD (4%) + +AUFSCHLÜSSELUNG NACH FEATURE +─────────────────────────────────────────────────────── +Chat Playground $ 95.00 USD (61%) +Automation $ 45.80 USD (29%) +Chatbot (Feature) $ 16.00 USD (10%) + +TOP 5 NUTZER (nach Verbrauch) +─────────────────────────────────────────────────────── +1. Max Mustermann $ 48.50 USD +2. Anna Schmidt $ 35.20 USD +3. Peter Weber $ 28.90 USD +4. Lisa Müller $ 22.10 USD +5. Thomas Meier $ 12.30 USD + +KONTOSTAND +─────────────────────────────────────────────────────── +Vorheriger Stand: $ 456.05 USD +Verbrauch: -$ 156.80 USD +─────────────────────────────────────────────────────── +Aktueller Stand: $ 299.25 USD + +Bei Fragen stehen wir Ihnen gerne zur Verfügung. + +Freundliche Grüsse +PowerOn Platform +``` + +### Warnung bei niedriger Balance + +**Trigger:** Balance unterschreitet `warningThreshold` + +**Empfänger:** `BillingSettings.notifyEmails` + +``` +Betreff: ⚠️ PowerOn - Niedriges Guthaben für SOHA Treuhand AG + +Ihr Guthaben für den Mandanten "SOHA Treuhand AG" hat die +Warnschwelle unterschritten. + +Aktuelles Guthaben: $ 45.00 USD +Warnschwelle: $ 50.00 USD (10%) + +Bitte laden Sie Ihr Guthaben auf, um Unterbrechungen zu vermeiden. + +[Guthaben aufladen →] + +Freundliche Grüsse +PowerOn Platform +``` + +--- + +## Implementierungsschritte + +### Phase 1: Datenmodell & Grundstruktur + +1. **Datenbank-Schema erstellen** + - `BillingAccount`, `BillingTransaction`, `BillingSettings`, `UsageStatistics` Tabellen + - Migration für bestehende Mandanten (Default: `UNLIMITED`) + +2. **Erweiterung bestehender Modelle** + - `ChatWorkflow.mandateId` hinzufügen + - `ChatWorkflow.allowedProviders` hinzufügen + - `AutomationDefinition.allowedProviders` hinzufügen + +3. **BillingService implementieren** + - Balance-Prüfung + - Transaktions-Erfassung + - Provider-Validierung + +### Phase 2: Integration in Workflows + +1. **ChatWorkflow erweitern** + - `mandateId` bei Start speichern + - Provider-Filter bei Model-Selection + +2. **Billing-Checks einbauen** + - Vor jedem AI-Call: Balance prüfen + - Nach jedem AI-Call: Kosten erfassen + +3. **Error Handling** + - `InsufficientBalanceException` + - `ProviderNotAllowedException` + - User-freundliche Fehlermeldungen + +### Phase 3: API-Endpoints + +1. **User-Billing APIs** + - Balance-Abfrage + - Transaktions-Historie + - Statistiken mit Chart-Daten + +2. **Admin-Billing APIs** + - Settings verwalten + - Guthaben aufladen + - Report-Export + +### Phase 4: UI-Integration + +1. **Billing Dashboard** + - Neue Seite: `/billing` + - Chart-Komponenten für Statistiken + +2. **Provider-Auswahl** + - Chat Playground erweitern + - Automation-Editor erweitern + +3. **Admin-Seiten** + - Billing-Verwaltung + - Mandanten-Konfiguration + +### Phase 5: Bootstrap & Migration + +1. **Root-Mandant Setup** + - `PREPAY_USER` Modell + - 10 USD Startguthaben für neue User + +2. **Bestehende User migrieren** + - Accounts erstellen + - Initiales Guthaben gutschreiben + +3. **Bestehende Mandanten migrieren** + - Default-Settings erstellen + - `UNLIMITED` oder `PREPAY_MANDATE` + +### Phase 6: Email-Reports + +1. **Email-Templates erstellen** + - Monatsreport + - Balance-Warnung + +2. **Scheduler einrichten** + - Monatsende-Cronjob + - Warnschwellen-Check + +3. **Email-Service integrieren** + - Template-Rendering + - Email-Versand + +--- + +## Offene Fragen und Entscheidungen + +### Beantwortet + +1. **Soll Chat Playground über Mandanten laufen?** + → **Ja**, dies ist für konsistente Verrechnung notwendig. `ChatWorkflow.mandateId` wird aus dem Request-Context übernommen. + +2. **Welche Services laufen noch nicht über Mandanten?** + → `data.chat.*` und `data.files.*` sind aktuell `USER_OWNED`. Für Billing muss `ChatWorkflow` den Mandanten-Kontext speichern. + +3. **Läuft Automation bereits über Mandanten?** + → **Ja**, `AutomationDefinition.mandateId` existiert bereits. Erweiterung nur um `allowedProviders`. + +### Noch zu klären + +1. **Payment-Integration** + - Wie laden Kunden Guthaben auf? (Stripe, Rechnung, manuell) + - Self-Service oder nur Admin? + +2. **Preisgestaltung** + - Aufschlag auf Provider-Kosten? (z.B. +20%) + - Fixkosten pro Mandant/User? + +3. **Währung** + - Nur USD intern, Anzeige in CHF/EUR? + - Wechselkurs-Handling? + +4. **Datenhaltung** + - Wie lange Transaktions-Historie aufbewahren? + - Archivierung alter Daten? + +5. **GDPR/Datenschutz** + - User-Löschung: Was passiert mit Billing-Daten? + - Anonymisierung nach X Jahren? + +--- + +## Glossar + +| Begriff | Beschreibung | +|---------|--------------| +| **AICore** | Plugin-System für LLM-Provider Integration | +| **Billing Account** | Konto für Guthaben/Saldo (pro Mandant oder User) | +| **Connector** | AICore-Plugin für einen spezifischen Provider | +| **Kostenstelle** | Mandant, dem Kosten zugeordnet werden | +| **Mandate** | Mandant/Tenant in der Multi-Tenant-Architektur | +| **Provider** | LLM-Anbieter (anthropic, openai, etc.) | +| **priceCHF** | Berechneter Preis einer AI-Operation in USD | + +--- + +## Anhang: Wichtige Dateien + +### Bestehend (zu erweitern) + +``` +gateway/modules/ +├── aicore/ +│ ├── aicoreModelSelector.py # Provider-Filter hinzufügen +│ └── aicoreBase.py # connectorType für Billing +├── datamodels/ +│ ├── datamodelChat.py # ChatWorkflow erweitern +│ └── datamodelUam.py # Mandate-Referenz +├── features/automation/ +│ └── datamodelFeatureAutomation.py # allowedProviders hinzufügen +├── interfaces/ +│ └── interfaceBootstrap.py # Root-Mandant Billing Setup +├── workflows/ +│ ├── automation/mainWorkflow.py # Billing-Integration +│ └── processing/workflowProcessor.py # Balance-Checks +└── routes/ + └── routeChat.py # mandateId im Workflow speichern +``` + +### Neu zu erstellen + +``` +gateway/modules/ +├── datamodels/ +│ └── datamodelBilling.py # Neue Billing-Modelle +├── interfaces/ +│ └── interfaceDbBilling.py # Billing CRUD-Operationen +├── services/ +│ └── serviceBilling/ +│ ├── mainServiceBilling.py # Zentrale Billing-Logik +│ └── subBillingReports.py # Report-Generierung +└── routes/ + └── routeBilling.py # Billing API-Endpoints +``` diff --git a/implementation/doc_automation_templates_db_and_editor_concept.md b/implementation/doc_automation_templates_db_and_editor_concept.md new file mode 100644 index 0000000..a7fa9d7 --- /dev/null +++ b/implementation/doc_automation_templates_db_and_editor_concept.md @@ -0,0 +1,161 @@ +# Automation Templates in DB und Editor – Konzept + +## Übersicht + +Templates werden von der Python-Datei in die Datenbank verschoben. Placeholders gehören nicht zum Template, sondern nur zur konkreten **Anwendung** (AutomationDefinition). Template-Liste und **CRUD auf Templates** (Anlegen/Ändern/Löschen) gemäss **RBAC** – Nutzer verwalten ihre **eigenen** Templates (z.B. MY-Level); SysAdmin macht nur den **Bootstrap** (initialer Seed). Im UI: **Verwaltung der Automation-Objekte** (bereits vorhanden) und ein **Automation Editor** mit Modus „Definition“ oder „Template“. + +--- + +## 1. Datenmodell + +### 1.1 Bestehend: AutomationDefinition (weiter nutzbar) + +- **Datei:** `gateway/modules/features/automation/datamodelFeatureAutomation.py` +- **Tabelle:** `AutomationDefinition` (Namespace `data.automation`), RBAC bereits: MY-Level (user-owned). +- **Felder (Auszug):** + - `id`, `mandateId`, `featureInstanceId`, `label`, `schedule`, `active`, `eventId`, `status`, `executionLogs` + - **`template`** (string): JSON des Workflow-Plans. Wird aus einem **AutomationTemplate** übernommen, wenn der User „Aus Template erstellen“ wählt – danach keine Referenz mehr, die Definition kann unabhängig weiterbearbeitet werden. Kann auch frei (ohne Template) gepflegt werden. + - **`placeholders`** (Dict[str, str]): Nur hier – Werte für `{{KEY:name}}` bei der **spezifischen Anwendung**. Templates enthalten keine Placeholders. + +**Fazit:** AutomationDefinition bleibt zentrales Modell; **keine** `templateId`-Referenz. Beim Erzeugen aus einem Template wird der Template-Inhalt in die Definition kopiert; danach wird nur noch die Definition im Editor bearbeitet. Eine Definition kann ihrerseits **als neues Template gespeichert** werden – Berechtigung dazu gemäss **RBAC** (wer create auf AutomationTemplate hat). + +### 1.2 Neu: AutomationTemplate (nur DB, ohne Placeholders) + +- **Zweck:** Vordefinierte Workflow-Vorlagen **ohne** Platzhalter. CRUD gemäss **RBAC** – Nutzer ihre eigenen (z.B. MY); Bootstrap (initialer Seed) nur durch SysAdmin/System. +- **Vorschlag Felder:** + - `id` (PK, UUID) + - `label` (**MultiLanguage**, z.B. `{"en": "SharePoint Summary", "de": "SharePoint Zusammenfassung"}`): Anzeigename z.B. „SharePoint Themen Zusammenfassung“ + - `overview` (**MultiLanguage**, optional): Kurzbeschreibung + - **`template`** (string): JSON der Workflow-Struktur **mit** Platzhaltern im Format `{{KEY:placeholderName}}`. Enthält z.B. `overview`, `tasks` mit `actionList`, `execMethod`, `execAction`, `execParameters` (dort die Keys). Keine konkreten Werte für Placeholders – die kommen erst in AutomationDefinition.placeholders. + - `_createdAt`, `_updatedAt`, `_createdBy` (Systemfelder, optional je nach bestehendem Muster) + +**Keine** `placeholders`-Spalte in AutomationTemplate. Placeholders existieren nur in AutomationDefinition bei der Nutzung eines Templates. + +### 1.3 Beziehung (ohne Referenz) + +- Beim **„Aus Template erstellen“**: Inhalt von `AutomationTemplate.template` wird in eine **neue** AutomationDefinition kopiert (`template` + leere oder vorgeschlagene `placeholders`). Das **Label** wird in der **Sprache des aktuellen Users** übernommen (`definition.label = template.label[userLanguage]`). Es wird **keine** Referenz (templateId) angelegt – die Definition ist danach eigenständig und kann beliebig geändert werden. +- Eine **bestehende AutomationDefinition** wird ausschließlich im Editor bearbeitet (template + placeholders). +- **„Als Template speichern“**: Aus einer AutomationDefinition kann ein neues AutomationTemplate angelegt werden – Inhalt von `template` wird übernommen, **ohne** konkrete Placeholder-Werte (nur `{{KEY:...}}` bleibt). Berechtigung gemäss **RBAC** (wer create auf AutomationTemplate hat). + +--- + +## 2. RBAC und Bootstrap + +### 2.1 AutomationTemplate + +- **Bootstrap:** Beim ersten Start (oder Migration) werden die bisherigen Vorlagen aus `subAutomationTemplates.py` in die Tabelle **AutomationTemplate** eingespielt – **ohne scharfe Parameter** (keine konkreten Placeholder-Werte aus den bisherigen `parameters`-Blöcken). Nur die reinen Template-JSON-Strukturen mit `{{KEY:...}}`. +- **RBAC:** Gemäss RBAC-System – Sichtbarkeit und **CREATE/UPDATE/DELETE** auf AutomationTemplate nach RBAC-Regeln (z.B. Nutzer können **ihre eigenen** Templates anlegen/ändern/löschen, MY-Level). SysAdmin nur für **Bootstrap** (initialer Seed der Vorlagen aus Python). +- **UI-Context:** `ui.feature.automation.templates` – Sichtbarkeit und Aktionen (lesen, erstellen, bearbeiten, löschen) nach RBAC. + +### 2.2 AutomationDefinition (unverändert) + +- SysAdmin: kann alle AutomationDefinition-Einträge sehen/bearbeiten (z.B. über bestehende RBAC-Logik mit mandateId/context). +- User: nur eigene AutomationDefinition-Einträge (MY). Scheduling-Verwaltung: SysAdmin kann alle Schedulings bearbeiten, jeder User nur die eigenen Automations. + +--- + +## 3. Backend-Logik + +### 3.1 AutomationTemplate CRUD + +- **Neue API** unter z.B. `/api/automation-templates` (oder unter bestehendem Router mit Präfix). **Zugriff gemäss RBAC** – Nutzer sehen/bearbeiten/löschen nur ihre eigenen Templates (z.B. MY; Filter/Prüfung über `_createdBy` bzw. RBAC-Regeln für `data.automation.AutomationTemplate`). + - `GET /api/automation-templates` – Liste (evtl. paginiert), gefiltert nach RBAC + - `GET /api/automation-templates/{id}` – Einzelabruf (nur wenn berechtigt) + - `POST /api/automation-templates` – Anlegen (wer create hat) + - `PUT /api/automation-templates/{id}` – Aktualisieren (wer update auf diesen Eintrag hat) + - `DELETE /api/automation-templates/{id}` – Löschen (wer delete auf diesen Eintrag hat) +- **Bootstrap/Migration (nur SysAdmin/System):** Einmalig Daten aus `subAutomationTemplates.py` lesen, pro „template“-Eintrag einen AutomationTemplate-Datensatz anlegen (nur `label`/`overview` + JSON-`template` mit `{{KEY:...}}`, keine `parameters` speichern). Danach CRUD nur noch über RBAC. + +### 3.2 Template-API und Sichtbarkeit + +- **GET /api/automations/templates** (bzw. neuer Endpoint für AutomationTemplate): Liste aus **DB** (AutomationTemplate). **Sichtbarkeit gemäss RBAC** – Nutzer sehen die Templates, auf die sie Zugriff haben (z.B. eigene + ggf. Gruppen); CREATE/UPDATE/DELETE über gleiche API, Berechtigung pro Request per RBAC. + +### 3.3 Nutzbare Actions (Katalog, RBAC-gefiltert) + +- **Endpoint** z.B. `GET /api/automations/actions`: + - **Datenquelle:** Pro Action die bestehende **WorkflowActionDefinition** nutzen (`actionId`, `description`, `parameters` mit Typ, `frontendType`, etc.) – diese Daten sind pro Action bereits verfügbar (Method-Basis / methodDiscovery). + - **RBAC:** Nur Actions zurückgeben, für die der aktuelle User Berechtigung hat (RESOURCE-Context, `actionId` – z.B. `_checkActionPermission(actionId)`). So entsteht die „Übersicht aller nutzbaren Actions“ nach RBAC. + - **Format:** Pro Action die Action-Definition ausliefern (actionId, description, parameters-Schema); optional ein **Beispiel-JSON-Snippet** pro Action (minimales action-Objekt: execMethod, execAction, execParameters, execResultLabel) für Copy/Paste ins Template-JSON. + +### 3.4 JSON-Validierung und Placeholder-Extraktion + +- Beim Speichern einer AutomationDefinition im Editor (oder in einem separaten „Validate“-Schritt): + - **JSON validieren:** `template`-String parsen, Syntax-Check; bei Fehler Meldung anzeigen. + - **Placeholder-Extraktion:** Regex oder Parser über den `template`-String, alle Vorkommen von `{{KEY:placeholderName}}` sammeln und als vorgeschlagene Keys in `placeholders` anzeigen bzw. bestehende `placeholders` anpassen (neue Keys hinzufügen, nicht mehr vorkommende optional entfernen oder behalten). + +--- + +## 4. UI – Admin-Seiten (frontend_nyla) + +### 4.1 Seite: Verwaltung der Automation-Objekte und Scheduling (existiert) + +- **Route/Seite:** Bereits vorhanden (z.B. AutomationsPage, Route `automations`). +- **Funktion:** Liste der AutomationDefinition-Einträge; Erstellen, Bearbeiten, Löschen, Ausführen, Logs. +- **Berechtigung:** SysAdmin kann alle Automations und deren Scheduling bearbeiten; normale User nur die eigenen. Entspricht dem bestehenden MY-Level plus Admin-Logik (z.B. isSysAdmin sieht alle). + +### 4.2 Seite: Automation Editor (neu) – ein Editor, zwei Modi + +- **Zweck:** Im Editor wird **entweder** ein **AutomationTemplate** bearbeitet **oder** eine **AutomationDefinition** erstellt/bearbeitet. Entscheidend ist ein **Modus-Flag** im Editor (nicht templateId). + +**Modus-Flag im Editor:** + +- **„Definition bearbeiten / neu erstellen“** (Standard für normale User): + - Inhalt kommt aus einer bestehenden AutomationDefinition oder aus „Aus Template erstellen“ (dann wird Template-Inhalt in eine **neue** Definition kopiert). + - Speichern → **AutomationDefinition** wird erstellt bzw. aktualisiert (template + placeholders). Keine Referenz zum Template. + +- **„Template bearbeiten“**: + - Inhalt kommt aus einem AutomationTemplate (oder „Neu“ für leeres Template). Sichtbar/bearbeitbar gemäss **RBAC** (Nutzer nur eigene). + - Speichern → **AutomationTemplate** wird erstellt bzw. aktualisiert. Keine placeholders im Template. + +- **„Als Template speichern“** (von einer Definition aus): Aus der aktuell geöffneten AutomationDefinition wird ein neues AutomationTemplate angelegt (nur template-JSON mit `{{KEY:...}}`, keine scharfen Werte). Erlaubt für Nutzer, die gemäss **RBAC** create auf AutomationTemplate haben. + +**Saubere Trennung:** Derselbe Editor (gleiche UI: JSON + Platzhalter-Key/Value + Actions-Katalog). Nur das Zielobjekt und die Speicher-Logik unterscheiden sich je nach Modus: Save schreibt entweder in AutomationDefinition oder in AutomationTemplate. Keine templateId nötig. + +**Editor-Funktionen (unabhängig vom Modus):** + +1. **JSON bearbeiten (Text, Copy/Paste, Validierung)** + - Großes Textfeld (oder Code-Editor) mit dem Inhalt von `template` (aus Definition oder Template). + - Nach Bearbeitung: „Validieren“-Button oder beim Speichern: JSON parsen; bei Fehler Meldung anzeigen. + - Nach Validierung: **Platzhalter anpassen** – aus dem JSON alle `{{KEY:xyz}}` extrahieren und in der Placeholder-Verwaltung als Keys anzeigen/aktualisieren. (Bei Template-Modus: Placeholder-UI nur zur Anzeige/Vorschau; gespeichert wird nur das Template ohne Werte.) + +2. **Platzhalter als dynamische Key/Value-Objekte** + - Liste aller in `template` vorkommenden Placeholder-Keys (aus Extraktion). + - Pro Key: Wert eingeben. **Typ:** Aus der **Action-Definition** (WorkflowActionParameter, frontendType) der Action, in der der Key vorkommt. + - Werte werden nur bei **Definition** gespeichert (`AutomationDefinition.placeholders`). Beim Template-Modus keine Werte persistiert. + +3. **Übersicht nutzbarer Actions (RBAC)** + - Bereich „Nutzbare Actions“: Daten von `GET /api/automations/actions` – pro Action die **Action-Definition** (actionId, description, parameters-Schema). + - **Copy/Paste:** Pro Action ein JSON-Snippet (action-Objekt) anzeigen, Buttons „Copy JSON“ / „Insert at cursor“ zum Einfügen ins Template-JSON. + +**Navigation:** Von der Automations-Verwaltung: „Neu“ (leer oder „Aus Template erstellen“) bzw. „Bearbeiten“ → Editor im **Definitions-Modus**. Wer gemäss RBAC Templates verwalten darf: „Template bearbeiten“ / „Templates verwalten“ → Editor im **Template-Modus** oder Liste AutomationTemplate (nur eigene bzw. nach RBAC sichtbare). + +### 4.3 Template-Verwaltung + +- **Seite:** „Templates verwalten“. Sichtbar/verfügbar gemäss **RBAC** – Nutzer sehen und verwalten **ihre eigenen** Templates (CRUD nach RBAC, z.B. MY). +- **Inhalt:** CRUD für **AutomationTemplate**. Liste mit Bearbeiten/Neu/Löschen; beim Bearbeiten: `label`, `overview`, `template` (JSON mit `{{KEY:...}}`, ohne konkrete Placeholder-Werte). Template-Liste gefiltert nach Berechtigung („Aus Template erstellen“ nutzt dieselbe Liste). + +--- + +## 5. Zusammenfassung + +| Thema | Inhalt | +|------|--------| +| **Datenmodell** | AutomationDefinition unverändert, **keine** templateId. Neu: AutomationTemplate (nur DB, ohne placeholders). Definition aus Template = Kopie des Inhalts; Definition kann als Template gespeichert werden. | +| **Templates** | Von Python in DB; Bootstrap: nur Template-JSON mit `{{KEY:...}}`, **keine scharfen Parameter**. | +| **Placeholders** | Nur in AutomationDefinition; nie im Template. | +| **RBAC Templates** | Sichtbarkeit und CREATE/UPDATE/DELETE gemäss RBAC – Nutzer ihre **eigenen** Templates (z.B. MY); SysAdmin nur **Bootstrap** (initialer Seed). | +| **RBAC Automations** | Unverändert: SysAdmin alle, User nur eigene (Scheduling + Definition). | +| **Backend** | AutomationTemplate CRUD gemäss **RBAC** (Nutzer eigene); Bootstrap nur SysAdmin/System; GET nutzbare Actions mit **Daten pro Action-Definition** (WorkflowActionDefinition), RBAC-gefiltert; JSON-Validierung + Placeholder-Extraktion. | +| **UI** | Verwaltung Automations + Scheduling (existiert); ein **Automation Editor** mit **Modus-Flag**: Template bearbeiten vs. Definition erstellen/bearbeiten; „Als Template speichern“ aus Definition (gemäss RBAC). | + +--- + +## 6. Erledigte Klarstellungen + +- **Keine templateId:** Eine AutomationDefinition wird aus einem Template **erzeugt** (Inhalt kopiert), danach keine Referenz – die Definition wird im Editor bearbeitet und kann sich vom Template unterscheiden. Eine Definition kann ihrerseits **als neues Template gespeichert** werden – Berechtigung gemäss **RBAC**. +- **Bootstrap:** Templates dürfen **keine scharfen Parameter** haben (nur `{{KEY:...}}`). **Nur SysAdmin/System** führt den initialen Bootstrap (Seed aus Python) aus; danach CRUD auf Templates **gemäss RBAC** (Nutzer ihre eigenen). +- **GET nutzbare Actions:** **Daten pro Action-Definition** nutzen (WorkflowActionDefinition) – pro Action verfügbar. +- **Template-Liste und CRUD:** Sichtbarkeit und CREATE/UPDATE/DELETE AutomationTemplate **gemäss RBAC** – Nutzer verwalten ihre **eigenen** Templates; SysAdmin nur Bootstrap. +- **Editor sauber mit Modus-Flag:** Ein Flag im Editor steuert, ob ein **Template** oder eine **Definition** bearbeitet wird. Je nachdem schreibt Speichern in AutomationTemplate oder AutomationDefinition. „Als Template speichern“ erzeugt aus einer Definition ein neues Template – erlaubt für jeden mit RBAC-Recht create auf AutomationTemplate. + +Dieses Dokument kann als Basis für Implementierungs-Tickets (Backend: Modelle, API, Bootstrap; Frontend: Editor mit Modus, Template-Verwaltung) verwendet werden. diff --git a/implementation/doc_automation_templates_impl_frontend.md b/implementation/doc_automation_templates_impl_frontend.md new file mode 100644 index 0000000..2def5d4 --- /dev/null +++ b/implementation/doc_automation_templates_impl_frontend.md @@ -0,0 +1,933 @@ +# Automation Templates – Frontend Implementation Concept + +**Basis:** `doc_automation_templates_db_and_editor_concept.md` + +--- + +## ✅ STATUS: IMPLEMENTIERT (2026-02-03) + +**Backend ist vollständig implementiert:** +- ✅ API: `GET/POST/PUT/DELETE /api/automation-templates` +- ✅ Model: `AutomationTemplate` mit `TextMultilingual` Feldern +- ✅ Navigation: `/workflows/automation-templates` in mainSystem.py +- ✅ API: `GET /api/automations/actions` - Actions-Katalog + +**Frontend implementiert:** +| Komponente | Status | +|------------|--------| +| `automationApi.ts` - Typen + API-Funktionen | ✅ Implementiert | +| `useAutomations.ts` - Hooks (useWorkflowActions) | ✅ Implementiert | +| `AutomationTemplatesPage.tsx` | ✅ Implementiert | +| `AutomationEditor.tsx` | ✅ **NEU** - Full-screen Editor | +| `ActionsPanel.tsx` | ✅ Implementiert | +| `App.tsx` - Route | ✅ Implementiert | +| `pageRegistry.tsx` - Icon | ✅ Implementiert | + +**Neue Dateien:** +- `src/components/AutomationEditor/AutomationEditor.tsx` +- `src/components/AutomationEditor/AutomationEditor.module.css` +- `src/components/AutomationEditor/index.ts` + +--- + +## WICHTIGE HINWEISE + +### FormGenerator: Automatische Multilingual-Unterstützung + +| Komponente | TextMultilingual-Handling | +|------------|---------------------------| +| **FormGeneratorTable** | ✅ **Automatisch** - erkennt TextMultilingual und rendert in User-Sprache | +| **FormGeneratorForm** | ✅ **Automatisch** - mit `type: 'multilingual'` in Attribut-Definition | + +**Kein manueller Code nötig!** Einfach Daten mit TextMultilingual-Feldern übergeben. + +### Bestehende vs. Neue API-Typen + +| Typ | Quelle | Verwendung | +|-----|--------|------------| +| `AutomationTemplate` | `subAutomationTemplates.py` (Legacy) | Alte hardcoded Templates | +| `AutomationTemplateDB` | DB (neu) | Neue datenbankbasierte Templates | + +Die bestehende `fetchAutomationTemplates()` Funktion lädt Legacy-Templates. Für die neuen DB-Templates wird `fetchAutomationTemplatesDB()` verwendet. + +--- + +## 1. Übersicht der Komponenten + +| Komponente | Zweck | Status | +|------------|-------|--------| +| `automationApi.ts` | API-Funktionen für Templates + Actions | Erweitern | +| `useAutomations.ts` | Hooks für Templates + Actions | Erweitern | +| `AutomationsPage.tsx` | Liste der Definitions; öffnet Editor | Erweitern | +| `AutomationTemplatesPage.tsx` | Liste der Templates (CRUD) | **Neu** | +| `AutomationEditor.tsx` | Editor mit Modus-Flag (Definition/Template) | **Neu** | +| `ActionsPanel.tsx` | Actions-Katalog mit Copy/Paste | **Neu** | + +--- + +## 2. API-Erweiterungen (automationApi.ts) + +### 2.1 Neuer Typ: AutomationTemplate (DB-Version, MultiLanguage) + +```typescript +// Multilingual text type (matches backend TextMultilingual) +export interface TextMultilingual { + en: string; + ge?: string; + fr?: string; + it?: string; +} + +// NEW: AutomationTemplate from DB (not the old in-memory structure) +export interface AutomationTemplateDB { + id: string; + label: TextMultilingual; + overview?: TextMultilingual; + template: string; // JSON string with {{KEY:...}} placeholders + _createdAt?: number; + _createdBy?: string; + _createdByUserName?: string; +} + +// Action definition from backend +export interface WorkflowAction { + method: string; + action: string; + actionId: string; + description: string; + category?: string; + parameters: WorkflowActionParameter[]; + exampleJson: { + execMethod: string; + execAction: string; + execParameters: Record; + execResultLabel: string; + }; +} + +export interface WorkflowActionParameter { + name: string; + type: string; + frontendType: string; + required: boolean; + default?: any; + description: string; + frontendOptions?: string | string[]; +} +``` + +### 2.2 Neue API-Funktionen + +```typescript +// ============================================================================ +// AUTOMATION TEMPLATES (DB) API +// ============================================================================ + +/** + * Fetch all automation templates (RBAC-filtered: own templates) + * Endpoint: GET /api/automation-templates + */ +export async function fetchAutomationTemplatesDB( + request: ApiRequestFunction +): Promise { + const data = await request({ + url: '/api/automation-templates', + method: 'get' + }); + + if (data?.items && Array.isArray(data.items)) { + return data.items; + } + return Array.isArray(data) ? data : []; +} + +/** + * Fetch single template by ID + * Endpoint: GET /api/automation-templates/{id} + */ +export async function fetchAutomationTemplateById( + request: ApiRequestFunction, + templateId: string +): Promise { + try { + return await request({ + url: `/api/automation-templates/${templateId}`, + method: 'get' + }); + } catch (error) { + console.error('Error fetching template:', error); + return null; + } +} + +/** + * Create new automation template + * Endpoint: POST /api/automation-templates + */ +export async function createAutomationTemplateApi( + request: ApiRequestFunction, + templateData: Omit +): Promise { + return await request({ + url: '/api/automation-templates', + method: 'post', + data: templateData + }); +} + +/** + * Update automation template + * Endpoint: PUT /api/automation-templates/{id} + */ +export async function updateAutomationTemplateApi( + request: ApiRequestFunction, + templateId: string, + templateData: Partial +): Promise { + return await request({ + url: `/api/automation-templates/${templateId}`, + method: 'put', + data: templateData + }); +} + +/** + * Delete automation template + * Endpoint: DELETE /api/automation-templates/{id} + */ +export async function deleteAutomationTemplateApi( + request: ApiRequestFunction, + templateId: string +): Promise { + await request({ + url: `/api/automation-templates/${templateId}`, + method: 'delete' + }); +} + +// ============================================================================ +// WORKFLOW ACTIONS API +// ============================================================================ + +/** + * Fetch available workflow actions (RBAC-filtered) + * Endpoint: GET /api/automations/actions + */ +export async function fetchWorkflowActions( + request: ApiRequestFunction +): Promise { + const data = await request({ + url: '/api/automations/actions', + method: 'get' + }); + + return data?.actions || []; +} +``` + +--- + +## 3. Hooks-Erweiterungen (useAutomations.ts) + +### 3.1 Neuer Hook: useAutomationTemplates + +```typescript +/** + * Hook for managing AutomationTemplates (DB) + */ +export function useAutomationTemplates() { + const [templates, setTemplates] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const { request } = useApiRequest(); + const { checkPermission } = usePermissions(); + const [permissions, setPermissions] = useState(null); + + const fetchTemplates = useCallback(async () => { + setLoading(true); + try { + const data = await fetchAutomationTemplatesDB(request); + setTemplates(data); + } catch (e: any) { + setError(e.message); + setTemplates([]); + } finally { + setLoading(false); + } + }, [request]); + + const fetchPermissions = useCallback(async () => { + const perms = await checkPermission('DATA', 'AutomationTemplate'); + setPermissions(perms); + return perms; + }, [checkPermission]); + + const createTemplate = useCallback(async (data: Omit) => { + return await createAutomationTemplateApi(request, data); + }, [request]); + + const updateTemplate = useCallback(async (id: string, data: Partial) => { + return await updateAutomationTemplateApi(request, id, data); + }, [request]); + + const deleteTemplate = useCallback(async (id: string) => { + await deleteAutomationTemplateApi(request, id); + }, [request]); + + return { + templates, + loading, + error, + permissions, + fetchTemplates, + fetchPermissions, + createTemplate, + updateTemplate, + deleteTemplate, + }; +} +``` + +### 3.2 Neuer Hook: useWorkflowActions + +```typescript +/** + * Hook for fetching available workflow actions (RBAC-filtered) + */ +export function useWorkflowActions() { + const [actions, setActions] = useState([]); + const [loading, setLoading] = useState(false); + const { request } = useApiRequest(); + + const fetchActions = useCallback(async () => { + setLoading(true); + try { + const data = await fetchWorkflowActions(request); + setActions(data); + } catch (e) { + console.error('Error fetching actions:', e); + setActions([]); + } finally { + setLoading(false); + } + }, [request]); + + return { actions, loading, fetchActions }; +} +``` + +--- + +## 4. Neue Seite: AutomationTemplatesPage.tsx + +### 4.1 Imports + +```typescript +import React, { useState, useMemo, useEffect } from 'react'; +import { FaSync, FaPlus, FaFileAlt } from 'react-icons/fa'; +import { useLanguage } from '../../providers/language/LanguageContext'; +import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; +import { useAutomationTemplates, AutomationTemplateDB, TextMultilingual } from '../../hooks/useAutomations'; +import { AutomationEditor } from '../../components/AutomationEditor'; // Neu +import styles from '../admin/Admin.module.css'; +``` + +### 4.2 Struktur + +```typescript +/** + * AutomationTemplatesPage + * + * CRUD für AutomationTemplates (eigene Templates verwalten). + * Nutzt FormGeneratorTable wie AutomationsPage. + */ +export const AutomationTemplatesPage: React.FC = () => { + const { + templates, + loading, + permissions, + fetchTemplates, + fetchPermissions, + createTemplate, + updateTemplate, + deleteTemplate, + } = useAutomationTemplates(); + + const [showEditor, setShowEditor] = useState(false); + const [editingTemplate, setEditingTemplate] = useState(null); + + useEffect(() => { + fetchTemplates(); + fetchPermissions(); + }, []); + + // Check permissions + const canCreate = permissions?.create !== 'n'; + const canUpdate = permissions?.update !== 'n'; + const canDelete = permissions?.delete !== 'n'; + + // Open Editor in "Template mode" + const handleEdit = (template: AutomationTemplateDB) => { + setEditingTemplate(template); + setShowEditor(true); + }; + + const handleCreate = () => { + setEditingTemplate(null); // New template + setShowEditor(true); + }; + + const handleSave = async (data: Partial) => { + if (editingTemplate) { + await updateTemplate(editingTemplate.id, data); + } else { + await createTemplate(data as any); + } + setShowEditor(false); + fetchTemplates(); + }; + + // Columns - FormGeneratorTable rendert TextMultilingual automatisch in User-Sprache! + const columns = useMemo(() => [ + { key: 'label', label: 'Label', type: 'string' as const, sortable: true, searchable: true }, + { key: 'overview', label: 'Beschreibung', type: 'string' as const, width: 300 }, + { key: '_createdByUserName', label: 'Erstellt von', type: 'string' as const }, + ], []); + + // Handle delete + const handleDelete = async (template: AutomationTemplateDB) => { + await deleteTemplate(template.id); + fetchTemplates(); + }; + + return ( +
+
+
+

Automation-Vorlagen

+

Verwalten Sie Ihre Workflow-Vorlagen

+
+
+ + {canCreate && ( + + )} +
+
+ +
+ +
+ + {showEditor && ( + setShowEditor(false)} + /> + )} +
+ ); +}; +``` + +--- + +## 5. Neue Komponente: AutomationEditor.tsx + +### 5.1 Konzept: Saubere Lösung via FormGeneratorForm + +**Kernidee:** FormGeneratorForm nutzen für ALLE Felder inkl. Multilingual! + +```typescript +/** + * Attribute-Definitionen je nach Modus + * FormGeneratorForm rendert type:'multilingual' automatisch mit Sprach-Tabs + */ +function getEditorAttributes(mode: 'definition' | 'template'): AttributeDefinition[] { + const baseAttributes: AttributeDefinition[] = [ + { + name: 'template', + label: 'Template JSON', + type: 'textarea', + required: true, + minRows: 15, + maxRows: 30, + description: 'Workflow-Definition als JSON' + } + ]; + + if (mode === 'template') { + return [ + { name: 'label', label: 'Label', type: 'multilingual', required: true }, + { name: 'overview', label: 'Beschreibung', type: 'multilingual' }, + ...baseAttributes + ]; + } else { + return [ + { name: 'label', label: 'Label', type: 'text', required: true }, + { name: 'schedule', label: 'Schedule (Cron)', type: 'text' }, + { name: 'active', label: 'Aktiv', type: 'checkbox' }, + ...baseAttributes, + { name: 'placeholders', label: 'Platzhalter', type: 'textarea', description: 'JSON-Objekt' } + ]; + } +} +``` + +### 5.2 Komponente + +```typescript +interface AutomationEditorProps { + mode: 'definition' | 'template'; + initialData?: AutomationTemplateDB | Automation | null; + onSave: (data: any) => Promise; + onCancel: () => void; +} + +export const AutomationEditor: React.FC = ({ + mode, + initialData, + onSave, + onCancel, +}) => { + const [showActionsPanel, setShowActionsPanel] = useState(false); + + // Daten für FormGeneratorForm vorbereiten + const formData = useMemo(() => { + if (!initialData) return {}; + return { + ...initialData, + template: typeof initialData.template === 'string' + ? initialData.template + : JSON.stringify(initialData.template, null, 2) + }; + }, [initialData]); + + return ( +
+
+
+

{mode === 'template' ? 'Template bearbeiten' : 'Automation bearbeiten'}

+ +
+ +
+ {/* Main Form - FormGeneratorForm macht alles! */} +
+ +
+ + {/* Actions Panel (optional, für Copy/Paste) */} +
+ + {showActionsPanel && } +
+
+
+
+ ); +}; +``` + +**Vorteile dieser Lösung:** +- Kein manueller Code für Multilingual-Inputs +- FormGeneratorForm validiert automatisch +- Konsistent mit dem Rest der Anwendung +- Weniger Code, weniger Bugs + +--- + +## 6. Neue Komponente: ActionsPanel.tsx + +### 6.1 Struktur + +```typescript +interface ActionsPanelProps { + onInsert: (actionJson: string) => void; + onCopy?: (actionJson: string) => void; +} + +export const ActionsPanel: React.FC = ({ onInsert, onCopy }) => { + const { actions, loading, fetchActions } = useWorkflowActions(); + const [filter, setFilter] = useState(''); + const [expandedAction, setExpandedAction] = useState(null); + + useEffect(() => { + fetchActions(); + }, []); + + const filteredActions = useMemo(() => { + if (!filter) return actions; + const lower = filter.toLowerCase(); + return actions.filter(a => + a.method.toLowerCase().includes(lower) || + a.action.toLowerCase().includes(lower) || + a.description.toLowerCase().includes(lower) + ); + }, [actions, filter]); + + // Group by method + const groupedActions = useMemo(() => { + const groups: Record = {}; + filteredActions.forEach(action => { + if (!groups[action.method]) { + groups[action.method] = []; + } + groups[action.method].push(action); + }); + return groups; + }, [filteredActions]); + + const handleCopy = (action: WorkflowAction) => { + const json = JSON.stringify(action.exampleJson, null, 2); + navigator.clipboard.writeText(json); + onCopy?.(json); + }; + + const handleInsert = (action: WorkflowAction) => { + const json = JSON.stringify(action.exampleJson, null, 2); + onInsert(json); + }; + + return ( +
+

Verfügbare Actions

+ + setFilter(e.target.value)} + /> + + {loading ? ( +
Lade Actions...
+ ) : ( +
+ {Object.entries(groupedActions).map(([method, methodActions]) => ( +
+

{method}

+ {methodActions.map(action => ( +
+
setExpandedAction( + expandedAction === action.actionId ? null : action.actionId + )} + > + + {action.action} + + + {action.description} + +
+ + {expandedAction === action.actionId && ( +
+
Parameter:
+
    + {action.parameters.map(p => ( +
  • + {p.name} + {p.required && *} + : {p.description} +
  • + ))} +
+ +
Beispiel JSON:
+
{JSON.stringify(action.exampleJson, null, 2)}
+ +
+ + +
+
+ )} +
+ ))} +
+ ))} +
+ )} +
+ ); +}; +``` + +--- + +## 7. Anpassungen in AutomationsPage.tsx + +### 7.1 Editor-Integration + +Bestehende `handleEditClick` und Template-Auswahl anpassen: + +```typescript +// State for editor +const [showEditor, setShowEditor] = useState(false); +const [editorMode, setEditorMode] = useState<'definition' | 'template'>('definition'); +const [editorInitialData, setEditorInitialData] = useState(null); + +// Edit existing definition +const handleEditClick = async (automation: Automation) => { + const fullAutomation = await fetchAutomationById(automation.id); + setEditorInitialData(fullAutomation); + setEditorMode('definition'); + setShowEditor(true); +}; + +// Create from template +const handleCreateFromTemplate = async (template: AutomationTemplateDB) => { + // Extract label in user's language + const userLang = getUserLanguage(); // e.g., 'de' or 'en' + const label = template.label[userLang] || template.label.en || 'New Automation'; + + // Pre-fill new definition + const newDefinition: Partial = { + label: label, + template: template.template, + placeholders: {}, + schedule: '0 22 * * *', + active: false, + mandateId: mandateId, + featureInstanceId: featureInstanceId, + }; + + setEditorInitialData(newDefinition as Automation); + setEditorMode('definition'); + setShowEditor(true); +}; + +// Save handler from editor +const handleEditorSave = async (data: any) => { + if (editorMode === 'definition') { + if (editorInitialData?.id) { + await handleAutomationUpdate(editorInitialData.id, data); + } else { + await handleAutomationCreate(data); + } + } + // Template save is handled in AutomationTemplatesPage + setShowEditor(false); + refetch(); +}; +``` + +### 7.2 Template-Auswahl Modal anpassen + +Bisherige Template-Auswahl nutzt jetzt `AutomationTemplateDB` statt altes Format: + +```typescript +const [templatesDB, setTemplatesDB] = useState([]); + +const loadTemplates = async () => { + setLoadingTemplates(true); + const data = await fetchAutomationTemplatesDB(request); + setTemplatesDB(data); + setLoadingTemplates(false); +}; + +// In Template-Modal: Display label in current language +// Nutze inline-Zugriff oder FormGeneratorTable für Liste +const { currentLanguage } = useLanguage(); +const getLang = (ml: TextMultilingual | undefined) => + ml?.[currentLanguage as keyof TextMultilingual] || ml?.en || ''; + +{templatesDB.map(template => ( +
handleCreateFromTemplate(template)}> +

{getLang(template.label)}

+

{getLang(template.overview)}

+
+))} +``` + +--- + +## 8. Navigation / Routing + +### Architektur-Hinweis: ObjectKey-Formate + +| Kontext | Format | Beispiel | Verwendung | +|---------|--------|----------|------------| +| Backend Navigation | `ui.system.xxx` | `ui.system.automations` | NAVIGATION_SECTIONS in mainSystem.py | +| Frontend Icons | `page.system.xxx` | `page.system.automations` | pageRegistry.tsx PAGE_ICONS | +| RBAC DATA | `data.automation.xxx` | `data.automation.AutomationTemplate` | Tabellen-Zugriffsregeln | +| RBAC UI | `ui.feature.automation.xxx` | `ui.feature.automation.templates` | Feature-UI-Objekte | + +Die Sidebar-Navigation kommt vom **Backend** (`/api/navigation`), nicht aus pageRegistry. PageRegistry liefert nur die **Icons** zu den vom Backend gelieferten Items. + +--- + +### 8.1 Backend: mainSystem.py (NAVIGATION_SECTIONS) + +**WICHTIG:** Die Navigation wird vom Backend gesteuert! In `gateway/modules/system/mainSystem.py` unter `NAVIGATION_SECTIONS` im Abschnitt "workflows" hinzufügen: + +```python +{ + "id": "automation-templates", + "objectKey": "ui.system.automation-templates", + "label": {"en": "Templates", "de": "Vorlagen", "fr": "Modèles"}, + "icon": "FaFileAlt", # Oder FaCopy für Vorlagen + "path": "/workflows/automation-templates", + "order": 35, # Nach automations (30) + "public": True, +}, +``` + +### 8.2 Frontend: App.tsx + +Route innerhalb des `` Blocks hinzufügen: + +```typescript +// In App.tsx, im workflows-Block: + + } /> + } /> + } /> + } /> {/* NEU */} + +``` + +**Import hinzufügen:** +```typescript +import { AutomationTemplatesPage } from './pages/workflows/AutomationTemplatesPage'; +``` + +### 8.3 Frontend: pageRegistry.tsx + +**WICHTIG:** Format ist `page.system.xxx`, nicht `ui.system.xxx`! + +```typescript +// Import hinzufügen (FaFileAlt ist bereits importiert): +// import { ..., FaFileAlt, ... } from 'react-icons/fa'; + +// In PAGE_ICONS hinzufügen: +'page.system.automation-templates': , +``` + +### 8.4 Frontend: pages/workflows/index.ts + +Export hinzufügen: +```typescript +export { PlaygroundPage } from './PlaygroundPage'; +export { WorkflowsPage } from './WorkflowsPage'; +export { AutomationsPage } from './AutomationsPage'; +export { AutomationTemplatesPage } from './AutomationTemplatesPage'; // NEU +``` + +### 8.5 Locales (de.ts, en.ts, fr.ts) + +Übersetzungen hinzufügen: + +**de.ts:** +```typescript +'nav.automation-templates': 'Vorlagen', +'automation-templates.title': 'Automation-Vorlagen', +'automation-templates.create': 'Neue Vorlage', +'automation-templates.edit': 'Vorlage bearbeiten', +'automation-templates.empty': 'Keine Vorlagen vorhanden', +``` + +**en.ts:** +```typescript +'nav.automation-templates': 'Templates', +'automation-templates.title': 'Automation Templates', +'automation-templates.create': 'New Template', +'automation-templates.edit': 'Edit Template', +'automation-templates.empty': 'No templates available', +``` + +--- + +## 9. Hilfsfunktionen + +> **Hinweis:** `getMultilingualText()` ist NICHT nötig - FormGeneratorTable rendert TextMultilingual automatisch in der User-Sprache via `formatTextMultilingual()` intern. + +### 9.1 extractPlaceholdersFromJson + +```typescript +/** + * Extract {{KEY:name}} placeholders from JSON string + */ +export function extractPlaceholdersFromJson(jsonString: string): string[] { + const regex = /\{\{KEY:(\w+)\}\}/g; + const keys: string[] = []; + let match; + while ((match = regex.exec(jsonString)) !== null) { + if (!keys.includes(match[1])) { + keys.push(match[1]); + } + } + return keys; +} +``` + +--- + +## 10. Zusammenfassung der Dateien + +| Datei | Änderung | +|-------|----------| +| `api/automationApi.ts` | Neue Typen + API-Funktionen für Templates (DB) und Actions | +| `hooks/useAutomations.ts` | Neue Hooks: `useAutomationTemplates`, `useWorkflowActions` | +| `pages/workflows/AutomationsPage.tsx` | Editor-Integration, Template-Auswahl mit DB-Templates | +| `pages/workflows/AutomationTemplatesPage.tsx` | **Neu**: CRUD für eigene Templates | +| `components/AutomationEditor/AutomationEditor.tsx` | **Neu**: Editor mit Modus-Flag | +| `components/AutomationEditor/ActionsPanel.tsx` | **Neu**: Actions-Katalog | +| `App.tsx` | Neue Route für AutomationTemplatesPage | +| `config/pageRegistry.tsx` | Icon für neue Seite | + +--- + +## 11. Implementierungsreihenfolge + +1. **API-Typen und Funktionen** (`automationApi.ts`) +2. **Hooks** (`useAutomations.ts` erweitern) +3. **ActionsPanel** (unabhängig testbar) +4. **AutomationEditor** (Kern-Komponente) +5. **AutomationTemplatesPage** (CRUD für Templates) +6. **AutomationsPage anpassen** (Editor-Integration) +7. **Routing** (App.tsx, Navigation) diff --git a/implementation/doc_automation_templates_impl_gateway.md b/implementation/doc_automation_templates_impl_gateway.md new file mode 100644 index 0000000..e958d9f --- /dev/null +++ b/implementation/doc_automation_templates_impl_gateway.md @@ -0,0 +1,466 @@ +# 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 diff --git a/implementation/implementation_content_handling_with_dynamic_ai_v2.md b/implementation/implementation_content_handling_with_dynamic_ai_v2.md index 1fcd499..e433308 100644 --- a/implementation/implementation_content_handling_with_dynamic_ai_v2.md +++ b/implementation/implementation_content_handling_with_dynamic_ai_v2.md @@ -147,7 +147,7 @@ async def _callWithContentParts(self, request: AiCallRequest) -> AiCallResponse: return AiCallResponse( content=mergedContent, modelName="multiple", - priceUsd=sum(r.priceUsd for r in allResults), + priceCHF=sum(r.priceCHF for r in allResults), processingTime=sum(r.processingTime for r in allResults), bytesSent=sum(r.bytesSent for r in allResults), bytesReceived=sum(r.bytesReceived for r in allResults), @@ -185,7 +185,7 @@ async def _processContentPartWithFallback(self, contentPart: ContentPart, prompt # Merge chunk results mergedContent = self._mergeChunkResults(chunkResults) - totalPrice = sum(r.priceUsd for r in chunkResults) + totalPrice = sum(r.priceCHF for r in chunkResults) totalTime = sum(r.processingTime for r in chunkResults) totalBytesSent = sum(r.bytesSent for r in chunkResults) totalBytesReceived = sum(r.bytesReceived for r in chunkResults) @@ -195,7 +195,7 @@ async def _processContentPartWithFallback(self, contentPart: ContentPart, prompt return AiCallResponse( content=mergedContent, modelName=model.name, - priceUsd=totalPrice, + priceCHF=totalPrice, processingTime=totalTime, bytesSent=totalBytesSent, bytesReceived=totalBytesReceived, @@ -316,7 +316,7 @@ async def _processPartsWithMapping(self, extractionResult: List[ContentExtracted "resultSize": len(response.content), "typeGroup": part.typeGroup, "modelName": response.modelName, - "priceUsd": response.priceUsd + "priceCHF": response.priceCHF } ) diff --git a/implementation/implementation_refactor_stats-unified.md b/implementation/implementation_refactor_stats-unified.md index 236a69d..2c34eb1 100644 --- a/implementation/implementation_refactor_stats-unified.md +++ b/implementation/implementation_refactor_stats-unified.md @@ -20,7 +20,7 @@ Owner: Workflow/Data Layer - Example values: "action.outlook.readMails", "ai.process.document.name" - Add: engine: str - Example values: "ai.anthropic.35", "ai.tavily.basic", "renderer.docx" - - Add: priceUsd: float (calculated price in USD for the operation) + - Add: priceCHF: float (calculated price in USD for the operation) - ChatWorkflow - stats: change from Optional[ChatStat] to List[ChatStat] (default []) @@ -52,7 +52,7 @@ DB logic: ## Service Layer Changes (modules/services/serviceWorkflow/mainServiceWorkflow.py) - Replace storeWorkflowStat(workflow, statData) to: - - Coerce statData to ChatStat fields (process, engine, priceUsd, bytesSent, bytesReceived, processingTime, errorCount) --> to handover pydantic model (with id empty), not the single values + - Coerce statData to ChatStat fields (process, engine, priceCHF, bytesSent, bytesReceived, processingTime, errorCount) --> to handover pydantic model (with id empty), not the single values - Set id and workflowId - Persist via interfaceDbChat.createWorkflowStat - Append to workflow.stats in memory @@ -65,10 +65,10 @@ DB logic: ## Instrumentation Points (where to emit stats) 1) AI Calls (modules/interfaces/interfaceAiObjects.py) - - Each AI call measures processing time, calculates priceUsd, and tracks bytes sent/received - - Returns standardized AiCallResponse with priceUsd, processingTime, bytesSent, bytesReceived, errorCount included (to adapt pydantic model) + - Each AI call measures processing time, calculates priceCHF, and tracks bytes sent/received + - Returns standardized AiCallResponse with priceCHF, processingTime, bytesSent, bytesReceived, errorCount included (to adapt pydantic model) - All AI functions (call, callImage, generateImage, webQuery, etc.) return this standardized response - - Model-specific pricing functions calculate priceUsd based on (processingTime, bytesSent, bytesReceived) + - Model-specific pricing functions calculate priceCHF based on (processingTime, bytesSent, bytesReceived) 2) Service Layer (modules/services/serviceWorkflow/mainServiceWorkflow.py) - Receives AiCallResponse from AI interface @@ -78,7 +78,7 @@ DB logic: 3) Extraction/Generation Services - services/serviceExtraction/mainServiceExtraction.py and related extractors - services/serviceGeneration/* (document rendering/generation) - - Emit ChatStat per completed operation with process, engine, processingTime, bytes, priceUsd, etc. + - Emit ChatStat per completed operation with process, engine, processingTime, bytes, priceCHF, etc. 4) Workflow-level summaries (optional) - At workflow completion, compute a summary stat (aggregated bytes/costs) if desired @@ -90,7 +90,7 @@ DB logic: Update AiCallResponse to include standardized stats fields: - Remove: usedTokens, costEstimate -- Add: priceUsd: float (calculated price in USD) +- Add: priceCHF: float (calculated price in USD) - Add: processingTime: float (duration in seconds) - Add: bytesSent: int (input data size in bytes) - Add: bytesReceived: int (output data size in bytes) @@ -100,7 +100,7 @@ Update AiCallResponse to include standardized stats fields: Each AI model should implement a pricing calculation function that takes the operation parameters and returns the cost in USD: -- Function signature: `_calculatePriceUsd(processingTime: float, bytesSent: int, bytesReceived: int) -> float` +- Function signature: `_calculatepriceCHF(processingTime: float, bytesSent: int, bytesReceived: int) -> float` - Parameters: - `processingTime`: Duration of the operation in seconds - `bytesSent`: Size of input data in bytes @@ -136,7 +136,7 @@ Example implementations: ### Phase 1: Core Data Model Changes 1. **Update ChatStat model** (modules/datamodels/datamodelChat.py) - Remove: successRate, tokenCount, tokenPriceUnit, tokenPriceAmount, messageId - - Add: process: str, engine: str, priceUsd: float + - Add: process: str, engine: str, priceCHF: float - Keep: id, workflowId, processingTime, bytesSent, bytesReceived, errorCount 2. **Update ChatWorkflow model** (modules/datamodels/datamodelChat.py) @@ -148,7 +148,7 @@ Example implementations: 4. **Update AiCallResponse model** (modules/datamodels/datamodelAi.py) - Remove: usedTokens, costEstimate - - Add: priceUsd: float, processingTime: float, bytesSent: int, bytesReceived: int, errorCount: int + - Add: priceCHF: float, processingTime: float, bytesSent: int, bytesReceived: int, errorCount: int ### Phase 2: Database Interface Changes 5. **Update interfaceDbChatObjects.py** @@ -159,14 +159,14 @@ Example implementations: ### Phase 3: AI Interface Layer Changes 6. **Add model-specific pricing functions** (modules/interfaces/interfaceAiObjects.py) - - Add _calculatePriceUsd() method to each model in aiModels registry + - Add _calculatepriceCHF() method to each model in aiModels registry - Implement pricing logic for each connector type (OpenAI, Anthropic, Perplexity, Tavily) 7. **Update all AI call methods** (modules/interfaces/interfaceAiObjects.py) - Add timing measurement (start/end timestamps) - Calculate bytesSent (input data size) - Calculate bytesReceived (output data size) - - Call model-specific _calculatePriceUsd() + - Call model-specific _calculatepriceCHF() - Return standardized AiCallResponse with all stats fields 8. **Update specific AI methods:** @@ -207,7 +207,7 @@ Example implementations: - Update UI polling to handle stats array 15. **Add comprehensive logging** - - Debug logs for each stat emission (process, engine, bytes, priceUsd) + - Debug logs for each stat emission (process, engine, bytes, priceCHF) - Error handling for stats emission failures ### Phase 7: Cleanup and Validation @@ -231,7 +231,7 @@ Example implementations: - **Centralized measurement**: AI interface layer handles all timing and byte counting, ensuring consistency. - **Standardized responses**: All AI calls return AiCallResponse with complete stats data. -- **Model-specific pricing**: Each model implements `_calculatePriceUsd()` with its own pricing strategy. +- **Model-specific pricing**: Each model implements `_calculatepriceCHF()` with its own pricing strategy. - **Service layer simplicity**: Service layer just creates ChatStat objects from AiCallResponse data. - **Append-only stats**: Simplifies auditing and invoicing; no implicit overwrites. - **Workflow-scoped stats**: All stats attached to workflow, not messages; UI gets complete history.