Compare commits

...

7 Commits

Author SHA1 Message Date
非法操作
21f785a2f2
Merge a16a1fffb1 into 5ff02b469f 2024-11-14 21:26:04 +08:00
hejl
a16a1fffb1 Revert "add xAI"
This reverts commit 024579febc.
2024-11-05 11:51:40 +08:00
hejl
024579febc add xAI 2024-11-05 10:22:02 +08:00
hejl
1c252e3127 fix re-run CI 2024-11-02 21:43:04 +08:00
hejl
a7b175ce09 fix re-run CI 2024-11-02 21:42:57 +08:00
hejl
074746cfd2 fix CI 2024-11-02 21:33:29 +08:00
hejl
ad00cc8ca1 support multi select 2024-11-02 14:00:49 +08:00
14 changed files with 267 additions and 94 deletions

View File

@ -173,7 +173,7 @@ class BaseAgentRunner(AppRunner):
}:
continue
enum = []
if parameter.type == ToolParameter.ToolParameterType.SELECT:
if parameter.type in {ToolParameter.ToolParameterType.SELECT, ToolParameter.ToolParameterType.MULTI_SELECT}:
enum = [option.value for option in parameter.options]
message_tool.parameters["properties"][parameter.name] = {
@ -264,7 +264,7 @@ class BaseAgentRunner(AppRunner):
}:
continue
enum = []
if parameter.type == ToolParameter.ToolParameterType.SELECT:
if parameter.type in {ToolParameter.ToolParameterType.SELECT, ToolParameter.ToolParameterType.MULTI_SELECT}:
enum = [option.value for option in parameter.options]
prompt_tool.parameters["properties"][parameter.name] = {

View File

@ -142,12 +142,13 @@ class ToolParameter(BaseModel):
NUMBER = "number"
BOOLEAN = "boolean"
SELECT = "select"
MULTI_SELECT = "multi-select"
SECRET_INPUT = "secret-input"
FILE = "file"
FILES = "files"
# deprecated, should not use.
SYSTEM_FILES = "systme-files"
SYSTEM_FILES = "system-files"
def as_normal_type(self):
if self in {
@ -155,6 +156,8 @@ class ToolParameter(BaseModel):
ToolParameter.ToolParameterType.SELECT,
}:
return "string"
elif self == ToolParameter.ToolParameterType.MULTI_SELECT:
return "array"
return self.value
def cast_value(self, value: Any, /):
@ -198,6 +201,7 @@ class ToolParameter(BaseModel):
ToolParameter.ToolParameterType.SYSTEM_FILES
| ToolParameter.ToolParameterType.FILE
| ToolParameter.ToolParameterType.FILES
| ToolParameter.ToolParameterType.MULTI_SELECT
):
return value
case _:

View File

@ -29,7 +29,7 @@ class CrawlTool(BuiltinTool):
payload["allowExternalLinks"] = tool_parameters.get("allowExternalLinks", False)
payload["webhook"] = tool_parameters.get("webhook")
scrapeOptions["formats"] = get_array_params(tool_parameters, "formats")
scrapeOptions["formats"] = tool_parameters.get("formats")
scrapeOptions["headers"] = get_json_params(tool_parameters, "headers")
scrapeOptions["includeTags"] = get_array_params(tool_parameters, "includeTags")
scrapeOptions["excludeTags"] = get_array_params(tool_parameters, "excludeTags")

View File

@ -123,18 +123,35 @@ parameters:
form: form
############## Scrape Options #######################
- name: formats
type: string
type: multi-select
label:
en_US: Formats
en_US: Output Formats
zh_Hans: 结果的格式
placeholder:
en_US: Use commas to separate multiple tags
zh_Hans: 多个标签时使用半角逗号分隔
options:
- value: markdown
label:
en_US: markdown
- value: html
label:
en_US: html
- value: rawHtml
label:
en_US: rawHtml
- value: links
label:
en_US: links
- value: screenshot
label:
en_US: screenshot
- value: extract
label:
en_US: extract
- value: screenshot@fullPage
label:
en_US: screenshot@fullPage
human_description:
en_US: |
Formats to include in the output. Available options: markdown, html, rawHtml, links, screenshot
zh_Hans: |
输出中应包含的格式。可以填入: markdown, html, rawHtml, links, screenshot
en_US: Formats to include in the output. Multiple selections possible.
zh_Hans: 输出中应包含的格式。可多选。
form: form
- name: headers
type: string

View File

@ -18,7 +18,7 @@ class ScrapeTool(BuiltinTool):
payload = {}
extract = {}
payload["formats"] = get_array_params(tool_parameters, "formats")
payload["formats"] = tool_parameters.get("formats")
payload["onlyMainContent"] = tool_parameters.get("onlyMainContent", True)
payload["includeTags"] = get_array_params(tool_parameters, "includeTags")
payload["excludeTags"] = get_array_params(tool_parameters, "excludeTags")

View File

@ -23,18 +23,35 @@ parameters:
form: llm
############## Payload #######################
- name: formats
type: string
type: multi-select
label:
en_US: Formats
en_US: Output Formats
zh_Hans: 结果的格式
placeholder:
en_US: Use commas to separate multiple tags
zh_Hans: 多个标签时使用半角逗号分隔
options:
- value: markdown
label:
en_US: markdown
- value: html
label:
en_US: html
- value: rawHtml
label:
en_US: rawHtml
- value: links
label:
en_US: links
- value: screenshot
label:
en_US: screenshot
- value: extract
label:
en_US: extract
- value: screenshot@fullPage
label:
en_US: screenshot@fullPage
human_description:
en_US: |
Formats to include in the output. Available options: markdown, html, rawHtml, links, screenshot, extract, screenshot@fullPage
zh_Hans: |
输出中应包含的格式。可以填入: markdown, html, rawHtml, links, screenshot, extract, screenshot@fullPage
en_US: Formats to include in the output. Multiple selections possible.
zh_Hans: 输出中应包含的格式。可多选。
form: form
- name: onlyMainContent
type: boolean

View File

@ -7,7 +7,6 @@ from core.tools.entities.tool_entities import ToolParameter, ToolProviderCredent
from core.tools.entities.values import ToolLabelEnum, default_tool_label_dict
from core.tools.errors import (
ToolNotFoundError,
ToolParameterValidationError,
ToolProviderNotFoundError,
)
from core.tools.provider.tool_provider import ToolProviderController
@ -146,70 +145,6 @@ class BuiltinToolProviderController(ToolProviderController):
"""
return self.identity.tags or []
def validate_parameters(self, tool_id: int, tool_name: str, tool_parameters: dict[str, Any]) -> None:
"""
validate the parameters of the tool and set the default value if needed
:param tool_name: the name of the tool, defined in `get_tools`
:param tool_parameters: the parameters of the tool
"""
tool_parameters_schema = self.get_parameters(tool_name)
tool_parameters_need_to_validate: dict[str, ToolParameter] = {}
for parameter in tool_parameters_schema:
tool_parameters_need_to_validate[parameter.name] = parameter
for parameter in tool_parameters:
if parameter not in tool_parameters_need_to_validate:
raise ToolParameterValidationError(f"parameter {parameter} not found in tool {tool_name}")
# check type
parameter_schema = tool_parameters_need_to_validate[parameter]
if parameter_schema.type == ToolParameter.ToolParameterType.STRING:
if not isinstance(tool_parameters[parameter], str):
raise ToolParameterValidationError(f"parameter {parameter} should be string")
elif parameter_schema.type == ToolParameter.ToolParameterType.NUMBER:
if not isinstance(tool_parameters[parameter], int | float):
raise ToolParameterValidationError(f"parameter {parameter} should be number")
if parameter_schema.min is not None and tool_parameters[parameter] < parameter_schema.min:
raise ToolParameterValidationError(
f"parameter {parameter} should be greater than {parameter_schema.min}"
)
if parameter_schema.max is not None and tool_parameters[parameter] > parameter_schema.max:
raise ToolParameterValidationError(
f"parameter {parameter} should be less than {parameter_schema.max}"
)
elif parameter_schema.type == ToolParameter.ToolParameterType.BOOLEAN:
if not isinstance(tool_parameters[parameter], bool):
raise ToolParameterValidationError(f"parameter {parameter} should be boolean")
elif parameter_schema.type == ToolParameter.ToolParameterType.SELECT:
if not isinstance(tool_parameters[parameter], str):
raise ToolParameterValidationError(f"parameter {parameter} should be string")
options = parameter_schema.options
if not isinstance(options, list):
raise ToolParameterValidationError(f"parameter {parameter} options should be list")
if tool_parameters[parameter] not in [x.value for x in options]:
raise ToolParameterValidationError(f"parameter {parameter} should be one of {options}")
tool_parameters_need_to_validate.pop(parameter)
for parameter in tool_parameters_need_to_validate:
parameter_schema = tool_parameters_need_to_validate[parameter]
if parameter_schema.required:
raise ToolParameterValidationError(f"parameter {parameter} is required")
# the parameter is not set currently, set the default value if needed
if parameter_schema.default is not None:
default_value = parameter_schema.type.cast_value(parameter_schema.default)
tool_parameters[parameter] = default_value
def validate_credentials(self, credentials: dict[str, Any]) -> None:
"""
validate the credentials of the provider

View File

@ -117,6 +117,18 @@ class ToolProviderController(BaseModel, ABC):
if tool_parameters[parameter] not in [x.value for x in options]:
raise ToolParameterValidationError(f"parameter {parameter} should be one of {options}")
elif parameter_schema.type == ToolParameter.ToolParameterType.MULTI_SELECT:
if not isinstance(tool_parameters[parameter], list):
raise ToolParameterValidationError(f"parameter {parameter} should be list")
options = parameter_schema.options
if not isinstance(options, list):
raise ToolParameterValidationError(f"parameter {parameter} options should be list")
for item in tool_parameters[parameter]:
if item not in [x.value for x in options]:
raise ToolParameterValidationError(f"parameter {parameter} should be one of {options}")
tool_parameters_need_to_validate.pop(parameter)
for parameter in tool_parameters_need_to_validate:

View File

@ -220,6 +220,11 @@ class ToolManager:
raise ValueError(
f"tool parameter {parameter_rule.name} value {parameter_value} not in options {options}"
)
elif parameter_rule.type == ToolParameter.ToolParameterType.MULTI_SELECT and parameter_value is not None:
options = [x.value for x in parameter_rule.options]
for value in parameter_value:
if value not in options:
raise ValueError(f"tool parameter {parameter_rule.name} value {value} not in options {options}")
return parameter_rule.type.cast_value(parameter_value)
@ -228,7 +233,7 @@ class ToolManager:
cls, tenant_id: str, app_id: str, agent_tool: AgentToolEntity, invoke_from: InvokeFrom = InvokeFrom.DEBUGGER
) -> Tool:
"""
get the agent tool runtime
get the agent tool runtim
"""
tool_entity = cls.get_tool_runtime(
provider_type=agent_tool.provider_type,

View File

@ -5,6 +5,7 @@ def test_get_parameter_type():
assert ToolParameter.ToolParameterType.STRING.as_normal_type() == "string"
assert ToolParameter.ToolParameterType.SELECT.as_normal_type() == "string"
assert ToolParameter.ToolParameterType.SECRET_INPUT.as_normal_type() == "string"
assert ToolParameter.ToolParameterType.MULTI_SELECT.as_normal_type() == "array"
assert ToolParameter.ToolParameterType.BOOLEAN.as_normal_type() == "boolean"
assert ToolParameter.ToolParameterType.NUMBER.as_normal_type() == "number"
assert ToolParameter.ToolParameterType.FILE.as_normal_type() == "file"
@ -30,6 +31,10 @@ def test_cast_parameter_by_type():
assert ToolParameter.ToolParameterType.SELECT.cast_value(1.0) == "1.0"
assert ToolParameter.ToolParameterType.SELECT.cast_value(None) == ""
# multi select
assert ToolParameter.ToolParameterType.MULTI_SELECT.cast_value(["test", "test2"]) == ["test", "test2"]
assert ToolParameter.ToolParameterType.MULTI_SELECT.cast_value([2, 1, 1.0]) == [2, 1, 1.0]
# boolean
true_values = [True, "True", "true", "1", "YES", "Yes", "yes", "y", "something"]
for value in true_values:

View File

@ -29,7 +29,7 @@ export type Item = {
export type ISelectProps = {
className?: string
wrapperClassName?: string
renderTrigger?: (value: Item | null) => JSX.Element | null
renderTrigger?: (value: Item | Item[] | null) => JSX.Element | null
items?: Item[]
defaultValue?: number | string
disabled?: boolean
@ -378,5 +378,127 @@ const PortalSelect: FC<PortalSelectProps> = ({
</PortalToFollowElem>
)
}
export { SimpleSelect, PortalSelect }
export type MultiSelectProps = Omit<ISelectProps, 'onSelect'> & {
onSelect: (values: Item[]) => void
selectedValues?: (number | string)[]
}
const MultiSelect: FC<MultiSelectProps> = ({
className,
wrapperClassName = '',
renderTrigger,
items = defaultItems,
selectedValues = [],
disabled = false,
onSelect,
placeholder,
optionWrapClassName,
optionClassName,
hideChecked,
renderOption,
}) => {
const { t } = useTranslation()
const localPlaceholder = placeholder || t('common.placeholder.select')
const [selectedItems, setSelectedItems] = useState<Item[]>([])
useEffect(() => {
const defaultSelected = items.filter((item: Item) => selectedValues.includes(item.value))
setSelectedItems(defaultSelected)
}, [selectedValues, items])
const handleSelect = (newItems: Item[]) => {
if (!disabled) {
setSelectedItems(newItems)
onSelect(newItems)
}
}
const removeItem = (item: Item) => {
const newSelectedItems = selectedItems.filter(i => i.value !== item.value)
setSelectedItems(newSelectedItems)
onSelect(newSelectedItems)
}
return (
<Listbox
value={selectedItems}
onChange={handleSelect}
multiple
>
<div className={classNames('group/multi-select relative', wrapperClassName)}>
{renderTrigger && <Listbox.Button className='w-full'>{renderTrigger(selectedItems)}</Listbox.Button>}
{!renderTrigger && (
<Listbox.Button className={classNames(`flex flex-wrap items-center w-full min-h-[36px] rounded-lg border-0 bg-gray-100 pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover/multi-select:bg-state-base-hover-alt ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}>
{selectedItems.length > 0
? (selectedItems.map(item => (
<span key={item.value} className="inline-flex items-center m-1 px-2 py-1 rounded bg-gray-200">
{item.name}
<XMarkIcon
className="ml-1 h-4 w-4 text-gray-400 cursor-pointer"
onClick={(e) => {
e.stopPropagation()
removeItem(item)
}}
/>
</span>
))
)
: (
<span className="text-gray-400">{localPlaceholder}</span>
)}
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronDownIcon
className="h-4 w-4 text-text-quaternary group-hover/multi-select:text-text-secondary"
aria-hidden="true"
/>
</span>
</Listbox.Button>
)}
{!disabled && (
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className={classNames('absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm', optionWrapClassName)}>
{items.map((item: Item) => (
<Listbox.Option
key={item.value}
className={({ active }) =>
classNames(
`relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700 ${active ? 'bg-gray-100' : ''}`,
optionClassName,
)
}
value={item}
>
{({ selected }) => (
<>
{renderOption
? renderOption({ item, selected })
: (
<>
<span className={classNames('block', selected && 'font-normal')}>{item.name}</span>
{selected && !hideChecked && (
<span className="absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700">
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
)}
</>
)}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
)}
</div>
</Listbox>
)
}
export { SimpleSelect, PortalSelect, MultiSelect }
export default React.memo(Select)

View File

@ -11,6 +11,7 @@ export enum FormTypeEnum {
textNumber = 'number-input',
secretInput = 'secret-input',
select = 'select',
multiSelect = 'multi-select',
radio = 'radio',
boolean = 'boolean',
files = 'files',

View File

@ -14,7 +14,7 @@ import { FormTypeEnum } from '../declarations'
import { useLanguage } from '../hooks'
import Input from './Input'
import cn from '@/utils/classnames'
import { SimpleSelect } from '@/app/components/base/select'
import { MultiSelect, SimpleSelect } from '@/app/components/base/select'
import Tooltip from '@/app/components/base/tooltip'
import Radio from '@/app/components/base/radio'
type FormProps = {
@ -53,7 +53,7 @@ const Form: FC<FormProps> = ({
const language = useLanguage()
const [changeKey, setChangeKey] = useState('')
const handleFormChange = (key: string, val: string | boolean) => {
const handleFormChange = (key: string, val: string | boolean | any[]) => {
if (isEditMode && (key === '__model_type' || key === '__model_name'))
return
@ -220,6 +220,44 @@ const Form: FC<FormProps> = ({
)
}
if (formSchema.type === 'multi-select') {
const {
options,
variable,
label,
show_on,
required,
placeholder,
} = formSchema as CredentialFormSchemaSelect
if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
return null
const handleMultiSelect = (selectedItems: any[]) => {
handleFormChange(variable, selectedItems.map(item => item.value))
}
return (
<div key={variable} className={cn(itemClassName, 'py-3')}>
<div className={cn(fieldLabelClassName, 'flex items-center py-2 text-sm text-gray-900')}>
{label[language] || label.en_US}
{required && <span className='ml-1 text-red-500'>*</span>}
{tooltipContent}
</div>
<MultiSelect
className={cn(inputClassName)}
disabled={readonly}
selectedValues={value[variable] || []}
items={options.map(item => ({ value: item.value, name: item.label[language] || item.label.en_US }))}
onSelect={handleMultiSelect}
placeholder={placeholder?.[language] || placeholder?.en_US}
/>
{fieldMoreInfo?.(formSchema)}
{validating && changeKey === variable && <ValidatingTip />}
</div>
)
}
if (formSchema.type === 'boolean') {
const {
variable,

View File

@ -50,6 +50,8 @@ const InputVarList: FC<Props> = ({
return 'Files'
else if (type === FormTypeEnum.select)
return 'Options'
else if (type === FormTypeEnum.multiSelect)
return 'Array'
else
return 'String'
}
@ -139,9 +141,10 @@ const InputVarList: FC<Props> = ({
const varInput = value[variable]
const isNumber = type === FormTypeEnum.textNumber
const isSelect = type === FormTypeEnum.select
const isMultiSelect = type === FormTypeEnum.multiSelect
const isFile = type === FormTypeEnum.file
const isFileArray = type === FormTypeEnum.files
const isString = !isNumber && !isSelect && !isFile && !isFileArray
const isString = !isNumber && !isSelect && !isMultiSelect && !isFile && !isFileArray
return (
<div key={variable} className='space-y-1'>
@ -177,6 +180,20 @@ const InputVarList: FC<Props> = ({
schema={schema}
/>
)}
{
isMultiSelect && (
<VarReferencePicker
readonly={readOnly}
isShowNodeName
nodeId={nodeId}
value={varInput?.value || []}
onChange={handleNotMixedTypeChange(variable)}
onOpen={handleOpen(index)}
defaultVarKindType={VarKindType.variable}
filterVar={(varPayload: Var) => varPayload.type === VarType.arrayString || varPayload.type === VarType.arrayNumber}
/>
)
}
{isFile && (
<VarReferencePicker
readonly={readOnly}