workflow fixes
This commit is contained in:
parent
a24b20d302
commit
b418207c2c
6 changed files with 101 additions and 24 deletions
|
|
@ -113,7 +113,13 @@ class DriveAdapter(ServiceAdapter):
|
||||||
|
|
||||||
async def search(self, query: str, path: Optional[str] = None) -> List[ExternalEntry]:
|
async def search(self, query: str, path: Optional[str] = None) -> List[ExternalEntry]:
|
||||||
safeQuery = query.replace("'", "\\'")
|
safeQuery = query.replace("'", "\\'")
|
||||||
url = f"{_DRIVE_BASE}/files?q=name contains '{safeQuery}' and trashed=false&fields=files(id,name,mimeType,size)&pageSize=25"
|
folderId = (path or "").strip("/")
|
||||||
|
qParts = [f"name contains '{safeQuery}'", "trashed=false"]
|
||||||
|
if folderId:
|
||||||
|
qParts.append(f"'{folderId}' in parents")
|
||||||
|
qStr = " and ".join(qParts)
|
||||||
|
url = f"{_DRIVE_BASE}/files?q={qStr}&fields=files(id,name,mimeType,size)&pageSize=25"
|
||||||
|
logger.debug(f"Google Drive search: q={qStr}")
|
||||||
result = await _googleGet(self._token, url)
|
result = await _googleGet(self._token, url)
|
||||||
if "error" in result:
|
if "error" in result:
|
||||||
return []
|
return []
|
||||||
|
|
|
||||||
|
|
@ -112,8 +112,11 @@ def _buildDataSourceContext(chatService, dataSourceIds: List[str]) -> str:
|
||||||
"""Build a description of active data sources for the agent prompt."""
|
"""Build a description of active data sources for the agent prompt."""
|
||||||
parts = [
|
parts = [
|
||||||
"The user has attached the following external data sources to this prompt.",
|
"The user has attached the following external data sources to this prompt.",
|
||||||
"IMPORTANT: Use the dataSourceId (UUID) exactly as shown below when calling browseDataSource or searchDataSource.",
|
"IMPORTANT RULES for attached data sources:",
|
||||||
"Use downloadFromDataSource to download a specific file into local storage.",
|
"- Use ONLY browseDataSource, searchDataSource, and downloadFromDataSource to access these sources.",
|
||||||
|
"- Use the dataSourceId (UUID) exactly as shown below.",
|
||||||
|
"- Do NOT use listFiles, externalBrowse, or externalSearch for attached data sources -- those tools are for other purposes.",
|
||||||
|
"- browseDataSource returns BOTH files and folders at the given path.",
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
found = False
|
found = False
|
||||||
|
|
@ -139,12 +142,32 @@ def _buildDataSourceContext(chatService, dataSourceIds: List[str]) -> str:
|
||||||
return "\n".join(parts) if found else ""
|
return "\n".join(parts) if found else ""
|
||||||
|
|
||||||
|
|
||||||
def _deriveWorkflowName(prompt: str, maxLen: int = 40) -> str:
|
async def _deriveWorkflowName(prompt: str, aiService) -> str:
|
||||||
"""Derive a short workflow name from the user's first prompt."""
|
"""Use AI to generate a concise workflow title from the user prompt."""
|
||||||
clean = " ".join(prompt.split())
|
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, PriorityEnum
|
||||||
if len(clean) <= maxLen:
|
try:
|
||||||
return clean
|
cleanPrompt = prompt.split("\n[Active Data Sources]")[0].strip()[:300]
|
||||||
return clean[:maxLen].rsplit(" ", 1)[0] + "..."
|
req = AiCallRequest(
|
||||||
|
prompt=(
|
||||||
|
"Generate a short title (3-6 words) for a chat conversation that starts with this user message. "
|
||||||
|
"Reply with ONLY the title, nothing else. Same language as the user message.\n\n"
|
||||||
|
f"User message: {cleanPrompt}"
|
||||||
|
),
|
||||||
|
options=AiCallOptions(
|
||||||
|
operationType=OperationTypeEnum.DATA_EXTRACT,
|
||||||
|
priority=PriorityEnum.SPEED,
|
||||||
|
compressPrompt=False,
|
||||||
|
temperature=0.3,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
resp = await aiService.callAi(req)
|
||||||
|
title = (resp.content or "").strip().strip('"\'').strip()
|
||||||
|
if title and len(title) <= 60:
|
||||||
|
return title
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"AI naming failed, using fallback: {e}")
|
||||||
|
text = prompt.split("\n[Active Data Sources]")[0].split("\n")[0].strip()[:50]
|
||||||
|
return text or "Chat"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -170,11 +193,10 @@ async def streamWorkspaceStart(
|
||||||
if not workflow:
|
if not workflow:
|
||||||
raise HTTPException(status_code=404, detail=f"Workflow {userInput.workflowId} not found")
|
raise HTTPException(status_code=404, detail=f"Workflow {userInput.workflowId} not found")
|
||||||
else:
|
else:
|
||||||
autoName = _deriveWorkflowName(userInput.prompt)
|
|
||||||
workflow = chatInterface.createWorkflow({
|
workflow = chatInterface.createWorkflow({
|
||||||
"featureInstanceId": instanceId,
|
"featureInstanceId": instanceId,
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"name": autoName,
|
"name": "",
|
||||||
"workflowMode": "Dynamic",
|
"workflowMode": "Dynamic",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -267,6 +289,25 @@ async def _runWorkspaceAgent(
|
||||||
)
|
)
|
||||||
agentService = getService("agent", ctx)
|
agentService = getService("agent", ctx)
|
||||||
chatService = getService("chat", ctx)
|
chatService = getService("chat", ctx)
|
||||||
|
aiService = getService("ai", ctx)
|
||||||
|
|
||||||
|
wfRecord = chatInterface.getWorkflow(workflowId) if workflowId else None
|
||||||
|
wfName = ""
|
||||||
|
if wfRecord:
|
||||||
|
wfName = wfRecord.get("name", "") if isinstance(wfRecord, dict) else getattr(wfRecord, "name", "")
|
||||||
|
if not wfName.strip() or wfName.startswith("Neuer Chat"):
|
||||||
|
async def _nameInBackground():
|
||||||
|
try:
|
||||||
|
autoName = await _deriveWorkflowName(prompt, aiService)
|
||||||
|
chatInterface.updateWorkflow(workflowId, {"name": autoName})
|
||||||
|
await eventManager.emit_event(queueId, "workflowUpdated", {
|
||||||
|
"type": "workflowUpdated",
|
||||||
|
"workflowId": workflowId,
|
||||||
|
"name": autoName,
|
||||||
|
})
|
||||||
|
except Exception as nameErr:
|
||||||
|
logger.warning(f"AI workflow naming failed: {nameErr}")
|
||||||
|
asyncio.ensure_future(_nameInBackground())
|
||||||
|
|
||||||
enrichedPrompt = prompt
|
enrichedPrompt = prompt
|
||||||
if dataSourceIds:
|
if dataSourceIds:
|
||||||
|
|
@ -300,12 +341,16 @@ async def _runWorkspaceAgent(
|
||||||
|
|
||||||
if event.type in (AgentEventTypeEnum.FINAL, AgentEventTypeEnum.ERROR):
|
if event.type in (AgentEventTypeEnum.FINAL, AgentEventTypeEnum.ERROR):
|
||||||
if event.content:
|
if event.content:
|
||||||
|
try:
|
||||||
chatInterface.createMessage({
|
chatInterface.createMessage({
|
||||||
"workflowId": workflowId,
|
"workflowId": workflowId,
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"message": event.content,
|
"message": event.content,
|
||||||
})
|
})
|
||||||
|
except Exception as msgErr:
|
||||||
|
logger.error(f"Failed to persist assistant message: {msgErr}")
|
||||||
|
|
||||||
|
logger.info(f"Agent loop completed for workflow {workflowId}, sending 'complete' event")
|
||||||
await eventManager.emit_event(queueId, "complete", {
|
await eventManager.emit_event(queueId, "complete", {
|
||||||
"type": "complete",
|
"type": "complete",
|
||||||
"workflowId": workflowId,
|
"workflowId": workflowId,
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,22 @@ class EventManager:
|
||||||
Returns:
|
Returns:
|
||||||
Async queue for events
|
Async queue for events
|
||||||
"""
|
"""
|
||||||
|
if workflow_id in self._cleanup_tasks:
|
||||||
|
self._cleanup_tasks[workflow_id].cancel()
|
||||||
|
del self._cleanup_tasks[workflow_id]
|
||||||
|
logger.debug(f"Cancelled pending cleanup for workflow {workflow_id}")
|
||||||
|
|
||||||
if workflow_id not in self._queues:
|
if workflow_id not in self._queues:
|
||||||
self._queues[workflow_id] = asyncio.Queue()
|
self._queues[workflow_id] = asyncio.Queue()
|
||||||
logger.debug(f"Created event queue for workflow {workflow_id}")
|
logger.debug(f"Created event queue for workflow {workflow_id}")
|
||||||
|
else:
|
||||||
|
old = self._queues[workflow_id]
|
||||||
|
while not old.empty():
|
||||||
|
try:
|
||||||
|
old.get_nowait()
|
||||||
|
except asyncio.QueueEmpty:
|
||||||
|
break
|
||||||
|
logger.debug(f"Reusing event queue for workflow {workflow_id} (drained stale events)")
|
||||||
return self._queues[workflow_id]
|
return self._queues[workflow_id]
|
||||||
|
|
||||||
def get_queue(self, workflow_id: str) -> Optional[asyncio.Queue]:
|
def get_queue(self, workflow_id: str) -> Optional[asyncio.Queue]:
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@ async def runAgentLoop(
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
logger.info(f"Agent round {state.currentRound}/{state.maxRounds} for workflow {workflowId} (tools={state.totalToolCalls}, cost={state.totalCostCHF:.4f})")
|
||||||
yield AgentEvent(
|
yield AgentEvent(
|
||||||
type=AgentEventTypeEnum.AGENT_PROGRESS,
|
type=AgentEventTypeEnum.AGENT_PROGRESS,
|
||||||
data={
|
data={
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,9 @@ class AgentService:
|
||||||
):
|
):
|
||||||
if event.type == AgentEventTypeEnum.AGENT_SUMMARY:
|
if event.type == AgentEventTypeEnum.AGENT_SUMMARY:
|
||||||
await self._persistTrace(workflowId, event.data or {})
|
await self._persistTrace(workflowId, event.data or {})
|
||||||
|
logger.debug(f"runAgent yielding event type={event.type}")
|
||||||
yield event
|
yield event
|
||||||
|
logger.info(f"runAgent loop completed for workflow {workflowId}")
|
||||||
|
|
||||||
async def _enrichPromptWithFiles(self, prompt: str, fileIds: List[str] = None) -> str:
|
async def _enrichPromptWithFiles(self, prompt: str, fileIds: List[str] = None) -> str:
|
||||||
"""Resolve file metadata + FileContentIndex for attached fileIds and prepend to prompt.
|
"""Resolve file metadata + FileContentIndex for attached fileIds and prepend to prompt.
|
||||||
|
|
@ -586,7 +588,7 @@ def _registerCoreTools(registry: ToolRegistry, services):
|
||||||
|
|
||||||
registry.register(
|
registry.register(
|
||||||
"listFiles", _listFiles,
|
"listFiles", _listFiles,
|
||||||
description="List files with optional filters (folder, tags, search text).",
|
description="List LOCAL workspace files (uploaded/generated). NOT for external data sources -- use browseDataSource instead.",
|
||||||
parameters={
|
parameters={
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -600,7 +602,7 @@ def _registerCoreTools(registry: ToolRegistry, services):
|
||||||
|
|
||||||
registry.register(
|
registry.register(
|
||||||
"searchFiles", _searchFiles,
|
"searchFiles", _searchFiles,
|
||||||
description="Search files by name, description, or tags.",
|
description="Search LOCAL workspace files by name, description, or tags. NOT for external data sources -- use searchDataSource instead.",
|
||||||
parameters={
|
parameters={
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -614,7 +616,7 @@ def _registerCoreTools(registry: ToolRegistry, services):
|
||||||
|
|
||||||
registry.register(
|
registry.register(
|
||||||
"listFolders", _listFolders,
|
"listFolders", _listFolders,
|
||||||
description="List file folders. Use parentId to browse folder hierarchy.",
|
description="List LOCAL workspace folders. NOT for external data sources -- use browseDataSource instead.",
|
||||||
parameters={
|
parameters={
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -859,7 +861,7 @@ def _registerCoreTools(registry: ToolRegistry, services):
|
||||||
|
|
||||||
registry.register(
|
registry.register(
|
||||||
"externalBrowse", _externalBrowse,
|
"externalBrowse", _externalBrowse,
|
||||||
description="Browse files and folders in an external data source (SharePoint, Drive, FTP).",
|
description="Browse files in an external source by connectionId+service. For ATTACHED data sources, prefer browseDataSource instead.",
|
||||||
parameters={
|
parameters={
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -903,7 +905,7 @@ def _registerCoreTools(registry: ToolRegistry, services):
|
||||||
|
|
||||||
registry.register(
|
registry.register(
|
||||||
"externalSearch", _externalSearch,
|
"externalSearch", _externalSearch,
|
||||||
description="Search for files in an external data source.",
|
description="Search files in an external source by connectionId+service. For ATTACHED data sources, prefer searchDataSource instead.",
|
||||||
parameters={
|
parameters={
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -964,7 +966,10 @@ def _registerCoreTools(registry: ToolRegistry, services):
|
||||||
return ToolResult(toolCallId="", toolName="browseDataSource", success=False, error="dataSourceId is required")
|
return ToolResult(toolCallId="", toolName="browseDataSource", success=False, error="dataSourceId is required")
|
||||||
try:
|
try:
|
||||||
connectionId, service, basePath = await _resolveDataSource(dsId)
|
connectionId, service, basePath = await _resolveDataSource(dsId)
|
||||||
browsePath = f"{basePath.rstrip('/')}/{subPath.lstrip('/')}" if subPath else basePath
|
if subPath:
|
||||||
|
browsePath = subPath
|
||||||
|
else:
|
||||||
|
browsePath = basePath
|
||||||
from modules.connectors.connectorResolver import ConnectorResolver
|
from modules.connectors.connectorResolver import ConnectorResolver
|
||||||
resolver = ConnectorResolver(
|
resolver = ConnectorResolver(
|
||||||
services.getService("security"),
|
services.getService("security"),
|
||||||
|
|
@ -1035,7 +1040,7 @@ def _registerCoreTools(registry: ToolRegistry, services):
|
||||||
|
|
||||||
registry.register(
|
registry.register(
|
||||||
"browseDataSource", _browseDataSource,
|
"browseDataSource", _browseDataSource,
|
||||||
description="Browse files and folders in an attached data source by its dataSourceId. Returns file/folder listing.",
|
description="Browse files AND folders in an ATTACHED data source by its dataSourceId. This is the PRIMARY tool for listing data source contents.",
|
||||||
parameters={
|
parameters={
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -1610,7 +1615,7 @@ def _registerCoreTools(registry: ToolRegistry, services):
|
||||||
sideEvents = []
|
sideEvents = []
|
||||||
chatService = services.chat
|
chatService = services.chat
|
||||||
|
|
||||||
sanitizedTitle = _re.sub(r'[^a-zA-Z0-9._-]', '_', title).strip('_') or "document"
|
sanitizedTitle = _re.sub(r'[^\w._-]', '_', title, flags=_re.UNICODE).strip('_') or "document"
|
||||||
|
|
||||||
for doc in documents:
|
for doc in documents:
|
||||||
docData = doc.documentData if hasattr(doc, "documentData") else b""
|
docData = doc.documentData if hasattr(doc, "documentData") else b""
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,8 @@ class ToolRegistry:
|
||||||
)
|
)
|
||||||
|
|
||||||
handler = self._handlers[toolCall.name]
|
handler = self._handlers[toolCall.name]
|
||||||
|
argsSummary = ", ".join(f"{k}={str(v)[:80]}" for k, v in (toolCall.args or {}).items())
|
||||||
|
logger.info(f"Tool dispatch: {toolCall.name}({argsSummary})")
|
||||||
try:
|
try:
|
||||||
result = await handler(toolCall.args, context or {})
|
result = await handler(toolCall.args, context or {})
|
||||||
durationMs = int((time.time() - startTime) * 1000)
|
durationMs = int((time.time() - startTime) * 1000)
|
||||||
|
|
@ -93,6 +95,11 @@ class ToolRegistry:
|
||||||
if isinstance(result, ToolResult):
|
if isinstance(result, ToolResult):
|
||||||
result.toolCallId = toolCall.id
|
result.toolCallId = toolCall.id
|
||||||
result.durationMs = durationMs
|
result.durationMs = durationMs
|
||||||
|
dataSummary = (result.data[:200] + "...") if result.data and len(result.data) > 200 else (result.data or "")
|
||||||
|
if result.success:
|
||||||
|
logger.info(f"Tool result: {toolCall.name} OK ({durationMs}ms) → {dataSummary}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Tool result: {toolCall.name} FAILED ({durationMs}ms) → {result.error}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
return ToolResult(
|
return ToolResult(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue