wiki/c-work/1-plan/2026-04-ai-reports-theming-and-pipeline.md
ValueOn AG 6eeeb962f6 upd
2026-04-29 20:23:03 +02:00

614 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- status: plan -->
<!-- started: 2026-04-29 -->
<!-- component: gateway, frontend-nyla -->
# 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`).
- 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.
## Betroffene Module
- Gateway:
- `gateway/modules/serviceCenter/services/serviceAgent/coreTools/_mediaTools.py`
-- `_markdownToDocumentJson` raus, auf gemeinsamen Helper
umstellen; `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.
- `gateway/modules/datamodels/datamodelJson.py` -- Schema-Erweiterung
fuer Inline-Runs und `cellContent`.
- `gateway/modules/datamodels/datamodelAi.py` --
`AiCallRequest.allowedModels: Optional[List[str]]` (Whitelist).
- `gateway/modules/serviceCenter/services/serviceAi/mainServiceAi.py`
-- Whitelist-Check vor Modellwahl, Fail-fast wenn keiner uebrig.
- `gateway/modules/features/graphicalEditor/nodeDefinitions/ai.py`
-- Standardparameter `requireNeutralization` + `allowedModels` zu
allen `ai.*`-Nodes (gemeinsamer Helper).
- `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).
- 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
}
```
Backwards-kompatibel: Wenn der MD-Parser nur Text findet, bleibt es
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.).
**Markdown-Erkennung:** `![alt](file:xyz)` und `![alt](file:xyz "100pt")`
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`,
`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).
- **`WorkspaceUserInput.allowedProviders`** + **`requireNeutralization`**
werden vom Frontend pro Request mitgegeben
(`routeFeatureWorkspace.py` Z. 111-113) und in den
`ServiceCenterContext` gesetzt (Z. 716-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<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 |
## Umsetzungs-Checkliste
### Phase 1 -- MD->JSON konsolidieren + Inline-Runs
- [ ] `subDocumentUtility.markdownToDocumentJson` zur einzigen Quelle
machen.
- [ ] `_mediaTools._markdownToDocumentJson` durch Aufruf des Helpers
ersetzen, lokale Funktion entfernen.
- [ ] Schema-Erweiterung in `datamodels/datamodelJson.py`:
- `paragraph.inlineRuns: List[InlineRun]` (alt `text: str` als
Backwards-Compat-Feld weiter unterstuetzt).
- `bullet_list.items: List[List[InlineRun]]`.
- `table.cell` neu strukturiert: `cellContent: List[InlineRun]`
oder `cellBlocks: List[Section]` (fuer komplexere Zellen mit
eigenem Paragraph/Image-Block).
- [ ] MD-Parser im konsolidierten Helper:
- Inline `![alt](file:xyz)` an beliebiger Position erkennen ->
`image`-Run.
- Inline `[text](url)` -> `link`-Run.
- `**bold**` / `*italic*` / `` `code` `` -> entsprechende Runs.
- Tabellen-Zellen: erkennen ob nur Text -> einzelner Text-Run;
sonst voll geparst (inkl. Bild).
- [ ] 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 `<img>`.
- [ ] 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: `<img>` direkt in `<td>` / `<p>` / `<li>` -- 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. 175-178). 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).
- [ ] `__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` (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).
- [ ] `WorkspaceUserInput.allowedModels: List[str] = []` (additiv,
`routeFeatureWorkspace.py` Z. 109-114). `requireNeutralization`
und `allowedProviders` sind dort schon vorhanden (Z. 112-113).
- [ ] In `_runWorkspaceAgent` (Z. 693+): `allowedModels` ebenso
durchreichen wie heute `allowedProviders` (Z. 716-717) -- in das
`request.options.allowedModels`-Feld schreiben (siehe Phase 5
Datenmodell).
- [ ] 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: `style`-Block in `aiPrompt`-Node
Parametern setzen (Finanzreport-Defaults: dunkelblaue Headings,
konservatives Calibri etc.) -- damit Trustee out-of-the-box gut
aussieht.
- [ ] `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).
### Phase 7 -- Tests / Snapshots
- [ ] Pro Format ein Snapshot-Test mit Custom-Style:
`test_render_with_agent_style_<format>.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 ![Logo](file:abc) wurde besprochen ...`, When DOCX gerendert, Then erscheint das Bild inline mitten im Paragraph | must |
| 4 | Given Markdown-Tabellenzelle `\| ![](file:xyz) \|`, 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 | 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