fixed nodes handovers
This commit is contained in:
parent
b500bfa6c1
commit
c140bd14d4
8 changed files with 276 additions and 40 deletions
|
|
@ -110,11 +110,13 @@ class DocumentReferenceList(BaseModel):
|
|||
# docItem:documentId
|
||||
references.append(DocumentItemReference(documentId=parts[0]))
|
||||
|
||||
# Unknown format - skip or log warning
|
||||
else:
|
||||
# Try to parse as simple string (backward compatibility)
|
||||
# Assume it's a label if it doesn't match known patterns
|
||||
if refStr:
|
||||
if not refStr:
|
||||
continue
|
||||
import re
|
||||
if re.match(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', refStr, re.I):
|
||||
references.append(DocumentItemReference(documentId=refStr))
|
||||
else:
|
||||
references.append(DocumentListReference(label=refStr))
|
||||
|
||||
return cls(references=references)
|
||||
|
|
|
|||
|
|
@ -24,8 +24,13 @@ AI_NODES = [
|
|||
{"name": "resultType", "type": "string", "required": False, "frontendType": "select",
|
||||
"frontendOptions": {"options": ["txt", "json", "md", "csv", "xml", "html", "pdf", "docx", "xlsx", "pptx", "png", "jpg"]},
|
||||
"description": t("Ausgabeformat"), "default": "txt"},
|
||||
{"name": "documentList", "type": "string", "required": False, "frontendType": "hidden",
|
||||
"description": t("Dokumentenliste (via Wire oder DataRef)"), "default": ""},
|
||||
{"name": "documentList", "type": "DocumentList", "required": False, "frontendType": "dataRef",
|
||||
"description": t("Dokumentenliste (Upstream-Output binden)"), "default": ""},
|
||||
{"name": "context", "type": "string", "required": False, "frontendType": "dataRef",
|
||||
"description": t("Kontextdaten fuer den Prompt (Upstream-Output binden)"), "default": ""},
|
||||
{"name": "documentTheme", "type": "string", "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": "boolean", "required": False, "frontendType": "checkbox",
|
||||
"description": t("Einfacher Modus"), "default": True},
|
||||
] + _AI_COMMON_PARAMS,
|
||||
|
|
@ -62,8 +67,8 @@ AI_NODES = [
|
|||
"label": t("Dokument zusammenfassen"),
|
||||
"description": t("Dokumentinhalt zusammenfassen"),
|
||||
"parameters": [
|
||||
{"name": "documentList", "type": "string", "required": True, "frontendType": "hidden",
|
||||
"description": t("Dokumentenliste (via Wire oder DataRef)"), "default": ""},
|
||||
{"name": "documentList", "type": "DocumentList", "required": True, "frontendType": "dataRef",
|
||||
"description": t("Dokumentenliste (Upstream-Output binden)"), "default": ""},
|
||||
{"name": "summaryLength", "type": "string", "required": False, "frontendType": "select",
|
||||
"frontendOptions": {"options": ["brief", "medium", "detailed"]},
|
||||
"description": t("Kurz, mittel oder ausführlich"), "default": "medium"},
|
||||
|
|
@ -82,8 +87,8 @@ AI_NODES = [
|
|||
"label": t("Dokument übersetzen"),
|
||||
"description": t("Dokument in Zielsprache übersetzen"),
|
||||
"parameters": [
|
||||
{"name": "documentList", "type": "string", "required": True, "frontendType": "hidden",
|
||||
"description": t("Dokumentenliste (via Wire oder DataRef)"), "default": ""},
|
||||
{"name": "documentList", "type": "DocumentList", "required": True, "frontendType": "dataRef",
|
||||
"description": t("Dokumentenliste (Upstream-Output binden)"), "default": ""},
|
||||
{"name": "targetLanguage", "type": "string", "required": True, "frontendType": "text",
|
||||
"description": t("Zielsprache (z.B. de, en, French)")},
|
||||
] + _AI_COMMON_PARAMS,
|
||||
|
|
@ -101,8 +106,8 @@ AI_NODES = [
|
|||
"label": t("Dokument konvertieren"),
|
||||
"description": t("Dokument in anderes Format konvertieren"),
|
||||
"parameters": [
|
||||
{"name": "documentList", "type": "string", "required": True, "frontendType": "hidden",
|
||||
"description": t("Dokumentenliste (via Wire oder DataRef)"), "default": ""},
|
||||
{"name": "documentList", "type": "DocumentList", "required": True, "frontendType": "dataRef",
|
||||
"description": t("Dokumentenliste (Upstream-Output binden)"), "default": ""},
|
||||
{"name": "targetFormat", "type": "string", "required": True, "frontendType": "select",
|
||||
"frontendOptions": {"options": ["docx", "pdf", "xlsx", "csv", "txt", "html", "json", "md"]},
|
||||
"description": t("Zielformat")},
|
||||
|
|
|
|||
|
|
@ -383,7 +383,7 @@ def _buildAnalysisWorkflowGraph(prompt: str) -> Dict[str, Any]:
|
|||
"parameters": {
|
||||
"aiPrompt": prompt + _FINANCE_STYLE_HINT,
|
||||
"context": {"type": "ref", "nodeId": "refresh", "path": ["data", "accountingData"]},
|
||||
"requireNeutralization": True,
|
||||
"requireNeutralization": False,
|
||||
"simpleMode": False,
|
||||
}, "position": {"x": 500, "y": 0}},
|
||||
],
|
||||
|
|
@ -478,7 +478,7 @@ TEMPLATE_WORKFLOWS = [
|
|||
),
|
||||
"resultType": "xlsx",
|
||||
"documentTheme": "finance",
|
||||
"requireNeutralization": True,
|
||||
"requireNeutralization": False,
|
||||
"documentList": {"type": "ref", "nodeId": "trigger", "path": ["payload", "documentList"]},
|
||||
"context": {"type": "ref", "nodeId": "refresh", "path": ["data", "accountingData"]},
|
||||
"simpleMode": False,
|
||||
|
|
|
|||
|
|
@ -115,6 +115,10 @@ def initBootstrap(db: DatabaseConnector) -> None:
|
|||
# Bootstrap system workflow templates for graphical editor
|
||||
_bootstrapSystemTemplates(db)
|
||||
|
||||
# Sync feature template workflows (update graph of existing instance workflows
|
||||
# whose templateSourceId matches a current code-defined template)
|
||||
_syncFeatureTemplateWorkflows()
|
||||
|
||||
# Ensure billing settings and accounts exist for all mandates
|
||||
_bootstrapBilling()
|
||||
|
||||
|
|
@ -190,6 +194,97 @@ def _bootstrapSystemTemplates(db: DatabaseConnector) -> None:
|
|||
logger.warning(f"System workflow template bootstrap failed: {e}")
|
||||
|
||||
|
||||
def _syncFeatureTemplateWorkflows() -> None:
|
||||
"""Sync existing instance-scoped workflows with current code-defined templates.
|
||||
|
||||
For each feature that exposes getTemplateWorkflows(), find all AutoWorkflow
|
||||
rows whose templateSourceId matches a template ID and update their graph
|
||||
if the code-defined version has changed. Preserves instance-specific
|
||||
fields (label, tags, targetFeatureInstanceId, invocations, active).
|
||||
Idempotent, runs on every boot.
|
||||
"""
|
||||
import json
|
||||
|
||||
try:
|
||||
from modules.system.registry import loadFeatureMainModules
|
||||
from modules.features.graphicalEditor.datamodelFeatureGraphicalEditor import AutoWorkflow
|
||||
from modules.features.graphicalEditor.interfaceFeatureGraphicalEditor import graphicalEditorDatabase
|
||||
|
||||
mainModules = loadFeatureMainModules()
|
||||
|
||||
templatesBySourceId: dict = {}
|
||||
for featureCode, mod in mainModules.items():
|
||||
getTemplateWorkflows = getattr(mod, "getTemplateWorkflows", None)
|
||||
if not getTemplateWorkflows:
|
||||
continue
|
||||
try:
|
||||
templates = getTemplateWorkflows() or []
|
||||
except Exception:
|
||||
continue
|
||||
for tpl in templates:
|
||||
tplId = tpl.get("id")
|
||||
if tplId:
|
||||
templatesBySourceId[tplId] = tpl
|
||||
|
||||
if not templatesBySourceId:
|
||||
logger.info("_syncFeatureTemplateWorkflows: no templates found, skipping")
|
||||
return
|
||||
logger.info(f"_syncFeatureTemplateWorkflows: found {len(templatesBySourceId)} template(s): {list(templatesBySourceId.keys())}")
|
||||
|
||||
greenfieldDb = DatabaseConnector(
|
||||
dbHost=APP_CONFIG.get("DB_HOST", "localhost"),
|
||||
dbDatabase=graphicalEditorDatabase,
|
||||
dbUser=APP_CONFIG.get("DB_USER"),
|
||||
dbPassword=APP_CONFIG.get("DB_PASSWORD_SECRET") or APP_CONFIG.get("DB_PASSWORD"),
|
||||
)
|
||||
|
||||
updated = 0
|
||||
for sourceId, tpl in templatesBySourceId.items():
|
||||
instances = greenfieldDb.getRecordset(AutoWorkflow, recordFilter={
|
||||
"templateSourceId": sourceId,
|
||||
"isTemplate": False,
|
||||
})
|
||||
if not instances:
|
||||
continue
|
||||
|
||||
canonicalGraph = tpl.get("graph", {})
|
||||
|
||||
for inst in instances:
|
||||
instId = inst.get("id") if isinstance(inst, dict) else getattr(inst, "id", None)
|
||||
targetInstanceId = (
|
||||
inst.get("targetFeatureInstanceId") if isinstance(inst, dict)
|
||||
else getattr(inst, "targetFeatureInstanceId", None)
|
||||
) or ""
|
||||
|
||||
graphJson = json.dumps(canonicalGraph)
|
||||
graphJson = graphJson.replace("{{featureInstanceId}}", targetInstanceId)
|
||||
newGraph = json.loads(graphJson)
|
||||
|
||||
existingGraph = inst.get("graph") if isinstance(inst, dict) else getattr(inst, "graph", None)
|
||||
if isinstance(existingGraph, str):
|
||||
try:
|
||||
existingGraph = json.loads(existingGraph)
|
||||
except Exception:
|
||||
existingGraph = None
|
||||
|
||||
if existingGraph == newGraph:
|
||||
logger.debug(f"_syncFeatureTemplateWorkflows: graph unchanged for workflow {instId} (template={sourceId})")
|
||||
continue
|
||||
logger.debug(f"_syncFeatureTemplateWorkflows: graph DIFFERS for workflow {instId} (template={sourceId}), updating")
|
||||
|
||||
greenfieldDb.recordModify(AutoWorkflow, instId, {"graph": newGraph})
|
||||
updated += 1
|
||||
logger.info(f"_syncFeatureTemplateWorkflows: updated graph for workflow {instId} (template={sourceId})")
|
||||
|
||||
if updated:
|
||||
logger.info(f"_syncFeatureTemplateWorkflows: synced {updated} workflow(s) with current templates")
|
||||
else:
|
||||
logger.info("_syncFeatureTemplateWorkflows: all instance graphs already match current templates")
|
||||
greenfieldDb.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"Feature template workflow sync failed: {e}")
|
||||
|
||||
|
||||
def _buildSystemTemplates():
|
||||
"""Build the graph definitions for platform system templates."""
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -58,6 +58,36 @@ def _getUserAccessibleInstanceIds(userId: str) -> list[str]:
|
|||
]
|
||||
|
||||
|
||||
_FILE_REF_KEYS = ("fileId", "documentId", "fileIds", "documents")
|
||||
|
||||
|
||||
def _extractFileIdsFromValue(value, accumulator: set[str]) -> None:
|
||||
"""Recursively scan a value (dict/list/str) for file id references."""
|
||||
if isinstance(value, dict):
|
||||
for key, sub in value.items():
|
||||
if key in _FILE_REF_KEYS:
|
||||
_collectFileIdsFromRef(sub, accumulator)
|
||||
else:
|
||||
_extractFileIdsFromValue(sub, accumulator)
|
||||
elif isinstance(value, list):
|
||||
for item in value:
|
||||
_extractFileIdsFromValue(item, accumulator)
|
||||
|
||||
|
||||
def _collectFileIdsFromRef(val, accumulator: set[str]) -> None:
|
||||
"""Add file ids from a value located under a known file-reference key."""
|
||||
if isinstance(val, str) and val:
|
||||
accumulator.add(val)
|
||||
elif isinstance(val, list):
|
||||
for v in val:
|
||||
if isinstance(v, str) and v:
|
||||
accumulator.add(v)
|
||||
elif isinstance(v, dict) and v.get("id"):
|
||||
accumulator.add(v["id"])
|
||||
elif isinstance(val, dict) and val.get("id"):
|
||||
accumulator.add(val["id"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
@limiter.limit("60/minute")
|
||||
def listWorkspaceRuns(
|
||||
|
|
@ -198,40 +228,68 @@ def getWorkspaceRunDetail(
|
|||
steps = [dict(s) for s in stepRecords]
|
||||
steps.sort(key=lambda s: s.get("startedAt") or 0)
|
||||
|
||||
fileItems: list = []
|
||||
allFileIds: set[str] = set()
|
||||
perStepFileIds: list[tuple[set[str], set[str]]] = []
|
||||
for step in steps:
|
||||
inputIds: set[str] = set()
|
||||
outputIds: set[str] = set()
|
||||
_extractFileIdsFromValue(step.get("inputSnapshot") or {}, inputIds)
|
||||
_extractFileIdsFromValue(step.get("output") or {}, outputIds)
|
||||
perStepFileIds.append((inputIds, outputIds))
|
||||
allFileIds.update(inputIds)
|
||||
allFileIds.update(outputIds)
|
||||
|
||||
nodeOutputs = run.get("nodeOutputs") or {}
|
||||
runLevelIds: set[str] = set()
|
||||
_extractFileIdsFromValue(nodeOutputs, runLevelIds)
|
||||
allFileIds.update(runLevelIds)
|
||||
|
||||
fileMetaById: dict[str, dict] = {}
|
||||
try:
|
||||
from modules.datamodels.datamodelFiles import FileItem
|
||||
from modules.interfaces.interfaceDbManagement import ComponentObjects
|
||||
mgmtDb = ComponentObjects().db
|
||||
if mgmtDb._ensureTableExists(FileItem):
|
||||
nodeOutputs = run.get("nodeOutputs") or {}
|
||||
fileIds: set[str] = set()
|
||||
for nodeId, output in nodeOutputs.items():
|
||||
if not isinstance(output, dict):
|
||||
continue
|
||||
for key in ("fileId", "documentId", "fileIds", "documents"):
|
||||
val = output.get(key)
|
||||
if isinstance(val, str) and val:
|
||||
fileIds.add(val)
|
||||
elif isinstance(val, list):
|
||||
for v in val:
|
||||
if isinstance(v, str) and v:
|
||||
fileIds.add(v)
|
||||
elif isinstance(v, dict) and v.get("id"):
|
||||
fileIds.add(v["id"])
|
||||
for fid in fileIds:
|
||||
for fid in allFileIds:
|
||||
try:
|
||||
rec = mgmtDb.getRecord(FileItem, fid)
|
||||
if rec:
|
||||
fileItems.append(dict(rec))
|
||||
recDict = dict(rec)
|
||||
fileMetaById[fid] = {
|
||||
"id": fid,
|
||||
"fileName": recDict.get("fileName") or recDict.get("name"),
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning("getWorkspaceRunDetail: file lookup failed: %s", e)
|
||||
|
||||
def _resolveFileList(ids: set[str]) -> list[dict]:
|
||||
return [fileMetaById[fid] for fid in ids if fid in fileMetaById]
|
||||
|
||||
assignedFileIds: set[str] = set()
|
||||
for step, (inputIds, outputIds) in zip(steps, perStepFileIds):
|
||||
step["inputFiles"] = _resolveFileList(inputIds)
|
||||
step["outputFiles"] = _resolveFileList(outputIds)
|
||||
assignedFileIds.update(inputIds)
|
||||
assignedFileIds.update(outputIds)
|
||||
|
||||
unassignedFiles = _resolveFileList(allFileIds - assignedFileIds)
|
||||
allFiles = _resolveFileList(allFileIds)
|
||||
|
||||
run["workflowLabel"] = run.get("label") or workflow.get("label") or wfId
|
||||
run["targetFeatureInstanceId"] = tid
|
||||
|
||||
targetInstanceLabel = None
|
||||
if tid:
|
||||
try:
|
||||
from modules.routes.routeHelpers import resolveInstanceLabels
|
||||
labelMap = resolveInstanceLabels([tid])
|
||||
targetInstanceLabel = labelMap.get(tid)
|
||||
except Exception:
|
||||
pass
|
||||
run["targetInstanceLabel"] = targetInstanceLabel
|
||||
|
||||
return {
|
||||
"run": run,
|
||||
"workflow": {
|
||||
|
|
@ -242,5 +300,6 @@ def getWorkspaceRunDetail(
|
|||
"tags": workflow.get("tags", []),
|
||||
} if workflow else None,
|
||||
"steps": steps,
|
||||
"files": fileItems,
|
||||
"files": allFiles,
|
||||
"unassignedFiles": unassignedFiles,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,13 +199,8 @@ class ChatService:
|
|||
label = parts[1]
|
||||
messageFound = None
|
||||
for message in workflow.messages:
|
||||
# Validate message belongs to this workflow
|
||||
msgWorkflowId = getattr(message, 'workflowId', None)
|
||||
if not msgWorkflowId or msgWorkflowId != workflowId:
|
||||
if msgWorkflowId:
|
||||
logger.warning(f"Message {message.id} has workflowId {msgWorkflowId} but belongs to workflow {workflowId}. Skipping.")
|
||||
else:
|
||||
logger.warning(f"Message {message.id} has no workflowId. Skipping.")
|
||||
continue
|
||||
|
||||
msgLabel = getattr(message, 'documentsLabel', None)
|
||||
|
|
@ -213,7 +208,6 @@ class ChatService:
|
|||
messageFound = message
|
||||
break
|
||||
|
||||
# If found, add documents
|
||||
if messageFound and messageFound.documents:
|
||||
allDocuments.extend(messageFound.documents)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -73,6 +73,47 @@ def _action_docs_to_content_parts(services, docs: List[Any]) -> List[ContentPart
|
|||
logger.info(f"ai.process: Extracted {len(ec.parts)} parts from {name} (no persistence)")
|
||||
return all_parts
|
||||
|
||||
def _resolve_file_refs_to_content_parts(services, fileIdRefs) -> List[ContentPart]:
|
||||
"""Fetch files by ID from the file store and extract content.
|
||||
Used for automation2 workflows where documents are file-store references,
|
||||
not chat message attachments."""
|
||||
from modules.datamodels.datamodelExtraction import ExtractionOptions, MergeStrategy
|
||||
|
||||
mgmt = getattr(services, 'interfaceDbComponent', None)
|
||||
extraction = getattr(services, 'extraction', None)
|
||||
if not mgmt or not extraction:
|
||||
logger.warning("_resolve_file_refs_to_content_parts: missing interfaceDbComponent or extraction service")
|
||||
return []
|
||||
|
||||
allParts: List[ContentPart] = []
|
||||
opts = ExtractionOptions(prompt="", mergeStrategy=MergeStrategy())
|
||||
for ref in fileIdRefs:
|
||||
fileId = ref.documentId
|
||||
fileMeta = mgmt.getFile(fileId)
|
||||
if not fileMeta:
|
||||
logger.warning(f"_resolve_file_refs_to_content_parts: file {fileId} not found")
|
||||
continue
|
||||
fileData = mgmt.getFileData(fileId)
|
||||
if not fileData:
|
||||
logger.warning(f"_resolve_file_refs_to_content_parts: no data for file {fileId}")
|
||||
continue
|
||||
fileName = getattr(fileMeta, 'fileName', fileId)
|
||||
mimeType = getattr(fileMeta, 'mimeType', 'application/octet-stream')
|
||||
ec = extraction.extractContentFromBytes(
|
||||
documentBytes=fileData,
|
||||
fileName=fileName,
|
||||
mimeType=mimeType,
|
||||
documentId=fileId,
|
||||
options=opts,
|
||||
)
|
||||
for p in ec.parts:
|
||||
if p.data or getattr(p, "typeGroup", "") == "image":
|
||||
p.metadata.setdefault("originalFileName", fileName)
|
||||
allParts.append(p)
|
||||
logger.info(f"_resolve_file_refs_to_content_parts: extracted {len(ec.parts)} parts from {fileName}")
|
||||
return allParts
|
||||
|
||||
|
||||
async def process(self, parameters: Dict[str, Any]) -> ActionResult:
|
||||
operationId = None
|
||||
try:
|
||||
|
|
@ -129,6 +170,17 @@ async def process(self, parameters: Dict[str, Any]) -> ActionResult:
|
|||
f"ai.process: Coerced documentList ({type(documentListParam).__name__}) "
|
||||
f"to DocumentReferenceList with {len(documentList.references)} references"
|
||||
)
|
||||
|
||||
# Resolve DocumentItemReferences (file-ID refs from automation2) directly
|
||||
# from the file store. These cannot be resolved via chat messages.
|
||||
from modules.datamodels.datamodelDocref import DocumentItemReference
|
||||
fileIdRefs = [r for r in documentList.references if isinstance(r, DocumentItemReference)]
|
||||
if fileIdRefs:
|
||||
extractedParts = _resolve_file_refs_to_content_parts(self.services, fileIdRefs)
|
||||
if extractedParts:
|
||||
inline_content_parts = (inline_content_parts or []) + extractedParts
|
||||
remaining = [r for r in documentList.references if not isinstance(r, DocumentItemReference)]
|
||||
documentList = DocumentReferenceList(references=remaining)
|
||||
|
||||
# Optional: if omitted, formats determined from prompt. Default "txt" is validation fallback only.
|
||||
resultType = parameters.get("resultType")
|
||||
|
|
@ -157,7 +209,19 @@ async def process(self, parameters: Dict[str, Any]) -> ActionResult:
|
|||
|
||||
mimeMap = {"txt": "text/plain", "json": "application/json", "html": "text/html", "md": "text/markdown", "csv": "text/csv", "xml": "application/xml"}
|
||||
output_mime_type = mimeMap.get(normalized_result_type, "text/plain") if normalized_result_type else "text/plain"
|
||||
|
||||
|
||||
# Normalize context: workflow refs may resolve to dict/list instead of str
|
||||
paramContext = parameters.get("context")
|
||||
if paramContext is not None and not isinstance(paramContext, str):
|
||||
try:
|
||||
paramContext = json.dumps(paramContext, ensure_ascii=False, default=str)
|
||||
parameters["context"] = paramContext
|
||||
logger.info(f"ai.process: Serialized non-string context ({type(parameters.get('context')).__name__}) to JSON ({len(paramContext)} chars)")
|
||||
except Exception as e:
|
||||
logger.warning(f"ai.process: Failed to serialize context: {e}")
|
||||
paramContext = str(paramContext)
|
||||
parameters["context"] = paramContext
|
||||
|
||||
# Phase 7.3: Pass documentList and/or contentParts to AI service
|
||||
contentParts: Optional[List[ContentPart]] = inline_content_parts
|
||||
if "contentParts" in parameters and not inline_content_parts:
|
||||
|
|
|
|||
|
|
@ -56,6 +56,23 @@ class MethodAi(MethodBase):
|
|||
required=False,
|
||||
description="Document reference(s) in any format to use as input/context"
|
||||
),
|
||||
"context": WorkflowActionParameter(
|
||||
name="context",
|
||||
type="str",
|
||||
frontendType=FrontendType.TEXTAREA,
|
||||
required=False,
|
||||
default="",
|
||||
description="Additional context data (string or upstream-bound dict/list, e.g. accounting data) appended to the prompt. Non-string values are JSON-serialized."
|
||||
),
|
||||
"documentTheme": WorkflowActionParameter(
|
||||
name="documentTheme",
|
||||
type="str",
|
||||
frontendType=FrontendType.SELECT,
|
||||
frontendOptions=["general", "finance", "legal", "technical", "hr"],
|
||||
required=False,
|
||||
default="general",
|
||||
description="Style hint for the document renderer (e.g. finance, legal). Used by the AI agent to choose colors and layout."
|
||||
),
|
||||
"resultType": WorkflowActionParameter(
|
||||
name="resultType",
|
||||
type="str",
|
||||
|
|
|
|||
Loading…
Reference in a new issue