From 2d7da8a66d5b3f90f0cf7d0a85d8b04b03a834f0 Mon Sep 17 00:00:00 2001
From: patrick-motsch
Date: Thu, 19 Feb 2026 00:09:00 +0100
Subject: [PATCH] fix: resolve STT AttributeError and int/str TypeError in
teamsbot service
Co-authored-by: Cursor
---
.../features/teamsbot/routeFeatureTeamsbot.py | 4 +-
modules/features/teamsbot/service.py | 48 ++---
.../trustee/datamodelFeatureTrustee.py | 86 ++------
.../trustee/interfaceFeatureTrustee.py | 187 ++----------------
modules/features/trustee/mainTrustee.py | 15 +-
5 files changed, 54 insertions(+), 286 deletions(-)
diff --git a/modules/features/teamsbot/routeFeatureTeamsbot.py b/modules/features/teamsbot/routeFeatureTeamsbot.py
index 8619505f..f426ae1b 100644
--- a/modules/features/teamsbot/routeFeatureTeamsbot.py
+++ b/modules/features/teamsbot/routeFeatureTeamsbot.py
@@ -490,7 +490,7 @@ def _getEffectiveConfig(instanceId: str, userId: str, interface) -> TeamsbotConf
overrides[field] = value
if overrides:
- return baseConfig.model_copy(update=overrides)
+ return TeamsbotConfig.model_validate({**baseConfig.model_dump(), **overrides})
return baseConfig
@@ -1228,7 +1228,7 @@ async def botWebsocket(
if value is not None:
overrides[field] = value
if overrides:
- config = config.model_copy(update=overrides)
+ config = TeamsbotConfig.model_validate({**config.model_dump(), **overrides})
logger.info(f"Browser Bot WebSocket: Applied user settings overrides: {list(overrides.keys())}")
service = TeamsbotService(originalUser, mandateId, instanceId, config)
diff --git a/modules/features/teamsbot/service.py b/modules/features/teamsbot/service.py
index b00b6ccb..40c8962c 100644
--- a/modules/features/teamsbot/service.py
+++ b/modules/features/teamsbot/service.py
@@ -400,38 +400,30 @@ class TeamsbotService:
if len(audioBytes) < 1000:
return
- # Use the existing Google Cloud Speech connector for STT
- speechConnector = voiceInterface.getSpeechConnector() if voiceInterface else None
- if not speechConnector or not hasattr(speechConnector, 'speech_client'):
- logger.warning(f"[AudioChunk] No speech client available for session {sessionId}")
+ if not voiceInterface:
+ logger.warning(f"[AudioChunk] No voice interface available for session {sessionId}")
return
- from google.cloud import speech
- config = speech.RecognitionConfig(
- encoding=speech.RecognitionConfig.AudioEncoding.LINEAR16,
- sample_rate_hertz=sampleRate,
- language_code=self.config.language or "de-DE",
- enable_automatic_punctuation=True,
+ sttResult = await voiceInterface.speechToText(
+ audioContent=audioBytes,
+ language=self.config.language or "de-DE",
+ sampleRate=sampleRate,
)
- audio = speech.RecognitionAudio(content=audioBytes)
- response = speechConnector.speech_client.recognize(config=config, audio=audio)
-
- for result in response.results:
- if result.alternatives:
- text = result.alternatives[0].transcript.strip()
- if text:
- logger.info(f"[AudioChunk] STT result: {text[:80]}...")
- await self._processTranscript(
- sessionId=sessionId,
- speaker="Meeting Audio",
- text=text,
- isFinal=True,
- interface=interface,
- voiceInterface=voiceInterface,
- websocket=websocket,
- source="audioCapture",
- )
+ if sttResult and sttResult.get("success") and sttResult.get("text"):
+ text = sttResult["text"].strip()
+ if text:
+ logger.info(f"[AudioChunk] STT result: {text[:80]}...")
+ await self._processTranscript(
+ sessionId=sessionId,
+ speaker="Meeting Audio",
+ text=text,
+ isFinal=True,
+ interface=interface,
+ voiceInterface=voiceInterface,
+ websocket=websocket,
+ source="audioCapture",
+ )
except Exception as e:
logger.error(f"[AudioChunk] STT error for session {sessionId}: {type(e).__name__}: {e}")
diff --git a/modules/features/trustee/datamodelFeatureTrustee.py b/modules/features/trustee/datamodelFeatureTrustee.py
index 1402f91e..e1282e3b 100644
--- a/modules/features/trustee/datamodelFeatureTrustee.py
+++ b/modules/features/trustee/datamodelFeatureTrustee.py
@@ -1,6 +1,6 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
-"""Trustee models: TrusteeOrganisation, TrusteeRole, TrusteeAccess, TrusteeContract, TrusteeDocument, TrusteePosition, TrusteePositionDocument."""
+"""Trustee models: TrusteeOrganisation, TrusteeRole, TrusteeAccess, TrusteeContract, TrusteeDocument, TrusteePosition."""
from typing import Optional
from pydantic import BaseModel, Field
@@ -384,9 +384,8 @@ registerModelLabels(
class TrusteePosition(BaseModel):
"""Contains booking positions (expense entries).
- Note: organisationId and contractId removed as per architecture decision:
- - The feature instance IS the organisation
- - Contracts are eliminated from the model
+ Each position references exactly one source document via documentId (1:N relationship).
+ One document (e.g. bank statement) can generate many positions.
"""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
@@ -397,6 +396,16 @@ class TrusteePosition(BaseModel):
"frontend_required": False
}
)
+ documentId: Optional[str] = Field(
+ default=None,
+ description="Reference to TrusteeDocument.id (source document that generated this position)",
+ json_schema_extra={
+ "frontend_type": "select",
+ "frontend_readonly": False,
+ "frontend_required": False,
+ "frontend_options": "/api/trustee/{instanceId}/documents/options"
+ }
+ )
valuta: Optional[str] = Field(
default=None,
description="Value date (ISO format: YYYY-MM-DD)",
@@ -538,6 +547,7 @@ registerModelLabels(
{"en": "Position", "fr": "Position", "de": "Position"},
{
"id": {"en": "ID", "fr": "ID", "de": "ID"},
+ "documentId": {"en": "Document", "fr": "Document", "de": "Dokument"},
"valuta": {"en": "Value Date", "fr": "Date de valeur", "de": "Valutadatum"},
"transactionDateTime": {"en": "Transaction Date/Time", "fr": "Date/Heure de transaction", "de": "Transaktionszeitpunkt"},
"company": {"en": "Company", "fr": "Entreprise", "de": "Firma"},
@@ -555,71 +565,3 @@ registerModelLabels(
)
-class TrusteePositionDocument(BaseModel):
- """Cross-reference table linking positions to documents (many-to-many).
-
- Note: organisationId and contractId removed as per architecture decision:
- - The feature instance IS the organisation
- - Contracts are eliminated from the model
- """
- id: str = Field(
- default_factory=lambda: str(uuid.uuid4()),
- description="Unique link ID",
- json_schema_extra={
- "frontend_type": "text",
- "frontend_readonly": True,
- "frontend_required": False
- }
- )
- documentId: str = Field(
- description="Reference to TrusteeDocument.id",
- json_schema_extra={
- "frontend_type": "select",
- "frontend_readonly": False,
- "frontend_required": True,
- "frontend_options": "/api/trustee/{instanceId}/documents/options"
- }
- )
- positionId: str = Field(
- description="Reference to TrusteePosition.id",
- json_schema_extra={
- "frontend_type": "select",
- "frontend_readonly": False,
- "frontend_required": True,
- "frontend_options": "/api/trustee/{instanceId}/positions/options"
- }
- )
- mandateId: Optional[str] = Field(
- default=None,
- description="Mandate ID (auto-set from context)",
- json_schema_extra={
- "frontend_type": "text",
- "frontend_readonly": True,
- "frontend_required": False,
- "frontend_hidden": True
- }
- )
- featureInstanceId: Optional[str] = Field(
- default=None,
- description="Feature Instance ID for instance-level isolation (auto-set from context)",
- json_schema_extra={
- "frontend_type": "text",
- "frontend_readonly": True,
- "frontend_required": False,
- "frontend_hidden": True
- }
- )
- # System attributes are automatically set by DatabaseConnector
-
-
-registerModelLabels(
- "TrusteePositionDocument",
- {"en": "Position-Document Link", "fr": "Lien Position-Document", "de": "Position-Dokument-Verknüpfung"},
- {
- "id": {"en": "ID", "fr": "ID", "de": "ID"},
- "documentId": {"en": "Document", "fr": "Document", "de": "Dokument"},
- "positionId": {"en": "Position", "fr": "Position", "de": "Position"},
- "mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
- "featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
- },
-)
diff --git a/modules/features/trustee/interfaceFeatureTrustee.py b/modules/features/trustee/interfaceFeatureTrustee.py
index 7b3e4a6b..9ead992d 100644
--- a/modules/features/trustee/interfaceFeatureTrustee.py
+++ b/modules/features/trustee/interfaceFeatureTrustee.py
@@ -23,7 +23,6 @@ from .datamodelFeatureTrustee import (
TrusteeContract,
TrusteeDocument,
TrusteePosition,
- TrusteePositionDocument,
)
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResult
@@ -1107,10 +1106,8 @@ class TrusteeObjects:
def deleteDocument(self, documentId: str) -> bool:
"""Delete a document.
- All position-document cross-table entries (TrusteePositionDocument) referencing
- this document are deleted first, then the document.
+ Positions referencing this document will have their documentId set to None.
"""
- # Get existing document to check creator
existingRecords = self.db.getRecordset(TrusteeDocument, recordFilter={"id": documentId})
existing = existingRecords[0] if existingRecords else None
@@ -1120,12 +1117,17 @@ class TrusteeObjects:
createdBy = existing.get("_createdBy")
- # Check system RBAC permission (userreport can only delete their own records)
if not self.checkCombinedPermission(TrusteeDocument, "delete", recordCreatedBy=createdBy):
logger.warning(f"User {self.userId} lacks permission to delete document")
return False
- self._deletePositionDocumentLinksForDocument(documentId)
+ # Clear documentId on positions that reference this document
+ positions = self.db.getRecordset(TrusteePosition, recordFilter={"documentId": documentId})
+ for pos in positions:
+ posId = pos.get("id")
+ if posId:
+ self.db.recordModify(TrusteePosition, posId, {"documentId": None})
+
return self.db.recordDelete(TrusteeDocument, documentId)
# ===== Position CRUD =====
@@ -1276,12 +1278,7 @@ class TrusteeObjects:
return TrusteePosition(**{k: v for k, v in updatedRecord.items() if not k.startswith("_")})
def deletePosition(self, positionId: str) -> bool:
- """Delete a position.
-
- All position-document cross-table entries (TrusteePositionDocument) referencing
- this position are deleted first, then the position.
- """
- # Get existing position to check creator
+ """Delete a position."""
existingRecords = self.db.getRecordset(TrusteePosition, recordFilter={"id": positionId})
existing = existingRecords[0] if existingRecords else None
@@ -1291,177 +1288,27 @@ class TrusteeObjects:
createdBy = existing.get("_createdBy")
- # Check system RBAC permission (userreport can only delete their own records)
if not self.checkCombinedPermission(TrusteePosition, "delete", recordCreatedBy=createdBy):
logger.warning(f"User {self.userId} lacks permission to delete position")
return False
- self._deletePositionDocumentLinksForPosition(positionId)
return self.db.recordDelete(TrusteePosition, positionId)
- # ===== Position-Document Link CRUD =====
+ # ===== Position-Document Queries =====
- def createPositionDocument(self, data: Dict[str, Any]) -> Optional[TrusteePositionDocument]:
- """Create a new position-document link.
-
- Note: organisationId and contractId removed - feature instance IS the organisation.
- Permission is checked via system RBAC (feature-level access).
- """
- # Check system RBAC permission
- if not self.checkCombinedPermission(TrusteePositionDocument, "create"):
- logger.warning(f"User {self.userId} lacks permission to create position-document link")
- return None
-
- # Auto-set context fields
- data["mandateId"] = self.mandateId
- data["featureInstanceId"] = self.featureInstanceId
-
- import uuid
- linkId = data.get("id") or str(uuid.uuid4())
- data["id"] = linkId
-
- createdRecord = self.db.recordCreate(TrusteePositionDocument, data)
- if createdRecord and createdRecord.get("id"):
- return TrusteePositionDocument(**{k: v for k, v in createdRecord.items() if not k.startswith("_")})
- return None
-
- def getPositionDocument(self, linkId: str) -> Optional[TrusteePositionDocument]:
- """Get a single position-document link by ID."""
- records = self.db.getRecordset(TrusteePositionDocument, recordFilter={"id": linkId})
- if not records:
- return None
- return TrusteePositionDocument(**{k: v for k, v in records[0].items() if not k.startswith("_")})
-
- def updatePositionDocument(self, linkId: str, data: Dict[str, Any]) -> Optional[TrusteePositionDocument]:
- """Update a position-document link."""
- # Check permission
- if not self.checkCombinedPermission(TrusteePositionDocument, "update"):
- logger.warning(f"User {self.userId} lacks permission to update position-document link")
- return None
-
- # Verify link exists and belongs to this instance
- existing = self.db.getRecordset(TrusteePositionDocument, recordFilter={"id": linkId})
- if not existing:
- logger.warning(f"Position-document link {linkId} not found")
- return None
-
- existingRecord = existing[0]
- if existingRecord.get("featureInstanceId") != self.featureInstanceId:
- logger.warning(f"Link {linkId} belongs to different instance")
- return None
-
- # Prevent changing context fields
- data.pop("id", None)
- data.pop("mandateId", None)
- data.pop("featureInstanceId", None)
-
- updatedRecord = self.db.recordModify(TrusteePositionDocument, linkId, data)
- if updatedRecord and updatedRecord.get("id"):
- return TrusteePositionDocument(**{k: v for k, v in updatedRecord.items() if not k.startswith("_")})
- return None
-
- def getAllPositionDocuments(self, params: Optional[PaginationParams] = None) -> PaginatedResult:
- """Get all position-document links with RBAC filtering + feature-level access filtering."""
- # Step 1: System RBAC filtering with per-row permissions
+ def getPositionsByDocument(self, documentId: str) -> List[TrusteePosition]:
+ """Get all positions that reference a specific document (1:N via documentId FK)."""
records = getRecordsetWithRBAC(
connector=self.db,
- modelClass=TrusteePositionDocument,
- currentUser=self.currentUser,
- recordFilter=None,
- orderBy="id",
- mandateId=self.mandateId,
- featureInstanceId=self.featureInstanceId,
- enrichPermissions=True,
- featureCode=self.FEATURE_CODE
- )
-
- totalItems = len(records)
- if params:
- pageSize = params.pageSize or 20
- page = params.page or 1
- startIdx = (page - 1) * pageSize
- endIdx = startIdx + pageSize
- items = records[startIdx:endIdx]
- totalPages = math.ceil(totalItems / pageSize) if pageSize > 0 else 1
- else:
- items = records
- totalPages = 1
- page = 1
- pageSize = totalItems
-
- return PaginatedResult(
- items=items,
- totalItems=totalItems,
- totalPages=totalPages
- )
-
- def getDocumentsForPosition(self, positionId: str) -> List[TrusteePositionDocument]:
- """Get all documents linked to a position."""
- # Step 1: System RBAC filtering
- links = getRecordsetWithRBAC(
- connector=self.db,
- modelClass=TrusteePositionDocument,
- currentUser=self.currentUser,
- recordFilter={"positionId": positionId},
- orderBy="id",
- mandateId=self.mandateId,
- featureInstanceId=self.featureInstanceId,
- featureCode=self.FEATURE_CODE
- )
- return [TrusteePositionDocument(**{k: v for k, v in r.items() if not k.startswith("_")}) for r in links]
-
- def getPositionsForDocument(self, documentId: str) -> List[TrusteePositionDocument]:
- """Get all positions linked to a document."""
- # Step 1: System RBAC filtering
- links = getRecordsetWithRBAC(
- connector=self.db,
- modelClass=TrusteePositionDocument,
+ modelClass=TrusteePosition,
currentUser=self.currentUser,
recordFilter={"documentId": documentId},
- orderBy="id",
+ orderBy="valuta",
mandateId=self.mandateId,
featureInstanceId=self.featureInstanceId,
featureCode=self.FEATURE_CODE
)
- return [TrusteePositionDocument(**{k: v for k, v in r.items() if not k.startswith("_")}) for r in links]
-
- def deletePositionDocument(self, linkId: str) -> bool:
- """Delete a position-document link.
-
- Note: organisationId and contractId removed - feature instance IS the organisation.
- """
- # Get existing link to check creator
- existingRecords = self.db.getRecordset(TrusteePositionDocument, recordFilter={"id": linkId})
- existing = existingRecords[0] if existingRecords else None
-
- if not existing:
- logger.warning(f"Position-document link {linkId} not found")
- return False
-
- createdBy = existing.get("_createdBy")
-
- # Check system RBAC permission (userreport can only delete their own records)
- if not self.checkCombinedPermission(TrusteePositionDocument, "delete", recordCreatedBy=createdBy):
- logger.warning(f"User {self.userId} lacks permission to delete position-document link")
- return False
-
- return self.db.recordDelete(TrusteePositionDocument, linkId)
-
- def _deletePositionDocumentLinksForDocument(self, documentId: str) -> None:
- """Delete all position-document cross-table entries referencing this document."""
- links = self.db.getRecordset(TrusteePositionDocument, recordFilter={"documentId": documentId})
- for link in links:
- linkId = link.get("id")
- if linkId:
- self.db.recordDelete(TrusteePositionDocument, linkId)
-
- def _deletePositionDocumentLinksForPosition(self, positionId: str) -> None:
- """Delete all position-document cross-table entries referencing this position."""
- links = self.db.getRecordset(TrusteePositionDocument, recordFilter={"positionId": positionId})
- for link in links:
- linkId = link.get("id")
- if linkId:
- self.db.recordDelete(TrusteePositionDocument, linkId)
+ return [TrusteePosition(**{k: v for k, v in r.items() if not k.startswith("_")}) for r in records]
# ===== Trustee-specific Access Check =====
@@ -1583,7 +1430,7 @@ class TrusteeObjects:
# operate role: CRUD for contracts, documents, positions
if "operate" in roles:
- if modelClass in (TrusteeContract, TrusteeDocument, TrusteePosition, TrusteePositionDocument):
+ if modelClass in (TrusteeContract, TrusteeDocument, TrusteePosition):
return True
# operate can read organisations
if modelClass == TrusteeOrganisation and operation == "read":
@@ -1591,7 +1438,7 @@ class TrusteeObjects:
# userreport role: CRUD own records for documents/positions
if "userreport" in roles:
- if modelClass in (TrusteeDocument, TrusteePosition, TrusteePositionDocument):
+ if modelClass in (TrusteeDocument, TrusteePosition):
# For create, always allowed
if operation == "create":
return True
diff --git a/modules/features/trustee/mainTrustee.py b/modules/features/trustee/mainTrustee.py
index 8d7e1243..bfd443ef 100644
--- a/modules/features/trustee/mainTrustee.py
+++ b/modules/features/trustee/mainTrustee.py
@@ -33,11 +33,6 @@ UI_OBJECTS = [
"label": {"en": "Documents", "de": "Dokumente", "fr": "Documents"},
"meta": {"area": "documents"}
},
- {
- "objectKey": "ui.feature.trustee.position-documents",
- "label": {"en": "Position Documents", "de": "Positions-Dokumente", "fr": "Documents de position"},
- "meta": {"area": "position-documents"}
- },
{
"objectKey": "ui.feature.trustee.expense-import",
"label": {"en": "Expense Import", "de": "Spesen Import", "fr": "Import de dépenses"},
@@ -63,11 +58,6 @@ DATA_OBJECTS = [
"label": {"en": "Document", "de": "Dokument", "fr": "Document"},
"meta": {"table": "TrusteeDocument", "fields": ["id", "filename", "mimeType", "fileSize", "uploadDate"]}
},
- {
- "objectKey": "data.feature.trustee.TrusteePositionDocument",
- "label": {"en": "Position-Document Assignment", "de": "Position-Dokument Zuordnung", "fr": "Assignation Position-Document"},
- "meta": {"table": "TrusteePositionDocument", "fields": ["id", "positionId", "documentId"]}
- },
{
"objectKey": "data.feature.trustee.*",
"label": {"en": "All Trustee Data", "de": "Alle Treuhand-Daten", "fr": "Toutes les données fiduciaires"},
@@ -148,7 +138,6 @@ TEMPLATE_ROLES = [
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
{"context": "UI", "item": "ui.feature.trustee.documents", "view": True},
- {"context": "UI", "item": "ui.feature.trustee.position-documents", "view": True},
# Group-level DATA access
{"context": "DATA", "item": None, "view": True, "read": "g", "create": "g", "update": "g", "delete": "g"},
]
@@ -165,12 +154,10 @@ TEMPLATE_ROLES = [
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
{"context": "UI", "item": "ui.feature.trustee.documents", "view": True},
- {"context": "UI", "item": "ui.feature.trustee.position-documents", "view": True},
{"context": "UI", "item": "ui.feature.trustee.expense-import", "view": True},
- # Own records only (MY level) - explizite Regeln pro Tabelle
+ # Own records only (MY level)
{"context": "DATA", "item": "data.feature.trustee.TrusteePosition", "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
{"context": "DATA", "item": "data.feature.trustee.TrusteeDocument", "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
- {"context": "DATA", "item": "data.feature.trustee.TrusteePositionDocument", "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
]
},
]