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: