# AI Reports: Generisches Style-Management, AI-Call-Konfiguration, Inline-Bilder
## 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:
- `gateway/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.
- `gateway/modules/serviceCenter/services/serviceGeneration/subDocumentUtility.py`
-- alleinige MD->JSON-Funktion mit Inline-Run-Modell.
- `gateway/modules/serviceCenter/services/serviceGeneration/mainServiceGeneration.py`
-- `_resolveStyle(metadata.style) -> ResolvedStyle` (Defaults +
Agent-Overrides).
- `gateway/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.
- `gateway/modules/datamodels/datamodelJson.py` -- Schema-Erweiterung
fuer Inline-Runs und `cellContent`.
- `gateway/modules/datamodels/datamodelAi.py` --
`AiCallOptions.allowedModels: Optional[List[str]]` (Whitelist;
analog `allowedProviders` Z. 164).
- `gateway/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.
- `gateway/modules/features/graphicalEditor/nodeDefinitions/ai.py`
-- Standardparameter `requireNeutralization` + `allowedModels` zu
allen `ai.*`-Nodes (gemeinsamer Helper `_AI_COMMON_PARAMS`).
- `gateway/modules/workflows/methods/methodAi/actions/*.py` -- Node-
Parameter durchreichen in den `AiCallRequest`.
- `gateway/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 (`gateway/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 `
` | trivial `
` in `
` | trivial ` ` in `` | -- |
| 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`,
`private-llm`).
- **`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 gateway/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 + request.requireNeutralization"| Gate[mainServiceAi._call]
Gate --> EffMod[_calculateEffectiveModels RBAC AND Workflow AND Node]
Gate --> EffProv[_calculateEffectiveProviders 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 requireNeutralization+allowedModels] -->|"per-request"| Ctx[ServiceCenterContext]
WorkspaceSet[GET/PUT /workspace/.../user-settings 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
- [ ] `subDocumentUtility.markdownToDocumentJson` zur einzigen Quelle
machen (module-level Funktion, Z. 12+).
- [ ] `_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.
- [ ] 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.
- [ ] 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).
- [ ] Helper `_renderInlineRuns(runs, style, container)` als gemeinsame
Spec; pro Renderer eigene Implementierung.
### Phase 2 -- Style-System
- [ ] Defaults-Modul `gateway/modules/serviceCenter/services/serviceGeneration/styleDefaults.py`
mit neutralen Default-Werten fuer alle Style-Felder.
- [ ] Style-Resolver in `mainServiceGeneration.py`:
`_resolveStyle(metadataStyle: dict | None) -> ResolvedStyle`
(deep-merge defaults <- agentStyle).
- [ ] Renderer auf Resolver umstellen: kein Renderer hat mehr eigene
Default-Konstanten.
- [ ] `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)
- [ ] 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).
- [ ] XLSX (`rendererXlsx.py`):
- `_renderJsonImage` Bildbreite aus `style.image`.
- Tabellen-Header aus `style.table`.
- Inline-Bilder per Cell-Anchor (siehe Phase 4).
- [ ] PPTX (`rendererPptx.py`):
- Slide-Size aus `style.page`.
- Title/Body aus `style.headings`.
- Inline-Bilder als zusaetzliche Shape (Fallback).
- [ ] HTML (`rendererHtml.py`):
- `lang` aus `metadata.language`.
- Inline-CSS aus `style` generiert (`_generateCssStyles`
umbauen).
- Inline-Bilder als ` `.
- [ ] 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)
- [ ] DOCX: Cell-Image via `cell.add_paragraph().add_run().add_picture(...)`.
- [ ] DOCX/PDF: Inline-Image im Paragraph als Run-mit-Image.
- [ ] XLSX: `openpyxl.drawing.image.Image` mit Cell-Anchor; pro Bild eine
neue Image-Instance (openpyxl-Constraint).
- [ ] HTML: ` ` direkt in `` / ` ` / ` ` -- trivial.
- [ ] 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`).
- [ ] Tests pro Format: 1× Inline-Bild im Paragraph, 1× in
Tabellenzelle, 1× in Listen-Item.
### Phase 5 -- AI-Call-Konfiguration pro Node (Datenmodell + Backend)
- [ ] `gateway/modules/datamodels/datamodelAi.py`:
- `AiCallOptions.allowedModels: Optional[List[str]] = None`
(analog zu `allowedProviders` in Z. 164 -- KEIN neues Feld in
`AiCallRequest`).
- [ ] `gateway/modules/datamodels/datamodelAuto.py` (oder wo immer das
`Workflow`-Modell liegt):
- `Workflow.allowedModels: Optional[List[str]] = None` (analog
`Workflow.allowedProviders`).
- [ ] `gateway/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).
- [ ] `_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)
- [ ] Neue persistente Settings pro User × 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`).
- [ ] 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).
- [ ] `WorkspaceInputRequest.allowedModels: List[str] = Field(default_factory=list)`
(additiv, `routeFeatureWorkspace.py` Z. 102-113).
`requireNeutralization` und `allowedProviders` sind dort schon
vorhanden (Z. 111-112).
- [ ] 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`.
- [ ] 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
- [ ] `gateway/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.
- [ ] `gateway/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.
- [ ] 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)
- [ ] 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.
- [ ] `requireNeutralization: true` falls Trustee-Daten sensibel
(entscheidet Compliance-Doku, Default empfohlen).
- [ ] `allowedModels` pro Trustee-Workflow optional pinned (z.B. nur
private LLM fuer Finance-Daten).
- [ ] `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
- [ ] Pro Format ein Snapshot-Test mit Custom-Style:
`test_render_with_agent_style_.py`.
- [ ] Inline-Bilder-Tests pro Format (siehe Phase 4).
- [ ] 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 `` | 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 | gateway/tests/serviceGeneration/test_agent_style_docx.py | pending |
| T2 | 3 | integration | ja | gateway/tests/serviceGeneration/test_inline_image_paragraph.py | pending |
| T3 | 4 | integration | ja | gateway/tests/serviceGeneration/test_inline_image_table_cell.py | pending |
| T4 | 5 | integration | ja | gateway/tests/serviceGeneration/test_inline_image_list_item_html.py | pending |
| T5 | 6 | integration | ja | gateway/tests/serviceGeneration/test_inline_image_pptx_fallback.py | pending |
| T6 | 7 | integration | ja | gateway/tests/serviceAi/test_node_neutralization_flag.py | pending |
| T7 | 8,9 | integration | ja | gateway/tests/serviceAi/test_allowed_models_whitelist.py | pending |
| T8 | 10 | integration | ja | gateway/tests/features/workspace/test_user_settings_persistence.py | pending |
| T9 | 11 | integration | ja | gateway/tests/serviceAi/test_workspace_propagates_settings.py | pending |
| T10 | 12 | unit | ja | gateway/tests/serviceAi/test_effective_models_and_providers.py | pending |
| T11 | 13 | unit | ja | gateway/tests/serviceCenter/test_md_to_json_consolidation.py | pending |
| T12 | -- | unit | ja | frontend_nyla/src/components/flowEditor/__tests__/AiNodeProperties.test.tsx | pending |
## Links
- Aktuelle Pipeline-Architektur: Subagent-Report 2026-04-29.
- Renderer-Code: `gateway/modules/serviceCenter/services/serviceGeneration/renderers/`.
- MD->JSON-Code:
`gateway/modules/serviceCenter/services/serviceAgent/coreTools/_mediaTools.py`,
`gateway/modules/serviceCenter/services/serviceGeneration/subDocumentUtility.py`.
- AI-Gate: `gateway/modules/serviceCenter/services/serviceAi/mainServiceAi.py`.
- Neutralisierung: `wiki/b-reference/platform/neutralization.md`.
- Wiki: `wiki/b-reference/gateway/ai-agent.md`,
`wiki/b-reference/gateway/agent-file-bridge.md`,
`wiki/b-reference/gateway/workflow.md`.
## Abschluss
- [ ] `wiki/b-reference/gateway/ai-agent.md` -- Style-System + per-call
Konfiguration ergaenzen
- [ ] `wiki/b-reference/gateway/workflow.md` -- AI-Node-Standardparameter
dokumentieren
- [ ] `wiki/b-reference/platform/neutralization.md` -- per-Node-Trigger
ergaenzen (Praezedenzkette: Feature -> Workspace -> Node -> Request)
- [ ] `wiki/TOPICS.md` ggf. neuer Eintrag "AI-Node-Konfiguration"
- [ ] Dieses Dokument -> `z-archive/` verschoben
| |