feat(workflow): workflow as tool output schema (#26241)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: Novice <novice12185727@gmail.com>
This commit is contained in:
CrabSAMA
2025-11-27 16:50:48 +08:00
committed by GitHub
parent 299bd351fd
commit 820925a866
21 changed files with 438 additions and 34 deletions

View File

@@ -38,7 +38,7 @@ import {
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button'
import type { InputVar } from '@/app/components/workflow/types'
import type { InputVar, Variable } from '@/app/components/workflow/types'
import { appDefaultIconBackground } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
@@ -103,6 +103,7 @@ export type AppPublisherProps = {
crossAxisOffset?: number
toolPublished?: boolean
inputs?: InputVar[]
outputs?: Variable[]
onRefreshData?: () => void
workflowToolAvailable?: boolean
missingStartNode?: boolean
@@ -125,6 +126,7 @@ const AppPublisher = ({
crossAxisOffset = 0,
toolPublished,
inputs,
outputs,
onRefreshData,
workflowToolAvailable = true,
missingStartNode = false,
@@ -457,6 +459,7 @@ const AppPublisher = ({
name={appDetail?.name}
description={appDetail?.description}
inputs={inputs}
outputs={outputs}
handlePublish={handlePublish}
onRefreshData={onRefreshData}
disabledReason={workflowToolMessage}

View File

@@ -1,4 +1,5 @@
import type { TypeWithI18N } from '../header/account-setting/model-provider-page/declarations'
import type { VarType } from '../workflow/types'
export enum LOC {
tools = 'tools',
@@ -194,6 +195,21 @@ export type WorkflowToolProviderParameter = {
type?: string
}
export type WorkflowToolProviderOutputParameter = {
name: string
description: string
type?: VarType
reserved?: boolean
}
export type WorkflowToolProviderOutputSchema = {
type: string
properties: Record<string, {
type: string
description: string
}>
}
export type WorkflowToolProviderRequest = {
name: string
icon: Emoji
@@ -218,6 +234,7 @@ export type WorkflowToolProviderResponse = {
description: TypeWithI18N
labels: string[]
parameters: ParamItem[]
output_schema: WorkflowToolProviderOutputSchema
}
privacy_policy: string
}

View File

@@ -11,8 +11,8 @@ import WorkflowToolModal from '@/app/components/tools/workflow-tool'
import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast'
import { createWorkflowToolProvider, fetchWorkflowToolDetailByAppID, saveWorkflowToolProvider } from '@/service/tools'
import type { Emoji, WorkflowToolProviderParameter, WorkflowToolProviderRequest, WorkflowToolProviderResponse } from '@/app/components/tools/types'
import type { InputVar } from '@/app/components/workflow/types'
import type { Emoji, WorkflowToolProviderOutputParameter, WorkflowToolProviderParameter, WorkflowToolProviderRequest, WorkflowToolProviderResponse } from '@/app/components/tools/types'
import type { InputVar, Variable } from '@/app/components/workflow/types'
import type { PublishWorkflowParams } from '@/types/workflow'
import { useAppContext } from '@/context/app-context'
import { useInvalidateAllWorkflowTools } from '@/service/use-tools'
@@ -26,6 +26,7 @@ type Props = {
name: string
description: string
inputs?: InputVar[]
outputs?: Variable[]
handlePublish: (params?: PublishWorkflowParams) => Promise<void>
onRefreshData?: () => void
disabledReason?: string
@@ -40,6 +41,7 @@ const WorkflowToolConfigureButton = ({
name,
description,
inputs,
outputs,
handlePublish,
onRefreshData,
disabledReason,
@@ -80,6 +82,8 @@ const WorkflowToolConfigureButton = ({
const payload = useMemo(() => {
let parameters: WorkflowToolProviderParameter[] = []
let outputParameters: WorkflowToolProviderOutputParameter[] = []
if (!published) {
parameters = (inputs || []).map((item) => {
return {
@@ -90,6 +94,13 @@ const WorkflowToolConfigureButton = ({
type: item.type,
}
})
outputParameters = (outputs || []).map((item) => {
return {
name: item.variable,
description: '',
type: item.value_type,
}
})
}
else if (detail && detail.tool) {
parameters = (inputs || []).map((item) => {
@@ -101,6 +112,14 @@ const WorkflowToolConfigureButton = ({
form: detail.tool.parameters.find(param => param.name === item.variable)?.form || 'llm',
}
})
outputParameters = (outputs || []).map((item) => {
const found = detail.tool.output_schema?.properties?.[item.variable]
return {
name: item.variable,
description: found ? found.description : '',
type: item.value_type,
}
})
}
return {
icon: detail?.icon || icon,
@@ -108,6 +127,7 @@ const WorkflowToolConfigureButton = ({
name: detail?.name || '',
description: detail?.description || description,
parameters,
outputParameters,
labels: detail?.tool?.labels || [],
privacy_policy: detail?.privacy_policy || '',
...(published

View File

@@ -1,9 +1,9 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { produce } from 'immer'
import type { Emoji, WorkflowToolProviderParameter, WorkflowToolProviderRequest } from '../types'
import type { Emoji, WorkflowToolProviderOutputParameter, WorkflowToolProviderParameter, WorkflowToolProviderRequest } from '../types'
import cn from '@/utils/classnames'
import Drawer from '@/app/components/base/drawer-plus'
import Input from '@/app/components/base/input'
@@ -16,6 +16,8 @@ import MethodSelector from '@/app/components/tools/workflow-tool/method-selector
import LabelSelector from '@/app/components/tools/labels/selector'
import ConfirmModal from '@/app/components/tools/workflow-tool/confirm-modal'
import Tooltip from '@/app/components/base/tooltip'
import { VarType } from '@/app/components/workflow/types'
import { RiErrorWarningLine } from '@remixicon/react'
type Props = {
isAdd?: boolean
@@ -45,7 +47,29 @@ const WorkflowToolAsModal: FC<Props> = ({
const [name, setName] = useState(payload.name)
const [description, setDescription] = useState(payload.description)
const [parameters, setParameters] = useState<WorkflowToolProviderParameter[]>(payload.parameters)
const handleParameterChange = (key: string, value: string, index: number) => {
const outputParameters = useMemo<WorkflowToolProviderOutputParameter[]>(() => payload.outputParameters, [payload.outputParameters])
const reservedOutputParameters: WorkflowToolProviderOutputParameter[] = [
{
name: 'text',
description: t('workflow.nodes.tool.outputVars.text'),
type: VarType.string,
reserved: true,
},
{
name: 'files',
description: t('workflow.nodes.tool.outputVars.files.title'),
type: VarType.arrayFile,
reserved: true,
},
{
name: 'json',
description: t('workflow.nodes.tool.outputVars.json'),
type: VarType.arrayObject,
reserved: true,
},
]
const handleParameterChange = (key: string, value: any, index: number) => {
const newData = produce(parameters, (draft: WorkflowToolProviderParameter[]) => {
if (key === 'description')
draft[index].description = value
@@ -69,6 +93,10 @@ const WorkflowToolAsModal: FC<Props> = ({
return /^\w+$/.test(name)
}
const isOutputParameterReserved = (name: string) => {
return reservedOutputParameters.find(p => p.name === name)
}
const onConfirm = () => {
let errorMessage = ''
if (!label)
@@ -225,6 +253,51 @@ const WorkflowToolAsModal: FC<Props> = ({
</table>
</div>
</div>
{/* Tool Output */}
<div>
<div className='system-sm-medium py-2 text-text-primary'>{t('tools.createTool.toolOutput.title')}</div>
<div className='w-full overflow-x-auto rounded-lg border border-divider-regular'>
<table className='w-full text-xs font-normal leading-[18px] text-text-secondary'>
<thead className='uppercase text-text-tertiary'>
<tr className='border-b border-divider-regular'>
<th className="w-[156px] p-2 pl-3 font-medium">{t('tools.createTool.name')}</th>
<th className="p-2 pl-3 font-medium">{t('tools.createTool.toolOutput.description')}</th>
</tr>
</thead>
<tbody>
{[...reservedOutputParameters, ...outputParameters].map((item, index) => (
<tr key={index} className='border-b border-divider-regular last:border-0'>
<td className="max-w-[156px] p-2 pl-3">
<div className='text-[13px] leading-[18px]'>
<div title={item.name} className='flex items-center'>
<span className='truncate font-medium text-text-primary'>{item.name}</span>
<span className='shrink-0 pl-1 text-xs leading-[18px] text-[#ec4a0a]'>{item.reserved ? t('tools.createTool.toolOutput.reserved') : ''}</span>
{
!item.reserved && isOutputParameterReserved(item.name) ? (
<Tooltip
popupContent={
<div className='w-[180px]'>
{t('tools.createTool.toolOutput.reservedParameterDuplicateTip')}
</div>
}
>
<RiErrorWarningLine className='h-3 w-3 text-text-warning-secondary' />
</Tooltip>
) : null
}
</div>
<div className='text-text-tertiary'>{item.type}</div>
</div>
</td>
<td className="w-[236px] p-2 pl-3 text-text-tertiary">
<span className='text-[13px] font-normal leading-[18px] text-text-secondary'>{item.description}</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Tags */}
<div>
<div className='system-sm-medium py-2 text-text-primary'>{t('tools.createTool.toolInput.label')}</div>

View File

@@ -39,6 +39,7 @@ import useTheme from '@/hooks/use-theme'
import cn from '@/utils/classnames'
import { useIsChatMode } from '@/app/components/workflow/hooks'
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
import type { EndNodeType } from '@/app/components/workflow/nodes/end/types'
import { useProviderContext } from '@/context/provider-context'
import { Plan } from '@/app/components/billing/type'
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
@@ -61,6 +62,7 @@ const FeaturesTrigger = () => {
const nodes = useNodes()
const hasWorkflowNodes = nodes.length > 0
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const endNode = nodes.find(node => node.data.type === BlockEnum.End)
const startVariables = (startNode as Node<StartNodeType>)?.data?.variables
const edges = useEdges<CommonEdgeType>()
@@ -81,6 +83,7 @@ const FeaturesTrigger = () => {
return data
}, [fileSettings?.image?.enabled, startVariables])
const endVariables = useMemo(() => (endNode as Node<EndNodeType>)?.data?.outputs || [], [endNode])
const { handleCheckBeforePublish } = useChecklistBeforePublish()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
@@ -201,6 +204,7 @@ const FeaturesTrigger = () => {
disabled: nodesReadOnly || !hasWorkflowNodes,
toolPublished,
inputs: variables,
outputs: endVariables,
onRefreshData: handleToolConfigureUpdate,
onPublish,
onToggle: onPublisherToggle,

View File

@@ -121,6 +121,7 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
/>
{outputSchema.map((outputItem) => {
const schemaType = getMatchedSchemaType(outputItem.value, schemaTypeDefinitions)
// TODO empty object type always match `qa_structured` schema type
return (
<div key={outputItem.name}>
{outputItem.value?.type === 'object' ? (