teamsbot ux fixes
This commit is contained in:
parent
3718745931
commit
16ab816c65
5 changed files with 249 additions and 28 deletions
|
|
@ -3290,16 +3290,50 @@ class TeamsbotService:
|
||||||
self, lastToolLabel: Optional[str] = None
|
self, lastToolLabel: Optional[str] = None
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""Per-round progress notice for long agent runs (meeting voice /
|
"""Per-round progress notice for long agent runs (meeting voice /
|
||||||
chat, ephemeral). Phrasing is AI-localised once per session;
|
chat, ephemeral). Generates a single short phrase in the bot's
|
||||||
``{activity}`` placeholder is substituted with the tool's
|
configured language that describes the current activity. Unlike
|
||||||
``displayLabel`` from the ToolDefinition. Returns ``None`` if
|
the cached ephemeral phrases, this is a per-call AI generation
|
||||||
generation failed."""
|
to avoid mixing English displayLabels into non-English speech."""
|
||||||
activity = lastToolLabel or "processing your request"
|
targetLang = (self.config.language or "").strip() or "en-US"
|
||||||
return await self._pickEphemeralPhrase(
|
botName = (self.config.botName or "the assistant").strip()
|
||||||
"agentRound",
|
activityHint = lastToolLabel or "working on the task"
|
||||||
substitutions={"activity": activity},
|
|
||||||
|
prompt = (
|
||||||
|
f"You are a meeting assistant named '{botName}'.\n"
|
||||||
|
f"Target spoken language (BCP-47): {targetLang}\n\n"
|
||||||
|
f"The assistant is currently busy with: {activityHint}\n\n"
|
||||||
|
f"Generate ONE short sentence (max 12 words) in {targetLang} "
|
||||||
|
f"that tells the audience what the assistant is doing right now. "
|
||||||
|
f"Natural, spoken style. No step numbers. No quotes around the output.\n"
|
||||||
|
f"Output ONLY the sentence, nothing else."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
aiService = createAiService(
|
||||||
|
self.currentUser, self.mandateId, self.instanceId
|
||||||
|
)
|
||||||
|
await aiService.ensureAiObjectsInitialized()
|
||||||
|
request = AiCallRequest(
|
||||||
|
prompt=prompt,
|
||||||
|
context="",
|
||||||
|
options=AiCallOptions(
|
||||||
|
operationType=OperationTypeEnum.DATA_ANALYSE,
|
||||||
|
priority=PriorityEnum.SPEED,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
response = await aiService.callAi(request)
|
||||||
|
except Exception as aiErr:
|
||||||
|
logger.debug(f"Agent round phrase generation failed: {aiErr}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not response or response.errorCount != 0 or not response.content:
|
||||||
|
return None
|
||||||
|
|
||||||
|
result = response.content.strip().strip('"').strip("'")
|
||||||
|
if len(result) > 200:
|
||||||
|
result = result[:200]
|
||||||
|
return result
|
||||||
|
|
||||||
async def _notifyMeetingEphemeral(self, sessionId: str, text: str) -> None:
|
async def _notifyMeetingEphemeral(self, sessionId: str, text: str) -> None:
|
||||||
"""Deliver a short line to the meeting (TTS + chat per config) without
|
"""Deliver a short line to the meeting (TTS + chat per config) without
|
||||||
persisting botResponses/transcripts, so the main agent answer stays the
|
persisting botResponses/transcripts, so the main agent answer stays the
|
||||||
|
|
@ -3455,6 +3489,18 @@ class TeamsbotService:
|
||||||
"promptId": promptId,
|
"promptId": promptId,
|
||||||
"status": "toolCall",
|
"status": "toolCall",
|
||||||
"toolName": toolName,
|
"toolName": toolName,
|
||||||
|
"displayLabel": lastToolLabel,
|
||||||
|
})
|
||||||
|
elif event.type == AgentEventTypeEnum.TOOL_RESULT:
|
||||||
|
evtData = event.data or {}
|
||||||
|
resultSnippet = (evtData.get("data") or "")[:200]
|
||||||
|
await _emitSessionEvent(sessionId, "agentRun", {
|
||||||
|
"source": sourceLabel,
|
||||||
|
"promptId": promptId,
|
||||||
|
"status": "toolResult",
|
||||||
|
"toolName": evtData.get("toolName", ""),
|
||||||
|
"success": evtData.get("success", True),
|
||||||
|
"summary": resultSnippet,
|
||||||
})
|
})
|
||||||
elif event.type == AgentEventTypeEnum.FILE_CREATED:
|
elif event.type == AgentEventTypeEnum.FILE_CREATED:
|
||||||
await _emitSessionEvent(sessionId, "documentCreated", event.data or {})
|
await _emitSessionEvent(sessionId, "documentCreated", event.data or {})
|
||||||
|
|
|
||||||
|
|
@ -310,11 +310,15 @@ def _registerWorkspaceTools(registry: ToolRegistry, services):
|
||||||
return ToolResult(toolCallId="", toolName="writeFile", success=False, error="name is required for mode=create")
|
return ToolResult(toolCallId="", toolName="writeFile", success=False, error="name is required for mode=create")
|
||||||
fileItem, _ = dbMgmt.saveUploadedFile(content.encode("utf-8"), name)
|
fileItem, _ = dbMgmt.saveUploadedFile(content.encode("utf-8"), name)
|
||||||
fiId = context.get("featureInstanceId") or (services.featureInstanceId if services else "")
|
fiId = context.get("featureInstanceId") or (services.featureInstanceId if services else "")
|
||||||
|
updateFields: Dict[str, Any] = {}
|
||||||
if fiId:
|
if fiId:
|
||||||
dbMgmt.updateFile(fileItem.id, {"featureInstanceId": fiId})
|
updateFields["featureInstanceId"] = fiId
|
||||||
# File group tree removed — groupId arg and instance-group assignment no longer apply
|
if args.get("folderId"):
|
||||||
|
updateFields["folderId"] = args["folderId"]
|
||||||
if args.get("tags"):
|
if args.get("tags"):
|
||||||
dbMgmt.updateFile(fileItem.id, {"tags": args["tags"]})
|
updateFields["tags"] = args["tags"]
|
||||||
|
if updateFields:
|
||||||
|
dbMgmt.updateFile(fileItem.id, updateFields)
|
||||||
|
|
||||||
chatDocId = _attachFileAsChatDocument(
|
chatDocId = _attachFileAsChatDocument(
|
||||||
services, fileItem,
|
services, fileItem,
|
||||||
|
|
@ -429,7 +433,7 @@ def _registerWorkspaceTools(registry: ToolRegistry, services):
|
||||||
"writeFile", _writeFile,
|
"writeFile", _writeFile,
|
||||||
description=(
|
description=(
|
||||||
"Create, append, or overwrite a file. Modes:\n"
|
"Create, append, or overwrite a file. Modes:\n"
|
||||||
"- create (default): create a new file (name required).\n"
|
"- create (default): create a new file (name required). Use folderId to place it in a specific folder.\n"
|
||||||
"- append: append content to an existing file (fileId required). "
|
"- append: append content to an existing file (fileId required). "
|
||||||
"Use for large content that exceeds a single tool call (~8000 chars per call).\n"
|
"Use for large content that exceeds a single tool call (~8000 chars per call).\n"
|
||||||
"- overwrite: replace entire file content (fileId required).\n"
|
"- overwrite: replace entire file content (fileId required).\n"
|
||||||
|
|
@ -445,7 +449,7 @@ def _registerWorkspaceTools(registry: ToolRegistry, services):
|
||||||
"content": {"type": "string", "description": "Content to write/append"},
|
"content": {"type": "string", "description": "Content to write/append"},
|
||||||
"mode": {"type": "string", "enum": ["create", "append", "overwrite"], "description": "Write mode (default: create)"},
|
"mode": {"type": "string", "enum": ["create", "append", "overwrite"], "description": "Write mode (default: create)"},
|
||||||
"fileId": {"type": "string", "description": "File ID (required for mode=append/overwrite)"},
|
"fileId": {"type": "string", "description": "File ID (required for mode=append/overwrite)"},
|
||||||
"groupId": {"type": "string", "description": "Group ID to place the file in (mode=create only). Omit to use the instance default group."},
|
"folderId": {"type": "string", "description": "Folder ID to place the file in (mode=create only). Use listFolders to find IDs. Omit for root."},
|
||||||
"tags": {"type": "array", "items": {"type": "string"}, "description": "Tags (mode=create only)"},
|
"tags": {"type": "array", "items": {"type": "string"}, "description": "Tags (mode=create only)"},
|
||||||
},
|
},
|
||||||
"required": ["content"]
|
"required": ["content"]
|
||||||
|
|
@ -704,7 +708,147 @@ def _registerWorkspaceTools(registry: ToolRegistry, services):
|
||||||
readOnly=False
|
readOnly=False
|
||||||
)
|
)
|
||||||
|
|
||||||
# Group tree tools removed — file grouping now uses view-based display grouping (TableListView)
|
# ---- Folder management tools ----
|
||||||
|
|
||||||
|
async def _createFolder(args: Dict[str, Any], context: Dict[str, Any]):
|
||||||
|
name = args.get("name", "")
|
||||||
|
parentId = args.get("parentId") or None
|
||||||
|
if not name:
|
||||||
|
return ToolResult(toolCallId="", toolName="createFolder", success=False, error="name is required")
|
||||||
|
try:
|
||||||
|
chatService = services.chat
|
||||||
|
dbMgmt = chatService.interfaceDbComponent
|
||||||
|
folder = dbMgmt.createFolder(name, parentId=parentId)
|
||||||
|
folderId = folder.get("id") if isinstance(folder, dict) else getattr(folder, "id", None)
|
||||||
|
folderName = folder.get("name") if isinstance(folder, dict) else getattr(folder, "name", name)
|
||||||
|
return ToolResult(
|
||||||
|
toolCallId="", toolName="createFolder", success=True,
|
||||||
|
data=f"Folder '{folderName}' created (id: {folderId})" + (f" inside parent {parentId}" if parentId else ""),
|
||||||
|
sideEvents=[{"type": "folderCreated", "data": {"folderId": folderId, "folderName": folderName, "parentId": parentId}}],
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return ToolResult(toolCallId="", toolName="createFolder", success=False, error=str(e))
|
||||||
|
|
||||||
|
async def _listFolders(args: Dict[str, Any], context: Dict[str, Any]):
|
||||||
|
try:
|
||||||
|
chatService = services.chat
|
||||||
|
dbMgmt = chatService.interfaceDbComponent
|
||||||
|
folders = dbMgmt.getOwnFolderTree()
|
||||||
|
if not folders:
|
||||||
|
return ToolResult(toolCallId="", toolName="listFolders", success=True, data="No folders found.")
|
||||||
|
lines = []
|
||||||
|
folderMap: Dict[Optional[str], List] = {}
|
||||||
|
for f in folders:
|
||||||
|
pid = f.get("parentId") if isinstance(f, dict) else getattr(f, "parentId", None)
|
||||||
|
folderMap.setdefault(pid, []).append(f)
|
||||||
|
|
||||||
|
def _walk(parentId: Optional[str], indent: int):
|
||||||
|
for f in sorted(folderMap.get(parentId, []), key=lambda x: (x.get("name") if isinstance(x, dict) else getattr(x, "name", "")).lower()):
|
||||||
|
fId = f.get("id") if isinstance(f, dict) else getattr(f, "id", "")
|
||||||
|
fName = f.get("name") if isinstance(f, dict) else getattr(f, "name", "")
|
||||||
|
prefix = " " * indent
|
||||||
|
lines.append(f"{prefix}- {fName} (id: {fId})")
|
||||||
|
_walk(fId, indent + 1)
|
||||||
|
|
||||||
|
_walk(None, 0)
|
||||||
|
return ToolResult(toolCallId="", toolName="listFolders", success=True, data="\n".join(lines))
|
||||||
|
except Exception as e:
|
||||||
|
return ToolResult(toolCallId="", toolName="listFolders", success=False, error=str(e))
|
||||||
|
|
||||||
|
async def _moveFile(args: Dict[str, Any], context: Dict[str, Any]):
|
||||||
|
fileId = args.get("fileId", "")
|
||||||
|
folderId = args.get("folderId")
|
||||||
|
if not fileId:
|
||||||
|
return ToolResult(toolCallId="", toolName="moveFile", success=False, error="fileId is required")
|
||||||
|
try:
|
||||||
|
chatService = services.chat
|
||||||
|
dbMgmt = chatService.interfaceDbComponent
|
||||||
|
file = dbMgmt.getFile(fileId)
|
||||||
|
if not file:
|
||||||
|
return ToolResult(toolCallId="", toolName="moveFile", success=False, error=f"File {fileId} not found")
|
||||||
|
dbMgmt.updateFile(fileId, {"folderId": folderId or None})
|
||||||
|
targetLabel = f"folder {folderId}" if folderId else "root"
|
||||||
|
return ToolResult(
|
||||||
|
toolCallId="", toolName="moveFile", success=True,
|
||||||
|
data=f"File '{file.fileName}' (id: {fileId}) moved to {targetLabel}",
|
||||||
|
sideEvents=[{"type": "fileUpdated", "data": {"fileId": fileId, "fileName": file.fileName}}],
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return ToolResult(toolCallId="", toolName="moveFile", success=False, error=str(e))
|
||||||
|
|
||||||
|
registry.register(
|
||||||
|
"createFolder", _createFolder,
|
||||||
|
description=(
|
||||||
|
"Create a new folder in the workspace file tree. "
|
||||||
|
"Use parentId to create nested folders. Returns the new folder ID."
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string", "description": "Folder name"},
|
||||||
|
"parentId": {"type": "string", "description": "Parent folder ID for nesting. Omit to create at root level."},
|
||||||
|
},
|
||||||
|
"required": ["name"]
|
||||||
|
},
|
||||||
|
readOnly=False
|
||||||
|
)
|
||||||
|
|
||||||
|
registry.register(
|
||||||
|
"listFolders", _listFolders,
|
||||||
|
description=(
|
||||||
|
"List all folders in the workspace as an indented tree. "
|
||||||
|
"Use to find folder IDs for createFolder (parentId), writeFile (folderId), or moveFile."
|
||||||
|
),
|
||||||
|
parameters={"type": "object", "properties": {}},
|
||||||
|
readOnly=True
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _renameFolder(args: Dict[str, Any], context: Dict[str, Any]):
|
||||||
|
folderId = args.get("folderId", "")
|
||||||
|
newName = args.get("newName", "")
|
||||||
|
if not folderId or not newName:
|
||||||
|
return ToolResult(toolCallId="", toolName="renameFolder", success=False, error="folderId and newName are required")
|
||||||
|
try:
|
||||||
|
chatService = services.chat
|
||||||
|
dbMgmt = chatService.interfaceDbComponent
|
||||||
|
folder = dbMgmt.renameFolder(folderId, newName)
|
||||||
|
return ToolResult(
|
||||||
|
toolCallId="", toolName="renameFolder", success=True,
|
||||||
|
data=f"Folder {folderId} renamed to '{newName}'",
|
||||||
|
sideEvents=[{"type": "folderUpdated", "data": {"folderId": folderId, "folderName": newName}}],
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return ToolResult(toolCallId="", toolName="renameFolder", success=False, error=str(e))
|
||||||
|
|
||||||
|
registry.register(
|
||||||
|
"renameFolder", _renameFolder,
|
||||||
|
description="Rename an existing folder in the workspace file tree.",
|
||||||
|
parameters={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"folderId": {"type": "string", "description": "The folder ID to rename"},
|
||||||
|
"newName": {"type": "string", "description": "New folder name"},
|
||||||
|
},
|
||||||
|
"required": ["folderId", "newName"]
|
||||||
|
},
|
||||||
|
readOnly=False
|
||||||
|
)
|
||||||
|
|
||||||
|
registry.register(
|
||||||
|
"moveFile", _moveFile,
|
||||||
|
description=(
|
||||||
|
"Move a file into a specific folder. Set folderId to null or omit to move the file back to the root level."
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fileId": {"type": "string", "description": "The file ID to move"},
|
||||||
|
"folderId": {"type": "string", "description": "Target folder ID. Omit or null to move to root."},
|
||||||
|
},
|
||||||
|
"required": ["fileId"]
|
||||||
|
},
|
||||||
|
readOnly=False
|
||||||
|
)
|
||||||
|
|
||||||
registry.register(
|
registry.register(
|
||||||
"replaceInFile", _replaceInFile,
|
"replaceInFile", _replaceInFile,
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ class ContainerExtractor(Extractor):
|
||||||
"""Extract by recursively unpacking the container."""
|
"""Extract by recursively unpacking the container."""
|
||||||
fileName = context.get("fileName", "archive")
|
fileName = context.get("fileName", "archive")
|
||||||
mimeType = context.get("mimeType", "application/octet-stream")
|
mimeType = context.get("mimeType", "application/octet-stream")
|
||||||
|
cascadeDepth = context.get("_cascadeDepth", 0)
|
||||||
|
|
||||||
rootId = makeId()
|
rootId = makeId()
|
||||||
parts: List[ContentPart] = [
|
parts: List[ContentPart] = [
|
||||||
|
|
@ -97,7 +98,7 @@ class ContainerExtractor(Extractor):
|
||||||
parts.extend(lazy)
|
parts.extend(lazy)
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
state = {"totalSize": 0, "fileCount": 0}
|
state = {"totalSize": 0, "fileCount": 0, "cascadeDepth": cascadeDepth}
|
||||||
try:
|
try:
|
||||||
childParts = _resolveContainerRecursive(
|
childParts = _resolveContainerRecursive(
|
||||||
fileBytes, mimeType, fileName, rootId, "", 0, state
|
fileBytes, mimeType, fileName, rootId, "", 0, state
|
||||||
|
|
@ -209,7 +210,12 @@ def _addFilePart(
|
||||||
|
|
||||||
if extractor and not isinstance(extractor, ContainerExtractor):
|
if extractor and not isinstance(extractor, ContainerExtractor):
|
||||||
try:
|
try:
|
||||||
childParts = extractor.extract(data, {"fileName": fileName, "mimeType": detectedMime})
|
cascadeDepth = state.get("cascadeDepth", 0)
|
||||||
|
childParts = extractor.extract(data, {
|
||||||
|
"fileName": fileName,
|
||||||
|
"mimeType": detectedMime,
|
||||||
|
"_cascadeDepth": cascadeDepth + 1,
|
||||||
|
})
|
||||||
for part in childParts:
|
for part in childParts:
|
||||||
part.parentId = parentId
|
part.parentId = parentId
|
||||||
if not part.metadata:
|
if not part.metadata:
|
||||||
|
|
|
||||||
|
|
@ -53,12 +53,13 @@ class EmailExtractor(Extractor):
|
||||||
def extract(self, fileBytes: bytes, context: Dict[str, Any]) -> List[ContentPart]:
|
def extract(self, fileBytes: bytes, context: Dict[str, Any]) -> List[ContentPart]:
|
||||||
fileName = context.get("fileName", "email")
|
fileName = context.get("fileName", "email")
|
||||||
lower = (fileName or "").lower()
|
lower = (fileName or "").lower()
|
||||||
|
depth = context.get("_cascadeDepth", 0)
|
||||||
|
|
||||||
if lower.endswith(".msg"):
|
if lower.endswith(".msg"):
|
||||||
return self._extractMsg(fileBytes, fileName)
|
return self._extractMsg(fileBytes, fileName, depth)
|
||||||
return self._extractEml(fileBytes, fileName)
|
return self._extractEml(fileBytes, fileName, depth)
|
||||||
|
|
||||||
def _extractEml(self, fileBytes: bytes, fileName: str) -> List[ContentPart]:
|
def _extractEml(self, fileBytes: bytes, fileName: str, depth: int = 0) -> List[ContentPart]:
|
||||||
"""Parse standard EML (RFC 822) using stdlib email."""
|
"""Parse standard EML (RFC 822) using stdlib email."""
|
||||||
rootId = makeId()
|
rootId = makeId()
|
||||||
parts: List[ContentPart] = []
|
parts: List[ContentPart] = []
|
||||||
|
|
@ -91,7 +92,7 @@ class EmailExtractor(Extractor):
|
||||||
attachName = part.get_filename() or "attachment"
|
attachName = part.get_filename() or "attachment"
|
||||||
attachData = part.get_payload(decode=True)
|
attachData = part.get_payload(decode=True)
|
||||||
if attachData:
|
if attachData:
|
||||||
parts.extend(_delegateAttachment(attachData, attachName, rootId))
|
parts.extend(_delegateAttachment(attachData, attachName, rootId, depth))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if contentType == "text/plain":
|
if contentType == "text/plain":
|
||||||
|
|
@ -113,7 +114,7 @@ class EmailExtractor(Extractor):
|
||||||
|
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
def _extractMsg(self, fileBytes: bytes, fileName: str) -> List[ContentPart]:
|
def _extractMsg(self, fileBytes: bytes, fileName: str, depth: int = 0) -> List[ContentPart]:
|
||||||
"""Parse Outlook MSG files using extract-msg (optional)."""
|
"""Parse Outlook MSG files using extract-msg (optional)."""
|
||||||
rootId = makeId()
|
rootId = makeId()
|
||||||
parts: List[ContentPart] = []
|
parts: List[ContentPart] = []
|
||||||
|
|
@ -179,7 +180,7 @@ class EmailExtractor(Extractor):
|
||||||
attachName = getattr(attachment, "longFilename", None) or getattr(attachment, "shortFilename", None) or "attachment"
|
attachName = getattr(attachment, "longFilename", None) or getattr(attachment, "shortFilename", None) or "attachment"
|
||||||
attachData = getattr(attachment, "data", None)
|
attachData = getattr(attachment, "data", None)
|
||||||
if attachData:
|
if attachData:
|
||||||
parts.extend(_delegateAttachment(attachData, attachName, rootId))
|
parts.extend(_delegateAttachment(attachData, attachName, rootId, depth))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
msgFile.close()
|
msgFile.close()
|
||||||
|
|
@ -199,18 +200,39 @@ def _buildHeaderText(msg) -> str:
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def _delegateAttachment(attachData: bytes, attachName: str, parentId: str) -> List[ContentPart]:
|
_MAX_CASCADE_DEPTH = 10
|
||||||
"""Delegate an attachment to the appropriate type-specific extractor."""
|
|
||||||
|
def _delegateAttachment(attachData: bytes, attachName: str, parentId: str, depth: int = 0) -> List[ContentPart]:
|
||||||
|
"""Delegate an attachment to the appropriate type-specific extractor.
|
||||||
|
|
||||||
|
Passes ``_cascadeDepth`` through the context so nested Email→Container→Email
|
||||||
|
chains share a global depth counter and don't recurse infinitely.
|
||||||
|
"""
|
||||||
|
if depth >= _MAX_CASCADE_DEPTH:
|
||||||
|
logger.warning(f"Cascade depth {depth} reached for {attachName}, skipping extraction")
|
||||||
|
import base64
|
||||||
|
encodedData = base64.b64encode(attachData).decode("utf-8") if attachData else ""
|
||||||
|
return [ContentPart(
|
||||||
|
id=makeId(), parentId=parentId, label=attachName,
|
||||||
|
typeGroup="binary", mimeType="application/octet-stream",
|
||||||
|
data=encodedData,
|
||||||
|
metadata={"size": len(attachData), "emailAttachment": attachName, "cascadeDepthExceeded": True},
|
||||||
|
)]
|
||||||
|
|
||||||
guessedMime, _ = mimetypes.guess_type(attachName)
|
guessedMime, _ = mimetypes.guess_type(attachName)
|
||||||
detectedMime = guessedMime or "application/octet-stream"
|
detectedMime = guessedMime or "application/octet-stream"
|
||||||
|
|
||||||
from ..subRegistry import ExtractorRegistry
|
from ..subRegistry import getExtractorRegistry
|
||||||
registry = ExtractorRegistry()
|
registry = getExtractorRegistry()
|
||||||
extractor = registry.resolve(detectedMime, attachName)
|
extractor = registry.resolve(detectedMime, attachName)
|
||||||
|
|
||||||
if extractor and not isinstance(extractor, EmailExtractor):
|
if extractor:
|
||||||
try:
|
try:
|
||||||
childParts = extractor.extract(attachData, {"fileName": attachName, "mimeType": detectedMime})
|
childParts = extractor.extract(attachData, {
|
||||||
|
"fileName": attachName,
|
||||||
|
"mimeType": detectedMime,
|
||||||
|
"_cascadeDepth": depth + 1,
|
||||||
|
})
|
||||||
for part in childParts:
|
for part in childParts:
|
||||||
part.parentId = parentId
|
part.parentId = parentId
|
||||||
if not part.metadata:
|
if not part.metadata:
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,9 @@ asyncpg==0.30.0
|
||||||
## Stripe payments
|
## Stripe payments
|
||||||
stripe>=11.0.0
|
stripe>=11.0.0
|
||||||
|
|
||||||
|
## Outlook MSG file extraction
|
||||||
|
extract-msg>=0.55.0
|
||||||
|
|
||||||
## Geospatial libraries for STAC connector
|
## Geospatial libraries for STAC connector
|
||||||
pyproj>=3.6.0 # For coordinate transformations (EPSG:2056 <-> EPSG:4326)
|
pyproj>=3.6.0 # For coordinate transformations (EPSG:2056 <-> EPSG:4326)
|
||||||
shapely>=2.0.0 # For geometric operations (intersections, area calculations)
|
shapely>=2.0.0 # For geometric operations (intersections, area calculations)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue