wiki/b-reference/platform-core/agent-file-bridge.md
2026-06-02 09:42:12 +02:00

220 lines
10 KiB
Markdown

<!-- status: canonical -->
<!-- lastReviewed: 2026-04-29 -->
<!-- verifiedAgainst: platform-core/modules/serviceCenter/services/serviceAgent/coreTools/_helpers.py (_attachFileAsChatDocument helper); _dataSourceTools.py / _workspaceTools.py / _mediaTools.py (5 file-producing agent tools); mainServiceAgent.py runAgent workflow propagation; mainServiceChat.py (storeMessageWithDocuments + getChatDocumentsFromDocumentList); workflowProcessor.persistTaskResult (canonical pattern); methodTrustee.extractFromFiles (peer pattern); datamodelChat.ChatDocument; datamodelDocref.coerceDocumentReferenceList -->
# 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:<id>`-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<br/>saveGeneratedFile| FI[FileItem]
FI -->|_attachFileAsChatDocument| Bridge((Bridge Helper))
Bridge -->|storeMessageWithDocuments| CM[ChatMessage]
CM --> CD[ChatDocument]
CD -.fileId.-> FI
CD -.id used as.-> Ref{{docItem:&lt;chatDocId&gt;}}
Ref -->|documentList parameter| AITools[ai_process<br/>ai_summarizeDocument<br/>context_extractContent<br/>context_neutralizeData]
AITools -->|getChatDocumentsFromDocumentList| CD
```
## Komponenten
### `_attachFileAsChatDocument` (Single Source of Truth)
Datei: `platform-core/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:<chatDocId>` -> hineinkopieren in `documentList` von AI-Tools.
* `file id: <fileId>` -> fuer `readFile`, `searchInFileContent`,
`writeFile mode=append`, Bild-Embeds (`![alt](file:<id>)`).
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: `platform-core/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:<service>` |
| `writeFile mode=create` | `_workspaceTools.py` | nach `saveUploadedFile`, `label=writeFile:<name>` |
| `renderDocument` | `_mediaTools.py` | im Multi-Doc-Loop pro generiertem File, `label=renderDocument:<name>` |
| `generateImage` | `_mediaTools.py` | im Multi-Image-Loop pro PNG, `label=generateImage:<name>` |
| `createChart` | `_mediaTools.py` | nach Chart-PNG-Save, `label=createChart:<name>` |
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
`platform-core/modules/datamodels/datamodelDocref.py` parst das, was der LLM
am anderen Ende reinschickt -- typischerweise
* `["docItem:<id>", "docItem:<id2>"]` (kanonisch),
* `[{"id": "<id>", "name": "..."}]` (LLM-Listen-Style),
* `{"documents": [{"id": "<id>"}, ...]}` (LLM-Dict-Wrapper),
* `"docItem:<id>"` (Single-String).
Alle vier Shapes werden in `DocumentReferenceList` mit
`DocumentItemReference(documentId=<id>)` umgewandelt; daraus baut
`getChatDocumentsFromDocumentList` die `docItem:<id>`-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: <class 'dict'>` | 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:<label>`-Resolver benutzt, der mehrere Files
ueber ein gemeinsames Label sammelt -- nicht primaerer Pfad, aber
vorhanden.