wiki/c-work/4-done/2026-04-ai-reports-theming-and-pipeline.md
2026-06-02 09:42:12 +02:00

650 lines
34 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: 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
## 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:** `![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`,
`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 `![alt](file:xyz)` 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 ![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 | 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