From cea6522122e63b51aedbabe0e000f1efc72faa8b Mon Sep 17 00:00:00 2001 From: Yeuoly <45712896+Yeuoly@users.noreply.github.com> Date: Thu, 26 Jun 2025 17:44:14 +0800 Subject: [PATCH] feat: add DYNAMIC_SELECT parameter type for dynamic options in parameter entities (#21425) --- api/controllers/console/workspace/plugin.py | 39 ++++++++++ api/core/entities/parameter_entities.py | 5 ++ api/core/plugin/entities/parameters.py | 1 + api/core/plugin/entities/plugin_daemon.py | 7 +- api/core/plugin/impl/dynamic_select.py | 45 +++++++++++ api/core/tools/entities/tool_entities.py | 1 + api/core/tools/utils/configuration.py | 1 + .../plugin/plugin_parameter_service.py | 74 +++++++++++++++++++ web/app/components/base/select/index.tsx | 26 +++++-- .../model-provider-page/declarations.ts | 2 + .../components/variable/constant-field.tsx | 8 +- .../variable/var-reference-picker.tsx | 63 +++++++++++++++- .../nodes/tool/components/input-var-list.tsx | 17 ++++- .../components/workflow/nodes/tool/panel.tsx | 3 + web/service/use-plugins.ts | 17 ++++- 15 files changed, 293 insertions(+), 16 deletions(-) create mode 100644 api/core/plugin/impl/dynamic_select.py create mode 100644 api/services/plugin/plugin_parameter_service.py diff --git a/api/controllers/console/workspace/plugin.py b/api/controllers/console/workspace/plugin.py index 9bddbb4b4b..c0a4734828 100644 --- a/api/controllers/console/workspace/plugin.py +++ b/api/controllers/console/workspace/plugin.py @@ -13,6 +13,7 @@ from core.model_runtime.utils.encoders import jsonable_encoder from core.plugin.impl.exc import PluginDaemonClientSideError from libs.login import login_required from models.account import TenantPluginPermission +from services.plugin.plugin_parameter_service import PluginParameterService from services.plugin.plugin_permission_service import PluginPermissionService from services.plugin.plugin_service import PluginService @@ -497,6 +498,42 @@ class PluginFetchPermissionApi(Resource): ) +class PluginFetchDynamicSelectOptionsApi(Resource): + @setup_required + @login_required + @account_initialization_required + def get(self): + # check if the user is admin or owner + if not current_user.is_admin_or_owner: + raise Forbidden() + + tenant_id = current_user.current_tenant_id + user_id = current_user.id + + parser = reqparse.RequestParser() + parser.add_argument("plugin_id", type=str, required=True, location="args") + parser.add_argument("provider", type=str, required=True, location="args") + parser.add_argument("action", type=str, required=True, location="args") + parser.add_argument("parameter", type=str, required=True, location="args") + parser.add_argument("provider_type", type=str, required=True, location="args") + args = parser.parse_args() + + try: + options = PluginParameterService.get_dynamic_select_options( + tenant_id, + user_id, + args["plugin_id"], + args["provider"], + args["action"], + args["parameter"], + args["provider_type"], + ) + except PluginDaemonClientSideError as e: + raise ValueError(e) + + return jsonable_encoder({"options": options}) + + api.add_resource(PluginDebuggingKeyApi, "/workspaces/current/plugin/debugging-key") api.add_resource(PluginListApi, "/workspaces/current/plugin/list") api.add_resource(PluginListLatestVersionsApi, "/workspaces/current/plugin/list/latest-versions") @@ -521,3 +558,5 @@ api.add_resource(PluginFetchMarketplacePkgApi, "/workspaces/current/plugin/marke api.add_resource(PluginChangePermissionApi, "/workspaces/current/plugin/permission/change") api.add_resource(PluginFetchPermissionApi, "/workspaces/current/plugin/permission/fetch") + +api.add_resource(PluginFetchDynamicSelectOptionsApi, "/workspaces/current/plugin/parameters/dynamic-options") diff --git a/api/core/entities/parameter_entities.py b/api/core/entities/parameter_entities.py index 36800bc263..b071bfa5b1 100644 --- a/api/core/entities/parameter_entities.py +++ b/api/core/entities/parameter_entities.py @@ -15,6 +15,11 @@ class CommonParameterType(StrEnum): MODEL_SELECTOR = "model-selector" TOOLS_SELECTOR = "array[tools]" + # Dynamic select parameter + # Once you are not sure about the available options until authorization is done + # eg: Select a Slack channel from a Slack workspace + DYNAMIC_SELECT = "dynamic-select" + # TOOL_SELECTOR = "tool-selector" diff --git a/api/core/plugin/entities/parameters.py b/api/core/plugin/entities/parameters.py index 895dd0d0fc..a011801006 100644 --- a/api/core/plugin/entities/parameters.py +++ b/api/core/plugin/entities/parameters.py @@ -35,6 +35,7 @@ class PluginParameterType(enum.StrEnum): APP_SELECTOR = CommonParameterType.APP_SELECTOR.value MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR.value TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR.value + DYNAMIC_SELECT = CommonParameterType.DYNAMIC_SELECT.value # deprecated, should not use. SYSTEM_FILES = CommonParameterType.SYSTEM_FILES.value diff --git a/api/core/plugin/entities/plugin_daemon.py b/api/core/plugin/entities/plugin_daemon.py index e0d2857e97..592b42c0da 100644 --- a/api/core/plugin/entities/plugin_daemon.py +++ b/api/core/plugin/entities/plugin_daemon.py @@ -1,4 +1,4 @@ -from collections.abc import Mapping +from collections.abc import Mapping, Sequence from datetime import datetime from enum import StrEnum from typing import Any, Generic, Optional, TypeVar @@ -9,6 +9,7 @@ from core.agent.plugin_entities import AgentProviderEntityWithPlugin from core.model_runtime.entities.model_entities import AIModelEntity from core.model_runtime.entities.provider_entities import ProviderEntity from core.plugin.entities.base import BasePluginEntity +from core.plugin.entities.parameters import PluginParameterOption from core.plugin.entities.plugin import PluginDeclaration, PluginEntity from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolProviderEntityWithPlugin @@ -186,3 +187,7 @@ class PluginOAuthCredentialsResponse(BaseModel): class PluginListResponse(BaseModel): list: list[PluginEntity] total: int + + +class PluginDynamicSelectOptionsResponse(BaseModel): + options: Sequence[PluginParameterOption] = Field(description="The options of the dynamic select.") diff --git a/api/core/plugin/impl/dynamic_select.py b/api/core/plugin/impl/dynamic_select.py new file mode 100644 index 0000000000..f4fb051ee1 --- /dev/null +++ b/api/core/plugin/impl/dynamic_select.py @@ -0,0 +1,45 @@ +from collections.abc import Mapping +from typing import Any + +from core.plugin.entities.plugin import GenericProviderID +from core.plugin.entities.plugin_daemon import PluginDynamicSelectOptionsResponse +from core.plugin.impl.base import BasePluginClient + + +class DynamicSelectClient(BasePluginClient): + def fetch_dynamic_select_options( + self, + tenant_id: str, + user_id: str, + plugin_id: str, + provider: str, + action: str, + credentials: Mapping[str, Any], + parameter: str, + ) -> PluginDynamicSelectOptionsResponse: + """ + Fetch dynamic select options for a plugin parameter. + """ + response = self._request_with_plugin_daemon_response_stream( + "POST", + f"plugin/{tenant_id}/dispatch/dynamic_select/fetch_parameter_options", + PluginDynamicSelectOptionsResponse, + data={ + "user_id": user_id, + "data": { + "provider": GenericProviderID(provider).provider_name, + "credentials": credentials, + "provider_action": action, + "parameter": parameter, + }, + }, + headers={ + "X-Plugin-ID": plugin_id, + "Content-Type": "application/json", + }, + ) + + for options in response: + return options + + raise ValueError("Plugin service returned no options") diff --git a/api/core/tools/entities/tool_entities.py b/api/core/tools/entities/tool_entities.py index 03047c0545..d2c28076ae 100644 --- a/api/core/tools/entities/tool_entities.py +++ b/api/core/tools/entities/tool_entities.py @@ -240,6 +240,7 @@ class ToolParameter(PluginParameter): FILES = PluginParameterType.FILES.value APP_SELECTOR = PluginParameterType.APP_SELECTOR.value MODEL_SELECTOR = PluginParameterType.MODEL_SELECTOR.value + DYNAMIC_SELECT = PluginParameterType.DYNAMIC_SELECT.value # deprecated, should not use. SYSTEM_FILES = PluginParameterType.SYSTEM_FILES.value diff --git a/api/core/tools/utils/configuration.py b/api/core/tools/utils/configuration.py index 6a5fba65bd..1f23e90351 100644 --- a/api/core/tools/utils/configuration.py +++ b/api/core/tools/utils/configuration.py @@ -86,6 +86,7 @@ class ProviderConfigEncrypter(BaseModel): cached_credentials = cache.get() if cached_credentials: return cached_credentials + data = self._deep_copy(data) # get fields need to be decrypted fields = dict[str, BasicProviderConfig]() diff --git a/api/services/plugin/plugin_parameter_service.py b/api/services/plugin/plugin_parameter_service.py new file mode 100644 index 0000000000..393213c0e2 --- /dev/null +++ b/api/services/plugin/plugin_parameter_service.py @@ -0,0 +1,74 @@ +from collections.abc import Mapping, Sequence +from typing import Any, Literal + +from sqlalchemy.orm import Session + +from core.plugin.entities.parameters import PluginParameterOption +from core.plugin.impl.dynamic_select import DynamicSelectClient +from core.tools.tool_manager import ToolManager +from core.tools.utils.configuration import ProviderConfigEncrypter +from extensions.ext_database import db +from models.tools import BuiltinToolProvider + + +class PluginParameterService: + @staticmethod + def get_dynamic_select_options( + tenant_id: str, + user_id: str, + plugin_id: str, + provider: str, + action: str, + parameter: str, + provider_type: Literal["tool"], + ) -> Sequence[PluginParameterOption]: + """ + Get dynamic select options for a plugin parameter. + + Args: + tenant_id: The tenant ID. + plugin_id: The plugin ID. + provider: The provider name. + action: The action name. + parameter: The parameter name. + """ + credentials: Mapping[str, Any] = {} + + match provider_type: + case "tool": + provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) + # init tool configuration + tool_configuration = ProviderConfigEncrypter( + tenant_id=tenant_id, + config=[x.to_basic_provider_config() for x in provider_controller.get_credentials_schema()], + provider_type=provider_controller.provider_type.value, + provider_identity=provider_controller.entity.identity.name, + ) + + # check if credentials are required + if not provider_controller.need_credentials: + credentials = {} + else: + # fetch credentials from db + with Session(db.engine) as session: + db_record = ( + session.query(BuiltinToolProvider) + .filter( + BuiltinToolProvider.tenant_id == tenant_id, + BuiltinToolProvider.provider == provider, + ) + .first() + ) + + if db_record is None: + raise ValueError(f"Builtin provider {provider} not found when fetching credentials") + + credentials = tool_configuration.decrypt(db_record.credentials) + case _: + raise ValueError(f"Invalid provider type: {provider_type}") + + return ( + DynamicSelectClient() + .fetch_dynamic_select_options(tenant_id, user_id, plugin_id, provider, action, credentials, parameter) + .options + ) diff --git a/web/app/components/base/select/index.tsx b/web/app/components/base/select/index.tsx index fa8730f698..44cb0522f6 100644 --- a/web/app/components/base/select/index.tsx +++ b/web/app/components/base/select/index.tsx @@ -1,10 +1,10 @@ 'use client' import type { FC } from 'react' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { Combobox, ComboboxButton, ComboboxInput, ComboboxOption, ComboboxOptions, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react' import { ChevronDownIcon, ChevronUpIcon, XMarkIcon } from '@heroicons/react/20/solid' import Badge from '../badge/index' -import { RiCheckLine } from '@remixicon/react' +import { RiCheckLine, RiLoader4Line } from '@remixicon/react' import { useTranslation } from 'react-i18next' import classNames from '@/utils/classnames' import { @@ -51,6 +51,8 @@ export type ISelectProps = { item: Item selected: boolean }) => React.ReactNode + isLoading?: boolean + onOpenChange?: (open: boolean) => void } const Select: FC = ({ className, @@ -178,17 +180,20 @@ const SimpleSelect: FC = ({ defaultValue = 1, disabled = false, onSelect, + onOpenChange, placeholder, optionWrapClassName, optionClassName, hideChecked, notClearable, renderOption, + isLoading = false, }) => { const { t } = useTranslation() const localPlaceholder = placeholder || t('common.placeholder.select') const [selectedItem, setSelectedItem] = useState(null) + useEffect(() => { let defaultSelect = null const existed = items.find((item: Item) => item.value === defaultValue) @@ -199,8 +204,10 @@ const SimpleSelect: FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultValue]) + const listboxRef = useRef(null) + return ( - { if (!disabled) { @@ -212,10 +219,17 @@ const SimpleSelect: FC = ({
{renderTrigger && {renderTrigger(selectedItem)}} {!renderTrigger && ( - + { + // get data-open, use setTimeout to ensure the attribute is set + setTimeout(() => { + if (listboxRef.current) + onOpenChange?.(listboxRef.current.getAttribute('data-open') !== null) + }) + }} className={classNames(`flex items-center w-full h-full rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-state-base-hover-alt group-hover/simple-select:bg-state-base-hover-alt ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}> {selectedItem?.name ?? localPlaceholder} - {(selectedItem && !notClearable) + {isLoading ? + : (selectedItem && !notClearable) ? ( { @@ -237,7 +251,7 @@ const SimpleSelect: FC = ({ )} - {!disabled && ( + {(!disabled) && ( {items.map((item: Item) => ( void + onOpenChange?: (open: boolean) => void + isLoading?: boolean } const DEFAULT_SCHEMA = {} as CredentialFormSchema @@ -22,6 +24,8 @@ const ConstantField: FC = ({ readonly, value, onChange, + onOpenChange, + isLoading, }) => { const language = useLanguage() const placeholder = (schema as CredentialFormSchemaSelect).placeholder @@ -36,7 +40,7 @@ const ConstantField: FC = ({ return ( <> - {schema.type === FormTypeEnum.select && ( + {(schema.type === FormTypeEnum.select || schema.type === FormTypeEnum.dynamicSelect) && ( = ({ items={(schema as CredentialFormSchemaSelect).options.map(option => ({ value: option.value, name: option.label[language] || option.label.en_US }))} onSelect={item => handleSelectChange(item.value)} placeholder={placeholder?.[language] || placeholder?.en_US} + onOpenChange={onOpenChange} + isLoading={isLoading} /> )} {schema.type === FormTypeEnum.textNumber && ( diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx index e9825cd44a..2c89e722cd 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx @@ -6,6 +6,7 @@ import { RiArrowDownSLine, RiCloseLine, RiErrorWarningFill, + RiLoader4Line, RiMoreLine, } from '@remixicon/react' import produce from 'immer' @@ -16,8 +17,9 @@ import VarReferencePopup from './var-reference-popup' import { getNodeInfoById, isConversationVar, isENV, isSystemVar, varTypeToStructType } from './utils' import ConstantField from './constant-field' import cn from '@/utils/classnames' -import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' -import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' +import type { Node, NodeOutPutVar, ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types' +import type { CredentialFormSchemaSelect } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { type CredentialFormSchema, type FormOption, FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { BlockEnum } from '@/app/components/workflow/types' import { VarBlockIcon } from '@/app/components/workflow/block-icon' import { Line3 } from '@/app/components/base/icons/src/public/common' @@ -40,6 +42,8 @@ import Tooltip from '@/app/components/base/tooltip' import { isExceptionVariable } from '@/app/components/workflow/utils' import VarFullPathPanel from './var-full-path-panel' import { noop } from 'lodash-es' +import { useFetchDynamicOptions } from '@/service/use-plugins' +import type { Tool } from '@/app/components/tools/types' const TRIGGER_DEFAULT_WIDTH = 227 @@ -68,6 +72,8 @@ type Props = { minWidth?: number popupFor?: 'assigned' | 'toAssigned' zIndex?: number + currentTool?: Tool + currentProvider?: ToolWithProvider } const DEFAULT_VALUE_SELECTOR: Props['value'] = [] @@ -97,6 +103,8 @@ const VarReferencePicker: FC = ({ minWidth, popupFor, zIndex, + currentTool, + currentProvider, }) => { const { t } = useTranslation() const store = useStoreApi() @@ -316,6 +324,42 @@ const VarReferencePicker: FC = ({ return null }, [isValidVar, isShowAPart, hasValue, t, outputVarNode?.title, outputVarNode?.type, value, type]) + + const [dynamicOptions, setDynamicOptions] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions( + currentProvider?.plugin_id || '', currentProvider?.name || '', currentTool?.name || '', (schema as CredentialFormSchemaSelect)?.variable || '', + 'tool', + ) + const handleFetchDynamicOptions = async () => { + if (schema?.type !== FormTypeEnum.dynamicSelect || !currentTool || !currentProvider) + return + setIsLoading(true) + try { + const data = await fetchDynamicOptions() + setDynamicOptions(data?.options || []) + } + finally { + setIsLoading(false) + } + } + useEffect(() => { + handleFetchDynamicOptions() + }, [currentTool, currentProvider, schema]) + + const schemaWithDynamicSelect = useMemo(() => { + if (schema?.type !== FormTypeEnum.dynamicSelect) + return schema + // rewrite schema.options with dynamicOptions + if (dynamicOptions) { + return { + ...schema, + options: dynamicOptions, + } + } + return schema + }, [dynamicOptions]) + return (
= ({ void)} - schema={schema as CredentialFormSchema} + schema={schemaWithDynamicSelect as CredentialFormSchema} readonly={readonly} + isLoading={isLoading} /> ) : ( @@ -412,6 +457,7 @@ const VarReferencePicker: FC = ({ )}
{!hasValue && } + {isLoading && } {isEnv && } {isChatVar && }
= ({ {!isValidVar && } ) - :
{placeholder ?? t('workflow.common.setVarValuePlaceholder')}
} + :
+ {isLoading ? ( +
+ + {placeholder ?? t('workflow.common.setVarValuePlaceholder')} +
+ ) : ( + placeholder ?? t('workflow.common.setVarValuePlaceholder') + )} +
}
diff --git a/web/app/components/workflow/nodes/tool/components/input-var-list.tsx b/web/app/components/workflow/nodes/tool/components/input-var-list.tsx index 1a609c58f5..244e54a5f2 100644 --- a/web/app/components/workflow/nodes/tool/components/input-var-list.tsx +++ b/web/app/components/workflow/nodes/tool/components/input-var-list.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next' import type { ToolVarInputs } from '../types' import { VarType as VarKindType } from '../types' import cn from '@/utils/classnames' -import type { ValueSelector, Var } from '@/app/components/workflow/types' +import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types' import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' @@ -17,6 +17,7 @@ import { VarType } from '@/app/components/workflow/types' import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector' import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector' import { noop } from 'lodash-es' +import type { Tool } from '@/app/components/tools/types' type Props = { readOnly: boolean @@ -27,6 +28,8 @@ type Props = { onOpen?: (index: number) => void isSupportConstantValue?: boolean filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean + currentTool?: Tool + currentProvider?: ToolWithProvider } const InputVarList: FC = ({ @@ -38,6 +41,8 @@ const InputVarList: FC = ({ onOpen = noop, isSupportConstantValue, filterVar, + currentTool, + currentProvider, }) => { const language = useLanguage() const { t } = useTranslation() @@ -58,6 +63,8 @@ const InputVarList: FC = ({ return 'ModelSelector' else if (type === FormTypeEnum.toolSelector) return 'ToolSelector' + else if (type === FormTypeEnum.dynamicSelect || type === FormTypeEnum.select) + return 'Select' else return 'String' } @@ -149,6 +156,7 @@ const InputVarList: FC = ({ const handleOpen = useCallback((index: number) => { return () => onOpen(index) }, [onOpen]) + return (
{ @@ -163,7 +171,8 @@ const InputVarList: FC = ({ } = schema const varInput = value[variable] const isNumber = type === FormTypeEnum.textNumber - const isSelect = type === FormTypeEnum.select + const isDynamicSelect = type === FormTypeEnum.dynamicSelect + const isSelect = type === FormTypeEnum.select || type === FormTypeEnum.dynamicSelect const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files const isAppSelector = type === FormTypeEnum.appSelector const isModelSelector = type === FormTypeEnum.modelSelector @@ -198,11 +207,13 @@ const InputVarList: FC = ({ value={varInput?.type === VarKindType.constant ? (varInput?.value ?? '') : (varInput?.value ?? [])} onChange={handleNotMixedTypeChange(variable)} onOpen={handleOpen(index)} - defaultVarKindType={varInput?.type || (isNumber ? VarKindType.constant : VarKindType.variable)} + defaultVarKindType={varInput?.type || ((isNumber || isDynamicSelect) ? VarKindType.constant : VarKindType.variable)} isSupportConstantValue={isSupportConstantValue} filterVar={isNumber ? filterVar : undefined} availableVars={isSelect ? availableVars : undefined} schema={schema} + currentTool={currentTool} + currentProvider={currentProvider} /> )} {isFile && ( diff --git a/web/app/components/workflow/nodes/tool/panel.tsx b/web/app/components/workflow/nodes/tool/panel.tsx index 5dd5242c71..038159870e 100644 --- a/web/app/components/workflow/nodes/tool/panel.tsx +++ b/web/app/components/workflow/nodes/tool/panel.tsx @@ -42,6 +42,7 @@ const Panel: FC> = ({ isLoading, outputSchema, hasObjectOutput, + currTool, } = useConfig(id, data) if (isLoading) { @@ -80,6 +81,8 @@ const Panel: FC> = ({ filterVar={filterVar} isSupportConstantValue onOpen={handleOnVarOpen} + currentProvider={currCollection} + currentTool={currTool} /> )} diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index ecfdbcf993..ff092bb037 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect } from 'react' import type { + FormOption, ModelProvider, } from '@/app/components/header/account-setting/model-provider-page/declarations' import { fetchModelProviderModelList } from '@/service/common' @@ -477,7 +478,7 @@ export const usePluginTaskList = (category?: PluginType) => { refreshPluginList(category ? { category } as any : undefined, !category) } } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isRefetching]) const handleRefetch = useCallback(() => { @@ -571,3 +572,17 @@ export const usePluginInfo = (providerName?: string) => { enabled: !!providerName, }) } + +export const useFetchDynamicOptions = (plugin_id: string, provider: string, action: string, parameter: string, provider_type: 'tool') => { + return useMutation({ + mutationFn: () => get<{ options: FormOption[] }>('/workspaces/current/plugin/parameters/dynamic-options', { + params: { + plugin_id, + provider, + action, + parameter, + provider_type, + }, + }), + }) +}