diff --git a/modules/chat/handling/handlingTasks.py b/modules/chat/handling/handlingTasks.py index 662990fd..8d783264 100644 --- a/modules/chat/handling/handlingTasks.py +++ b/modules/chat/handling/handlingTasks.py @@ -642,8 +642,13 @@ class HandlingTasks: action.result = first_doc.documentData.get("result", "") elif hasattr(first_doc, 'documentData') and isinstance(first_doc.documentData, str): action.result = first_doc.documentData - action.execResultLabel = result.resultLabel or result_label - await self.createActionMessage(action, result, workflow, result.resultLabel or result_label, created_documents, task_step, task_index) + # Preserve the action's execResultLabel for document routing + # Action methods should NOT return resultLabel - this is managed by the action handler + if not action.execResultLabel: + logger.warning(f"Action {action.execMethod}.{action.execAction} has no execResultLabel set") + # Always use the action's execResultLabel for message creation to ensure proper document routing + message_result_label = action.execResultLabel + await self.createActionMessage(action, result, workflow, message_result_label, created_documents, task_step, task_index) # Log action results logger.info(f"✓ Action completed successfully") @@ -719,7 +724,7 @@ class HandlingTasks: return ActionResult( success=result.success, documents=original_documents, # Preserve original documents field from method result - resultLabel=result.resultLabel or result_label, + resultLabel=action.execResultLabel, # Always use action's execResultLabel error=result.error or "" ) except Exception as e: @@ -728,7 +733,7 @@ class HandlingTasks: return ActionResult( success=False, documents=[], # Empty documents for error case - resultLabel=result_label, + resultLabel=action.execResultLabel, error=str(e) ) diff --git a/modules/chat/methodBase.py b/modules/chat/methodBase.py index 99c602d1..010a7994 100644 --- a/modules/chat/methodBase.py +++ b/modules/chat/methodBase.py @@ -10,7 +10,17 @@ import inspect logger = logging.getLogger(__name__) def action(func): - """Decorator to mark a method as an available action""" + """Decorator to mark a method as an available action + + IMPORTANT: Action methods should NOT return resultLabel in their ActionResult. + The resultLabel is managed by the action handler using the action's execResultLabel + from the action plan. This ensures consistent document routing throughout the workflow. + + Action methods should only return: + - success: bool + - documents: List[ActionDocument] + - error: str (if success=False) + """ @wraps(func) async def wrapper(self, parameters: Dict[str, Any], *args, **kwargs): return await func(self, parameters, *args, **kwargs) diff --git a/modules/chat/serviceCenter.py b/modules/chat/serviceCenter.py index 8ea6d000..63b94f62 100644 --- a/modules/chat/serviceCenter.py +++ b/modules/chat/serviceCenter.py @@ -468,7 +468,8 @@ class ServiceCenter: token = None token_status = "unknown" try: - token = self.interfaceApp.getToken(connection.authority.value) + # Use getConnectionToken to find token for this specific connection + token = self.interfaceApp.getConnectionToken(connection.id) if token: if hasattr(token, 'expiresAt') and token.expiresAt: current_time = get_utc_timestamp() diff --git a/modules/interfaces/interfaceAppObjects.py b/modules/interfaces/interfaceAppObjects.py index 587d5b6b..c88638fd 100644 --- a/modules/interfaces/interfaceAppObjects.py +++ b/modules/interfaces/interfaceAppObjects.py @@ -740,9 +740,13 @@ class AppObjects: "message": f"Error checking username availability: {str(e)}" } - def saveToken(self, token: Token) -> None: - """Save a token for the current user""" + def saveAccessToken(self, token: Token) -> None: + """Save an access token for the current user (must NOT have connectionId)""" try: + # Validate that this is NOT a connection token + if token.connectionId: + raise ValueError("Access tokens cannot have connectionId - use saveConnectionToken instead") + # Validate user context if not self.currentUser or not self.currentUser.id: raise ValueError("No valid user context available for token storage") @@ -758,7 +762,7 @@ class AppObjects: # Convert to dict and ensure all fields are properly set token_dict = token.dict() - # Ensure userId is set to current user (this might override the token's userId) + # Ensure userId is set to current user token_dict["userId"] = self.currentUser.id # Save to database @@ -767,19 +771,57 @@ class AppObjects: # Clear cache to ensure fresh data self._clearTableCache("tokens") - - except Exception as e: - logger.error(f"Error saving token: {str(e)}") + logger.error(f"Error saving access token: {str(e)}") raise - def getToken(self, authority: str, auto_refresh: bool = True) -> Optional[Token]: - """Get the latest valid token for the current user and authority, optionally auto-refresh if expired""" + def saveConnectionToken(self, token: Token) -> None: + """Save a connection token (must have connectionId)""" try: - # Get tokens for this user and authority + # Validate that this IS a connection token + if not token.connectionId: + raise ValueError("Connection tokens must have connectionId - use saveAccessToken instead") + + # Validate user context + if not self.currentUser or not self.currentUser.id: + raise ValueError("No valid user context available for token storage") + + # Set the user ID for the connection token + token.userId = self.currentUser.id + + # Ensure token has required fields + if not token.id: + token.id = str(uuid.uuid4()) + if not token.createdAt: + token.createdAt = get_utc_timestamp() + + # Convert to dict and ensure all fields are properly set + token_dict = token.dict() + # Ensure userId is set to current user + token_dict["userId"] = self.currentUser.id + + # Save to database + self.db.recordCreate("tokens", token_dict) + + # Clear cache to ensure fresh data + self._clearTableCache("tokens") + + except Exception as e: + logger.error(f"Error saving connection token: {str(e)}") + raise + + def getAccessToken(self, authority: str, auto_refresh: bool = True) -> Optional[Token]: + """Get the latest valid access token for the current user and authority, optionally auto-refresh if expired""" + try: + # Validate that we're not looking for connection tokens + if not self.currentUser or not self.currentUser.id: + raise ValueError("No valid user context available for token retrieval") + + # Get access tokens for this user and authority (must NOT have connectionId) tokens = self.db.getRecordset("tokens", recordFilter={ "userId": self.currentUser.id, - "authority": authority + "authority": authority, + "connectionId": None # Ensure we only get access tokens }) if not tokens: @@ -792,8 +834,6 @@ class AppObjects: # Check if token is expired if latest_token.expiresAt and latest_token.expiresAt < get_utc_timestamp(): if auto_refresh: - - # Import TokenManager here to avoid circular imports from modules.security.tokenManager import TokenManager token_manager = TokenManager() @@ -802,45 +842,58 @@ class AppObjects: refreshed_token = token_manager.refresh_token(latest_token) if refreshed_token: # Save the new token and delete the old one - self.saveToken(refreshed_token) - self.deleteToken(authority) + self.saveAccessToken(refreshed_token) + self.deleteAccessToken(authority) - return refreshed_token else: - logger.warning(f"Failed to refresh expired token for {authority}") + logger.warning(f"Failed to refresh expired access token for {authority}") return None else: - logger.warning(f"Token for {authority} is expired (expiresAt: {latest_token.expiresAt})") + logger.warning(f"Access token for {authority} is expired (expiresAt: {latest_token.expiresAt})") return None return latest_token except Exception as e: - logger.error(f"Error getting token: {str(e)}") + logger.error(f"Error getting access token: {str(e)}") return None - def getTokenForConnection(self, connectionId: str, auto_refresh: bool = True) -> Optional[Token]: - """Get the token for a specific connection, optionally auto-refresh if expired""" + def getConnectionToken(self, connectionId: str, auto_refresh: bool = True) -> Optional[Token]: + """Get the connection token for a specific connectionId, optionally auto-refresh if expired""" try: + logger.debug(f"Getting connection token for connectionId: {connectionId}") + + # Validate connectionId + if not connectionId: + raise ValueError("connectionId is required for getConnectionToken") + # Get token for this specific connection + logger.debug(f"Querying tokens table with connectionId: {connectionId}") + + # Query for specific connection tokens = self.db.getRecordset("tokens", recordFilter={ "connectionId": connectionId }) + logger.debug(f"Raw tokens from database for connectionId {connectionId}: {tokens}") + logger.debug(f"Tokens count: {len(tokens) if tokens else 0}") + if not tokens: - logger.warning(f"No token found for connection: {connectionId}") + logger.warning(f"No connection token found for connectionId: {connectionId}") return None # Sort by expiration date and get the latest (most recent expiration) + logger.debug(f"Sorting tokens by expiresAt, current tokens: {tokens}") tokens.sort(key=lambda x: x.get("expiresAt", 0), reverse=True) latest_token = Token(**tokens[0]) + logger.debug(f"Latest connection token: {latest_token}") + logger.debug(f"Token expiresAt: {latest_token.expiresAt}, type: {type(latest_token.expiresAt)}") + logger.debug(f"Current UTC timestamp: {get_utc_timestamp()}, type: {type(get_utc_timestamp())}") # Check if token is expired if latest_token.expiresAt and latest_token.expiresAt < get_utc_timestamp(): if auto_refresh: - - # Import TokenManager here to avoid circular imports from modules.security.tokenManager import TokenManager token_manager = TokenManager() @@ -849,31 +902,36 @@ class AppObjects: refreshed_token = token_manager.refresh_token(latest_token) if refreshed_token: # Save the new token and delete the old one - self.saveToken(refreshed_token) - self.deleteTokenByConnectionId(connectionId) + self.saveConnectionToken(refreshed_token) + self.deleteConnectionTokenByConnectionId(connectionId) - return refreshed_token else: - logger.warning(f"Failed to refresh expired token for connection {connectionId}") + logger.warning(f"Failed to refresh expired connection token for connectionId {connectionId}") return None else: - logger.warning(f"Token for connection {connectionId} is expired (expiresAt: {latest_token.expiresAt})") + logger.warning(f"Connection token for connectionId {connectionId} is expired (expiresAt: {latest_token.expiresAt})") return None + logger.debug(f"Returning valid connection token: {latest_token}") return latest_token except Exception as e: - logger.error(f"Error getting token for connection {connectionId}: {str(e)}") + logger.error(f"Error getting connection token for connectionId {connectionId}: {str(e)}") return None - def deleteToken(self, authority: str) -> None: - """Delete all tokens for the current user and authority""" + def deleteAccessToken(self, authority: str) -> None: + """Delete all access tokens for the current user and authority""" try: - # Get tokens to delete + # Validate user context + if not self.currentUser or not self.currentUser.id: + raise ValueError("No valid user context available for token deletion") + + # Get access tokens to delete (must NOT have connectionId) tokens = self.db.getRecordset("tokens", recordFilter={ "userId": self.currentUser.id, - "authority": authority + "authority": authority, + "connectionId": None # Ensure we only delete access tokens }) # Delete each token @@ -884,13 +942,17 @@ class AppObjects: self._clearTableCache("tokens") except Exception as e: - logger.error(f"Error deleting token: {str(e)}") + logger.error(f"Error deleting access token: {str(e)}") raise - def deleteTokenByConnectionId(self, connectionId: str) -> None: - """Delete all tokens for a specific connection""" + def deleteConnectionTokenByConnectionId(self, connectionId: str) -> None: + """Delete all connection tokens for a specific connectionId""" try: - # Get tokens to delete + # Validate connectionId + if not connectionId: + raise ValueError("connectionId is required for deleteConnectionTokenByConnectionId") + + # Get connection tokens to delete tokens = self.db.getRecordset("tokens", recordFilter={ "connectionId": connectionId }) @@ -903,9 +965,15 @@ class AppObjects: self._clearTableCache("tokens") except Exception as e: - logger.error(f"Error deleting token for connection {connectionId}: {str(e)}") + logger.error(f"Error deleting connection token for connectionId {connectionId}: {str(e)}") raise + # Backward compatibility method + def getTokenForConnection(self, connectionId: str, auto_refresh: bool = True) -> Optional[Token]: + """Backward compatibility method - use getConnectionToken instead""" + logger.warning("getTokenForConnection is deprecated, use getConnectionToken instead") + return self.getConnectionToken(connectionId, auto_refresh) + def cleanupExpiredTokens(self) -> int: """Clean up expired tokens for all connections, returns count of cleaned tokens""" try: diff --git a/modules/interfaces/interfaceChatModel.py b/modules/interfaces/interfaceChatModel.py index 1e2363a5..f698d66b 100644 --- a/modules/interfaces/interfaceChatModel.py +++ b/modules/interfaces/interfaceChatModel.py @@ -31,32 +31,41 @@ register_model_labels( ) class ActionResult(BaseModel, ModelMixin): - """Clean action result with documents as primary output""" + """Clean action result with documents as primary output + + IMPORTANT: Action methods should NOT set resultLabel in their return value. + The resultLabel is managed by the action handler using the action's execResultLabel + from the action plan. This ensures consistent document routing throughout the workflow. + """ # Core result success: bool = Field(description="Whether execution succeeded") error: Optional[str] = Field(None, description="Error message if failed") # Primary output - documents documents: List[ActionDocument] = Field(default_factory=list, description="Document outputs") - resultLabel: Optional[str] = Field(None, description="Label for document routing") + resultLabel: Optional[str] = Field(None, description="Label for document routing (set by action handler, not by action methods)") @classmethod - def success(cls, documents: List[ActionDocument] = None, resultLabel: str = None) -> 'ActionResult': - """Create a successful action result""" + def success(cls, documents: List[ActionDocument] = None) -> 'ActionResult': + """Create a successful action result + + Note: Do not set resultLabel - this is managed by the action handler + """ return cls( success=True, - documents=documents or [], - resultLabel=resultLabel + documents=documents or [] ) @classmethod - def failure(cls, error: str, documents: List[ActionDocument] = None, resultLabel: str = None) -> 'ActionResult': - """Create a failed action result""" + def failure(cls, error: str, documents: List[ActionDocument] = None) -> 'ActionResult': + """Create a failed action result + + Note: Do not set resultLabel - this is managed by the action handler + """ return cls( success=False, documents=documents or [], - error=error, - resultLabel=resultLabel + error=error ) # Register labels for ActionResult diff --git a/modules/methods/methodOutlook.py b/modules/methods/methodOutlook.py index 15d5bae8..65a6c3f2 100644 --- a/modules/methods/methodOutlook.py +++ b/modules/methods/methodOutlook.py @@ -102,18 +102,25 @@ class MethodOutlook(MethodBase): Helper function to get Microsoft connection details. """ try: + logger.debug(f"Getting Microsoft connection for reference: {connectionReference}") + # Get the connection from the service userConnection = self.service.getUserConnectionFromConnectionReference(connectionReference) if not userConnection: logger.error(f"Connection not found: {connectionReference}") return None + logger.debug(f"Found connection: {userConnection.id}, status: {userConnection.status.value}, authority: {userConnection.authority.value}") + # Get the token for this specific connection - token = self.service.interfaceApp.getTokenForConnection(userConnection.id) + token = self.service.interfaceApp.getConnectionToken(userConnection.id) if not token: logger.error(f"Token not found for connection: {userConnection.id}") + logger.debug(f"Connection details: {userConnection}") return None + logger.debug(f"Token retrieved for connection {userConnection.id}") + # Check if token is expired if hasattr(token, 'expiresAt') and token.expiresAt: current_time = get_utc_timestamp() @@ -126,8 +133,6 @@ class MethodOutlook(MethodBase): logger.error(f"Connection is not active: {userConnection.id}, status: {userConnection.status.value}") return None - - return { "id": userConnection.id, "accessToken": token.tokenAccess, @@ -482,8 +487,7 @@ class MethodOutlook(MethodBase): "timestamp": get_utc_timestamp() }, "mimeType": "application/json" - }], - resultLabel="outlook_emails" + }] ) except Exception as e: @@ -529,8 +533,10 @@ class MethodOutlook(MethodBase): ) # Get Microsoft connection + logger.debug(f"Getting Microsoft connection for sendEmail action") connection = self._getMicrosoftConnection(connectionReference) if not connection: + logger.error(f"Failed to get Microsoft connection for reference: {connectionReference}") return ActionResult.failure(error="Failed to get Microsoft connection") # Get the composed email document @@ -777,8 +783,7 @@ class MethodOutlook(MethodBase): "timestamp": get_utc_timestamp() }, "mimeType": "application/json" - }], - resultLabel="email_draft_created" + }] ) else: logger.error(f"Failed to create draft. Status: {response.status_code}, Response: {response.text}") @@ -815,8 +820,7 @@ class MethodOutlook(MethodBase): "timestamp": get_utc_timestamp() }, "mimeType": "application/json" - }], - resultLabel="outlook_email_draft" + }] ) except Exception as e: @@ -998,8 +1002,7 @@ class MethodOutlook(MethodBase): "timestamp": get_utc_timestamp() }, "mimeType": "application/json" - }], - resultLabel="outlook_email_search" + }] ) except Exception as e: @@ -1122,8 +1125,7 @@ class MethodOutlook(MethodBase): "timestamp": get_utc_timestamp() }, "mimeType": "application/json" - }], - resultLabel="outlook_drafts_list" + }] ) except Exception as e: @@ -1233,8 +1235,7 @@ class MethodOutlook(MethodBase): "timestamp": get_utc_timestamp() }, "mimeType": "application/json" - }], - resultLabel="outlook_drafts_found" + }] ) except Exception as e: @@ -1377,8 +1378,7 @@ class MethodOutlook(MethodBase): "timestamp": get_utc_timestamp() }, "mimeType": "application/json" - }], - resultLabel="outlook_drafts_folder_check" + }] ) except Exception as e: @@ -1618,8 +1618,7 @@ class MethodOutlook(MethodBase): "documentName": f"composed_email_{int(get_utc_timestamp())}.json", "documentData": result_data, "mimeType": "application/json" - }], - resultLabel="composed_email" + }] ) except Exception as e: @@ -1662,8 +1661,7 @@ class MethodOutlook(MethodBase): "status": "ready" }, "mimeType": "application/json" - }], - resultLabel="permissions_ready" + }] ) else: return ActionResult( @@ -1680,8 +1678,7 @@ class MethodOutlook(MethodBase): }, "mimeType": "application/json" }], - error="Connection lacks necessary permissions for Outlook operations", - resultLabel="permissions_missing" + error="Connection lacks necessary permissions for Outlook operations" ) except Exception as e: diff --git a/modules/methods/methodSharepoint.py b/modules/methods/methodSharepoint.py index bd2047fe..361c804b 100644 --- a/modules/methods/methodSharepoint.py +++ b/modules/methods/methodSharepoint.py @@ -44,7 +44,7 @@ class MethodSharepoint(MethodBase): return None # Get the token for this specific connection - token = self.service.interfaceApp.getTokenForConnection(userConnection.id) + token = self.service.interfaceApp.getConnectionToken(userConnection.id) if not token: logger.warning(f"No token found for connection {userConnection.id}") return None @@ -283,8 +283,7 @@ class MethodSharepoint(MethodBase): "documentData": result_data, "mimeType": output_mime_type } - ], - resultLabel="sharepoint_find_path" + ] ) except Exception as e: @@ -474,8 +473,7 @@ class MethodSharepoint(MethodBase): "documentData": result_data, "mimeType": output_mime_type } - ], - resultLabel="sharepoint_documents" + ] ) except Exception as e: logger.error(f"Error reading SharePoint documents: {str(e)}") @@ -652,8 +650,7 @@ class MethodSharepoint(MethodBase): "documentData": result_data, "mimeType": output_mime_type } - ], - resultLabel="sharepoint_upload" + ] ) except Exception as e: @@ -861,8 +858,7 @@ class MethodSharepoint(MethodBase): "documentData": result_data, "mimeType": output_mime_type } - ], - resultLabel="sharepoint_document_list" + ] ) except Exception as e: diff --git a/modules/methods/methodWeb.py b/modules/methods/methodWeb.py index 36f20143..47945cf4 100644 --- a/modules/methods/methodWeb.py +++ b/modules/methods/methodWeb.py @@ -562,8 +562,7 @@ class MethodWeb(MethodBase): "documentData": result_data, "mimeType": output_mime_type } - ], - resultLabel="web_search_results" + ] ) except Exception as e: @@ -698,8 +697,7 @@ class MethodWeb(MethodBase): "documentData": result_data, "mimeType": output_mime_type } - ], - resultLabel="web_crawl_results" + ] ) except Exception as e: @@ -803,8 +801,7 @@ class MethodWeb(MethodBase): "documentData": result_data, "mimeType": output_mime_type } - ], - resultLabel="web_scrape_results" + ] ) except Exception as e: diff --git a/modules/routes/routeSecurityGoogle.py b/modules/routes/routeSecurityGoogle.py index bfa80f4e..b297680d 100644 --- a/modules/routes/routeSecurityGoogle.py +++ b/modules/routes/routeSecurityGoogle.py @@ -223,9 +223,9 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse createdAt=get_utc_timestamp() ) - # Save token + # Save access token (no connectionId) appInterface = getInterface(user) - appInterface.saveToken(token) + appInterface.saveAccessToken(token) # Return success page with token data return HTMLResponse( @@ -347,7 +347,7 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse expiresAt=create_expiration_timestamp(token_response.get("expires_in", 0)), createdAt=get_utc_timestamp() ) - interface.saveToken(token) + interface.saveConnectionToken(token) # Return success page with connection data return HTMLResponse( @@ -491,7 +491,7 @@ async def refresh_token( logger.debug(f"Found Google connection: {google_connection.id}, status={google_connection.status}") # Get the token for this specific connection using the new method - current_token = appInterface.getTokenForConnection(google_connection.id, auto_refresh=False) + current_token = appInterface.getConnectionToken(google_connection.id, auto_refresh=False) if not current_token: raise HTTPException( @@ -507,9 +507,9 @@ async def refresh_token( refreshed_token = token_manager.refresh_token(current_token) if refreshed_token: - # Save the new token and delete the old one - appInterface.saveToken(refreshed_token) - appInterface.deleteTokenByConnectionId(google_connection.id) + # Save the new connection token and delete the old one + appInterface.saveConnectionToken(refreshed_token) + appInterface.deleteConnectionTokenByConnectionId(google_connection.id) # Update the connection's expiration time google_connection.expiresAt = float(refreshed_token.expiresAt) diff --git a/modules/routes/routeSecurityLocal.py b/modules/routes/routeSecurityLocal.py index 4d53d216..76b3bb1a 100644 --- a/modules/routes/routeSecurityLocal.py +++ b/modules/routes/routeSecurityLocal.py @@ -103,8 +103,8 @@ async def login( expiresAt=expires_at.timestamp() ) - # Save token - userInterface.saveToken(token) + # Save access token + userInterface.saveAccessToken(token) # Create response data response_data = { diff --git a/modules/routes/routeSecurityMsft.py b/modules/routes/routeSecurityMsft.py index 90a957c9..d46e4897 100644 --- a/modules/routes/routeSecurityMsft.py +++ b/modules/routes/routeSecurityMsft.py @@ -173,9 +173,9 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse createdAt=get_utc_timestamp() ) - # Save token + # Save access token (no connectionId) appInterface = getInterface(user) - appInterface.saveToken(token) + appInterface.saveAccessToken(token) # Create JWT token data jwt_token_data = { @@ -198,8 +198,8 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse createdAt=get_utc_timestamp() ) - # Save JWT token - appInterface.saveToken(jwt_token_obj) + # Save JWT access token + appInterface.saveAccessToken(jwt_token_obj) # Convert token to dict and ensure proper timestamp handling token_dict = jwt_token_obj.to_dict() @@ -328,7 +328,7 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse ) - interface.saveToken(token) + interface.saveConnectionToken(token) # Return success page with connection data @@ -499,7 +499,7 @@ async def refresh_token( # Get the token for this specific connection using the new method # Enable auto-refresh to handle expired tokens gracefully - current_token = appInterface.getTokenForConnection(msft_connection.id, auto_refresh=True) + current_token = appInterface.getConnectionToken(msft_connection.id, auto_refresh=True) if not current_token: raise HTTPException( @@ -515,9 +515,9 @@ async def refresh_token( refreshed_token = token_manager.refresh_token(current_token) if refreshed_token: - # Save the new token and delete the old one - appInterface.saveToken(refreshed_token) - appInterface.deleteTokenByConnectionId(msft_connection.id) + # Save the new connection token and delete the old one + appInterface.saveConnectionToken(refreshed_token) + appInterface.deleteConnectionTokenByConnectionId(msft_connection.id) # Update the connection's expiration time msft_connection.expiresAt = float(refreshed_token.expiresAt) diff --git a/notes/changelog.txt b/notes/changelog.txt index b4c71704..b7b189c1 100644 --- a/notes/changelog.txt +++ b/notes/changelog.txt @@ -1,6 +1,12 @@ TODO +- ui: Besseres Rendering der Tasks, Actions, Files (hierarchisch eingerückt) und der Log Entries ohne Rahmen +- ui: Beim Laden des Workflows die Logs und Messages synchron laden chronologisch +- documents: Sprechende Filenamen für user, ein Label für die interne Nutzung +- Chat: Pro Action und Task eine Message an den User in der UserLanguage + + - check history --> tasks? - model reference diagram for all models. who uses who? --> to see the basic building blocks