From 8a70c6ea9aa97fc1c5d73d640c55657e89a1cb55 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Wed, 29 Apr 2026 01:52:46 +0200
Subject: [PATCH] wired infomaniac to ai adapters and tools
---
TOPICS.md | 1 +
b-reference/gateway/agent-file-bridge.md | 220 +++++++++++++++++++++++
c-work/_CHANGELOG.md | 12 ++
3 files changed, 233 insertions(+)
create mode 100644 b-reference/gateway/agent-file-bridge.md
diff --git a/TOPICS.md b/TOPICS.md
index b2226cd..7fb01ea 100644
--- a/TOPICS.md
+++ b/TOPICS.md
@@ -20,6 +20,7 @@ Lade immer zuerst diese Datei. Dann gezielt die passende(n) Referenz-Datei(en).
| Komponentenübersicht | b-reference/product.md | Repo-übergreifende Fragen, Tech-Stack |
| Gateway-Architektur | b-reference/gateway/architecture.md | Backend-Module, Services, Interfaces |
| AI Agent & Tools | b-reference/gateway/ai-agent.md | Agent-Verhalten, Tool-Registrierung, RAG |
+| Agent-Tool File Bridge | b-reference/gateway/agent-file-bridge.md | Wie Agent-Tool-Outputs (download / writeFile / renderDocument / generateImage / createChart) als ChatDocument im Workflow landen, `docItem:`-Pattern, runAgent Workflow-Propagation |
| Workflow-Engine | b-reference/gateway/workflow.md | Methoden, Aktionen, WorkflowManager |
| Automation | b-reference/gateway/automation.md | Graphical Editor, Scheduler, System-Automatisierung (`/automations`, `/api/system/workflow-runs/*`) |
| Billing & Subscriptions | b-reference/gateway/billing.md | Abrechnung, Prepaid, State Machine |
diff --git a/b-reference/gateway/agent-file-bridge.md b/b-reference/gateway/agent-file-bridge.md
new file mode 100644
index 0000000..d2dba3d
--- /dev/null
+++ b/b-reference/gateway/agent-file-bridge.md
@@ -0,0 +1,220 @@
+
+
+
+
+# Agent-Tool File Bridge
+
+## Worum geht es
+
+Ein Agent-Tool kann eine Datei produzieren (Download, generieren, schreiben).
+Daraus muss zwingend dreierlei werden, sonst sind die nachgelagerten
+AI-Tools (`ai_process`, `ai_summarizeDocument`, `context_extractContent`,
+`context_neutralizeData`) blind:
+
+1. ein **`FileItem`** in der Management-DB (Inhalt + Metadaten),
+2. ein **`ChatDocument`**, das im aktuellen Workflow auf das `FileItem`
+ verweist und unter `workflow.messages[*].documents[*]` landet,
+3. eine **`ChatMessage`**, die das `ChatDocument` traegt (sonst hat es
+ keinen `messageId` und ist nicht referenzierbar).
+
+`getChatDocumentsFromDocumentList` (in `mainServiceChat.py`) loest
+`docItem:`-Referenzen ausschliesslich gegen `ChatDocument.id`-Werte
+in `workflow.messages[*].documents[*]` auf. **Wenn der Agent nur ein
+`FileItem` erzeugt, aber kein `ChatDocument`, ist der File-Output fuer
+jedes documentList-konsumierende Tool unsichtbar.**
+
+## Symptom (vor dem Fix)
+
+Klassischer Trace nach `downloadFromDataSource` -> `ai_summarizeDocument`:
+
+```
+INFO ai_summarizeDocument called with documentList=[...]
+WARN getChatDocumentsFromDocumentList: No workflow available (self._workflow is not set)
+INFO Building structure prompt with 0 valid ContentParts
+```
+
+Erstes WARN entsteht, wenn `runAgent` das Workflow nicht in die
+Service-Contexts propagiert. Zweites Symptom (0 ContentParts) bleibt
+auch danach, solange Agent-Tools nur ein `FileItem` produzieren ohne
+`ChatDocument`.
+
+## Architektur-Pattern
+
+```mermaid
+flowchart LR
+ Tool[Agent Tool\ndownloadFromDataSource\nwriteFile create\nrenderDocument\ngenerateImage\ncreateChart] -->|saveUploadedFile saveGeneratedFile| FI[FileItem]
+ FI -->|_attachFileAsChatDocument| Bridge((Bridge Helper))
+ Bridge -->|storeMessageWithDocuments| CM[ChatMessage]
+ CM --> CD[ChatDocument]
+ CD -.fileId.-> FI
+ CD -.id used as.-> Ref{{docItem:<chatDocId>}}
+ Ref -->|documentList parameter| AITools[ai_process ai_summarizeDocument context_extractContent context_neutralizeData]
+ AITools -->|getChatDocumentsFromDocumentList| CD
+```
+
+## Komponenten
+
+### `_attachFileAsChatDocument` (Single Source of Truth)
+
+Datei: `gateway/modules/serviceCenter/services/serviceAgent/coreTools/_helpers.py`
+
+```python
+def _attachFileAsChatDocument(
+ services: Any,
+ fileItem: Any,
+ *,
+ label: str = "agent_tool_output",
+ userMessage: str = "",
+ role: str = "assistant",
+) -> Optional[str]:
+ """Bind a persisted FileItem to the active workflow as a ChatDocument.
+
+ Returns the new ChatDocument.id (or None if no active workflow).
+ """
+```
+
+Liest `chatService._workflow` (= `chatService._context.workflow`),
+baut ein `ChatDocument`-Dict mit `roundNumber`/`taskNumber`/`actionNumber`
+aus dem Workflow, packt es in eine `ChatMessage` (Rolle `assistant`,
+Status `step`, eindeutiges `documentsLabel`) und persistiert via
+`chatService.storeMessageWithDocuments`. Wirft nie -- liefert im
+Fehlerfall `None` und loggt Warning.
+
+### `_formatToolFileResult` (Unified ToolResult Text)
+
+Renderkonvention: jedes file-erzeugende Tool gibt im Result-Text
+**immer beide IDs** raus:
+
+```
+Downloaded 'platform-overview.html' (87654 bytes)
+ documentList ref: docItem:abcd-1234-...
+ file id: c7bf161b-8113-4b53-...
+```
+
+* `docItem:` -> hineinkopieren in `documentList` von AI-Tools.
+* `file id: ` -> fuer `readFile`, `searchInFileContent`,
+ `writeFile mode=append`, Bild-Embeds (``).
+
+Wenn kein aktiver Workflow gebunden ist, faellt die `documentList ref`-Zeile
+weg -- die Datei ist trotzdem ueber `file id` direkt lesbar. Der
+documentList-Pfad braucht Workflow-Kontext sowieso.
+
+### Workflow-Propagation in `runAgent`
+
+Datei: `gateway/modules/serviceCenter/services/serviceAgent/mainServiceAgent.py`
+
+`runAgent(workflowId=...)` setzt das Workflow-Objekt jetzt in
+`self.services.workflow` UND in `_context.workflow` aller Services
+(`chat`, `ai`, `extraction`, `sharepoint`, `clickup`, `utils`, `billing`,
+`generation`). Spiegel von `workflowManager._propagateWorkflowToContext`.
+Ohne diesen Schritt ist `chatService._workflow` `None` und der Bridge-Helper
+greift nie.
+
+## Aufrufstellen
+
+| Tool | Datei | Aufruf |
+|------|-------|--------|
+| `downloadFromDataSource` | `_dataSourceTools.py` | nach `saveUploadedFile`, `label=datasource:` |
+| `writeFile mode=create` | `_workspaceTools.py` | nach `saveUploadedFile`, `label=writeFile:` |
+| `renderDocument` | `_mediaTools.py` | im Multi-Doc-Loop pro generiertem File, `label=renderDocument:` |
+| `generateImage` | `_mediaTools.py` | im Multi-Image-Loop pro PNG, `label=generateImage:` |
+| `createChart` | `_mediaTools.py` | nach Chart-PNG-Save, `label=createChart:` |
+
+Alle anderen file-konsumierenden Tools (`readFile`, `replaceInFile`,
+`searchInFileContent`, `listFiles`, `getFileInfo`, ...) brauchen keinen
+Bridge-Aufruf -- sie veraendern nur Inhalt oder lesen, ohne neuen Workflow-State zu erzeugen.
+
+## Vergleich mit anderen Bridges
+
+| Pfad | Helper | Wo |
+|------|--------|-----|
+| Workflow-Action mit `ActionResult.documents` | `persistTaskResult` (intern) | `workflowProcessor.py:608-707` |
+| `methodTrustee.extractFromFiles` (FileItem-IDs aus Sub-Trustee) | inline Pattern | `methodTrustee/actions/extractFromFiles.py:507-545` |
+| Agent-Tool-Output (NEU) | `_attachFileAsChatDocument` | `coreTools/_helpers.py` |
+
+Alle drei produzieren am Ende dieselbe Struktur:
+`ChatMessage` -> `ChatDocument` -> `fileId`. Der Agent-Pfad war bisher
+die einzige Luecke.
+
+## Tolerante `documentList`-Parsing-Schicht
+
+Komplementaer dazu: `coerceDocumentReferenceList` in
+`gateway/modules/datamodels/datamodelDocref.py` parst das, was der LLM
+am anderen Ende reinschickt -- typischerweise
+
+* `["docItem:", "docItem:"]` (kanonisch),
+* `[{"id": "", "name": "..."}]` (LLM-Listen-Style),
+* `{"documents": [{"id": ""}, ...]}` (LLM-Dict-Wrapper),
+* `"docItem:"` (Single-String).
+
+Alle vier Shapes werden in `DocumentReferenceList` mit
+`DocumentItemReference(documentId=)` umgewandelt; daraus baut
+`getChatDocumentsFromDocumentList` die `docItem:`-Suchschluessel
+und matched gegen `workflow.messages[*].documents[*].id` -- also exakt
+gegen die `ChatDocument.id`s, die der Bridge-Helper persistiert.
+
+Die Kette ist damit:
+
+```
+agent tool
+ -> _attachFileAsChatDocument (writes ChatDocument.id = X)
+ -> _formatToolFileResult (returns "docItem:X" to LLM)
+ -> LLM puts "docItem:X" in documentList (or any tolerant variant)
+ -> coerceDocumentReferenceList (normalises to docItem:X)
+ -> getChatDocumentsFromDocumentList (matches workflow.messages[*].documents[*].id == X)
+ -> ai_summarizeDocument receives ContentParts
+```
+
+## Failure Modes
+
+| Symptom | Ursache | Wo schauen |
+|---------|---------|------------|
+| `getChatDocumentsFromDocumentList: No workflow available` | `chatService._context.workflow` ist `None` | `mainServiceAgent.runAgent` -- propagiert es das Workflow? |
+| "Building structure prompt with 0 valid ContentParts" trotz erfolgreichem Download | Bridge nicht aufgerufen oder Workflow None | Pruefen ob `_attachFileAsChatDocument` `None` returnt |
+| `Invalid documentList type: ` | Tolerant-Coercer wurde umgangen | Action benutzt nicht `coerceDocumentReferenceList` |
+| `'dict' object has no attribute 'fileName'` aus `listFiles` | `getAllFiles` returnt dicts, Code greift mit `.attribute` | `mainServiceChat.listFiles` muss `.get(...)` benutzen |
+| `File with ID ... not found` direkt nach `Duplicate detected for user ...` | "Ghost duplicate" -- `checkForDuplicateFile` returned ein RBAC-unsichtbares File. Wurzel: `interfaceDbComponent` ohne `featureInstanceId` initialisiert, Duplicate-Suche faellt auf mandate-Scope zurueck. | `interfaceDbManagement.checkForDuplicateFile` macht jetzt einen `getFile`-Cross-Check; wenn `getFile None`, gilt als kein Duplicate -> Caller erstellt frische per-Scope-Kopie. Fix in 2026-04-29. |
+
+## Ghost-Duplicate-Fix (2026-04-29)
+
+Symptom des Tages: `downloadFromDataSource` returnte zuverlaessig
+`File with ID 21b0b995-... not found`, **direkt nach** einer
+`Duplicate detected for user ...` INFO-Zeile aus `saveUploadedFile`.
+
+Mechanik:
+
+* `interfaceDbComponent` wird ueber `serviceHub` ohne `featureInstanceId`
+ konstruiert -- nur `mandateId` wird durchgereicht.
+* `checkForDuplicateFile` baute bisher den `recordFilter` mit
+ `if self.featureInstanceId: ... elif self.mandateId: ...` und benutzte
+ `db.getRecordset` (kein RBAC-Filter). Foglich matchte er ein File aus
+ einer **anderen** featureInstance, solange Hash + Name + sysCreatedBy
+ + Mandate uebereinstimmten.
+* `getFile` benutzt aber `getRecordsetWithRBAC` und filtert das
+ fremd-scope File raus -> `None`.
+* Caller (`saveUploadedFile.updateFile(...)`) crashed mit
+ `File with ID ... not found`.
+
+Sauberer Fix in `checkForDuplicateFile`: nach dem Recordset-Treffer
+zwingend ein `self.getFile(fileId)`-Cross-Check. Wenn RBAC blockiert,
+returnen wir `None` -> der Caller erstellt eine frische per-Scope-Kopie.
+Damit entfaellt das Geister-Duplicate komplett, ohne Workaround in
+`updateFile` und ohne `interfaceDbComponent` umzuverdrahten. Folge:
+identische Files koennen pro Mandate **mehrfach** existieren (eines
+pro featureInstance) -- das ist gewollt, weil sie pro Scope eigene
+Folder-/Tag-/Neutralize-Metadaten brauchen.
+
+## Auch wichtig
+
+* Der RBAC-Check in `interfaceDbChat.createMessage`
+ (`checkRbacPermission(ChatWorkflow, "update", workflowId)`) gilt
+ weiter -- ohne Update-Permission auf den Workflow kein ChatDocument-Bind.
+ Gewollt.
+* `chatService.storeMessageWithDocuments` synchronisiert auch das
+ in-Memory-Workflow-Objekt (haengt die neue ChatMessage an
+ `workflow.messages` an), so dass der gleiche Run sofort darauf
+ zugreifen kann.
+* Das `documentsLabel` (`label`-Argument im Helper) wird vom
+ alternativen `docList: