664 lines
36 KiB
Markdown
664 lines
36 KiB
Markdown
<!-- status: done -->
|
||
<!-- started: 2026-04-29 -->
|
||
<!-- implemented: 2026-04-29 -->
|
||
<!-- reviewed: 2026-04-29 (critical review passed, corrections applied) -->
|
||
<!-- completed: 2026-05-01 -->
|
||
<!-- component: gateway, ui-nyla -->
|
||
|
||
# AI Reports: Generisches Style-Management, AI-Call-Konfiguration, Inline-Bilder
|
||
|
||
> **Nachtrag 2026-06-02 (A3-Reconciliation, SUPERSEDED):** Der damalige Entscheid "Theme als reiner
|
||
> Prompt-Hint, keine Renderer-Theme-Presets" wurde teilweise revidiert. `documentTheme`
|
||
> wurde als agent-ueberschreibbare Preset-Bibliothek (`THEME_PRESETS`/`resolveTheme`) verkabelt.
|
||
> **Dieser Schritt wurde am 2026-06-03 rueckgaengig gemacht (siehe unten).**
|
||
|
||
> **Nachtrag 2026-06-03 (Style-System-Overhaul):** `THEME_PRESETS`, `resolveTheme()` und der
|
||
> `documentTheme`-Parameter wurden vollstaendig entfernt — zurueck zum ADR-Originalentscheid
|
||
> (kein Theme-Katalog). Stattdessen: AI-getriebene Style-Enhancement (`_enhanceStyleWithAi`
|
||
> in `mainServiceGeneration.py`) analysiert Dokumenttitel, -typ und User-Request und liefert
|
||
> ein Style-Delta auf den modernisierten `DEFAULT_STYLE` (Cursor/VS-Code-Aesthetik). Neue
|
||
> Features: Smart Table Styling (per-table `tableStyle`-Overrides, `borderStyle` grid/horizontal/none,
|
||
> `bandingEnabled`, `cellPaddingPt`, automatische `columnAlignments`), `coverPage`- und
|
||
> `caption`-Style-Sektionen. Kanonische Referenz: `b-reference/platform-core/document-rendering.md`.
|
||
|
||
## Beschreibung und Kontext
|
||
|
||
Drei verzahnte Themen rund um AI-erzeugte Reports und AI-Calls in
|
||
Workflows / Workspace:
|
||
|
||
1. **Style-Management generisch und Agent-getrieben.** Die heutigen
|
||
Renderer (DOCX, XLSX, PPTX, HTML, PDF) haben hardcoded Farben (z.B.
|
||
DOCX-Tabellen-Header `4472C4`), feste Bildbreiten, hartes
|
||
`lang="en"` im HTML. Es gibt keinen Mechanismus, dass ein Agent
|
||
pro Bericht Styling, Logo, Schrift, Farben mitgeben kann. Wir
|
||
wollen **kein** fixes Template-/Theme-Mandate-CI-Konzept, sondern
|
||
ein **generisches Style-Schema**, das der Agent fuellt -- die
|
||
Renderer mappen daraus.
|
||
2. **Per-Call AI-Konfiguration: Neutralisierung + erlaubte Modelle.**
|
||
Heute laufen AI-Calls aus Workflows ueber das zentrale AI-Gate
|
||
(`mainServiceAi.py`); Neutralisierung wird via Feature/Session/Request-
|
||
Flags gewaehlt, aber im FlowEditor ist sie pro Node nicht explizit
|
||
einstellbar. Eine **Whitelist erlaubter Modelle pro Node** existiert
|
||
ueberhaupt nicht. Im AI-Workspace soll beides automatisch aus dem
|
||
Workspace-Kontext kommen, im FlowEditor pro Node.
|
||
3. **Inline-Bilder ueberall.** Die heutige MD->JSON-Pipeline
|
||
(`_mediaTools._markdownToDocumentJson`,
|
||
`subDocumentUtility.markdownToDocumentJson`) flacht alles auf Strings:
|
||
Bilder muessen eine eigene Markdown-Zeile sein. Ein Bild in einer
|
||
Tabellenzelle, mitten in einem Paragraph, in einer Listenzeile etc.
|
||
geht heute nicht.
|
||
|
||
Zusaetzliche technische Schuld:
|
||
|
||
- MD->JSON ist **doppelt** implementiert (`_mediaTools` und
|
||
`subDocumentUtility`) -- Drift-Risiko.
|
||
- Agent ruft `renderReport` ohne `aiService` auf (`_mediaTools.py`
|
||
282-287), KI-Style-Anpassung im Renderer-Pfad ist damit inaktiv. Das
|
||
wird durch den neuen Agent-Style-Pfad ohnehin obsolet.
|
||
|
||
**Geschaeftstreiber:** Reports sind das visuelle Endprodukt, das User mit
|
||
Kunden teilen. Ein Agent, der pro Bericht Layout-Entscheidungen trifft
|
||
(Finanzreport vs. Marketing-Flyer vs. neutrales Memo), hebt die
|
||
wahrgenommene Qualitaet deutlich. Per-Node-Neutralisierung +
|
||
Modell-Whitelist machen Compliance und Kostenkontrolle pro Workflow
|
||
explizit und auditierbar.
|
||
|
||
## Fokus und kritische Details
|
||
|
||
- Markdown bleibt User-/Agent-Eingangs-Schicht. JSON ist nur das interne
|
||
deterministisch erzeugte Zwischenformat. **Keine** Aenderung am MD-
|
||
First-Konzept.
|
||
- Style-Schema: muss generisch genug sein, dass die heutigen Renderer
|
||
alle relevanten Visual-Konstanten daraus ziehen, aber **kein**
|
||
branchenspezifisches Preset-Inventar mit harter Auswahl.
|
||
- Defaults sind wichtig: wenn der Agent nichts angibt, muss der Renderer
|
||
eine vernuenftige neutrale Default-Style-Set verwenden.
|
||
- Inline-Bilder erfordern Schema-Aenderung beim `cell`/`paragraph`/
|
||
`bullet_list_item`-Content: weg von `string`, hin zu `inline-Run-
|
||
Liste` (`text` | `image` | `link` | `bold` | `italic`).
|
||
- **KEIN Backwards-Compat:** Altes String-basiertes Format wird NICHT
|
||
weiter unterstuetzt. Renderer bekommen NUR das neue Inline-Run-
|
||
Modell. Alte Workflow-Runs mit altem JSON-Output brechen bei erneutem
|
||
Rendering -- akzeptabler Trade-off fuer saubere Codebasis.
|
||
- Per-Node AI-Konfiguration darf das **zentrale AI-Gate** nicht
|
||
umgehen: das Gate bleibt einzige Stelle, die Neutralisierung wirklich
|
||
durchsetzt. Node-Parameter werden in den `AiCallRequest` reingefuettert
|
||
und folgen den heutigen Praezedenzregeln (Feature -> Session -> Request,
|
||
OR-Logik fuer Neutralisierung).
|
||
|
||
## Ziel und Nicht-Ziele
|
||
|
||
- Ziel:
|
||
- **Style:** Generisches Style-Schema, vom Agent pro Render-Call
|
||
fuellbar (`metadata.style`). Renderer mappen einheitlich. Sensible
|
||
Defaults wenn nichts gesetzt.
|
||
- **AI-Call-Konfig:** Pro `ai.*`-Node im FlowEditor zwei neue Parameter
|
||
`requireNeutralization` (boolean) und `allowedModels` (Liste).
|
||
Workspace-Kontext fuellt sie automatisch fuer Workspace-Calls.
|
||
Validierung am AI-Gate (Whitelist-Check vor Modellwahl).
|
||
- **Inline-Bilder:** Bilder in Paragraph, Tabellenzelle, Listenzeile,
|
||
Code-Block-Caption etc. moeglich. MD-Syntax erkennt sie an
|
||
beliebiger Position. Renderer rendert sie inline wo das Format es
|
||
kann; sonst dokumentierter Fallback.
|
||
- **Konsolidierung:** 1× MD->JSON-Pipeline (Drift weg).
|
||
- NICHT: Komplett neuer Renderer (z.B. WeasyPrint statt reportlab).
|
||
- NICHT: Native XLSX-Charts via openpyxl ChartObjects (separater Plan).
|
||
- NICHT: Mandate-CI-Konzept, Logo-Inheritance, Theme-Presets.
|
||
- NICHT: User-konfigurierbare Custom-Themes via Admin-UI.
|
||
- NICHT: Aenderung am bestehenden Neutralisierungs-Engine; nur Eingangs-
|
||
Pfad ergaenzen.
|
||
- NICHT: Backwards-Compat fuer altes JSON-Format (Clean-Break).
|
||
|
||
## Betroffene Module
|
||
|
||
- Gateway:
|
||
- `platform-core/modules/serviceCenter/services/serviceAgent/coreTools/_mediaTools.py`
|
||
-- `_markdownToDocumentJson` (nested in `_registerMediaTools`, Z. 27-163)
|
||
entfernen, Aufruf auf `subDocumentUtility.markdownToDocumentJson`
|
||
umleiten. Image-Resolution-Logik (Z. 241-277, loest fileId -> base64
|
||
aus KnowledgeStore/Chat) VERBLEIBT in `_mediaTools` als Post-
|
||
Processing-Schritt nach dem Parser-Aufruf.
|
||
`renderDocument`-Tool-Schema ergaenzt um optionalen `style`-Parameter.
|
||
- `platform-core/modules/serviceCenter/services/serviceGeneration/subDocumentUtility.py`
|
||
-- alleinige MD->JSON-Funktion mit Inline-Run-Modell.
|
||
- `platform-core/modules/serviceCenter/services/serviceGeneration/mainServiceGeneration.py`
|
||
-- `_resolveStyle(metadata.style) -> ResolvedStyle` (Defaults +
|
||
Agent-Overrides).
|
||
- `platform-core/modules/serviceCenter/services/serviceGeneration/renderers/*`
|
||
-- alle 5 Renderer auf Style-Lookup umstellen, Inline-Run-Renderer.
|
||
NUR neues Format (InlineRun); altes String-Format wird nicht mehr
|
||
unterstuetzt.
|
||
- `platform-core/modules/datamodels/datamodelJson.py` -- Schema-Erweiterung
|
||
fuer Inline-Runs und `cellContent`.
|
||
- `platform-core/modules/datamodels/datamodelAi.py` --
|
||
`AiCallOptions.allowedModels: Optional[List[str]]` (Whitelist;
|
||
analog `allowedProviders` Z. 164).
|
||
- `platform-core/modules/serviceCenter/services/serviceAi/mainServiceAi.py`
|
||
-- `_calculateEffectiveModels()` (Z. ~1196, analog zu
|
||
`_calculateEffectiveProviders`); Whitelist-Check vor Modellwahl,
|
||
Fail-fast wenn keiner uebrig. `_ServicesAdapter.__getattr__`
|
||
(Z. 88-90) um `"allowedModels"` ergaenzen.
|
||
- `platform-core/modules/features/graphicalEditor/nodeDefinitions/ai.py`
|
||
-- Standardparameter `requireNeutralization` + `allowedModels` zu
|
||
allen `ai.*`-Nodes (gemeinsamer Helper `_AI_COMMON_PARAMS`).
|
||
- `platform-core/modules/workflows/methods/methodAi/actions/*.py` -- Node-
|
||
Parameter durchreichen in den `AiCallRequest`.
|
||
- `platform-core/modules/serviceCenter/services/serviceAgent/conversationManager.py`
|
||
-- Workspace-Kontext-Defaults fuer `requireNeutralization` /
|
||
`allowedModels` aus Workspace-Config in `ServiceCenterContext`
|
||
setzen.
|
||
- Frontend:
|
||
- FlowEditor: Pro AI-Node die zwei neuen Parameter im Properties-
|
||
Panel anzeigen (`requireNeutralization` Toggle, `allowedModels`
|
||
Multi-Select). Hilfetexte.
|
||
- Workspace-Settings: bestehender Neutralisierungs-Toggle bleibt;
|
||
neuer Multi-Select fuer `allowedModels` (Default: alle verfuegbaren
|
||
Modelle des Mandate).
|
||
- HINWEIS: Request-Model heisst `WorkspaceInputRequest` (NICHT
|
||
`WorkspaceUserInput`; siehe `routeFeatureWorkspace.py` Z. 102-113).
|
||
- Style-Vorschau (klein): Optional Snippet im FlowEditor, das den
|
||
`style`-Parameter eines `file.create`/`renderDocument`-Nodes
|
||
visualisieren kann (deferred).
|
||
- DB-Migration: nein im engeren Sinne. `Workflow`-Tabelle bekommt keinen
|
||
neuen Pflicht-Spalten (Node-Parameter leben im Graph-JSON).
|
||
`WorkspaceSettings.allowedModels`-Feld additiv via Auto-Init.
|
||
|
||
## Style-Schema (generisch, Agent-getrieben)
|
||
|
||
Der Agent uebergibt einen `style`-Block in `metadata` (alle Felder
|
||
optional, Renderer fallen auf Defaults zurueck):
|
||
|
||
```json
|
||
{
|
||
"metadata": {
|
||
"language": "de",
|
||
"documents": [...],
|
||
"style": {
|
||
"fonts": {
|
||
"primary": "Calibri",
|
||
"monospace": "Consolas"
|
||
},
|
||
"colors": {
|
||
"primary": "#0F3D7A",
|
||
"secondary": "#1F2937",
|
||
"accent": "#E11D48",
|
||
"background": "#FFFFFF"
|
||
},
|
||
"headings": {
|
||
"h1": { "sizePt": 24, "weight": "bold", "color": "#0F3D7A", "spaceBeforePt": 12, "spaceAfterPt": 6 },
|
||
"h2": { "sizePt": 18, "weight": "bold", "color": "#0F3D7A" },
|
||
"h3": { "sizePt": 14, "weight": "bold" },
|
||
"h4": { "sizePt": 12, "weight": "bold" }
|
||
},
|
||
"paragraph": { "sizePt": 11, "lineSpacing": 1.15 },
|
||
"table": {
|
||
"headerBg": "#0F3D7A",
|
||
"headerFg": "#FFFFFF",
|
||
"rowBandingEven": "#F5F8FC",
|
||
"rowBandingOdd": "#FFFFFF",
|
||
"borderColor": "#CBD5E1",
|
||
"borderWidthPt": 0.5
|
||
},
|
||
"list": { "bulletChar": "•", "indentPt": 18 },
|
||
"image": { "defaultWidthPt": 480, "alignment": "center" },
|
||
"page": {
|
||
"format": "A4",
|
||
"marginsPt": { "top": 60, "bottom": 60, "left": 60, "right": 60 },
|
||
"showPageNumbers": true,
|
||
"headerHeight": 30,
|
||
"footerHeight": 30,
|
||
"headerLogo": { "fileId": "abc", "alignment": "right", "heightPt": 24 },
|
||
"headerText": "Quartalsbericht Q3/2026",
|
||
"footerText": "Vertraulich -- Seite {page}/{pages}"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**Logo:** Es gibt keinen Mandate-Inheritance-Pfad. Wenn ein Logo in den
|
||
Bericht soll, gibt der Agent eine `fileId` an (Datei muss zuvor via
|
||
`generateImage`/`writeFile` oder DataSource-Download erzeugt sein und
|
||
liegt bereits als `FileItem` vor).
|
||
|
||
**Defaults (`platform-core/modules/serviceCenter/services/serviceGeneration/styleDefaults.py`):**
|
||
neutrale Default-Werte fuer alle Felder. `_resolveStyle(agentStyle) ->
|
||
ResolvedStyle` macht ein deep-merge `defaults <- agentStyle`. Renderer
|
||
sieht immer eine vollstaendig aufgeloeste Struktur.
|
||
|
||
**Generische "Style-Komponenten" auf Renderer-Seite:** Pro visuellem
|
||
Element (heading, paragraph, table, listItem, image, codeBlock, page)
|
||
existiert eine Renderer-Funktion, die ausschliesslich die `ResolvedStyle`
|
||
und den Content-Knoten konsumiert. So ist die Implementierung pro
|
||
Format gleichfoermig (DOCX/XLSX/PPTX/HTML/PDF -- alle haben ein
|
||
`renderHeading(headingNode, style.headings.hN)` etc.).
|
||
|
||
## Inline-Run-Modell (fuer Inline-Bilder ueberall)
|
||
|
||
Heute: `paragraph.text: str`, `cell: str`, `bullet_list.items: [str]`.
|
||
|
||
Neu: `inlineRuns: List[InlineRun]` mit
|
||
```json
|
||
{ "type": "text" | "image" | "link" | "bold" | "italic" | "code",
|
||
"value": "...",
|
||
"fileId": "...", // bei type=image
|
||
"widthPt": 96, // optional bei type=image, default klein/inline
|
||
"href": "..." // bei type=link
|
||
}
|
||
```
|
||
|
||
Wenn der MD-Parser nur Text findet, entsteht ein einzelner Run mit
|
||
`type: text`. Renderer bekommen einen Helper
|
||
`_renderInlineRuns(runs, style, container)`, der pro Run das richtige
|
||
Element produziert (Text-Run mit Style, Inline-Image-Anchor,
|
||
Hyperlink-Run, etc.). **Kein** altes `text: str`-Feld -- Renderer
|
||
arbeiten ausschliesslich mit `inlineRuns`.
|
||
|
||
**Markdown-Erkennung:** `` und ``
|
||
mitten in einem Paragraph oder in einer Tabellenzelle wird vom Parser
|
||
zu einem `image`-Run. Listen-Items dasselbe.
|
||
|
||
**Renderer-Faehigkeiten (Matrix):**
|
||
|
||
| Format | Inline-Bild im Paragraph | Inline-Bild in Zelle | Inline-Bild in Listen-Item | Fallback |
|
||
|--------|--------------------------|----------------------|-----------------------------|----------|
|
||
| DOCX | nativ via `add_picture` | nativ via Cell-Paragraph | nativ | -- |
|
||
| XLSX | Bild ueber Zelle anchorn | nativ Cell-Anchor | -- (kein Listen-Konzept) | Bild unter Zeile |
|
||
| HTML | trivial `<img>` | trivial `<img>` in `<td>` | trivial `<img>` in `<li>` | -- |
|
||
| PPTX | nicht nativ in Text-Run | begrenzt | -- | Bild als separate Shape unter Text |
|
||
| PDF | reportlab Inline-Image | reportlab Cell-Image | begrenzt | Bild in eigenem Flowable unter Text |
|
||
|
||
## AI-Call-Konfiguration pro Node (Neutralisierung + Modell-Whitelist)
|
||
|
||
### Was es heute schon gibt (wichtig fuer Plan-Verstaendnis)
|
||
|
||
- **`AiCallRequest.requireNeutralization`** existiert (`datamodelAi.py` Z. 177).
|
||
- **`AiCallOptions.allowedProviders`** existiert (`datamodelAi.py` Z. 164) --
|
||
Provider-Whitelist auf Anbieter-Ebene (z.B. `anthropic`, `openai`,
|
||
`service-llm-private`).
|
||
- **`Workflow.allowedProviders`** wird ueber `__getattr__` durchgereicht
|
||
(`mainServiceAi.py` Z. 89).
|
||
- **`_calculateEffectiveProviders()`** macht RBAC ∩ Workflow.allowedProviders
|
||
und schreibt das in `request.options.allowedProviders` (`mainServiceAi.py`
|
||
Z. 175-178).
|
||
- **`WorkspaceInputRequest.allowedProviders`** + **`requireNeutralization`**
|
||
werden vom Frontend pro Request mitgegeben
|
||
(`routeFeatureWorkspace.py` Z. 111-113) und in den
|
||
`ServiceCenterContext` gesetzt (Z. 713-719).
|
||
- **`ServiceCenterContext.requireNeutralization`** existiert (`context.py`
|
||
Z. 23).
|
||
- **`GET /api/system/ai-models`** liefert die verfuegbaren Modelle
|
||
(`routeSystem.py` Z. 937).
|
||
|
||
### Ziele dieses Plans (additiv zur bestehenden Provider-Logik)
|
||
|
||
Wir fuehren `allowedModels` als **feinere Granularitaet neben** dem
|
||
bestehenden `allowedProviders` ein -- nicht als Ersatz. Ein Provider
|
||
darf erlaubt sein, aber nur bestimmte Modelle dieses Providers
|
||
nutzbar (z.B. Provider `openai` erlaubt, aber nur `gpt-5-mini` nicht
|
||
`gpt-5`).
|
||
|
||
- **Pro `ai.*`-Node im FlowEditor (NEU):**
|
||
- `requireNeutralization` (`boolean`, Default `false`).
|
||
- `allowedModels` (`list[string]`, Default `[]` = "alle").
|
||
- (`allowedProviders` koennte spaeter analog folgen -- bewusst NICHT in
|
||
diesem Plan, weil heute nur Workflow-Level eingestellt wird.)
|
||
- **Im AI-Workspace (Default-Persistenz):**
|
||
- `requireNeutralization`: heute schon im `WorkspaceUserInput` --
|
||
bleibt, jetzt aber **persistent** als User-Default.
|
||
- `allowedProviders`: heute schon im `WorkspaceUserInput` -- bleibt,
|
||
jetzt ebenfalls persistent als User-Default.
|
||
- `allowedModels` (NEU): zusaetzliches Feld in `WorkspaceUserInput` +
|
||
UI-Feld in den Workspace-Einstellungen.
|
||
- **Persistenz-Entscheidung (siehe Entscheidungs-Tabelle):** Phase 5a
|
||
kommt mit neuem Endpoint `GET/PUT /api/workspace/{instanceId}/user-
|
||
settings` fuer ALLE DREI Felder als User-spezifische Defaults pro
|
||
Workspace -- damit der User die Settings nicht bei jedem Browser-
|
||
Open neu setzen muss. Frontend laedt Defaults beim Workspace-Open
|
||
und schickt sie pro Call mit.
|
||
- **Datenmodell-Erweiterung:**
|
||
- `AiCallOptions.allowedModels: Optional[List[str]] = None` (analog
|
||
zu `allowedProviders`).
|
||
- `Workflow.allowedModels: Optional[List[str]] = None` (analog zu
|
||
`Workflow.allowedProviders`).
|
||
- **Validierung am zentralen AI-Gate (`mainServiceAi.py`):**
|
||
- `requireNeutralization` folgt **bestehender** OR-Logik (keine
|
||
Aenderung).
|
||
- Neue private Funktion `_calculateEffectiveModels()` analog zu
|
||
`_calculateEffectiveProviders()`: RBAC ∩ Workflow.allowedModels ∩
|
||
Node.allowedModels (Node setzt es ueber `request.options`).
|
||
- **Effektive Filterkette:** Provider-Filter (heute) AND Model-Filter
|
||
(neu) AND functionCall-Anforderung (heute).
|
||
- Ist die gefilterte Liste leer -> Fail-fast mit
|
||
`RuntimeError("No allowed model available for this call (allowedModels=[...], allowedProviders=[...])")`.
|
||
|
||
### Beispiel: AI-Node-Definition (gekuerzt)
|
||
|
||
```python
|
||
# in platform-core/modules/features/graphicalEditor/nodeDefinitions/ai.py
|
||
# (allowedProviders existiert bereits auf Workflow-Ebene; hier nur die
|
||
# zwei NEUEN Felder pro AI-Node)
|
||
_AI_COMMON_PARAMS = [
|
||
{"name": "requireNeutralization", "type": "boolean", "required": False,
|
||
"frontendType": "checkbox", "default": False,
|
||
"description": t("Eingaben fuer diesen Call neutralisieren")},
|
||
{"name": "allowedModels", "type": "array", "required": False,
|
||
"frontendType": "modelMultiSelect", "default": [],
|
||
"description": t("Erlaubte LLM-Modelle (leer = alle erlaubten); zusaetzlicher AND-Filter ueber allowedProviders")},
|
||
]
|
||
|
||
# pro Node anhaengen, z.B. ai.prompt:
|
||
{
|
||
"id": "ai.prompt",
|
||
...
|
||
"parameters": [
|
||
{"name": "aiPrompt", ...},
|
||
{"name": "resultType", ...},
|
||
{"name": "documentList", ...},
|
||
{"name": "simpleMode", ...},
|
||
*_AI_COMMON_PARAMS,
|
||
],
|
||
...
|
||
}
|
||
```
|
||
|
||
### Datenfluss
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
Node[ai.* Node Parameters] -->|"requireNeutralization, allowedModels"| Action[methodAi/actions/*.py]
|
||
Action -->|"sets request.options.allowedModels<br/>+ request.requireNeutralization"| Gate[mainServiceAi._call]
|
||
Gate --> EffMod[_calculateEffectiveModels<br/>RBAC AND Workflow AND Node]
|
||
Gate --> EffProv[_calculateEffectiveProviders<br/>existing]
|
||
EffMod --> Pick[_pickModel]
|
||
EffProv --> Pick
|
||
Pick -->|"empty?"| Fail[Fail-fast RuntimeError]
|
||
Pick -->|"ok"| Neutral[_shouldNeutralize OR-Logik existing]
|
||
Neutral --> LLM[Modell-Call]
|
||
WorkspaceUI[WorkspaceUserInput<br/>requireNeutralization+allowedModels] -->|"per-request"| Ctx[ServiceCenterContext]
|
||
WorkspaceSet[GET/PUT /workspace/.../user-settings<br/>NEU] -->|"defaults"| WorkspaceUI
|
||
```
|
||
|
||
## Entscheidungen
|
||
|
||
| Datum | Entscheidung | Begruendung |
|
||
|-------|-------------|------------|
|
||
| 2026-04-29 | KEIN fixer Theme-Katalog, KEIN Mandate-CI-Konzept | User-Korrektur: generisches Style-Management vom Agent gefuellt |
|
||
| 2026-04-29 | Style-Schema generisch in `metadata.style`, Defaults zentral | Agent kann pro Bericht entscheiden; Renderer haben einheitliche Mapping-Schicht |
|
||
| 2026-04-29 | Inline-Run-Modell statt String-basierter Content | Voraussetzung fuer "Bilder ueberall" |
|
||
| 2026-04-29 | Per-Node `requireNeutralization` + `allowedModels` als Standardparameter ALLER `ai.*`-Nodes | Keine Sonder-Felder pro Node-Typ; im FlowEditor konsistent |
|
||
| 2026-04-29 | `allowedModels` ergaenzt das bestehende `allowedProviders`, ersetzt es nicht | Provider = Anbieter-Ebene (existiert), Model = konkrete Modell-Ebene (neu, feiner) |
|
||
| 2026-04-29 | `allowedModels` lebt in `AiCallOptions` (analog `allowedProviders`), NICHT direkt in `AiCallRequest` | Symmetrie zur bestehenden Provider-Whitelist |
|
||
| 2026-04-29 | Neuer Workspace-User-Settings-Endpoint `GET/PUT /api/workspace/{instanceId}/user-settings` fuer ALLE DREI Felder (`requireNeutralization`, `allowedProviders`, `allowedModels`) | Heute werden `requireNeutralization`/`allowedProviders` pro Request aus dem Frontend mitgeliefert -- ohne Persistenz muesste der User die Settings bei jedem Browser-Open neu setzen. Konsistenz-Argument: wenn `allowedModels` persistiert wird, dann auch die anderen zwei -- sonst zwei Default-Mechanismen mit unterschiedlichem Verhalten. |
|
||
| 2026-04-29 | `ServiceCenterContext.requireNeutralization` ist bereits gesetzt; analoge `allowedModels`-Propagation NICHT noetig (Frontend schickt es ohnehin pro Call) | Kontextfeld nur dort, wo Sub-Service ohne Frontend-Input es braucht |
|
||
| 2026-04-29 | Whitelist-Check vor Modellwahl; leer -> Fail-fast | Compliance: Workflow soll **deterministisch** scheitern statt stillschweigend ein nicht-erlaubtes Modell zu nehmen |
|
||
| 2026-04-29 | MD->JSON-Konsolidierung in `subDocumentUtility.py` | Bereits zentralerer Pfad als `_mediaTools` |
|
||
| 2026-04-29 | Native XLSX-Charts via openpyxl spaeter | Heute reicht PNG-Embed; Chart-API ist eigene Komplexitaet |
|
||
| 2026-04-29 | KEIN Backwards-Compat fuer altes JSON-Format | Clean-Break: alte Runs brechen, akzeptabler Trade-off fuer saubere Codebasis ohne Dispatch-Overhead |
|
||
| 2026-04-29 | `WorkspaceUserSettings` in `poweron_app` (nicht `poweron_workspace`) | Dort leben User-spezifische Instanz-Daten (FeatureAccess etc.) |
|
||
| 2026-04-29 | Image-Resolution-Logik verbleibt in `_mediaTools` (Post-Processing) | Nur der MD-Parser wird konsolidiert; fileId->base64-Auflosung ist kontextabhaengig (Chat-History, KnowledgeStore) |
|
||
| 2026-04-29 | `documentTheme` (Plan B) wird Prompt-Hint, NICHT Renderer-Parameter | Renderer hat kein Theme-Konzept; Agent leitet Style aus Hint ab |
|
||
|
||
## Umsetzungs-Checkliste
|
||
|
||
### Phase 1 -- MD->JSON konsolidieren + Inline-Runs
|
||
|
||
- [x] `subDocumentUtility.markdownToDocumentJson` zur einzigen Quelle
|
||
machen (module-level Funktion, Z. 12+).
|
||
- [x] `_mediaTools._markdownToDocumentJson` (nested in
|
||
`_registerMediaTools`, Z. 27-163) ENTFERNEN. Stattdessen
|
||
`subDocumentUtility.markdownToDocumentJson` aufrufen. Die Image-
|
||
Resolution-Logik (Z. 241-277: fileId -> base64 aus
|
||
KnowledgeStore/Chat) VERBLEIBT als Post-Processing nach dem
|
||
Parser-Aufruf.
|
||
- [x] Schema-Erweiterung in `datamodels/datamodelJson.py`:
|
||
- `paragraph.inlineRuns: List[InlineRun]` (KEIN Backwards-Compat-
|
||
Feld `text: str` -- Clean-Break).
|
||
- `bullet_list.items: List[List[InlineRun]]`.
|
||
- `table.cell` neu: `cellContent: List[InlineRun]`.
|
||
- Altes String-basiertes Format wird NICHT mehr unterstuetzt.
|
||
- [x] MD-Parser im konsolidierten Helper:
|
||
- Inline `` an beliebiger Position erkennen ->
|
||
`image`-Run.
|
||
- Inline `[text](url)` -> `link`-Run.
|
||
- `**bold**` / `*italic*` / `` `code` `` -> entsprechende Runs.
|
||
- Tabellen-Zellen: voll geparst (inkl. Bild, Links, Formatierung).
|
||
- [x] Helper `_renderInlineRuns(runs, style, container)` als gemeinsame
|
||
Spec; pro Renderer eigene Implementierung.
|
||
|
||
### Phase 2 -- Style-System
|
||
|
||
- [x] Defaults-Modul `platform-core/modules/serviceCenter/services/serviceGeneration/styleDefaults.py`
|
||
mit neutralen Default-Werten fuer alle Style-Felder.
|
||
- [x] Style-Resolver in `mainServiceGeneration.py`:
|
||
`_resolveStyle(metadataStyle: dict | None) -> ResolvedStyle`
|
||
(deep-merge defaults <- agentStyle).
|
||
- [x] Renderer auf Resolver umstellen: kein Renderer hat mehr eigene
|
||
Default-Konstanten.
|
||
- [x] `renderDocument`-Tool (`_mediaTools.py`) Schema:
|
||
- Optionaler Parameter `style` (object) wird in `metadata.style`
|
||
gemappt.
|
||
- Tool-Beschreibung dokumentiert das Schema mit Beispielen.
|
||
|
||
### Phase 3 -- Renderer-Refactor (generische Style-Komponenten)
|
||
|
||
- [x] DOCX (`rendererDocx.py`):
|
||
- `_createTableRowXml` (~ 644-697) Header-Farbe/-Schrift aus
|
||
`style.table`.
|
||
- `_renderJsonImage` (~ 1016-1082) Default-Breite aus
|
||
`style.image.defaultWidthPt`.
|
||
- Headings aus `style.headings.hX`.
|
||
- Inline-Run-Renderer fuer Paragraph/Cell.
|
||
- Page-Margins / Header-Logo / Footer-Text aus `style.page`.
|
||
- `templateName: corporate|minimal` deprecaten (1 Release lang
|
||
warnen, dann raus).
|
||
- [x] XLSX (`rendererXlsx.py`):
|
||
- `_renderJsonImage` Bildbreite aus `style.image`.
|
||
- Tabellen-Header aus `style.table`.
|
||
- Inline-Bilder per Cell-Anchor (siehe Phase 4).
|
||
- [x] PPTX (`rendererPptx.py`):
|
||
- Slide-Size aus `style.page`.
|
||
- Title/Body aus `style.headings`.
|
||
- Inline-Bilder als zusaetzliche Shape (Fallback).
|
||
- [x] HTML (`rendererHtml.py`):
|
||
- `lang` aus `metadata.language`.
|
||
- Inline-CSS aus `style` generiert (`_generateCssStyles`
|
||
umbauen).
|
||
- Inline-Bilder als `<img>`.
|
||
- [x] PDF (`rendererPdf.py`):
|
||
- Sample-Style-Set aus `style`.
|
||
- Page-Margins aus `style.page.marginsPt`.
|
||
- Inline-Bilder als reportlab Inline-Image.
|
||
|
||
### Phase 4 -- Inline-Bilder ueberall (Renderer-Spezifika)
|
||
|
||
- [x] DOCX: Cell-Image via `cell.add_paragraph().add_run().add_picture(...)`.
|
||
- [x] DOCX/PDF: Inline-Image im Paragraph als Run-mit-Image.
|
||
- [x] XLSX: `openpyxl.drawing.image.Image` mit Cell-Anchor; pro Bild eine
|
||
neue Image-Instance (openpyxl-Constraint).
|
||
- [x] HTML: `<img>` direkt in `<td>` / `<p>` / `<li>` -- trivial.
|
||
- [x] PPTX: Inline-in-Text-Run nicht moeglich -> Image als zusaetzliche
|
||
Shape unter dem Text-Frame, mit Warning im JSON-Output
|
||
(`_renderingFallback: pptx-image-below-text`).
|
||
- [x] Tests pro Format: 1× Inline-Bild im Paragraph, 1× in
|
||
Tabellenzelle, 1× in Listen-Item.
|
||
|
||
### Phase 5 -- AI-Call-Konfiguration pro Node (Datenmodell + Backend)
|
||
|
||
- [x] `platform-core/modules/datamodels/datamodelAi.py`:
|
||
- `AiCallOptions.allowedModels: Optional[List[str]] = None`
|
||
(analog zu `allowedProviders` in Z. 164 -- KEIN neues Feld in
|
||
`AiCallRequest`).
|
||
- [x] `platform-core/modules/datamodels/datamodelAuto.py` (oder wo immer das
|
||
`Workflow`-Modell liegt):
|
||
- `Workflow.allowedModels: Optional[List[str]] = None` (analog
|
||
`Workflow.allowedProviders`).
|
||
- [x] `platform-core/modules/serviceCenter/services/serviceAi/mainServiceAi.py`:
|
||
- Neue private Methode `_calculateEffectiveModels()` analog zu
|
||
`_calculateEffectiveProviders()` (Z. ~1196). RBAC-Modelle ∩
|
||
`Workflow.allowedModels` ∩ `request.options.allowedModels`.
|
||
- In `_call`/`_callStream`/`callEmbedding` analog zur
|
||
Provider-Logik aufrufen und in `request.options.allowedModels`
|
||
schreiben.
|
||
- `_pickModel` filtert anschliessend AND-verknuepft auf Provider
|
||
UND Model-Whitelist.
|
||
- Fail-fast `RuntimeError` wenn die Filterkette leer wird, mit
|
||
Audit-Log-Zeile (modelle, provider, rbac-permitted, workflow,
|
||
node).
|
||
- [x] `_ServicesAdapter.__getattr__` in `mainServiceAi.py` (Z. 88-90)
|
||
erweitern: Tuple-Liste um `"allowedModels"` ergaenzen (damit
|
||
`service.allowedModels` analog `service.allowedProviders`
|
||
aus dem Workflow durchgereicht wird).
|
||
|
||
### Phase 5a -- Workspace User-Settings (Persistenz fuer alle drei Felder)
|
||
|
||
- [x] Neue persistente Settings pro User x Workspace-Instanz.
|
||
Tabelle `WorkspaceUserSettings` in **`poweron_app`** (dort wo
|
||
`FeatureAccess`, `FeatureInstance` etc. leben; User-spezifische
|
||
Instanz-Daten). Additiv via Auto-Init:
|
||
- `instanceId UUID NOT NULL`
|
||
- `userId UUID NOT NULL`
|
||
- `requireNeutralization BOOL DEFAULT false`
|
||
- `allowedProviders JSONB DEFAULT '[]'`
|
||
- `allowedModels JSONB DEFAULT '[]'`
|
||
- PK / Unique: (`instanceId`, `userId`).
|
||
- [x] Backend-Endpoints in `routeFeatureWorkspace.py`:
|
||
- `GET /api/workspace/{instanceId}/user-settings` -- effektive
|
||
Settings fuer current user (Auto-Insert mit Defaults wenn
|
||
leer).
|
||
- `PUT /api/workspace/{instanceId}/user-settings` -- speichern
|
||
(alle drei Felder im Body).
|
||
- [x] `WorkspaceInputRequest.allowedModels: List[str] = Field(default_factory=list)`
|
||
(additiv, `routeFeatureWorkspace.py` Z. 102-113).
|
||
`requireNeutralization` und `allowedProviders` sind dort schon
|
||
vorhanden (Z. 111-112).
|
||
- [x] In `_runWorkspaceAgent` (Z. 693+): `allowedModels` ebenso
|
||
durchreichen wie heute `allowedProviders` (Z. 713-717) -- in das
|
||
`request.options.allowedModels`-Feld schreiben (siehe Phase 5
|
||
Datenmodell). Pattern analog:
|
||
`aiService.services.allowedModels = allowedModels`.
|
||
- [x] Frontend Workspace:
|
||
- Workspace-Settings-Tab/Panel mit allen drei Feldern:
|
||
- Toggle `requireNeutralization`.
|
||
- Multi-Select `allowedProviders` (Optionen aus
|
||
`GET /api/system/ai-models` -> distinct connectorTypes).
|
||
- Multi-Select `allowedModels` (Optionen aus
|
||
`GET /api/system/ai-models` -> displayNames).
|
||
- Beim Workspace-Open: Settings via
|
||
`GET .../user-settings` laden, Defaults in den Workspace-State
|
||
setzen.
|
||
- "Speichern"-Button im Settings-Panel ruft `PUT .../user-
|
||
settings` mit allen drei Feldern.
|
||
- Bei jedem Workspace-Call: aktuelle Settings als
|
||
`WorkspaceUserInput`-Felder mitgeben (alle drei).
|
||
|
||
### Phase 5b -- AI-Nodes im FlowEditor
|
||
|
||
- [x] `platform-core/modules/features/graphicalEditor/nodeDefinitions/ai.py`:
|
||
- Modul-lokaler Helper `_AI_COMMON_PARAMS = [...]` mit den 2
|
||
neuen Parametern.
|
||
- In ALLE 8 Eintraege von `AI_NODES` als `*_AI_COMMON_PARAMS`
|
||
anhaengen.
|
||
- [x] `platform-core/modules/workflows/methods/methodAi/actions/*.py`:
|
||
- In jeder Action (`process.py`, `webResearch.py`,
|
||
`summarizeDocument.py`, `translateDocument.py`,
|
||
`convertDocument.py`, `generateDocument.py`,
|
||
`generateCode.py`, `consolidate.py`):
|
||
- `requireNeutralization = parameters.get("requireNeutralization")`
|
||
-> in `request.requireNeutralization` setzen.
|
||
- `allowedModels = parameters.get("allowedModels", [])` -> in
|
||
`request.options.allowedModels` setzen.
|
||
- Helper-Funktion `_applyCommonAiParams(parameters, request)` in
|
||
`methodAi/_common.py` (neu) zum Reduzieren von Boilerplate.
|
||
- [x] FlowEditor (Frontend):
|
||
- Neuer `frontendType: "modelMultiSelect"` -- holt verfuegbare
|
||
Modelle ueber `GET /api/system/ai-models` und stellt
|
||
Multi-Select dar.
|
||
- Frontend-Generic-Form-Generator unterstuetzt diesen Type
|
||
(Properties-Panel pro AI-Node).
|
||
|
||
### Phase 6 -- Trustee-Templates anpassen (entkoppelt vom Bau-Plan B)
|
||
|
||
- [x] In `mainTrustee.py`-Templates: neuen Node-Parameter `style`-Block
|
||
setzen (Finanzreport-Defaults: dunkelblaue Headings,
|
||
konservatives Calibri etc.) -- damit Trustee out-of-the-box gut
|
||
aussieht. Der in Plan B gesetzte `documentTheme: "finance"`
|
||
wird als Hint fuer den Agent im Prompt benutzt (NICHT als
|
||
Renderer-Parameter); der Agent soll daraus seinen `style`-Block
|
||
ableiten.
|
||
- [x] `requireNeutralization: false` (User-Entscheid: Buchhaltungsdaten
|
||
duerfen nicht anonymisiert werden, sonst Fantasie-Zahlen).
|
||
- [x] `allowedModels` pro Trustee-Workflow optional pinned (z.B. nur
|
||
private LLM fuer Finance-Daten).
|
||
- [x] `documentTheme`-Parameter im Budget-Template (aus Plan B) in den
|
||
Prompt als Kontext-Hint einfuegen, statt als separaten
|
||
Renderer-Parameter. Der Renderer kennt KEIN Theme-Konzept.
|
||
|
||
### Phase 7 -- Tests / Snapshots
|
||
|
||
- [x] Pro Format ein Snapshot-Test mit Custom-Style:
|
||
`test_render_with_agent_style_<format>.py`.
|
||
- [x] Inline-Bilder-Tests pro Format (siehe Phase 4).
|
||
- [x] AI-Gate-Tests:
|
||
`test_allowed_models_filter.py` (Whitelist greift),
|
||
`test_allowed_models_empty_fails.py` (Fail-fast),
|
||
`test_workspace_propagates_neutralization.py`.
|
||
|
||
## Akzeptanzkriterien
|
||
|
||
| # | Kriterium (Given-When-Then) | Prio |
|
||
|---|-----------------------------|------|
|
||
| 1 | Given Agent ruft `renderDocument` mit `style.colors.primary="#E11D48"`, When DOCX rendert, Then Headings nutzen diese Farbe | must |
|
||
| 2 | Given Agent ruft `renderDocument` ohne `style`-Block, When Renderer arbeitet, Then werden neutrale Defaults verwendet (kein Crash) | must |
|
||
| 3 | Given Markdown enthaelt `Bei der Sitzung  wurde besprochen ...`, When DOCX gerendert, Then erscheint das Bild inline mitten im Paragraph | must |
|
||
| 4 | Given Markdown-Tabellenzelle `\|  \|`, When DOCX/HTML/XLSX gerendert, Then Bild liegt in der Zelle | must |
|
||
| 5 | Given Markdown-Listen-Item mit Inline-Bild, When HTML gerendert, Then Bild im `<li>` | should |
|
||
| 6 | Given PPTX gerendert mit Inline-Bild im Paragraph, When Output inspiziert, Then Bild ist als separate Shape unterhalb mit Warning im Run-Log | should |
|
||
| 7 | Given AI-Node mit `requireNeutralization: true`, When Workflow laeuft, Then Prompt/Context werden im AI-Gate neutralisiert (Beweis im Audit-Log) | must |
|
||
| 8 | Given AI-Node mit `allowedModels: ["claude-4-7-opus"]`, When Workflow laeuft und nur dieses Modell verfuegbar, Then Call benutzt es | must |
|
||
| 9 | Given AI-Node mit `allowedModels: ["nichtExistent"]`, When Workflow laeuft, Then Fail-fast mit Error-Message inkl. RBAC/Workflow/Node-Filter-Snapshot | must |
|
||
| 10 | Given Workspace mit gespeicherten User-Settings `requireNeutralization=true, allowedProviders=["openai"], allowedModels=["gpt-5-mini"]`, When User Workspace neu oeffnet, Then sind ALLE drei Settings vorausgefuellt und werden bei jedem Call mitgeschickt | must |
|
||
| 11 | Given Workspace mit `requireNeutralization: true`, When Sub-Agent-AI-Tools laufen, Then alle Calls werden neutralisiert (ServiceCenterContext propagiert) | must |
|
||
| 12 | Given Provider-Whitelist `allowedProviders=["openai"]` UND Model-Whitelist `allowedModels=["claude-4-7-opus"]`, When Filterung laeuft, Then leere Liste -> Fail-fast (AND-Logik, nicht OR) | must |
|
||
| 13 | Given alte Implementierungen `_mediaTools._markdownToDocumentJson`, When neu gebaut, Then ruft sie nur noch den Helper aus `subDocumentUtility` auf | must |
|
||
|
||
## Testplan
|
||
|
||
| ID | AC | Art | Automatisiert | Repo-Pfad | Status |
|
||
|----|----|-----|--------------|-----------|--------|
|
||
| T1 | 1,2 | snapshot | ja | platform-core/tests/serviceGeneration/test_agent_style_docx.py | done |
|
||
| T2 | 3 | integration | ja | platform-core/tests/serviceGeneration/test_inline_image_paragraph.py | done |
|
||
| T3 | 4 | integration | ja | platform-core/tests/serviceGeneration/test_inline_image_table_cell.py | done |
|
||
| T4 | 5 | integration | ja | platform-core/tests/serviceGeneration/test_inline_image_list_item_html.py | done |
|
||
| T5 | 6 | integration | ja | platform-core/tests/serviceGeneration/test_inline_image_pptx_fallback.py | done |
|
||
| T6 | 7 | integration | ja | platform-core/tests/serviceAi/test_node_neutralization_flag.py | done |
|
||
| T7 | 8,9 | integration | ja | platform-core/tests/serviceAi/test_allowed_models_whitelist.py | done |
|
||
| T8 | 10 | integration | ja | platform-core/tests/features/workspace/test_user_settings_persistence.py | done |
|
||
| T9 | 11 | integration | ja | platform-core/tests/serviceAi/test_workspace_propagates_settings.py | done |
|
||
| T10 | 12 | unit | ja | platform-core/tests/serviceAi/test_effective_models_and_providers.py | done |
|
||
| T11 | 13 | unit | ja | platform-core/tests/serviceCenter/test_md_to_json_consolidation.py | done |
|
||
| T12 | -- | unit | ja | ui-nyla/src/components/flowEditor/__tests__/AiNodeProperties.test.tsx | deferred |
|
||
|
||
## Links
|
||
|
||
- Aktuelle Pipeline-Architektur: Subagent-Report 2026-04-29.
|
||
- Renderer-Code: `platform-core/modules/serviceCenter/services/serviceGeneration/renderers/`.
|
||
- MD->JSON-Code:
|
||
`platform-core/modules/serviceCenter/services/serviceAgent/coreTools/_mediaTools.py`,
|
||
`platform-core/modules/serviceCenter/services/serviceGeneration/subDocumentUtility.py`.
|
||
- AI-Gate: `platform-core/modules/serviceCenter/services/serviceAi/mainServiceAi.py`.
|
||
- Neutralisierung: `wiki/b-reference/platform/neutralization.md`.
|
||
- Wiki: `wiki/b-reference/platform-core/ai-agent.md`,
|
||
`wiki/b-reference/platform-core/agent-file-bridge.md`,
|
||
`wiki/b-reference/platform-core/workflow.md`.
|
||
|
||
## Abschluss
|
||
|
||
- [ ] `wiki/b-reference/platform-core/ai-agent.md` -- Style-System + per-call
|
||
Konfiguration ergaenzen (TODO: bei naechster Wiki-Review-Runde)
|
||
- [ ] `wiki/b-reference/platform-core/workflow.md` -- AI-Node-Standardparameter
|
||
dokumentieren (TODO: bei naechster Wiki-Review-Runde)
|
||
- [ ] `wiki/b-reference/platform/neutralization.md` -- per-Node-Trigger
|
||
ergaenzen (TODO: bei naechster Wiki-Review-Runde)
|
||
- [ ] `wiki/TOPICS.md` ggf. neuer Eintrag "AI-Node-Konfiguration"
|
||
- [x] Dieses Dokument -> `4-done/` verschoben
|