720 lines
27 KiB
Python
720 lines
27 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""Unified AI Workspace routes.
|
|
|
|
SSE-based endpoints that combine the capabilities of Codeeditor, Chatbot,
|
|
and Playground into a single agent-driven workspace.
|
|
"""
|
|
|
|
import logging
|
|
import json
|
|
import asyncio
|
|
from typing import Optional, List
|
|
|
|
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Request, UploadFile, File
|
|
from fastapi.responses import StreamingResponse, JSONResponse
|
|
from pydantic import BaseModel, Field
|
|
|
|
from modules.auth import limiter, getRequestContext, RequestContext
|
|
from modules.interfaces import interfaceDbChat, interfaceDbManagement
|
|
from modules.interfaces.interfaceAiObjects import AiObjects
|
|
from modules.serviceCenter.core.serviceStreaming import get_event_manager
|
|
from modules.serviceCenter.services.serviceAgent.datamodelAgent import AgentEventTypeEnum
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(
|
|
prefix="/api/workspace",
|
|
tags=["Unified Workspace"],
|
|
responses={404: {"description": "Not found"}},
|
|
)
|
|
|
|
_aiObjects: Optional[AiObjects] = None
|
|
|
|
|
|
class WorkspaceInputRequest(BaseModel):
|
|
"""Prompt input for the unified workspace."""
|
|
prompt: str = Field(description="User prompt text")
|
|
fileIds: List[str] = Field(default_factory=list, description="Referenced file IDs")
|
|
uploadedFiles: List[str] = Field(default_factory=list, description="Newly uploaded file IDs")
|
|
dataSourceIds: List[str] = Field(default_factory=list, description="Active DataSource IDs")
|
|
voiceMode: bool = Field(default=False, description="Enable voice response")
|
|
workflowId: Optional[str] = Field(default=None, description="Continue existing workflow")
|
|
userLanguage: str = Field(default="en", description="User language code")
|
|
|
|
|
|
async def _getAiObjects() -> AiObjects:
|
|
global _aiObjects
|
|
if _aiObjects is None:
|
|
_aiObjects = await AiObjects.create()
|
|
return _aiObjects
|
|
|
|
|
|
def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
|
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
|
rootInterface = getRootInterface()
|
|
instance = rootInterface.getFeatureInstance(instanceId)
|
|
if not instance:
|
|
raise HTTPException(status_code=404, detail=f"Feature instance {instanceId} not found")
|
|
featureAccess = rootInterface.getFeatureAccess(str(context.user.id), instanceId)
|
|
if not featureAccess or not featureAccess.enabled:
|
|
raise HTTPException(status_code=403, detail="Access denied to this feature instance")
|
|
return str(instance.mandateId) if instance.mandateId else None
|
|
|
|
|
|
def _getChatInterface(context: RequestContext, featureInstanceId: str = None):
|
|
return interfaceDbChat.getInterface(
|
|
context.user,
|
|
mandateId=str(context.mandateId) if context.mandateId else None,
|
|
featureInstanceId=featureInstanceId,
|
|
)
|
|
|
|
|
|
def _buildResolverDbInterface(chatService):
|
|
"""Build a DB adapter that ConnectorResolver can use to load UserConnections.
|
|
|
|
ConnectorResolver calls db.getUserConnection(connectionId).
|
|
interfaceDbApp provides getUserConnectionById(connectionId).
|
|
This adapter bridges the method name difference.
|
|
"""
|
|
class _ResolverDbAdapter:
|
|
def __init__(self, appInterface):
|
|
self._app = appInterface
|
|
def getUserConnection(self, connectionId: str):
|
|
if hasattr(self._app, "getUserConnectionById"):
|
|
return self._app.getUserConnectionById(connectionId)
|
|
return None
|
|
appIf = getattr(chatService, "interfaceDbApp", None)
|
|
if appIf:
|
|
return _ResolverDbAdapter(appIf)
|
|
return getattr(chatService, "interfaceDbComponent", None)
|
|
|
|
|
|
def _getDbManagement(context: RequestContext, featureInstanceId: str = None):
|
|
return interfaceDbManagement.getInterface(
|
|
context.user,
|
|
mandateId=str(context.mandateId) if context.mandateId else None,
|
|
featureInstanceId=featureInstanceId,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SSE Stream endpoint
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.post("/{instanceId}/start/stream")
|
|
@limiter.limit("60/minute")
|
|
async def streamWorkspaceStart(
|
|
request: Request,
|
|
instanceId: str = Path(..., description="Feature instance ID"),
|
|
userInput: WorkspaceInputRequest = Body(...),
|
|
context: RequestContext = Depends(getRequestContext),
|
|
):
|
|
"""Start or continue a Workspace session with SSE streaming via serviceAgent."""
|
|
mandateId = _validateInstanceAccess(instanceId, context)
|
|
chatInterface = _getChatInterface(context, featureInstanceId=instanceId)
|
|
aiObjects = await _getAiObjects()
|
|
eventManager = get_event_manager()
|
|
|
|
if userInput.workflowId:
|
|
workflow = chatInterface.getWorkflow(userInput.workflowId)
|
|
if not workflow:
|
|
raise HTTPException(status_code=404, detail=f"Workflow {userInput.workflowId} not found")
|
|
else:
|
|
existingWorkflows = chatInterface.getWorkflows() or []
|
|
nextNum = len(existingWorkflows) + 1
|
|
workflow = chatInterface.createWorkflow({
|
|
"featureInstanceId": instanceId,
|
|
"status": "active",
|
|
"name": f"Chat {nextNum}",
|
|
"workflowMode": "Dynamic",
|
|
})
|
|
|
|
workflowId = workflow.get("id") if isinstance(workflow, dict) else getattr(workflow, "id", str(workflow))
|
|
queueId = f"workspace-{workflowId}"
|
|
eventManager.create_queue(queueId)
|
|
|
|
chatInterface.createMessage({
|
|
"workflowId": workflowId,
|
|
"role": "user",
|
|
"message": userInput.prompt,
|
|
})
|
|
|
|
asyncio.ensure_future(
|
|
_runWorkspaceAgent(
|
|
workflowId=workflowId,
|
|
queueId=queueId,
|
|
prompt=userInput.prompt,
|
|
fileIds=userInput.fileIds,
|
|
dataSourceIds=userInput.dataSourceIds,
|
|
voiceMode=userInput.voiceMode,
|
|
instanceId=instanceId,
|
|
user=context.user,
|
|
mandateId=mandateId or "",
|
|
aiObjects=aiObjects,
|
|
chatInterface=chatInterface,
|
|
eventManager=eventManager,
|
|
userLanguage=userInput.userLanguage,
|
|
)
|
|
)
|
|
|
|
async def _sseGenerator():
|
|
queue = eventManager.get_queue(queueId)
|
|
if not queue:
|
|
return
|
|
while True:
|
|
try:
|
|
event = await asyncio.wait_for(queue.get(), timeout=120)
|
|
except asyncio.TimeoutError:
|
|
yield "data: {\"type\": \"keepalive\"}\n\n"
|
|
continue
|
|
|
|
if event is None:
|
|
break
|
|
|
|
ssePayload = event.get("data", event) if isinstance(event, dict) else event
|
|
yield f"data: {json.dumps(ssePayload, default=str)}\n\n"
|
|
|
|
eventType = ssePayload.get("type", "") if isinstance(ssePayload, dict) else ""
|
|
if eventType in ("complete", "error", "stopped"):
|
|
break
|
|
|
|
await eventManager.cleanup(queueId, delay=30)
|
|
|
|
return StreamingResponse(
|
|
_sseGenerator(),
|
|
media_type="text/event-stream",
|
|
headers={
|
|
"Cache-Control": "no-cache",
|
|
"Connection": "keep-alive",
|
|
"X-Accel-Buffering": "no",
|
|
},
|
|
)
|
|
|
|
|
|
async def _runWorkspaceAgent(
|
|
workflowId: str,
|
|
queueId: str,
|
|
prompt: str,
|
|
fileIds: List[str],
|
|
dataSourceIds: List[str],
|
|
voiceMode: bool,
|
|
instanceId: str,
|
|
user,
|
|
mandateId: str,
|
|
aiObjects,
|
|
chatInterface,
|
|
eventManager,
|
|
userLanguage: str = "en",
|
|
):
|
|
"""Run the serviceAgent loop and forward events to the SSE queue."""
|
|
try:
|
|
from modules.serviceCenter import getService
|
|
from modules.serviceCenter.context import ServiceCenterContext
|
|
ctx = ServiceCenterContext(
|
|
user=user,
|
|
mandate_id=mandateId,
|
|
feature_instance_id=instanceId,
|
|
workflow_id=workflowId,
|
|
)
|
|
agentService = getService("agent", ctx)
|
|
|
|
async for event in agentService.runAgent(
|
|
prompt=prompt,
|
|
fileIds=fileIds,
|
|
workflowId=workflowId,
|
|
userLanguage=userLanguage,
|
|
):
|
|
sseEvent = {
|
|
"type": event.type.value if hasattr(event.type, "value") else event.type,
|
|
"workflowId": workflowId,
|
|
}
|
|
if event.content:
|
|
sseEvent["content"] = event.content
|
|
if event.type == AgentEventTypeEnum.MESSAGE:
|
|
sseEvent["item"] = {
|
|
"id": f"msg-{workflowId}-{id(event)}",
|
|
"role": "assistant",
|
|
"content": event.content,
|
|
"workflowId": workflowId,
|
|
}
|
|
if event.data:
|
|
sseEvent["item"] = event.data
|
|
|
|
await eventManager.emit_event(queueId, sseEvent["type"], sseEvent)
|
|
|
|
if event.type in (AgentEventTypeEnum.FINAL, AgentEventTypeEnum.ERROR):
|
|
if event.content:
|
|
chatInterface.createMessage({
|
|
"workflowId": workflowId,
|
|
"role": "assistant",
|
|
"message": event.content,
|
|
})
|
|
|
|
await eventManager.emit_event(queueId, "complete", {
|
|
"type": "complete",
|
|
"workflowId": workflowId,
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Workspace agent error: {e}", exc_info=True)
|
|
await eventManager.emit_event(queueId, "error", {
|
|
"type": "error",
|
|
"content": str(e),
|
|
"workflowId": workflowId,
|
|
})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stop endpoint
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.post("/{instanceId}/{workflowId}/stop")
|
|
@limiter.limit("30/minute")
|
|
async def stopWorkspace(
|
|
request: Request,
|
|
instanceId: str = Path(...),
|
|
workflowId: str = Path(...),
|
|
context: RequestContext = Depends(getRequestContext),
|
|
):
|
|
_validateInstanceAccess(instanceId, context)
|
|
queueId = f"workspace-{workflowId}"
|
|
eventManager = get_event_manager()
|
|
await eventManager.emit_event(queueId, "stopped", {
|
|
"type": "stopped",
|
|
"workflowId": workflowId,
|
|
})
|
|
return JSONResponse({"status": "stopped", "workflowId": workflowId})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Workflow / Conversation endpoints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/{instanceId}/workflows")
|
|
@limiter.limit("60/minute")
|
|
async def listWorkspaceWorkflows(
|
|
request: Request,
|
|
instanceId: str = Path(...),
|
|
context: RequestContext = Depends(getRequestContext),
|
|
):
|
|
"""List all workspace workflows/conversations for this instance."""
|
|
_validateInstanceAccess(instanceId, context)
|
|
chatInterface = _getChatInterface(context, featureInstanceId=instanceId)
|
|
workflows = chatInterface.getWorkflows() or []
|
|
items = []
|
|
for wf in workflows:
|
|
if isinstance(wf, dict):
|
|
items.append(wf)
|
|
else:
|
|
items.append({
|
|
"id": getattr(wf, "id", None),
|
|
"name": getattr(wf, "name", ""),
|
|
"status": getattr(wf, "status", ""),
|
|
"startedAt": getattr(wf, "startedAt", None),
|
|
"lastActivity": getattr(wf, "lastActivity", None),
|
|
})
|
|
return JSONResponse({"workflows": items})
|
|
|
|
|
|
class UpdateWorkflowRequest(BaseModel):
|
|
"""Request body for updating a workflow (PATCH)."""
|
|
name: Optional[str] = Field(default=None, description="New workflow name")
|
|
|
|
|
|
@router.patch("/{instanceId}/workflows/{workflowId}")
|
|
@limiter.limit("60/minute")
|
|
async def patchWorkspaceWorkflow(
|
|
request: Request,
|
|
instanceId: str = Path(..., description="Feature instance ID"),
|
|
workflowId: str = Path(..., description="Workflow ID to update"),
|
|
body: UpdateWorkflowRequest = Body(...),
|
|
context: RequestContext = Depends(getRequestContext),
|
|
):
|
|
"""Update a workspace workflow (e.g. rename)."""
|
|
_validateInstanceAccess(instanceId, context)
|
|
chatInterface = _getChatInterface(context, featureInstanceId=instanceId)
|
|
workflow = chatInterface.getWorkflow(workflowId)
|
|
if not workflow:
|
|
raise HTTPException(status_code=404, detail=f"Workflow {workflowId} not found")
|
|
updateData = {}
|
|
if body.name is not None:
|
|
updateData["name"] = body.name
|
|
if not updateData:
|
|
updated = workflow
|
|
else:
|
|
updated = chatInterface.updateWorkflow(workflowId, updateData)
|
|
if isinstance(updated, dict):
|
|
return JSONResponse(updated)
|
|
return JSONResponse({
|
|
"id": getattr(updated, "id", None),
|
|
"name": getattr(updated, "name", ""),
|
|
"status": getattr(updated, "status", ""),
|
|
"startedAt": getattr(updated, "startedAt", None),
|
|
"lastActivity": getattr(updated, "lastActivity", None),
|
|
})
|
|
|
|
|
|
@router.get("/{instanceId}/workflows/{workflowId}/messages")
|
|
@limiter.limit("60/minute")
|
|
async def getWorkspaceMessages(
|
|
request: Request,
|
|
instanceId: str = Path(...),
|
|
workflowId: str = Path(...),
|
|
context: RequestContext = Depends(getRequestContext),
|
|
):
|
|
"""Get all messages for a workspace workflow/conversation."""
|
|
_validateInstanceAccess(instanceId, context)
|
|
chatInterface = _getChatInterface(context, featureInstanceId=instanceId)
|
|
messages = chatInterface.getMessages(workflowId) or []
|
|
items = []
|
|
for msg in messages:
|
|
if isinstance(msg, dict):
|
|
items.append(msg)
|
|
else:
|
|
items.append({
|
|
"id": getattr(msg, "id", None),
|
|
"role": getattr(msg, "role", ""),
|
|
"content": getattr(msg, "message", "") or getattr(msg, "content", ""),
|
|
"createdAt": getattr(msg, "publishedAt", None) or getattr(msg, "createdAt", None),
|
|
})
|
|
return JSONResponse({"messages": items})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# File and folder list endpoints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/{instanceId}/files")
|
|
@limiter.limit("60/minute")
|
|
async def listWorkspaceFiles(
|
|
request: Request,
|
|
instanceId: str = Path(...),
|
|
folderId: Optional[str] = Query(None),
|
|
tags: Optional[str] = Query(None),
|
|
search: Optional[str] = Query(None),
|
|
context: RequestContext = Depends(getRequestContext),
|
|
):
|
|
_validateInstanceAccess(instanceId, context)
|
|
dbMgmt = _getDbManagement(context, featureInstanceId=instanceId)
|
|
files = dbMgmt.getAllFiles()
|
|
return JSONResponse({"files": [f if isinstance(f, dict) else f.model_dump() for f in (files or [])]})
|
|
|
|
|
|
@router.get("/{instanceId}/files/{fileId}/content")
|
|
@limiter.limit("60/minute")
|
|
async def getFileContent(
|
|
request: Request,
|
|
instanceId: str = Path(...),
|
|
fileId: str = Path(...),
|
|
context: RequestContext = Depends(getRequestContext),
|
|
):
|
|
"""Return the raw content of a file for preview."""
|
|
from fastapi.responses import Response
|
|
_validateInstanceAccess(instanceId, context)
|
|
dbMgmt = _getDbManagement(context, featureInstanceId=instanceId)
|
|
fileRecord = dbMgmt.getFile(fileId)
|
|
if not fileRecord:
|
|
raise HTTPException(status_code=404, detail=f"File {fileId} not found")
|
|
fileData = fileRecord if isinstance(fileRecord, dict) else fileRecord.model_dump()
|
|
filePath = fileData.get("filePath")
|
|
if not filePath:
|
|
raise HTTPException(status_code=404, detail="File has no stored path")
|
|
import os
|
|
if not os.path.isfile(filePath):
|
|
raise HTTPException(status_code=404, detail="File not found on disk")
|
|
mimeType = fileData.get("mimeType", "application/octet-stream")
|
|
with open(filePath, "rb") as fh:
|
|
content = fh.read()
|
|
return Response(content=content, media_type=mimeType)
|
|
|
|
|
|
@router.get("/{instanceId}/folders")
|
|
@limiter.limit("60/minute")
|
|
async def listWorkspaceFolders(
|
|
request: Request,
|
|
instanceId: str = Path(...),
|
|
parentId: Optional[str] = Query(None),
|
|
context: RequestContext = Depends(getRequestContext),
|
|
):
|
|
_validateInstanceAccess(instanceId, context)
|
|
try:
|
|
from modules.serviceCenter import getService
|
|
from modules.serviceCenter.context import ServiceCenterContext
|
|
ctx = ServiceCenterContext(
|
|
user=context.user,
|
|
mandate_id=str(context.mandateId) if context.mandateId else None,
|
|
feature_instance_id=instanceId,
|
|
)
|
|
chatService = getService("chat", ctx)
|
|
folders = chatService.listFolders(parentId=parentId)
|
|
return JSONResponse({"folders": folders or []})
|
|
except Exception:
|
|
return JSONResponse({"folders": []})
|
|
|
|
|
|
@router.get("/{instanceId}/datasources")
|
|
@limiter.limit("60/minute")
|
|
async def listWorkspaceDataSources(
|
|
request: Request,
|
|
instanceId: str = Path(...),
|
|
context: RequestContext = Depends(getRequestContext),
|
|
):
|
|
_validateInstanceAccess(instanceId, context)
|
|
try:
|
|
from modules.serviceCenter import getService
|
|
from modules.serviceCenter.context import ServiceCenterContext
|
|
ctx = ServiceCenterContext(
|
|
user=context.user,
|
|
mandate_id=str(context.mandateId) if context.mandateId else None,
|
|
feature_instance_id=instanceId,
|
|
)
|
|
chatService = getService("chat", ctx)
|
|
dataSources = chatService.listDataSources(featureInstanceId=instanceId)
|
|
return JSONResponse({"dataSources": dataSources or []})
|
|
except Exception:
|
|
return JSONResponse({"dataSources": []})
|
|
|
|
|
|
@router.get("/{instanceId}/connections")
|
|
@limiter.limit("60/minute")
|
|
async def listWorkspaceConnections(
|
|
request: Request,
|
|
instanceId: str = Path(...),
|
|
context: RequestContext = Depends(getRequestContext),
|
|
):
|
|
"""Return the user's active connections (UserConnections)."""
|
|
_validateInstanceAccess(instanceId, context)
|
|
from modules.serviceCenter import getService
|
|
from modules.serviceCenter.context import ServiceCenterContext
|
|
ctx = ServiceCenterContext(
|
|
user=context.user,
|
|
mandate_id=str(context.mandateId) if context.mandateId else None,
|
|
feature_instance_id=instanceId,
|
|
)
|
|
chatService = getService("chat", ctx)
|
|
connections = chatService.getUserConnections()
|
|
items = []
|
|
for c in connections or []:
|
|
conn = c if isinstance(c, dict) else (c.model_dump() if hasattr(c, "model_dump") else {})
|
|
authority = conn.get("authority")
|
|
if hasattr(authority, "value"):
|
|
authority = authority.value
|
|
status = conn.get("status")
|
|
if hasattr(status, "value"):
|
|
status = status.value
|
|
items.append({
|
|
"id": conn.get("id"),
|
|
"authority": authority,
|
|
"externalUsername": conn.get("externalUsername"),
|
|
"externalEmail": conn.get("externalEmail"),
|
|
"status": status,
|
|
})
|
|
return JSONResponse({"connections": items})
|
|
|
|
|
|
class CreateDataSourceRequest(BaseModel):
|
|
"""Request body for creating a DataSource."""
|
|
connectionId: str = Field(description="Connection ID")
|
|
sourceType: str = Field(description="Source type")
|
|
path: str = Field(description="Path")
|
|
label: str = Field(description="Label")
|
|
|
|
|
|
@router.post("/{instanceId}/datasources")
|
|
@limiter.limit("60/minute")
|
|
async def createWorkspaceDataSource(
|
|
request: Request,
|
|
instanceId: str = Path(...),
|
|
body: CreateDataSourceRequest = Body(...),
|
|
context: RequestContext = Depends(getRequestContext),
|
|
):
|
|
"""Create a new DataSource for this workspace instance."""
|
|
_validateInstanceAccess(instanceId, context)
|
|
from modules.serviceCenter import getService
|
|
from modules.serviceCenter.context import ServiceCenterContext
|
|
ctx = ServiceCenterContext(
|
|
user=context.user,
|
|
mandate_id=str(context.mandateId) if context.mandateId else None,
|
|
feature_instance_id=instanceId,
|
|
)
|
|
chatService = getService("chat", ctx)
|
|
dataSource = chatService.createDataSource(
|
|
connectionId=body.connectionId,
|
|
sourceType=body.sourceType,
|
|
path=body.path,
|
|
label=body.label,
|
|
featureInstanceId=instanceId,
|
|
)
|
|
return JSONResponse(dataSource if isinstance(dataSource, dict) else dataSource.model_dump())
|
|
|
|
|
|
@router.delete("/{instanceId}/datasources/{dataSourceId}")
|
|
@limiter.limit("60/minute")
|
|
async def deleteWorkspaceDataSource(
|
|
request: Request,
|
|
instanceId: str = Path(...),
|
|
dataSourceId: str = Path(...),
|
|
context: RequestContext = Depends(getRequestContext),
|
|
):
|
|
"""Delete a DataSource."""
|
|
_validateInstanceAccess(instanceId, context)
|
|
from modules.serviceCenter import getService
|
|
from modules.serviceCenter.context import ServiceCenterContext
|
|
ctx = ServiceCenterContext(
|
|
user=context.user,
|
|
mandate_id=str(context.mandateId) if context.mandateId else None,
|
|
feature_instance_id=instanceId,
|
|
)
|
|
chatService = getService("chat", ctx)
|
|
chatService.deleteDataSource(dataSourceId)
|
|
return JSONResponse({"success": True})
|
|
|
|
|
|
@router.get("/{instanceId}/connections/{connectionId}/services")
|
|
@limiter.limit("30/minute")
|
|
async def listConnectionServices(
|
|
request: Request,
|
|
instanceId: str = Path(...),
|
|
connectionId: str = Path(...),
|
|
context: RequestContext = Depends(getRequestContext),
|
|
):
|
|
"""Return the available services for a specific UserConnection."""
|
|
_validateInstanceAccess(instanceId, context)
|
|
try:
|
|
from modules.connectors.connectorResolver import ConnectorResolver
|
|
from modules.serviceCenter import getService as getSvc
|
|
from modules.serviceCenter.context import ServiceCenterContext
|
|
ctx = ServiceCenterContext(
|
|
user=context.user,
|
|
mandate_id=str(context.mandateId) if context.mandateId else None,
|
|
feature_instance_id=instanceId,
|
|
)
|
|
chatService = getSvc("chat", ctx)
|
|
securityService = getSvc("security", ctx)
|
|
dbInterface = _buildResolverDbInterface(chatService)
|
|
resolver = ConnectorResolver(securityService, dbInterface)
|
|
provider = await resolver.resolve(connectionId)
|
|
services = provider.getAvailableServices()
|
|
_serviceLabels = {
|
|
"sharepoint": "SharePoint",
|
|
"outlook": "Outlook",
|
|
"teams": "Teams",
|
|
"onedrive": "OneDrive",
|
|
"drive": "Google Drive",
|
|
"gmail": "Gmail",
|
|
"files": "Files (FTP)",
|
|
}
|
|
_serviceIcons = {
|
|
"sharepoint": "sharepoint",
|
|
"outlook": "mail",
|
|
"teams": "chat",
|
|
"onedrive": "cloud",
|
|
"drive": "cloud",
|
|
"gmail": "mail",
|
|
"files": "folder",
|
|
}
|
|
items = [
|
|
{
|
|
"service": s,
|
|
"label": _serviceLabels.get(s, s),
|
|
"icon": _serviceIcons.get(s, "folder"),
|
|
}
|
|
for s in services
|
|
]
|
|
return JSONResponse({"services": items})
|
|
except Exception as e:
|
|
logger.error(f"Error listing services for connection {connectionId}: {e}")
|
|
return JSONResponse({"services": [], "error": str(e)}, status_code=400)
|
|
|
|
|
|
@router.get("/{instanceId}/connections/{connectionId}/browse")
|
|
@limiter.limit("60/minute")
|
|
async def browseConnectionService(
|
|
request: Request,
|
|
instanceId: str = Path(...),
|
|
connectionId: str = Path(...),
|
|
service: str = Query(..., description="Service name (e.g. sharepoint, onedrive, outlook)"),
|
|
path: str = Query("/", description="Path within the service to browse"),
|
|
context: RequestContext = Depends(getRequestContext),
|
|
):
|
|
"""Browse folders/items within a connection's service at a given path."""
|
|
_validateInstanceAccess(instanceId, context)
|
|
try:
|
|
from modules.connectors.connectorResolver import ConnectorResolver
|
|
from modules.serviceCenter import getService as getSvc
|
|
from modules.serviceCenter.context import ServiceCenterContext
|
|
ctx = ServiceCenterContext(
|
|
user=context.user,
|
|
mandate_id=str(context.mandateId) if context.mandateId else None,
|
|
feature_instance_id=instanceId,
|
|
)
|
|
chatService = getSvc("chat", ctx)
|
|
securityService = getSvc("security", ctx)
|
|
dbInterface = _buildResolverDbInterface(chatService)
|
|
resolver = ConnectorResolver(securityService, dbInterface)
|
|
adapter = await resolver.resolveService(connectionId, service)
|
|
entries = await adapter.browse(path, filter=None)
|
|
items = []
|
|
for entry in (entries or []):
|
|
items.append({
|
|
"name": entry.name,
|
|
"path": entry.path,
|
|
"isFolder": entry.isFolder,
|
|
"size": entry.size,
|
|
"mimeType": entry.mimeType,
|
|
"metadata": entry.metadata if hasattr(entry, "metadata") else {},
|
|
})
|
|
return JSONResponse({"items": items, "path": path, "service": service})
|
|
except Exception as e:
|
|
logger.error(f"Error browsing {service} for connection {connectionId} at '{path}': {e}")
|
|
return JSONResponse({"items": [], "error": str(e)}, status_code=400)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Voice endpoints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.post("/{instanceId}/voice/transcribe")
|
|
@limiter.limit("30/minute")
|
|
async def transcribeVoice(
|
|
request: Request,
|
|
instanceId: str = Path(...),
|
|
audio: UploadFile = File(...),
|
|
context: RequestContext = Depends(getRequestContext),
|
|
):
|
|
"""Transcribe audio to text using speech-to-text."""
|
|
_validateInstanceAccess(instanceId, context)
|
|
audioBytes = await audio.read()
|
|
try:
|
|
import aiohttp
|
|
formData = aiohttp.FormData()
|
|
formData.add_field("audio", audioBytes, filename=audio.filename or "audio.webm")
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.post(
|
|
f"{request.base_url}api/voice-google/speech-to-text",
|
|
data=formData,
|
|
) as resp:
|
|
if resp.status == 200:
|
|
result = await resp.json()
|
|
return JSONResponse({"text": result.get("text", "")})
|
|
return JSONResponse({"text": "", "error": f"STT failed: {resp.status}"})
|
|
except Exception as e:
|
|
logger.error(f"Voice transcription error: {e}")
|
|
return JSONResponse({"text": "", "error": str(e)})
|
|
|
|
|
|
@router.post("/{instanceId}/voice/synthesize")
|
|
@limiter.limit("30/minute")
|
|
async def synthesizeVoice(
|
|
request: Request,
|
|
instanceId: str = Path(...),
|
|
body: dict = Body(...),
|
|
context: RequestContext = Depends(getRequestContext),
|
|
):
|
|
"""Synthesize text to speech audio."""
|
|
_validateInstanceAccess(instanceId, context)
|
|
text = body.get("text", "")
|
|
if not text:
|
|
raise HTTPException(status_code=400, detail="text is required")
|
|
return JSONResponse({"audio": None, "note": "TTS via browser Speech Synthesis API recommended"})
|