mirror of https://github.com/langgenius/dify.git
feat: add MCP tools
This commit is contained in:
parent
336769deb1
commit
9d496ed3dc
|
|
@ -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 <available_nodes>.
|
||||
- Use only tools listed in <available_tools>. When using a tool node, set type=tool and tool=<provider_id>/<tool_name>.
|
||||
- Use only tools listed in <available_tools>. When using a tool node, set type=tool and tool=<tool_key>.
|
||||
- Tools may include MCP providers (provider_type=mcp). Tool selection still uses tool_key.
|
||||
- Prefer reusing node titles from <existing_nodes> 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:
|
||||
<id>["type=<type>|title=<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}}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
},
|
||||
}]
|
||||
},
|
||||
}
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in New Issue