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

10 KiB

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

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

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.ids, 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.