access extracted from interface and all routes in separate modules
This commit is contained in:
parent
8ea05780d1
commit
5d78faf7ff
14 changed files with 392 additions and 1330 deletions
75
app.py
75
app.py
|
|
@ -102,7 +102,7 @@ app = FastAPI(
|
||||||
# CORS configuration using environment variables
|
# CORS configuration using environment variables
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins= get_allowed_origins(), # ["http://localhost:8080","http://localhost:8081"], #get_allowed_origins(),
|
allow_origins= get_allowed_origins(),
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|
@ -118,80 +118,16 @@ os.makedirs(staticFolder, exist_ok=True)
|
||||||
# Mount static files with proper configuration
|
# Mount static files with proper configuration
|
||||||
app.mount("/static", StaticFiles(directory=str(staticFolder), html=True), name="static")
|
app.mount("/static", StaticFiles(directory=str(staticFolder), html=True), name="static")
|
||||||
|
|
||||||
# Add favicon route
|
|
||||||
@app.get("/favicon.ico")
|
|
||||||
async def favicon():
|
|
||||||
return FileResponse(str(staticFolder / "favicon.ico"), media_type="image/x-icon")
|
|
||||||
|
|
||||||
# General Elements
|
|
||||||
@app.get("/", tags=["General"])
|
|
||||||
async def root():
|
|
||||||
"""API status endpoint"""
|
|
||||||
return {"status": "online", "message": "Data Platform API is active"}
|
|
||||||
|
|
||||||
@app.get("/api/test", tags=["General"])
|
|
||||||
async def getTest():
|
|
||||||
return f"Status: OK. Alowed origins: {APP_CONFIG.get('APP_ALLOWED_ORIGINS')}"
|
|
||||||
|
|
||||||
@app.options("/{fullPath:path}", tags=["General"])
|
|
||||||
async def optionsRoute(fullPath: str):
|
|
||||||
return Response(status_code=200)
|
|
||||||
|
|
||||||
@app.get("/api/environment", tags=["General"])
|
|
||||||
async def get_environment():
|
|
||||||
"""Get environment configuration for frontend"""
|
|
||||||
return {
|
|
||||||
"apiBaseUrl": APP_CONFIG.get("APP_API_URL", ""),
|
|
||||||
"environment": APP_CONFIG.get("APP_ENV", "development"),
|
|
||||||
"instanceLabel": APP_CONFIG.get("APP_ENV_LABEL", "Development"),
|
|
||||||
# Add other environment variables the frontend might need
|
|
||||||
}
|
|
||||||
|
|
||||||
# Token endpoint for login
|
|
||||||
@app.post("/api/token", response_model=gatewayModel.Token, tags=["General"])
|
|
||||||
async def loginForAccessToken(formData: OAuth2PasswordRequestForm = Depends()):
|
|
||||||
# Initialize Gateway interface without context
|
|
||||||
gateway = getGatewayInterface()
|
|
||||||
|
|
||||||
# Authenticate user
|
|
||||||
user = gateway.authenticateUser(formData.username, formData.password)
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="Invalid username or password",
|
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create token with tenant ID
|
|
||||||
accessTokenExpires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
||||||
accessToken = createAccessToken(
|
|
||||||
data={
|
|
||||||
"sub": user["username"],
|
|
||||||
"mandateId": user["mandateId"]
|
|
||||||
},
|
|
||||||
expiresDelta=accessTokenExpires
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"accessToken": accessToken, "tokenType": "bearer"}
|
|
||||||
|
|
||||||
# Get user info
|
|
||||||
@app.get("/api/user/me", response_model=Dict[str, Any], tags=["General"])
|
|
||||||
async def readUserMe(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
|
|
||||||
return currentUser
|
|
||||||
|
|
||||||
# Include all routers
|
# Include all routers
|
||||||
|
from routes.routeGeneral import router as generalRouter
|
||||||
|
app.include_router(generalRouter)
|
||||||
|
|
||||||
from routes.routeAttributes import router as attributesRouter
|
from routes.routeAttributes import router as attributesRouter
|
||||||
app.include_router(attributesRouter)
|
app.include_router(attributesRouter)
|
||||||
|
|
||||||
gateway = getGatewayInterface()
|
|
||||||
|
|
||||||
from routes.routeMandates import router as mandateRouter
|
from routes.routeMandates import router as mandateRouter
|
||||||
app.include_router(mandateRouter)
|
app.include_router(mandateRouter)
|
||||||
|
|
||||||
gateway = getGatewayInterface()
|
|
||||||
|
|
||||||
|
|
||||||
from routes.routeUsers import router as userRouter
|
from routes.routeUsers import router as userRouter
|
||||||
app.include_router(userRouter)
|
app.include_router(userRouter)
|
||||||
|
|
||||||
|
|
@ -206,6 +142,3 @@ app.include_router(workflowRouter)
|
||||||
|
|
||||||
from routes.routeMsft import router as msftRouter
|
from routes.routeMsft import router as msftRouter
|
||||||
app.include_router(msftRouter)
|
app.include_router(msftRouter)
|
||||||
|
|
||||||
#if __name__ == "__main__":
|
|
||||||
# uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)
|
|
||||||
111
modules/gatewayAccess.py
Normal file
111
modules/gatewayAccess.py
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
"""
|
||||||
|
Access control functions for the Gateway system.
|
||||||
|
Manages user access and permissions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
|
def _uam(currentUser: Dict[str, Any], table: str, recordset: List[Dict[str, Any]], mandateId: int, userId: int, db) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Unified user access management function that filters data based on user privileges
|
||||||
|
and adds access control attributes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
currentUser: Current user information dictionary
|
||||||
|
table: Name of the table
|
||||||
|
recordset: Recordset to filter based on access rules
|
||||||
|
mandateId: Current mandate ID
|
||||||
|
userId: Current user ID
|
||||||
|
db: Database connector instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Filtered recordset with access control attributes
|
||||||
|
"""
|
||||||
|
userPrivilege = currentUser.get("privilege", "user")
|
||||||
|
filtered_records = []
|
||||||
|
|
||||||
|
# Apply filtering based on privilege
|
||||||
|
if userPrivilege == "sysadmin":
|
||||||
|
filtered_records = recordset # System admins see all records
|
||||||
|
elif userPrivilege == "admin":
|
||||||
|
# Admins see records in their mandate
|
||||||
|
filtered_records = [r for r in recordset if r.get("mandateId") == mandateId]
|
||||||
|
else: # Regular users
|
||||||
|
# Users only see records they own within their mandate
|
||||||
|
filtered_records = [r for r in recordset
|
||||||
|
if r.get("mandateId") == mandateId and r.get("userId") == userId]
|
||||||
|
|
||||||
|
# Add access control attributes to each record
|
||||||
|
for record in filtered_records:
|
||||||
|
record_id = record.get("id")
|
||||||
|
|
||||||
|
# Set access control flags based on user permissions
|
||||||
|
if table == "mandates":
|
||||||
|
record["_hideView"] = False # Everyone can view
|
||||||
|
record["_hideEdit"] = not _canModify(currentUser, "mandates", record_id, mandateId, userId, db)
|
||||||
|
record["_hideDelete"] = not _canModify(currentUser, "mandates", record_id, mandateId, userId, db)
|
||||||
|
elif table == "users":
|
||||||
|
record["_hideView"] = False # Everyone can view
|
||||||
|
record["_hideEdit"] = not _canModify(currentUser, "users", record_id, mandateId, userId, db)
|
||||||
|
record["_hideDelete"] = not _canModify(currentUser, "users", record_id, mandateId, userId, db)
|
||||||
|
else:
|
||||||
|
# Default access control for other tables
|
||||||
|
record["_hideView"] = False
|
||||||
|
record["_hideEdit"] = not _canModify(currentUser, table, record_id, mandateId, userId, db)
|
||||||
|
record["_hideDelete"] = not _canModify(currentUser, table, record_id, mandateId, userId, db)
|
||||||
|
|
||||||
|
return filtered_records
|
||||||
|
|
||||||
|
def _canModify(currentUser: Dict[str, Any], table: str, recordId: Optional[int] = None, mandateId: int = None, userId: int = None, db = None) -> bool:
|
||||||
|
"""
|
||||||
|
Checks if the current user can modify (create/update/delete) records in a table.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
currentUser: Current user information dictionary
|
||||||
|
table: Name of the table
|
||||||
|
recordId: Optional record ID for specific record check
|
||||||
|
mandateId: Current mandate ID
|
||||||
|
userId: Current user ID
|
||||||
|
db: Database connector instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Boolean indicating permission
|
||||||
|
"""
|
||||||
|
userPrivilege = currentUser.get("privilege", "user")
|
||||||
|
|
||||||
|
# System admins can modify anything
|
||||||
|
if userPrivilege == "sysadmin":
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check specific record permissions
|
||||||
|
if recordId is not None:
|
||||||
|
# Get the record to check ownership
|
||||||
|
records = db.getRecordset(table, recordFilter={"id": recordId})
|
||||||
|
if not records:
|
||||||
|
return False
|
||||||
|
|
||||||
|
record = records[0]
|
||||||
|
|
||||||
|
# Admins can modify anything in their mandate
|
||||||
|
if userPrivilege == "admin" and record.get("mandateId") == mandateId:
|
||||||
|
# Exception: Can't modify Root mandate unless you are a sysadmin
|
||||||
|
if table == "mandates" and recordId == 1 and userPrivilege != "sysadmin":
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Users can only modify their own records
|
||||||
|
if (record.get("mandateId") == mandateId and
|
||||||
|
record.get("userId") == userId):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# For general table modify permission (e.g., create)
|
||||||
|
# Admins can create anything in their mandate
|
||||||
|
if userPrivilege == "admin":
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Regular users can create most entities
|
||||||
|
if table == "mandates":
|
||||||
|
return False # Regular users can't create mandates
|
||||||
|
return True
|
||||||
|
|
@ -11,6 +11,7 @@ from passlib.context import CryptContext
|
||||||
|
|
||||||
from connectors.connectorDbJson import DatabaseConnector
|
from connectors.connectorDbJson import DatabaseConnector
|
||||||
from modules.configuration import APP_CONFIG
|
from modules.configuration import APP_CONFIG
|
||||||
|
from modules.gatewayAccess import _uam, _canModify
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -133,40 +134,7 @@ class GatewayInterface:
|
||||||
Returns:
|
Returns:
|
||||||
Filtered recordset with access control attributes
|
Filtered recordset with access control attributes
|
||||||
"""
|
"""
|
||||||
userPrivilege = self.currentUser.get("privilege", "user")
|
return _uam(self.currentUser, table, recordset, self.mandateId, self.userId, self.db)
|
||||||
filtered_records = []
|
|
||||||
|
|
||||||
# Apply filtering based on privilege
|
|
||||||
if userPrivilege == "sysadmin":
|
|
||||||
filtered_records = recordset # System admins see all records
|
|
||||||
elif userPrivilege == "admin":
|
|
||||||
# Admins see records in their mandate
|
|
||||||
filtered_records = [r for r in recordset if r.get("mandateId") == self.mandateId]
|
|
||||||
else: # Regular users
|
|
||||||
# Users only see records they own within their mandate
|
|
||||||
filtered_records = [r for r in recordset
|
|
||||||
if r.get("mandateId") == self.mandateId and r.get("userId") == self.userId]
|
|
||||||
|
|
||||||
# Add access control attributes to each record
|
|
||||||
for record in filtered_records:
|
|
||||||
record_id = record.get("id")
|
|
||||||
|
|
||||||
# Set access control flags based on user permissions
|
|
||||||
if table == "mandates":
|
|
||||||
record["_hideView"] = False # Everyone can view
|
|
||||||
record["_hideEdit"] = not self._canModify("mandates", record_id)
|
|
||||||
record["_hideDelete"] = not self._canModify("mandates", record_id)
|
|
||||||
elif table == "users":
|
|
||||||
record["_hideView"] = False # Everyone can view
|
|
||||||
record["_hideEdit"] = not self._canModify("users", record_id)
|
|
||||||
record["_hideDelete"] = not self._canModify("users", record_id)
|
|
||||||
else:
|
|
||||||
# Default access control for other tables
|
|
||||||
record["_hideView"] = False
|
|
||||||
record["_hideEdit"] = not self._canModify(table, record_id)
|
|
||||||
record["_hideDelete"] = not self._canModify(table, record_id)
|
|
||||||
|
|
||||||
return filtered_records
|
|
||||||
|
|
||||||
def _canModify(self, table: str, recordId: Optional[int] = None) -> bool:
|
def _canModify(self, table: str, recordId: Optional[int] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|
@ -179,44 +147,7 @@ class GatewayInterface:
|
||||||
Returns:
|
Returns:
|
||||||
Boolean indicating permission
|
Boolean indicating permission
|
||||||
"""
|
"""
|
||||||
userPrivilege = self.currentUser.get("privilege", "user")
|
return _canModify(self.currentUser, table, recordId, self.mandateId, self.userId, self.db)
|
||||||
|
|
||||||
# System admins can modify anything
|
|
||||||
if userPrivilege == "sysadmin":
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check specific record permissions
|
|
||||||
if recordId is not None:
|
|
||||||
# Get the record to check ownership
|
|
||||||
records = self.db.getRecordset(table, recordFilter={"id": recordId})
|
|
||||||
if not records:
|
|
||||||
return False
|
|
||||||
|
|
||||||
record = records[0]
|
|
||||||
|
|
||||||
# Admins can modify anything in their mandate
|
|
||||||
if userPrivilege == "admin" and record.get("mandateId") == self.mandateId:
|
|
||||||
# Exception: Can't modify Root mandate unless you are a sysadmin
|
|
||||||
if table == "mandates" and recordId == 1 and userPrivilege != "sysadmin":
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Users can only modify their own records
|
|
||||||
if (record.get("mandateId") == self.mandateId and
|
|
||||||
record.get("userId") == self.userId):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
# For general table modify permission (e.g., create)
|
|
||||||
# Admins can create anything in their mandate
|
|
||||||
if userPrivilege == "admin":
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Regular users can create most entities
|
|
||||||
if table == "mandates":
|
|
||||||
return False # Regular users can't create mandates
|
|
||||||
return True
|
|
||||||
|
|
||||||
def getInitialId(self, table: str) -> Optional[int]:
|
def getInitialId(self, table: str) -> Optional[int]:
|
||||||
"""Returns the initial ID for a table."""
|
"""Returns the initial ID for a table."""
|
||||||
|
|
|
||||||
131
modules/lucydomAccess.py
Normal file
131
modules/lucydomAccess.py
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
"""
|
||||||
|
Access control module for LucyDOM interface.
|
||||||
|
Handles user access management and permission checks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
|
class LucyDOMAccess:
|
||||||
|
"""
|
||||||
|
Access control class for LucyDOM interface.
|
||||||
|
Handles user access management and permission checks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, currentUser: Dict[str, Any], mandateId: int, userId: int):
|
||||||
|
"""Initialize with user context."""
|
||||||
|
self.currentUser = currentUser
|
||||||
|
self.mandateId = mandateId
|
||||||
|
self.userId = userId
|
||||||
|
|
||||||
|
def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Unified user access management function that filters data based on user privileges
|
||||||
|
and adds access control attributes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
table: Name of the table
|
||||||
|
recordset: Recordset to filter based on access rules
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Filtered recordset with access control attributes
|
||||||
|
"""
|
||||||
|
userPrivilege = self.currentUser.get("privilege", "user")
|
||||||
|
filtered_records = []
|
||||||
|
|
||||||
|
# Apply filtering based on privilege
|
||||||
|
if userPrivilege == "sysadmin":
|
||||||
|
filtered_records = recordset # System admins see all records
|
||||||
|
elif userPrivilege == "admin":
|
||||||
|
# Admins see records in their mandate
|
||||||
|
filtered_records = [r for r in recordset if r.get("mandateId") == self.mandateId]
|
||||||
|
else: # Regular users
|
||||||
|
# To see all prompts from mandate 0 and own
|
||||||
|
if table == "prompts":
|
||||||
|
filtered_records = [r for r in recordset if
|
||||||
|
(r.get("mandateId") == self.mandateId and r.get("userId") == self.userId)
|
||||||
|
or
|
||||||
|
(r.get("mandateId") == 0)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
# Users see only their records
|
||||||
|
filtered_records = [r for r in recordset
|
||||||
|
if r.get("mandateId") == self.mandateId and r.get("userId") == self.userId]
|
||||||
|
|
||||||
|
# Add access control attributes to each record
|
||||||
|
for record in filtered_records:
|
||||||
|
record_id = record.get("id")
|
||||||
|
|
||||||
|
# Set access control flags based on user permissions
|
||||||
|
if table == "prompts":
|
||||||
|
record["_hideView"] = False # Everyone can view
|
||||||
|
record["_hideEdit"] = not self._canModify("prompts", record_id)
|
||||||
|
record["_hideDelete"] = not self._canModify("prompts", record_id)
|
||||||
|
elif table == "files":
|
||||||
|
record["_hideView"] = False # Everyone can view
|
||||||
|
record["_hideEdit"] = not self._canModify("files", record_id)
|
||||||
|
record["_hideDelete"] = not self._canModify("files", record_id)
|
||||||
|
record["_hideDownload"] = not self._canModify("files", record_id)
|
||||||
|
elif table == "workflows":
|
||||||
|
record["_hideView"] = False # Everyone can view
|
||||||
|
record["_hideEdit"] = not self._canModify("workflows", record_id)
|
||||||
|
record["_hideDelete"] = not self._canModify("workflows", record_id)
|
||||||
|
elif table == "workflowMessages":
|
||||||
|
record["_hideView"] = False # Everyone can view
|
||||||
|
record["_hideEdit"] = not self._canModify("workflows", record.get("workflowId"))
|
||||||
|
record["_hideDelete"] = not self._canModify("workflows", record.get("workflowId"))
|
||||||
|
elif table == "workflowLogs":
|
||||||
|
record["_hideView"] = False # Everyone can view
|
||||||
|
record["_hideEdit"] = not self._canModify("workflows", record.get("workflowId"))
|
||||||
|
record["_hideDelete"] = not self._canModify("workflows", record.get("workflowId"))
|
||||||
|
else:
|
||||||
|
# Default access control for other tables
|
||||||
|
record["_hideView"] = False
|
||||||
|
record["_hideEdit"] = not self._canModify(table, record_id)
|
||||||
|
record["_hideDelete"] = not self._canModify(table, record_id)
|
||||||
|
|
||||||
|
return filtered_records
|
||||||
|
|
||||||
|
def _canModify(self, table: str, recordId: Optional[int] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Checks if the current user can modify (create/update/delete) records in a table.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
table: Name of the table
|
||||||
|
recordId: Optional record ID for specific record check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Boolean indicating permission
|
||||||
|
"""
|
||||||
|
userPrivilege = self.currentUser.get("privilege", "user")
|
||||||
|
|
||||||
|
# System admins can modify anything
|
||||||
|
if userPrivilege == "sysadmin":
|
||||||
|
return True
|
||||||
|
|
||||||
|
# For regular users and admins, check specific cases
|
||||||
|
if recordId is not None:
|
||||||
|
# Get the record to check ownership
|
||||||
|
records = self.db.getRecordset(table, recordFilter={"id": recordId})
|
||||||
|
if not records:
|
||||||
|
return False
|
||||||
|
|
||||||
|
record = records[0]
|
||||||
|
|
||||||
|
# Admins can modify anything in their mandate
|
||||||
|
if userPrivilege == "admin" and record.get("mandateId") == self.mandateId:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Regular users can only modify their own records
|
||||||
|
if (record.get("mandateId") == self.mandateId and
|
||||||
|
record.get("userId") == self.userId):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# For general modification permission (e.g., create)
|
||||||
|
# Admins can create anything in their mandate
|
||||||
|
if userPrivilege == "admin":
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Regular users can create in most tables
|
||||||
|
return True
|
||||||
|
|
@ -14,6 +14,7 @@ import hashlib
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from modules.mimeUtils import isTextMimeType, determineContentEncoding
|
from modules.mimeUtils import isTextMimeType, determineContentEncoding
|
||||||
|
from modules.lucydomAccess import LucyDOMAccess
|
||||||
|
|
||||||
# DYNAMIC PART: Connectors to the Interface
|
# DYNAMIC PART: Connectors to the Interface
|
||||||
from connectors.connectorDbJson import DatabaseConnector
|
from connectors.connectorDbJson import DatabaseConnector
|
||||||
|
|
@ -74,6 +75,10 @@ class LucyDOMInterface:
|
||||||
# Load user information
|
# Load user information
|
||||||
self.currentUser = self._getCurrentUserInfo()
|
self.currentUser = self._getCurrentUserInfo()
|
||||||
|
|
||||||
|
# Initialize access control
|
||||||
|
self.access = LucyDOMAccess(self.currentUser, self.mandateId, self.userId)
|
||||||
|
self.access.db = self.db # Share database connection
|
||||||
|
|
||||||
# Initialize standard database records if needed
|
# Initialize standard database records if needed
|
||||||
self._initRecords()
|
self._initRecords()
|
||||||
|
|
||||||
|
|
@ -161,117 +166,12 @@ class LucyDOMInterface:
|
||||||
logger.info(f"Prompt '{promptData.get('name', 'Standard')}' was created with ID {createdPrompt['id']}")
|
logger.info(f"Prompt '{promptData.get('name', 'Standard')}' was created with ID {createdPrompt['id']}")
|
||||||
|
|
||||||
def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""Delegate to access control module."""
|
||||||
Unified user access management function that filters data based on user privileges
|
return self.access._uam(table, recordset)
|
||||||
and adds access control attributes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
table: Name of the table
|
|
||||||
recordset: Recordset to filter based on access rules
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Filtered recordset with access control attributes
|
|
||||||
"""
|
|
||||||
userPrivilege = self.currentUser.get("privilege", "user")
|
|
||||||
filtered_records = []
|
|
||||||
|
|
||||||
# Apply filtering based on privilege
|
|
||||||
if userPrivilege == "sysadmin":
|
|
||||||
filtered_records = recordset # System admins see all records
|
|
||||||
elif userPrivilege == "admin":
|
|
||||||
# Admins see records in their mandate
|
|
||||||
filtered_records = [r for r in recordset if r.get("mandateId") == self.mandateId]
|
|
||||||
else: # Regular users
|
|
||||||
# To see all prompts from mandate 0 and own
|
|
||||||
if table == "prompts":
|
|
||||||
filtered_records = [r for r in recordset if
|
|
||||||
(r.get("mandateId") == self.mandateId and r.get("userId") == self.userId)
|
|
||||||
or
|
|
||||||
(r.get("mandateId") == 0)
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
# Users see only their records
|
|
||||||
filtered_records = [r for r in recordset
|
|
||||||
if r.get("mandateId") == self.mandateId and r.get("userId") == self.userId]
|
|
||||||
|
|
||||||
# Add access control attributes to each record
|
|
||||||
for record in filtered_records:
|
|
||||||
record_id = record.get("id")
|
|
||||||
|
|
||||||
# Set access control flags based on user permissions
|
|
||||||
if table == "prompts":
|
|
||||||
record["_hideView"] = False # Everyone can view
|
|
||||||
record["_hideEdit"] = not self._canModify("prompts", record_id)
|
|
||||||
record["_hideDelete"] = not self._canModify("prompts", record_id)
|
|
||||||
elif table == "files":
|
|
||||||
record["_hideView"] = False # Everyone can view
|
|
||||||
record["_hideEdit"] = not self._canModify("files", record_id)
|
|
||||||
record["_hideDelete"] = not self._canModify("files", record_id)
|
|
||||||
record["_hideDownload"] = not self._canModify("files", record_id)
|
|
||||||
elif table == "workflows":
|
|
||||||
record["_hideView"] = False # Everyone can view
|
|
||||||
record["_hideEdit"] = not self._canModify("workflows", record_id)
|
|
||||||
record["_hideDelete"] = not self._canModify("workflows", record_id)
|
|
||||||
elif table == "workflowMessages":
|
|
||||||
record["_hideView"] = False # Everyone can view
|
|
||||||
record["_hideEdit"] = not self._canModify("workflows", record.get("workflowId"))
|
|
||||||
record["_hideDelete"] = not self._canModify("workflows", record.get("workflowId"))
|
|
||||||
elif table == "workflowLogs":
|
|
||||||
record["_hideView"] = False # Everyone can view
|
|
||||||
record["_hideEdit"] = not self._canModify("workflows", record.get("workflowId"))
|
|
||||||
record["_hideDelete"] = not self._canModify("workflows", record.get("workflowId"))
|
|
||||||
else:
|
|
||||||
# Default access control for other tables
|
|
||||||
record["_hideView"] = False
|
|
||||||
record["_hideEdit"] = not self._canModify(table, record_id)
|
|
||||||
record["_hideDelete"] = not self._canModify(table, record_id)
|
|
||||||
|
|
||||||
return filtered_records
|
|
||||||
|
|
||||||
def _canModify(self, table: str, recordId: Optional[int] = None) -> bool:
|
def _canModify(self, table: str, recordId: Optional[int] = None) -> bool:
|
||||||
"""
|
"""Delegate to access control module."""
|
||||||
Checks if the current user can modify (create/update/delete) records in a table.
|
return self.access._canModify(table, recordId)
|
||||||
|
|
||||||
Args:
|
|
||||||
table: Name of the table
|
|
||||||
recordId: Optional record ID for specific record check
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Boolean indicating permission
|
|
||||||
"""
|
|
||||||
userPrivilege = self.currentUser.get("privilege", "user")
|
|
||||||
|
|
||||||
# System admins can modify anything
|
|
||||||
if userPrivilege == "sysadmin":
|
|
||||||
return True
|
|
||||||
|
|
||||||
# For regular users and admins, check specific cases
|
|
||||||
if recordId is not None:
|
|
||||||
# Get the record to check ownership
|
|
||||||
records = self.db.getRecordset(table, recordFilter={"id": recordId})
|
|
||||||
if not records:
|
|
||||||
return False
|
|
||||||
|
|
||||||
record = records[0]
|
|
||||||
|
|
||||||
# Admins can modify anything in their mandate
|
|
||||||
if userPrivilege == "admin" and record.get("mandateId") == self.mandateId:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Regular users can only modify their own records
|
|
||||||
if (record.get("mandateId") == self.mandateId and
|
|
||||||
record.get("userId") == self.userId):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
# For general modification permission (e.g., create)
|
|
||||||
# Admins can create anything in their mandate
|
|
||||||
if userPrivilege == "admin":
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Regular users can create in most tables
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Language support method
|
# Language support method
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Variables
|
|
||||||
SUBSCRIPTION_ID="213596c9-34b2-4677-a712-45ed127cdae5"
|
|
||||||
RESOURCE_GROUP="volucy-group"
|
|
||||||
APP_NAME="poweron-gateway"
|
|
||||||
DOMAIN_NAME="gateway.poweron-center.net"
|
|
||||||
CERT_PASSWORD="TheSecurePass$(date +%s)" # Unique password with timestamp
|
|
||||||
|
|
||||||
# Login to Azure (uncomment if not already logged in)
|
|
||||||
# az login
|
|
||||||
|
|
||||||
# Set subscription
|
|
||||||
echo "Setting subscription..."
|
|
||||||
az account set --subscription "$SUBSCRIPTION_ID"
|
|
||||||
|
|
||||||
# Create directory for certificate files
|
|
||||||
mkdir -p cert-files
|
|
||||||
cd cert-files
|
|
||||||
|
|
||||||
# Create OpenSSL config file with required extensions
|
|
||||||
cat > openssl.cnf << EOF
|
|
||||||
[ req ]
|
|
||||||
default_bits = 2048
|
|
||||||
distinguished_name = req_distinguished_name
|
|
||||||
req_extensions = req_ext
|
|
||||||
[ req_distinguished_name ]
|
|
||||||
countryName = Country Name (2 letter code)
|
|
||||||
stateOrProvinceName = State or Province Name (full name)
|
|
||||||
localityName = Locality Name (eg, city)
|
|
||||||
organizationName = Organization Name (eg, company)
|
|
||||||
commonName = Common Name (e.g. server FQDN)
|
|
||||||
[ req_ext ]
|
|
||||||
subjectAltName = @alt_names
|
|
||||||
extendedKeyUsage = serverAuth
|
|
||||||
[alt_names]
|
|
||||||
DNS.1 = ${DOMAIN_NAME}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Generate private key
|
|
||||||
openssl genrsa -out private.key 2048
|
|
||||||
|
|
||||||
# Create CSR with config file
|
|
||||||
openssl req -new -key private.key -out request.csr -config openssl.cnf -subj "/C=US/ST=State/L=City/O=Organization/CN=${DOMAIN_NAME}"
|
|
||||||
|
|
||||||
# Generate self-signed certificate with extensions
|
|
||||||
openssl x509 -req -days 365 -in request.csr -signkey private.key -out certificate.crt \
|
|
||||||
-extfile openssl.cnf -extensions req_ext
|
|
||||||
|
|
||||||
# Create PFX file
|
|
||||||
openssl pkcs12 -export -out self-signed-cert.pfx -inkey private.key -in certificate.crt -passout pass:$CERT_PASSWORD
|
|
||||||
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
# Upload certificate to App Service
|
|
||||||
echo "Uploading certificate..."
|
|
||||||
UPLOAD_RESULT=$(az webapp config ssl upload \
|
|
||||||
--resource-group "$RESOURCE_GROUP" \
|
|
||||||
--name "$APP_NAME" \
|
|
||||||
--certificate-file "cert-files/self-signed-cert.pfx" \
|
|
||||||
--certificate-password "$CERT_PASSWORD")
|
|
||||||
|
|
||||||
# Extract thumbprint from upload result
|
|
||||||
CERT_THUMBPRINT=$(echo $UPLOAD_RESULT | jq -r '.thumbprint')
|
|
||||||
|
|
||||||
echo "Certificate uploaded successfully with thumbprint: $CERT_THUMBPRINT"
|
|
||||||
|
|
||||||
# If the thumbprint is empty, try to find it another way
|
|
||||||
if [ -z "$CERT_THUMBPRINT" ] || [ "$CERT_THUMBPRINT" == "null" ]; then
|
|
||||||
echo "Thumbprint not found in upload result. Trying to list certificates..."
|
|
||||||
CERT_LIST=$(az webapp config ssl list --resource-group "$RESOURCE_GROUP")
|
|
||||||
|
|
||||||
# Look for the most recently uploaded certificate
|
|
||||||
CERT_THUMBPRINT=$(echo $CERT_LIST | jq -r 'sort_by(.expirationDate) | reverse | .[0].thumbprint')
|
|
||||||
|
|
||||||
if [ -z "$CERT_THUMBPRINT" ] || [ "$CERT_THUMBPRINT" == "null" ]; then
|
|
||||||
echo "Error: Could not find certificate thumbprint."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Using certificate thumbprint: $CERT_THUMBPRINT"
|
|
||||||
|
|
||||||
# Make sure the custom domain is added
|
|
||||||
echo "Checking if custom domain exists..."
|
|
||||||
DOMAIN_EXISTS=$(az webapp config hostname list --resource-group "$RESOURCE_GROUP" --webapp-name "$APP_NAME" | jq -r ".[] | select(.name==\"$DOMAIN_NAME\") | .name")
|
|
||||||
|
|
||||||
if [ -z "$DOMAIN_EXISTS" ]; then
|
|
||||||
echo "Adding custom domain..."
|
|
||||||
az webapp config hostname add \
|
|
||||||
--resource-group "$RESOURCE_GROUP" \
|
|
||||||
--webapp-name "$APP_NAME" \
|
|
||||||
--hostname "$DOMAIN_NAME"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add IP-based SSL binding
|
|
||||||
echo "Creating IP-based SSL binding..."
|
|
||||||
az webapp config ssl bind \
|
|
||||||
--resource-group "$RESOURCE_GROUP" \
|
|
||||||
--name "$APP_NAME" \
|
|
||||||
--certificate-thumbprint "$CERT_THUMBPRINT" \
|
|
||||||
--ssl-type "IP"
|
|
||||||
|
|
||||||
echo "SSL binding completed. Your domain should now be secured."
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
agentDocumentation delivers a ".docx" file, but the content is a ".md" text markup file
|
agentDocumentation delivers a ".docx" file, but the content is a ".md" text markup file
|
||||||
|
|
||||||
access management to extract into separate modules "lucydomAccess.py" and "gatewayAccess.py". Here to move the functions from "*Interface.py", which define what access which role has.
|
agentDocumentation to extract data per chapter
|
||||||
|
|
||||||
check data extraction tabelle im pdf
|
check data extraction tabelle im pdf
|
||||||
|
|
||||||
|
|
@ -22,8 +22,6 @@ sharepoint connector with document search, content search, content extraction
|
||||||
|
|
||||||
PRIO2:
|
PRIO2:
|
||||||
|
|
||||||
sharepoint connector with document search, content search, content extraction
|
|
||||||
|
|
||||||
Split big files into content-parts
|
Split big files into content-parts
|
||||||
|
|
||||||
Integrate NDA Text as modal form - Data governance agreement by login with checkbox
|
Integrate NDA Text as modal form - Data governance agreement by login with checkbox
|
||||||
|
|
@ -36,11 +34,13 @@ Tools to transfer incl funds:
|
||||||
- Google SERPAPI (shelly)
|
- Google SERPAPI (shelly)
|
||||||
- Anthropic Claude (valueon + shelly)
|
- Anthropic Claude (valueon + shelly)
|
||||||
- Cursor Pro
|
- Cursor Pro
|
||||||
|
- Mermaid
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
----------------------- DONE
|
----------------------- DONE
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
FRONTEND:
|
FRONTEND:
|
||||||
- login page and register page withoug fallback. they have mandatory to load their login.html or register.html pages to work (not html in the code).
|
- login page and register page withoug fallback. they have mandatory to load their login.html or register.html pages to work (not html in the code).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
Architectural Flow
|
|
||||||
|
|
||||||
workflow.js - Entry point that:
|
|
||||||
|
|
||||||
Creates the central state object
|
|
||||||
Initializes API and UI components
|
|
||||||
Connects components together
|
|
||||||
Exposes required public functions
|
|
||||||
|
|
||||||
|
|
||||||
workflow_state.js - State management that:
|
|
||||||
|
|
||||||
Maintains a single source of truth for workflow data
|
|
||||||
Implements an observer pattern for state changes
|
|
||||||
Handles state transitions and business logic
|
|
||||||
Validates state updates
|
|
||||||
|
|
||||||
|
|
||||||
workflow_api.js - API communication that:
|
|
||||||
|
|
||||||
Abstracts all API calls to backend services
|
|
||||||
Manages polling for workflow status
|
|
||||||
Processes API responses
|
|
||||||
Tracks data transfer statistics
|
|
||||||
Updates state with API results
|
|
||||||
|
|
||||||
|
|
||||||
workflow_ui.js - UI layer that:
|
|
||||||
|
|
||||||
Renders UI based on current state
|
|
||||||
Sets up event listeners for user interactions
|
|
||||||
Handles DOM manipulation
|
|
||||||
Controls layout adjustments
|
|
||||||
Triggers state changes via user actions
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Data Flow
|
|
||||||
|
|
||||||
User initiates action in the UI
|
|
||||||
UI controller handles the action and calls relevant API methods
|
|
||||||
API communicates with backend and updates state
|
|
||||||
State notifies observers about changes
|
|
||||||
UI reacts to state changes and updates display
|
|
||||||
Polling continues until completed state is reached
|
|
||||||
Final UI update happens when workflow completes
|
|
||||||
|
|
@ -1,366 +0,0 @@
|
||||||
# State Machine Documentation for Backend Chat Workflow
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Chat Workflow system implements a state machine that processes user inputs through a sequence of well-defined steps. The system orchestrates interactions between users, project managers, and specialized agents to produce final outputs.
|
|
||||||
|
|
||||||
## Core Objects
|
|
||||||
|
|
||||||
### Workflow Object
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "uuid-string",
|
|
||||||
"mandateId": int,
|
|
||||||
"userId": int,
|
|
||||||
"name": "Workflow name",
|
|
||||||
"startedAt": "ISO-datetime",
|
|
||||||
"messages": [], // References to messages
|
|
||||||
"messageIds": [], // List of message IDs
|
|
||||||
"logs": [], // Log entries
|
|
||||||
"dataStats": {}, // Performance metrics
|
|
||||||
"currentRound": int, // Increments with each interaction
|
|
||||||
"status": "string", // running, completed, failed, stopped
|
|
||||||
"lastActivity": "ISO-datetime"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Message Object
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "msg_uuid-string",
|
|
||||||
"workflowId": "workflow-uuid",
|
|
||||||
"role": "string", // user, assistant
|
|
||||||
"agentName": "string", // Empty for user, agent name for assistant
|
|
||||||
"content": "string", // The message text
|
|
||||||
"documents": [], // List of document objects
|
|
||||||
"timestamp": "ISO-datetime",
|
|
||||||
"sequenceNo": int, // Position in conversation
|
|
||||||
"status": "string" // first, step, last
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Log Entry Object
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "log_uuid-string",
|
|
||||||
"workflowId": "workflow-uuid",
|
|
||||||
"message": "string",
|
|
||||||
"progress": int, // Optional, 0-100
|
|
||||||
"type": "string", // info, warning, error
|
|
||||||
"timestamp": "ISO-datetime",
|
|
||||||
"agentName": "string", // Name of the agent that generated the log
|
|
||||||
"status": "string" // current workflow status (running, completed, failed, stopped)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Document Object
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "doc_uuid-string",
|
|
||||||
"fileId": int,
|
|
||||||
"name": "string", // Filename without extension
|
|
||||||
"ext": "string", // File extension
|
|
||||||
"data": "base64-encoded-string", // File contents
|
|
||||||
"contents": [] // Extracted content items in text format
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Content Item Object
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"sequenceNr": int, // Sequence in the document
|
|
||||||
"name": "string",
|
|
||||||
"ext": "string",
|
|
||||||
"contentType": "string", // mime type
|
|
||||||
"data": "string|base64", // Original content
|
|
||||||
"dataExtracted": "string", // Optional AI-processed content based on extraction requirement
|
|
||||||
"metadata": {
|
|
||||||
"isText": boolean,
|
|
||||||
"base64Encoded": boolean,
|
|
||||||
"aiProcessed": boolean,
|
|
||||||
// Optional metadata specific to content type
|
|
||||||
},
|
|
||||||
"summary": "string" // AI-generated static summary of the content
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## State Machine Workflow
|
|
||||||
|
|
||||||
### 1. Workflow Initialization
|
|
||||||
- **Trigger**: User message received via `/api/workflows/start` OR `/api/workflows/start?id=string`
|
|
||||||
- **Input**: `UserInputRequest` with `prompt` and optional `listFileId`
|
|
||||||
- **Process**:
|
|
||||||
- If `id` existing and workflow exists for `id`==`workflowId`: Load workflow, increment `currentRound`, set status "running"
|
|
||||||
- Else: Create new workflow with "currentRound"=1, status "running"
|
|
||||||
- **Logs**: "Workflow initialized" or "Running workflow", progress 0%
|
|
||||||
- **API Responses**:
|
|
||||||
- Success: 200 OK with workflow ID
|
|
||||||
- Error: 400 Bad Request if input invalid, 404 Not Found if workflow ID not found
|
|
||||||
|
|
||||||
### 2. Workflow Exception
|
|
||||||
- **Trigger**:
|
|
||||||
- User stopped workflow via API
|
|
||||||
- An exception happened
|
|
||||||
- **Process**:
|
|
||||||
- If status=="stopped": Set workflow status to "stopped", add message with status "last", update lastActivity, stop execution immediately
|
|
||||||
- If status=="failed": Set workflow status to "failed", add message with status "last", update lastActivity, stop execution immediately
|
|
||||||
- Else: Continue normally
|
|
||||||
- **Logs**: "Workflow failure reported", progress 100%
|
|
||||||
- **API Responses**:
|
|
||||||
- For stop request: 200 OK when workflow successfully stopped
|
|
||||||
- For exceptions: 500 Internal Server Error with error details
|
|
||||||
|
|
||||||
### 3. User Message Processing
|
|
||||||
- **Process**:
|
|
||||||
- Transform user input into message object with documents, message status "first"
|
|
||||||
- Extract contents from files using `getDocumentContents()`
|
|
||||||
- Generate static summaries for each content item
|
|
||||||
- **State Changes**:
|
|
||||||
- Add user message to `workflow.messages` array
|
|
||||||
- Add message ID to `workflow.messageIds` array
|
|
||||||
- Update `workflow.lastActivity`
|
|
||||||
- **Logs**: "Workflow processing started", progress 0%
|
|
||||||
|
|
||||||
### 4. Project Manager Analysis
|
|
||||||
- **Process**:
|
|
||||||
- Generate prompt for project manager AI
|
|
||||||
- Project manager analyzes request and documents
|
|
||||||
- Project manager generates work plan and response
|
|
||||||
- **Outputs**:
|
|
||||||
- `objFinalDocuments`: List of str "filename.ext" for expected final output documents
|
|
||||||
- `objWorkplan`: List of agent tasks
|
|
||||||
- `objUserResponse`: Text response to user
|
|
||||||
- `userLanguage`: Detected language code (e.g. en)
|
|
||||||
- **State Changes**:
|
|
||||||
- Add assistant message with project manager response, status "step"
|
|
||||||
- Set user language in mydom interface
|
|
||||||
- **Logs**: "Analyzing request and planning work" (10%), "Planned outputs" (20%), "Work plan created" (25%)
|
|
||||||
|
|
||||||
### 5. Agent Execution
|
|
||||||
- **Process** (For each task in workplan):
|
|
||||||
- Prepare input documents for agent
|
|
||||||
- Execute agent with standardized task object
|
|
||||||
- Save produced documents
|
|
||||||
- Create assistant message with agent response, status "step"
|
|
||||||
- **Agent Task Object**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"taskId": "uuid-string",
|
|
||||||
"workflowId": "workflow-uuid",
|
|
||||||
"prompt": "string",
|
|
||||||
"inputDocuments": [], // list of documents including original document data and all content items data with original (attribute "data") and based on prompt (attribute "dataExtracted")
|
|
||||||
"outputSpecifications": [
|
|
||||||
{
|
|
||||||
"label": "filename.ext",
|
|
||||||
"description": "string"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"context": {
|
|
||||||
"workflowRound": int,
|
|
||||||
"agentType": "string",
|
|
||||||
"timestamp": "ISO-datetime",
|
|
||||||
"language": "language-code"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **Agent Result Object**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"feedback": "string", // Text describing what the agent did
|
|
||||||
"documents": [
|
|
||||||
{
|
|
||||||
"label": "filename.ext",
|
|
||||||
"content": "string|binary" // Document contents
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **State Changes**: Add assistant message for each agent with agentName set, status "step"
|
|
||||||
- **Logs**: "Running task X/Y: agentName" with progress updates from 30% to 90%
|
|
||||||
|
|
||||||
### 6. Final Response Generation
|
|
||||||
- **Process**:
|
|
||||||
- Create final message reviewing promised and delivered documents
|
|
||||||
- Add documents to workflow
|
|
||||||
- **State Changes**: Add final assistant message from projectManager, status "last"
|
|
||||||
- **Logs**: "Creating final response" (90%)
|
|
||||||
|
|
||||||
### 7. Workflow Completion
|
|
||||||
- **Process**:
|
|
||||||
- Finalize workflow and update status
|
|
||||||
- **State Changes**:
|
|
||||||
- Set workflow status to "completed"
|
|
||||||
- Update `workflow.lastActivity`
|
|
||||||
- **Logs**: "Workflow completed successfully" with progress 100%
|
|
||||||
- **API Responses**:
|
|
||||||
- A message with status "last" is included in the response
|
|
||||||
- Status endpoint will return "completed"
|
|
||||||
|
|
||||||
### 8. Workflow Stopped
|
|
||||||
- **Trigger**: `/api/workflows/{workflowId}/stop` endpoint called
|
|
||||||
- **Process**:
|
|
||||||
- Immediately interrupt workflow execution
|
|
||||||
- Save current state and mark as stopped
|
|
||||||
- **State Changes**:
|
|
||||||
- Set workflow status to "stopped"
|
|
||||||
- Update lastActivity timestamp
|
|
||||||
- **Logs**: "Workflow stopped by user" with progress 100%
|
|
||||||
- **API Responses**:
|
|
||||||
- 200 OK with confirmation message
|
|
||||||
|
|
||||||
### 9. Workflow Failed
|
|
||||||
- **Trigger**: Exception during workflow execution
|
|
||||||
- **Process**:
|
|
||||||
- Log error details
|
|
||||||
- Set workflow status to "failed"
|
|
||||||
- **State Changes**:
|
|
||||||
- Set workflow status to "failed"
|
|
||||||
- Update lastActivity timestamp
|
|
||||||
- **Logs**: Detailed error message with progress 100%
|
|
||||||
- **API Responses**:
|
|
||||||
- Status endpoint will return "failed" with error context
|
|
||||||
|
|
||||||
### 10. Workflow Resumption
|
|
||||||
- **Trigger**: `/api/workflows/start?id={workflowId}` endpoint called with existing workflow ID
|
|
||||||
- **Process**:
|
|
||||||
- Load existing workflow
|
|
||||||
- Increment currentRound counter
|
|
||||||
- Start processing from user message
|
|
||||||
- **State Changes**:
|
|
||||||
- Set status to "running"
|
|
||||||
- Increment currentRound
|
|
||||||
- Add new user message
|
|
||||||
- **Logs**: "Resuming workflow, round {currentRound}" with progress 0%
|
|
||||||
- **API Responses**:
|
|
||||||
- Same as workflow initialization
|
|
||||||
|
|
||||||
### 11. Workflow Reset/Deletion
|
|
||||||
- **Trigger**: `/api/workflows/{workflowId}` DELETE endpoint called
|
|
||||||
- **Process**:
|
|
||||||
- Remove all workflow data from storage
|
|
||||||
- **State Changes**:
|
|
||||||
- Workflow no longer exists in the system
|
|
||||||
- **Logs**: Log in system log that workflow was deleted
|
|
||||||
- **API Responses**:
|
|
||||||
- 200 OK if successful
|
|
||||||
- 404 Not Found if workflow didn't exist
|
|
||||||
|
|
||||||
## API Endpoints and Polling Support
|
|
||||||
|
|
||||||
### Main Workflow Endpoints
|
|
||||||
- `POST /api/workflows/start?id=string`: Submit user input to start a new workflow, optional with workflow id to continue existing workflow
|
|
||||||
- `POST /api/workflows/{workflowId}/stop`: Stop a running workflow: Immediately to set workflow status to "stopped"
|
|
||||||
- `DELETE /api/workflows/{workflowId}`: Delete a workflow
|
|
||||||
- `GET /api/workflows/{workflowId}/status`: Get workflow status (running, completed, failed, stopped)
|
|
||||||
- `GET /api/workflows/{workflowId}/logs?id=string`: Get workflow logs, optional with log id to get only logs produced after and including log with log id
|
|
||||||
- `GET /api/workflows/{workflowId}/messages?id=string`: Get workflow messages, optional with message id to get only messages produced after and including message with log id
|
|
||||||
|
|
||||||
### Document Management
|
|
||||||
- `DELETE /api/workflows/{workflowId}/messages/{messageId}`: Delete a message
|
|
||||||
- `DELETE /api/workflows/{workflowId}/messages/{messageId}/files/{fileId}`: Remove file from message
|
|
||||||
|
|
||||||
### Backend Support for Frontend Polling
|
|
||||||
|
|
||||||
The backend implements efficient support for frontend polling mechanisms:
|
|
||||||
|
|
||||||
1. **Selective Data Transfer**:
|
|
||||||
- Both `/logs` and `/messages` endpoints accept an optional `id` parameter
|
|
||||||
- When provided, only records with IDs equal to or newer than the specified ID are returned
|
|
||||||
- This minimizes data transfer and improves performance
|
|
||||||
|
|
||||||
2. **Log Storage**:
|
|
||||||
- Each log entry includes timestamp, progress indicators, and status
|
|
||||||
- Frontend can accurately track workflow progress and update UI accordingly
|
|
||||||
- Logs are stored in chronological order with monotonically increasing IDs
|
|
||||||
|
|
||||||
3. **Message Handling**:
|
|
||||||
- Messages include a status field ("first", "step", "last")
|
|
||||||
- The "last" status indicates completion of the current workflow round
|
|
||||||
- Frontend uses this to determine when to enable user input
|
|
||||||
|
|
||||||
4. **Status Endpoint**:
|
|
||||||
- Lightweight endpoint that returns only the current workflow status
|
|
||||||
- Used by frontend to detect state changes without transferring all data
|
|
||||||
- Also includes lastActivity timestamp to detect stalled workflows
|
|
||||||
|
|
||||||
5. **Caching Layer**:
|
|
||||||
- Backend implements caching for frequent polling requests
|
|
||||||
- Reduces database load and improves response times
|
|
||||||
- Cache invalidation occurs when workflow status changes
|
|
||||||
|
|
||||||
6. **Batch Processing**:
|
|
||||||
- Large log or message sets are paginated automatically
|
|
||||||
- Frontend receives data in manageable chunks
|
|
||||||
- Prevents memory issues with long-running workflows
|
|
||||||
|
|
||||||
## Document Object Structure Clarification
|
|
||||||
The Document Object contains both raw data and processed contents:
|
|
||||||
- `data`: Contains the base64-encoded binary representation of the entire original file
|
|
||||||
- `contents`: Contains an array of structured Content Item objects extracted from the original file
|
|
||||||
|
|
||||||
The relationship works as follows:
|
|
||||||
1. When a file is uploaded, its binary data is stored in the `data` field
|
|
||||||
2. The original file's complete data is always preserved in the document's `data` field
|
|
||||||
3. The file is then processed by content extractors based on file type (PDF, image, text, etc.)
|
|
||||||
4. Each logically separate piece of content is added to the `contents` array
|
|
||||||
5. For text files, there might be just one content item
|
|
||||||
6. For PDFs, there might be multiple content items (one per page or per embedded image)
|
|
||||||
7. For complex documents, content items might represent different sections or formats
|
|
||||||
8. Each content item contains its own `data` field with the specific extracted content; for agents convenience it contains the additional field `dataExtracted` with extracted data based on agents task prompt
|
|
||||||
|
|
||||||
This dual structure allows agents to:
|
|
||||||
- Access the complete original file when needed
|
|
||||||
- Work with pre-processed, extracted content for efficiency
|
|
||||||
- Process specific sections of a document without loading the entire file
|
|
||||||
|
|
||||||
## State Transitions
|
|
||||||
|
|
||||||
```
|
|
||||||
[null] → [running] // New workflow created
|
|
||||||
[running] → [completed] // Workflow completes successfully
|
|
||||||
[running] → [stopped] // User manually stops workflow
|
|
||||||
[running] → [failed] // Error occurs during workflow
|
|
||||||
[completed] → [running] // User continues workflow with new input (new round)
|
|
||||||
[stopped] → [running] // User continues after manual stop (new round)
|
|
||||||
[failed] → [running] // User retries workflow despite error (new round)
|
|
||||||
[any] → [null] // Workflow deleted
|
|
||||||
```
|
|
||||||
|
|
||||||
## Exception Handling
|
|
||||||
|
|
||||||
### Rules
|
|
||||||
- Workflow status changes to "failed" on exceptions, all message and workflow generation exceptions to handle to ensure data consistency in the database
|
|
||||||
- Errors are logged in workflow logs with type "error"
|
|
||||||
- Produced project manager analysis output, inputs to agents, output from agents, workflow items, message items are all logged for debugging in the logger with type "debug"
|
|
||||||
- HTTP exceptions are returned to the client with appropriate status codes
|
|
||||||
- Failed agent tasks are recorded but don't stop the workflow
|
|
||||||
|
|
||||||
### Workflow Stop Conditions
|
|
||||||
- User explicitly cancels the workflow via the stop endpoint
|
|
||||||
|
|
||||||
Action to take:
|
|
||||||
- workflow to set to "stopped" status
|
|
||||||
|
|
||||||
### Workflow Failure Conditions
|
|
||||||
- Unhandled exceptions in the main workflow execution path
|
|
||||||
- Project manager analysis fails to generate a valid workplan
|
|
||||||
- More than 50% of the agent tasks in the workplan fail to complete
|
|
||||||
- Timeout exceeded (workflow runs longer than the configured maximum duration)
|
|
||||||
- System resource limits exceeded (memory, CPU, etc.)
|
|
||||||
|
|
||||||
Action to take, when a workflow fails:
|
|
||||||
- The last log entry will contain details about the failure reason
|
|
||||||
- workflow to set to "failed" status
|
|
||||||
|
|
||||||
### Workflow Exception Checkpoints
|
|
||||||
At the following points in the code the Workflow Execution routine is called:
|
|
||||||
- Before adding or updating a message to the workflow
|
|
||||||
- Before doing an API call
|
|
||||||
|
|
||||||
## Special Notes
|
|
||||||
|
|
||||||
1. **Document Processing**: Files uploaded by users are processed with content extraction to make them accessible to agents.
|
|
||||||
2. **AI Language Support**: The system detects and adapts to the user's language.
|
|
||||||
3. **Round Counting**: Each interaction increments the `currentRound` counter.
|
|
||||||
4. **Agent Registry**: Agents are loaded dynamically and registered in the AgentRegistry.
|
|
||||||
5. **Standardized Task Processing**: All agents implement the same task processing interface.
|
|
||||||
|
|
@ -1,453 +0,0 @@
|
||||||
# State Machine Documentation for Frontend Chat Workflow
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Chat Workflow frontend implements a state machine that manages user interactions through a well-defined sequence of states. This system coordinates the user interface components, handles file attachments, and manages communication with the backend to provide a seamless multi-agent chat experience.
|
|
||||||
|
|
||||||
## Core Objects
|
|
||||||
|
|
||||||
### Frontend Workflow State Object
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "string", // null or "running", "completed", "failed", "stopped"
|
|
||||||
"workflowId": "string", // null or Unique workflow identifier
|
|
||||||
"logs": [], // Log entries
|
|
||||||
"chatMessages": [], // Chat messages
|
|
||||||
"lastPolledLogId": "string", // ID of last polled log
|
|
||||||
"lastPolledMessageId": "string", // ID of last polled message
|
|
||||||
"dataStats": {
|
|
||||||
"bytesSent": int, // Data sent to backend
|
|
||||||
"bytesReceived": int, // Data received from backend
|
|
||||||
"tokensUsed": float // Used tokens required for billing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### User Input State Object
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"promptText": "string", // Current user input text
|
|
||||||
"additionalFiles": [], // List of file IDs to attach
|
|
||||||
"domElements": {} // References to UI DOM elements
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Log Entry Object
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "log_timestamp",
|
|
||||||
"message": "string",
|
|
||||||
"progress": int, // Optional, 0-100
|
|
||||||
"type": "string", // info, warning, error
|
|
||||||
"timestamp": "ISO-datetime",
|
|
||||||
"agentName": "string", // Name of the agent that generated the log
|
|
||||||
"waiting": boolean, // Whether this log shows a waiting indicator
|
|
||||||
"highlighted": boolean // Whether this log should be visually highlighted
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Chat Message Object
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "msg_type_timestamp",
|
|
||||||
"role": "string", // user, assistant
|
|
||||||
"agentName": "string",
|
|
||||||
"content": "string",
|
|
||||||
"documents": [], // List of document objects
|
|
||||||
"timestamp": "ISO-datetime",
|
|
||||||
"status": "string" // first, step, last ("workflowComplete" can be asked as `status`=="last")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### File Object
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "file_uuid-string",
|
|
||||||
"name": "filename.ext",
|
|
||||||
"size": int,
|
|
||||||
"fileId": int,
|
|
||||||
"contentType": "string", // mime type
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## State Machine Workflow
|
|
||||||
|
|
||||||
### 1. Initial State
|
|
||||||
- **State**: `null` (No workflow active)
|
|
||||||
- **UI Elements**:
|
|
||||||
- Empty chat container with placeholder
|
|
||||||
- Prompt input field enabled
|
|
||||||
- "Start" button enabled
|
|
||||||
- "Stop" button hidden
|
|
||||||
- File upload area enabled
|
|
||||||
- Empty log panel
|
|
||||||
- **User Actions**:
|
|
||||||
- Enter prompt text
|
|
||||||
- Add files via upload or drag & drop
|
|
||||||
- Select pre-defined prompts from dropdown
|
|
||||||
- Click Start button
|
|
||||||
- **API Interactions**: None in this state
|
|
||||||
|
|
||||||
### 2. Prompt Preparation
|
|
||||||
- **State**: `null` (transitioning to `running`)
|
|
||||||
- **Triggers**:
|
|
||||||
- User typing in prompt field (updates `userInputState.promptText`)
|
|
||||||
- File uploads/drag & drop (adds to `userInputState.additionalFiles`)
|
|
||||||
- Prompt selection from dropdown (sets `userInputState.promptText`)
|
|
||||||
- **Process**:
|
|
||||||
- Files are uploaded to backend via `uploadAndAddFile()`
|
|
||||||
- Files appear in attachment list with name, size, remove option
|
|
||||||
- Prompt visualization is updated with file count and names
|
|
||||||
- **State Changes**:
|
|
||||||
- `userInputState.additionalFiles` array updated
|
|
||||||
- `userInputState.promptText` updated
|
|
||||||
- **UI Updates**:
|
|
||||||
- File attachment list rendered
|
|
||||||
- Prompt preview area shows attached files
|
|
||||||
- **API Interactions**:
|
|
||||||
- `POST /api/files/upload`: For each file being attached, an API call is made to upload the file and get a file ID
|
|
||||||
- Response contains file metadata which is stored in the frontend state
|
|
||||||
|
|
||||||
### 3. Workflow Initialization
|
|
||||||
- **State**: `running`
|
|
||||||
- **Trigger**: User clicks "Start" button or presses Enter
|
|
||||||
- **Process**:
|
|
||||||
- Validate user input (ensures non-empty prompt)
|
|
||||||
- Set loading state (disable start button, show spinner)
|
|
||||||
- Submit prompt and files
|
|
||||||
- Receive workflow ID from backend
|
|
||||||
- **State Changes**:
|
|
||||||
- `status` → `running`
|
|
||||||
- `workflowId` set to returned ID
|
|
||||||
- `userInputState.additionalFiles` reset to empty array
|
|
||||||
- `userInputState.promptText` reset to empty string
|
|
||||||
- **UI Updates**:
|
|
||||||
- Chat messages container shown
|
|
||||||
- User message appears in chat
|
|
||||||
- Log entry added: "Workflow started"
|
|
||||||
- System chat message added: "Multi-Agent Chat has been started"
|
|
||||||
- Start button becomes Send button, deactivated and with animation
|
|
||||||
- Stop button becomes visible
|
|
||||||
- Input field disabled temporarily
|
|
||||||
- **API Interactions**:
|
|
||||||
- `POST /api/workflows/start`: Submit user prompt and list of file IDs
|
|
||||||
- Request payload: `{ prompt: string, fileIds: array }`
|
|
||||||
- Response contains `workflowId` which is stored for future API calls
|
|
||||||
- For continuing an existing workflow: `POST /api/workflows/start?id={workflowId}`
|
|
||||||
|
|
||||||
### 4. Polling for Updates
|
|
||||||
- **State**: `running`
|
|
||||||
- **Process**:
|
|
||||||
- `pollWorkflowStatus()` initiated with workflow ID
|
|
||||||
- `pollWorkflowLogs()` and `pollWorkflowMessages()` called periodically
|
|
||||||
- Last log and message IDs tracked to request only new items
|
|
||||||
- New logs and messages added to state
|
|
||||||
- **State Changes**:
|
|
||||||
- `logs` array updated with new log entries
|
|
||||||
- `chatMessages` array updated with new messages
|
|
||||||
- `lastPolledLogId` and `lastPolledMessageId` updated
|
|
||||||
- `dataStats` updated with sent/received bytes and tokens
|
|
||||||
- **UI Updates**:
|
|
||||||
- Updated and new logs rendered in log panel
|
|
||||||
- Updated and new chat messages rendered in chat area
|
|
||||||
- Waiting animation shown on latest log entry
|
|
||||||
- Data statistics counters updated
|
|
||||||
- **API Interactions**:
|
|
||||||
- `GET /api/workflows/{workflowId}/status`: Polls workflow status (every 2000ms)
|
|
||||||
- `GET /api/workflows/{workflowId}/logs?id={lastPolledLogId}`: Gets only logs newer than the last polled log
|
|
||||||
- `GET /api/workflows/{workflowId}/messages?id={lastPolledMessageId}`: Gets only messages newer than the last polled message
|
|
||||||
- Polling continues until workflow status changes from "running" or a message with status "last" is received
|
|
||||||
|
|
||||||
### 5. Workflow Running
|
|
||||||
- **State**: `running`
|
|
||||||
- **Process**:
|
|
||||||
- Backend processes user input through agent workflow
|
|
||||||
- Frontend continuously polls for updates
|
|
||||||
- Log entries show progress of agents
|
|
||||||
- Chat messages display agent responses
|
|
||||||
- **UI Elements**:
|
|
||||||
- Stop button visible and enabled
|
|
||||||
- Input field disabled
|
|
||||||
- Log panel shows processing steps with progress indicators
|
|
||||||
- Chat displays multi-agent conversation
|
|
||||||
- **User Actions**:
|
|
||||||
- Click Stop button to interrupt workflow
|
|
||||||
- View file attachments in messages
|
|
||||||
- Preview or download files
|
|
||||||
- **API Interactions**:
|
|
||||||
- Continued polling via endpoints described in State 4
|
|
||||||
- `POST /api/workflows/{workflowId}/stop`: If user clicks Stop button
|
|
||||||
|
|
||||||
### 6. Workflow Completion
|
|
||||||
- **State**: `completed`
|
|
||||||
- **Trigger**: Backend returns workflow status "completed" or message with status "last"
|
|
||||||
- **Process**:
|
|
||||||
- Final message displayed, input enabled for new prompt
|
|
||||||
- Polling stops
|
|
||||||
- **State Changes**:
|
|
||||||
- `status` → `completed`
|
|
||||||
- **UI Updates**:
|
|
||||||
- Stop button hidden
|
|
||||||
- Start button enabled
|
|
||||||
- Input field enabled and focused
|
|
||||||
- Final log shows "Workflow completed"
|
|
||||||
- **API Interactions**:
|
|
||||||
- No further API calls until user inputs new prompt
|
|
||||||
|
|
||||||
### 7. Workflow Failure
|
|
||||||
- **State**: `failed`
|
|
||||||
- **Trigger**: Backend returns workflow status "failed"
|
|
||||||
- **Process**:
|
|
||||||
- Error message displayed, option to retry
|
|
||||||
- Polling stops
|
|
||||||
- **State Changes**:
|
|
||||||
- `status` → `failed`
|
|
||||||
- **UI Updates**:
|
|
||||||
- Error indicators in UI
|
|
||||||
- Retry option enabled
|
|
||||||
- Input field enabled
|
|
||||||
- **API Interactions**:
|
|
||||||
- No further API calls until user initiates retry
|
|
||||||
|
|
||||||
### 8. Workflow Stopped
|
|
||||||
- **State**: `stopped`
|
|
||||||
- **Trigger**: User clicks Stop button or backend returns "stopped"
|
|
||||||
- **Process**:
|
|
||||||
- Workflow processing interrupted
|
|
||||||
- Polling stops
|
|
||||||
- **State Changes**:
|
|
||||||
- `status` → `stopped`
|
|
||||||
- **UI Updates**:
|
|
||||||
- Resume option enabled
|
|
||||||
- Start button enabled
|
|
||||||
- Input field enabled
|
|
||||||
- **API Interactions**:
|
|
||||||
- No further API calls until user continues or resets
|
|
||||||
|
|
||||||
### 9. User Input Requested
|
|
||||||
- **State**: Varies based on workflow state
|
|
||||||
- **Trigger**: Received message with status "last" or workflow status not "running"
|
|
||||||
- **Process**:
|
|
||||||
- Special log entry added: "Waiting for user input to continue"
|
|
||||||
- Input field enabled for user response
|
|
||||||
- **State Changes**:
|
|
||||||
- `status` → The status from the workflow
|
|
||||||
- **UI Updates**:
|
|
||||||
- Stop Polling
|
|
||||||
- Waiting animation stopped
|
|
||||||
- Input field enabled and focused
|
|
||||||
- Send button enabled
|
|
||||||
- Send button shows "Start" for new workflow or "Send" for continuation
|
|
||||||
- Input field may show specific placeholder
|
|
||||||
- Stop button hidden
|
|
||||||
- **API Interactions**:
|
|
||||||
- No API calls until user provides input
|
|
||||||
|
|
||||||
### 10. Continuation Preparation
|
|
||||||
- **State**: `completed`, `failed`, or `stopped` (transitioning to `running`)
|
|
||||||
- **Trigger**: User enters new input after previous workflow cycle
|
|
||||||
- **Process**:
|
|
||||||
- Prepare continuation with existing workflow ID
|
|
||||||
- Similar to Prompt Preparation but preserves context
|
|
||||||
- **State Changes**:
|
|
||||||
- `userInputState.promptText` updated
|
|
||||||
- `userInputState.additionalFiles` updated if new files added
|
|
||||||
- **UI Updates**:
|
|
||||||
- File attachment list updated if changed
|
|
||||||
- Send button ready for continuation
|
|
||||||
- **API Interactions**:
|
|
||||||
- Same as Prompt Preparation if new files are added
|
|
||||||
|
|
||||||
### 11. Workflow Resumption
|
|
||||||
- **State**: `running`
|
|
||||||
- **Trigger**: User continues workflow after stop/failure/completion
|
|
||||||
- **Process**:
|
|
||||||
- Submit new input with existing workflow ID
|
|
||||||
- **State Changes**:
|
|
||||||
- `status` → `running`
|
|
||||||
- New user message added
|
|
||||||
- **UI Updates**:
|
|
||||||
- Similar to Workflow Initialization but preserves history
|
|
||||||
- Polling resumes
|
|
||||||
- **API Interactions**:
|
|
||||||
- `POST /api/workflows/start?id={workflowId}`: Sends continuation prompt with existing workflow ID
|
|
||||||
- Polling endpoints resume as in State 4
|
|
||||||
|
|
||||||
### 12. Workflow Reset
|
|
||||||
- **State**: `null`
|
|
||||||
- **Trigger**: User clicks Reset button
|
|
||||||
- **Process**:
|
|
||||||
- All state data cleared
|
|
||||||
- UI reset to initial state
|
|
||||||
- **State Changes**:
|
|
||||||
- `status` → `null`
|
|
||||||
- `workflowId` → `null`
|
|
||||||
- `logs` → `[]`
|
|
||||||
- `chatMessages` → `[]`
|
|
||||||
- **UI Updates**:
|
|
||||||
- Empty chat state shown
|
|
||||||
- Log panel cleared
|
|
||||||
- Input field reset
|
|
||||||
- Chat area cleared
|
|
||||||
- File attachments removed
|
|
||||||
- **API Interactions**:
|
|
||||||
- Optional `DELETE /api/workflows/{workflowId}` to clean up server resources
|
|
||||||
|
|
||||||
## API Interaction Details
|
|
||||||
|
|
||||||
### Polling Implementation
|
|
||||||
The frontend implements a sophisticated polling mechanism that efficiently retrieves only the new data:
|
|
||||||
|
|
||||||
1. **Initialization**:
|
|
||||||
- When a workflow starts, the frontend stores the workflow ID
|
|
||||||
- Initial polling begins immediately after receiving workflow ID
|
|
||||||
|
|
||||||
2. **Selective Data Retrieval**:
|
|
||||||
- Frontend tracks `lastPolledLogId` and `lastPolledMessageId`
|
|
||||||
- Each polling request includes the last ID to receive only newer items
|
|
||||||
- This significantly reduces bandwidth and processing requirements
|
|
||||||
|
|
||||||
3. **Polling Schedule**:
|
|
||||||
- Status polling: Every 2000ms while in `running` state
|
|
||||||
- Logs polling: Every 1000ms while in `running` state
|
|
||||||
- Messages polling: Every 1000ms while in `running` state
|
|
||||||
- All polling stops when workflow status changes from `running`
|
|
||||||
|
|
||||||
4. **Retry Mechanism**:
|
|
||||||
- Network failures trigger exponential backoff
|
|
||||||
- Starting at 1000ms, doubling up to 16000ms max
|
|
||||||
- After 5 consecutive failures, displays connection error
|
|
||||||
|
|
||||||
5. **Polling Suspension**:
|
|
||||||
- Polling automatically pauses when browser tab is inactive
|
|
||||||
- Resumes when tab becomes active again
|
|
||||||
- Can be manually suspended with `suspendPolling()` during UI transitions
|
|
||||||
|
|
||||||
### API Endpoints Used
|
|
||||||
|
|
||||||
| Frontend Function | Backend Endpoint | Parameters | Description |
|
|
||||||
|-------------------|------------------|------------|-------------|
|
|
||||||
| `startWorkflow()` | `POST /api/workflows/start` | `{ prompt: string, fileIds: [] }` | Starts new workflow |
|
|
||||||
| `continueWorkflow()` | `POST /api/workflows/start?id={workflowId}` | `{ prompt: string, fileIds: [] }` | Continues existing workflow |
|
|
||||||
| `pollWorkflowStatus()` | `GET /api/workflows/{workflowId}/status` | None | Gets current workflow status |
|
|
||||||
| `pollWorkflowLogs()` | `GET /api/workflows/{workflowId}/logs?id={lastLogId}` | Optional log ID | Gets logs newer than specified ID |
|
|
||||||
| `pollWorkflowMessages()` | `GET /api/workflows/{workflowId}/messages?id={lastMessageId}` | Optional message ID | Gets messages newer than specified ID |
|
|
||||||
| `stopWorkflow()` | `POST /api/workflows/{workflowId}/stop` | None | Interrupts running workflow |
|
|
||||||
| `resetWorkflow()` | `DELETE /api/workflows/{workflowId}` | None | Cleans up workflow resources |
|
|
||||||
| `uploadFile()` | `POST /api/files/upload` | FormData with file | Uploads file and returns file ID |
|
|
||||||
| `deleteMessage()` | `DELETE /api/workflows/{workflowId}/messages/{messageId}` | None | Removes message from workflow |
|
|
||||||
| `removeFileFromMessage()` | `DELETE /api/workflows/{workflowId}/messages/{messageId}/files/{fileId}` | None | Removes file from message |
|
|
||||||
|
|
||||||
## Special Features
|
|
||||||
|
|
||||||
### File Handling
|
|
||||||
1. **File Upload**:
|
|
||||||
- Direct upload via button or drag & drop
|
|
||||||
- FileId added to `userInputState.additionalFiles`
|
|
||||||
- UI renders file in attachment list
|
|
||||||
- Each file can be removed individually
|
|
||||||
|
|
||||||
2. **File Preview**:
|
|
||||||
- Files in messages can be previewed
|
|
||||||
- Preview shows content based on file type (text, image, PDF)
|
|
||||||
- Files can be downloaded or copied to clipboard
|
|
||||||
- Various file formats supported with appropriate visualizations
|
|
||||||
|
|
||||||
### Chat Message Rendering
|
|
||||||
1. **Message Types**:
|
|
||||||
- User messages (grey background)
|
|
||||||
- Agent messages (white background)
|
|
||||||
- Moderator questions (specially formatted)
|
|
||||||
|
|
||||||
2. **Message Features**:
|
|
||||||
- Collapsible for long content
|
|
||||||
- Symbol to delete message
|
|
||||||
- File attachments with preview options
|
|
||||||
- Markdown-like formatting support
|
|
||||||
- Agent name and timestamp display
|
|
||||||
|
|
||||||
### Log Panel Features
|
|
||||||
1. **Log Types**:
|
|
||||||
- Info (blue)
|
|
||||||
- Warning (orange)
|
|
||||||
- Error (red)
|
|
||||||
|
|
||||||
2. **Log Features**:
|
|
||||||
- Log items are read selectively from API: Only the last log entry and newer ones
|
|
||||||
- Progress indicators for agent tasks; before a next log message is rendered, the last log message progress is set to 100%
|
|
||||||
- Waiting animation for ongoing processes
|
|
||||||
- Agent-specific highlighting
|
|
||||||
- Collapsible detailed information, meaning the first n (e.g., 40) characters are by default displayed with "..." at the end. By clicking on "..." the additional part can be expanded/collapsed
|
|
||||||
- Timestamp display
|
|
||||||
|
|
||||||
### Waiting States
|
|
||||||
1. **Animation**:
|
|
||||||
- Dots animation in log entries
|
|
||||||
- Spinner in Send button during loading
|
|
||||||
- Progress indicators in agent logs
|
|
||||||
|
|
||||||
2. **State Management**:
|
|
||||||
- `waiting` flag in log objects
|
|
||||||
- `waitingDotsInterval` controls animation
|
|
||||||
- `setLoadingState()` manages UI element states
|
|
||||||
|
|
||||||
## State Transitions
|
|
||||||
|
|
||||||
```
|
|
||||||
[null] → [running] // Initial prompt submission
|
|
||||||
[running] → [running] // Ongoing polling updates
|
|
||||||
[running] → [completed] // Workflow completes successfully
|
|
||||||
[running] → [stopped] // User manually stops workflow
|
|
||||||
[running] → [failed] // Error occurs during workflow
|
|
||||||
[completed] → [running] // User continues workflow with new input
|
|
||||||
[stopped] → [running] // User continues after manual stop
|
|
||||||
[failed] → [running] // User retries workflow despite error
|
|
||||||
[any] → [null] // User resets workflow
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
### Client-side Errors
|
|
||||||
1. **Network Errors**:
|
|
||||||
- Retry mechanism in polling functions
|
|
||||||
- Exponential backoff for repeated failures
|
|
||||||
- Informative error logs for debugging
|
|
||||||
|
|
||||||
2. **UI Errors**:
|
|
||||||
- Validation before state changes
|
|
||||||
- Fallback DOM element selection
|
|
||||||
- Error boundaries for component isolation
|
|
||||||
|
|
||||||
### Backend Communication Errors
|
|
||||||
1. **API Failures**:
|
|
||||||
- Error logging in console
|
|
||||||
- User-friendly error messages in UI
|
|
||||||
- Error state with option to retry
|
|
||||||
- Toast notifications for transient errors
|
|
||||||
|
|
||||||
2. **Data Inconsistencies**:
|
|
||||||
- ID matching with fallback to partial matching
|
|
||||||
- Multiple file reference methods
|
|
||||||
- Content extraction fallbacks
|
|
||||||
|
|
||||||
## Implementation Notes
|
|
||||||
|
|
||||||
1. **Module Structure**:
|
|
||||||
- `workflow.js`: Main initialization and coordination
|
|
||||||
- `workflowState.js`: State management
|
|
||||||
- `workflowApi.js`: Backend communication
|
|
||||||
- `workflowUi.js`: UI rendering
|
|
||||||
- `workflowUtils.js`: Helper functions
|
|
||||||
|
|
||||||
2. **Key Functions**:
|
|
||||||
- `workflowInit()`: Entry point for initialization
|
|
||||||
- `workflowStatusUpdate()`: Core state transition function
|
|
||||||
- `workflowStatusPoll()`: Main update loop
|
|
||||||
- `workflowUserInput()`: Handles user input submission
|
|
||||||
- `renderLogs()` and `renderMessages()`: UI update functions
|
|
||||||
|
|
||||||
3. **Performance Optimizations**:
|
|
||||||
- Selective DOM updates
|
|
||||||
- Scroll position preservation
|
|
||||||
- Data size estimation for statistics
|
|
||||||
- Conditional re-rendering
|
|
||||||
48
notes/produce_diagrams.md
Normal file
48
notes/produce_diagrams.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
MERMAID DIAGRAM:
|
||||||
|
|
||||||
|
can you make chart "wiki/diagramm_komponenten.mermaid". produce an component diagram, based on current code in poweron/*
|
||||||
|
if document existsadd missing components, remove obsolete components.
|
||||||
|
|
||||||
|
in box texts to use <br> instead of \n
|
||||||
|
|
||||||
|
for all subgraphs to to add path on a separate line to find the module in the code.
|
||||||
|
|
||||||
|
read all code modules caerfully to identify all components and their relations.
|
||||||
|
|
||||||
|
connectors without texts, only lines.
|
||||||
|
|
||||||
|
to add connector between frontend and backend (apiCalls.js -> app.py)
|
||||||
|
|
||||||
|
to connect app.py (Main application module) with the route*.py
|
||||||
|
|
||||||
|
to put all items of frontend into subgraph "Frontend"
|
||||||
|
to put all items of gateway into subgraph "Gateway"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
to put following boxes to a dedicated subgraph within their existing subgraph:
|
||||||
|
- workflowManager.py, workflowAgentsRegistry.py, documentProcessor.py, --> "Workflow"
|
||||||
|
- mimeUtils.py, defAttributes.py, configuration.py, autho.py --> "Shared"
|
||||||
|
- agent*.py --> "Agents"
|
||||||
|
- workflow*.js --> "Workflow"
|
||||||
|
- all *.js in js/modules/ not starting with workflow* --> "Administration"
|
||||||
|
- formGeneric.js not to put to subgraph "Shared", but to a separated subgraph "Shared
|
||||||
|
|
||||||
|
to connect the main.js (main app in the frontend) to nativation.js, globalState.js, login.js, register.js, msftCall.js, config.js
|
||||||
|
|
||||||
|
to connect navigation.js to moduleLoader.js
|
||||||
|
|
||||||
|
to connect moduleLoader.js to workflow.js, and all *.js in js/modules/ not starting with workflow*
|
||||||
|
|
||||||
|
to connect all *.js in js/modules/ not starting with workflow* --> formGeneric.js
|
||||||
|
|
||||||
|
to connect fomrGeneric.js --> apiCalls.js
|
||||||
|
|
||||||
|
|
||||||
|
to use underscores (e.g. Backend_Python, Workflow_Modules, etc.) for all subgraph titles.
|
||||||
|
|
||||||
|
if adding legend, then to give same colors like references to legend
|
||||||
|
|
||||||
|
|
@ -90,6 +90,7 @@ Für größere Installationen die JSON-basierte Datenbank ersetzen durch:
|
||||||
### git permanent login with vs code
|
### git permanent login with vs code
|
||||||
git remote set-url origin https://valueon@github.com/valueonag/gateway
|
git remote set-url origin https://valueon@github.com/valueonag/gateway
|
||||||
git remote set-url origin https://valueon@github.com/valueonag/frontend_agents
|
git remote set-url origin https://valueon@github.com/valueonag/frontend_agents
|
||||||
|
git remote set-url origin https://valueon@github.com/valueonag/wiki
|
||||||
|
|
||||||
## Lizenz
|
## Lizenz
|
||||||
|
|
||||||
|
|
|
||||||
105
notes/start.sh
105
notes/start.sh
|
|
@ -1,105 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Erkennen des Betriebssystems
|
|
||||||
case "$(uname -s)" in
|
|
||||||
CYGWIN*|MINGW*|MSYS*|Windows*)
|
|
||||||
OS="Windows"
|
|
||||||
VENV_PATH="gwserver/venv/Scripts"
|
|
||||||
ACTIVATE_CMD="$VENV_PATH/activate"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
OS="Unix"
|
|
||||||
VENV_PATH="gwserver/venv/bin"
|
|
||||||
ACTIVATE_CMD="$VENV_PATH/activate"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "Erkanntes Betriebssystem: $OS"
|
|
||||||
|
|
||||||
# Prüfen, ob Python installiert ist
|
|
||||||
PYTHON_CMD=""
|
|
||||||
if command -v python3 &>/dev/null; then
|
|
||||||
PYTHON_CMD="python3"
|
|
||||||
elif command -v python &>/dev/null; then
|
|
||||||
PYTHON_CMD="python"
|
|
||||||
else
|
|
||||||
echo "Python ist nicht installiert. Bitte installieren Sie Python 3.8 oder höher."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Virtuelle Umgebung erstellen, falls sie nicht existiert
|
|
||||||
if [ ! -d "gwserver/venv" ]; then
|
|
||||||
echo "Erstelle virtuelle Python-Umgebung..."
|
|
||||||
cd gwserver
|
|
||||||
$PYTHON_CMD -m venv venv
|
|
||||||
cd ..
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Virtuelle Umgebung aktivieren
|
|
||||||
echo "Aktiviere virtuelle Umgebung..."
|
|
||||||
if [ "$OS" = "Windows" ]; then
|
|
||||||
source "$ACTIVATE_CMD" 2>/dev/null || . "$ACTIVATE_CMD" 2>/dev/null || echo "Warnung: Aktivierung der virtuellen Umgebung fehlgeschlagen. Fahre fort..."
|
|
||||||
else
|
|
||||||
source "$ACTIVATE_CMD" 2>/dev/null || . "$ACTIVATE_CMD" 2>/dev/null || echo "Warnung: Aktivierung der virtuellen Umgebung fehlgeschlagen. Fahre fort..."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Abhängigkeiten installieren
|
|
||||||
echo "Installiere Abhängigkeiten..."
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# gwserver als Hintergrundprozess starten
|
|
||||||
echo "Starte gwserver-Server..."
|
|
||||||
|
|
||||||
# START CORE
|
|
||||||
cd gwserver
|
|
||||||
if [ "$OS" = "Windows" ]; then
|
|
||||||
# Windows: starte in einem separaten Prozess
|
|
||||||
start uvicorn app:app --reload --host 0.0.0.0 --port 8000
|
|
||||||
# Keine PID-Erfassung nötig unter Windows mit 'start'
|
|
||||||
GWSERVER_PID=""
|
|
||||||
else
|
|
||||||
# Unix: starte im Hintergrund und erfasse PID
|
|
||||||
uvicorn app:app --reload --host 0.0.0.0 --port 8000 &
|
|
||||||
GWSERVER_PID=$!
|
|
||||||
fi
|
|
||||||
# END CORE
|
|
||||||
|
|
||||||
cd ..
|
|
||||||
echo "gwserver API läuft auf: http://localhost:8000"
|
|
||||||
echo "API-Dokumentation: http://localhost:8000/docs"
|
|
||||||
echo "Drücke STRG+C, um Server zu beenden"
|
|
||||||
|
|
||||||
# Funktion zum Beenden der Server bei STRG+C
|
|
||||||
cleanup() {
|
|
||||||
echo -e "\nBeende Server..."
|
|
||||||
if [ "$OS" = "Windows" ]; then
|
|
||||||
# Windows: Taskkill für uvicorn
|
|
||||||
taskkill //F //IM uvicorn.exe 2>/dev/null || echo "Konnte uvicorn nicht beenden"
|
|
||||||
else
|
|
||||||
# Unix: Verwende die erfasste PID
|
|
||||||
if [ -n "$GWSERVER_PID" ]; then
|
|
||||||
kill $GWSERVER_PID 2>/dev/null || echo "Konnte Prozess $GWSERVER_PID nicht beenden"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
echo "Server wurden beendet"
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Signal-Handler für STRG+C
|
|
||||||
trap cleanup SIGINT
|
|
||||||
|
|
||||||
# Warten auf Benutzeraktion
|
|
||||||
if [ "$OS" = "Windows" ]; then
|
|
||||||
# Unter Windows: Warte auf Eingabe
|
|
||||||
read -p "Drücke Enter zum Beenden..." dummy
|
|
||||||
cleanup
|
|
||||||
else
|
|
||||||
# Unter Unix: Warte auf Prozess
|
|
||||||
if [ -n "$GWSERVER_PID" ]; then
|
|
||||||
wait $GWSERVER_PID
|
|
||||||
else
|
|
||||||
# Fallback: einfach warten
|
|
||||||
read -p "Drücke Enter zum Beenden..." dummy
|
|
||||||
cleanup
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
81
routes/routeGeneral.py
Normal file
81
routes/routeGeneral.py
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
from fastapi import APIRouter, HTTPException, Depends, Body, status, Response
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
|
from typing import Dict, Any
|
||||||
|
from datetime import timedelta
|
||||||
|
import pathlib
|
||||||
|
import os
|
||||||
|
|
||||||
|
from modules.configuration import APP_CONFIG
|
||||||
|
from modules.auth import (
|
||||||
|
createAccessToken,
|
||||||
|
getCurrentActiveUser,
|
||||||
|
getUserContext,
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES
|
||||||
|
)
|
||||||
|
import modules.gatewayModel as gatewayModel
|
||||||
|
from modules.gatewayInterface import getGatewayInterface
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
# Static folder for favicon
|
||||||
|
baseDir = pathlib.Path(__file__).parent.parent
|
||||||
|
staticFolder = baseDir / "static"
|
||||||
|
|
||||||
|
@router.get("/favicon.ico")
|
||||||
|
async def favicon():
|
||||||
|
return FileResponse(str(staticFolder / "favicon.ico"), media_type="image/x-icon")
|
||||||
|
|
||||||
|
@router.get("/", tags=["General"])
|
||||||
|
async def root():
|
||||||
|
"""API status endpoint"""
|
||||||
|
return {"status": "online", "message": "Data Platform API is active"}
|
||||||
|
|
||||||
|
@router.get("/api/test", tags=["General"])
|
||||||
|
async def getTest():
|
||||||
|
return f"Status: OK. Alowed origins: {APP_CONFIG.get('APP_ALLOWED_ORIGINS')}"
|
||||||
|
|
||||||
|
@router.options("/{fullPath:path}", tags=["General"])
|
||||||
|
async def optionsRoute(fullPath: str):
|
||||||
|
return Response(status_code=200)
|
||||||
|
|
||||||
|
@router.get("/api/environment", tags=["General"])
|
||||||
|
async def get_environment():
|
||||||
|
"""Get environment configuration for frontend"""
|
||||||
|
return {
|
||||||
|
"apiBaseUrl": APP_CONFIG.get("APP_API_URL", ""),
|
||||||
|
"environment": APP_CONFIG.get("APP_ENV", "development"),
|
||||||
|
"instanceLabel": APP_CONFIG.get("APP_ENV_LABEL", "Development"),
|
||||||
|
# Add other environment variables the frontend might need
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.post("/api/token", response_model=gatewayModel.Token, tags=["General"])
|
||||||
|
async def loginForAccessToken(formData: OAuth2PasswordRequestForm = Depends()):
|
||||||
|
# Initialize Gateway interface without context
|
||||||
|
gateway = getGatewayInterface()
|
||||||
|
|
||||||
|
# Authenticate user
|
||||||
|
user = gateway.authenticateUser(formData.username, formData.password)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Invalid username or password",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create token with tenant ID
|
||||||
|
accessTokenExpires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
accessToken = createAccessToken(
|
||||||
|
data={
|
||||||
|
"sub": user["username"],
|
||||||
|
"mandateId": user["mandateId"]
|
||||||
|
},
|
||||||
|
expiresDelta=accessTokenExpires
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"accessToken": accessToken, "tokenType": "bearer"}
|
||||||
|
|
||||||
|
@router.get("/api/user/me", response_model=Dict[str, Any], tags=["General"])
|
||||||
|
async def readUserMe(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
|
||||||
|
return currentUser
|
||||||
Loading…
Reference in a new issue