grapheditor editor and template handling
This commit is contained in:
parent
f65223137e
commit
13242fa5ac
8 changed files with 244 additions and 58 deletions
13
app.py
13
app.py
|
|
@ -244,6 +244,8 @@ def initLogging():
|
||||||
"fastapi.security.oauth2",
|
"fastapi.security.oauth2",
|
||||||
"msal",
|
"msal",
|
||||||
"azure.core.pipeline.policies.http_logging_policy",
|
"azure.core.pipeline.policies.http_logging_policy",
|
||||||
|
"stripe",
|
||||||
|
"apscheduler",
|
||||||
]
|
]
|
||||||
for loggerName in noisyLoggers:
|
for loggerName in noisyLoggers:
|
||||||
logging.getLogger(loggerName).setLevel(logging.WARNING)
|
logging.getLogger(loggerName).setLevel(logging.WARNING)
|
||||||
|
|
@ -294,16 +296,7 @@ except Exception as e:
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
logger.info("Application is starting up")
|
logger.info("Application is starting up")
|
||||||
|
|
||||||
# --- Pre-warm AI connectors FIRST (before any other startup work) ---
|
# AI connectors already pre-warmed at module-load via _eager_prewarm() in aicoreModelRegistry.
|
||||||
# Avoids 4–8 s latency on first chatbot request; must run before first use.
|
|
||||||
try:
|
|
||||||
import modules.aicore.aicoreModelRegistry # noqa: F401 - triggers eager pre-warm
|
|
||||||
from modules.aicore.aicoreModelRegistry import modelRegistry
|
|
||||||
modelRegistry.ensureConnectorsRegistered()
|
|
||||||
modelRegistry.refreshModels(force=True)
|
|
||||||
logger.info("AI connectors and model registry pre-warmed")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"AI pre-warm failed: {e}")
|
|
||||||
|
|
||||||
# Bootstrap database if needed (creates initial users, mandates, roles, etc.)
|
# Bootstrap database if needed (creates initial users, mandates, roles, etc.)
|
||||||
# This must happen before getting root interface
|
# This must happen before getting root interface
|
||||||
|
|
|
||||||
|
|
@ -556,7 +556,9 @@ class GraphicalEditorObjects:
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
def getTemplates(self, scope: str = None) -> List[Dict[str, Any]]:
|
def getTemplates(self, scope: str = None) -> List[Dict[str, Any]]:
|
||||||
"""Get workflow templates, optionally filtered by scope."""
|
"""Get workflow templates, optionally filtered by scope.
|
||||||
|
Always includes system-scope templates (mandateId=None) alongside mandate-owned ones.
|
||||||
|
"""
|
||||||
if not self.db._ensureTableExists(AutoWorkflow):
|
if not self.db._ensureTableExists(AutoWorkflow):
|
||||||
return []
|
return []
|
||||||
rf: Dict[str, Any] = {
|
rf: Dict[str, Any] = {
|
||||||
|
|
@ -566,7 +568,21 @@ class GraphicalEditorObjects:
|
||||||
}
|
}
|
||||||
if scope:
|
if scope:
|
||||||
rf["templateScope"] = scope
|
rf["templateScope"] = scope
|
||||||
records = self.db.getRecordset(AutoWorkflow, recordFilter=rf)
|
records = self.db.getRecordset(AutoWorkflow, recordFilter=rf) or []
|
||||||
|
|
||||||
|
if scope is None or scope == "system":
|
||||||
|
systemFilter: Dict[str, Any] = {
|
||||||
|
"isTemplate": True,
|
||||||
|
"templateScope": "system",
|
||||||
|
"mandateId": None,
|
||||||
|
}
|
||||||
|
systemRecords = self.db.getRecordset(AutoWorkflow, recordFilter=systemFilter) or []
|
||||||
|
seenIds = {(r.get("id") if isinstance(r, dict) else getattr(r, "id", None)) for r in records}
|
||||||
|
for sr in systemRecords:
|
||||||
|
srId = sr.get("id") if isinstance(sr, dict) else getattr(sr, "id", None)
|
||||||
|
if srId not in seenIds:
|
||||||
|
records.append(sr)
|
||||||
|
|
||||||
return [dict(r) for r in records] if records else []
|
return [dict(r) for r in records] if records else []
|
||||||
|
|
||||||
def createTemplateFromWorkflow(self, workflowId: str, scope: str = "user") -> Optional[Dict[str, Any]]:
|
def createTemplateFromWorkflow(self, workflowId: str, scope: str = "user") -> Optional[Dict[str, Any]]:
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from .email import EMAIL_NODES
|
||||||
from .sharepoint import SHAREPOINT_NODES
|
from .sharepoint import SHAREPOINT_NODES
|
||||||
from .clickup import CLICKUP_NODES
|
from .clickup import CLICKUP_NODES
|
||||||
from .file import FILE_NODES
|
from .file import FILE_NODES
|
||||||
|
from .trustee import TRUSTEE_NODES
|
||||||
|
|
||||||
STATIC_NODE_TYPES = (
|
STATIC_NODE_TYPES = (
|
||||||
TRIGGER_NODES
|
TRIGGER_NODES
|
||||||
|
|
@ -19,4 +20,5 @@ STATIC_NODE_TYPES = (
|
||||||
+ SHAREPOINT_NODES
|
+ SHAREPOINT_NODES
|
||||||
+ CLICKUP_NODES
|
+ CLICKUP_NODES
|
||||||
+ FILE_NODES
|
+ FILE_NODES
|
||||||
|
+ TRUSTEE_NODES
|
||||||
)
|
)
|
||||||
|
|
|
||||||
68
modules/features/graphicalEditor/nodeDefinitions/trustee.py
Normal file
68
modules/features/graphicalEditor/nodeDefinitions/trustee.py
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Copyright (c) 2025 Patrick Motsch
|
||||||
|
# Trustee node definitions - map to methodTrustee actions.
|
||||||
|
# Pipeline: extractFromFiles -> processDocuments -> syncToAccounting.
|
||||||
|
|
||||||
|
TRUSTEE_NODES = [
|
||||||
|
{
|
||||||
|
"id": "trustee.extractFromFiles",
|
||||||
|
"category": "trustee",
|
||||||
|
"label": {"en": "Extract Documents", "de": "Dokumente extrahieren", "fr": "Extraire documents"},
|
||||||
|
"description": {
|
||||||
|
"en": "Extract document type and data from PDF/JPG via AI (from fileIds or SharePoint folder)",
|
||||||
|
"de": "Dokumenttyp und Daten aus PDF/JPG per AI extrahieren (aus Dateien oder SharePoint-Ordner)",
|
||||||
|
"fr": "Extraire type et données de PDF/JPG par IA",
|
||||||
|
},
|
||||||
|
"parameters": [
|
||||||
|
{"name": "connectionId", "type": "string", "required": False, "description": {"en": "SharePoint connection (if reading from SharePoint)", "de": "SharePoint-Verbindung (falls aus SharePoint)", "fr": "Connexion SharePoint"}, "default": ""},
|
||||||
|
{"name": "sharepointFolder", "type": "string", "required": False, "description": {"en": "SharePoint folder path (e.g. /sites/MySite/Documents/Expenses)", "de": "SharePoint-Ordnerpfad", "fr": "Chemin dossier SharePoint"}, "default": ""},
|
||||||
|
{"name": "featureInstanceId", "type": "string", "required": True, "description": {"en": "Trustee feature instance ID", "de": "Trustee Feature-Instanz-ID", "fr": "ID instance Trustee"}},
|
||||||
|
{"name": "prompt", "type": "string", "required": False, "description": {"en": "AI prompt for extraction (optional)", "de": "AI-Prompt für Extraktion (optional)", "fr": "Prompt IA pour extraction"}, "default": ""},
|
||||||
|
],
|
||||||
|
"inputs": 1,
|
||||||
|
"outputs": 1,
|
||||||
|
"meta": {"icon": "mdi-file-document-scan", "color": "#4CAF50"},
|
||||||
|
"_method": "trustee",
|
||||||
|
"_action": "extractFromFiles",
|
||||||
|
"_paramMap": {"connectionId": "connectionReference", "sharepointFolder": "sharepointFolder", "featureInstanceId": "featureInstanceId", "prompt": "prompt"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trustee.processDocuments",
|
||||||
|
"category": "trustee",
|
||||||
|
"label": {"en": "Process Documents", "de": "Dokumente verarbeiten", "fr": "Traiter documents"},
|
||||||
|
"description": {
|
||||||
|
"en": "Create TrusteeDocument + TrusteePosition from extraction result",
|
||||||
|
"de": "TrusteeDocument + TrusteePosition aus Extraktionsergebnis erstellen",
|
||||||
|
"fr": "Créer TrusteeDocument + TrusteePosition à partir du résultat",
|
||||||
|
},
|
||||||
|
"parameters": [
|
||||||
|
{"name": "documentList", "type": "string", "required": True, "description": {"en": "Reference to extractFromFiles result", "de": "Referenz auf extractFromFiles-Ergebnis", "fr": "Référence au résultat extractFromFiles"}},
|
||||||
|
{"name": "featureInstanceId", "type": "string", "required": True, "description": {"en": "Trustee feature instance ID", "de": "Trustee Feature-Instanz-ID", "fr": "ID instance Trustee"}},
|
||||||
|
],
|
||||||
|
"inputs": 1,
|
||||||
|
"outputs": 1,
|
||||||
|
"meta": {"icon": "mdi-file-document-check", "color": "#4CAF50"},
|
||||||
|
"_method": "trustee",
|
||||||
|
"_action": "processDocuments",
|
||||||
|
"_paramMap": {"documentList": "documentList", "featureInstanceId": "featureInstanceId"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trustee.syncToAccounting",
|
||||||
|
"category": "trustee",
|
||||||
|
"label": {"en": "Sync to Accounting", "de": "In Buchhaltung synchronisieren", "fr": "Synchroniser comptabilité"},
|
||||||
|
"description": {
|
||||||
|
"en": "Push trustee positions to accounting system",
|
||||||
|
"de": "Trustee-Positionen in Buchhaltungssystem übertragen",
|
||||||
|
"fr": "Transférer les positions vers la comptabilité",
|
||||||
|
},
|
||||||
|
"parameters": [
|
||||||
|
{"name": "documentList", "type": "string", "required": True, "description": {"en": "Reference to processDocuments result", "de": "Referenz auf processDocuments-Ergebnis", "fr": "Référence au résultat processDocuments"}},
|
||||||
|
{"name": "featureInstanceId", "type": "string", "required": True, "description": {"en": "Trustee feature instance ID", "de": "Trustee Feature-Instanz-ID", "fr": "ID instance Trustee"}},
|
||||||
|
],
|
||||||
|
"inputs": 1,
|
||||||
|
"outputs": 1,
|
||||||
|
"meta": {"icon": "mdi-calculator", "color": "#4CAF50"},
|
||||||
|
"_method": "trustee",
|
||||||
|
"_action": "syncToAccounting",
|
||||||
|
"_paramMap": {"documentList": "documentList", "featureInstanceId": "featureInstanceId"},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
@ -470,6 +470,9 @@ async def post_editor_chat(
|
||||||
|
|
||||||
userLanguage = body.get("userLanguage", "de")
|
userLanguage = body.get("userLanguage", "de")
|
||||||
conversationHistory = body.get("conversationHistory") or []
|
conversationHistory = body.get("conversationHistory") or []
|
||||||
|
fileIds = body.get("fileIds") or []
|
||||||
|
dataSourceIds = body.get("dataSourceIds") or []
|
||||||
|
featureDataSourceIds = body.get("featureDataSourceIds") or []
|
||||||
|
|
||||||
from modules.serviceCenter.core.serviceStreaming import get_event_manager
|
from modules.serviceCenter.core.serviceStreaming import get_event_manager
|
||||||
sseEventManager = get_event_manager()
|
sseEventManager = get_event_manager()
|
||||||
|
|
@ -487,6 +490,9 @@ async def post_editor_chat(
|
||||||
sseEventManager=sseEventManager,
|
sseEventManager=sseEventManager,
|
||||||
userLanguage=userLanguage,
|
userLanguage=userLanguage,
|
||||||
conversationHistory=conversationHistory,
|
conversationHistory=conversationHistory,
|
||||||
|
fileIds=fileIds,
|
||||||
|
dataSourceIds=dataSourceIds,
|
||||||
|
featureDataSourceIds=featureDataSourceIds,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
sseEventManager.register_agent_task(queueId, agentTask)
|
sseEventManager.register_agent_task(queueId, agentTask)
|
||||||
|
|
@ -531,6 +537,9 @@ async def _runEditorAgent(
|
||||||
sseEventManager=None,
|
sseEventManager=None,
|
||||||
userLanguage: str = "de",
|
userLanguage: str = "de",
|
||||||
conversationHistory: List[Dict[str, Any]] = None,
|
conversationHistory: List[Dict[str, Any]] = None,
|
||||||
|
fileIds: List[str] = None,
|
||||||
|
dataSourceIds: List[str] = None,
|
||||||
|
featureDataSourceIds: List[str] = None,
|
||||||
):
|
):
|
||||||
"""Run the serviceAgent loop with workflow toolbox and forward events to the SSE queue."""
|
"""Run the serviceAgent loop with workflow toolbox and forward events to the SSE queue."""
|
||||||
try:
|
try:
|
||||||
|
|
@ -555,10 +564,25 @@ async def _runEditorAgent(
|
||||||
"Respond concisely and confirm what you changed."
|
"Respond concisely and confirm what you changed."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
enrichedPrompt = prompt
|
||||||
|
if dataSourceIds:
|
||||||
|
from modules.features.workspace.routeFeatureWorkspace import _buildDataSourceContext
|
||||||
|
chatSvc = getService("chat", ctx)
|
||||||
|
dsInfo = _buildDataSourceContext(chatSvc, dataSourceIds)
|
||||||
|
if dsInfo:
|
||||||
|
enrichedPrompt = f"{prompt}\n\n[Active Data Sources]\n{dsInfo}"
|
||||||
|
|
||||||
|
if featureDataSourceIds:
|
||||||
|
from modules.features.workspace.routeFeatureWorkspace import _buildFeatureDataSourceContext
|
||||||
|
fdsInfo = _buildFeatureDataSourceContext(featureDataSourceIds)
|
||||||
|
if fdsInfo:
|
||||||
|
enrichedPrompt = f"{enrichedPrompt}\n\n[Attached Feature Data Sources]\n{fdsInfo}"
|
||||||
|
|
||||||
accumulatedText = ""
|
accumulatedText = ""
|
||||||
|
|
||||||
async for event in agentService.runAgent(
|
async for event in agentService.runAgent(
|
||||||
prompt=prompt,
|
prompt=enrichedPrompt,
|
||||||
|
fileIds=fileIds or [],
|
||||||
workflowId=workflowId,
|
workflowId=workflowId,
|
||||||
userLanguage=userLanguage,
|
userLanguage=userLanguage,
|
||||||
conversationHistory=conversationHistory or [],
|
conversationHistory=conversationHistory or [],
|
||||||
|
|
|
||||||
|
|
@ -1221,17 +1221,15 @@ async def listWorkspaceDataSources(
|
||||||
instanceId: str = Path(...),
|
instanceId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext),
|
context: RequestContext = Depends(getRequestContext),
|
||||||
):
|
):
|
||||||
_validateInstanceAccess(instanceId, context)
|
wsMandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||||
try:
|
try:
|
||||||
from modules.serviceCenter import getService
|
from modules.datamodels.datamodelDataSource import DataSource
|
||||||
from modules.serviceCenter.context import ServiceCenterContext
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
ctx = ServiceCenterContext(
|
rootIf = getRootInterface()
|
||||||
user=context.user,
|
recordFilter: dict = {"featureInstanceId": instanceId}
|
||||||
mandate_id=str(context.mandateId) if context.mandateId else None,
|
if wsMandateId:
|
||||||
feature_instance_id=instanceId,
|
recordFilter["mandateId"] = wsMandateId
|
||||||
)
|
dataSources = rootIf.db.getRecordset(DataSource, recordFilter=recordFilter)
|
||||||
chatService = getService("chat", ctx)
|
|
||||||
dataSources = chatService.listDataSources(featureInstanceId=instanceId)
|
|
||||||
return JSONResponse({"dataSources": dataSources or []})
|
return JSONResponse({"dataSources": dataSources or []})
|
||||||
except Exception:
|
except Exception:
|
||||||
return JSONResponse({"dataSources": []})
|
return JSONResponse({"dataSources": []})
|
||||||
|
|
@ -1642,16 +1640,16 @@ async def listFeatureDataSources(
|
||||||
instanceId: str = Path(...),
|
instanceId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext),
|
context: RequestContext = Depends(getRequestContext),
|
||||||
):
|
):
|
||||||
"""List active FeatureDataSources for this workspace instance."""
|
"""List active FeatureDataSources for this workspace instance, scoped to mandate."""
|
||||||
_validateInstanceAccess(instanceId, context)
|
wsMandateId, _ = _validateInstanceAccess(instanceId, context)
|
||||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
from modules.datamodels.datamodelFeatureDataSource import FeatureDataSource
|
from modules.datamodels.datamodelFeatureDataSource import FeatureDataSource
|
||||||
|
|
||||||
rootIf = getRootInterface()
|
rootIf = getRootInterface()
|
||||||
records = rootIf.db.getRecordset(
|
recordFilter: dict = {"workspaceInstanceId": instanceId}
|
||||||
FeatureDataSource,
|
if wsMandateId:
|
||||||
recordFilter={"workspaceInstanceId": instanceId},
|
recordFilter["mandateId"] = wsMandateId
|
||||||
)
|
records = rootIf.db.getRecordset(FeatureDataSource, recordFilter=recordFilter)
|
||||||
return JSONResponse({"featureDataSources": records or []})
|
return JSONResponse({"featureDataSources": records or []})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,13 +38,21 @@ pwdContext = CryptContext(schemes=["argon2"], deprecated="auto")
|
||||||
# Cache für Role-IDs (roleLabel -> roleId)
|
# Cache für Role-IDs (roleLabel -> roleId)
|
||||||
_roleIdCache: Dict[str, str] = {}
|
_roleIdCache: Dict[str, str] = {}
|
||||||
|
|
||||||
|
_bootstrapDone: bool = False
|
||||||
|
|
||||||
def initBootstrap(db: DatabaseConnector) -> None:
|
def initBootstrap(db: DatabaseConnector) -> None:
|
||||||
"""
|
"""
|
||||||
Main bootstrap entry point - initializes all system components.
|
Main bootstrap entry point - initializes all system components.
|
||||||
|
Idempotent: runs only once per process regardless of how many callers invoke it.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db: Database connector instance
|
db: Database connector instance
|
||||||
"""
|
"""
|
||||||
|
global _bootstrapDone
|
||||||
|
if _bootstrapDone:
|
||||||
|
return
|
||||||
|
_bootstrapDone = True
|
||||||
|
|
||||||
logger.info("Starting system bootstrap")
|
logger.info("Starting system bootstrap")
|
||||||
|
|
||||||
# Initialize root mandate
|
# Initialize root mandate
|
||||||
|
|
@ -144,6 +152,112 @@ def initBootstrap(db: DatabaseConnector) -> None:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Mandate retention purge failed: {e}")
|
logger.warning(f"Mandate retention purge failed: {e}")
|
||||||
|
|
||||||
|
# Bootstrap system workflow templates for graphical editor
|
||||||
|
_bootstrapSystemTemplates(db)
|
||||||
|
|
||||||
|
|
||||||
|
def _bootstrapSystemTemplates(db: DatabaseConnector) -> None:
|
||||||
|
"""
|
||||||
|
Seed platform-wide workflow templates (templateScope='system', mandateId=None).
|
||||||
|
Idempotent: skips if templates with the same label already exist.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from modules.features.graphicalEditor.datamodelFeatureGraphicalEditor import AutoWorkflow
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
greenfieldDb = DatabaseConnector(
|
||||||
|
dbHost=APP_CONFIG.get("DB_HOST", "localhost"),
|
||||||
|
dbDatabase="poweron_graphicaleditor",
|
||||||
|
dbUser=APP_CONFIG.get("DB_USER"),
|
||||||
|
dbPassword=APP_CONFIG.get("DB_PASSWORD_SECRET") or APP_CONFIG.get("DB_PASSWORD"),
|
||||||
|
)
|
||||||
|
greenfieldDb._ensureTableExists(AutoWorkflow)
|
||||||
|
|
||||||
|
existing = greenfieldDb.getRecordset(AutoWorkflow, recordFilter={
|
||||||
|
"isTemplate": True,
|
||||||
|
"templateScope": "system",
|
||||||
|
})
|
||||||
|
existingLabels = {r.get("label") if isinstance(r, dict) else getattr(r, "label", "") for r in (existing or [])}
|
||||||
|
|
||||||
|
templates = _buildSystemTemplates()
|
||||||
|
created = 0
|
||||||
|
for tpl in templates:
|
||||||
|
if tpl["label"] in existingLabels:
|
||||||
|
continue
|
||||||
|
tpl["id"] = str(uuid.uuid4())
|
||||||
|
greenfieldDb.recordCreate(AutoWorkflow, tpl)
|
||||||
|
created += 1
|
||||||
|
|
||||||
|
if created:
|
||||||
|
logger.info(f"Bootstrapped {created} system workflow template(s)")
|
||||||
|
greenfieldDb.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"System workflow template bootstrap failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _buildSystemTemplates():
|
||||||
|
"""Build the graph definitions for platform system templates."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"label": "Personal Assistant: E-Mail-Antwort-Drafting",
|
||||||
|
"mandateId": None,
|
||||||
|
"featureInstanceId": None,
|
||||||
|
"isTemplate": True,
|
||||||
|
"templateScope": "system",
|
||||||
|
"sharedReadOnly": True,
|
||||||
|
"active": False,
|
||||||
|
"graph": {
|
||||||
|
"nodes": [
|
||||||
|
{"id": "n1", "type": "trigger.schedule", "x": 50, "y": 200, "title": "Täglicher Check", "parameters": {}},
|
||||||
|
{"id": "n2", "type": "email.checkEmail", "x": 300, "y": 200, "title": "Mailbox prüfen", "parameters": {}},
|
||||||
|
{"id": "n3", "type": "flow.loop", "x": 550, "y": 200, "title": "Pro E-Mail", "parameters": {}},
|
||||||
|
{"id": "n4", "type": "ai.prompt", "x": 800, "y": 200, "title": "Analyse: Antwort nötig?", "parameters": {}},
|
||||||
|
{"id": "n5", "type": "flow.ifElse", "x": 1050, "y": 200, "title": "Antwort nötig?", "parameters": {}},
|
||||||
|
{"id": "n6", "type": "ai.prompt", "x": 1300, "y": 100, "title": "Kontext abrufen & Antwort formulieren", "parameters": {}},
|
||||||
|
{"id": "n7", "type": "email.draftEmail", "x": 1550, "y": 100, "title": "Draft erstellen", "parameters": {}},
|
||||||
|
],
|
||||||
|
"connections": [
|
||||||
|
{"source": "n1", "target": "n2", "sourceOutput": 0, "targetInput": 0},
|
||||||
|
{"source": "n2", "target": "n3", "sourceOutput": 0, "targetInput": 0},
|
||||||
|
{"source": "n3", "target": "n4", "sourceOutput": 0, "targetInput": 0},
|
||||||
|
{"source": "n4", "target": "n5", "sourceOutput": 0, "targetInput": 0},
|
||||||
|
{"source": "n5", "target": "n6", "sourceOutput": 0, "targetInput": 0},
|
||||||
|
{"source": "n6", "target": "n7", "sourceOutput": 0, "targetInput": 0},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"invocations": [{"type": "schedule", "cronExpression": "0 8 * * 1-5"}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Treuhand: PDF-Klassifizierung & Trustee-Import",
|
||||||
|
"mandateId": None,
|
||||||
|
"featureInstanceId": None,
|
||||||
|
"isTemplate": True,
|
||||||
|
"templateScope": "system",
|
||||||
|
"sharedReadOnly": True,
|
||||||
|
"active": False,
|
||||||
|
"graph": {
|
||||||
|
"nodes": [
|
||||||
|
{"id": "n1", "type": "trigger.schedule", "x": 50, "y": 200, "title": "Geplanter Import", "parameters": {}},
|
||||||
|
{"id": "n2", "type": "sharepoint.listFiles", "x": 300, "y": 200, "title": "SharePoint Ordner lesen", "parameters": {}},
|
||||||
|
{"id": "n3", "type": "flow.loop", "x": 550, "y": 200, "title": "Pro Dokument", "parameters": {}},
|
||||||
|
{"id": "n4", "type": "sharepoint.readFile", "x": 800, "y": 200, "title": "PDF-Inhalt lesen", "parameters": {}},
|
||||||
|
{"id": "n5", "type": "ai.prompt", "x": 1050, "y": 200, "title": "Typ klassifizieren (Rechnung, Beleg, Bankauszug, Vertrag, etc.)", "parameters": {}},
|
||||||
|
{"id": "n6", "type": "trustee.extractFromFiles", "x": 1300, "y": 200, "title": "Dokument extrahieren", "parameters": {}},
|
||||||
|
{"id": "n7", "type": "trustee.processDocuments", "x": 1550, "y": 200, "title": "In Trustee einlesen", "parameters": {}},
|
||||||
|
],
|
||||||
|
"connections": [
|
||||||
|
{"source": "n1", "target": "n2", "sourceOutput": 0, "targetInput": 0},
|
||||||
|
{"source": "n2", "target": "n3", "sourceOutput": 0, "targetInput": 0},
|
||||||
|
{"source": "n3", "target": "n4", "sourceOutput": 0, "targetInput": 0},
|
||||||
|
{"source": "n4", "target": "n5", "sourceOutput": 0, "targetInput": 0},
|
||||||
|
{"source": "n5", "target": "n6", "sourceOutput": 0, "targetInput": 0},
|
||||||
|
{"source": "n6", "target": "n7", "sourceOutput": 0, "targetInput": 0},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"invocations": [{"type": "schedule", "cronExpression": "0 7 * * 1-5"}],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def initRootMandateFeatures(db: DatabaseConnector, mandateId: str) -> None:
|
def initRootMandateFeatures(db: DatabaseConnector, mandateId: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import uuid
|
||||||
from modules.connectors.connectorDbPostgre import DatabaseConnector, _get_cached_connector
|
from modules.connectors.connectorDbPostgre import DatabaseConnector, _get_cached_connector
|
||||||
from modules.shared.configuration import APP_CONFIG
|
from modules.shared.configuration import APP_CONFIG
|
||||||
from modules.shared.timeUtils import getUtcTimestamp, parseTimestamp
|
from modules.shared.timeUtils import getUtcTimestamp, parseTimestamp
|
||||||
from modules.interfaces.interfaceBootstrap import initBootstrap
|
|
||||||
from modules.interfaces.interfaceRbac import getRecordsetWithRBAC
|
from modules.interfaces.interfaceRbac import getRecordsetWithRBAC
|
||||||
from modules.security.rbac import RbacClass
|
from modules.security.rbac import RbacClass
|
||||||
from modules.datamodels.datamodelUam import (
|
from modules.datamodels.datamodelUam import (
|
||||||
|
|
@ -55,8 +54,6 @@ _gatewayInterfaces = {}
|
||||||
# Root interface instance
|
# Root interface instance
|
||||||
_rootAppObjects = None
|
_rootAppObjects = None
|
||||||
|
|
||||||
# Bootstrap completion flag - ensures bootstrap runs only ONCE per application lifecycle
|
|
||||||
_bootstrapCompleted = False
|
|
||||||
|
|
||||||
# Password-Hashing
|
# Password-Hashing
|
||||||
pwdContext = CryptContext(schemes=["argon2"], deprecated="auto")
|
pwdContext = CryptContext(schemes=["argon2"], deprecated="auto")
|
||||||
|
|
@ -79,9 +76,6 @@ class AppObjects:
|
||||||
# Initialize database
|
# Initialize database
|
||||||
self._initializeDatabase()
|
self._initializeDatabase()
|
||||||
|
|
||||||
# Initialize standard records if needed
|
|
||||||
self._initRecords()
|
|
||||||
|
|
||||||
# Set user context if provided
|
# Set user context if provided
|
||||||
if currentUser:
|
if currentUser:
|
||||||
self.setUserContext(currentUser)
|
self.setUserContext(currentUser)
|
||||||
|
|
@ -195,29 +189,6 @@ class AppObjects:
|
||||||
|
|
||||||
return simpleFields, objectFields
|
return simpleFields, objectFields
|
||||||
|
|
||||||
def _initRecords(self):
|
|
||||||
"""Initialize standard records if they don't exist.
|
|
||||||
|
|
||||||
Uses a global flag to ensure bootstrap only runs ONCE per application lifecycle.
|
|
||||||
The flag is set BEFORE calling bootstrap to prevent recursive calls during bootstrap.
|
|
||||||
"""
|
|
||||||
global _bootstrapCompleted
|
|
||||||
|
|
||||||
if _bootstrapCompleted:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Set flag BEFORE bootstrap to prevent recursive calls during bootstrap
|
|
||||||
_bootstrapCompleted = True
|
|
||||||
logger.info("Starting bootstrap (will only run once)")
|
|
||||||
|
|
||||||
try:
|
|
||||||
initBootstrap(self.db)
|
|
||||||
logger.info("Bootstrap completed successfully")
|
|
||||||
except Exception as e:
|
|
||||||
# Reset flag on failure so bootstrap can be retried
|
|
||||||
_bootstrapCompleted = False
|
|
||||||
logger.error(f"Bootstrap failed: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def checkRbacPermission(
|
def checkRbacPermission(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue