ai workspace ui fixes
This commit is contained in:
parent
f51c313c2c
commit
b5dc643dac
5 changed files with 176 additions and 32 deletions
|
|
@ -1030,7 +1030,10 @@ class DatabaseConnector:
|
|||
existingRecord.update(record)
|
||||
|
||||
# Save updated record
|
||||
self._saveRecord(model_class, recordId, existingRecord)
|
||||
saved = self._saveRecord(model_class, recordId, existingRecord)
|
||||
if not saved:
|
||||
table = model_class.__name__
|
||||
raise ValueError(f"Failed to save record {recordId} to table {table}")
|
||||
return existingRecord
|
||||
|
||||
def recordDelete(self, model_class: type, recordId: str) -> bool:
|
||||
|
|
|
|||
|
|
@ -98,6 +98,30 @@ def _getDbManagement(context: RequestContext, featureInstanceId: str = None):
|
|||
)
|
||||
|
||||
|
||||
def _buildDataSourceContext(chatInterface, dataSourceIds: List[str]) -> str:
|
||||
"""Build a description of active data sources for the agent prompt."""
|
||||
parts = []
|
||||
for dsId in dataSourceIds:
|
||||
try:
|
||||
ds = chatInterface.db.recordGet("DataSource", dsId)
|
||||
if ds:
|
||||
label = ds.get("label", "")
|
||||
sourceType = ds.get("sourceType", "")
|
||||
path = ds.get("path", "/")
|
||||
parts.append(f"- {label} ({sourceType}, path: {path})")
|
||||
except Exception:
|
||||
pass
|
||||
return "\n".join(parts) if parts else ""
|
||||
|
||||
|
||||
def _deriveWorkflowName(prompt: str, maxLen: int = 40) -> str:
|
||||
"""Derive a short workflow name from the user's first prompt."""
|
||||
clean = " ".join(prompt.split())
|
||||
if len(clean) <= maxLen:
|
||||
return clean
|
||||
return clean[:maxLen].rsplit(" ", 1)[0] + "..."
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SSE Stream endpoint
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -121,12 +145,11 @@ async def streamWorkspaceStart(
|
|||
if not workflow:
|
||||
raise HTTPException(status_code=404, detail=f"Workflow {userInput.workflowId} not found")
|
||||
else:
|
||||
existingWorkflows = chatInterface.getWorkflows() or []
|
||||
nextNum = len(existingWorkflows) + 1
|
||||
autoName = _deriveWorkflowName(userInput.prompt)
|
||||
workflow = chatInterface.createWorkflow({
|
||||
"featureInstanceId": instanceId,
|
||||
"status": "active",
|
||||
"name": f"Chat {nextNum}",
|
||||
"name": autoName,
|
||||
"workflowMode": "Dynamic",
|
||||
})
|
||||
|
||||
|
|
@ -219,8 +242,14 @@ async def _runWorkspaceAgent(
|
|||
)
|
||||
agentService = getService("agent", ctx)
|
||||
|
||||
enrichedPrompt = prompt
|
||||
if dataSourceIds:
|
||||
dsInfo = _buildDataSourceContext(chatInterface, dataSourceIds)
|
||||
if dsInfo:
|
||||
enrichedPrompt = f"{prompt}\n\n[Active Data Sources]\n{dsInfo}"
|
||||
|
||||
async for event in agentService.runAgent(
|
||||
prompt=prompt,
|
||||
prompt=enrichedPrompt,
|
||||
fileIds=fileIds,
|
||||
workflowId=workflowId,
|
||||
userLanguage=userLanguage,
|
||||
|
|
@ -296,30 +325,35 @@ async def stopWorkspace(
|
|||
async def listWorkspaceWorkflows(
|
||||
request: Request,
|
||||
instanceId: str = Path(...),
|
||||
includeArchived: bool = Query(default=False, description="Include archived workflows"),
|
||||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""List all workspace workflows/conversations for this instance."""
|
||||
"""List 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)
|
||||
item = wf
|
||||
else:
|
||||
items.append({
|
||||
item = {
|
||||
"id": getattr(wf, "id", None),
|
||||
"name": getattr(wf, "name", ""),
|
||||
"status": getattr(wf, "status", ""),
|
||||
"startedAt": getattr(wf, "startedAt", None),
|
||||
"lastActivity": getattr(wf, "lastActivity", None),
|
||||
})
|
||||
}
|
||||
if not includeArchived and item.get("status") == "archived":
|
||||
continue
|
||||
items.append(item)
|
||||
return JSONResponse({"workflows": items})
|
||||
|
||||
|
||||
class UpdateWorkflowRequest(BaseModel):
|
||||
"""Request body for updating a workflow (PATCH)."""
|
||||
name: Optional[str] = Field(default=None, description="New workflow name")
|
||||
status: Optional[str] = Field(default=None, description="New status (active, archived)")
|
||||
|
||||
|
||||
@router.patch("/{instanceId}/workflows/{workflowId}")
|
||||
|
|
@ -340,6 +374,8 @@ async def patchWorkspaceWorkflow(
|
|||
updateData = {}
|
||||
if body.name is not None:
|
||||
updateData["name"] = body.name
|
||||
if body.status is not None:
|
||||
updateData["status"] = body.status
|
||||
if not updateData:
|
||||
updated = workflow
|
||||
else:
|
||||
|
|
@ -355,6 +391,47 @@ async def patchWorkspaceWorkflow(
|
|||
})
|
||||
|
||||
|
||||
@router.delete("/{instanceId}/workflows/{workflowId}")
|
||||
@limiter.limit("30/minute")
|
||||
async def deleteWorkspaceWorkflow(
|
||||
request: Request,
|
||||
instanceId: str = Path(...),
|
||||
workflowId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Delete a workspace workflow and its messages."""
|
||||
_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")
|
||||
chatInterface.deleteWorkflow(workflowId)
|
||||
return JSONResponse({"status": "deleted", "workflowId": workflowId})
|
||||
|
||||
|
||||
@router.post("/{instanceId}/workflows")
|
||||
@limiter.limit("30/minute")
|
||||
async def createWorkspaceWorkflow(
|
||||
request: Request,
|
||||
instanceId: str = Path(...),
|
||||
body: dict = Body(default={}),
|
||||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
"""Create a new empty workspace workflow."""
|
||||
_validateInstanceAccess(instanceId, context)
|
||||
chatInterface = _getChatInterface(context, featureInstanceId=instanceId)
|
||||
name = body.get("name", "Neuer Chat")
|
||||
workflow = chatInterface.createWorkflow({
|
||||
"featureInstanceId": instanceId,
|
||||
"status": "active",
|
||||
"name": name,
|
||||
"workflowMode": "Dynamic",
|
||||
})
|
||||
wfId = workflow.get("id") if isinstance(workflow, dict) else getattr(workflow, "id", None)
|
||||
wfName = workflow.get("name") if isinstance(workflow, dict) else getattr(workflow, "name", name)
|
||||
return JSONResponse({"id": wfId, "name": wfName, "status": "active"})
|
||||
|
||||
|
||||
@router.get("/{instanceId}/workflows/{workflowId}/messages")
|
||||
@limiter.limit("60/minute")
|
||||
async def getWorkspaceMessages(
|
||||
|
|
@ -398,7 +475,23 @@ async def listWorkspaceFiles(
|
|||
_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 [])]})
|
||||
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
rootInterface = getRootInterface()
|
||||
instanceLabelCache: dict = {}
|
||||
|
||||
result = []
|
||||
for f in (files or []):
|
||||
item = f if isinstance(f, dict) else f.model_dump()
|
||||
fiId = item.get("featureInstanceId") or ""
|
||||
if fiId and fiId not in instanceLabelCache:
|
||||
fi = rootInterface.getFeatureInstance(fiId)
|
||||
instanceLabelCache[fiId] = fi.label if fi else fiId
|
||||
item["featureInstanceId"] = fiId
|
||||
item["featureInstanceLabel"] = instanceLabelCache.get(fiId, "(Global)")
|
||||
result.append(item)
|
||||
|
||||
return JSONResponse({"files": result})
|
||||
|
||||
|
||||
@router.get("/{instanceId}/files/{fileId}/content")
|
||||
|
|
@ -735,11 +828,18 @@ async def getVoiceSettings(
|
|||
_validateInstanceAccess(instanceId, context)
|
||||
dbMgmt = _getDbManagement(context, instanceId)
|
||||
userId = str(context.user.id)
|
||||
try:
|
||||
vs = dbMgmt.getVoiceSettings(userId)
|
||||
if not vs:
|
||||
logger.info(f"GET voice settings: not found for user={userId}, creating defaults")
|
||||
vs = dbMgmt.getOrCreateVoiceSettings(userId)
|
||||
result = vs.model_dump() if vs else {}
|
||||
mapKeys = list(result.get("ttsVoiceMap", {}).keys()) if result else []
|
||||
logger.info(f"GET voice settings for user={userId}: ttsVoiceMap languages={mapKeys}")
|
||||
return JSONResponse(result)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load voice settings for user={userId}: {e}", exc_info=True)
|
||||
return JSONResponse({"ttsVoiceMap": {}}, status_code=200)
|
||||
|
||||
|
||||
@router.put("/{instanceId}/settings/voice")
|
||||
|
|
@ -755,8 +855,11 @@ async def updateVoiceSettings(
|
|||
dbMgmt = _getDbManagement(context, instanceId)
|
||||
userId = str(context.user.id)
|
||||
|
||||
try:
|
||||
logger.info(f"PUT voice settings for user={userId}, instance={instanceId}, body keys={list(body.keys())}")
|
||||
vs = dbMgmt.getVoiceSettings(userId)
|
||||
if not vs:
|
||||
logger.info(f"No existing voice settings, creating new for user={userId}")
|
||||
createData = {
|
||||
"userId": userId,
|
||||
"mandateId": str(context.mandateId) if context.mandateId else "",
|
||||
|
|
@ -764,11 +867,17 @@ async def updateVoiceSettings(
|
|||
}
|
||||
createData.update(body)
|
||||
created = dbMgmt.createVoiceSettings(createData)
|
||||
logger.info(f"Created voice settings for user={userId}, ttsVoiceMap keys={list((created or {}).get('ttsVoiceMap', {}).keys())}")
|
||||
return JSONResponse(created)
|
||||
|
||||
updateData = {k: v for k, v in body.items() if k not in ("id", "userId", "mandateId", "featureInstanceId", "creationDate")}
|
||||
logger.info(f"Updating voice settings for user={userId}, update keys={list(updateData.keys())}")
|
||||
updated = dbMgmt.updateVoiceSettings(userId, updateData)
|
||||
logger.info(f"Updated voice settings for user={userId}, ttsVoiceMap keys={list((updated or {}).get('ttsVoiceMap', {}).keys())}")
|
||||
return JSONResponse(updated)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update voice settings for user={userId}: {e}", exc_info=True)
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
|
||||
@router.get("/{instanceId}/voice/languages")
|
||||
|
|
|
|||
|
|
@ -219,6 +219,7 @@ async def upload_file(
|
|||
request: Request,
|
||||
file: UploadFile = File(...),
|
||||
workflowId: Optional[str] = Form(None),
|
||||
featureInstanceId: Optional[str] = Form(None),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> JSONResponse:
|
||||
# Add fileName property to UploadFile for consistency with backend model
|
||||
|
|
@ -241,6 +242,10 @@ async def upload_file(
|
|||
# Save file via LucyDOM interface in the database
|
||||
fileItem, duplicateType = managementInterface.saveUploadedFile(fileContent, file.filename)
|
||||
|
||||
if featureInstanceId and not fileItem.featureInstanceId:
|
||||
managementInterface.updateFile(fileItem.id, {"featureInstanceId": featureInstanceId})
|
||||
fileItem.featureInstanceId = featureInstanceId
|
||||
|
||||
# Determine response message based on duplicate type
|
||||
if duplicateType == "exact_duplicate":
|
||||
message = f"File '{file.filename}' already exists with identical content. Reusing existing file."
|
||||
|
|
|
|||
|
|
@ -138,8 +138,23 @@ class ConversationManager:
|
|||
if len(nonSystemMessages) <= keepRecent + 1:
|
||||
return None
|
||||
|
||||
messagesToSummarize = nonSystemMessages[:-keepRecent]
|
||||
recentMessages = nonSystemMessages[-keepRecent:]
|
||||
splitIdx = len(nonSystemMessages) - keepRecent
|
||||
# Ensure the split doesn't orphan tool messages from their assistant.
|
||||
# Walk backwards from splitIdx: if we're landing in the middle of a
|
||||
# tool-call sequence (assistant+tool_calls → tool → tool …), include
|
||||
# the entire sequence in recentMessages.
|
||||
while splitIdx > 0 and nonSystemMessages[splitIdx].get("role") == "tool":
|
||||
splitIdx -= 1
|
||||
# Also include the assistant message that triggered the tool calls.
|
||||
if splitIdx > 0 and splitIdx < len(nonSystemMessages) and \
|
||||
nonSystemMessages[splitIdx].get("role") == "assistant" and \
|
||||
nonSystemMessages[splitIdx].get("tool_calls"):
|
||||
pass # splitIdx already points at the assistant; keep it in recent
|
||||
elif splitIdx == 0:
|
||||
return None # nothing to summarize
|
||||
|
||||
messagesToSummarize = nonSystemMessages[:splitIdx]
|
||||
recentMessages = nonSystemMessages[splitIdx:]
|
||||
|
||||
summaryInput = _formatMessagesForSummary(messagesToSummarize)
|
||||
previousSummary = self._summaries[-1]["content"] if self._summaries else ""
|
||||
|
|
|
|||
|
|
@ -478,10 +478,16 @@ def _registerCoreTools(registry: ToolRegistry, services):
|
|||
return ToolResult(toolCallId="", toolName="webSearch", success=False, error="query is required")
|
||||
try:
|
||||
webService = services.getService("web")
|
||||
result = await webService.search(query)
|
||||
result = await webService.performWebResearch(
|
||||
prompt=query,
|
||||
urls=[],
|
||||
country=None,
|
||||
language=args.get("language"),
|
||||
)
|
||||
summary = result.get("summary", "") if isinstance(result, dict) else str(result)
|
||||
return ToolResult(
|
||||
toolCallId="", toolName="webSearch", success=True,
|
||||
data=result if isinstance(result, str) else str(result)
|
||||
data=summary or str(result)
|
||||
)
|
||||
except Exception as e:
|
||||
return ToolResult(toolCallId="", toolName="webSearch", success=False, error=str(e))
|
||||
|
|
@ -542,6 +548,9 @@ def _registerCoreTools(registry: ToolRegistry, services):
|
|||
fileItem, _ = chatService.interfaceDbComponent.saveUploadedFile(
|
||||
content.encode("utf-8"), name
|
||||
)
|
||||
fiId = context.get("featureInstanceId") or (services.featureInstanceId if services else "")
|
||||
if fiId:
|
||||
chatService.interfaceDbComponent.updateFile(fileItem.id, {"featureInstanceId": fiId})
|
||||
if args.get("folderId"):
|
||||
chatService.interfaceDbComponent.updateFile(fileItem.id, {"folderId": args["folderId"]})
|
||||
if args.get("tags"):
|
||||
|
|
@ -1455,6 +1464,9 @@ def _registerCoreTools(registry: ToolRegistry, services):
|
|||
|
||||
if fileItem:
|
||||
fid = fileItem.id if hasattr(fileItem, "id") else fileItem.get("id", "?")
|
||||
fiId = context.get("featureInstanceId") or (services.featureInstanceId if services else "")
|
||||
if fiId:
|
||||
chatService.interfaceDbComponent.updateFile(fid, {"featureInstanceId": fiId})
|
||||
savedFiles.append(f"- {docName} (id: {fid})")
|
||||
sideEvents.append({
|
||||
"type": "fileCreated",
|
||||
|
|
|
|||
Loading…
Reference in a new issue