cleaned servicebag and removed servicehub
This commit is contained in:
parent
c1655bdd0a
commit
4f8473bd70
66 changed files with 814 additions and 948 deletions
11
app.py
11
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class AutoTemplateScope(str, Enum):
|
|||
SYSTEM = "system"
|
||||
|
||||
|
||||
GRAPHICAL_EDITOR_DATABASE = "poweron_graphicaleditor"
|
||||
WORKFLOW_AUTOMATION_DATABASE = "poweron_graphicaleditor"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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}'",
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
32
modules/shared/systemComponentRegistry.py
Normal file
32
modules/shared/systemComponentRegistry.py
Normal file
|
|
@ -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, []))
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())}"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)}")
|
||||
|
|
|
|||
|
|
@ -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)}")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
Loading…
Reference in a new issue