From 06e68c343b01b69c9fe0434b6f26e1b2636311ed Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Tue, 9 Jun 2026 22:59:26 +0200
Subject: [PATCH] automation fixes
---
app.py | 2 +
modules/datamodels/datamodelNavigation.py | 51 ++--------
modules/dbHelpers/fkLabelResolver.py | 18 ++++
.../realEstate/routeFeatureRealEstate.py | 18 +++-
.../features/trustee/routeFeatureTrustee.py | 38 ++++---
modules/routes/routeAdminFeatures.py | 7 +-
modules/routes/routeAdminRbacRules.py | 5 +-
modules/routes/routeAudit.py | 10 ++
modules/routes/routeDataPrompts.py | 2 +-
modules/routes/routeSubscription.py | 4 +
modules/routes/routeWorkflowAutomation.py | 98 ++++++++++++++++++-
.../services/serviceChat/mainServiceChat.py | 3 +
.../services/serviceKnowledge/udbNodes.py | 2 +-
.../engine/executionEngine.py | 2 +-
.../engine/executors/actionNodeExecutor.py | 16 +--
.../methodContext/actions/extractContent.py | 14 +--
tests/unit/services/test_inheritFlags.py | 2 +-
17 files changed, 207 insertions(+), 85 deletions(-)
diff --git a/app.py b/app.py
index 68341361..55cc7fc0 100644
--- a/app.py
+++ b/app.py
@@ -501,6 +501,8 @@ async def lifespan(app: FastAPI):
return
if isinstance(exc, ConnectionAbortedError):
return
+ if exc and "LocalProtocolError" in type(exc).__name__:
+ return
loop.default_exception_handler(ctx)
main_loop.set_exception_handler(_suppressClientDisconnect)
except RuntimeError:
diff --git a/modules/datamodels/datamodelNavigation.py b/modules/datamodels/datamodelNavigation.py
index 5c40a165..101cef99 100644
--- a/modules/datamodels/datamodelNavigation.py
+++ b/modules/datamodels/datamodelNavigation.py
@@ -150,52 +150,21 @@ NAVIGATION_SECTIONS = [
},
],
},
- # --- Workflow-Automation (System-Komponente, cross-mandate) ---
+ # --- Solution Design (System-Komponente, cross-mandate) ---
+ # Single nav entry; tabs are managed internally by WorkflowAutomationHubPage.
{
"id": "workflowAutomation",
- "title": t("Workflow-Automation"),
+ "title": t("Lösungsdesign"),
"order": 25,
"items": [
{
- "id": "wa-workflows",
- "objectKey": "ui.system.workflowAutomation.workflows",
- "label": t("Workflows"),
+ "id": "wa-hub",
+ "objectKey": "ui.system.workflowAutomation",
+ "label": t("Workflow-Automation"),
"icon": "FaSitemap",
- "path": "/workflow-automation?tab=workflows",
+ "path": "/workflow-automation",
"order": 10,
},
- {
- "id": "wa-editor",
- "objectKey": "ui.system.workflowAutomation.editor",
- "label": t("Editor"),
- "icon": "FaProjectDiagram",
- "path": "/workflow-automation?tab=editor",
- "order": 20,
- },
- {
- "id": "wa-templates",
- "objectKey": "ui.system.workflowAutomation.templates",
- "label": t("Vorlagen"),
- "icon": "FaCopy",
- "path": "/workflow-automation?tab=templates",
- "order": 30,
- },
- {
- "id": "wa-runs",
- "objectKey": "ui.system.workflowAutomation.runs",
- "label": t("Läufe"),
- "icon": "FaPlay",
- "path": "/workflow-automation?tab=runs",
- "order": 40,
- },
- {
- "id": "wa-tasks",
- "objectKey": "ui.system.workflowAutomation.tasks",
- "label": t("Tasks"),
- "icon": "FaTasks",
- "path": "/workflow-automation?tab=tasks",
- "order": 50,
- },
],
},
# --- Administration (with subgroups) ---
@@ -237,7 +206,7 @@ NAVIGATION_SECTIONS = [
{
"id": "admin-users",
"objectKey": "ui.admin.users",
- "label": t("Benutzer"),
+ "label": t("Übersicht"),
"icon": "FaUsers",
"path": "/admin/users",
"order": 10,
@@ -246,7 +215,7 @@ NAVIGATION_SECTIONS = [
{
"id": "admin-invitations",
"objectKey": "ui.admin.invitations",
- "label": t("Benutzer-Einladungen"),
+ "label": t("Einladungen"),
"icon": "FaEnvelopeOpenText",
"path": "/admin/invitations",
"order": 20,
@@ -255,7 +224,7 @@ NAVIGATION_SECTIONS = [
{
"id": "admin-user-access-overview",
"objectKey": "ui.admin.userAccessOverview",
- "label": t("Benutzer-Zugriffsübersicht"),
+ "label": t("Zugriffe"),
"icon": "FaClipboardList",
"path": "/admin/user-access-overview",
"order": 30,
diff --git a/modules/dbHelpers/fkLabelResolver.py b/modules/dbHelpers/fkLabelResolver.py
index 35a673af..e9829001 100644
--- a/modules/dbHelpers/fkLabelResolver.py
+++ b/modules/dbHelpers/fkLabelResolver.py
@@ -96,6 +96,23 @@ def resolveRoleLabels(db, ids: List[str]) -> Dict[str, Optional[str]]:
return out
+def resolveFileLabels(db, ids: List[str]) -> Dict[str, Optional[str]]:
+ """Resolve FileItem IDs to fileName. Returns None for unresolvable."""
+ if not ids:
+ return {}
+ from modules.datamodels.datamodelFiles import FileItem as _FileItem
+ recs = db.getRecordset(
+ _FileItem,
+ recordFilter={"id": list(set(ids))},
+ ) or []
+ out: Dict[str, Optional[str]] = {i: None for i in ids}
+ for r in recs:
+ fid = r.get("id")
+ if fid:
+ out[fid] = r.get("fileName") or None
+ return out
+
+
# ---------------------------------------------------------------------------
# Resolver registry
# ---------------------------------------------------------------------------
@@ -105,6 +122,7 @@ _BUILTIN_FK_RESOLVERS: Dict[str, Callable] = {
"FeatureInstance": resolveInstanceLabels,
"UserInDB": resolveUserLabels,
"Role": resolveRoleLabels,
+ "FileItem": resolveFileLabels,
}
diff --git a/modules/features/realEstate/routeFeatureRealEstate.py b/modules/features/realEstate/routeFeatureRealEstate.py
index 5723ab39..d32c48cd 100644
--- a/modules/features/realEstate/routeFeatureRealEstate.py
+++ b/modules/features/realEstate/routeFeatureRealEstate.py
@@ -263,7 +263,7 @@ def get_projects(
if mode == "filterValues":
if not column:
raise HTTPException(status_code=400, detail="column parameter required for mode=filterValues")
- enrichRowsWithFkLabels(itemDicts, Projekt, db=interface.db)
+ enrichRowsWithFkLabels(itemDicts, Projekt, db=getRootInterface().db)
return handleFilterValuesInMemory(itemDicts, column, pagination)
return handleIdsInMemory(itemDicts, pagination)
@@ -271,7 +271,9 @@ def get_projects(
paginationParams = _parsePagination(pagination)
if paginationParams:
from modules.dbHelpers.paginationHelpers import applyFiltersAndSort
+ from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
itemDicts = [i.model_dump() if hasattr(i, 'model_dump') else i for i in items]
+ enrichRowsWithFkLabels(itemDicts, Projekt, db=getRootInterface().db)
filtered = applyFiltersAndSort(itemDicts, paginationParams)
total_items = len(filtered)
total_pages = (total_items + paginationParams.pageSize - 1) // paginationParams.pageSize
@@ -289,7 +291,10 @@ def get_projects(
filters=paginationParams.filters
)
)
- return PaginatedResponse(items=items, pagination=None)
+ from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
+ itemDicts = [i.model_dump() if hasattr(i, 'model_dump') else i for i in items]
+ enrichRowsWithFkLabels(itemDicts, Projekt, db=getRootInterface().db)
+ return PaginatedResponse(items=itemDicts, pagination=None)
@router.get("/{instanceId}/projects/{projectId}", response_model=Projekt)
@@ -405,7 +410,7 @@ def get_parcels(
if mode == "filterValues":
if not column:
raise HTTPException(status_code=400, detail="column parameter required for mode=filterValues")
- enrichRowsWithFkLabels(itemDicts, Parzelle, db=interface.db)
+ enrichRowsWithFkLabels(itemDicts, Parzelle, db=getRootInterface().db)
return handleFilterValuesInMemory(itemDicts, column, pagination)
return handleIdsInMemory(itemDicts, pagination)
@@ -413,7 +418,9 @@ def get_parcels(
paginationParams = _parsePagination(pagination)
if paginationParams:
from modules.dbHelpers.paginationHelpers import applyFiltersAndSort
+ from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
itemDicts = [i.model_dump() if hasattr(i, 'model_dump') else i for i in items]
+ enrichRowsWithFkLabels(itemDicts, Parzelle, db=getRootInterface().db)
filtered = applyFiltersAndSort(itemDicts, paginationParams)
total_items = len(filtered)
total_pages = (total_items + paginationParams.pageSize - 1) // paginationParams.pageSize
@@ -431,7 +438,10 @@ def get_parcels(
filters=paginationParams.filters
)
)
- return PaginatedResponse(items=items, pagination=None)
+ from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
+ itemDicts = [i.model_dump() if hasattr(i, 'model_dump') else i for i in items]
+ enrichRowsWithFkLabels(itemDicts, Parzelle, db=getRootInterface().db)
+ return PaginatedResponse(items=itemDicts, pagination=None)
@router.get("/{instanceId}/parcels/{parcelId}", response_model=Parzelle)
diff --git a/modules/features/trustee/routeFeatureTrustee.py b/modules/features/trustee/routeFeatureTrustee.py
index c06f8604..8b5ba94a 100644
--- a/modules/features/trustee/routeFeatureTrustee.py
+++ b/modules/features/trustee/routeFeatureTrustee.py
@@ -437,7 +437,7 @@ def get_organisations(
return [r.model_dump() if hasattr(r, "model_dump") else r for r in items]
if paginationParams and hasattr(result, 'items'):
- enriched = enrichRowsWithFkLabels(_toDicts(result.items), TrusteeOrganisation, db=interface.db)
+ enriched = enrichRowsWithFkLabels(_toDicts(result.items), TrusteeOrganisation, db=getRootInterface().db)
return {
"items": enriched,
"pagination": PaginationMetadata(
@@ -450,7 +450,7 @@ def get_organisations(
).model_dump(),
}
items = result if isinstance(result, list) else result.items
- enriched = enrichRowsWithFkLabels(_toDicts(items), TrusteeOrganisation, db=interface.db)
+ enriched = enrichRowsWithFkLabels(_toDicts(items), TrusteeOrganisation, db=getRootInterface().db)
return {"items": enriched, "pagination": None}
@@ -557,7 +557,7 @@ def get_roles(
return [r.model_dump() if hasattr(r, "model_dump") else r for r in items]
if paginationParams and hasattr(result, 'items'):
- enriched = enrichRowsWithFkLabels(_toDicts(result.items), TrusteeRole, db=interface.db)
+ enriched = enrichRowsWithFkLabels(_toDicts(result.items), TrusteeRole, db=getRootInterface().db)
return {
"items": enriched,
"pagination": PaginationMetadata(
@@ -570,7 +570,7 @@ def get_roles(
).model_dump(),
}
items = result if isinstance(result, list) else result.items
- enriched = enrichRowsWithFkLabels(_toDicts(items), TrusteeRole, db=interface.db)
+ enriched = enrichRowsWithFkLabels(_toDicts(items), TrusteeRole, db=getRootInterface().db)
return {"items": enriched, "pagination": None}
@@ -677,7 +677,7 @@ def get_all_access(
return [r.model_dump() if hasattr(r, "model_dump") else r for r in items]
if paginationParams and hasattr(result, 'items'):
- enriched = enrichRowsWithFkLabels(_toDicts(result.items), TrusteeAccess, db=interface.db)
+ enriched = enrichRowsWithFkLabels(_toDicts(result.items), TrusteeAccess, db=getRootInterface().db)
return {
"items": enriched,
"pagination": PaginationMetadata(
@@ -690,7 +690,7 @@ def get_all_access(
).model_dump(),
}
items = result if isinstance(result, list) else result.items
- enriched = enrichRowsWithFkLabels(_toDicts(items), TrusteeAccess, db=interface.db)
+ enriched = enrichRowsWithFkLabels(_toDicts(items), TrusteeAccess, db=getRootInterface().db)
return {"items": enriched, "pagination": None}
@@ -827,7 +827,7 @@ def get_contracts(
return [r.model_dump() if hasattr(r, "model_dump") else r for r in items]
if paginationParams and hasattr(result, 'items'):
- enriched = enrichRowsWithFkLabels(_toDicts(result.items), TrusteeContract, db=interface.db)
+ enriched = enrichRowsWithFkLabels(_toDicts(result.items), TrusteeContract, db=getRootInterface().db)
return {
"items": enriched,
"pagination": PaginationMetadata(
@@ -840,7 +840,7 @@ def get_contracts(
).model_dump(),
}
items = result if isinstance(result, list) else result.items
- enriched = enrichRowsWithFkLabels(_toDicts(items), TrusteeContract, db=interface.db)
+ enriched = enrichRowsWithFkLabels(_toDicts(items), TrusteeContract, db=getRootInterface().db)
return {"items": enriched, "pagination": None}
@@ -953,6 +953,7 @@ def get_documents(
context: RequestContext = Depends(getRequestContext)
):
"""Get all documents (metadata only) with optional pagination."""
+ from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
mandateId = _validateInstanceAccess(instanceId, context)
if mode in ("filterValues", "ids"):
@@ -966,8 +967,9 @@ def get_documents(
return [r.model_dump() if hasattr(r, 'model_dump') else r for r in items]
if paginationParams and hasattr(result, 'items'):
+ enriched = enrichRowsWithFkLabels(_itemsToDicts(result.items), TrusteeDocument, db=getRootInterface().db)
return {
- "items": _itemsToDicts(result.items),
+ "items": enriched,
"pagination": PaginationMetadata(
currentPage=paginationParams.page or 1,
pageSize=paginationParams.pageSize or 20,
@@ -978,7 +980,8 @@ def get_documents(
).model_dump(),
}
items = result if isinstance(result, list) else result.items
- return {"items": _itemsToDicts(items), "pagination": None}
+ enriched = enrichRowsWithFkLabels(_itemsToDicts(items), TrusteeDocument, db=getRootInterface().db)
+ return {"items": enriched, "pagination": None}
def _handleDocumentMode(instanceId, mandateId, mode, column, pagination, context):
@@ -991,7 +994,7 @@ def _handleDocumentMode(instanceId, mandateId, mode, column, pagination, context
raise HTTPException(status_code=400, detail="column parameter required for mode=filterValues")
result = interface.getAllDocuments(None)
items = [r.model_dump() if hasattr(r, 'model_dump') else r for r in (result.items if hasattr(result, 'items') else result)]
- enrichRowsWithFkLabels(items, TrusteeDocument, db=interface.db)
+ enrichRowsWithFkLabels(items, TrusteeDocument, db=getRootInterface().db)
return handleFilterValuesInMemory(items, column, pagination)
if mode == "ids":
result = interface.getAllDocuments(None)
@@ -1229,6 +1232,7 @@ def get_positions(
context: RequestContext = Depends(getRequestContext)
):
"""Get all positions with optional pagination."""
+ from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
mandateId = _validateInstanceAccess(instanceId, context)
if mode in ("filterValues", "ids"):
@@ -1241,9 +1245,12 @@ def get_positions(
def _itemsToDicts(items):
return [r.model_dump() if hasattr(r, 'model_dump') else r for r in items]
+ featureResolvers = _buildFeatureInternalResolvers(TrusteePosition, interface.db)
+
if paginationParams and hasattr(result, 'items'):
items = _itemsToDicts(result.items)
_enrichPositionsWithSyncStatus(items, interface, instanceId)
+ enrichRowsWithFkLabels(items, TrusteePosition, db=getRootInterface().db, extraResolvers=featureResolvers or None)
return {
"items": items,
"pagination": PaginationMetadata(
@@ -1258,6 +1265,7 @@ def get_positions(
rawItems = result if isinstance(result, list) else result.items
items = _itemsToDicts(rawItems)
_enrichPositionsWithSyncStatus(items, interface, instanceId)
+ enrichRowsWithFkLabels(items, TrusteePosition, db=getRootInterface().db, extraResolvers=featureResolvers or None)
return {"items": items, "pagination": None}
@@ -1273,7 +1281,7 @@ def _handlePositionMode(instanceId, mandateId, mode, column, pagination, context
result = interface.getAllPositions(None)
items = [r.model_dump() if hasattr(r, 'model_dump') else r for r in (result.items if hasattr(result, 'items') else result)]
_enrichPositionsWithSyncStatus(items, interface, instanceId)
- enrichRowsWithFkLabels(items, TrusteePositionView, db=interface.db)
+ enrichRowsWithFkLabels(items, TrusteePositionView, db=getRootInterface().db)
return handleFilterValuesInMemory(items, column, pagination)
if mode == "ids":
result = interface.getAllPositions(None)
@@ -2075,7 +2083,7 @@ def _paginatedReadEndpoint(
rawItems = result.items if hasattr(result, "items") else result
items = [r.model_dump() if hasattr(r, "model_dump") else r for r in rawItems]
featureResolvers = _buildFeatureInternalResolvers(modelClass, interface.db)
- enrichRowsWithFkLabels(items, modelClass, db=interface.db, extraResolvers=featureResolvers or None)
+ enrichRowsWithFkLabels(items, modelClass, db=getRootInterface().db, extraResolvers=featureResolvers or None)
return handleFilterValuesInMemory(items, column, pagination)
if mode == "ids":
@@ -2113,7 +2121,7 @@ def _paginatedReadEndpoint(
if paginationParams and hasattr(result, "items"):
enriched = enrichRowsWithFkLabels(
_itemsToDicts(result.items), modelClass,
- db=interface.db, extraResolvers=featureResolvers or None,
+ db=getRootInterface().db, extraResolvers=featureResolvers or None,
)
return {
"items": enriched,
@@ -2129,7 +2137,7 @@ def _paginatedReadEndpoint(
items = result.items if hasattr(result, "items") else result
enriched = enrichRowsWithFkLabels(
_itemsToDicts(items), modelClass,
- db=interface.db, extraResolvers=featureResolvers or None,
+ db=getRootInterface().db, extraResolvers=featureResolvers or None,
)
return {"items": enriched, "pagination": None}
diff --git a/modules/routes/routeAdminFeatures.py b/modules/routes/routeAdminFeatures.py
index 0a9626dc..e8daa385 100644
--- a/modules/routes/routeAdminFeatures.py
+++ b/modules/routes/routeAdminFeatures.py
@@ -472,12 +472,13 @@ def list_feature_instances(
items = [inst.model_dump() for inst in instances]
+ from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
+ from modules.datamodels.datamodelFeatures import FeatureInstance
+ enrichRowsWithFkLabels(items, FeatureInstance, db=rootInterface.db)
+
if mode == "filterValues":
if not column:
raise HTTPException(status_code=400, detail="column parameter required for mode=filterValues")
- from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
- from modules.datamodels.datamodelFeatures import FeatureInstance
- enrichRowsWithFkLabels(items, FeatureInstance, db=rootInterface.db)
return handleFilterValuesInMemory(items, column, pagination)
if mode == "ids":
diff --git a/modules/routes/routeAdminRbacRules.py b/modules/routes/routeAdminRbacRules.py
index 36577de7..83aaef00 100644
--- a/modules/routes/routeAdminRbacRules.py
+++ b/modules/routes/routeAdminRbacRules.py
@@ -940,6 +940,8 @@ def list_roles(
if paginationParams:
from modules.dbHelpers.paginationHelpers import applyFiltersAndSort
+ from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
+ enrichRowsWithFkLabels(result, Role, db=interface.db)
sortedResult = applyFiltersAndSort(result, paginationParams)
totalItems = len(sortedResult)
totalPages = math.ceil(totalItems / paginationParams.pageSize) if totalItems > 0 else 0
@@ -959,7 +961,8 @@ def list_roles(
)
)
else:
- # No pagination - return all roles
+ from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
+ enrichRowsWithFkLabels(result, Role, db=interface.db)
return PaginatedResponse(
items=result,
pagination=None
diff --git a/modules/routes/routeAudit.py b/modules/routes/routeAudit.py
index 9dfd074d..f6aa16c8 100644
--- a/modules/routes/routeAudit.py
+++ b/modules/routes/routeAudit.py
@@ -363,6 +363,16 @@ async def getNeutralizationMappings(
_enrichUserAndInstanceLabels(items, context)
+ fileIds = list({r.get("fileId") for r in items if r.get("fileId")})
+ if fileIds:
+ from modules.dbHelpers.fkLabelResolver import resolveFileLabels
+ from modules.interfaces.interfaceDbApp import getRootInterface
+ fileMap = resolveFileLabels(getRootInterface().db, fileIds)
+ for r in items:
+ fid = r.get("fileId")
+ if fid and fid in fileMap:
+ r["fileIdLabel"] = fileMap[fid] or fid
+
if mode == "filterValues" and column:
items = _applySortFilterSearch(items, filtersJson=filters)
return _distinctColumnValues(items, column)
diff --git a/modules/routes/routeDataPrompts.py b/modules/routes/routeDataPrompts.py
index bbb566e7..164d4233 100644
--- a/modules/routes/routeDataPrompts.py
+++ b/modules/routes/routeDataPrompts.py
@@ -120,7 +120,7 @@ def get_prompts(
def _promptsToEnrichedDicts(promptItems):
dicts = [r.model_dump() if hasattr(r, 'model_dump') else (dict(r) if not isinstance(r, dict) else r) for r in promptItems]
- enrichRowsWithFkLabels(dicts, Prompt, db=managementInterface.db)
+ enrichRowsWithFkLabels(dicts, Prompt, db=getAppInterface(currentUser).db)
return dicts
managementInterface = interfaceDbManagement.getInterface(currentUser)
diff --git a/modules/routes/routeSubscription.py b/modules/routes/routeSubscription.py
index 709d70e5..57a00093 100644
--- a/modules/routes/routeSubscription.py
+++ b/modules/routes/routeSubscription.py
@@ -514,6 +514,10 @@ def getAllSubscriptions(
raise HTTPException(status_code=400, detail=f"Invalid pagination parameter: {str(e)}")
enriched = _buildEnrichedSubscriptions()
+ from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
+ from modules.datamodels.datamodelSubscription import MandateSubscription
+ from modules.interfaces.interfaceDbApp import getRootInterface as _getRootIf
+ enrichRowsWithFkLabels(enriched, MandateSubscription, db=_getRootIf().db)
filtered = applyFiltersAndSort(enriched, paginationParams)
if paginationParams:
diff --git a/modules/routes/routeWorkflowAutomation.py b/modules/routes/routeWorkflowAutomation.py
index 4fb7cca9..fe3e8853 100644
--- a/modules/routes/routeWorkflowAutomation.py
+++ b/modules/routes/routeWorkflowAutomation.py
@@ -63,6 +63,8 @@ async def _listWorkflows(
pagination: Optional[str] = Query(default=None),
mandateId: Optional[str] = Query(default=None),
):
+ from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
+ from modules.interfaces.interfaceDbApp import getRootInterface as _getRootIface
db = _getWorkflowAutomationDb()
try:
db._ensureTableExists(AutoWorkflow)
@@ -76,6 +78,7 @@ async def _listWorkflows(
params = _parsePaginationOr400(pagination)
records = db.getRecordset(AutoWorkflow, recordFilter=scopeFilter)
+ enrichRowsWithFkLabels(records or [], AutoWorkflow, db=_getRootIface().db)
if params:
filtered = applyFiltersAndSort(records or [], params)
pageItems, totalItems = paginateInMemory(filtered, params)
@@ -169,6 +172,8 @@ async def _listRuns(
mandateId: Optional[str] = Query(default=None),
workflowId: Optional[str] = Query(default=None),
):
+ from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
+ from modules.interfaces.interfaceDbApp import getRootInterface as _getRootIface
db = _getWorkflowAutomationDb()
try:
db._ensureTableExists(AutoRun)
@@ -185,6 +190,15 @@ async def _listRuns(
params = _parsePaginationOr400(pagination)
records = db.getRecordset(AutoRun, recordFilter=scopeFilter)
+
+ def _resolveWorkflowLabels(ids):
+ wfRecs = db.getRecordset(AutoWorkflow, recordFilter={"id": list(set(ids))}) or []
+ return {r.get("id"): r.get("label") or r.get("name") for r in wfRecs}
+
+ enrichRowsWithFkLabels(
+ records or [], AutoRun, db=_getRootIface().db,
+ extraResolvers={"workflowId": _resolveWorkflowLabels},
+ )
if params:
filtered = applyFiltersAndSort(records or [], params)
pageItems, totalItems = paginateInMemory(filtered, params)
@@ -991,7 +1005,7 @@ def _getMetrics(
try:
workflows = db.getRecordset(AutoWorkflow, recordFilter=scopeFilter) or [] if db._ensureTableExists(AutoWorkflow) else []
wfIds = [w.get("id") for w in workflows]
- runFilter = {"workflowId": {"$in": wfIds}} if wfIds else {"workflowId": "__none__"}
+ runFilter = {"workflowId": wfIds} if wfIds else {"workflowId": "__none__"}
runs = db.getRecordset(AutoRun, recordFilter=runFilter) or [] if db._ensureTableExists(AutoRun) else []
tasks = db.getRecordset(AutoTask, recordFilter=runFilter) or [] if db._ensureTableExists(AutoTask) else []
finally:
@@ -1275,7 +1289,8 @@ def _getRunDetail(
if tid:
try:
from modules.dbHelpers.fkLabelResolver import resolveInstanceLabels
- labelMap = resolveInstanceLabels(db, [tid])
+ from modules.interfaces.interfaceDbApp import getRootInterface as _getRootIface
+ labelMap = resolveInstanceLabels(_getRootIface().db, [tid])
targetInstanceLabel = labelMap.get(tid)
except Exception:
pass
@@ -1465,6 +1480,85 @@ async def _executeWorkflow(
return result
+@router.post("/execute")
+@limiter.limit("30/minute")
+async def _executeWorkflowFromBody(
+ request: Request,
+ body: dict = Body(..., description="{ workflowId?, graph?, targetInstanceId?, payload?, runEnvelope? }"),
+ context: RequestContext = Depends(getRequestContext),
+) -> dict:
+ """Execute a workflow — workflowId from body or ad-hoc graph execution."""
+ from modules.workflowAutomation.mainWorkflowAutomation import _getWorkflowAutomationServices
+ from modules.workflowAutomation.engine.executionEngine import executeGraph
+ from modules.interfaces.interfaceWorkflowAutomation import _getWorkflowAutomationInterface
+ from modules.workflows.processing.shared.methodDiscovery import discoverMethods
+
+ userId = str(context.user.id) if context.user else None
+ workflowId = body.get("workflowId") or ""
+ targetInstanceId = body.get("targetInstanceId") or ""
+
+ wf = None
+ if workflowId:
+ db = _getWorkflowAutomationDb()
+ try:
+ db._ensureTableExists(AutoWorkflow)
+ wf = db.getRecord(AutoWorkflow, workflowId)
+ finally:
+ db.close()
+ if not wf:
+ raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
+ _validateWorkflowAccess(context, wf, "execute")
+
+ mandateId = (wf.get("mandateId") if wf else None) or str(context.mandateId or "")
+ instanceId = (wf.get("featureInstanceId") if wf else None) or targetInstanceId or str(context.featureInstanceId or "")
+ targetFeatureInstanceId = (wf.get("targetFeatureInstanceId") if wf else None) or targetInstanceId or ""
+
+ services = _getWorkflowAutomationServices(
+ context.user,
+ mandateId=mandateId,
+ featureInstanceId=instanceId,
+ )
+ discoverMethods(services)
+
+ graph = body.get("graph") or body.get("payload") or {}
+ if wf and not (graph.get("nodes") or []):
+ graph = wf.get("graph") or {}
+
+ logger.info(
+ "workflowAutomation /execute: workflowId=%s nodes=%d userId=%s",
+ workflowId, len(graph.get("nodes") or []), userId,
+ )
+
+ workflowForEnvelope = wf
+ runEnv = _buildExecuteRunEnvelope(
+ body,
+ workflowForEnvelope,
+ userId,
+ getattr(context.user, "language", None) if context.user else None,
+ )
+ wfLabel = (wf.get("label") if wf else None) or ""
+
+ iface = _getWorkflowAutomationInterface(context.user, mandateId, instanceId)
+ result = await executeGraph(
+ graph=graph,
+ services=services,
+ workflowId=workflowId or None,
+ instanceId=instanceId,
+ userId=userId,
+ mandateId=mandateId,
+ automation2_interface=iface,
+ run_envelope=runEnv,
+ label=wfLabel,
+ targetFeatureInstanceId=targetFeatureInstanceId,
+ )
+ logger.info(
+ "workflowAutomation /execute result: success=%s error=%s paused=%s",
+ result.get("success"), result.get("error"), result.get("paused"),
+ )
+ _startEmailPollerIfNeeded(result)
+ return result
+
+
# ---------------------------------------------------------------------------
# Version management
# ---------------------------------------------------------------------------
diff --git a/modules/serviceCenter/services/serviceChat/mainServiceChat.py b/modules/serviceCenter/services/serviceChat/mainServiceChat.py
index 18ab2a68..44e42583 100644
--- a/modules/serviceCenter/services/serviceChat/mainServiceChat.py
+++ b/modules/serviceCenter/services/serviceChat/mainServiceChat.py
@@ -1003,6 +1003,9 @@ class ChatService:
"""Get workflow by ID by delegating to the chat interface"""
try:
logger.debug(f"getWorkflow called with workflowId: {workflowId}")
+ if workflowId.startswith("transient-"):
+ logger.debug(f"getWorkflow: skipping DB lookup for transient workflow {workflowId}")
+ return None
result = self.interfaceDbChat.getWorkflow(workflowId)
if result:
logger.debug(f"getWorkflow returned workflow with ID: {result.id}")
diff --git a/modules/serviceCenter/services/serviceKnowledge/udbNodes.py b/modules/serviceCenter/services/serviceKnowledge/udbNodes.py
index 00f07bfb..c6bc0622 100644
--- a/modules/serviceCenter/services/serviceKnowledge/udbNodes.py
+++ b/modules/serviceCenter/services/serviceKnowledge/udbNodes.py
@@ -21,7 +21,7 @@ model (see wiki/b-reference/platform/unified-data-bar.md):
- FdsRecordNode (+children)-- feature-owned FeatureDataSource records
- FdsFieldNode -- virtual per-column nodes under fdsTable
-The classes use `_inheritFlags.py` as a helper module for the actual
+The classes use `modules/serviceCenter/core/flagResolution.py` as a helper module for the actual
walk/aggregate/cascade arithmetic, so the inheritance semantics live in
one place. The classes themselves only express "what does this node type
DO" -- ownership, RBAC, persistence routing, child enumeration.
diff --git a/modules/workflowAutomation/engine/executionEngine.py b/modules/workflowAutomation/engine/executionEngine.py
index e188adab..807b6743 100644
--- a/modules/workflowAutomation/engine/executionEngine.py
+++ b/modules/workflowAutomation/engine/executionEngine.py
@@ -770,7 +770,7 @@ async def executeGraph(
waFileLogger: Optional[RunFileLogger] = None
nodeOutputs: Dict[str, Any] = dict(initialNodeOutputs or {})
- if not runId and automation2_interface and workflowId and not is_resume:
+ if not runId and automation2_interface and not is_resume:
run_context = {
"connectionMap": connectionMap,
"inputSources": inputSources,
diff --git a/modules/workflowAutomation/engine/executors/actionNodeExecutor.py b/modules/workflowAutomation/engine/executors/actionNodeExecutor.py
index 12dffc31..aa472f15 100644
--- a/modules/workflowAutomation/engine/executors/actionNodeExecutor.py
+++ b/modules/workflowAutomation/engine/executors/actionNodeExecutor.py
@@ -599,9 +599,9 @@ class ActionNodeExecutor:
logger.exception("ActionNodeExecutor node %s FAILED: %s", nodeId, e)
return _normalizeError(e, outputSchema)
finally:
- if chatService:
+ if self.services.chat:
try:
- chatService.progressLogFinish(nodeOperationId, actionSuccess)
+ self.services.chat.progressLogFinish(nodeOperationId, actionSuccess)
except Exception:
pass
@@ -630,11 +630,11 @@ class ActionNodeExecutor:
rawBytes = coerceDocumentDataToBytes(rawData)
if isinstance(dumped, dict) and rawBytes:
try:
- _mgmt = self.services.interfaceDbComponent
+ _chatSvc = self.services.chat
_docName = dumped.get("documentName") or f"workflow-result-{nodeId}.bin"
_mimeType = dumped.get("mimeType") or "application/octet-stream"
- _fileItem = _mgmt.createFile(_docName, _mimeType, rawBytes, folderId=persist_folder_id)
- _mgmt.createFileData(_fileItem.id, rawBytes)
+ _fileItem = _chatSvc.createFile(_docName, _mimeType, rawBytes, folderId=persist_folder_id)
+ _chatSvc.createFileData(_fileItem.id, rawBytes)
dumped["fileId"] = _fileItem.id
dumped["id"] = _fileItem.id
dumped["fileName"] = _fileItem.fileName
@@ -656,7 +656,7 @@ class ActionNodeExecutor:
"documents": docsList,
"count": len(docsList),
}
- _attachConnectionProvenance(list_out, resolvedParams, outputSchema, chatService, self.services)
+ _attachConnectionProvenance(list_out, resolvedParams, outputSchema, self.services.chat, self.services)
return normalizeToSchema(list_out, outputSchema)
extractedContext = ""
@@ -751,7 +751,7 @@ class ActionNodeExecutor:
"mode": data_dict.get("mode", resolvedParams.get("mode", "summarize")),
"count": int(data_dict.get("count", 0)),
}
- _attachConnectionProvenance(cr_out, resolvedParams, outputSchema, chatService, self.services)
+ _attachConnectionProvenance(cr_out, resolvedParams, outputSchema, self.services.chat, self.services)
return normalizeToSchema(cr_out, outputSchema)
if nodeDef.get("popDocumentsFromOutput"):
@@ -760,7 +760,7 @@ class ActionNodeExecutor:
if outputSchema in ("AiResult", "ActionResult") and result.success:
_attach_unified_presentation_data(out, node_def=nodeDef)
- _attachConnectionProvenance(out, resolvedParams, outputSchema, chatService, self.services)
+ _attachConnectionProvenance(out, resolvedParams, outputSchema, self.services.chat, self.services)
# When the node declares ``surfaceDataAsTopLevel`` (typical for
# dynamic-schema context nodes whose output keys are graph-defined),
diff --git a/modules/workflows/methods/methodContext/actions/extractContent.py b/modules/workflows/methods/methodContext/actions/extractContent.py
index 5172ced2..19677d2b 100644
--- a/modules/workflows/methods/methodContext/actions/extractContent.py
+++ b/modules/workflows/methods/methodContext/actions/extractContent.py
@@ -1194,8 +1194,8 @@ def _persist_extracted_image_parts(
)
return content_extracted_serial, artifacts
- if services and hasattr(services, "interfaceDbComponent"):
- mgmt = services.interfaceDbComponent
+ if services and hasattr(services, "chat"):
+ mgmt = services.chat
else:
from modules.interfaces.interfaceDbManagement import getInterface as _get_mgmt
from modules.security.rootAccess import getRootUser
@@ -1206,7 +1206,7 @@ def _persist_extracted_image_parts(
return content_extracted_serial, artifacts
if not mgmt:
- logger.warning("extractContent image persist: no interfaceDbComponent available")
+ logger.warning("extractContent image persist: no chat service available")
return content_extracted_serial, artifacts
stem = re.sub(r"[^\w\-]+", "_", name_stem).strip("_") or "extract"
@@ -1310,11 +1310,11 @@ _IMAGE_MAX_DIMENSION = 1200
def _get_mgmt_for_presentation_render(services: Any) -> Optional[Any]:
- mgmt = getattr(services, "interfaceDbComponent", None) if services else None
- if mgmt:
- return mgmt
if not services:
return None
+ chat = getattr(services, "chat", None)
+ if chat:
+ return chat
try:
import modules.interfaces.interfaceDbManagement as iface
@@ -1385,7 +1385,7 @@ def _load_image_bytes_by_file_id(services: Any, file_id: str) -> Optional[bytes]
if not mgmt or not hasattr(mgmt, "getFileData"):
raise ValueError(
"no management interface available to load persisted image bytes — "
- "services.interfaceDbComponent / mandate / instance must be set"
+ "services.chat / mandate / instance must be set"
)
return mgmt.getFileData(str(file_id))
diff --git a/tests/unit/services/test_inheritFlags.py b/tests/unit/services/test_inheritFlags.py
index be3b41cf..07b29bc0 100644
--- a/tests/unit/services/test_inheritFlags.py
+++ b/tests/unit/services/test_inheritFlags.py
@@ -17,7 +17,7 @@ import unittest
from typing import List
from unittest.mock import MagicMock
-from modules.serviceCenter.services.serviceKnowledge import _inheritFlags
+from modules.serviceCenter.core import flagResolution as _inheritFlags
def _ds(idVal: str, path: str, **flags) -> dict: