From 4f8473bd701ed35bc2d4e1fafad0958c502ebb15 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Mon, 8 Jun 2026 23:35:31 +0200
Subject: [PATCH] cleaned servicebag and removed servicehub
---
app.py | 11 +
modules/datamodels/datamodelViews.py | 2 +-
.../datamodels/datamodelWorkflowAutomation.py | 2 +-
modules/demoConfigs/investorDemo2026.py | 27 +--
modules/demoConfigs/pwgDemo2026.py | 119 ++---------
.../neutralization/neutralizePlayground.py | 92 +++++----
.../mainServiceNeutralization.py | 12 +-
.../features/realEstate/serviceAiIntent.py | 7 +-
modules/features/realEstate/serviceBzo.py | 15 +-
.../interfaces/_legacyMigrationTelemetry.py | 4 +-
modules/interfaces/interfaceBootstrap.py | 13 +-
modules/interfaces/interfaceDbApp.py | 13 +-
modules/interfaces/interfaceDbManagement.py | 8 +-
modules/interfaces/interfaceFeatures.py | 7 +-
.../interfaces/interfaceWorkflowAutomation.py | 10 +-
.../editor => nodeCatalog}/entryPoints.py | 70 ++++---
modules/routes/routeBilling.py | 4 +-
modules/routes/routeClickup.py | 10 +-
modules/routes/routeSharepoint.py | 16 +-
modules/routes/routeSystem.py | 4 +-
modules/routes/routeWorkflowAutomation.py | 37 ++--
modules/serviceCenter/__init__.py | 47 +++--
modules/serviceCenter/resolver.py | 7 +-
modules/serviceCenter/serviceHub.py | 189 ------------------
.../serviceAgent/actionToolAdapter.py | 4 +-
.../coreTools/_connectionTools.py | 5 +-
.../coreTools/_crossWorkflowTools.py | 8 +-
.../coreTools/_dataSourceTools.py | 4 +-
.../coreTools/_featureSubAgentTools.py | 5 -
.../serviceAgent/coreTools/_helpers.py | 13 +-
.../serviceAgent/coreTools/_mediaTools.py | 42 ++--
.../serviceAgent/coreTools/_workspaceTools.py | 50 ++---
.../services/serviceAgent/mainServiceAgent.py | 29 +--
.../serviceAi/subContentExtraction.py | 4 +-
.../services/serviceAi/subDocumentIntents.py | 2 +-
.../services/serviceBilling/stripeCheckout.py | 2 +-
.../services/serviceChat/mainServiceChat.py | 145 ++++++++++++++
.../services/serviceClickup/__init__.py | 4 +-
.../serviceClickup/mainServiceClickup.py | 2 +-
.../mainServiceGeneration.py | 24 +--
modules/shared/systemComponentRegistry.py | 32 +++
.../workflowArtifactVisibility.py | 4 +-
modules/shared/workflowState.py | 2 +-
.../engine/executionEngine.py | 74 +++----
.../engine/executors/actionNodeExecutor.py | 40 +---
.../engine/executors/inputExecutor.py | 4 +-
.../engine/runFileLogger.py | 134 ++++++-------
modules/workflowAutomation/helpers.py | 4 +-
.../mainWorkflowAutomation.py | 64 +-----
.../scheduler/mainScheduler.py | 4 +-
.../methods/methodAi/actions/process.py | 21 +-
.../methods/methodAi/actions/webResearch.py | 21 +-
modules/workflows/methods/methodBase.py | 6 +-
.../methodContext/actions/extractContent.py | 28 +--
.../methods/methodFile/actions/create.py | 45 +----
.../actions/downloadFileByPath.py | 18 +-
.../processing/modes/modeAutomation.py | 4 +-
.../workflows/processing/modes/modeBase.py | 7 +-
.../workflows/processing/workflowProcessor.py | 16 +-
modules/workflows/workflowManager.py | 18 +-
tests/eval/runTrusteeBenchmark.py | 27 ++-
tests/functional/test01_ai_model_selection.py | 23 ++-
tests/functional/test02_ai_models.py | 27 ++-
tests/functional/test03_ai_operations.py | 28 ++-
tests/functional/test04_ai_behavior.py | 28 ++-
.../workflow/test_extract_content_handover.py | 14 +-
66 files changed, 814 insertions(+), 948 deletions(-)
rename modules/{workflowAutomation/editor => nodeCatalog}/entryPoints.py (63%)
delete mode 100644 modules/serviceCenter/serviceHub.py
create mode 100644 modules/shared/systemComponentRegistry.py
rename modules/{workflowAutomation/engine => shared}/workflowArtifactVisibility.py (90%)
diff --git a/app.py b/app.py
index 185ea95d..f76f7083 100644
--- a/app.py
+++ b/app.py
@@ -311,6 +311,17 @@ async def lifespan(app: FastAPI):
# AI connectors already pre-warmed at module-load via _eager_prewarm() in aicoreModelRegistry.
+ # Register system-component lifecycle hooks (Composition Root — inverts L4->L5b dependency)
+ from modules.shared.systemComponentRegistry import registerLifecycleHook
+ from modules.workflowAutomation.mainWorkflowAutomation import (
+ onBootstrap as _waOnBootstrap,
+ onMandateDelete as _waOnMandateDelete,
+ onInstanceCreate as _waOnInstanceCreate,
+ )
+ registerLifecycleHook("onBootstrap", _waOnBootstrap)
+ registerLifecycleHook("onMandateDelete", _waOnMandateDelete)
+ registerLifecycleHook("onInstanceCreate", _waOnInstanceCreate)
+
# Bootstrap database if needed (creates initial users, mandates, roles, etc.)
# This must happen before getting root interface
from modules.security.rootAccess import getRootDbAppConnector
diff --git a/modules/datamodels/datamodelViews.py b/modules/datamodels/datamodelViews.py
index 03a5a27f..28625d16 100644
--- a/modules/datamodels/datamodelViews.py
+++ b/modules/datamodels/datamodelViews.py
@@ -247,7 +247,7 @@ from modules.datamodels.datamodelFeatures import AutoWorkflow
@i18nModel("Workflow (Ansicht)")
-class Automation2WorkflowView(AutoWorkflow):
+class AutoWorkflowView(AutoWorkflow):
"""AutoWorkflow extended with computed dashboard fields.
Used exclusively for /api/attributes/ so the frontend can resolve column
diff --git a/modules/datamodels/datamodelWorkflowAutomation.py b/modules/datamodels/datamodelWorkflowAutomation.py
index c9957c25..51d84814 100644
--- a/modules/datamodels/datamodelWorkflowAutomation.py
+++ b/modules/datamodels/datamodelWorkflowAutomation.py
@@ -53,7 +53,7 @@ class AutoTemplateScope(str, Enum):
SYSTEM = "system"
-GRAPHICAL_EDITOR_DATABASE = "poweron_graphicaleditor"
+WORKFLOW_AUTOMATION_DATABASE = "poweron_graphicaleditor"
# ---------------------------------------------------------------------------
diff --git a/modules/demoConfigs/investorDemo2026.py b/modules/demoConfigs/investorDemo2026.py
index 62b523d1..84fc5e01 100644
--- a/modules/demoConfigs/investorDemo2026.py
+++ b/modules/demoConfigs/investorDemo2026.py
@@ -60,7 +60,7 @@ class InvestorDemo2026(BaseDemoConfig):
label = "Investor Demo April 2026"
description = (
"Two mandates (HappyLife AG + Alpina Treuhand AG), one SysAdmin user, "
- "trustee with RMA, workspace, graph editor, and neutralization."
+ "trustee with RMA, workspace, workflow automation, and neutralization."
)
credentials = [
{
@@ -554,20 +554,21 @@ class InvestorDemo2026(BaseDemoConfig):
try:
from modules.datamodels.datamodelWorkflowAutomation import (
AutoWorkflow, AutoVersion, AutoRun, AutoStepLog, AutoTask,
+ WORKFLOW_AUTOMATION_DATABASE,
)
from modules.connectors.connectorDbPostgre import DatabaseConnector
from modules.shared.configuration import APP_CONFIG
- geDb = DatabaseConnector(
+ waDb = DatabaseConnector(
dbHost=APP_CONFIG.get("DB_HOST", "localhost"),
- dbDatabase="poweron_graphicaleditor",
+ dbDatabase=WORKFLOW_AUTOMATION_DATABASE,
dbUser=APP_CONFIG.get("DB_USER"),
dbPassword=APP_CONFIG.get("DB_PASSWORD_SECRET") or APP_CONFIG.get("DB_PASSWORD"),
dbPort=int(APP_CONFIG.get("DB_PORT", 5432)),
userId=None,
)
- workflows = geDb.getRecordset(AutoWorkflow, recordFilter={
+ workflows = waDb.getRecordset(AutoWorkflow, recordFilter={
"mandateId": mandateId,
"featureInstanceId": featureInstanceId,
}) or []
@@ -577,20 +578,20 @@ class InvestorDemo2026(BaseDemoConfig):
if not wfId:
continue
- for version in geDb.getRecordset(AutoVersion, recordFilter={"workflowId": wfId}) or []:
- geDb.recordDelete(AutoVersion, version.get("id"))
+ for version in waDb.getRecordset(AutoVersion, recordFilter={"workflowId": wfId}) or []:
+ waDb.recordDelete(AutoVersion, version.get("id"))
- runs = geDb.getRecordset(AutoRun, recordFilter={"workflowId": wfId}) or []
+ runs = waDb.getRecordset(AutoRun, recordFilter={"workflowId": wfId}) or []
for run in runs:
runId = run.get("id")
- for stepLog in geDb.getRecordset(AutoStepLog, recordFilter={"runId": runId}) or []:
- geDb.recordDelete(AutoStepLog, stepLog.get("id"))
- geDb.recordDelete(AutoRun, runId)
+ for stepLog in waDb.getRecordset(AutoStepLog, recordFilter={"runId": runId}) or []:
+ waDb.recordDelete(AutoStepLog, stepLog.get("id"))
+ waDb.recordDelete(AutoRun, runId)
- for task in geDb.getRecordset(AutoTask, recordFilter={"workflowId": wfId}) or []:
- geDb.recordDelete(AutoTask, task.get("id"))
+ for task in waDb.getRecordset(AutoTask, recordFilter={"workflowId": wfId}) or []:
+ waDb.recordDelete(AutoTask, task.get("id"))
- geDb.recordDelete(AutoWorkflow, wfId)
+ waDb.recordDelete(AutoWorkflow, wfId)
if workflows:
summary["removed"].append(f"{len(workflows)} AutoWorkflows in {mandateLabel}")
diff --git a/modules/demoConfigs/pwgDemo2026.py b/modules/demoConfigs/pwgDemo2026.py
index efcd8c8a..2d301f7a 100644
--- a/modules/demoConfigs/pwgDemo2026.py
+++ b/modules/demoConfigs/pwgDemo2026.py
@@ -51,9 +51,6 @@ _FEATURES_PWG = [
{"code": "neutralization", "label": "Datenschutz"},
]
-# Filename markers used to identify the imported pilot workflow on remove().
-_PILOT_WORKFLOW_LABEL = "PWG Pilot: Jahresmietzinsbestätigung"
-_PILOT_WORKFLOW_FILE = "pwg-mietzinsbestaetigung-pilot.workflow.json"
_SEED_TRUSTEE_FILE = "_seedTrusteeData.json"
@@ -62,8 +59,7 @@ class PwgDemo2026(BaseDemoConfig):
label = "PWG Pilot Demo (Mietzinsbestätigungen)"
description = (
"Stiftung PWG, ein Demo-Sachbearbeiter, Trustee mit fiktiven Mietern, "
- "Graph-Editor mit dem Pilot-Workflow für Jahresmietzinsbestätigungen "
- "(als File importiert, active=false). Idempotent."
+ "Workflow-Automation (als File importiert, active=false). Idempotent."
)
credentials = [
{
@@ -536,92 +532,6 @@ class PwgDemo2026(BaseDemoConfig):
if skippedTenants:
summary["skipped"].append(f"PWG seed: {skippedTenants} tenants already present")
- def _ensurePilotWorkflow(self, mandateId: str, featureInstanceId: str, summary: Dict):
- """Import the pilot workflow JSON into the WorkflowAutomation DB.
-
- Uses the schema-aware import pipeline introduced in Phase 1
- (``_workflowFileSchema.envelopeToWorkflowData`` +
- ``WorkflowAutomationObjects.importWorkflowFromDict``). The workflow is
- always created with ``active=False`` so a manual trigger is required
- — this matches the demo-bootstrap safety default.
- """
- envelopePath = _demoDataDir() / "workflows" / _PILOT_WORKFLOW_FILE
- if not envelopePath.is_file():
- summary["errors"].append(f"Pilot workflow file missing: {envelopePath}")
- return
- try:
- envelope = json.loads(envelopePath.read_text(encoding="utf-8"))
- except Exception as exc:
- summary["errors"].append(f"Pilot workflow file unreadable: {exc}")
- return
-
- try:
- geDb = _openWorkflowAutomationDb()
- except Exception as exc:
- summary["errors"].append(f"WorkflowAutomation DB connection failed: {exc}")
- return
-
- from modules.nodeCatalog._workflowFileSchema import (
- envelopeToWorkflowData,
- validateFileEnvelope,
- )
- from modules.datamodels.datamodelWorkflowAutomation import AutoWorkflow
- from modules.workflowAutomation.editor.nodeRegistry import STATIC_NODE_TYPES
-
- existing = geDb.getRecordset(AutoWorkflow, recordFilter={
- "mandateId": mandateId,
- "featureInstanceId": featureInstanceId,
- "label": _PILOT_WORKFLOW_LABEL,
- }) or []
- if existing:
- summary["skipped"].append(f"Pilot workflow already imported ({existing[0].get('id')})")
- return
-
- knownTypes = [n.get("id") for n in STATIC_NODE_TYPES if isinstance(n, dict) and n.get("id")]
- try:
- normalized, warnings = validateFileEnvelope(envelope, knownNodeTypes=knownTypes)
- except Exception as exc:
- summary["errors"].append(f"Pilot workflow envelope invalid: {exc}")
- return
- if warnings:
- summary["created"].append(f"Pilot workflow warnings: {warnings}")
-
- data = envelopeToWorkflowData(
- normalized,
- mandateId=mandateId,
- featureInstanceId=featureInstanceId,
- )
- # Inject the trustee feature-instance id into the parameters so the
- # node runtime resolves it without manual editor cleanup.
- trusteeInstanceId = self._guessTrusteeInstanceId(mandateId)
- if trusteeInstanceId:
- for node in data.get("graph", {}).get("nodes", []) or []:
- params = node.get("parameters") or {}
- if "featureInstanceId" in params and not params["featureInstanceId"]:
- params["featureInstanceId"] = trusteeInstanceId
- node["parameters"] = params
-
- # Force-import: AutoWorkflow.create accepts our envelope-derived data
- # (graph, label, invocations, …) verbatim; we add ids/timestamps that
- # AutoWorkflow expects.
- record = AutoWorkflow(
- id=str(uuid.uuid4()),
- mandateId=mandateId,
- featureInstanceId=featureInstanceId,
- label=data.get("label") or _PILOT_WORKFLOW_LABEL,
- description=data.get("description") or "",
- tags=data.get("tags") or [],
- graph=data.get("graph") or {"nodes": [], "connections": []},
- invocations=data.get("invocations") or [],
- templateScope=data.get("templateScope") or "instance",
- sharedReadOnly=bool(data.get("sharedReadOnly")),
- notifyOnFailure=bool(data.get("notifyOnFailure", True)),
- active=False,
- )
- created = geDb.recordCreate(AutoWorkflow, record)
- summary["created"].append(f"Pilot workflow imported (active=false, id={created.get('id')})")
- logger.info(f"Imported pilot workflow into workflowAutomation instance {featureInstanceId}")
-
def _guessTrusteeInstanceId(self, mandateId: str) -> Optional[str]:
"""Return the first trustee feature-instance id of the given mandate.
@@ -728,23 +638,23 @@ class PwgDemo2026(BaseDemoConfig):
AutoVersion,
AutoWorkflow,
)
- geDb = _openWorkflowAutomationDb()
- workflows = geDb.getRecordset(AutoWorkflow, recordFilter={
+ waDb = _openWorkflowAutomationDb()
+ workflows = waDb.getRecordset(AutoWorkflow, recordFilter={
"mandateId": mandateId,
"featureInstanceId": featureInstanceId,
}) or []
for wf in workflows:
wfId = wf.get("id")
- for version in geDb.getRecordset(AutoVersion, recordFilter={"workflowId": wfId}) or []:
- geDb.recordDelete(AutoVersion, version.get("id"))
- for run in geDb.getRecordset(AutoRun, recordFilter={"workflowId": wfId}) or []:
+ for version in waDb.getRecordset(AutoVersion, recordFilter={"workflowId": wfId}) or []:
+ waDb.recordDelete(AutoVersion, version.get("id"))
+ for run in waDb.getRecordset(AutoRun, recordFilter={"workflowId": wfId}) or []:
runId = run.get("id")
- for step in geDb.getRecordset(AutoStepLog, recordFilter={"runId": runId}) or []:
- geDb.recordDelete(AutoStepLog, step.get("id"))
- geDb.recordDelete(AutoRun, runId)
- for task in geDb.getRecordset(AutoTask, recordFilter={"workflowId": wfId}) or []:
- geDb.recordDelete(AutoTask, task.get("id"))
- geDb.recordDelete(AutoWorkflow, wfId)
+ for step in waDb.getRecordset(AutoStepLog, recordFilter={"runId": runId}) or []:
+ waDb.recordDelete(AutoStepLog, step.get("id"))
+ waDb.recordDelete(AutoRun, runId)
+ for task in waDb.getRecordset(AutoTask, recordFilter={"workflowId": wfId}) or []:
+ waDb.recordDelete(AutoTask, task.get("id"))
+ waDb.recordDelete(AutoWorkflow, wfId)
if workflows:
summary["removed"].append(f"{len(workflows)} AutoWorkflows in {mandateLabel}")
except Exception as e:
@@ -814,12 +724,13 @@ def _openTrusteeDb():
def _openWorkflowAutomationDb():
- """Open a privileged DB connection to ``poweron_graphicaleditor``."""
+ """Open a privileged DB connection to the workflow-automation database."""
from modules.connectors.connectorDbPostgre import DatabaseConnector
+ from modules.datamodels.datamodelWorkflowAutomation import WORKFLOW_AUTOMATION_DATABASE
from modules.shared.configuration import APP_CONFIG
return DatabaseConnector(
dbHost=APP_CONFIG.get("DB_HOST", "localhost"),
- dbDatabase="poweron_graphicaleditor",
+ dbDatabase=WORKFLOW_AUTOMATION_DATABASE,
dbUser=APP_CONFIG.get("DB_USER"),
dbPassword=APP_CONFIG.get("DB_PASSWORD_SECRET") or APP_CONFIG.get("DB_PASSWORD"),
dbPort=int(APP_CONFIG.get("DB_PORT", 5432)),
diff --git a/modules/features/neutralization/neutralizePlayground.py b/modules/features/neutralization/neutralizePlayground.py
index 0bd50b49..e855ad22 100644
--- a/modules/features/neutralization/neutralizePlayground.py
+++ b/modules/features/neutralization/neutralizePlayground.py
@@ -9,7 +9,8 @@ from urllib.parse import urlparse, unquote
from modules.datamodels.datamodelUam import User
from .datamodelFeatureNeutralizer import DataNeutralizerAttributes, DataNeutraliserConfig, DataNeutralizationSnapshot
from .interfaceFeatureNeutralizer import getInterface as _getNeutralizerInterface
-from modules.serviceCenter.serviceHub import getInterface as getServices
+from modules.serviceCenter import getService
+from modules.serviceCenter.context import ServiceCenterContext
logger = logging.getLogger(__name__)
@@ -21,10 +22,13 @@ class NeutralizationPlayground:
self.currentUser = currentUser
self.mandateId = mandateId
self.featureInstanceId = featureInstanceId
- self.services = getServices(currentUser, None, mandateId=mandateId, featureInstanceId=featureInstanceId)
+ self._ctx = ServiceCenterContext(user=currentUser, mandate_id=mandateId, feature_instance_id=featureInstanceId)
+
+ def _getService(self, name: str):
+ return getService(name, self._ctx)
def processText(self, text: str) -> Dict[str, Any]:
- return self.services.neutralization.processText(text)
+ return self._getService("neutralization").processText(text)
async def processUploadedFileAsync(self, file_bytes: bytes, filename: str) -> Dict[str, Any]:
"""Process an uploaded file (bytes + filename). Returns neutralized result for text or binary.
@@ -43,32 +47,35 @@ class NeutralizationPlayground:
original_file_id = None
neutralized_file_id = None
+ neutralizationService = self._getService("neutralization")
- # Save original file to user files
- if self.services.interfaceDbComponent:
+ try:
+ chatService = self._getService("chat")
+ except Exception:
+ chatService = None
+
+ if chatService:
try:
- file_item, _ = self.services.interfaceDbComponent.saveUploadedFile(file_bytes, filename)
+ file_item, _ = chatService.saveUploadedFile(file_bytes, filename)
original_file_id = str(file_item.id)
except Exception as e:
logger.warning(f"Could not save original file to user files: {e}")
if is_binary:
- result = await self.services.neutralization.processBinaryBytesAsync(file_bytes, filename, mime)
+ result = await neutralizationService.processBinaryBytesAsync(file_bytes, filename, mime)
neu_bytes = result.get('neutralized_bytes')
logger.debug(f"Binary result: neu_bytes type={type(neu_bytes).__name__}, len={len(neu_bytes) if neu_bytes is not None else 0}")
if neu_bytes is not None and len(neu_bytes) > 0:
result['neutralized_file_base64'] = base64.b64encode(neu_bytes).decode('ascii')
result['neutralized_file_name'] = result.get('neutralized_file_name', f'neutralized_{filename}')
result['mime_type'] = result.get('mime_type', mime)
- # Save neutralized binary to user files
- if self.services.interfaceDbComponent:
+ if chatService:
try:
neu_name = result['neutralized_file_name']
- file_item, _ = self.services.interfaceDbComponent.saveUploadedFile(neu_bytes, neu_name)
+ file_item, _ = chatService.saveUploadedFile(neu_bytes, neu_name)
neutralized_file_id = str(file_item.id)
except Exception as e:
logger.warning(f"Could not save neutralized file to user files: {e}")
- # Remove raw bytes before JSON response (avoid serialization issues; use base64 only)
result.pop('neutralized_bytes', None)
result['original_file_id'] = original_file_id
result['neutralized_file_id'] = neutralized_file_id
@@ -86,15 +93,14 @@ class NeutralizationPlayground:
'neutralized_file_id': None,
'processed_info': {'type': 'error', 'error': 'File could not be decoded as text. Supported: UTF-8, Latin-1. For PDF/Word/Excel, use supported binary formats.'}
}
- result = await self.services.neutralization.processTextAsync(text_content)
+ result = await neutralizationService.processTextAsync(text_content)
result['neutralized_file_name'] = f'neutralized_{filename}'
- # Save neutralized text as file to user files
- if self.services.interfaceDbComponent and result.get('neutralized_text') is not None:
+ if chatService and result.get('neutralized_text') is not None:
try:
neu_text = result['neutralized_text']
neu_bytes = neu_text.encode('utf-8')
neu_name = result['neutralized_file_name']
- file_item, _ = self.services.interfaceDbComponent.saveUploadedFile(neu_bytes, neu_name)
+ file_item, _ = chatService.saveUploadedFile(neu_bytes, neu_name)
neutralized_file_id = str(file_item.id)
except Exception as e:
logger.warning(f"Could not save neutralized text file to user files: {e}")
@@ -111,7 +117,7 @@ class NeutralizationPlayground:
errors: List[str] = []
for fileId in fileIds:
try:
- res = self.services.neutralization.processFile(fileId)
+ res = self._getService("neutralization").processFile(fileId)
results.append({
'file_id': fileId,
'neutralized_file_name': res.get('neutralized_file_name'),
@@ -137,12 +143,12 @@ class NeutralizationPlayground:
# Cleanup attributes
def cleanAttributes(self, fileId: str) -> bool:
- return self.services.neutralization.deleteNeutralizationAttributes(fileId)
+ return self._getService("neutralization").deleteNeutralizationAttributes(fileId)
# Stats
def getStats(self) -> Dict[str, Any]:
try:
- allAttributes = self.services.neutralization.getAttributes()
+ allAttributes = self._getService("neutralization").getAttributes()
patternCounts: Dict[str, int] = {}
for attr in allAttributes:
# Handle both dict and object access patterns
@@ -184,24 +190,24 @@ class NeutralizationPlayground:
# Additional methods needed by the route
def getConfig(self) -> Optional[DataNeutraliserConfig]:
"""Get neutralization configuration"""
- return self.services.neutralization.getConfig()
+ return self._getService("neutralization").getConfig()
def saveConfig(self, configData: Dict[str, Any]) -> DataNeutraliserConfig:
"""Save neutralization configuration"""
- return self.services.neutralization.saveConfig(configData)
+ return self._getService("neutralization").saveConfig(configData)
def neutralizeText(self, text: str, fileId: str = None) -> Dict[str, Any]:
"""Neutralize text content"""
- return self.services.neutralization.processText(text)
+ return self._getService("neutralization").processText(text)
def resolveText(self, text: str) -> str:
"""Resolve UIDs in neutralized text back to original text"""
- return self.services.neutralization.resolveText(text)
+ return self._getService("neutralization").resolveText(text)
def getSnapshots(self) -> List[DataNeutralizationSnapshot]:
"""Return stored neutralization text snapshots."""
try:
- return self.services.neutralization.getSnapshots()
+ return self._getService("neutralization").getSnapshots()
except Exception as e:
logger.error(f"Error getting snapshots: {e}")
return []
@@ -209,7 +215,7 @@ class NeutralizationPlayground:
def getAttributes(self, fileId: str = None) -> List[DataNeutralizerAttributes]:
"""Get neutralization attributes, optionally filtered by file ID"""
try:
- allAttributes = self.services.neutralization.getAttributes()
+ allAttributes = self._getService("neutralization").getAttributes()
if fileId:
want = str(fileId).strip()
@@ -227,8 +233,7 @@ class NeutralizationPlayground:
async def processSharepointFiles(self, sourcePath: str, targetPath: str) -> Dict[str, Any]:
"""Process files from SharePoint source path and store neutralized files in target path"""
- from modules.serviceCenter.services.serviceSharepoint.mainServiceSharepoint import SharepointService
- processor = SharepointProcessor(self.currentUser, self.services)
+ processor = SharepointProcessor(self.currentUser, self._ctx)
return await processor.processSharepointFiles(sourcePath, targetPath)
def batchNeutralizeFiles(self, filesData: List[Dict[str, Any]]) -> Dict[str, Any]:
@@ -247,15 +252,18 @@ class NeutralizationPlayground:
# Internal SharePoint helper module separated to keep feature logic tidy
class SharepointProcessor:
- def __init__(self, currentUser: User, services):
+ def __init__(self, currentUser: User, ctx: ServiceCenterContext):
self.currentUser = currentUser
- self.services = services
+ self._ctx = ctx
+ self._sharepoint = getService("sharepoint", ctx)
+ self._neutralization = getService("neutralization", ctx)
+ from modules.interfaces.interfaceDbApp import getInterface as _getAppInterface
+ self._interfaceDbApp = _getAppInterface(currentUser, mandateId=ctx.mandate_id)
async def processSharepointFiles(self, sourcePath: str, targetPath: str) -> Dict[str, Any]:
try:
logger.info(f"Processing SharePoint files from {sourcePath} to {targetPath}")
- # Get SharePoint connection
connection = await self._getSharepointConnection(sourcePath)
if not connection:
return {
@@ -265,8 +273,7 @@ class SharepointProcessor:
'errors': ['No SharePoint connection found'],
}
- # Set access token for SharePoint service
- if not self.services.sharepoint.setAccessTokenFromConnection(connection):
+ if not self._sharepoint.setAccessTokenFromConnection(connection):
return {
'success': False,
'message': 'Failed to set SharePoint access token',
@@ -286,8 +293,7 @@ class SharepointProcessor:
async def _getSharepointConnection(self, sharepointPath: str = None):
try:
- # Use interface method to get user connections
- connections = self.services.interfaceDbApp.getUserConnections(self.services.interfaceDbApp.userId)
+ connections = self._interfaceDbApp.getUserConnections(self._interfaceDbApp.userId)
def _is_msft_connection(c):
av = c.authority.value if hasattr(c.authority, 'value') else str(getattr(c, 'authority', ''))
return av and str(av).lower() == 'msft'
@@ -322,7 +328,7 @@ class SharepointProcessor:
for connection in connections:
try:
- if not self.services.sharepoint.setAccessTokenFromConnection(connection):
+ if not self._sharepoint.setAccessTokenFromConnection(connection):
continue
if await self._testSharepointAccess(sharepointPath):
logger.info(f"Found matching connection for domain {targetDomain}: {connection.get('id')}")
@@ -340,7 +346,7 @@ class SharepointProcessor:
siteUrl, _ = self._parseSharepointPath(sharepointPath)
if not siteUrl:
return False
- siteInfo = await self.services.sharepoint.findSiteByWebUrl(siteUrl)
+ siteInfo = await self._sharepoint.findSiteByWebUrl(siteUrl)
return siteInfo is not None
except Exception:
return False
@@ -351,17 +357,17 @@ class SharepointProcessor:
targetSite, targetFolder = self._parseSharepointPath(targetPath)
if not sourceSite or not targetSite:
return {'success': False, 'message': 'Invalid SharePoint path format', 'processed_files': 0, 'errors': ['Invalid SharePoint path format']}
- sourceSiteInfo = await self.services.sharepoint.findSiteByWebUrl(sourceSite)
+ sourceSiteInfo = await self._sharepoint.findSiteByWebUrl(sourceSite)
if not sourceSiteInfo:
return {'success': False, 'message': f'Source site not found: {sourceSite}', 'processed_files': 0, 'errors': [f'Source site not found: {sourceSite}']}
- targetSiteInfo = await self.services.sharepoint.findSiteByWebUrl(targetSite)
+ targetSiteInfo = await self._sharepoint.findSiteByWebUrl(targetSite)
if not targetSiteInfo:
return {'success': False, 'message': f'Target site not found: {targetSite}', 'processed_files': 0, 'errors': [f'Target site not found: {targetSite}']}
logger.info(f"Listing files in folder: {sourceFolder} for site: {sourceSiteInfo['id']}")
- files = await self.services.sharepoint.listFolderContents(sourceSiteInfo['id'], sourceFolder)
+ files = await self._sharepoint.listFolderContents(sourceSiteInfo['id'], sourceFolder)
if not files:
logger.warning(f"No files found in folder '{sourceFolder}', trying root folder")
- files = await self.services.sharepoint.listFolderContents(sourceSiteInfo['id'], '')
+ files = await self._sharepoint.listFolderContents(sourceSiteInfo['id'], '')
if files:
folders = [f for f in files if f.get('type') == 'folder']
folderNames = [f.get('name') for f in folders]
@@ -385,7 +391,7 @@ class SharepointProcessor:
async def _processSingle(fileInfo: Dict[str, Any]):
try:
- fileContent = await self.services.sharepoint.downloadFile(sourceSiteInfo['id'], fileInfo['id'])
+ fileContent = await self._sharepoint.downloadFile(sourceSiteInfo['id'], fileInfo['id'])
if not fileContent:
return {'error': f"Failed to download file: {fileInfo['name']}"}
name_lower = (fileInfo.get('name') or '').lower()
@@ -402,7 +408,7 @@ class SharepointProcessor:
mime = next((mime_map[ext] for ext in BINARY_EXTS if name_lower.endswith(ext)), 'text/plain')
if is_binary:
- result = self.services.neutralization.processBinaryBytes(fileContent, fileInfo['name'], mime)
+ result = self._neutralization.processBinaryBytes(fileContent, fileInfo['name'], mime)
if result.get('neutralized_bytes'):
content_to_upload = result['neutralized_bytes']
else:
@@ -412,11 +418,11 @@ class SharepointProcessor:
textContent = fileContent.decode('utf-8')
except UnicodeDecodeError:
textContent = fileContent.decode('latin-1')
- result = await self.services.neutralization.processTextAsync(textContent)
+ result = await self._neutralization.processTextAsync(textContent)
content_to_upload = (result.get('neutralized_text') or '').encode('utf-8')
neutralizedFilename = f"neutralized_{fileInfo['name']}"
- uploadResult = await self.services.sharepoint.uploadFile(targetSiteInfo['id'], targetFolder, neutralizedFilename, content_to_upload)
+ uploadResult = await self._sharepoint.uploadFile(targetSiteInfo['id'], targetFolder, neutralizedFilename, content_to_upload)
if 'error' in uploadResult:
return {'error': f"Failed to upload neutralized file: {neutralizedFilename} - {uploadResult['error']}"}
return {
diff --git a/modules/features/neutralization/serviceNeutralization/mainServiceNeutralization.py b/modules/features/neutralization/serviceNeutralization/mainServiceNeutralization.py
index 809d6be5..4cfec864 100644
--- a/modules/features/neutralization/serviceNeutralization/mainServiceNeutralization.py
+++ b/modules/features/neutralization/serviceNeutralization/mainServiceNeutralization.py
@@ -51,7 +51,6 @@ class NeutralizationService:
"""
self.services = serviceCenter
self._getService = getServiceFn
- self.interfaceDbComponent = getattr(serviceCenter, "interfaceDbComponent", None)
# Create feature-specific interface for neutralizer DB operations
self.interfaceNeutralizer: InterfaceFeatureNeutralizer = None
@@ -305,19 +304,20 @@ class NeutralizationService:
raise
def processFile(self, fileId: str) -> Dict[str, Any]:
- """Neutralize a file referenced by its fileId using component interface.
+ """Neutralize a file referenced by its fileId using ChatService.
Supports text files directly; PDF/DOCX/XLSX/PPTX via extract -> neutralize -> generate."""
- if not self.interfaceDbComponent:
- raise ValueError("Component interface is required to process a file by fileId")
+ chatService = self._getService("chat") if self._getService else None
+ if not chatService:
+ raise ValueError("Chat service is required to process a file by fileId")
fileInfo = None
try:
- fileInfo = self.interfaceDbComponent.getFile(fileId)
+ fileInfo = chatService.getFile(fileId)
except Exception:
fileInfo = None
fileName = getattr(fileInfo, 'fileName', None) if fileInfo else None
mimeType = getattr(fileInfo, 'mimeType', None) if fileInfo else None
- fileData = self.interfaceDbComponent.getFileData(fileId)
+ fileData = chatService.getFileData(fileId)
if not fileData:
raise ValueError(f"No file data found for fileId: {fileId}")
diff --git a/modules/features/realEstate/serviceAiIntent.py b/modules/features/realEstate/serviceAiIntent.py
index 62efb1a0..ca53c98e 100644
--- a/modules/features/realEstate/serviceAiIntent.py
+++ b/modules/features/realEstate/serviceAiIntent.py
@@ -24,7 +24,8 @@ from .datamodelFeatureRealEstate import (
Kanton,
Land,
)
-from modules.serviceCenter.serviceHub import getInterface as getServices
+from modules.serviceCenter import getService
+from modules.serviceCenter.context import ServiceCenterContext
from .interfaceFeatureRealEstate import getInterface as getRealEstateInterface
from .serviceGeometry import fetch_parcel_polygon_from_swisstopo
@@ -231,8 +232,8 @@ async def processNaturalLanguageCommand(
logger.info(f"Processing natural language command for user {currentUser.id} (mandate: {mandateId})")
logger.debug(f"User input: {userInput}")
- services = getServices(currentUser, workflow=None, mandateId=mandateId)
- aiService = services.ai
+ ctx = ServiceCenterContext(user=currentUser, mandate_id=mandateId)
+ aiService = getService("ai", ctx)
intentAnalysis = await analyzeUserIntent(aiService, userInput)
diff --git a/modules/features/realEstate/serviceBzo.py b/modules/features/realEstate/serviceBzo.py
index c7510fb3..178c8021 100644
--- a/modules/features/realEstate/serviceBzo.py
+++ b/modules/features/realEstate/serviceBzo.py
@@ -12,7 +12,8 @@ from fastapi import HTTPException, status
from modules.datamodels.datamodelUam import User
from .datamodelFeatureRealEstate import DokumentTyp
-from modules.serviceCenter.serviceHub import getInterface as getServices
+from modules.serviceCenter import getService
+from modules.serviceCenter.context import ServiceCenterContext
from .interfaceFeatureRealEstate import getInterface as getRealEstateInterface
from modules.interfaces.interfaceDbManagement import getInterface as getComponentInterface
from modules.features.realEstate.bzoDocumentRetriever import BZODocumentRetriever
@@ -233,10 +234,8 @@ async def extract_bzo_information(
bzo_params_result = None
try:
- services = getServices(
- currentUser, workflow=None, mandateId=_mandateId, featureInstanceId=featureInstanceId
- )
- ai_service = services.ai
+ ctx = ServiceCenterContext(user=currentUser, mandate_id=_mandateId, feature_instance_id=featureInstanceId)
+ ai_service = getService("ai", ctx)
bzo_params_result = await run_bzo_params_extraction(
extracted_content=all_extracted_content,
bauzone=bauzone,
@@ -521,10 +520,8 @@ async def generate_bauzone_ai_summary(
AI-generated summary string
"""
try:
- services = getServices(
- currentUser, workflow=None, mandateId=mandateId, featureInstanceId=featureInstanceId
- )
- aiService = services.ai
+ ctx = ServiceCenterContext(user=currentUser, mandate_id=mandateId, feature_instance_id=featureInstanceId)
+ aiService = getService("ai", ctx)
context_parts = []
diff --git a/modules/interfaces/_legacyMigrationTelemetry.py b/modules/interfaces/_legacyMigrationTelemetry.py
index 02c2c184..d80905b1 100644
--- a/modules/interfaces/_legacyMigrationTelemetry.py
+++ b/modules/interfaces/_legacyMigrationTelemetry.py
@@ -158,7 +158,7 @@ def _backfillTargetFeatureInstanceId() -> None:
"""
def _do() -> None:
from modules.shared.configuration import APP_CONFIG
- from modules.datamodels.datamodelWorkflowAutomation import AutoWorkflow
+ from modules.datamodels.datamodelWorkflowAutomation import AutoWorkflow, WORKFLOW_AUTOMATION_DATABASE
dbHost = APP_CONFIG.get("DB_HOST", "localhost")
dbUser = APP_CONFIG.get("DB_USER")
@@ -166,7 +166,7 @@ def _backfillTargetFeatureInstanceId() -> None:
dbPort = int(APP_CONFIG.get("DB_PORT", 5432))
geDb = DatabaseConnector(
dbHost=dbHost,
- dbDatabase="poweron_graphicaleditor",
+ dbDatabase=WORKFLOW_AUTOMATION_DATABASE,
dbUser=dbUser,
dbPassword=dbPassword,
dbPort=dbPort,
diff --git a/modules/interfaces/interfaceBootstrap.py b/modules/interfaces/interfaceBootstrap.py
index 002cb02d..4764cd4a 100644
--- a/modules/interfaces/interfaceBootstrap.py
+++ b/modules/interfaces/interfaceBootstrap.py
@@ -110,12 +110,13 @@ def initBootstrap(db: DatabaseConnector) -> None:
except Exception as e:
logger.warning(f"Mandate retention purge failed: {e}")
- # WorkflowAutomation bootstrap (system component, not auto-discovered)
- try:
- from modules.workflowAutomation.mainWorkflowAutomation import onBootstrap as _waBootstrap
- _waBootstrap()
- except Exception as _waBootErr:
- logger.warning(f"onBootstrap hook for 'workflowAutomation' failed: {_waBootErr}")
+ # System-component lifecycle hooks (registered via app.py Composition Root)
+ from modules.shared.systemComponentRegistry import getLifecycleHooks
+ for _scHook in getLifecycleHooks("onBootstrap"):
+ try:
+ _scHook()
+ except Exception as _scErr:
+ logger.warning(f"onBootstrap hook for system component failed: {_scErr}")
# Let features run their own bootstrap logic via lifecycle hooks
from modules.shared.featureDiscovery import loadFeatureMainModules
diff --git a/modules/interfaces/interfaceDbApp.py b/modules/interfaces/interfaceDbApp.py
index 023e07f3..d5fb2e49 100644
--- a/modules/interfaces/interfaceDbApp.py
+++ b/modules/interfaces/interfaceDbApp.py
@@ -1870,12 +1870,13 @@ class AppObjects:
instances = self.db.getRecordset(FeatureInstance, recordFilter={"mandateId": mandateId})
- # 0-pre-wa. WorkflowAutomation cascade-delete (system component, not auto-discovered)
- try:
- from modules.workflowAutomation.mainWorkflowAutomation import onMandateDelete as _waDeleteHook
- _waDeleteHook(mandateId, instances)
- except Exception as _waDelErr:
- logger.warning(f"onMandateDelete hook for 'workflowAutomation' failed: {_waDelErr}")
+ # 0-pre-sc. System-component cascade-delete (registered via app.py Composition Root)
+ from modules.shared.systemComponentRegistry import getLifecycleHooks
+ for _scHook in getLifecycleHooks("onMandateDelete"):
+ try:
+ _scHook(mandateId, instances)
+ except Exception as _scErr:
+ logger.warning(f"onMandateDelete hook for system component failed: {_scErr}")
# 0-pre. Let features cascade-delete their own data via lifecycle hooks
from modules.shared.featureDiscovery import loadFeatureMainModules
diff --git a/modules/interfaces/interfaceDbManagement.py b/modules/interfaces/interfaceDbManagement.py
index 35a4008e..b3acfcfc 100644
--- a/modules/interfaces/interfaceDbManagement.py
+++ b/modules/interfaces/interfaceDbManagement.py
@@ -807,7 +807,7 @@ class ComponentObjects:
next ``updateFile`` / ``getFile`` then rejects with
``File with ID ... not found`` -- the well-known "ghost duplicate"
symptom seen when ``interfaceDbComponent`` is initialised without an
- ``featureInstanceId`` (e.g. via ``serviceHub``) but a same-hash+name
+ ``featureInstanceId`` (e.g. via ``serviceCenter``) but a same-hash+name
file exists in another featureInstance under the same mandate.
We therefore cross-check the candidate through the RBAC-aware ``getFile``
before returning it; if RBAC blocks it, we treat it as "no duplicate
@@ -933,9 +933,7 @@ class ComponentObjects:
If pagination is provided: PaginatedResult with items and metadata
"""
def _convertFileItems(files):
- from modules.workflowAutomation.engine.workflowArtifactVisibility import (
- suppress_workflow_file_in_workspace_ui,
- )
+ from modules.shared.workflowArtifactVisibility import suppressWorkflowFileInWorkspaceUi
fileItems = []
for file in files:
@@ -949,7 +947,7 @@ class ComponentObjects:
fileName = file.get("fileName")
if not fileName or fileName == "None":
continue
- if suppress_workflow_file_in_workspace_ui(file):
+ if suppressWorkflowFileInWorkspaceUi(file):
continue
if file.get("scope") is None:
diff --git a/modules/interfaces/interfaceFeatures.py b/modules/interfaces/interfaceFeatures.py
index c947806c..c391deaa 100644
--- a/modules/interfaces/interfaceFeatures.py
+++ b/modules/interfaces/interfaceFeatures.py
@@ -321,7 +321,12 @@ class FeatureInterface:
f"for feature '{featureCode}' to instance {instanceId} (mandate={mandateId})"
)
- from modules.workflowAutomation.mainWorkflowAutomation import onInstanceCreate as _waOnInstanceCreate
+ from modules.shared.systemComponentRegistry import getLifecycleHooks
+ _onInstanceCreateHooks = getLifecycleHooks("onInstanceCreate")
+ if not _onInstanceCreateHooks:
+ logger.warning("_copyTemplateWorkflows: no onInstanceCreate hooks registered")
+ return 0
+ _waOnInstanceCreate = _onInstanceCreateHooks[0]
try:
copied = _waOnInstanceCreate(mandateId, instanceId, featureCode, templateWorkflows)
diff --git a/modules/interfaces/interfaceWorkflowAutomation.py b/modules/interfaces/interfaceWorkflowAutomation.py
index ba8fe6e7..9859ff2d 100644
--- a/modules/interfaces/interfaceWorkflowAutomation.py
+++ b/modules/interfaces/interfaceWorkflowAutomation.py
@@ -46,7 +46,7 @@ def _make_json_serializable(obj: Any, _depth: int = 0) -> Any:
from modules.datamodels.datamodelUam import User
from modules.datamodels.datamodelWorkflowAutomation import (
- GRAPHICAL_EDITOR_DATABASE,
+ WORKFLOW_AUTOMATION_DATABASE,
AutoWorkflow,
AutoVersion,
AutoRun,
@@ -59,15 +59,15 @@ from modules.dbHelpers.dbRegistry import registerDatabase
logger = logging.getLogger(__name__)
-workflowAutomationDatabase = GRAPHICAL_EDITOR_DATABASE
+workflowAutomationDatabase = WORKFLOW_AUTOMATION_DATABASE
registerDatabase(workflowAutomationDatabase)
_CALLBACK_WORKFLOW_CHANGED = "workflowAutomation.workflow.changed"
def _invocationsSyncedWithGraph(graph, invocations):
- """Lazy-load entryPoints to avoid L4->L5 top-level import."""
- from modules.workflowAutomation.editor.entryPoints import invocations_synced_with_graph
- return invocations_synced_with_graph(graph, invocations)
+ """Sync invocations with graph trigger nodes (via nodeCatalog, L2)."""
+ from modules.nodeCatalog.entryPoints import invocationsSyncedWithGraph
+ return invocationsSyncedWithGraph(graph, invocations)
def _getWorkflowAutomationInterface(
diff --git a/modules/workflowAutomation/editor/entryPoints.py b/modules/nodeCatalog/entryPoints.py
similarity index 63%
rename from modules/workflowAutomation/editor/entryPoints.py
rename to modules/nodeCatalog/entryPoints.py
index 3b4763f7..b1a8ae03 100644
--- a/modules/workflowAutomation/editor/entryPoints.py
+++ b/modules/nodeCatalog/entryPoints.py
@@ -3,27 +3,28 @@
Workflow entry points (Starts) — configuration outside the flow editor.
Kinds align with run envelope trigger.type where applicable.
+
+Canonical location: modules.nodeCatalog.entryPoints (L2).
+Depends only on stdlib — no cross-module imports.
"""
import uuid
from typing import Any, Dict, List, Optional
-# On-demand (gear: Manueller Trigger, Formular)
KINDS_ON_DEMAND = frozenset({"manual", "form", "api"})
-# Always-on (gear: Zeitplan, Immer aktiv, plus legacy listener kinds)
KINDS_ALWAYS_ON = frozenset({"schedule", "always_on", "email", "webhook", "event"})
ALL_KINDS = KINDS_ON_DEMAND | KINDS_ALWAYS_ON
-def category_for_kind(kind: str) -> str:
+def categoryForKind(kind: str) -> str:
if kind in KINDS_ALWAYS_ON:
return "always_on"
return "on_demand"
-def default_manual_entry_point() -> Dict[str, Any]:
+def defaultManualEntryPoint() -> Dict[str, Any]:
"""Single default manual start when a workflow has no invocations yet."""
return {
"id": str(uuid.uuid4()),
@@ -36,7 +37,7 @@ def default_manual_entry_point() -> Dict[str, Any]:
}
-def _normalize_title(title: Any) -> str:
+def _normalizeTitle(title: Any) -> str:
"""Extract a plain string from a title value for storage (not display)."""
if isinstance(title, dict):
picked = title.get("xx") or next((v for v in title.values() if v), None)
@@ -46,14 +47,14 @@ def _normalize_title(title: Any) -> str:
return "Start"
-def normalize_invocation_entry(raw: Dict[str, Any]) -> Dict[str, Any]:
+def normalizeInvocationEntry(raw: Dict[str, Any]) -> Dict[str, Any]:
"""Validate and normalize a single entry point dict."""
kind = (raw.get("kind") or "manual").strip()
if kind not in ALL_KINDS:
kind = "manual"
cat = raw.get("category")
if cat not in ("on_demand", "always_on"):
- cat = category_for_kind(kind)
+ cat = categoryForKind(kind)
eid = raw.get("id") or str(uuid.uuid4())
enabled = raw.get("enabled", True)
if not isinstance(enabled, bool):
@@ -65,21 +66,21 @@ def normalize_invocation_entry(raw: Dict[str, Any]) -> Dict[str, Any]:
"kind": kind,
"category": cat,
"enabled": enabled,
- "title": _normalize_title(raw.get("title")),
+ "title": _normalizeTitle(raw.get("title")),
"description": desc,
"config": config,
}
-def normalize_invocations_list(items: Optional[List[Any]]) -> List[Dict[str, Any]]:
+def normalizeInvocationsList(items: Optional[List[Any]]) -> List[Dict[str, Any]]:
if not items:
- return [default_manual_entry_point()]
+ return [defaultManualEntryPoint()]
out: List[Dict[str, Any]] = []
for raw in items:
if isinstance(raw, dict):
- out.append(normalize_invocation_entry(raw))
+ out.append(normalizeInvocationEntry(raw))
if not out:
- return [default_manual_entry_point()]
+ return [defaultManualEntryPoint()]
return out
@@ -90,26 +91,36 @@ _NODE_TYPE_TO_KIND = {
}
-def invocations_synced_with_graph(
+def _getTriggerNodes(nodes: List[Dict]) -> List[Dict]:
+ """Return start/trigger nodes: type ``trigger.*``, or category ``trigger`` / ``start``."""
+ return [
+ n
+ for n in nodes
+ if (
+ str(n.get("type", "")).startswith("trigger.")
+ or n.get("category") in ("trigger", "start")
+ )
+ ]
+
+
+def invocationsSyncedWithGraph(
graph: Optional[Dict[str, Any]],
- stored_invocations: Optional[List[Any]],
+ storedInvocations: Optional[List[Any]],
) -> List[Dict[str, Any]]:
"""Derive primary invocation (index 0) from the first start node in ``graph``.
If the graph has no start node, only non-primary stored invocations are kept
(no injected default). Document order in ``nodes`` defines which start wins.
"""
- from modules.workflowAutomation.engine.graphUtils import getTriggerNodes
-
g = graph if isinstance(graph, dict) else {}
nodes = g.get("nodes") or []
- stored = list(stored_invocations or [])
+ stored = list(storedInvocations or [])
rest: List[Dict[str, Any]] = []
for raw in stored[1:]:
if isinstance(raw, dict):
- rest.append(normalize_invocation_entry(raw))
+ rest.append(normalizeInvocationEntry(raw))
- triggers = getTriggerNodes(nodes)
+ triggers = _getTriggerNodes(nodes)
if not triggers:
return rest
@@ -119,29 +130,28 @@ def invocations_synced_with_graph(
nid = node.get("id")
if not nid:
nid = str(uuid.uuid4())
- raw_title = node.get("title") or node.get("label") or "Start"
+ rawTitle = node.get("title") or node.get("label") or "Start"
- old_primary = stored[0] if stored and isinstance(stored[0], dict) else {}
+ oldPrimary = stored[0] if stored and isinstance(stored[0], dict) else {}
config: Dict[str, Any] = {}
- if isinstance(old_primary.get("config"), dict) and old_primary.get("kind") == kind:
- config = dict(old_primary["config"])
- desc = old_primary.get("description") if isinstance(old_primary.get("description"), dict) else {}
+ if isinstance(oldPrimary.get("config"), dict) and oldPrimary.get("kind") == kind:
+ config = dict(oldPrimary["config"])
+ desc = oldPrimary.get("description") if isinstance(oldPrimary.get("description"), dict) else {}
- primary_raw: Dict[str, Any] = {
+ primaryRaw: Dict[str, Any] = {
"id": str(nid),
"kind": kind,
"enabled": True,
- "title": raw_title,
+ "title": rawTitle,
"description": desc,
"config": config,
}
- primary = normalize_invocation_entry(primary_raw)
+ primary = normalizeInvocationEntry(primaryRaw)
return [primary] + rest
-# POST .../execute with entryPointId set to a schedule entry — no separate in-process scheduler here yet.
-def find_invocation(workflow: Dict[str, Any], entry_point_id: str) -> Optional[Dict[str, Any]]:
+def findInvocation(workflow: Dict[str, Any], entryPointId: str) -> Optional[Dict[str, Any]]:
for inv in workflow.get("invocations") or []:
- if isinstance(inv, dict) and inv.get("id") == entry_point_id:
+ if isinstance(inv, dict) and inv.get("id") == entryPointId:
return inv
return None
diff --git a/modules/routes/routeBilling.py b/modules/routes/routeBilling.py
index d193f9bb..143af2e2 100644
--- a/modules/routes/routeBilling.py
+++ b/modules/routes/routeBilling.py
@@ -907,8 +907,8 @@ def createCheckoutSession(
mandateLabel = targetMandateId
invoiceAddress = None
- from modules.serviceCenter.services.serviceBilling.stripeCheckout import create_checkout_session
- redirect_url = create_checkout_session(
+ from modules.serviceCenter.services.serviceBilling.stripeCheckout import createCheckoutSession
+ redirect_url = createCheckoutSession(
mandate_id=targetMandateId,
user_id=checkoutRequest.userId,
amount_chf=checkoutRequest.amount,
diff --git a/modules/routes/routeClickup.py b/modules/routes/routeClickup.py
index a6c6745d..41797d77 100644
--- a/modules/routes/routeClickup.py
+++ b/modules/routes/routeClickup.py
@@ -9,7 +9,8 @@ from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request, sta
from modules.auth import getCurrentUser, limiter
from modules.datamodels.datamodelUam import AuthAuthority, User, UserConnection
from modules.interfaces.interfaceDbApp import getInterface
-from modules.serviceCenter.serviceHub import getInterface as getServices
+from modules.serviceCenter import getService
+from modules.serviceCenter.context import ServiceCenterContext
from modules.shared.i18nRegistry import apiRouteContext
routeApiMsg = apiRouteContext("routeClickup")
@@ -59,13 +60,14 @@ def _clickup_connection_or_404(interface, connection_id: str, user_id: str) -> U
def _svc_for_connection(current_user: User, connection: UserConnection):
- services = getServices(current_user, None)
- if not services.clickup.setAccessTokenFromConnection(connection):
+ ctx = ServiceCenterContext(user=current_user)
+ clickupService = getService("clickup", ctx)
+ if not clickupService.setAccessTokenFromConnection(connection):
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=routeApiMsg("Failed to set ClickUp access token. Connection may be expired or invalid."),
)
- return services.clickup
+ return clickupService
@router.get("/{connectionId}/teams/{teamId}", response_model=Dict[str, Any])
diff --git a/modules/routes/routeSharepoint.py b/modules/routes/routeSharepoint.py
index b144328e..328a1bba 100644
--- a/modules/routes/routeSharepoint.py
+++ b/modules/routes/routeSharepoint.py
@@ -12,7 +12,8 @@ from fastapi import APIRouter, HTTPException, Depends, Path, Query, Request, sta
from modules.auth import limiter, getCurrentUser
from modules.datamodels.datamodelUam import User, UserConnection
from modules.interfaces.interfaceDbApp import getInterface
-from modules.serviceCenter.serviceHub import getInterface as getServices
+from modules.serviceCenter import getService
+from modules.serviceCenter.context import ServiceCenterContext
from modules.shared.i18nRegistry import apiRouteContext
routeApiMsg = apiRouteContext("routeSharepoint")
@@ -122,19 +123,17 @@ async def getSharepointFolderOptionsByReference(
detail=f"Connection is not a Microsoft connection (authority: {authority})"
)
- # Initialize services
- services = getServices(currentUser, None)
+ ctx = ServiceCenterContext(user=currentUser)
+ sharepointService = getService("sharepoint", ctx)
- # Set access token on SharePoint service
- if not services.sharepoint.setAccessTokenFromConnection(connection):
+ if not sharepointService.setAccessTokenFromConnection(connection):
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=routeApiMsg("Failed to set SharePoint access token. Connection may be expired or invalid.")
)
- # Mode 1: Return sites list if no siteId specified
if not siteId:
- sites = await services.sharepoint.discoverSites()
+ sites = await sharepointService.discoverSites()
return [
{
"type": "site",
@@ -148,9 +147,8 @@ async def getSharepointFolderOptionsByReference(
for site in sites
]
- # Mode 2: Return folders within specific site
folderPath = path or ""
- items = await services.sharepoint.listFolderContents(siteId, folderPath)
+ items = await sharepointService.listFolderContents(siteId, folderPath)
if not items:
return []
diff --git a/modules/routes/routeSystem.py b/modules/routes/routeSystem.py
index 8529206b..853c4b32 100644
--- a/modules/routes/routeSystem.py
+++ b/modules/routes/routeSystem.py
@@ -839,12 +839,12 @@ def _buildIntegrationsOverviewPayload(userId: str, user=None) -> Dict[str, Any]:
from modules.connectors.connectorDbPostgre import DatabaseConnector
from modules.datamodels.datamodelPagination import PaginationParams
from modules.datamodels.datamodelWorkflowAutomation import (
- AutoWorkflow, AutoRun,
+ AutoWorkflow, AutoRun, WORKFLOW_AUTOMATION_DATABASE,
)
wfDb = DatabaseConnector(
dbHost=APP_CONFIG.get("DB_HOST", "localhost"),
- dbDatabase="poweron_graphicaleditor",
+ dbDatabase=WORKFLOW_AUTOMATION_DATABASE,
dbUser=APP_CONFIG.get("DB_USER"),
dbPassword=APP_CONFIG.get("DB_PASSWORD_SECRET") or APP_CONFIG.get("DB_PASSWORD"),
dbPort=int(APP_CONFIG.get("DB_PORT", 5432)),
diff --git a/modules/routes/routeWorkflowAutomation.py b/modules/routes/routeWorkflowAutomation.py
index 81c009fb..8a9dd587 100644
--- a/modules/routes/routeWorkflowAutomation.py
+++ b/modules/routes/routeWorkflowAutomation.py
@@ -31,7 +31,7 @@ from modules.datamodels.datamodelPagination import PaginationParams, PaginationM
from modules.datamodels.datamodelWorkflowAutomation import (
AutoWorkflow, AutoVersion, AutoRun, AutoStepLog, AutoTask,
)
-from modules.dbHelpers.paginationHelpers import applyFiltersAndSort
+from modules.dbHelpers.paginationHelpers import applyFiltersAndSort, paginateInMemory
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.shared.i18nRegistry import apiRouteContext, resolveText
from modules.workflowAutomation.helpers import (
@@ -75,9 +75,12 @@ async def _listWorkflows(
scopeFilter = {"mandateId": mandateId}
params = _parsePaginationOr400(pagination)
- records = db.getRecordset(AutoWorkflow, recordFilter=scopeFilter, pagination=params)
- total = db.getRecordCount(AutoWorkflow, recordFilter=scopeFilter) if params else len(records or [])
- return {"items": records or [], "total": total}
+ records = db.getRecordset(AutoWorkflow, recordFilter=scopeFilter)
+ if params:
+ filtered = applyFiltersAndSort(records or [], params)
+ pageItems, totalItems = paginateInMemory(filtered, params)
+ return {"items": pageItems, "total": totalItems}
+ return {"items": records or [], "total": len(records or [])}
finally:
db.close()
@@ -181,9 +184,12 @@ async def _listRuns(
scopeFilter = {**(scopeFilter or {}), "workflowId": workflowId}
params = _parsePaginationOr400(pagination)
- records = db.getRecordset(AutoRun, recordFilter=scopeFilter, pagination=params)
- total = db.getRecordCount(AutoRun, recordFilter=scopeFilter) if params else len(records or [])
- return {"items": records or [], "total": total}
+ records = db.getRecordset(AutoRun, recordFilter=scopeFilter)
+ if params:
+ filtered = applyFiltersAndSort(records or [], params)
+ pageItems, totalItems = paginateInMemory(filtered, params)
+ return {"items": pageItems, "total": totalItems}
+ return {"items": records or [], "total": len(records or [])}
finally:
db.close()
@@ -234,9 +240,12 @@ async def _listTasks(
scopeFilter = {**(scopeFilter or {}), "status": status}
params = _parsePaginationOr400(pagination)
- records = db.getRecordset(AutoTask, recordFilter=scopeFilter, pagination=params)
- total = db.getRecordCount(AutoTask, recordFilter=scopeFilter) if params else len(records or [])
- return {"items": records or [], "total": total}
+ records = db.getRecordset(AutoTask, recordFilter=scopeFilter)
+ if params:
+ filtered = applyFiltersAndSort(records or [], params)
+ pageItems, totalItems = paginateInMemory(filtered, params)
+ return {"items": pageItems, "total": totalItems}
+ return {"items": records or [], "total": len(records or [])}
finally:
db.close()
@@ -1243,11 +1252,11 @@ def _getRunDetail(
except Exception as e:
logger.warning("_getRunDetail: file lookup failed: %s", e)
- from modules.workflowAutomation.engine.workflowArtifactVisibility import suppress_workflow_file_in_workspace_ui
+ from modules.shared.workflowArtifactVisibility import suppressWorkflowFileInWorkspaceUi
def _resolveFileList(ids: set) -> list:
rows = [dict(fileMetaById[fid]) for fid in ids if fid in fileMetaById]
- return [m for m in rows if not suppress_workflow_file_in_workspace_ui(m)]
+ return [m for m in rows if not suppressWorkflowFileInWorkspaceUi(m)]
assignedFileIds: set = set()
for step, (inputIds, outputIds) in zip(steps, perStepFileIds):
@@ -1305,7 +1314,7 @@ def _buildExecuteRunEnvelope(
merge_run_envelope,
normalize_run_envelope,
)
- from modules.workflowAutomation.editor.entryPoints import find_invocation
+ from modules.nodeCatalog.entryPoints import findInvocation
if isinstance(body.get("runEnvelope"), dict):
env = normalize_run_envelope(body["runEnvelope"], user_id=userId)
@@ -1321,7 +1330,7 @@ def _buildExecuteRunEnvelope(
status_code=400,
detail=routeApiMsg("entryPointId requires a saved workflow (workflowId must refer to a stored workflow)"),
)
- inv = find_invocation(workflow, entryPointId)
+ inv = findInvocation(workflow, entryPointId)
if not inv:
raise HTTPException(status_code=400, detail=routeApiMsg("entryPointId not found on workflow"))
if not inv.get("enabled", True):
diff --git a/modules/serviceCenter/__init__.py b/modules/serviceCenter/__init__.py
index fb40df65..a2590fc6 100644
--- a/modules/serviceCenter/__init__.py
+++ b/modules/serviceCenter/__init__.py
@@ -16,9 +16,10 @@ from modules.serviceCenter.registry import (
)
from modules.serviceCenter.resolver import (
resolve,
- get_resolution_cache,
- clear_cache,
+ getResolutionCache,
+ clearCache,
)
+from modules.serviceCenter.services.serviceAgent.mainServiceAgent import ServicesBag
logger = logging.getLogger(__name__)
@@ -37,7 +38,7 @@ def getService(
Returns:
Service instance
"""
- cache = get_resolution_cache()
+ cache = getResolutionCache()
resolving = set()
return resolve(key, context, cache, resolving)
@@ -80,13 +81,13 @@ def registerServiceObjects(catalogService) -> bool:
return False
-def can_access_service(
+def canAccessService(
user,
rbac,
- service_key: str,
- mandate_id: Optional[str] = None,
- feature_instance_id: Optional[str] = None,
- allow_when_no_rbac: bool = True,
+ serviceKey: str,
+ mandateId: Optional[str] = None,
+ featureInstanceId: Optional[str] = None,
+ allowWhenNoRbac: bool = True,
) -> bool:
"""
Check if user has permission to access the given service.
@@ -94,40 +95,42 @@ def can_access_service(
Args:
user: User object
rbac: RbacClass instance (e.g. from interfaceDbApp.rbac)
- service_key: Service key (e.g., "web", "extraction")
- mandate_id: Optional mandate context
- feature_instance_id: Optional feature instance context
- allow_when_no_rbac: If True, allow when rbac is None (migration/default)
+ serviceKey: Service key (e.g., "web", "extraction")
+ mandateId: Optional mandate context
+ featureInstanceId: Optional feature instance context
+ allowWhenNoRbac: If True, allow when rbac is None (migration/default)
Returns:
True if user has view permission on the service
"""
if not rbac:
- return allow_when_no_rbac
- if service_key not in IMPORTABLE_SERVICES:
+ return allowWhenNoRbac
+ if serviceKey not in IMPORTABLE_SERVICES:
return False
- obj = IMPORTABLE_SERVICES[service_key]
- object_key = obj.get("objectKey")
- if not object_key:
+ obj = IMPORTABLE_SERVICES[serviceKey]
+ objectKey = obj.get("objectKey")
+ if not objectKey:
return False
from modules.datamodels.datamodelRbac import AccessRuleContext
permissions = rbac.getUserPermissions(
user,
AccessRuleContext.RESOURCE,
- object_key,
- mandateId=mandate_id,
- featureInstanceId=feature_instance_id,
+ objectKey,
+ mandateId=mandateId,
+ featureInstanceId=featureInstanceId,
)
return permissions.view if permissions else False
+
__all__ = [
"ServiceCenterContext",
+ "ServicesBag",
"getService",
"preWarm",
- "clear_cache",
+ "clearCache",
"registerServiceObjects",
- "can_access_service",
+ "canAccessService",
"SERVICE_RBAC_OBJECTS",
"CORE_SERVICES",
"IMPORTABLE_SERVICES",
diff --git a/modules/serviceCenter/resolver.py b/modules/serviceCenter/resolver.py
index 5d400760..316ce052 100644
--- a/modules/serviceCenter/resolver.py
+++ b/modules/serviceCenter/resolver.py
@@ -75,18 +75,21 @@ except ImportError:
pass
-def get_resolution_cache() -> Dict[str, Any]:
+def getResolutionCache() -> Dict[str, Any]:
"""Get the module-level resolution cache (for preWarm/clear)."""
return _resolution_cache
-def clear_cache() -> None:
+
+def clearCache() -> None:
"""Clear the resolution cache."""
lock = _cache_lock if _cache_lock is not None else _DummyLock()
with lock:
_resolution_cache.clear()
+
+
class _DummyLock:
def __enter__(self):
return self
diff --git a/modules/serviceCenter/serviceHub.py b/modules/serviceCenter/serviceHub.py
deleted file mode 100644
index a42f8d0e..00000000
--- a/modules/serviceCenter/serviceHub.py
+++ /dev/null
@@ -1,189 +0,0 @@
-# Copyright (c) 2025 Patrick Motsch
-# All rights reserved.
-"""
-Service Hub.
-Consumer-facing aggregation layer for services, DB interfaces, and runtime state.
-
-Architecture:
-- serviceHub delegates service resolution to serviceCenter (DI container)
-- serviceHub owns DB interface initialization and runtime state
-- serviceCenter knows nothing about serviceHub (one-way dependency)
-
-Import-Regelwerk:
-- Zentrale Module (wie dieses) duerfen KEINE Feature-Container importieren
-- Feature-spezifische Services werden dynamisch geladen
-- Shared Services werden via serviceCenter resolved
-"""
-
-import os
-import importlib
-import glob
-from typing import Any, Optional, TYPE_CHECKING
-import logging
-
-from modules.datamodels.datamodelUam import User
-
-if TYPE_CHECKING:
- from modules.datamodels.datamodelChat import ChatWorkflow
-
-logger = logging.getLogger(__name__)
-
-_FEATURES_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "features")
-
-
-class PublicService:
- """Lightweight proxy exposing only public callable attributes of a target."""
-
- def __init__(self, target: Any, functionsOnly: bool = True, nameFilter=None):
- self._target = target
- self._functionsOnly = functionsOnly
- self._nameFilter = nameFilter
-
- def __getattr__(self, name: str):
- if name.startswith('_'):
- raise AttributeError(f"'{type(self._target).__name__}' attribute '{name}' is private")
- if self._nameFilter and not self._nameFilter(name):
- raise AttributeError(f"'{name}' not exposed by policy")
- attr = getattr(self._target, name)
- if self._functionsOnly and not callable(attr):
- raise AttributeError(f"'{name}' is not a function")
- return attr
-
- def __dir__(self):
- return sorted([
- n for n in dir(self._target)
- if not n.startswith('_')
- and (not self._functionsOnly or callable(getattr(self._target, n, None)))
- and (self._nameFilter(n) if self._nameFilter else True)
- ])
-
-
-class ServiceHub:
- """
- Consumer-facing aggregation of services, DB interfaces, and runtime state.
-
- Services are lazy-resolved via serviceCenter on first access.
- DB interfaces and runtime state are initialized eagerly.
- Feature services/interfaces are discovered dynamically from features/.
- """
-
- _SERVICE_CENTER_WRAPPING = {
- "ai": {"functionsOnly": False},
- }
-
- def __init__(self, user: User, workflow: "ChatWorkflow" = None, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None):
- self.user: User = user
- self.workflow = workflow
- self.mandateId: Optional[str] = mandateId
- self.featureInstanceId: Optional[str] = featureInstanceId
- self.currentUserPrompt: str = ""
- self.rawUserPrompt: str = ""
-
- from modules.serviceCenter.context import ServiceCenterContext
- self._serviceCenterContext = ServiceCenterContext(
- user=user,
- workflow=workflow,
- mandate_id=mandateId,
- feature_instance_id=featureInstanceId,
- )
-
- from modules.interfaces.interfaceDbApp import getInterface as getAppInterface
- self.interfaceDbApp = getAppInterface(user, mandateId=mandateId)
-
- from modules.interfaces.interfaceDbManagement import getInterface as getComponentInterface
- self.interfaceDbComponent = getComponentInterface(user, mandateId=mandateId)
-
- self.rbac = self.interfaceDbApp.rbac if self.interfaceDbApp else None
-
- from modules.interfaces.interfaceDbChat import getInterface as getChatInterface
- self.interfaceDbChat = getChatInterface(user, mandateId=mandateId, featureInstanceId=featureInstanceId)
-
- self._loadFeatureInterfaces()
- self._loadFeatureServices()
-
- def __getattr__(self, name: str):
- """Lazy-resolve services via serviceCenter on first access."""
- if name.startswith('_'):
- raise AttributeError(name)
- try:
- from modules.serviceCenter import getService
- service = getService(name, self._serviceCenterContext)
- wrapping = self._SERVICE_CENTER_WRAPPING.get(name, {})
- functionsOnly = wrapping.get("functionsOnly", True)
- wrapped = PublicService(service, functionsOnly=functionsOnly)
- setattr(self, name, wrapped)
- return wrapped
- except KeyError:
- raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
-
- def _loadFeatureInterfaces(self):
- """Dynamically load interfaces from feature containers by filename pattern."""
- pattern = os.path.join(_FEATURES_DIR, "*", "interfaceFeature*.py")
- for filepath in glob.glob(pattern):
- try:
- featureDir = os.path.basename(os.path.dirname(filepath))
- filename = os.path.basename(filepath)[:-3]
-
- modulePath = f"modules.features.{featureDir}.{filename}"
- module = importlib.import_module(modulePath)
-
- if hasattr(module, "getInterface"):
- interface = module.getInterface(self.user, mandateId=self.mandateId, featureInstanceId=self.featureInstanceId)
- attrName = filename.replace("interfaceFeature", "interfaceDb")
- setattr(self, attrName, interface)
- logger.debug(f"Loaded interface: {attrName} from {modulePath}")
- except Exception as e:
- logger.debug(f"Could not load interface from {filepath}: {e}")
-
- def _loadFeatureServices(self):
- """Dynamically load services from feature containers by filename pattern."""
- pattern = os.path.join(_FEATURES_DIR, "*", "service*", "mainService*.py")
- for filepath in glob.glob(pattern):
- try:
- serviceDir = os.path.basename(os.path.dirname(filepath))
- featureDir = os.path.basename(os.path.dirname(os.path.dirname(filepath)))
- filename = os.path.basename(filepath)[:-3]
-
- modulePath = f"modules.features.{featureDir}.{serviceDir}.{filename}"
- module = importlib.import_module(modulePath)
-
- serviceClass = None
- for attrName in dir(module):
- if attrName.endswith("Service") and not attrName.startswith("_"):
- cls = getattr(module, attrName)
- if isinstance(cls, type):
- serviceClass = cls
- break
-
- if serviceClass:
- attrName = serviceDir.replace("service", "").lower()
- if not attrName:
- attrName = serviceDir.lower()
-
- functionsOnly = attrName != "ai"
-
- def _makeServiceResolver(hub):
- def _resolver(depKey: str):
- return getattr(hub, depKey)
- return _resolver
-
- import inspect
- sig = inspect.signature(serviceClass.__init__)
- paramCount = len([p for p in sig.parameters if p != 'self'])
- if paramCount >= 2:
- serviceInstance = serviceClass(self, _makeServiceResolver(self))
- else:
- serviceInstance = serviceClass(self)
- setattr(self, attrName, PublicService(serviceInstance, functionsOnly=functionsOnly))
- logger.debug(f"Loaded service: {attrName} from {modulePath}")
- except Exception as e:
- logger.debug(f"Could not load service from {filepath}: {e}")
-
-
-# Backward-compatible alias
-Services = ServiceHub
-
-
-def getInterface(user: User, workflow: "ChatWorkflow" = None, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None) -> ServiceHub:
- """Get ServiceHub instance for the given user, mandate, and feature instance context."""
- return ServiceHub(user, workflow, mandateId=mandateId, featureInstanceId=featureInstanceId)
diff --git a/modules/serviceCenter/services/serviceAgent/actionToolAdapter.py b/modules/serviceCenter/services/serviceAgent/actionToolAdapter.py
index 9389ee85..4cfbb8c4 100644
--- a/modules/serviceCenter/services/serviceAgent/actionToolAdapter.py
+++ b/modules/serviceCenter/services/serviceAgent/actionToolAdapter.py
@@ -274,7 +274,7 @@ def _persistLargeDocument(doc, services, context: Dict[str, Any]) -> Optional[Di
docName = getattr(doc, "documentName", "unnamed")
docMime = getattr(doc, "mimeType", "application/octet-stream")
try:
- fileItem, _ = chatService.interfaceDbComponent.saveUploadedFile(docBytes, docName)
+ fileItem, _ = chatService.saveUploadedFile(docBytes, docName)
from modules.serviceCenter.services.serviceAgent.coreTools._helpers import (
_attachFileAsChatDocument,
@@ -295,7 +295,7 @@ def _persistLargeDocument(doc, services, context: Dict[str, Any]) -> Optional[Di
updateFields["mandateId"] = mandateId
if updateFields:
logger.debug("_persistLargeDocument: updating file %s with %s", fileItem.id, updateFields)
- chatService.interfaceDbComponent.updateFile(fileItem.id, updateFields)
+ chatService.updateFile(fileItem.id, updateFields)
else:
logger.warning("_persistLargeDocument: no updateFields for file %s (tempFolderId=%s, fiId=%s)", fileItem.id, tempFolderId, fiId)
diff --git a/modules/serviceCenter/services/serviceAgent/coreTools/_connectionTools.py b/modules/serviceCenter/services/serviceAgent/coreTools/_connectionTools.py
index 4bb97de9..0a9e678b 100644
--- a/modules/serviceCenter/services/serviceAgent/coreTools/_connectionTools.py
+++ b/modules/serviceCenter/services/serviceAgent/coreTools/_connectionTools.py
@@ -88,12 +88,11 @@ def registerConnectionTools(registry: ToolRegistry, services):
graphAttachments: List[Dict[str, Any]] = []
if attachmentFileIds:
chatService = services.chat
- dbMgmt = chatService.interfaceDbComponent
for fid in attachmentFileIds:
- fileRow = dbMgmt.getFile(fid)
+ fileRow = chatService.getFile(fid)
if not fileRow:
return ToolResult(toolCallId="", toolName="sendMail", success=False, error=f"Attachment file not found: {fid}")
- rawBytes = dbMgmt.getFileData(fid)
+ rawBytes = chatService.getFileData(fid)
if not rawBytes:
return ToolResult(toolCallId="", toolName="sendMail", success=False, error=f"Attachment file has no data: {fid}")
graphAttachments.append({
diff --git a/modules/serviceCenter/services/serviceAgent/coreTools/_crossWorkflowTools.py b/modules/serviceCenter/services/serviceAgent/coreTools/_crossWorkflowTools.py
index 055a4055..2675257c 100644
--- a/modules/serviceCenter/services/serviceAgent/coreTools/_crossWorkflowTools.py
+++ b/modules/serviceCenter/services/serviceAgent/coreTools/_crossWorkflowTools.py
@@ -27,8 +27,7 @@ def registerCrossWorkflowTools(registry: ToolRegistry, services):
"""List all chat workflows in this workspace with metadata."""
try:
chatService = services.chat
- chatInterface = chatService.interfaceDbChat
- allWorkflows = chatInterface.getWorkflows() or []
+ allWorkflows = chatService.getWorkflows() or []
allWorkflows.sort(
key=lambda w: w.get("sysCreatedAt") or w.get("startedAt") or 0,
@@ -43,7 +42,7 @@ def registerCrossWorkflowTools(registry: ToolRegistry, services):
createdAt = wf.get("sysCreatedAt") or wf.get("startedAt") or 0
lastActivity = wf.get("lastActivity") or createdAt
- msgs = chatInterface.getMessages(wfId) or []
+ msgs = chatService.getMessages(wfId) or []
messageCount = len(msgs)
lastPreview = ""
if msgs:
@@ -102,8 +101,7 @@ def registerCrossWorkflowTools(registry: ToolRegistry, services):
try:
chatService = services.chat
- chatInterface = chatService.interfaceDbChat
- allMsgs = chatInterface.getMessages(targetWorkflowId) or []
+ allMsgs = chatService.getMessages(targetWorkflowId) or []
sliced = allMsgs[offset:offset + limit]
items = []
diff --git a/modules/serviceCenter/services/serviceAgent/coreTools/_dataSourceTools.py b/modules/serviceCenter/services/serviceAgent/coreTools/_dataSourceTools.py
index 291f33dc..76fd0bae 100644
--- a/modules/serviceCenter/services/serviceAgent/coreTools/_dataSourceTools.py
+++ b/modules/serviceCenter/services/serviceAgent/coreTools/_dataSourceTools.py
@@ -359,7 +359,7 @@ def registerDataSourceTools(registry: ToolRegistry, services):
elif fileBytes[:2] == b"PK":
fileName = f"{fileName}.zip"
chatService = services.chat
- fileItem, _ = chatService.interfaceDbComponent.saveUploadedFile(fileBytes, fileName)
+ fileItem, _ = chatService.saveUploadedFile(fileBytes, fileName)
updateFields = {}
tempFolderId = _getOrCreateTempFolder(chatService)
if tempFolderId:
@@ -370,7 +370,7 @@ def registerDataSourceTools(registry: ToolRegistry, services):
if _sourceNeutralize:
updateFields["neutralize"] = True
if updateFields:
- chatService.interfaceDbComponent.updateFile(fileItem.id, updateFields)
+ chatService.updateFile(fileItem.id, updateFields)
chatDocId = _attachFileAsChatDocument(
services, fileItem,
diff --git a/modules/serviceCenter/services/serviceAgent/coreTools/_featureSubAgentTools.py b/modules/serviceCenter/services/serviceAgent/coreTools/_featureSubAgentTools.py
index e6efad99..2dfc4686 100644
--- a/modules/serviceCenter/services/serviceAgent/coreTools/_featureSubAgentTools.py
+++ b/modules/serviceCenter/services/serviceAgent/coreTools/_featureSubAgentTools.py
@@ -173,11 +173,6 @@ def registerFeatureSubAgentTools(registry: ToolRegistry, services):
neutralizePolicy[tn] = {"tableActive": tableActive, "explicitFields": explicitFields}
neutralizationService = services.getService("neutralization") if hasattr(services, "getService") else None
- if neutralizationService is not None and not getattr(neutralizationService, "interfaceDbComponent", None):
- try:
- neutralizationService.interfaceDbComponent = services.chat.interfaceDbComponent
- except Exception:
- pass
cacheKey = f"{featureInstanceId}:{hashlib.md5(question.encode()).hexdigest()}"
if cacheKey in _featureQueryCache:
diff --git a/modules/serviceCenter/services/serviceAgent/coreTools/_helpers.py b/modules/serviceCenter/services/serviceAgent/coreTools/_helpers.py
index ea80fdc7..4e69d849 100644
--- a/modules/serviceCenter/services/serviceAgent/coreTools/_helpers.py
+++ b/modules/serviceCenter/services/serviceAgent/coreTools/_helpers.py
@@ -48,23 +48,16 @@ def _looksLikeBinary(data: bytes, sampleSize: int = 1024) -> bool:
def _getOrCreateTempFolder(chatService) -> Optional[str]:
"""Return the ID of the user's 'Temp' folder, creating it if it doesn't exist."""
- ifc = getattr(chatService, "interfaceDbComponent", None)
- if not ifc:
- logger.warning("_getOrCreateTempFolder: no interfaceDbComponent on chatService")
- return None
- userId = getattr(ifc, "userId", None)
- if not userId:
- logger.warning("_getOrCreateTempFolder: userId is None on interfaceDbComponent")
- return None
try:
- ownFolders = ifc.getOwnFolderTree()
+ ownFolders = chatService.getOwnFolderTree()
for f in ownFolders:
if f.get("name") == "Temp":
folderId = f.get("id")
logger.debug("_getOrCreateTempFolder: found existing Temp folder %s", folderId)
return str(folderId) if folderId else None
- newFolder = ifc.createFolder("Temp")
+ newFolder = chatService.createFolder("Temp")
folderId = newFolder.get("id") if isinstance(newFolder, dict) else getattr(newFolder, "id", None)
+ userId = getattr(getattr(chatService, "interfaceDbComponent", None), "userId", None)
logger.info("_getOrCreateTempFolder: created Temp folder %s for user %s", folderId, userId)
return str(folderId) if folderId else None
except Exception as e:
diff --git a/modules/serviceCenter/services/serviceAgent/coreTools/_mediaTools.py b/modules/serviceCenter/services/serviceAgent/coreTools/_mediaTools.py
index 380c9950..e3978b72 100644
--- a/modules/serviceCenter/services/serviceAgent/coreTools/_mediaTools.py
+++ b/modules/serviceCenter/services/serviceAgent/coreTools/_mediaTools.py
@@ -46,8 +46,8 @@ def registerMediaTools(registry: ToolRegistry, services):
if sourceFileId:
try:
- dbMgmt = services.chat.interfaceDbComponent
- fileRow = dbMgmt.getFile(sourceFileId)
+ chatService = services.chat
+ fileRow = chatService.getFile(sourceFileId)
if not fileRow:
return ToolResult(
toolCallId="",
@@ -55,7 +55,7 @@ def registerMediaTools(registry: ToolRegistry, services):
success=False,
error=f"sourceFileId not found: {sourceFileId}",
)
- rawBytes = dbMgmt.getFileData(sourceFileId)
+ rawBytes = chatService.getFileData(sourceFileId)
if not rawBytes:
return ToolResult(
toolCallId="",
@@ -244,11 +244,7 @@ def registerMediaTools(registry: ToolRegistry, services):
if not docName.lower().endswith(f".{outputFormat}"):
docName = f"{sanitizedTitle}.{outputFormat}"
- fileItem = None
- if hasattr(chatService.interfaceDbComponent, "saveGeneratedFile"):
- fileItem = chatService.interfaceDbComponent.saveGeneratedFile(docData, docName, docMime)
- else:
- fileItem, _ = chatService.interfaceDbComponent.saveUploadedFile(docData, docName)
+ fileItem, _ = chatService.saveUploadedFile(docData, docName)
if fileItem:
fid = fileItem.id if hasattr(fileItem, "id") else fileItem.get("id", "?")
@@ -260,7 +256,7 @@ def registerMediaTools(registry: ToolRegistry, services):
if fiId:
updateFields["featureInstanceId"] = fiId
if updateFields:
- chatService.interfaceDbComponent.updateFile(fid, updateFields)
+ chatService.updateFile(fid, updateFields)
chatDocId = _attachFileAsChatDocument(
services, fileItem,
label=f"renderDocument:{docName}",
@@ -544,11 +540,7 @@ def registerMediaTools(registry: ToolRegistry, services):
if not docName.lower().endswith(".png"):
docName = f"{sanitizedTitle}.png"
- fileItem = None
- if hasattr(chatService.interfaceDbComponent, "saveGeneratedFile"):
- fileItem = chatService.interfaceDbComponent.saveGeneratedFile(docData, docName, docMime)
- else:
- fileItem, _ = chatService.interfaceDbComponent.saveUploadedFile(docData, docName)
+ fileItem, _ = chatService.saveUploadedFile(docData, docName)
if fileItem:
fid = fileItem.id if hasattr(fileItem, "id") else fileItem.get("id", "?")
@@ -560,7 +552,7 @@ def registerMediaTools(registry: ToolRegistry, services):
if fiId:
updateFields["featureInstanceId"] = fiId
if updateFields:
- chatService.interfaceDbComponent.updateFile(fid, updateFields)
+ chatService.updateFile(fid, updateFields)
chatDocId = _attachFileAsChatDocument(
services, fileItem,
label=f"generateImage:{docName}",
@@ -709,10 +701,7 @@ def registerMediaTools(registry: ToolRegistry, services):
sanitizedTitle = re.sub(r'[^\w._-]', '_', title, flags=re.UNICODE).strip('_') or "chart"
fileName = f"{sanitizedTitle}.png"
- if hasattr(chatService.interfaceDbComponent, "saveGeneratedFile"):
- fileItem = chatService.interfaceDbComponent.saveGeneratedFile(pngData, fileName, "image/png")
- else:
- fileItem, _ = chatService.interfaceDbComponent.saveUploadedFile(pngData, fileName)
+ fileItem, _ = chatService.saveUploadedFile(pngData, fileName)
fid = fileItem.id if hasattr(fileItem, "id") else fileItem.get("id", "?") if isinstance(fileItem, dict) else "?"
if fid != "?":
@@ -724,7 +713,7 @@ def registerMediaTools(registry: ToolRegistry, services):
if fiId:
updateFields["featureInstanceId"] = fiId
if updateFields:
- chatService.interfaceDbComponent.updateFile(fid, updateFields)
+ chatService.updateFile(fid, updateFields)
chatDocId = _attachFileAsChatDocument(
services, fileItem,
@@ -811,7 +800,7 @@ def registerMediaTools(registry: ToolRegistry, services):
return ToolResult(toolCallId="", toolName="speechToText", success=False, error="fileId is required")
try:
chatService = services.chat
- audioData = chatService.interfaceDbComponent.getFileData(fileId)
+ audioData = chatService.getFileData(fileId)
if not audioData:
return ToolResult(toolCallId="", toolName="speechToText", success=False, error=f"No data found for file {fileId}")
from modules.interfaces.interfaceVoiceObjects import getVoiceInterface
@@ -855,8 +844,6 @@ def registerMediaTools(registry: ToolRegistry, services):
neutralizationService = services.getService("neutralization")
if not neutralizationService:
return ToolResult(toolCallId="", toolName="neutralizeData", success=False, error="Neutralization service not available")
- if not neutralizationService.interfaceDbComponent:
- neutralizationService.interfaceDbComponent = services.chat.interfaceDbComponent
if text:
result = await neutralizationService.processTextAsync(text, fileId or None)
else:
@@ -890,16 +877,13 @@ def registerMediaTools(registry: ToolRegistry, services):
if not neutralizationService or not hasattr(neutralizationService, "resolveText"):
return ToolResult(toolCallId="", toolName="revealDocument", success=False,
error="Neutralization service not available")
- if not getattr(neutralizationService, "interfaceDbComponent", None):
- neutralizationService.interfaceDbComponent = services.chat.interfaceDbComponent
-
if fileId and not text:
- dbMgmt = services.chat.interfaceDbComponent
- fileRow = dbMgmt.getFile(fileId)
+ chatService = services.chat
+ fileRow = chatService.getFile(fileId)
if not fileRow:
return ToolResult(toolCallId="", toolName="revealDocument", success=False,
error=f"fileId not found: {fileId}")
- rawBytes = dbMgmt.getFileData(fileId)
+ rawBytes = chatService.getFileData(fileId)
if not rawBytes:
return ToolResult(toolCallId="", toolName="revealDocument", success=False,
error="File data not accessible")
diff --git a/modules/serviceCenter/services/serviceAgent/coreTools/_workspaceTools.py b/modules/serviceCenter/services/serviceAgent/coreTools/_workspaceTools.py
index 9b4d2818..a1d56e24 100644
--- a/modules/serviceCenter/services/serviceAgent/coreTools/_workspaceTools.py
+++ b/modules/serviceCenter/services/serviceAgent/coreTools/_workspaceTools.py
@@ -283,7 +283,7 @@ def registerWorkspaceTools(registry: ToolRegistry, services):
return ToolResult(toolCallId="", toolName="tagFile", success=False, error="fileId is required")
try:
chatService = services.chat
- chatService.interfaceDbComponent.updateFile(fileId, {"tags": tags})
+ chatService.updateFile(fileId, {"tags": tags})
return ToolResult(
toolCallId="", toolName="tagFile", success=True,
data=f"Tags updated to {tags} for file {fileId}"
@@ -302,22 +302,21 @@ def registerWorkspaceTools(registry: ToolRegistry, services):
try:
chatService = services.chat
- dbMgmt = chatService.interfaceDbComponent
if mode == "append":
if not fileId:
return ToolResult(toolCallId="", toolName="writeFile", success=False, error="fileId is required for mode=append")
- file = dbMgmt.getFile(fileId)
+ file = chatService.getFile(fileId)
if not file:
return ToolResult(toolCallId="", toolName="writeFile", success=False, error=f"File {fileId} not found")
- existingData = dbMgmt.getFileData(fileId) or b""
+ existingData = chatService.getFileData(fileId) or b""
try:
existingText = existingData.decode("utf-8")
except UnicodeDecodeError:
existingText = existingData.decode("latin-1", errors="replace")
newContent = existingText + content
- dbMgmt.updateFileData(fileId, newContent.encode("utf-8"))
- dbMgmt.updateFile(fileId, {"fileSize": len(newContent.encode("utf-8"))})
+ chatService.updateFileData(fileId, newContent.encode("utf-8"))
+ chatService.updateFile(fileId, {"fileSize": len(newContent.encode("utf-8"))})
return ToolResult(
toolCallId="", toolName="writeFile", success=True,
data=f"Appended {len(content)} chars to '{file.fileName}' (id: {fileId}, total: {len(newContent)} chars)",
@@ -327,11 +326,11 @@ def registerWorkspaceTools(registry: ToolRegistry, services):
if mode == "overwrite":
if not fileId:
return ToolResult(toolCallId="", toolName="writeFile", success=False, error="fileId is required for mode=overwrite")
- file = dbMgmt.getFile(fileId)
+ file = chatService.getFile(fileId)
if not file:
return ToolResult(toolCallId="", toolName="writeFile", success=False, error=f"File {fileId} not found")
- dbMgmt.updateFileData(fileId, content.encode("utf-8"))
- dbMgmt.updateFile(fileId, {"fileSize": len(content.encode("utf-8"))})
+ chatService.updateFileData(fileId, content.encode("utf-8"))
+ chatService.updateFile(fileId, {"fileSize": len(content.encode("utf-8"))})
return ToolResult(
toolCallId="", toolName="writeFile", success=True,
data=f"Overwritten '{file.fileName}' (id: {fileId}, {len(content)} chars)",
@@ -341,7 +340,7 @@ def registerWorkspaceTools(registry: ToolRegistry, services):
# mode == "create" (default)
if not name:
return ToolResult(toolCallId="", toolName="writeFile", success=False, error="name is required for mode=create")
- fileItem, _ = dbMgmt.saveUploadedFile(content.encode("utf-8"), name)
+ fileItem, _ = chatService.saveUploadedFile(content.encode("utf-8"), name)
fiId = context.get("featureInstanceId") or (services.featureInstanceId if services else "")
updateFields: Dict[str, Any] = {}
if fiId:
@@ -351,7 +350,7 @@ def registerWorkspaceTools(registry: ToolRegistry, services):
if args.get("tags"):
updateFields["tags"] = args["tags"]
if updateFields:
- dbMgmt.updateFile(fileItem.id, updateFields)
+ chatService.updateFile(fileItem.id, updateFields)
chatDocId = _attachFileAsChatDocument(
services, fileItem,
@@ -498,7 +497,7 @@ def registerWorkspaceTools(registry: ToolRegistry, services):
return ToolResult(toolCallId="", toolName="deleteFile", success=False, error="fileId is required")
try:
chatService = services.chat
- file = chatService.interfaceDbComponent.getFile(fileId)
+ file = chatService.getFile(fileId)
if not file:
return ToolResult(toolCallId="", toolName="deleteFile", success=False, error=f"File {fileId} not found")
fileName = file.fileName
@@ -508,7 +507,7 @@ def registerWorkspaceTools(registry: ToolRegistry, services):
knowledgeService.removeFile(fileId)
except Exception as e:
logger.warning(f"deleteFile: knowledge store cleanup failed for {fileId}: {e}")
- chatService.interfaceDbComponent.deleteFile(fileId)
+ chatService.deleteFile(fileId)
return ToolResult(
toolCallId="", toolName="deleteFile", success=True,
data=f"File '{fileName}' (id: {fileId}) deleted",
@@ -524,7 +523,7 @@ def registerWorkspaceTools(registry: ToolRegistry, services):
return ToolResult(toolCallId="", toolName="renameFile", success=False, error="fileId and newName are required")
try:
chatService = services.chat
- chatService.interfaceDbComponent.updateFile(fileId, {"fileName": newName})
+ chatService.updateFile(fileId, {"fileName": newName})
return ToolResult(
toolCallId="", toolName="renameFile", success=True,
data=f"File {fileId} renamed to '{newName}'",
@@ -651,7 +650,7 @@ def registerWorkspaceTools(registry: ToolRegistry, services):
return ToolResult(toolCallId="", toolName="copyFile", success=False, error="fileId is required")
try:
chatService = services.chat
- copiedFile = chatService.interfaceDbComponent.copyFile(
+ copiedFile = chatService.copyFile(
fileId,
newFileName=args.get("newFileName"),
)
@@ -676,16 +675,15 @@ def registerWorkspaceTools(registry: ToolRegistry, services):
return ToolResult(toolCallId="", toolName="replaceInFile", success=False, error="fileId and oldText are required")
try:
chatService = services.chat
- dbMgmt = chatService.interfaceDbComponent
- file = dbMgmt.getFile(fileId)
+ file = chatService.getFile(fileId)
if not file:
return ToolResult(toolCallId="", toolName="replaceInFile", success=False, error=f"File {fileId} not found")
- if not dbMgmt.isTextMimeType(file.mimeType):
+ if not chatService.isTextMimeType(file.mimeType):
return ToolResult(
toolCallId="", toolName="replaceInFile", success=False,
error=f"Cannot edit binary file ({file.mimeType}). Only text-based files are supported."
)
- rawData = dbMgmt.getFileData(fileId)
+ rawData = chatService.getFileData(fileId)
if not rawData:
return ToolResult(toolCallId="", toolName="replaceInFile", success=False, error="File has no content")
try:
@@ -750,8 +748,7 @@ def registerWorkspaceTools(registry: ToolRegistry, services):
return ToolResult(toolCallId="", toolName="createFolder", success=False, error="name is required")
try:
chatService = services.chat
- dbMgmt = chatService.interfaceDbComponent
- folder = dbMgmt.createFolder(name, parentId=parentId)
+ folder = chatService.createFolder(name, parentId=parentId)
folderId = folder.get("id") if isinstance(folder, dict) else getattr(folder, "id", None)
folderName = folder.get("name") if isinstance(folder, dict) else getattr(folder, "name", name)
return ToolResult(
@@ -765,8 +762,7 @@ def registerWorkspaceTools(registry: ToolRegistry, services):
async def _listFolders(args: Dict[str, Any], context: Dict[str, Any]):
try:
chatService = services.chat
- dbMgmt = chatService.interfaceDbComponent
- folders = dbMgmt.getOwnFolderTree()
+ folders = chatService.getOwnFolderTree()
if not folders:
return ToolResult(toolCallId="", toolName="listFolders", success=True, data="No folders found.")
lines = []
@@ -795,11 +791,10 @@ def registerWorkspaceTools(registry: ToolRegistry, services):
return ToolResult(toolCallId="", toolName="moveFile", success=False, error="fileId is required")
try:
chatService = services.chat
- dbMgmt = chatService.interfaceDbComponent
- file = dbMgmt.getFile(fileId)
+ file = chatService.getFile(fileId)
if not file:
return ToolResult(toolCallId="", toolName="moveFile", success=False, error=f"File {fileId} not found")
- dbMgmt.updateFile(fileId, {"folderId": folderId or None})
+ chatService.updateFile(fileId, {"folderId": folderId or None})
targetLabel = f"folder {folderId}" if folderId else "root"
return ToolResult(
toolCallId="", toolName="moveFile", success=True,
@@ -843,8 +838,7 @@ def registerWorkspaceTools(registry: ToolRegistry, services):
return ToolResult(toolCallId="", toolName="renameFolder", success=False, error="folderId and newName are required")
try:
chatService = services.chat
- dbMgmt = chatService.interfaceDbComponent
- folder = dbMgmt.renameFolder(folderId, newName)
+ folder = chatService.renameFolder(folderId, newName)
return ToolResult(
toolCallId="", toolName="renameFolder", success=True,
data=f"Folder {folderId} renamed to '{newName}'",
diff --git a/modules/serviceCenter/services/serviceAgent/mainServiceAgent.py b/modules/serviceCenter/services/serviceAgent/mainServiceAgent.py
index e0c57496..e977f596 100644
--- a/modules/serviceCenter/services/serviceAgent/mainServiceAgent.py
+++ b/modules/serviceCenter/services/serviceAgent/mainServiceAgent.py
@@ -26,7 +26,7 @@ from modules.serviceCenter.services.serviceBilling.mainServiceBilling import (
logger = logging.getLogger(__name__)
-def _toolbox_connection_authorities(services: "_ServicesAdapter") -> List[str]:
+def _toolbox_connection_authorities(services: "ServicesBag") -> List[str]:
"""Collect connection authority strings for toolbox gating (requiresConnection).
The optional ``connection`` service is not always registered; fall back to
@@ -59,8 +59,10 @@ def _toolbox_connection_authorities(services: "_ServicesAdapter") -> List[str]:
return list(seen)
-class _ServicesAdapter:
- """Adapter providing service access from (context, get_service)."""
+class ServicesBag:
+ """Canonical services bag providing service access from (context, get_service).
+ Used by AgentService and WorkflowAutomation as the single source of truth
+ for service resolution, RBAC checks, and context-scoped properties."""
def __init__(self, context, getService: Callable[[str], Any]):
self._context = context
@@ -105,13 +107,6 @@ class _ServicesAdapter:
def extraction(self):
return self._getService("extraction")
- @property
- def interfaceDbComponent(self):
- try:
- return self.chat.interfaceDbComponent
- except Exception:
- return None
-
@property
def rbac(self):
"""Same RbacClass as workflow hub (MethodBase permission checks during discoverMethods)."""
@@ -128,6 +123,15 @@ class _ServicesAdapter:
"""Access any service by name."""
return self._getService(name)
+ def canAccessService(self, serviceKey: str) -> bool:
+ """Check if current user has RBAC permission for a service."""
+ from modules.serviceCenter import canAccessService
+ return canAccessService(
+ self.user, self.rbac, serviceKey,
+ mandateId=self.mandateId,
+ featureInstanceId=self.featureInstanceId,
+ )
+
def __getattr__(self, name: str):
"""Resolve e.g. services.clickup for MethodClickup / ActionExecutor (discoverMethods)."""
if name.startswith("_"):
@@ -157,7 +161,7 @@ class AgentService:
def __init__(self, context, get_service: Callable[[str], Any]):
self._context = context
self._getService = get_service
- self.services = _ServicesAdapter(context, get_service)
+ self.services = ServicesBag(context, get_service)
async def runAgent(
self,
@@ -676,8 +680,7 @@ def _buildWorkflowHintItems(
Limited to 10 most recent other workflows to keep the hint small.
"""
try:
- chatInterface = services.chat.interfaceDbChat
- allWorkflows = chatInterface.getWorkflows() or []
+ allWorkflows = services.chat.getWorkflows() or []
except Exception:
return []
diff --git a/modules/serviceCenter/services/serviceAi/subContentExtraction.py b/modules/serviceCenter/services/serviceAi/subContentExtraction.py
index 6e5ddd42..d66db1cc 100644
--- a/modules/serviceCenter/services/serviceAi/subContentExtraction.py
+++ b/modules/serviceCenter/services/serviceAi/subContentExtraction.py
@@ -261,7 +261,7 @@ class ContentExtractor:
# Check if it's standardized JSON format (has "documents" or "sections")
if document.mimeType == "application/json":
- docBytes = self.services.interfaceDbComponent.getFileData(document.fileId)
+ docBytes = self.services.chat.getFileData(document.fileId)
if docBytes:
try:
docData = docBytes.decode('utf-8')
@@ -349,7 +349,7 @@ class ContentExtractor:
if document.mimeType.startswith("image/") or self._isBinary(document.mimeType):
try:
# Lade Binary-Daten (getFileData ist nicht async - keine await nötig)
- binaryData = self.services.interfaceDbComponent.getFileData(document.fileId)
+ binaryData = self.services.chat.getFileData(document.fileId)
if not binaryData:
logger.warning(f"No binary data found for document {document.id}")
continue
diff --git a/modules/serviceCenter/services/serviceAi/subDocumentIntents.py b/modules/serviceCenter/services/serviceAi/subDocumentIntents.py
index 7d47c18f..aae86fc2 100644
--- a/modules/serviceCenter/services/serviceAi/subDocumentIntents.py
+++ b/modules/serviceCenter/services/serviceAi/subDocumentIntents.py
@@ -155,7 +155,7 @@ class DocumentIntentAnalyzer:
return None
try:
- docBytes = self.services.interfaceDbComponent.getFileData(document.fileId)
+ docBytes = self.services.chat.getFileData(document.fileId)
if not docBytes:
return None
diff --git a/modules/serviceCenter/services/serviceBilling/stripeCheckout.py b/modules/serviceCenter/services/serviceBilling/stripeCheckout.py
index bb9feea7..010c4e4b 100644
--- a/modules/serviceCenter/services/serviceBilling/stripeCheckout.py
+++ b/modules/serviceCenter/services/serviceBilling/stripeCheckout.py
@@ -212,7 +212,7 @@ def _normalizeReturnUrl(returnUrl: str) -> str:
return urlunsplit((parsed.scheme, parsed.netloc, normalized_path, normalized_query, ""))
-def create_checkout_session(
+def createCheckoutSession(
mandate_id: str,
user_id: Optional[str],
amount_chf: float,
diff --git a/modules/serviceCenter/services/serviceChat/mainServiceChat.py b/modules/serviceCenter/services/serviceChat/mainServiceChat.py
index 3e3d9f15..77856a7d 100644
--- a/modules/serviceCenter/services/serviceChat/mainServiceChat.py
+++ b/modules/serviceCenter/services/serviceChat/mainServiceChat.py
@@ -788,6 +788,151 @@ class ChatService:
'workflowId': 'unknown'
}
+ def createActionItem(self, actionData: Dict[str, Any]):
+ """Create an ActionItem record in the chat DB.
+ Encapsulates low-level _separateObjectFields + db.recordCreate so callers
+ never need direct interfaceDbChat access."""
+ from modules.datamodels.datamodelChat import ActionItem
+ simpleFields, _objectFields = self.interfaceDbChat._separateObjectFields(ActionItem, actionData)
+ return self.interfaceDbChat.db.recordCreate(ActionItem, simpleFields)
+
+ def getUserConnectionById(self, connectionId: str):
+ """Get a single UserConnection by ID, delegating to interfaceDbApp."""
+ try:
+ if self.interfaceDbApp and hasattr(self.interfaceDbApp, "getUserConnectionById"):
+ return self.interfaceDbApp.getUserConnectionById(str(connectionId))
+ except Exception as e:
+ logger.error(f"Error getting user connection by ID {connectionId}: {e}")
+ return None
+
+ # ---- File-Write operations (delegate to interfaceDbComponent) ----
+
+ def saveUploadedFile(self, fileContent: bytes, fileName: str):
+ """Save uploaded file bytes. Returns (fileItem, duplicateStatus)."""
+ try:
+ return self.interfaceDbComponent.saveUploadedFile(fileContent, fileName)
+ except Exception as e:
+ logger.error(f"Error saving uploaded file '{fileName}': {e}")
+ raise
+
+ def createFile(self, name: str, mimeType: str, content: bytes, folderId=None):
+ """Create a new file record with content."""
+ try:
+ return self.interfaceDbComponent.createFile(name, mimeType, content, folderId=folderId)
+ except Exception as e:
+ logger.error(f"Error creating file '{name}': {e}")
+ raise
+
+ def createFileData(self, fileId: str, data: bytes):
+ """Write binary data for an existing file record."""
+ try:
+ return self.interfaceDbComponent.createFileData(fileId, data)
+ except Exception as e:
+ logger.error(f"Error creating file data for fileId '{fileId}': {e}")
+ raise
+
+ def updateFile(self, fileId: str, updateData: dict):
+ """Update file metadata (tags, fileName, fileSize, folderId, etc.)."""
+ try:
+ return self.interfaceDbComponent.updateFile(fileId, updateData)
+ except Exception as e:
+ logger.error(f"Error updating file '{fileId}': {e}")
+ raise
+
+ def updateFileData(self, fileId: str, data: bytes):
+ """Replace file binary content."""
+ try:
+ return self.interfaceDbComponent.updateFileData(fileId, data)
+ except Exception as e:
+ logger.error(f"Error updating file data for fileId '{fileId}': {e}")
+ raise
+
+ # ---- File-Manage operations (delegate to interfaceDbComponent) ----
+
+ def getFile(self, fileId: str):
+ """Get file metadata object by ID."""
+ try:
+ return self.interfaceDbComponent.getFile(fileId)
+ except Exception as e:
+ logger.error(f"Error getting file '{fileId}': {e}")
+ return None
+
+ def deleteFile(self, fileId: str):
+ """Delete a file by ID."""
+ try:
+ return self.interfaceDbComponent.deleteFile(fileId)
+ except Exception as e:
+ logger.error(f"Error deleting file '{fileId}': {e}")
+ raise
+
+ def copyFile(self, sourceFileId: str, newFileName=None):
+ """Copy a file, optionally with a new name."""
+ try:
+ return self.interfaceDbComponent.copyFile(sourceFileId, newFileName=newFileName)
+ except Exception as e:
+ logger.error(f"Error copying file '{sourceFileId}': {e}")
+ raise
+
+ def isTextMimeType(self, mimeType: str) -> bool:
+ """Check if a MIME type represents text content."""
+ try:
+ return self.interfaceDbComponent.isTextMimeType(mimeType)
+ except Exception as e:
+ logger.error(f"Error checking MIME type '{mimeType}': {e}")
+ return False
+
+ def getMimeType(self, fileName: str) -> str:
+ """Determine MIME type from file name."""
+ try:
+ return self.interfaceDbComponent.getMimeType(fileName)
+ except Exception as e:
+ logger.error(f"Error getting MIME type for '{fileName}': {e}")
+ return "application/octet-stream"
+
+ # ---- Folder operations (delegate to interfaceDbComponent) ----
+
+ def createFolder(self, name: str, parentId=None):
+ """Create a folder, optionally under a parent."""
+ try:
+ return self.interfaceDbComponent.createFolder(name, parentId=parentId)
+ except Exception as e:
+ logger.error(f"Error creating folder '{name}': {e}")
+ raise
+
+ def getOwnFolderTree(self):
+ """Get the user's folder tree."""
+ try:
+ return self.interfaceDbComponent.getOwnFolderTree()
+ except Exception as e:
+ logger.error(f"Error getting folder tree: {e}")
+ return None
+
+ def renameFolder(self, folderId: str, newName: str):
+ """Rename a folder."""
+ try:
+ return self.interfaceDbComponent.renameFolder(folderId, newName)
+ except Exception as e:
+ logger.error(f"Error renaming folder '{folderId}': {e}")
+ raise
+
+ # ---- Workflow-Listing operations (delegate to interfaceDbChat) ----
+
+ def getWorkflows(self, pagination=None):
+ """Get all workflows for the current context."""
+ try:
+ return self.interfaceDbChat.getWorkflows(pagination)
+ except Exception as e:
+ logger.error(f"Error getting workflows: {e}")
+ return []
+
+ def getMessages(self, workflowId: str, pagination=None):
+ """Get messages for a specific workflow."""
+ try:
+ return self.interfaceDbChat.getMessages(workflowId, pagination)
+ except Exception as e:
+ logger.error(f"Error getting messages for workflow '{workflowId}': {e}")
+ return []
+
def createWorkflow(self, workflowData: Dict[str, Any]):
"""Create a new workflow by delegating to the chat interface"""
try:
diff --git a/modules/serviceCenter/services/serviceClickup/__init__.py b/modules/serviceCenter/services/serviceClickup/__init__.py
index 6b3bb1f3..49f56ec0 100644
--- a/modules/serviceCenter/services/serviceClickup/__init__.py
+++ b/modules/serviceCenter/services/serviceClickup/__init__.py
@@ -2,6 +2,6 @@
# All rights reserved.
"""ClickUp service."""
-from .mainServiceClickup import ClickupService, clickup_authorization_header
+from .mainServiceClickup import ClickupService
-__all__ = ["ClickupService", "clickup_authorization_header"]
+__all__ = ["ClickupService"]
diff --git a/modules/serviceCenter/services/serviceClickup/mainServiceClickup.py b/modules/serviceCenter/services/serviceClickup/mainServiceClickup.py
index df216810..d1ef51b3 100644
--- a/modules/serviceCenter/services/serviceClickup/mainServiceClickup.py
+++ b/modules/serviceCenter/services/serviceClickup/mainServiceClickup.py
@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
_CLICKUP_API_BASE = "https://api.clickup.com/api/v2"
-def clickup_authorization_header(token: str) -> str:
+def _clickupAuthorizationHeader(token: str) -> str:
"""ClickUp: personal tokens are `pk_...` without Bearer; OAuth uses Bearer."""
return clickupAuthorizationHeader(token)
diff --git a/modules/serviceCenter/services/serviceGeneration/mainServiceGeneration.py b/modules/serviceCenter/services/serviceGeneration/mainServiceGeneration.py
index a7e9a36a..5dbf16de 100644
--- a/modules/serviceCenter/services/serviceGeneration/mainServiceGeneration.py
+++ b/modules/serviceCenter/services/serviceGeneration/mainServiceGeneration.py
@@ -31,7 +31,6 @@ class _ServicesAdapter:
self.mandateId = context.mandate_id
self.featureInstanceId = context.feature_instance_id
chat = get_service("chat")
- self.interfaceDbComponent = chat.interfaceDbComponent
self.interfaceDbChat = chat.interfaceDbChat
@property
@@ -56,7 +55,6 @@ class GenerationService:
"""Initialize with ServiceCenterContext and service resolver."""
self.services = _ServicesAdapter(context, get_service)
self._get_service = get_service
- self.interfaceDbComponent = self.services.interfaceDbComponent
self.interfaceDbChat = self.services.interfaceDbChat
def processActionResultDocuments(self, actionResult, action) -> List[Dict[str, Any]]:
@@ -289,10 +287,11 @@ class GenerationService:
logger.warning(f"Could not set workflow context on document: {str(e)}")
def _createDocument(self, fileName: str, mimeType: str, content: str, base64encoded: bool = True, messageId: str = None) -> Optional[ChatDocument]:
- """Create file and ChatDocument using interfaces without service indirection."""
+ """Create file and ChatDocument using chat service."""
try:
- if not self.interfaceDbComponent:
- logger.error("Component interface not available for document creation")
+ chat = self.services.chat
+ if not chat:
+ logger.error("Chat service not available for document creation")
return None
# Convert content to bytes
if base64encoded:
@@ -301,12 +300,12 @@ class GenerationService:
else:
content_bytes = content.encode('utf-8')
# Create file and store data
- file_item = self.interfaceDbComponent.createFile(
+ file_item = chat.createFile(
name=fileName,
mimeType=mimeType,
content=content_bytes
)
- self.interfaceDbComponent.createFileData(file_item.id, content_bytes)
+ chat.createFileData(file_item.id, content_bytes)
# Collect file info
file_info = self._getFileInfo(file_item.id)
if not file_info:
@@ -321,12 +320,6 @@ class GenerationService:
fileSize=file_info.get("size", 0),
mimeType=file_info.get("mimeType", mimeType)
)
- # Ensure document can access component interface later
- if hasattr(document, 'setComponentInterface') and self.interfaceDbComponent:
- try:
- document.setComponentInterface(self.interfaceDbComponent)
- except Exception:
- pass
return document
except Exception as e:
logger.error(f"Error creating document: {str(e)}")
@@ -334,9 +327,10 @@ class GenerationService:
def _getFileInfo(self, fileId: str) -> Optional[Dict[str, Any]]:
try:
- if not self.interfaceDbComponent:
+ chat = self.services.chat
+ if not chat:
return None
- file_item = self.interfaceDbComponent.getFile(fileId)
+ file_item = chat.getFile(fileId)
if file_item:
return {
"id": file_item.id,
diff --git a/modules/shared/systemComponentRegistry.py b/modules/shared/systemComponentRegistry.py
new file mode 100644
index 00000000..e4733a68
--- /dev/null
+++ b/modules/shared/systemComponentRegistry.py
@@ -0,0 +1,32 @@
+# Copyright (c) 2025 Patrick Motsch
+"""
+System-component lifecycle-hook registry (Layer L0 — shared).
+
+Higher-layer system components (e.g. workflowAutomation) register their
+lifecycle hooks here at boot time via ``app.py`` (Composition Root, L7).
+Interface modules read the registry generically — no upward imports needed.
+
+Supported events: ``onBootstrap``, ``onMandateDelete``, ``onInstanceCreate``.
+
+This is the same inversion pattern used by
+``serviceAgent/externalToolRegistry.py`` for agent tools.
+"""
+
+import logging
+from typing import Any, Callable, Dict, List
+
+logger = logging.getLogger(__name__)
+
+_hooks: Dict[str, List[Callable[..., Any]]] = {}
+
+
+def registerLifecycleHook(eventName: str, handler: Callable[..., Any]) -> None:
+ """Register a lifecycle handler for *eventName*."""
+ _hooks.setdefault(eventName, []).append(handler)
+ logger.info("Registered system-component lifecycle hook: %s -> %s",
+ eventName, getattr(handler, "__qualname__", repr(handler)))
+
+
+def getLifecycleHooks(eventName: str) -> List[Callable[..., Any]]:
+ """Return all registered handlers for *eventName* (may be empty)."""
+ return list(_hooks.get(eventName, []))
diff --git a/modules/workflowAutomation/engine/workflowArtifactVisibility.py b/modules/shared/workflowArtifactVisibility.py
similarity index 90%
rename from modules/workflowAutomation/engine/workflowArtifactVisibility.py
rename to modules/shared/workflowArtifactVisibility.py
index 0eb8d4bd..3431bee2 100644
--- a/modules/workflowAutomation/engine/workflowArtifactVisibility.py
+++ b/modules/shared/workflowArtifactVisibility.py
@@ -9,13 +9,13 @@ from typing import Any, Mapping, Optional
_WORKFLOW_INTERNAL_FILE_TAG = "_workflowInternal"
-def suppress_workflow_file_in_workspace_ui(meta: Optional[Mapping[str, Any]]) -> bool:
+def suppressWorkflowFileInWorkspaceUi(meta: Optional[Mapping[str, Any]]) -> bool:
"""True when a file row should not appear in user-facing file lists.
Used by Automation Workspace **and** ``/api/files/list`` (Meine Dateien).
Matches persisted JSON handovers from transient runs (``extracted_content_transient*``),
internal extract image files (``extract_media_*``), the ``_workflowInternal`` tag, and
- optional explicit flags.
+ optional explicit flags.
"""
if not isinstance(meta, Mapping):
return False
diff --git a/modules/shared/workflowState.py b/modules/shared/workflowState.py
index 069645b9..6a8680a3 100644
--- a/modules/shared/workflowState.py
+++ b/modules/shared/workflowState.py
@@ -32,7 +32,7 @@ def checkWorkflowStopped(services: Any) -> None:
try:
# Get the current workflow status from the database to avoid stale data
- currentWorkflow = services.interfaceDbChat.getWorkflow(workflow.id)
+ currentWorkflow = services.chat.getWorkflow(workflow.id)
if currentWorkflow and currentWorkflow.status == "stopped":
logger.info("Workflow stopped by user, aborting operation")
raise WorkflowStoppedException("Workflow was stopped by user")
diff --git a/modules/workflowAutomation/engine/executionEngine.py b/modules/workflowAutomation/engine/executionEngine.py
index 443de25d..99f7c2ed 100644
--- a/modules/workflowAutomation/engine/executionEngine.py
+++ b/modules/workflowAutomation/engine/executionEngine.py
@@ -34,8 +34,8 @@ from modules.nodeCatalog.nodeDefinitions import STATIC_NODE_TYPES
from modules.datamodels.serviceExceptions import SubscriptionInactiveException as _SubscriptionInactiveException, BillingContextError as _BillingContextError
from modules.workflowAutomation.engine.runFileLogger import (
RunFileLogger,
- graphical_editor_run_file_logging_enabled,
- merge_run_context_with_ge_log_prefix,
+ workflowAutomationRunFileLoggingEnabled,
+ mergeRunContextWithWaLogPrefix,
)
from modules.workflowAutomation.engine.runEnvelope import normalize_run_envelope
@@ -383,7 +383,7 @@ async def _ge_log_node_finished(
exec_rec["output"] = (
_stripBinaryValues(output) if isinstance(output, dict) else {"value": _stripBinaryValues(output)}
)
- await file_logger.append_node_execution_line(exec_rec)
+ await file_logger.appendNodeExecutionLine(exec_rec)
ctx_rec: Dict[str, Any] = {
"timestamp": ts,
@@ -398,7 +398,7 @@ async def _ge_log_node_finished(
ctx_rec["loopIndex"] = loop_index
if loop_node_id is not None:
ctx_rec["loopNodeId"] = loop_node_id
- await file_logger.append_context_snapshot_line(ctx_rec)
+ await file_logger.appendContextSnapshotLine(ctx_rec)
async def _executeWithRetry(executor, node, context, maxRetries: int = 0, retryDelaySeconds: float = 1.0):
@@ -511,7 +511,7 @@ async def _run_post_loop_done_nodes(
automation2_interface: Optional[Any],
runId: Optional[str],
processed_in_loop: Set[str],
- ge_file_logger: Optional[RunFileLogger] = None,
+ waFileLogger: Optional[RunFileLogger] = None,
) -> Optional[Dict[str, Any]]:
"""After all loop iterations: merge upstream into loop output and run the Done (output 1) branch once."""
_prim_in = getLoopPrimaryInputSource(loop_node_id, connectionMap, body_ids)
@@ -553,7 +553,7 @@ async def _run_post_loop_done_nodes(
if _skId:
_updateStepLog(automation2_interface, _skId, "skipped")
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=nodeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -586,7 +586,7 @@ async def _run_post_loop_done_nodes(
output=_dres if isinstance(_dres, dict) else {"value": _dres},
durationMs=_dDur, tokensUsed=_dTok, retryCount=_dRetry)
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=nodeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -603,7 +603,7 @@ async def _run_post_loop_done_nodes(
_updateStepLog(automation2_interface, _dStepId, "completed",
durationMs=int((time.time() - _dStart) * 1000))
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=nodeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -619,7 +619,7 @@ async def _run_post_loop_done_nodes(
_updateStepLog(automation2_interface, _dStepId, "completed",
durationMs=int((time.time() - _dStart) * 1000))
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=nodeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -636,7 +636,7 @@ async def _run_post_loop_done_nodes(
_updateStepLog(automation2_interface, _dStepId, "failed",
error="Subscription/Billing error", durationMs=_dFailDur)
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=nodeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -654,7 +654,7 @@ async def _run_post_loop_done_nodes(
_updateStepLog(automation2_interface, _dStepId, "failed",
error=str(_dex), durationMs=_dFailDur2)
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=nodeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -767,7 +767,7 @@ async def executeGraph(
except Exception as valErr:
logger.warning("executeGraph resume: schema validation failed for %s: %s", startAfterNodeId, valErr)
- ge_file_logger: Optional[RunFileLogger] = None
+ waFileLogger: Optional[RunFileLogger] = None
nodeOutputs: Dict[str, Any] = dict(initialNodeOutputs or {})
if not runId and automation2_interface and workflowId and not is_resume:
run_context = {
@@ -805,8 +805,8 @@ async def executeGraph(
)
runId = run.get("id") if run else None
logger.info("executeGraph created run %s label=%s", runId, run_label)
- if runId and graphical_editor_run_file_logging_enabled():
- ge_file_logger = RunFileLogger.bootstrap_new_run(
+ if runId and workflowAutomationRunFileLoggingEnabled():
+ waFileLogger = RunFileLogger.bootstrapNewRun(
automation2_interface,
runId,
run_context,
@@ -842,12 +842,12 @@ async def executeGraph(
_activeRunContexts[runId] = context
if (
- graphical_editor_run_file_logging_enabled()
+ workflowAutomationRunFileLoggingEnabled()
and automation2_interface
and runId
- and ge_file_logger is None
+ and waFileLogger is None
):
- ge_file_logger = RunFileLogger.ensure_attached(
+ waFileLogger = RunFileLogger.ensureAttached(
automation2_interface,
runId,
)
@@ -916,7 +916,7 @@ async def executeGraph(
output=result if isinstance(result, dict) else {"value": result},
durationMs=_rDur, retryCount=_rRetry)
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=nodeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -940,7 +940,7 @@ async def executeGraph(
_updateStepLog(automation2_interface, _rStepId, "completed",
durationMs=_rPauseDur)
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=nodeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -964,7 +964,7 @@ async def executeGraph(
_updateStepLog(automation2_interface, _rStepId, "completed",
durationMs=_rEmailDur)
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=nodeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -984,7 +984,7 @@ async def executeGraph(
_updateStepLog(automation2_interface, _rStepId, "failed",
error="Subscription/Billing error", durationMs=_rFailDurSb)
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=nodeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -1005,7 +1005,7 @@ async def executeGraph(
_updateStepLog(automation2_interface, _rStepId, "failed",
error=str(ex), durationMs=_rFailDurEx)
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=nodeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -1049,7 +1049,7 @@ async def executeGraph(
automation2_interface=automation2_interface,
runId=runId,
processed_in_loop=processed_in_loop,
- ge_file_logger=ge_file_logger,
+ waFileLogger=waFileLogger,
)
for i, node in enumerate(ordered):
@@ -1088,7 +1088,7 @@ async def executeGraph(
if _skipStepId:
_updateStepLog(automation2_interface, _skipStepId, "skipped")
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=nodeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -1206,7 +1206,7 @@ async def executeGraph(
output=bres if isinstance(bres, dict) else {"value": bres},
durationMs=_bDur, retryCount=_bRetry)
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=_activeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -1230,7 +1230,7 @@ async def executeGraph(
_updateStepLog(automation2_interface, _bStepId, "completed",
durationMs=_bHd)
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=_activeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -1256,7 +1256,7 @@ async def executeGraph(
_updateStepLog(automation2_interface, _bStepId, "completed",
durationMs=_bEd)
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=_activeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -1277,7 +1277,7 @@ async def executeGraph(
_updateStepLog(automation2_interface, _bStepId, "failed",
error="Subscription/Billing error", durationMs=_bSb)
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=_activeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -1299,7 +1299,7 @@ async def executeGraph(
_updateStepLog(automation2_interface, _bStepId, "failed",
error=str(ex), durationMs=_bFail)
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=_activeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -1393,7 +1393,7 @@ async def executeGraph(
automation2_interface=automation2_interface,
runId=runId,
processed_in_loop=processed_in_loop,
- ge_file_logger=ge_file_logger,
+ waFileLogger=waFileLogger,
)
_loopDurMs = int((time.time() - _stepStartMs) * 1000)
@@ -1407,7 +1407,7 @@ async def executeGraph(
output=_loopStepOut,
durationMs=_loopDurMs)
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=nodeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -1441,7 +1441,7 @@ async def executeGraph(
output=result if isinstance(result, dict) else {"value": result},
durationMs=_mergeDurMs, tokensUsed=_mergeTok, retryCount=retryCount)
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=nodeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -1471,7 +1471,7 @@ async def executeGraph(
output=result if isinstance(result, dict) else {"value": result},
durationMs=_durMs, tokensUsed=_tokens, retryCount=retryCount)
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=nodeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -1500,7 +1500,7 @@ async def executeGraph(
if _ge_in is None:
_ge_in = locals().get("_loopInputSnap") or {}
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=nodeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -1528,7 +1528,7 @@ async def executeGraph(
if _ge_email_in is None:
_ge_email_in = locals().get("_loopInputSnap") or {}
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=nodeOutputs,
run_envelope=context.get("runEnvelope"),
@@ -1564,7 +1564,7 @@ async def executeGraph(
}
if automation2_interface and e.runId:
prev_ctx = dict((automation2_interface.getRun(e.runId) or {}).get("context") or {})
- run_ctx = merge_run_context_with_ge_log_prefix(prev_ctx, run_ctx)
+ run_ctx = mergeRunContextWithWaLogPrefix(prev_ctx, run_ctx)
automation2_interface.updateRun(
e.runId,
status="paused",
@@ -1589,7 +1589,7 @@ async def executeGraph(
if _ge_fail_in is None:
_ge_fail_in = locals().get("_loopInputSnap") or {}
await _ge_log_node_finished(
- ge_file_logger,
+ waFileLogger,
run_id=runId,
node_outputs=nodeOutputs,
run_envelope=context.get("runEnvelope"),
diff --git a/modules/workflowAutomation/engine/executors/actionNodeExecutor.py b/modules/workflowAutomation/engine/executors/actionNodeExecutor.py
index 799d1606..82c0cbe1 100644
--- a/modules/workflowAutomation/engine/executors/actionNodeExecutor.py
+++ b/modules/workflowAutomation/engine/executors/actionNodeExecutor.py
@@ -210,10 +210,10 @@ def _resolveConnectionIdToReference(chatService, connectionId: str, services=Non
return f"connection:{authority}:{username}"
except Exception as e:
logger.debug("_resolveConnectionIdToReference chatService: %s", e)
- app = getattr(services, "interfaceDbApp", None) if services else None
- if app and hasattr(app, "getUserConnectionById"):
+ chatSvc = getattr(services, "chat", None) if services else None
+ if chatSvc and hasattr(chatSvc, "getUserConnectionById"):
try:
- conn = app.getUserConnectionById(str(connectionId))
+ conn = chatSvc.getUserConnectionById(str(connectionId))
if conn:
authority = getattr(conn, "authority", None)
if hasattr(authority, "value"):
@@ -542,8 +542,7 @@ class ActionNodeExecutor:
resolvedParams[pname] = _wired
# 3. Resolve connectionReference
- chatService = getattr(self.services, "chat", None)
- _resolveConnectionParam(resolvedParams, chatService, self.services)
+ _resolveConnectionParam(resolvedParams, self.services.chat, self.services)
# 3b. Optional graph-level injections declared on the node definition.
# - injectUpstreamPayload: True → ``_upstreamPayload`` (port 0 source output, transit-unwrapped)
@@ -580,12 +579,10 @@ class ActionNodeExecutor:
# 6. Create progress parent so nested actions have a hierarchy
nodeOperationId = f"node_{nodeId}_{context.get('_runId', 'x')}_{int(time.time())}"
- chatService = getattr(self.services, "chat", None)
- if chatService:
- try:
- chatService.progressLogStart(nodeOperationId, methodName.capitalize(), actionName, f"Node {nodeId}")
- except Exception:
- pass
+ try:
+ self.services.chat.progressLogStart(nodeOperationId, methodName.capitalize(), actionName, f"Node {nodeId}")
+ except Exception:
+ pass
resolvedParams["parentOperationId"] = nodeOperationId
# 9. Execute action
@@ -632,26 +629,7 @@ class ActionNodeExecutor:
rawBytes = coerceDocumentDataToBytes(rawData)
if isinstance(dumped, dict) and rawBytes:
try:
- from modules.interfaces.interfaceDbManagement import getInterface as _getMgmtInterface
- from modules.interfaces.interfaceDbApp import getInterface as _getAppInterface
- from modules.security.rootAccess import getRootUser
- _userId = context.get("userId")
- _mandateId = context.get("mandateId")
- _instanceId = context.get("instanceId")
- _owner = None
- if _userId:
- try:
- _umap = _getAppInterface(getRootUser()).getUsersByIds([str(_userId)])
- _owner = _umap.get(str(_userId))
- except Exception as _ue:
- logger.warning("Could not resolve workflow user for file persistence: %s", _ue)
- if _owner is None:
- _owner = getRootUser()
- logger.debug(
- "Persisting workflow document as root user (no resolved owner userId=%r)",
- _userId,
- )
- _mgmt = _getMgmtInterface(_owner, mandateId=_mandateId, featureInstanceId=_instanceId)
+ _mgmt = self.services.interfaceDbComponent
_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)
diff --git a/modules/workflowAutomation/engine/executors/inputExecutor.py b/modules/workflowAutomation/engine/executors/inputExecutor.py
index 39efcfe6..926dd3a8 100644
--- a/modules/workflowAutomation/engine/executors/inputExecutor.py
+++ b/modules/workflowAutomation/engine/executors/inputExecutor.py
@@ -47,9 +47,9 @@ class InputExecutor:
)
taskId = task.get("id")
- from modules.workflowAutomation.engine.runFileLogger import merge_persisted_run_context
+ from modules.workflowAutomation.engine.runFileLogger import mergePersistedRunContext
- _pause_ctx = merge_persisted_run_context(
+ _pause_ctx = mergePersistedRunContext(
self.automation2,
runId,
{
diff --git a/modules/workflowAutomation/engine/runFileLogger.py b/modules/workflowAutomation/engine/runFileLogger.py
index 07600317..af57c275 100644
--- a/modules/workflowAutomation/engine/runFileLogger.py
+++ b/modules/workflowAutomation/engine/runFileLogger.py
@@ -1,5 +1,5 @@
# Copyright (c) 2025 Patrick Motsch
-"""Per-run NDJSON logs for persisted Automation2 / graphical-editor runs."""
+"""Per-run NDJSON logs for persisted workflow-automation runs."""
from __future__ import annotations
@@ -16,40 +16,40 @@ from modules.shared.debugLogger import ensureDir, resolve_app_log_dir
logger = logging.getLogger(__name__)
-RUN_FILE_LOG_RELATIVE_ROOT = "graphical_editor_runs"
-CONTEXT_KEY = "_geRunFileLogRelativeDir"
+RUN_FILE_LOG_RELATIVE_ROOT = "workflow_automation_runs"
+CONTEXT_KEY = "_waRunFileLogRelativeDir"
EXECUTION_FILENAME = "node_execution.ndjson"
CONTEXT_SNAPSHOT_FILENAME = "workflow_context.ndjson"
-def graphical_editor_run_file_logging_enabled() -> bool:
+def workflowAutomationRunFileLoggingEnabled() -> bool:
"""True when NDJSON files should be written for each persisted run."""
- raw = APP_CONFIG.get("APP_GRAPHICAL_EDITOR_RUN_FILE_LOGGING", False)
+ raw = APP_CONFIG.get("APP_WORKFLOW_AUTOMATION_RUN_FILE_LOGGING") or APP_CONFIG.get("APP_GRAPHICAL_EDITOR_RUN_FILE_LOGGING", False)
if isinstance(raw, bool):
return raw
s = str(raw).strip().lower()
return s in ("1", "true", "yes", "on")
-def merge_run_context_with_ge_log_prefix(
- base_context: Optional[Dict[str, Any]],
+def mergeRunContextWithWaLogPrefix(
+ baseContext: Optional[Dict[str, Any]],
incoming: Dict[str, Any],
) -> Dict[str, Any]:
- """Copy ``CONTEXT_KEY`` from *base_context* onto *incoming* if present (pause paths)."""
+ """Copy ``CONTEXT_KEY`` from *baseContext* onto *incoming* if present (pause paths)."""
out = dict(incoming or {})
- prev = (base_context or {}).get(CONTEXT_KEY)
+ prev = (baseContext or {}).get(CONTEXT_KEY)
if prev is not None:
out[CONTEXT_KEY] = prev
return out
-def merge_persisted_run_context(
- automation2_interface: Any,
- run_id: str,
+def mergePersistedRunContext(
+ workflowAutomationInterface: Any,
+ runId: str,
replacement: Dict[str, Any],
) -> Dict[str, Any]:
- """``{**db_context, **replacement}`` so *_geRunFileLogRelativeDir* and other keys survive pause updates."""
- prev = dict((automation2_interface.getRun(run_id) or {}).get("context") or {})
+ """``{**db_context, **replacement}`` so *_waRunFileLogRelativeDir* and other keys survive pause updates."""
+ prev = dict((workflowAutomationInterface.getRun(runId) or {}).get("context") or {})
return {**prev, **(replacement or {})}
@@ -58,65 +58,65 @@ class RunFileLogger:
__slots__ = ("_exec_path", "_ctx_path", "_lock", "_run_id")
- def __init__(self, run_id: str, absolute_run_dir: str) -> None:
- self._run_id = run_id
- ensureDir(absolute_run_dir)
- self._exec_path = os.path.join(absolute_run_dir, EXECUTION_FILENAME)
- self._ctx_path = os.path.join(absolute_run_dir, CONTEXT_SNAPSHOT_FILENAME)
+ def __init__(self, runId: str, absoluteRunDir: str) -> None:
+ self._run_id = runId
+ ensureDir(absoluteRunDir)
+ self._exec_path = os.path.join(absoluteRunDir, EXECUTION_FILENAME)
+ self._ctx_path = os.path.join(absoluteRunDir, CONTEXT_SNAPSHOT_FILENAME)
self._lock = asyncio.Lock()
@property
- def run_id(self) -> str:
+ def runId(self) -> str:
return self._run_id
@staticmethod
- def fresh_run_subdirectory_name(run_id: str) -> str:
+ def freshRunSubdirectoryName(runId: str) -> str:
ts = datetime.now(timezone.utc).strftime("%Y_%m_%d_%H_%M_%S")
- return f"{ts}__{run_id}"
+ return f"{ts}__{runId}"
@staticmethod
- def relative_run_path(subdir_name: str) -> str:
+ def relativeRunPath(subdirName: str) -> str:
"""Path relative to ``APP_LOGGING_LOG_DIR`` (POSIX-style segments)."""
- return "/".join((RUN_FILE_LOG_RELATIVE_ROOT, subdir_name))
+ return "/".join((RUN_FILE_LOG_RELATIVE_ROOT, subdirName))
@classmethod
- def bootstrap_new_run(cls, automation2_interface: Any, run_id: str, run_context: Dict[str, Any]) -> RunFileLogger | None:
+ def bootstrapNewRun(cls, workflowAutomationInterface: Any, runId: str, runContext: Dict[str, Any]) -> RunFileLogger | None:
"""Create filesystem folder + persist CONTEXT_KEY via ``updateRun``."""
- if not graphical_editor_run_file_logging_enabled():
+ if not workflowAutomationRunFileLoggingEnabled():
return None
- if not automation2_interface or not run_id:
+ if not workflowAutomationInterface or not runId:
return None
- subdir = cls.fresh_run_subdirectory_name(run_id)
- rel = cls.relative_run_path(subdir)
+ subdir = cls.freshRunSubdirectoryName(runId)
+ rel = cls.relativeRunPath(subdir)
base = resolve_app_log_dir()
absolute = os.path.join(base, RUN_FILE_LOG_RELATIVE_ROOT, subdir)
- merged = dict(run_context or {})
+ merged = dict(runContext or {})
merged[CONTEXT_KEY] = rel
try:
- automation2_interface.updateRun(run_id, context=merged)
+ workflowAutomationInterface.updateRun(runId, context=merged)
except Exception as ex:
- logger.warning("GeRunFileLog: could not persist log dir on run=%s: %s", run_id, ex)
+ logger.warning("WaRunFileLog: could not persist log dir on run=%s: %s", runId, ex)
return None
logger.info(
- "GeRunFileLog: created run folder %s (run=%s)",
+ "WaRunFileLog: created run folder %s (run=%s)",
absolute,
- run_id,
+ runId,
)
- return cls(run_id, absolute)
+ return cls(runId, absolute)
@classmethod
- def open_from_run_record(cls, automation2_interface: Any, run_id: str) -> RunFileLogger | None:
+ def openFromRunRecord(cls, workflowAutomationInterface: Any, runId: str) -> RunFileLogger | None:
"""Open logger for an existing run using CONTEXT_KEY from DB."""
- if not graphical_editor_run_file_logging_enabled():
+ if not workflowAutomationRunFileLoggingEnabled():
return None
- if not automation2_interface or not run_id:
+ if not workflowAutomationInterface or not runId:
return None
try:
- run = automation2_interface.getRun(run_id) or {}
+ run = workflowAutomationInterface.getRun(runId) or {}
except Exception as ex:
- logger.debug("GeRunFileLog: getRun failed run=%s: %s", run_id, ex)
+ logger.debug("WaRunFileLog: getRun failed run=%s: %s", runId, ex)
return None
rel = (run.get("context") or {}).get(CONTEXT_KEY)
if not rel or not isinstance(rel, str):
@@ -126,21 +126,21 @@ class RunFileLogger:
cand = os.path.realpath(os.path.join(base_norm, *rel.replace("\\", "/").split("/")))
if cand != allowed_root and not cand.startswith(allowed_root + os.sep):
logger.warning(
- "GeRunFileLog: path outside log root denied for run=%s rel=%s",
- run_id,
+ "WaRunFileLog: path outside log root denied for run=%s rel=%s",
+ runId,
rel,
)
return None
absolute = cand
- return cls(run_id, absolute)
+ return cls(runId, absolute)
@classmethod
- def find_existing_absolute_dir(cls, run_id: str) -> Optional[str]:
+ def findExistingAbsoluteDir(cls, runId: str) -> Optional[str]:
"""If a folder named ``*{timestamp}__{run_id}`` exists under the log root, return its absolute path."""
root = os.path.realpath(os.path.join(resolve_app_log_dir(), RUN_FILE_LOG_RELATIVE_ROOT))
if not os.path.isdir(root):
return None
- suffix = f"__{run_id}"
+ suffix = f"__{runId}"
try:
names = sorted((n for n in os.listdir(root) if n.endswith(suffix)), reverse=True)
except OSError:
@@ -154,62 +154,62 @@ class RunFileLogger:
return cand if os.path.isdir(cand) else None
@classmethod
- def ensure_attached(cls, automation2_interface: Any, run_id: str) -> RunFileLogger | None:
- """Open logger from DB, or reattach an on-disk folder for *run_id*, or create a new one."""
- opened = cls.open_from_run_record(automation2_interface, run_id)
+ def ensureAttached(cls, workflowAutomationInterface: Any, runId: str) -> RunFileLogger | None:
+ """Open logger from DB, or reattach an on-disk folder for *runId*, or create a new one."""
+ opened = cls.openFromRunRecord(workflowAutomationInterface, runId)
if opened is not None:
return opened
- if not graphical_editor_run_file_logging_enabled():
+ if not workflowAutomationRunFileLoggingEnabled():
return None
- if not automation2_interface or not run_id:
+ if not workflowAutomationInterface or not runId:
return None
try:
- run = automation2_interface.getRun(run_id) or {}
+ run = workflowAutomationInterface.getRun(runId) or {}
except Exception as ex:
- logger.debug("GeRunFileLog: ensure getRun failed run=%s: %s", run_id, ex)
+ logger.debug("WaRunFileLog: ensure getRun failed run=%s: %s", runId, ex)
return None
prev_ctx = dict(run.get("context") or {})
- existing_abs = cls.find_existing_absolute_dir(run_id)
+ existing_abs = cls.findExistingAbsoluteDir(runId)
if existing_abs:
base_norm = os.path.realpath(resolve_app_log_dir())
rel = os.path.relpath(existing_abs, base_norm).replace(os.sep, "/")
merged = {**prev_ctx, CONTEXT_KEY: rel}
try:
- automation2_interface.updateRun(run_id, context=merged)
+ workflowAutomationInterface.updateRun(runId, context=merged)
except Exception as ex:
- logger.warning("GeRunFileLog: reattach persist failed run=%s: %s", run_id, ex)
+ logger.warning("WaRunFileLog: reattach persist failed run=%s: %s", runId, ex)
return None
- logger.info("GeRunFileLog: reattached existing folder for run=%s -> %s", run_id, existing_abs)
- return cls(run_id, existing_abs)
+ logger.info("WaRunFileLog: reattached existing folder for run=%s -> %s", runId, existing_abs)
+ return cls(runId, existing_abs)
- subdir = cls.fresh_run_subdirectory_name(run_id)
- rel = cls.relative_run_path(subdir)
+ subdir = cls.freshRunSubdirectoryName(runId)
+ rel = cls.relativeRunPath(subdir)
base = resolve_app_log_dir()
absolute = os.path.join(base, RUN_FILE_LOG_RELATIVE_ROOT, subdir)
merged = {**prev_ctx, CONTEXT_KEY: rel}
try:
- automation2_interface.updateRun(run_id, context=merged)
+ workflowAutomationInterface.updateRun(runId, context=merged)
except Exception as ex:
- logger.warning("GeRunFileLog: ensure new folder persist failed run=%s: %s", run_id, ex)
+ logger.warning("WaRunFileLog: ensure new folder persist failed run=%s: %s", runId, ex)
return None
- logger.info("GeRunFileLog: created late attach folder %s (run=%s)", absolute, run_id)
- return cls(run_id, absolute)
+ logger.info("WaRunFileLog: created late attach folder %s (run=%s)", absolute, runId)
+ return cls(runId, absolute)
- async def append_node_execution_line(self, record: Dict[str, Any]) -> None:
+ async def appendNodeExecutionLine(self, record: Dict[str, Any]) -> None:
line = json.dumps(record, ensure_ascii=False, default=str)
async with self._lock:
try:
with open(self._exec_path, "a", encoding="utf-8") as f:
f.write(line + "\n")
except Exception as ex:
- logger.warning("GeRunFileLog: append execution failed run=%s: %s", self._run_id, ex)
+ logger.warning("WaRunFileLog: append execution failed run=%s: %s", self._run_id, ex)
- async def append_context_snapshot_line(self, record: Dict[str, Any]) -> None:
+ async def appendContextSnapshotLine(self, record: Dict[str, Any]) -> None:
line = json.dumps(record, ensure_ascii=False, default=str)
async with self._lock:
try:
with open(self._ctx_path, "a", encoding="utf-8") as f:
f.write(line + "\n")
except Exception as ex:
- logger.warning("GeRunFileLog: append context snapshot failed run=%s: %s", self._run_id, ex)
+ logger.warning("WaRunFileLog: append context snapshot failed run=%s: %s", self._run_id, ex)
diff --git a/modules/workflowAutomation/helpers.py b/modules/workflowAutomation/helpers.py
index 21471e6e..ddbde49e 100644
--- a/modules/workflowAutomation/helpers.py
+++ b/modules/workflowAutomation/helpers.py
@@ -22,7 +22,7 @@ from modules.connectors.connectorDbPostgre import DatabaseConnector
from modules.datamodels.datamodelPagination import PaginationParams, normalize_pagination_dict
from modules.datamodels.datamodelWorkflowAutomation import (
AutoRun, AutoStepLog, AutoWorkflow, AutoTask, AutoVersion,
- GRAPHICAL_EDITOR_DATABASE,
+ WORKFLOW_AUTOMATION_DATABASE,
)
from modules.interfaces.interfaceDbApp import getRootInterface as _getRootIface
from modules.shared.configuration import APP_CONFIG
@@ -38,7 +38,7 @@ def _getWorkflowAutomationDb() -> DatabaseConnector:
"""Get a DatabaseConnector for the WorkflowAutomation (graphicaleditor) DB."""
return DatabaseConnector(
dbHost=APP_CONFIG.get("DB_HOST", "localhost"),
- dbDatabase=GRAPHICAL_EDITOR_DATABASE,
+ dbDatabase=WORKFLOW_AUTOMATION_DATABASE,
dbUser=APP_CONFIG.get("DB_USER"),
dbPassword=APP_CONFIG.get("DB_PASSWORD_SECRET") or APP_CONFIG.get("DB_PASSWORD"),
dbPort=int(APP_CONFIG.get("DB_PORT", 5432)),
diff --git a/modules/workflowAutomation/mainWorkflowAutomation.py b/modules/workflowAutomation/mainWorkflowAutomation.py
index 20c1d4fb..e3a38d84 100644
--- a/modules/workflowAutomation/mainWorkflowAutomation.py
+++ b/modules/workflowAutomation/mainWorkflowAutomation.py
@@ -39,12 +39,12 @@ def _getWorkflowAutomationServices(
mandateId: Optional[str] = None,
featureInstanceId: Optional[str] = None,
workflow=None,
-) -> "_WorkflowAutomationServiceHub":
+):
"""
- Get a service hub for WorkflowAutomation using the service center.
+ Get a ServicesBag for WorkflowAutomation using the service center.
Used for methodDiscovery (I/O nodes) and execution (ActionExecutor).
"""
- from modules.serviceCenter import getService
+ from modules.serviceCenter import getService, ServicesBag
from modules.serviceCenter.context import ServiceCenterContext
_workflow = workflow
@@ -61,55 +61,7 @@ def _getWorkflowAutomationServices(
feature_instance_id=featureInstanceId,
workflow=_workflow,
)
-
- hub = _WorkflowAutomationServiceHub()
- hub.user = user
- hub.mandateId = mandateId
- hub.featureInstanceId = featureInstanceId
- hub._service_context = ctx
- hub.workflow = _workflow
- hub.featureCode = COMPONENT_CODE
-
- for spec in REQUIRED_SERVICES:
- key = spec["serviceKey"]
- try:
- svc = getService(key, ctx)
- setattr(hub, key, svc)
- except Exception as e:
- logger.warning(f"Could not resolve service '{key}' for workflowAutomation: {e}")
- setattr(hub, key, None)
-
- if hub.chat:
- hub.interfaceDbApp = getattr(hub.chat, "interfaceDbApp", None)
- hub.interfaceDbComponent = getattr(hub.chat, "interfaceDbComponent", None)
- hub.interfaceDbChat = getattr(hub.chat, "interfaceDbChat", None)
- hub.rbac = getattr(hub.interfaceDbApp, "rbac", None) if getattr(hub, "interfaceDbApp", None) else None
-
- return hub
-
-
-
-
-class _WorkflowAutomationServiceHub:
- """Lightweight hub for WorkflowAutomation (methodDiscovery, execution)."""
-
- user = None
- mandateId = None
- featureInstanceId = None
- _service_context = None
- workflow = None
- featureCode = COMPONENT_CODE
- interfaceDbApp = None
- interfaceDbComponent = None
- interfaceDbChat = None
- rbac = None
- chat = None
- ai = None
- utils = None
- extraction = None
- sharepoint = None
- clickup = None
- generation = None
+ return ServicesBag(ctx, lambda key: getService(key, ctx))
# ---------------------------------------------------------------------------
@@ -119,7 +71,7 @@ class _WorkflowAutomationServiceHub:
def onMandateDelete(mandateId: str, instances: list) -> None:
"""Cascade-delete all AutoWorkflow data for this mandate."""
from modules.datamodels.datamodelWorkflowAutomation import (
- GRAPHICAL_EDITOR_DATABASE, AutoWorkflow, AutoVersion, AutoRun, AutoStepLog, AutoTask,
+ WORKFLOW_AUTOMATION_DATABASE, AutoWorkflow, AutoVersion, AutoRun, AutoStepLog, AutoTask,
)
from modules.connectors.connectorDbPostgre import DatabaseConnector
from modules.shared.configuration import APP_CONFIG
@@ -127,7 +79,7 @@ def onMandateDelete(mandateId: str, instances: list) -> None:
try:
waDb = DatabaseConnector(
dbHost=APP_CONFIG.get("DB_HOST", "localhost"),
- dbDatabase=GRAPHICAL_EDITOR_DATABASE,
+ dbDatabase=WORKFLOW_AUTOMATION_DATABASE,
dbUser=APP_CONFIG.get("DB_USER"),
dbPassword=APP_CONFIG.get("DB_PASSWORD_SECRET") or APP_CONFIG.get("DB_PASSWORD"),
dbPort=int(APP_CONFIG.get("DB_PORT", 5432)),
@@ -245,14 +197,14 @@ def onBootstrap() -> None:
_migrateRbacNamespace()
_registerAgentTools()
- from modules.datamodels.datamodelWorkflowAutomation import GRAPHICAL_EDITOR_DATABASE, AutoWorkflow
+ from modules.datamodels.datamodelWorkflowAutomation import WORKFLOW_AUTOMATION_DATABASE, AutoWorkflow
from modules.connectors.connectorDbPostgre import DatabaseConnector
from modules.shared.configuration import APP_CONFIG
try:
waDb = DatabaseConnector(
dbHost=APP_CONFIG.get("DB_HOST", "localhost"),
- dbDatabase=GRAPHICAL_EDITOR_DATABASE,
+ dbDatabase=WORKFLOW_AUTOMATION_DATABASE,
dbUser=APP_CONFIG.get("DB_USER"),
dbPassword=APP_CONFIG.get("DB_PASSWORD_SECRET") or APP_CONFIG.get("DB_PASSWORD"),
)
diff --git a/modules/workflowAutomation/scheduler/mainScheduler.py b/modules/workflowAutomation/scheduler/mainScheduler.py
index 2f45932e..ec368480 100644
--- a/modules/workflowAutomation/scheduler/mainScheduler.py
+++ b/modules/workflowAutomation/scheduler/mainScheduler.py
@@ -209,7 +209,7 @@ class WorkflowScheduler:
from modules.workflowAutomation.mainWorkflowAutomation import _getWorkflowAutomationServices
from modules.workflowAutomation.engine.executionEngine import executeGraph
from modules.workflows.processing.shared.methodDiscovery import discoverMethods
- from modules.workflowAutomation.editor.entryPoints import find_invocation
+ from modules.nodeCatalog.entryPoints import findInvocation
from modules.workflowAutomation.engine.runEnvelope import default_run_envelope, normalize_run_envelope
iface = _getWorkflowAutomationInterface(eventUser, mandateId, instanceId)
@@ -221,7 +221,7 @@ class WorkflowScheduler:
logger.info("WorkflowScheduler: workflow %s inactive, skipping", workflowId)
return
- inv = find_invocation(wf, entryPointId)
+ inv = findInvocation(wf, entryPointId)
if inv and (inv.get("kind") != "schedule" or not inv.get("enabled", True)):
logger.info("WorkflowScheduler: entry point %s disabled for workflow %s", entryPointId, workflowId)
return
diff --git a/modules/workflows/methods/methodAi/actions/process.py b/modules/workflows/methods/methodAi/actions/process.py
index 04046f39..e3cc10f0 100644
--- a/modules/workflows/methods/methodAi/actions/process.py
+++ b/modules/workflows/methods/methodAi/actions/process.py
@@ -40,7 +40,7 @@ def _action_docs_to_content_parts(services, docs: List[Any]) -> List[ContentPart
from modules.datamodels.datamodelExtraction import ExtractionOptions, MergeStrategy
all_parts = []
- extraction = getattr(services, "extraction", None)
+ extraction = services.extraction
if not extraction:
logger.warning("ai.process: No extraction service - cannot extract from inline documents")
return []
@@ -80,25 +80,24 @@ def _resolve_file_refs_to_content_parts(services, fileIdRefs) -> List[ContentPar
via ``getChatDocumentsFromDocumentList`` instead."""
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")
+ extraction = services.extraction
+ if not extraction:
+ logger.warning("_resolve_file_refs_to_content_parts: missing extraction service")
return []
allParts: List[ContentPart] = []
opts = ExtractionOptions(prompt="", mergeStrategy=MergeStrategy())
for ref in fileIdRefs:
fileId = ref.documentId
- fileMeta = mgmt.getFile(fileId)
+ fileMeta = services.chat.getFile(fileId)
if not fileMeta:
logger.warning("_resolve_file_refs_to_content_parts: file %s not found "
"(lookup scope: mandate=%s, featureInstanceId=%s, userId=%s)",
- fileId, getattr(mgmt, "mandateId", "?"),
- getattr(mgmt, "featureInstanceId", "?"),
- getattr(mgmt, "userId", "?"))
+ fileId, getattr(services, "mandateId", "?"),
+ getattr(services, "featureInstanceId", "?"),
+ getattr(services, "userId", "?"))
continue
- fileData = mgmt.getFileData(fileId)
+ fileData = services.chat.getFileData(fileId)
if not fileData:
logger.warning(f"_resolve_file_refs_to_content_parts: no data for file {fileId}")
continue
@@ -265,7 +264,7 @@ async def process(self, parameters: Dict[str, Any]) -> ActionResult:
try:
documents = self.services.chat.getChatDocumentsFromDocumentList(documentList)
simpleParts = _action_docs_to_content_parts(self.services, [
- {"documentData": self.services.interfaceDbComponent.getFileData(doc.fileId),
+ {"documentData": self.services.chat.getFileData(doc.fileId),
"documentName": getattr(doc, 'fileName', ''),
"mimeType": getattr(doc, 'mimeType', 'application/octet-stream')}
for doc in documents if hasattr(doc, 'fileId') and doc.fileId
diff --git a/modules/workflows/methods/methodAi/actions/webResearch.py b/modules/workflows/methods/methodAi/actions/webResearch.py
index 778faf11..b020cff4 100644
--- a/modules/workflows/methods/methodAi/actions/webResearch.py
+++ b/modules/workflows/methods/methodAi/actions/webResearch.py
@@ -44,8 +44,6 @@ def _build_research_prompt(parameters: Dict[str, Any]) -> str:
async def webResearch(self, parameters: Dict[str, Any]) -> ActionResult:
- from modules.serviceCenter import ServiceCenterContext, getService, can_access_service
-
operationId = None
try:
prompt = _build_research_prompt(parameters)
@@ -53,25 +51,10 @@ async def webResearch(self, parameters: Dict[str, Any]) -> ActionResult:
return ActionResult.isFailure(error="Research prompt is required")
# RBAC: Check service-level permission
- rbac = getattr(self.services, "rbac", None)
- if rbac and not can_access_service(
- self.services.user,
- rbac,
- "web",
- mandate_id=getattr(self.services, "mandateId", None),
- feature_instance_id=getattr(self.services, "featureInstanceId", None),
- ):
+ if hasattr(self.services, "canAccessService") and not self.services.canAccessService("web"):
return ActionResult.isFailure(error="Permission denied: Web research service")
- # Build context for service center
- context = ServiceCenterContext(
- user=self.services.user,
- mandate_id=getattr(self.services, "mandateId", None),
- feature_instance_id=getattr(self.services, "featureInstanceId", None),
- workflow_id=self.services.workflow.id if self.services.workflow else None,
- workflow=self.services.workflow,
- )
- web_service = getService("web", context)
+ web_service = self.services.getService("web")
# Init progress logger
workflowId = self.services.workflow.id if self.services.workflow else f"no-workflow-{int(time.time())}"
diff --git a/modules/workflows/methods/methodBase.py b/modules/workflows/methods/methodBase.py
index abc7b9c0..d9a941c5 100644
--- a/modules/workflows/methods/methodBase.py
+++ b/modules/workflows/methods/methodBase.py
@@ -133,7 +133,7 @@ class MethodBase:
return False
# Get current user from services.user (not from chat service)
- currentUser = getattr(self.services, 'user', None)
+ currentUser = self.services.user
if not currentUser:
self.logger.warning(f"No current user found (services.user is None). Action {actionId} will be denied.")
return False
@@ -141,8 +141,8 @@ class MethodBase:
# RBAC-Check: RESOURCE context, item = actionId
# mandateId/featureInstanceId from services context needed to resolve user roles
try:
- mandateId = getattr(self.services, 'mandateId', None)
- featureInstanceId = getattr(self.services, 'featureInstanceId', None)
+ mandateId = self.services.mandateId
+ featureInstanceId = self.services.featureInstanceId
permissions = self.services.rbac.getUserPermissions(
user=currentUser,
context=AccessRuleContext.RESOURCE,
diff --git a/modules/workflows/methods/methodContext/actions/extractContent.py b/modules/workflows/methods/methodContext/actions/extractContent.py
index 2c1a2f9c..e1869be3 100644
--- a/modules/workflows/methods/methodContext/actions/extractContent.py
+++ b/modules/workflows/methods/methodContext/actions/extractContent.py
@@ -1177,6 +1177,7 @@ def _persist_extracted_image_parts(
*,
name_stem: str,
run_context: Optional[Dict[str, Any]],
+ services=None,
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
"""Decode base64 image parts, persist bytes, replace with ``embeddedImageFileId``; return artifacts meta."""
artifacts: List[Dict[str, Any]] = []
@@ -1193,27 +1194,19 @@ def _persist_extracted_image_parts(
)
return content_extracted_serial, artifacts
- try:
+ if services and hasattr(services, "interfaceDbComponent"):
+ mgmt = services.interfaceDbComponent
+ else:
from modules.interfaces.interfaceDbManagement import getInterface as _get_mgmt
- from modules.interfaces.interfaceDbApp import getInterface as _get_app
from modules.security.rootAccess import getRootUser
- except Exception as exc:
- logger.warning("extractContent image persist: import failed: %s", exc)
- return content_extracted_serial, artifacts
-
- owner = getRootUser()
- uid = run_context.get("userId")
- if uid:
try:
- umap = _get_app(getRootUser()).getUsersByIds([str(uid)])
- owner = umap.get(str(uid)) or owner
- except Exception:
- pass
+ mgmt = _get_mgmt(getRootUser(), mandateId=str(mandate_id), featureInstanceId=str(instance_id))
+ except Exception as exc:
+ logger.warning("extractContent image persist: mgmt interface failed: %s", exc)
+ return content_extracted_serial, artifacts
- try:
- mgmt = _get_mgmt(owner, mandateId=str(mandate_id), featureInstanceId=str(instance_id))
- except Exception as exc:
- logger.warning("extractContent image persist: mgmt interface failed: %s", exc)
+ if not mgmt:
+ logger.warning("extractContent image persist: no interfaceDbComponent available")
return content_extracted_serial, artifacts
stem = re.sub(r"[^\w\-]+", "_", name_stem).strip("_") or "extract"
@@ -1826,6 +1819,7 @@ async def extractContent(self, parameters: Dict[str, Any]) -> ActionResult:
content_extracted_serial,
name_stem=stem,
run_context=run_ctx if isinstance(run_ctx, dict) else None,
+ services=self.services,
)
presentation = build_presentation_for_serial_extractions(content_extracted_serial, file_names, pres_cfg)
diff --git a/modules/workflows/methods/methodFile/actions/create.py b/modules/workflows/methods/methodFile/actions/create.py
index bb778c8f..973f62d0 100644
--- a/modules/workflows/methods/methodFile/actions/create.py
+++ b/modules/workflows/methods/methodFile/actions/create.py
@@ -58,22 +58,9 @@ def _persistDocumentsToUserFiles(
) -> 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."""
- mgmt = getattr(services, "interfaceDbComponent", None)
- if not mgmt:
- try:
- import modules.interfaces.interfaceDbManagement as iface
- user = getattr(services, "user", None)
- if not user:
- return
- mgmt = iface.getInterface(
- user,
- mandateId=getattr(services, "mandateId", None) or "",
- featureInstanceId=getattr(services, "featureInstanceId", None) or "",
- )
- except Exception as e:
- logger.warning("file.create: could not get management interface for persistence: %s", e)
- return
- if not mgmt:
+ chat = getattr(services, "chat", None)
+ if not chat:
+ logger.warning("file.create: chat service not available for persistence")
return
for doc in action_documents:
try:
@@ -97,8 +84,8 @@ def _persistDocumentsToUserFiles(
or doc.get("mimeType")
or "application/octet-stream"
)
- file_item = mgmt.createFile(doc_name, mime, content, folderId=folder_id)
- mgmt.createFileData(file_item.id, content)
+ file_item = chat.createFile(doc_name, mime, content, folderId=folder_id)
+ chat.createFileData(file_item.id, content)
meta = getattr(doc, "validationMetadata", None) or doc.get("validationMetadata") or {}
if isinstance(meta, dict):
meta["fileId"] = file_item.id
@@ -118,23 +105,11 @@ def _sanitize_output_stem(title: str) -> str:
def _get_management_interface(services) -> Optional[Any]:
- mgmt = getattr(services, "interfaceDbComponent", None)
- if mgmt:
- return mgmt
- try:
- import modules.interfaces.interfaceDbManagement as iface
-
- user = getattr(services, "user", None)
- if not user:
- return None
- return iface.getInterface(
- user,
- mandateId=getattr(services, "mandateId", None) or "",
- featureInstanceId=getattr(services, "featureInstanceId", None) or "",
- )
- except Exception as e:
- logger.warning("file.create: could not get management interface: %s", e)
- return None
+ """Get chat service for file operations."""
+ chat = getattr(services, "chat", None)
+ if chat:
+ return chat
+ return None
def _load_image_bytes_from_action_doc(doc: dict, services) -> Optional[bytes]:
diff --git a/modules/workflows/methods/methodSharepoint/actions/downloadFileByPath.py b/modules/workflows/methods/methodSharepoint/actions/downloadFileByPath.py
index 447d8c08..793e07c9 100644
--- a/modules/workflows/methods/methodSharepoint/actions/downloadFileByPath.py
+++ b/modules/workflows/methods/methodSharepoint/actions/downloadFileByPath.py
@@ -89,17 +89,15 @@ async def downloadFileByPath(self, parameters: Dict[str, Any]) -> ActionResult:
"downloadFileByPath"
)
- # Save to user's Files (FileItem + FileData) via interfaceDbComponent – appears in Files UI
+ # Save to user's Files (FileItem + FileData) via chat service – appears in Files UI
fileItem = None
- db = getattr(self.services, "interfaceDbComponent", None)
- if db:
- try:
- mimeType = db.getMimeType(filename) if hasattr(db, "getMimeType") else "application/octet-stream"
- fileItem = db.createFile(name=filename, mimeType=mimeType, content=fileContent)
- db.createFileData(fileItem.id, fileContent)
- logger.info(f"Saved SharePoint file to user Files: {filename} (id={fileItem.id})")
- except Exception as e:
- logger.warning(f"Could not save to user Files: {e}")
+ try:
+ mimeType = self.services.chat.getMimeType(filename)
+ fileItem = self.services.chat.createFile(name=filename, mimeType=mimeType, content=fileContent)
+ self.services.chat.createFileData(fileItem.id, fileContent)
+ logger.info(f"Saved SharePoint file to user Files: {filename} (id={fileItem.id})")
+ except Exception as e:
+ logger.warning(f"Could not save to user Files: {e}")
# Encode as base64 for workflow context (AI, data nodes)
fileBase64 = base64.b64encode(fileContent).decode('utf-8')
diff --git a/modules/workflows/processing/modes/modeAutomation.py b/modules/workflows/processing/modes/modeAutomation.py
index 229bed5b..b6beabd3 100644
--- a/modules/workflows/processing/modes/modeAutomation.py
+++ b/modules/workflows/processing/modes/modeAutomation.py
@@ -349,7 +349,7 @@ class AutomationMode(BaseMode):
workflow = self.services.workflow
updateData = {"totalActions": totalActions}
workflow.totalActions = totalActions
- self.services.interfaceDbChat.updateWorkflow(workflow.id, updateData)
+ self.services.chat.updateWorkflow(workflow.id, updateData)
logger.info(f"Updated workflow {workflow.id} after action planning: {updateData}")
except Exception as e:
logger.error(f"Error updating workflow after action planning: {str(e)}")
@@ -369,7 +369,7 @@ class AutomationMode(BaseMode):
updateData["totalActions"] = totalActions
if updateData:
- self.services.interfaceDbChat.updateWorkflow(workflow.id, updateData)
+ self.services.chat.updateWorkflow(workflow.id, updateData)
logger.info(f"Updated workflow {workflow.id} totals: {updateData}")
except Exception as e:
logger.error(f"Error setting workflow totals: {str(e)}")
diff --git a/modules/workflows/processing/modes/modeBase.py b/modules/workflows/processing/modes/modeBase.py
index a8a3e048..684f5d52 100644
--- a/modules/workflows/processing/modes/modeBase.py
+++ b/modules/workflows/processing/modes/modeBase.py
@@ -67,8 +67,7 @@ class BaseMode(ABC):
if "execParameters" not in actionData:
actionData["execParameters"] = {}
- simpleFields, objectFields = self.services.interfaceDbChat._separateObjectFields(ActionItem, actionData)
- createdAction = self.services.interfaceDbChat.db.recordCreate(ActionItem, simpleFields)
+ createdAction = self.services.chat.createActionItem(actionData)
return ActionItem(
id=createdAction["id"],
@@ -103,7 +102,7 @@ class BaseMode(ABC):
workflow.currentTask = taskNumber
workflow.currentAction = 0
workflow.totalActions = 0
- self.services.interfaceDbChat.updateWorkflow(workflow.id, updateData)
+ self.services.chat.updateWorkflow(workflow.id, updateData)
logger.info(f"Updated workflow {workflow.id} before executing task {taskNumber}")
except Exception as e:
logger.error(f"Error updating workflow before executing task: {str(e)}")
@@ -114,7 +113,7 @@ class BaseMode(ABC):
workflow = self.services.workflow
updateData = {"currentAction": actionNumber}
workflow.currentAction = actionNumber
- self.services.interfaceDbChat.updateWorkflow(workflow.id, updateData)
+ self.services.chat.updateWorkflow(workflow.id, updateData)
logger.info(f"Updated workflow {workflow.id} before executing action {actionNumber}")
except Exception as e:
logger.error(f"Error updating workflow before executing action: {str(e)}")
diff --git a/modules/workflows/processing/workflowProcessor.py b/modules/workflows/processing/workflowProcessor.py
index d6fa00f0..5123f934 100644
--- a/modules/workflows/processing/workflowProcessor.py
+++ b/modules/workflows/processing/workflowProcessor.py
@@ -190,7 +190,7 @@ class WorkflowProcessor:
self.workflow.totalActions = 0
# Update in database
- self.services.interfaceDbChat.updateWorkflow(self.workflow.id, updateData)
+ self.services.chat.updateWorkflow(self.workflow.id, updateData)
logger.info(f"Updated workflow {self.workflow.id} after task plan creation: {updateData}")
except Exception as e:
@@ -211,7 +211,7 @@ class WorkflowProcessor:
self.workflow.totalActions = 0
# Update in database
- self.services.interfaceDbChat.updateWorkflow(self.workflow.id, updateData)
+ self.services.chat.updateWorkflow(self.workflow.id, updateData)
logger.info(f"Updated workflow {self.workflow.id} before executing task {taskNumber}: {updateData}")
except Exception as e:
@@ -228,7 +228,7 @@ class WorkflowProcessor:
self.workflow.totalActions = totalActions
# Update in database
- self.services.interfaceDbChat.updateWorkflow(self.workflow.id, updateData)
+ self.services.chat.updateWorkflow(self.workflow.id, updateData)
logger.info(f"Updated workflow {self.workflow.id} after action planning: {updateData}")
except Exception as e:
@@ -245,7 +245,7 @@ class WorkflowProcessor:
self.workflow.currentAction = actionNumber
# Update in database
- self.services.interfaceDbChat.updateWorkflow(self.workflow.id, updateData)
+ self.services.chat.updateWorkflow(self.workflow.id, updateData)
logger.info(f"Updated workflow {self.workflow.id} before executing action {actionNumber}: {updateData}")
except Exception as e:
@@ -266,7 +266,7 @@ class WorkflowProcessor:
# Update workflow object in database if we have changes
if updateData:
- self.services.interfaceDbChat.updateWorkflow(self.workflow.id, updateData)
+ self.services.chat.updateWorkflow(self.workflow.id, updateData)
logger.info(f"Updated workflow {self.workflow.id} totals in database: {updateData}")
logger.debug(f"Updated workflow totals: Tasks {self.workflow.totalTasks if hasattr(self.workflow, 'totalTasks') else 'N/A'}, Actions {self.workflow.totalActions if hasattr(self.workflow, 'totalActions') else 'N/A'}")
@@ -290,7 +290,7 @@ class WorkflowProcessor:
self.workflow.totalActions = 0
# Update in database
- self.services.interfaceDbChat.updateWorkflow(self.workflow.id, updateData)
+ self.services.chat.updateWorkflow(self.workflow.id, updateData)
logger.info(f"Reset workflow {self.workflow.id} for new session: {updateData}")
except Exception as e:
@@ -636,12 +636,12 @@ class WorkflowProcessor:
else:
contentBytes = json.dumps(rawData, ensure_ascii=False).encode('utf-8')
- fileItem = self.services.interfaceDbComponent.createFile(
+ fileItem = self.services.chat.createFile(
name=actionDoc.documentName if hasattr(actionDoc, 'documentName') else f"task_{taskResult.taskId}_result.txt",
mimeType=actionDoc.mimeType if hasattr(actionDoc, 'mimeType') else "text/plain",
content=contentBytes
)
- self.services.interfaceDbComponent.createFileData(
+ self.services.chat.createFileData(
fileItem.id,
contentBytes
)
diff --git a/modules/workflows/workflowManager.py b/modules/workflows/workflowManager.py
index e983a139..7f06b325 100644
--- a/modules/workflows/workflowManager.py
+++ b/modules/workflows/workflowManager.py
@@ -118,7 +118,7 @@ class WorkflowManager:
"totalTasks": 0,
"totalActions": 0,
"mandateId": self.services.mandateId,
- "featureInstanceId": getattr(self.services, 'featureInstanceId', None), # Feature instance ID for isolation
+ "featureInstanceId": self.services.featureInstanceId,
"messageIds": [],
"workflowMode": workflowMode,
"maxSteps": 10 , # Set maxSteps
@@ -478,12 +478,12 @@ The following is the user's original input message. Analyze intent, normalize th
if userInput.prompt:
try:
originalPromptBytes = userInput.prompt.encode('utf-8')
- fileItem = self.services.interfaceDbComponent.createFile(
+ fileItem = self.services.chat.createFile(
name="user_prompt_original.md",
mimeType="text/markdown",
content=originalPromptBytes
)
- self.services.interfaceDbComponent.createFileData(fileItem.id, originalPromptBytes)
+ self.services.chat.createFileData(fileItem.id, originalPromptBytes)
fileInfo = self.services.chat.getFileInfo(fileItem.id)
doc = {
"fileId": fileItem.id,
@@ -544,13 +544,13 @@ The following is the user's original input message. Analyze intent, normalize th
for actionDoc in result.documents:
if hasattr(actionDoc, 'documentData') and actionDoc.documentData:
# Create file in component storage
- fileItem = self.services.interfaceDbComponent.createFile(
+ fileItem = self.services.chat.createFile(
name=actionDoc.documentName if hasattr(actionDoc, 'documentName') else "fast_path_response.txt",
mimeType=actionDoc.mimeType if hasattr(actionDoc, 'mimeType') else "text/plain",
content=actionDoc.documentData if isinstance(actionDoc.documentData, bytes) else actionDoc.documentData.encode('utf-8')
)
# Persist file data
- self.services.interfaceDbComponent.createFileData(fileItem.id, actionDoc.documentData if isinstance(actionDoc.documentData, bytes) else actionDoc.documentData.encode('utf-8'))
+ self.services.chat.createFileData(fileItem.id, actionDoc.documentData if isinstance(actionDoc.documentData, bytes) else actionDoc.documentData.encode('utf-8'))
# Get file info
fileInfo = self.services.chat.getFileInfo(fileItem.id)
@@ -667,12 +667,12 @@ The following is the user's original input message. Analyze intent, normalize th
if userInput.prompt:
try:
originalPromptBytes = userInput.prompt.encode('utf-8')
- fileItem = self.services.interfaceDbComponent.createFile(
+ fileItem = self.services.chat.createFile(
name="user_prompt_original.md",
mimeType="text/markdown",
content=originalPromptBytes
)
- self.services.interfaceDbComponent.createFileData(fileItem.id, originalPromptBytes)
+ self.services.chat.createFileData(fileItem.id, originalPromptBytes)
fileInfo = self.services.chat.getFileInfo(fileItem.id)
doc = {
"fileId": fileItem.id,
@@ -807,12 +807,12 @@ The following is the user's original input message. Analyze intent, normalize th
if userInput.prompt:
try:
originalPromptBytes = userInput.prompt.encode('utf-8')
- fileItem = self.services.interfaceDbComponent.createFile(
+ fileItem = self.services.chat.createFile(
name="user_prompt_original.md",
mimeType="text/markdown",
content=originalPromptBytes
)
- self.services.interfaceDbComponent.createFileData(fileItem.id, originalPromptBytes)
+ self.services.chat.createFileData(fileItem.id, originalPromptBytes)
fileInfo = self.services.chat.getFileInfo(fileItem.id)
doc = {
"fileId": fileItem.id,
diff --git a/tests/eval/runTrusteeBenchmark.py b/tests/eval/runTrusteeBenchmark.py
index 749bf996..7622b3d0 100644
--- a/tests/eval/runTrusteeBenchmark.py
+++ b/tests/eval/runTrusteeBenchmark.py
@@ -409,20 +409,39 @@ def _extractNumbers(text: str) -> List[float]:
def _bootstrapServices() -> Tuple[Any, str, str]:
- """Spin up a minimal service hub bound to the root user + initial mandate.
+ """Spin up a minimal services bag bound to the root user + initial mandate.
- Returns the ServiceHub, the user id, and the mandate id used for billing.
+ Returns a services bag, the user id, and the mandate id used for billing.
"""
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelUam import Mandate
- from modules.serviceCenter.serviceHub import getInterface as getServices
+ from modules.serviceCenter import getService
+ from modules.serviceCenter.context import ServiceCenterContext
rootInterface = getRootInterface()
user = rootInterface.currentUser
mandateId = rootInterface.getInitialId(Mandate)
if not mandateId:
raise RuntimeError("No initial mandate available -- run bootstrap loader first.")
- services = getServices(user, workflow=None, mandateId=mandateId, featureInstanceId=None)
+
+ ctx = ServiceCenterContext(user=user, mandate_id=mandateId)
+
+ class _BenchmarkServicesBag:
+ def __init__(self, ctx):
+ self._ctx = ctx
+ self.user = ctx.user
+ self.mandateId = ctx.mandate_id
+ self.featureInstanceId = ctx.feature_instance_id
+ self.workflow = ctx.workflow
+
+ def __getattr__(self, name):
+ if name.startswith("_"):
+ raise AttributeError(name)
+ svc = getService(name, self._ctx)
+ setattr(self, name, svc)
+ return svc
+
+ services = _BenchmarkServicesBag(ctx)
return services, user.id, mandateId
diff --git a/tests/functional/test01_ai_model_selection.py b/tests/functional/test01_ai_model_selection.py
index 7c69b927..4c299a26 100644
--- a/tests/functional/test01_ai_model_selection.py
+++ b/tests/functional/test01_ai_model_selection.py
@@ -19,7 +19,8 @@ _gateway_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".
if _gateway_path not in sys.path:
sys.path.insert(0, _gateway_path)
-from modules.serviceCenter.serviceHub import getInterface as getServices
+from modules.serviceCenter import getService
+from modules.serviceCenter.context import ServiceCenterContext
from modules.datamodels.datamodelAi import (
AiCallOptions,
AiCallRequest,
@@ -33,6 +34,23 @@ from modules.aicore.aicoreModelRegistry import modelRegistry
from modules.aicore.aicoreModelSelector import modelSelector
+class _TestServicesBag:
+ """Mutable services bag for tests — lazy-resolves via getService, allows attribute overrides."""
+ def __init__(self, ctx):
+ self._ctx = ctx
+ self.user = ctx.user
+ self.mandateId = ctx.mandate_id
+ self.featureInstanceId = ctx.feature_instance_id
+ self.workflow = ctx.workflow
+
+ def __getattr__(self, name):
+ if name.startswith("_"):
+ raise AttributeError(name)
+ svc = getService(name, self._ctx)
+ setattr(self, name, svc)
+ return svc
+
+
class ModelSelectionTester:
def __init__(self) -> None:
testUser = User(
@@ -43,7 +61,8 @@ class ModelSelectionTester:
language="en",
mandateId="test_mandate",
)
- self.services = getServices(testUser, None)
+ ctx = ServiceCenterContext(user=testUser)
+ self.services = _TestServicesBag(ctx)
async def initialize(self) -> None:
from modules.serviceCenter.services.serviceAi.mainServiceAi import AiService
diff --git a/tests/functional/test02_ai_models.py b/tests/functional/test02_ai_models.py
index 32aeed80..4569455e 100644
--- a/tests/functional/test02_ai_models.py
+++ b/tests/functional/test02_ai_models.py
@@ -31,14 +31,31 @@ _gateway_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".
if _gateway_path not in sys.path:
sys.path.insert(0, _gateway_path)
-# Import the service initialization
-from modules.serviceCenter.serviceHub import getInterface as getServices
+from modules.serviceCenter import getService
+from modules.serviceCenter.context import ServiceCenterContext
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum
from modules.datamodels.datamodelUam import User
+
+class _TestServicesBag:
+ """Mutable services bag for tests — lazy-resolves via getService, allows attribute overrides."""
+ def __init__(self, ctx):
+ self._ctx = ctx
+ self.user = ctx.user
+ self.mandateId = ctx.mandate_id
+ self.featureInstanceId = ctx.feature_instance_id
+ self.workflow = ctx.workflow
+
+ def __getattr__(self, name):
+ if name.startswith("_"):
+ raise AttributeError(name)
+ svc = getService(name, self._ctx)
+ setattr(self, name, svc)
+ return svc
+
+
class AIModelsTester:
def __init__(self):
- # Create a minimal user context for testing
testUser = User(
id="test_user",
username="test_user",
@@ -48,8 +65,8 @@ class AIModelsTester:
mandateId="test_mandate"
)
- # Initialize services using the existing system
- self.services = getServices(testUser, None) # Test user, no workflow
+ ctx = ServiceCenterContext(user=testUser)
+ self.services = _TestServicesBag(ctx)
self.testResults = []
# Create logs directory if it doesn't exist (go up 2 levels from tests/unit/services/)
diff --git a/tests/functional/test03_ai_operations.py b/tests/functional/test03_ai_operations.py
index 835078f0..ee38af8b 100644
--- a/tests/functional/test03_ai_operations.py
+++ b/tests/functional/test03_ai_operations.py
@@ -20,6 +20,8 @@ if _gateway_path not in sys.path:
from modules.datamodels.datamodelAi import OperationTypeEnum
from modules.datamodels.datamodelChat import ChatWorkflow, ChatDocument, WorkflowModeEnum
from modules.datamodels.datamodelUam import User
+from modules.serviceCenter import getService
+from modules.serviceCenter.context import ServiceCenterContext
class MethodAiOperationsTester:
@@ -96,15 +98,27 @@ class MethodAiOperationsTester:
import logging
logging.getLogger().setLevel(logging.DEBUG)
- # Import and initialize services
import modules.interfaces.interfaceDbChat as interfaceFeatureAiChat
- interfaceDbChat = interfaceDbChat.getInterface(self.testUser)
+ interfaceDbChat = interfaceFeatureAiChat.getInterface(self.testUser)
- # Import and initialize services
- from modules.serviceCenter.serviceHub import getInterface as getServices
-
- # Get services first
- self.services = getServices(self.testUser, None)
+ ctx = ServiceCenterContext(user=self.testUser, mandate_id=self.testMandateId)
+
+ class _TestServicesBag:
+ def __init__(self, ctx):
+ self._ctx = ctx
+ self.user = ctx.user
+ self.mandateId = ctx.mandate_id
+ self.featureInstanceId = ctx.feature_instance_id
+ self.workflow = ctx.workflow
+
+ def __getattr__(self, name):
+ if name.startswith("_"):
+ raise AttributeError(name)
+ svc = getService(name, self._ctx)
+ setattr(self, name, svc)
+ return svc
+
+ self.services = _TestServicesBag(ctx)
# Now create AND SAVE workflow in database using the interface
import uuid
diff --git a/tests/functional/test04_ai_behavior.py b/tests/functional/test04_ai_behavior.py
index 7845733a..276b9283 100644
--- a/tests/functional/test04_ai_behavior.py
+++ b/tests/functional/test04_ai_behavior.py
@@ -16,26 +16,40 @@ _gateway_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".
if _gateway_path not in sys.path:
sys.path.insert(0, _gateway_path)
-# Import the service initialization
-from modules.serviceCenter.serviceHub import getInterface as getServices
+from modules.serviceCenter import getService
+from modules.serviceCenter.context import ServiceCenterContext
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum
from modules.datamodels.datamodelUam import User
from modules.datamodels.datamodelWorkflow import AiResponse
-# The test uses the AI service which handles JSON template internally
+
+class _TestServicesBag:
+ """Mutable services bag for tests — lazy-resolves via getService, allows attribute overrides."""
+ def __init__(self, ctx):
+ self._ctx = ctx
+ self.user = ctx.user
+ self.mandateId = ctx.mandate_id
+ self.featureInstanceId = ctx.feature_instance_id
+ self.workflow = ctx.workflow
+
+ def __getattr__(self, name):
+ if name.startswith("_"):
+ raise AttributeError(name)
+ svc = getService(name, self._ctx)
+ setattr(self, name, svc)
+ return svc
+
class AIBehaviorTester:
def __init__(self):
- # Use root user for testing (has full access to everything)
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelUam import Mandate
rootInterface = getRootInterface()
self.testUser = rootInterface.currentUser
- # Get initial mandate ID for testing (User has no mandateId - use initial mandate)
self.testMandateId = rootInterface.getInitialId(Mandate)
- # Initialize services using the existing system
- self.services = getServices(self.testUser, None) # Test user, no workflow
+ ctx = ServiceCenterContext(user=self.testUser)
+ self.services = _TestServicesBag(ctx)
self.testResults = []
async def initialize(self):
diff --git a/tests/unit/workflow/test_extract_content_handover.py b/tests/unit/workflow/test_extract_content_handover.py
index 9153f350..8e8f409c 100644
--- a/tests/unit/workflow/test_extract_content_handover.py
+++ b/tests/unit/workflow/test_extract_content_handover.py
@@ -395,14 +395,14 @@ def test_action_result_contract_new_extract_payload_keys():
def test_automation_workspace_suppresses_extract_artifacts():
- from modules.workflowAutomation.engine.workflowArtifactVisibility import suppress_workflow_file_in_workspace_ui
+ from modules.shared.workflowArtifactVisibility import suppressWorkflowFileInWorkspaceUi
- assert suppress_workflow_file_in_workspace_ui({"fileName": "extracted_content_transient-abc_99.json"})
- assert suppress_workflow_file_in_workspace_ui({"fileName": "extract_media_stem_uuid.png"})
- assert not suppress_workflow_file_in_workspace_ui({"fileName": "export_2026.csv"})
- assert suppress_workflow_file_in_workspace_ui({"fileName": "", "suppressInWorkflowFileLists": True})
- assert suppress_workflow_file_in_workspace_ui({"fileName": "report.pdf", "tags": ["_workflowInternal"]})
- assert not suppress_workflow_file_in_workspace_ui({"fileName": "report.pdf", "tags": ["invoice"]})
+ assert suppressWorkflowFileInWorkspaceUi({"fileName": "extracted_content_transient-abc_99.json"})
+ assert suppressWorkflowFileInWorkspaceUi({"fileName": "extract_media_stem_uuid.png"})
+ assert not suppressWorkflowFileInWorkspaceUi({"fileName": "export_2026.csv"})
+ assert suppressWorkflowFileInWorkspaceUi({"fileName": "", "suppressInWorkflowFileLists": True})
+ assert suppressWorkflowFileInWorkspaceUi({"fileName": "report.pdf", "tags": ["_workflowInternal"]})
+ assert not suppressWorkflowFileInWorkspaceUi({"fileName": "report.pdf", "tags": ["invoice"]})
def test_normalize_presentation_envelopes_action_result_and_list():