diff --git a/api/core/llm_generator/prompts.py b/api/core/llm_generator/prompts.py index bbfb7dfa6d..766ae07231 100644 --- a/api/core/llm_generator/prompts.py +++ b/api/core/llm_generator/prompts.py @@ -148,20 +148,28 @@ You are an expert workflow designer. Generate a Mermaid flowchart based on the u Constraints: - Use only node types listed in . -- Use only tools listed in . When using a tool node, set type=tool and tool=/. +- Use only tools listed in . When using a tool node, set type=tool and tool=. +- Tools may include MCP providers (provider_type=mcp). Tool selection still uses tool_key. - Prefer reusing node titles from when possible. - Output must be valid Mermaid flowchart syntax, no markdown, no extra text. - First line must be: flowchart LR - Every node must be declared on its own line using: - ["type=|title=|tool=<provider_id>/<tool_name>"] + <id>["type=<type>|title=<title>|tool=<tool_key>"] - type is required and must match a type in <available_nodes>. - title is required for non-tool nodes. - tool is required only when type=tool, otherwise omit tool. +- Declare all node lines before any edges. - Edges must use: <id> --> <id> <id> -->|true| <id> <id> -->|false| <id> - Keep node ids unique and simple (N1, N2, ...). +- For complex orchestration: + - Break the request into stages (ingest, transform, decision, action, output). + - Use IfElse for branching and label edges true/false only. + - Fan-in branches by connecting multiple nodes into a shared downstream node. + - Avoid cycles unless explicitly requested. + - Keep each branch complete with a clear downstream target. <user_request> {{TASK_DESCRIPTION}} diff --git a/web/app/components/goto-anything/actions/banana.tsx b/web/app/components/goto-anything/actions/banana.tsx new file mode 100644 index 0000000000..a4b4c21023 --- /dev/null +++ b/web/app/components/goto-anything/actions/banana.tsx @@ -0,0 +1,39 @@ +import type { ActionItem } from './types' +import { RiSparklingFill } from '@remixicon/react' +import * as React from 'react' +import { isInWorkflowPage } from '@/app/components/workflow/constants' +import i18n from '@/i18n-config/i18next-config' + +const BANANA_PROMPT_EXAMPLE = 'Summarize a document, classify sentiment, then notify Slack' + +export const bananaAction: ActionItem = { + key: '@banana', + shortcut: '@banana', + title: i18n.t('app.gotoAnything.actions.vibeTitle'), + description: i18n.t('app.gotoAnything.actions.vibeDesc'), + search: async (_query, searchTerm = '', locale) => { + if (!isInWorkflowPage()) + return [] + + const trimmed = searchTerm.trim() + const hasInput = !!trimmed + + return [{ + id: 'banana-vibe', + title: i18n.t('app.gotoAnything.actions.vibeTitle', { lng: locale }) || 'Banana', + description: hasInput + ? i18n.t('app.gotoAnything.actions.vibeDesc', { lng: locale }) + : i18n.t('app.gotoAnything.actions.vibeHint', { lng: locale, prompt: BANANA_PROMPT_EXAMPLE }), + type: 'command' as const, + icon: ( + <div className="flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg"> + <RiSparklingFill className="h-4 w-4 text-text-tertiary" /> + </div> + ), + data: { + command: 'workflow.vibe', + args: { dsl: trimmed }, + }, + }] + }, +} diff --git a/web/app/components/workflow/hooks/use-workflow-vibe.tsx b/web/app/components/workflow/hooks/use-workflow-vibe.tsx index 9581379402..d1f4637389 100644 --- a/web/app/components/workflow/hooks/use-workflow-vibe.tsx +++ b/web/app/components/workflow/hooks/use-workflow-vibe.tsx @@ -64,7 +64,7 @@ type ParsedEdge = { } type ParseError = { - error: 'invalidMermaid' | 'missingNodeType' | 'unknownNodeType' | 'unknownTool' | 'missingNodeDefinition' + error: 'invalidMermaid' | 'missingNodeType' | 'unknownNodeType' | 'unknownTool' | 'missingNodeDefinition' | 'unknownNodeId' | 'unsupportedEdgeLabel' detail?: string } @@ -111,6 +111,9 @@ const parseNodeLabel = (label: string) => { info.type = tokens[0] } + if (!info.tool && info.tool_key) + info.tool = info.tool_key + return info } @@ -125,6 +128,17 @@ const parseNodeToken = (token: string) => { return null } +const normalizeBranchLabel = (label?: string) => { + if (!label) + return '' + const normalized = label.trim().toLowerCase() + if (['true', 'yes', 'y', '1'].includes(normalized)) + return 'true' + if (['false', 'no', 'n', '0'].includes(normalized)) + return 'false' + return '' +} + const parseMermaidFlowchart = ( raw: string, nodeTypeLookup: Map<string, BlockEnum>, @@ -137,6 +151,7 @@ const parseMermaidFlowchart = ( }).filter(Boolean) const nodesMap = new Map<string, ParsedNodeDraft>() + const declaredNodeIds = new Set<string>() const edges: ParsedEdge[] = [] const registerNode = (id: string, label?: string): ParseError | null => { @@ -172,6 +187,7 @@ const parseMermaidFlowchart = ( } nodesMap.set(id, { ...(existing || {}), ...nodeData }) + declaredNodeIds.add(id) return null } @@ -189,6 +205,11 @@ const parseMermaidFlowchart = ( if (!sourceToken || !targetToken) return { error: 'invalidMermaid', detail: line } + if (!sourceToken.label && !declaredNodeIds.has(sourceToken.id)) + return { error: 'unknownNodeId', detail: sourceToken.id } + if (!targetToken.label && !declaredNodeIds.has(targetToken.id)) + return { error: 'unknownNodeId', detail: targetToken.id } + const sourceError = registerNode(sourceToken.id, sourceToken.label) if (sourceError) return sourceError @@ -213,15 +234,26 @@ const parseMermaidFlowchart = ( } const parsedNodes: ParsedNode[] = [] + const nodeTypeById = new Map<string, BlockEnum>() for (const node of nodesMap.values()) { if (!node.type) return { error: 'missingNodeDefinition', detail: node.id } parsedNodes.push(node as ParsedNode) + nodeTypeById.set(node.id, node.type) } if (!parsedNodes.length) return { error: 'invalidMermaid', detail: '' } + for (const edge of edges) { + if (!edge.label) + continue + const sourceType = nodeTypeById.get(edge.sourceId) + const branchLabel = normalizeBranchLabel(edge.label) + if (sourceType !== BlockEnum.IfElse || !branchLabel) + return { error: 'unsupportedEdgeLabel', detail: edge.label } + } + return { nodes: parsedNodes, edges } } @@ -231,17 +263,6 @@ const dedupeHandles = (handles?: string[]) => { return Array.from(new Set(handles)) } -const normalizeBranchLabel = (label?: string) => { - if (!label) - return '' - const normalized = label.trim().toLowerCase() - if (['true', 'yes', 'y', '1'].includes(normalized)) - return 'true' - if (['false', 'no', 'n', '0'].includes(normalized)) - return 'false' - return '' -} - const buildToolParams = (parameters?: Tool['parameters']) => { const params: Record<string, string> = {} if (!parameters) @@ -426,6 +447,7 @@ export const useWorkflowVibe = () => { const toolsPayload = toolOptions.map(tool => ({ provider_id: tool.provider_id, provider_name: tool.provider_name, + provider_type: tool.provider_type, tool_name: tool.tool_name, tool_label: tool.tool_label, tool_key: `${tool.provider_id}/${tool.tool_name}`, @@ -467,12 +489,18 @@ export const useWorkflowVibe = () => { case 'missingNodeDefinition': Toast.notify({ type: 'error', message: t('workflow.vibe.invalidFlowchart') }) return + case 'unknownNodeId': + Toast.notify({ type: 'error', message: t('workflow.vibe.unknownNodeId', { id: parseResult.detail }) }) + return case 'unknownNodeType': Toast.notify({ type: 'error', message: t('workflow.vibe.nodeTypeUnavailable', { type: parseResult.detail }) }) return case 'unknownTool': Toast.notify({ type: 'error', message: t('workflow.vibe.toolUnavailable', { tool: parseResult.detail }) }) return + case 'unsupportedEdgeLabel': + Toast.notify({ type: 'error', message: t('workflow.vibe.unsupportedEdgeLabel', { label: parseResult.detail }) }) + return default: Toast.notify({ type: 'error', message: t('workflow.vibe.invalidFlowchart') }) return @@ -547,11 +575,11 @@ export const useWorkflowVibe = () => { }) const newEdges: Edge[] = [] - parseResult.edges.forEach((edgeSpec) => { + for (const edgeSpec of parseResult.edges) { const sourceNode = nodeIdMap.get(edgeSpec.sourceId) const targetNode = nodeIdMap.get(edgeSpec.targetId) if (!sourceNode || !targetNode) - return + continue let sourceHandle = 'source' if (sourceNode.data.type === BlockEnum.IfElse) { @@ -565,7 +593,7 @@ export const useWorkflowVibe = () => { } newEdges.push(buildEdge(sourceNode, targetNode, sourceHandle)) - }) + } const bounds = nodes.reduce( (acc, node) => { diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 9f85d4c558..022317d9b5 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -132,6 +132,8 @@ const translation = { invalidFlowchart: 'The generated flowchart could not be parsed.', nodeTypeUnavailable: 'Node type "{{type}}" is not available in this workflow.', toolUnavailable: 'Tool "{{tool}}" is not available in this workspace.', + unknownNodeId: 'Node "{{id}}" is used before it is defined.', + unsupportedEdgeLabel: 'Unsupported edge label "{{label}}". Only true/false are allowed for if/else.', }, publishLimit: { startNodeTitlePrefix: 'Upgrade to',