added upload folder location for all document creation nodes

This commit is contained in:
Ida 2026-05-06 10:19:20 +02:00
parent eeb9a4a161
commit 6e3da0d0d8
8 changed files with 110 additions and 12 deletions

View file

@ -30,9 +30,6 @@ AI_NODES = [
{"name": "context", "type": "Any", "required": False, "frontendType": "contextBuilder",
"description": t("Daten aus vorherigen Schritten"), "default": "",
"graphInherit": {"port": 0, "kind": "primaryTextRef"}},
{"name": "documentTheme", "type": "str", "required": False, "frontendType": "select",
"frontendOptions": {"options": ["general", "finance", "legal", "technical", "hr"]},
"description": t("Dokument-Thema (Style-Hinweis fuer den Renderer)"), "default": "general"},
{"name": "simpleMode", "type": "bool", "required": False, "frontendType": "checkbox",
"description": t("Einfacher Modus"), "default": True},
] + _AI_COMMON_PARAMS,
@ -80,9 +77,15 @@ AI_NODES = [
{"name": "documentList", "type": "DocumentList", "required": True, "frontendType": "dataRef",
"description": t("Dokumente aus vorherigen Schritten"),
"graphInherit": {"port": 0, "kind": "documentListWire"}},
{"name": "resultType", "type": "str", "required": False, "frontendType": "select",
"frontendOptions": {"options": ["txt", "json", "md", "csv", "xml", "html", "pdf", "docx", "xlsx", "pptx", "png", "jpg"]},
"description": t("Ausgabeformat"), "default": "txt"},
{"name": "summaryLength", "type": "str", "required": False, "frontendType": "select",
"frontendOptions": {"options": ["brief", "medium", "detailed"]},
"description": t("Kurz, mittel oder ausführlich"), "default": "medium"},
{"name": "folderId", "type": "str", "required": False, "frontendType": "userFileFolder",
"description": t("Zielordner in Meine Dateien"),
"default": ""},
] + _AI_COMMON_PARAMS,
"inputs": 1,
"outputs": 1,
@ -101,8 +104,14 @@ AI_NODES = [
{"name": "documentList", "type": "DocumentList", "required": True, "frontendType": "dataRef",
"description": t("Dokumente aus vorherigen Schritten"),
"graphInherit": {"port": 0, "kind": "documentListWire"}},
{"name": "resultType", "type": "str", "required": False, "frontendType": "select",
"frontendOptions": {"options": ["txt", "json", "md", "csv", "xml", "html", "pdf", "docx", "xlsx", "pptx", "png", "jpg"]},
"description": t("Ausgabeformat"), "default": "txt"},
{"name": "targetLanguage", "type": "str", "required": True, "frontendType": "text",
"description": t("Zielsprache (z.B. de, en, French)")},
{"name": "folderId", "type": "str", "required": False, "frontendType": "userFileFolder",
"description": t("Zielordner in Meine Dateien"),
"default": ""},
] + _AI_COMMON_PARAMS,
"inputs": 1,
"outputs": 1,
@ -124,6 +133,9 @@ AI_NODES = [
{"name": "targetFormat", "type": "str", "required": True, "frontendType": "select",
"frontendOptions": {"options": ["docx", "pdf", "xlsx", "csv", "txt", "html", "json", "md"]},
"description": t("Zielformat")},
{"name": "folderId", "type": "str", "required": False, "frontendType": "userFileFolder",
"description": t("Zielordner in Meine Dateien"),
"default": ""},
] + _AI_COMMON_PARAMS,
"inputs": 1,
"outputs": 1,
@ -149,6 +161,9 @@ AI_NODES = [
{"name": "documentType", "type": "str", "required": False, "frontendType": "select",
"frontendOptions": {"options": ["letter", "memo", "proposal", "contract", "report", "email"]},
"description": t("Dokumentart (Inhaltshinweis fuer die KI)"), "default": "proposal"},
{"name": "folderId", "type": "str", "required": False, "frontendType": "userFileFolder",
"description": t("Zielordner in Meine Dateien"),
"default": ""},
{"name": "context", "type": "Any", "required": False, "frontendType": "contextBuilder",
"description": t("Daten aus vorherigen Schritten"), "default": "",
"graphInherit": {"port": 0, "kind": "primaryTextRef"}},
@ -177,6 +192,9 @@ AI_NODES = [
{"name": "resultType", "type": "str", "required": False, "frontendType": "select",
"frontendOptions": {"options": ["py", "js", "ts", "html", "java", "cpp", "txt", "json", "csv", "xml"]},
"description": t("Datei-Endung der erzeugten Code-Datei"), "default": "py"},
{"name": "folderId", "type": "str", "required": False, "frontendType": "userFileFolder",
"description": t("Zielordner in Meine Dateien"),
"default": ""},
{"name": "context", "type": "Any", "required": False, "frontendType": "contextBuilder",
"description": t("Daten aus vorherigen Schritten"), "default": "",
"graphInherit": {"port": 0, "kind": "primaryTextRef"}},

View file

@ -15,6 +15,9 @@ FILE_NODES = [
"description": t("Ausgabeformat"), "default": "docx"},
{"name": "title", "type": "str", "required": False, "frontendType": "text",
"description": t("Dokumenttitel")},
{"name": "folderId", "type": "str", "required": False, "frontendType": "userFileFolder",
"description": t("Zielordner in Meine Dateien"),
"default": ""},
{"name": "context", "type": "Any", "required": False, "frontendType": "contextBuilder",
"description": t("Daten aus vorherigen Schritten"), "default": "",
"graphInherit": {"port": 0, "kind": "primaryTextRef"}},

View file

@ -1345,16 +1345,34 @@ class ComponentObjects:
return newfileName
counter += 1
def createFile(self, name: str, mimeType: str, content: bytes) -> FileItem:
def createFile(
self,
name: str,
mimeType: str,
content: bytes,
folderId: Optional[str] = None,
) -> FileItem:
"""Creates a new file entry if user has permission. Computes fileHash and fileSize from content.
Duplicate check: if a file with the same user + fileHash + fileName already exists,
the existing file is returned instead of creating a new one.
Same hash with different name is allowed (intentional copy by user).
When ``folderId`` is set, the folder must exist and the user must be allowed to modify it.
"""
if not self.checkRbacPermission(FileItem, "create"):
raise PermissionError("No permission to create files")
resolved_folder_id: Optional[str] = None
if folderId is not None:
raw = str(folderId).strip()
if raw:
folder = self.getFolder(raw)
if not folder:
raise FileNotFoundError(f"Folder {raw} not found")
self._requireFolderWriteAccess(folder, raw, "update")
resolved_folder_id = raw
# Compute file size and hash
fileSize = len(content)
fileHash = hashlib.sha256(content).hexdigest()
@ -1386,6 +1404,7 @@ class ComponentObjects:
mimeType=mimeType,
fileSize=fileSize,
fileHash=fileHash,
folderId=resolved_folder_id,
)
# Ensure audit user is always stored: workflow/singleton contexts sometimes leave
# the connector without _current_user_id, so _saveRecord skips sysCreatedBy →

View file

@ -88,6 +88,9 @@ class FrontendType(str, Enum):
FILTER_EXPRESSION = "filterExpression"
"""Filter expression builder for data.filter"""
USER_FILE_FOLDER = "userFileFolder"
"""User file storage folder (graph editor): browse My Files tree or create folders."""
# Mapping of custom types to their API endpoint for dynamic options
CUSTOM_TYPE_OPTIONS_API: Dict[FrontendType, str] = {

View file

@ -393,6 +393,13 @@ class ActionNodeExecutor:
return _normalizeError(e, outputSchema)
# 9. Persist generated documents as files and build JSON-safe output
_raw_folder_id = resolvedParams.get("folderId")
persist_folder_id: Optional[str] = None
if _raw_folder_id is not None:
_s = str(_raw_folder_id).strip()
if _s:
persist_folder_id = _s
docsList = []
for d in (result.documents or []):
dumped = d.model_dump() if hasattr(d, "model_dump") else dict(d) if isinstance(d, dict) else d
@ -432,7 +439,7 @@ class ActionNodeExecutor:
_mgmt = _getMgmtInterface(_owner, mandateId=_mandateId, featureInstanceId=_instanceId)
_docName = dumped.get("documentName") or f"workflow-result-{nodeId}.bin"
_mimeType = dumped.get("mimeType") or "application/octet-stream"
_fileItem = _mgmt.createFile(_docName, _mimeType, rawBytes)
_fileItem = _mgmt.createFile(_docName, _mimeType, rawBytes, folderId=persist_folder_id)
_mgmt.createFileData(_fileItem.id, rawBytes)
dumped["fileId"] = _fileItem.id
dumped["id"] = _fileItem.id

View file

@ -194,7 +194,14 @@ class MethodAi(MethodBase):
required=False,
default="txt",
description="Output file extension"
)
),
"folderId": WorkflowActionParameter(
name="folderId",
type="str",
frontendType=FrontendType.USER_FILE_FOLDER,
required=False,
description="Target folder in My Files when persisting workflow output",
),
},
execute=summarizeDocument.__get__(self, self.__class__)
),
@ -239,7 +246,14 @@ class MethodAi(MethodBase):
frontendType=FrontendType.TEXT,
required=False,
description="Output file extension. If not specified, uses same format as input"
)
),
"folderId": WorkflowActionParameter(
name="folderId",
type="str",
frontendType=FrontendType.USER_FILE_FOLDER,
required=False,
description="Target folder in My Files when persisting workflow output",
),
},
execute=translateDocument.__get__(self, self.__class__)
),
@ -271,7 +285,14 @@ class MethodAi(MethodBase):
required=False,
default=True,
description="Whether to preserve document structure (headings, tables, etc.)"
)
),
"folderId": WorkflowActionParameter(
name="folderId",
type="str",
frontendType=FrontendType.USER_FILE_FOLDER,
required=False,
description="Target folder in My Files when persisting workflow output",
),
},
execute=convertDocument.__get__(self, self.__class__)
),
@ -335,6 +356,13 @@ class MethodAi(MethodBase):
required=False,
description="Legacy/API output format extension (e.g. txt, docx). Ignored when outputFormat is set."
),
"folderId": WorkflowActionParameter(
name="folderId",
type="str",
frontendType=FrontendType.USER_FILE_FOLDER,
required=False,
description="Target folder in My Files when persisting workflow output",
),
},
execute=generateDocument.__get__(self, self.__class__)
),
@ -366,7 +394,14 @@ class MethodAi(MethodBase):
frontendOptions=["py", "js", "ts", "html", "java", "cpp", "txt", "json", "csv", "xml"],
required=False,
description="Output format (html, js, py, json, csv, xml, etc.). Optional: if omitted, formats are determined from prompt by AI. This action can return MULTIPLE files in a single call when the prompt requests multiple files. With per-document format determination, AI can determine different formats for different files based on prompt. When multiple files are requested, the action will return multiple documents (one per file)."
)
),
"folderId": WorkflowActionParameter(
name="folderId",
type="str",
frontendType=FrontendType.USER_FILE_FOLDER,
required=False,
description="Target folder in My Files when persisting workflow output",
),
},
execute=generateCode.__get__(self, self.__class__)
),

View file

@ -1,7 +1,7 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
from typing import Dict, Any
from typing import Dict, Any, Optional
import base64
import binascii
@ -17,6 +17,7 @@ logger = logging.getLogger(__name__)
def _persistDocumentsToUserFiles(
action_documents: list,
services,
folder_id: Optional[str] = None,
) -> None:
"""Persist file.create output documents to user's file storage (like upload).
Adds fileId to each document's validationMetadata for download links in UI."""
@ -70,7 +71,7 @@ def _persistDocumentsToUserFiles(
doc_name,
len(content),
)
file_item = mgmt.createFile(doc_name, mime, content)
file_item = mgmt.createFile(doc_name, mime, content, folderId=folder_id)
logger.info("file.create persist: createFile returned id=%s", file_item.id)
ok = mgmt.createFileData(file_item.id, content)
logger.info("file.create persist: createFileData returned %s for id=%s", ok, file_item.id)
@ -111,6 +112,11 @@ async def create(self, parameters: Dict[str, Any]) -> ActionResult:
"de",
)
folder_id: Optional[str] = None
raw_folder = parameters.get("folderId")
if raw_folder is not None and str(raw_folder).strip():
folder_id = str(raw_folder).strip()
try:
structured_content = markdownToDocumentJson(context, title, language)
if templateName:
@ -164,7 +170,7 @@ async def create(self, parameters: Dict[str, Any]) -> ActionResult:
},
))
_persistDocumentsToUserFiles(action_documents, self.services)
_persistDocumentsToUserFiles(action_documents, self.services, folder_id=folder_id)
return ActionResult.isSuccess(documents=action_documents)
except Exception as e:

View file

@ -73,6 +73,13 @@ class MethodFile(MethodBase):
default="de",
description="Language code",
),
"folderId": WorkflowActionParameter(
name="folderId",
type="str",
frontendType=FrontendType.USER_FILE_FOLDER,
required=False,
description="Optional My Files folder to store created documents",
),
},
execute=create.__get__(self, self.__class__),
),