enhanced voice center

This commit is contained in:
ValueOn AG 2025-12-03 11:18:33 +01:00
parent 9f46ca3b03
commit 6d393f9cf3
8 changed files with 425 additions and 24 deletions

View file

@ -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:

View file

@ -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]:
"""

View file

@ -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(...),

View file

@ -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])

View file

@ -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.

View file

@ -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)

View file

@ -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"
)

View file

@ -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
)
]
)