From 6d393f9cf3dd479e30a07ad3f90f21ada8cb04c7 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Wed, 3 Dec 2025 11:18:33 +0100 Subject: [PATCH] enhanced voice center --- modules/connectors/connectorVoiceGoogle.py | 55 +++++++++ modules/interfaces/interfaceVoiceObjects.py | 38 ++++++ modules/routes/routeVoiceGoogle.py | 42 +++++++ modules/workflows/methods/methodAi.py | 42 ++++++- modules/workflows/methods/methodBase.py | 60 +++++++++- modules/workflows/methods/methodContext.py | 20 +++- modules/workflows/methods/methodOutlook.py | 112 ++++++++++++++++-- modules/workflows/methods/methodSharepoint.py | 80 +++++++++++-- 8 files changed, 425 insertions(+), 24 deletions(-) diff --git a/modules/connectors/connectorVoiceGoogle.py b/modules/connectors/connectorVoiceGoogle.py index 715772d0..faead52a 100644 --- a/modules/connectors/connectorVoiceGoogle.py +++ b/modules/connectors/connectorVoiceGoogle.py @@ -403,6 +403,61 @@ class ConnectorGoogleSpeech: "error": str(e) } + async def detectLanguage(self, text: str) -> Dict: + """ + Detect the language of text using Google Cloud Translation API. + + Args: + text: Text to detect language for + + Returns: + Dict containing detected language code and confidence + """ + try: + if not text.strip(): + logger.warning("⚠️ Empty text provided for language detection") + return { + "success": False, + "language": "", + "error": "Empty text provided" + } + + # Use a sample of the text (middle 1000 bytes or full text if smaller) + textBytes = text.encode('utf-8') + if len(textBytes) > 1000: + # Take 1000 bytes from the middle + startPos = (len(textBytes) - 1000) // 2 + textSample = textBytes[startPos:startPos + 1000].decode('utf-8', errors='ignore') + else: + textSample = text + + logger.info(f"🔍 Detecting language for text sample: '{textSample[:100]}...'") + + # Use translation API with auto-detection (source_language=None) + result = self.translate_client.translate( + textSample, + source_language=None, # Auto-detect + target_language='en' # Dummy target, we only need detection + ) + + detectedLanguage = result.get('detectedSourceLanguage', '') + + logger.info(f"✅ Language detected: {detectedLanguage}") + + return { + "success": True, + "language": detectedLanguage, + "confidence": 1.0 # Google Translation API doesn't provide confidence, assume high + } + + except Exception as e: + logger.error(f"❌ Google Cloud Language Detection error: {e}") + return { + "success": False, + "language": "", + "error": str(e) + } + async def speechToTranslatedText(self, audioContent: bytes, fromLanguage: str = "de-DE", toLanguage: str = "en") -> Dict: diff --git a/modules/interfaces/interfaceVoiceObjects.py b/modules/interfaces/interfaceVoiceObjects.py index 87cb1413..cf3a1f12 100644 --- a/modules/interfaces/interfaceVoiceObjects.py +++ b/modules/interfaces/interfaceVoiceObjects.py @@ -99,6 +99,44 @@ class VoiceObjects: # Translation Operations + async def detectLanguage(self, text: str) -> Dict[str, Any]: + """ + Detect the language of text using Google Cloud Translation API. + + Args: + text: Text to detect language for + + Returns: + Dict containing detected language code and confidence + """ + try: + logger.info(f"🔍 Language detection request: '{text[:100]}...'") + + if not text.strip(): + return { + "success": False, + "language": "", + "error": "Empty text provided" + } + + connector = self._getGoogleSpeechConnector() + result = await connector.detectLanguage(text) + + if result["success"]: + logger.info(f"✅ Language detected: {result['language']}") + else: + logger.warning(f"⚠️ Language detection failed: {result.get('error', 'Unknown error')}") + + return result + + except Exception as e: + logger.error(f"❌ Language detection error: {e}") + return { + "success": False, + "language": "", + "error": str(e) + } + async def translateText(self, text: str, sourceLanguage: str = "de", targetLanguage: str = "en") -> Dict[str, Any]: """ diff --git a/modules/routes/routeVoiceGoogle.py b/modules/routes/routeVoiceGoogle.py index 7f33b19c..605feff7 100644 --- a/modules/routes/routeVoiceGoogle.py +++ b/modules/routes/routeVoiceGoogle.py @@ -115,6 +115,48 @@ async def speech_to_text( detail=f"Speech-to-text processing failed: {str(e)}" ) +@router.post("/detect-language") +async def detect_language( + text: str = Form(...), + currentUser: User = Depends(getCurrentUser) +): + """Detect the language of text using Google Cloud Translation API.""" + try: + logger.info(f"🔍 Language detection request: '{text[:100]}...'") + + if not text.strip(): + raise HTTPException( + status_code=400, + detail="Empty text provided for language detection" + ) + + # Get voice interface + voiceInterface = _getVoiceInterface(currentUser) + + # Perform language detection + result = await voiceInterface.detectLanguage(text) + + if result["success"]: + return { + "success": True, + "language": result["language"], + "confidence": result.get("confidence", 1.0) + } + else: + raise HTTPException( + status_code=400, + detail=f"Language detection failed: {result.get('error', 'Unknown error')}" + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"❌ Language detection error: {e}") + raise HTTPException( + status_code=500, + detail=f"Language detection processing failed: {str(e)}" + ) + @router.post("/translate") async def translate_text( text: str = Form(...), diff --git a/modules/workflows/methods/methodAi.py b/modules/workflows/methods/methodAi.py index c60469c8..f3e6aed6 100644 --- a/modules/workflows/methods/methodAi.py +++ b/modules/workflows/methods/methodAi.py @@ -172,11 +172,19 @@ class MethodAi(MethodBase): if aiResponse.documents and len(aiResponse.documents) > 0: action_documents = [] for doc in aiResponse.documents: + validationMetadata = { + "actionType": "ai.process", + "resultType": normalized_result_type, + "outputFormat": output_format, + "hasDocuments": True, + "documentCount": len(aiResponse.documents) + } action_documents.append(ActionDocument( documentName=doc.documentName, documentData=doc.documentData, mimeType=doc.mimeType or output_mime_type, - sourceJson=getattr(doc, 'sourceJson', None) # Preserve source JSON for structure validation + sourceJson=getattr(doc, 'sourceJson', None), # Preserve source JSON for structure validation + validationMetadata=validationMetadata )) final_documents = action_documents @@ -188,10 +196,18 @@ class MethodAi(MethodBase): extension=extension, action_name="result" ) + validationMetadata = { + "actionType": "ai.process", + "resultType": normalized_result_type, + "outputFormat": output_format, + "hasDocuments": False, + "contentType": "text" + } action_document = ActionDocument( documentName=meaningful_name, documentData=aiResponse.content, - mimeType=output_mime_type + mimeType=output_mime_type, + validationMetadata=validationMetadata ) final_documents = [action_document] @@ -288,10 +304,20 @@ class MethodAi(MethodBase): ) from modules.datamodels.datamodelChat import ActionDocument + validationMetadata = { + "actionType": "ai.webResearch", + "prompt": prompt, + "urlList": parameters.get("urlList", []), + "country": parameters.get("country"), + "language": parameters.get("language"), + "researchDepth": parameters.get("researchDepth", "general"), + "resultFormat": "json" + } actionDocument = ActionDocument( documentName=meaningfulName, documentData=result, - mimeType="application/json" + mimeType="application/json", + validationMetadata=validationMetadata ) return ActionResult.isSuccess(documents=[actionDocument]) @@ -490,11 +516,19 @@ class MethodAi(MethodBase): rendered_content = self._applyCsvOptions(rendered_content, renderOptions) from modules.datamodels.datamodelChat import ActionDocument + validationMetadata = { + "actionType": "ai.convert", + "inputFormat": normalizedInputFormat, + "outputFormat": normalizedOutputFormat, + "hasSourceJson": True, + "conversionType": "direct_rendering" + } actionDoc = ActionDocument( documentName=f"{doc.documentName.rsplit('.', 1)[0] if '.' in doc.documentName else doc.documentName}.{normalizedOutputFormat}", documentData=rendered_content, mimeType=mime_type, - sourceJson=jsonData # Preserve source JSON for structure validation + sourceJson=jsonData, # Preserve source JSON for structure validation + validationMetadata=validationMetadata ) return ActionResult.isSuccess(documents=[actionDoc]) diff --git a/modules/workflows/methods/methodBase.py b/modules/workflows/methods/methodBase.py index 3d6742aa..5bbe76c0 100644 --- a/modules/workflows/methods/methodBase.py +++ b/modules/workflows/methods/methodBase.py @@ -18,6 +18,19 @@ def action(func): - success: bool - documents: List[ActionDocument] - error: str (if success=False) + + REQUIRED: All ActionDocument instances MUST include validationMetadata for content validation + and refinement. Without validationMetadata, results cannot be approved. + + Example validationMetadata structure: + validationMetadata = { + "actionType": "moduleName.actionName", + "param1": value1, + "param2": value2, + # ... other relevant parameters for validation + } + + See MethodBase._createValidationMetadata() for a helper method to create standard metadata. """ @wraps(func) async def wrapper(self, parameters: Dict[str, Any], *args, **kwargs): @@ -26,7 +39,14 @@ def action(func): return wrapper class MethodBase: - """Base class for all methods""" + """Base class for all methods + + IMPORTANT: All actions that return ActionDocument instances MUST include validationMetadata. + This metadata is required for content validation and refinement. Without it, results cannot + be approved by the validation system. + + Use _createValidationMetadata() helper method to create standardized metadata structures. + """ def __init__(self, services: Any): """Initialize method with services object""" @@ -168,6 +188,44 @@ class MethodBase: else: return str(type_annotation) + def _createValidationMetadata(self, actionName: str, **kwargs) -> Dict[str, Any]: + """ + Helper method to create standardized validationMetadata for ActionDocument instances. + + This method ensures all actions include the required validationMetadata structure + for content validation and refinement. Without metadata, results cannot be approved. + + Args: + actionName: Name of the action (e.g., "readEmails", "uploadDocument") + **kwargs: Additional action-specific metadata fields + + Returns: + Dictionary with validationMetadata structure including: + - actionType: Full action identifier (moduleName.actionName) + - All provided kwargs as additional metadata fields + + Example: + validationMetadata = self._createValidationMetadata( + "readEmails", + connectionReference=connectionReference, + folder=folder, + limit=limit, + emailCount=len(emails) + ) + + ActionDocument( + documentName="emails.json", + documentData=json.dumps(data), + mimeType="application/json", + validationMetadata=validationMetadata # REQUIRED + ) + """ + metadata = { + "actionType": f"{self.name}.{actionName}" + } + metadata.update(kwargs) + return metadata + def _generateMeaningfulFileName(self, base_name: str, extension: str, workflow_context: Dict[str, Any] = None, action_name: str = None) -> str: """ Generate a meaningful file name with round/task/action information. diff --git a/modules/workflows/methods/methodContext.py b/modules/workflows/methods/methodContext.py index e974606c..604093bb 100644 --- a/modules/workflows/methods/methodContext.py +++ b/modules/workflows/methods/methodContext.py @@ -78,11 +78,19 @@ class MethodContext(MethodBase): "getDocumentIndex" ) + validationMetadata = { + "actionType": "context.getDocumentIndex", + "resultType": resultType, + "workflowId": getattr(workflow, 'id', 'unknown'), + "totalDocuments": indexData.get("totalDocuments", 0) if isinstance(indexData, dict) else 0 + } + # Create ActionDocument document = ActionDocument( documentName=filename, documentData=indexContent, - mimeType="application/json" if resultType == "json" else "text/plain" + mimeType="application/json" if resultType == "json" else "text/plain", + validationMetadata=validationMetadata ) return ActionResult.isSuccess(documents=[document]) @@ -313,10 +321,18 @@ class MethodContext(MethodBase): documentName = f"document_{i+1:03d}_extracted_{extracted.id}.json" # Store ContentExtracted object in ActionDocument.documentData + validationMetadata = { + "actionType": "context.extractContent", + "documentIndex": i, + "extractedId": extracted.id, + "partCount": len(extracted.parts) if extracted.parts else 0, + "originalFileName": originalDoc.fileName if originalDoc and hasattr(originalDoc, 'fileName') else None + } actionDoc = ActionDocument( documentName=documentName, documentData=extracted, # ContentExtracted object - mimeType="application/json" + mimeType="application/json", + validationMetadata=validationMetadata ) actionDocuments.append(actionDoc) diff --git a/modules/workflows/methods/methodOutlook.py b/modules/workflows/methods/methodOutlook.py index fa7b4e47..04d02830 100644 --- a/modules/workflows/methods/methodOutlook.py +++ b/modules/workflows/methods/methodOutlook.py @@ -465,11 +465,22 @@ class MethodOutlook(MethodBase): "timestamp": self.services.utils.timestampGetUtc() } + validationMetadata = { + "actionType": "outlook.readEmails", + "connectionReference": connectionReference, + "folder": folder, + "limit": limit, + "filter": filter, + "emailCount": email_data.get("count", 0), + "outputMimeType": outputMimeType + } + return ActionResult.isSuccess( documents=[ActionDocument( documentName=f"outlook_emails_{self._format_timestamp_for_filename()}.json", documentData=json.dumps(result_data, indent=2), - mimeType="application/json" + mimeType="application/json", + validationMetadata=validationMetadata )] ) @@ -695,12 +706,23 @@ class MethodOutlook(MethodBase): "timestamp": self.services.utils.timestampGetUtc() } + validationMetadata = { + "actionType": "outlook.searchEmails", + "connectionReference": connectionReference, + "query": query, + "folder": folder, + "limit": limit, + "resultCount": search_result.get("count", 0), + "outputMimeType": outputMimeType + } + return ActionResult( success=True, documents=[ActionDocument( documentName=f"outlook_email_search_{self._format_timestamp_for_filename()}.json", documentData=json.dumps(result_data, indent=2), - mimeType="application/json" + mimeType="application/json", + validationMetadata=validationMetadata )] ) @@ -818,12 +840,22 @@ class MethodOutlook(MethodBase): "timestamp": self.services.utils.timestampGetUtc() } + validationMetadata = { + "actionType": "outlook.listDrafts", + "connectionReference": connectionReference, + "folder": folder, + "limit": limit, + "draftCount": drafts_result.get("count", 0), + "outputMimeType": outputMimeType + } + return ActionResult( success=True, documents=[ActionDocument( documentName=f"outlook_drafts_list_{self._format_timestamp_for_filename()}.json", documentData=json.dumps(result_data, indent=2), - mimeType="application/json" + mimeType="application/json", + validationMetadata=validationMetadata )] ) @@ -928,12 +960,21 @@ class MethodOutlook(MethodBase): "timestamp": self.services.utils.timestampGetUtc() } + validationMetadata = { + "actionType": "outlook.findDrafts", + "connectionReference": connectionReference, + "limit": limit, + "totalDrafts": drafts_result.get("totalDrafts", 0), + "outputMimeType": outputMimeType + } + return ActionResult( success=True, documents=[ActionDocument( documentName=f"outlook_drafts_found_{self._format_timestamp_for_filename()}.json", documentData=json.dumps(result_data, indent=2), - mimeType="application/json" + mimeType="application/json", + validationMetadata=validationMetadata )] ) @@ -1069,12 +1110,22 @@ class MethodOutlook(MethodBase): "timestamp": self.services.utils.timestampGetUtc() } + validationMetadata = { + "actionType": "outlook.checkDraftsFolder", + "connectionReference": connectionReference, + "limit": limit, + "totalDrafts": drafts_result.get("totalDrafts", 0), + "draftsFolderId": drafts_result.get("draftsFolderId"), + "outputMimeType": outputMimeType + } + return ActionResult( success=True, documents=[ActionDocument( documentName=f"outlook_drafts_folder_check_{self._format_timestamp_for_filename()}.json", documentData=json.dumps(result_data, indent=2), - mimeType="application/json" + mimeType="application/json", + validationMetadata=validationMetadata )] ) @@ -1624,34 +1675,61 @@ Return JSON: # Determine overall success status if successfulEmails == 0: + validationMetadata = { + "actionType": "outlook.sendDraftEmail", + "connectionReference": connectionReference, + "totalEmails": totalEmails, + "successfulEmails": successfulEmails, + "failedEmails": failedEmails, + "status": "all_failed" + } return ActionResult.isFailure( error=f"Failed to send all {totalEmails} email(s)", documents=[ActionDocument( documentName=f"sent_mail_confirmation_{self._format_timestamp_for_filename()}.json", documentData=json.dumps(resultData, indent=2), - mimeType="application/json" + mimeType="application/json", + validationMetadata=validationMetadata )] ) elif failedEmails > 0: # Partial success logger.warning(f"Sent {successfulEmails} out of {totalEmails} emails. {failedEmails} failed.") + validationMetadata = { + "actionType": "outlook.sendDraftEmail", + "connectionReference": connectionReference, + "totalEmails": totalEmails, + "successfulEmails": successfulEmails, + "failedEmails": failedEmails, + "status": "partial_success" + } return ActionResult( success=True, documents=[ActionDocument( documentName=f"sent_mail_confirmation_{self._format_timestamp_for_filename()}.json", documentData=json.dumps(resultData, indent=2), - mimeType="application/json" + mimeType="application/json", + validationMetadata=validationMetadata )] ) else: # All successful logger.info(f"Successfully sent all {totalEmails} email(s)") + validationMetadata = { + "actionType": "outlook.sendDraftEmail", + "connectionReference": connectionReference, + "totalEmails": totalEmails, + "successfulEmails": successfulEmails, + "failedEmails": failedEmails, + "status": "all_successful" + } return ActionResult( success=True, documents=[ActionDocument( documentName=f"sent_mail_confirmation_{self._format_timestamp_for_filename()}.json", documentData=json.dumps(resultData, indent=2), - mimeType="application/json" + mimeType="application/json", + validationMetadata=validationMetadata )] ) @@ -1693,12 +1771,19 @@ Return JSON: "status": "ready" } + validationMetadata = { + "actionType": "outlook.checkPermissions", + "connectionReference": connectionReference, + "permissionsStatus": "ready", + "hasPermissions": True + } return ActionResult( success=True, documents=[ActionDocument( documentName=f"outlook_permissions_check_{self._format_timestamp_for_filename()}.json", documentData=json.dumps(result_data, indent=2), - mimeType="application/json" + mimeType="application/json", + validationMetadata=validationMetadata )] ) else: @@ -1711,12 +1796,19 @@ Return JSON: "message": "Please re-authenticate your Microsoft connection to get updated permissions." } + validationMetadata = { + "actionType": "outlook.checkPermissions", + "connectionReference": connectionReference, + "permissionsStatus": "needs_reauthentication", + "hasPermissions": False + } return ActionResult( success=False, documents=[ActionDocument( documentName=f"outlook_permissions_check_{self._format_timestamp_for_filename()}.json", documentData=json.dumps(result_data, indent=2), - mimeType="application/json" + mimeType="application/json", + validationMetadata=validationMetadata )], error="Connection lacks necessary permissions for Outlook operations" ) diff --git a/modules/workflows/methods/methodSharepoint.py b/modules/workflows/methods/methodSharepoint.py index 92d77e8e..a433e04c 100644 --- a/modules/workflows/methods/methodSharepoint.py +++ b/modules/workflows/methods/methodSharepoint.py @@ -1072,6 +1072,13 @@ class MethodSharepoint(MethodBase): outputExtension = ".json" # Default outputMimeType = "application/json" # Default + validationMetadata = { + "actionType": "sharepoint.findDocumentPath", + "searchQuery": searchQuery, + "maxResults": maxResults, + "totalResults": len(foundDocuments), + "hasResults": len(foundDocuments) > 0 + } return ActionResult( success=True, @@ -1079,7 +1086,8 @@ class MethodSharepoint(MethodBase): ActionDocument( documentName=f"sharepoint_find_path_{self._format_timestamp_for_filename()}{outputExtension}", documentData=json.dumps(resultData, indent=2), - mimeType=outputMimeType + mimeType=outputMimeType, + validationMetadata=validationMetadata ) ] ) @@ -1336,19 +1344,40 @@ class MethodSharepoint(MethodBase): if fileContent and isinstance(fileContent, bytes): # Encode binary content as Base64 string base64Content = base64.b64encode(fileContent).decode('utf-8') + validationMetadata = { + "actionType": "sharepoint.readDocuments", + "fileName": fileName, + "sharepointFileId": resultItem.get("sharepointFileId"), + "siteName": resultItem.get("siteName"), + "mimeType": mimeType, + "contentType": "binary", + "size": len(fileContent), + "includeMetadata": includeMetadata + } actionDoc = ActionDocument( documentName=fileName, documentData=base64Content, # Base64 string for binary files - mimeType=mimeType + mimeType=mimeType, + validationMetadata=validationMetadata ) actionDocuments.append(actionDoc) logger.info(f"Stored binary file {fileName} ({len(fileContent)} bytes) as Base64 in ActionDocument") elif fileContent: # Text content - store directly in documentData + validationMetadata = { + "actionType": "sharepoint.readDocuments", + "fileName": fileName, + "sharepointFileId": resultItem.get("sharepointFileId"), + "siteName": resultItem.get("siteName"), + "mimeType": mimeType, + "contentType": "text", + "includeMetadata": includeMetadata + } actionDoc = ActionDocument( documentName=fileName, documentData=fileContent if isinstance(fileContent, str) else str(fileContent), - mimeType=mimeType + mimeType=mimeType, + validationMetadata=validationMetadata ) actionDocuments.append(actionDoc) else: @@ -1366,10 +1395,20 @@ class MethodSharepoint(MethodBase): if resultItem.get("metadata"): docData["metadata"] = resultItem["metadata"] + validationMetadata = { + "actionType": "sharepoint.readDocuments", + "fileName": fileName, + "sharepointFileId": resultItem.get("sharepointFileId"), + "siteName": resultItem.get("siteName"), + "mimeType": mimeType, + "contentType": "metadata_only", + "includeMetadata": includeMetadata + } actionDoc = ActionDocument( documentName=fileName, documentData=json.dumps(docData, indent=2), - mimeType=mimeType + mimeType=mimeType, + validationMetadata=validationMetadata ) actionDocuments.append(actionDoc) @@ -1583,6 +1622,13 @@ class MethodSharepoint(MethodBase): outputExtension = ".json" # Default outputMimeType = "application/json" # Default + validationMetadata = { + "actionType": "sharepoint.readDocuments", + "connectionReference": connectionReference, + "documentCount": len(readResults), + "includeMetadata": includeMetadata, + "sitesSearched": len(sites) + } return ActionResult( success=True, @@ -1590,7 +1636,8 @@ class MethodSharepoint(MethodBase): ActionDocument( documentName=f"sharepoint_documents_{self._format_timestamp_for_filename()}{outputExtension}", documentData=json.dumps(resultData, indent=2), - mimeType=outputMimeType + mimeType=outputMimeType, + validationMetadata=validationMetadata ) ] ) @@ -1998,6 +2045,15 @@ class MethodSharepoint(MethodBase): outputExtension = ".json" # Default outputMimeType = "application/json" # Default + validationMetadata = { + "actionType": "sharepoint.uploadDocument", + "connectionReference": connectionReference, + "uploadPath": uploadPath, + "fileNames": fileNames, + "uploadCount": len(uploadResults), + "successfulUploads": len([r for r in uploadResults if r.get("uploadStatus") == "success"]), + "failedUploads": len([r for r in uploadResults if r.get("uploadStatus") == "failed"]) + } return ActionResult( success=True, @@ -2005,7 +2061,8 @@ class MethodSharepoint(MethodBase): ActionDocument( documentName=f"sharepoint_upload_{self._format_timestamp_for_filename()}{outputExtension}", documentData=json.dumps(resultData, indent=2), - mimeType=outputMimeType + mimeType=outputMimeType, + validationMetadata=validationMetadata ) ] ) @@ -2459,6 +2516,14 @@ class MethodSharepoint(MethodBase): outputExtension = ".json" # Default outputMimeType = "application/json" # Default + validationMetadata = { + "actionType": "sharepoint.listDocuments", + "pathQuery": listQuery, + "includeSubfolders": includeSubfolders, + "sitesSearched": len(sites), + "folderCount": len(listResults), + "totalItems": sum(len(result.get("siteResults", [])) for result in listResults) + } return ActionResult( success=True, @@ -2466,7 +2531,8 @@ class MethodSharepoint(MethodBase): ActionDocument( documentName=f"sharepoint_document_list_{self._format_timestamp_for_filename()}{outputExtension}", documentData=json.dumps(resultData, indent=2), - mimeType=outputMimeType + mimeType=outputMimeType, + validationMetadata=validationMetadata ) ] )