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:
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' ? (
|
||||
|
||||
Reference in New Issue
Block a user