diff --git a/api/core/workflow/nodes/agent_v2/runtime_request_builder.py b/api/core/workflow/nodes/agent_v2/runtime_request_builder.py index 0dcdcb35b36532..b044e2b4cacc39 100644 --- a/api/core/workflow/nodes/agent_v2/runtime_request_builder.py +++ b/api/core/workflow/nodes/agent_v2/runtime_request_builder.py @@ -53,6 +53,7 @@ from graphon.file import File, FileTransferMethod from graphon.variables.segments import Segment from graphon.variables.template_resolution import convert_template +from libs.workflow_template_values import resolve_template_value from models.agent import Agent, AgentConfigSnapshot, WorkflowAgentNodeBinding from models.agent_config_entities import ( AgentKnowledgeMetadataFilteringConfig, @@ -216,7 +217,7 @@ def build(self, context: WorkflowAgentRuntimeBuildContext) -> WorkflowAgentRunti runtime_config_skills=runtime_config_skills, ) soul_prompt = expand_prompt_mentions(agent_soul.prompt.system_prompt, soul_prompt_resolver).strip() - knowledge_config = build_knowledge_layer_config(agent_soul) + knowledge_config = build_knowledge_layer_config(agent_soul, variable_pool=context.variable_pool) context_window_tokens = resolve_model_context_window( run_context=context.dify_context, provider_name=agent_soul.model.model_provider, @@ -743,7 +744,9 @@ def build_shell_layer_config(agent_soul: AgentSoulConfig) -> DifyShellLayerConfi ) -def build_knowledge_layer_config(agent_soul: AgentSoulConfig) -> DifyKnowledgeBaseLayerConfig | None: +def build_knowledge_layer_config( + agent_soul: AgentSoulConfig, *, variable_pool: VariablePoolReader | None = None +) -> DifyKnowledgeBaseLayerConfig | None: """Map Agent Soul knowledge sets into one Dify knowledge-base layer. Agent Soul DTO validation owns malformed set rejection. Runtime mapping is @@ -751,6 +754,8 @@ def build_knowledge_layer_config(agent_soul: AgentSoulConfig) -> DifyKnowledgeBa policy, dataset refs, retrieval controls, and metadata-filtering controls. ``score_threshold=None`` means disabled threshold filtering and maps to the inner retrieval request's ``0.0`` default through the Agent backend DTO. + Workflow callers supply the current run's variable pool; ``None`` preserves + literal filters for standalone Agent requests, which have no workflow scope. """ if not agent_soul.knowledge.sets: return None @@ -774,7 +779,9 @@ def build_knowledge_layer_config(agent_soul: AgentSoulConfig) -> DifyKnowledgeBa value=knowledge_set.query.value, ), retrieval=_knowledge_retrieval_config(knowledge_set.retrieval), - metadata_filtering=_knowledge_metadata_filtering_config(knowledge_set.metadata_filtering), + metadata_filtering=_knowledge_metadata_filtering_config( + knowledge_set.metadata_filtering, variable_pool=variable_pool + ), ) for knowledge_set in agent_soul.knowledge.sets ], @@ -821,17 +828,33 @@ def _knowledge_retrieval_config(retrieval: AgentKnowledgeRetrievalConfig) -> Dif def _knowledge_metadata_filtering_config( metadata_filtering: AgentKnowledgeMetadataFilteringConfig, + *, + variable_pool: VariablePoolReader | None, ) -> DifyKnowledgeMetadataFilteringConfig: + conditions = metadata_filtering.conditions + if conditions is not None and metadata_filtering.mode == "manual" and variable_pool is not None: + try: + conditions = conditions.model_copy( + update={ + "conditions": [ + condition.model_copy(update={"value": resolve_template_value(condition.value, variable_pool)}) + for condition in conditions.conditions + ] + } + ) + except ValueError as error: + raise WorkflowAgentRuntimeRequestBuildError( + "agent_metadata_filter_variable_not_found", str(error) + ) from error + return DifyKnowledgeMetadataFilteringConfig( mode=metadata_filtering.mode, model_config=_knowledge_model_config(metadata_filtering.metadata_model_config), conditions=cast( Any, - metadata_filtering.conditions.model_dump( - mode="json", exclude={"conditions": {"__all__": {"id", "metadata_id"}}} - ), + conditions.model_dump(mode="json", exclude={"conditions": {"__all__": {"id", "metadata_id"}}}), ) - if metadata_filtering.conditions is not None + if conditions is not None else None, ) diff --git a/api/core/workflow/workflow_entry.py b/api/core/workflow/workflow_entry.py index c0f532a6750c21..ed71946728f324 100644 --- a/api/core/workflow/workflow_entry.py +++ b/api/core/workflow/workflow_entry.py @@ -22,6 +22,8 @@ is_start_node_type, resolve_workflow_node_class, ) +from core.workflow.nodes.agent_v2.binding_resolver import WorkflowAgentBindingResolver +from core.workflow.nodes.agent_v2.discriminator import is_dify_agent_node_data from core.workflow.nodes.human_input.boundary import HumanInputFormEventFilter from core.workflow.system_variables import ( default_system_variables, @@ -47,6 +49,8 @@ from graphon.nodes.container_effects import ContainerAwaitRequest from graphon.runtime import GraphRuntimeState, ReadOnlyGraphRuntimeStateWrapper, VariablePool from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader, load_into_variable_pool +from libs.workflow_template_values import template_value_selectors +from models.agent_config_entities import AgentSoulConfig from models.workflow import Workflow logger = logging.getLogger(__name__) @@ -271,6 +275,28 @@ def single_step_run( ) except NotImplementedError: variable_mapping = {} + if is_dify_agent_node_data(node_config_data): + # Agent Soul lives in its owned snapshot, outside graph node data. + # Single-step execution must also load references used only by its + # retrieval filters, which are absent from the task prompt. + bundle = WorkflowAgentBindingResolver().resolve( + tenant_id=workflow.tenant_id, + app_id=workflow.app_id, + workflow_id=workflow.id, + node_id=node_id, + ) + agent_soul = AgentSoulConfig.model_validate(bundle.snapshot.config_snapshot_dict) + variable_mapping = { + **variable_mapping, + **{ + f"{node_id}.metadata_filter.{'.'.join(selector)}": selector + for knowledge_set in agent_soul.knowledge.sets + if knowledge_set.metadata_filtering.mode == "manual" + and knowledge_set.metadata_filtering.conditions is not None + for condition in knowledge_set.metadata_filtering.conditions.conditions + for selector in template_value_selectors(condition.value) + }, + } variable_mapping = inject_default_system_variable_mappings( node_id=node_id, node_type=node_type, diff --git a/api/libs/workflow_template_values.py b/api/libs/workflow_template_values.py new file mode 100644 index 00000000000000..86f37a571a517b --- /dev/null +++ b/api/libs/workflow_template_values.py @@ -0,0 +1,52 @@ +from collections.abc import Iterator, Sequence +from typing import Protocol + +from graphon.nodes.base.variable_template_parser import extract_selectors_from_template +from graphon.variables.segments import Segment +from graphon.variables.template_resolution import VARIABLE_PATTERN + +type TemplateValue = str | Sequence[str] | int | float | None + + +class TemplateVariablePool(Protocol): + def get(self, selector: Sequence[str], /) -> Segment | None: ... + + +def template_value_selectors(value: TemplateValue) -> Iterator[list[str]]: + """Extract workflow references from a scalar string or string sequence.""" + values = [value] if isinstance(value, str) else value + if isinstance(values, Sequence): + for template in values: + for reference in extract_selectors_from_template(template): + yield list(reference.value_selector) + + +def resolve_template_value(value: TemplateValue, variable_pool: TemplateVariablePool) -> TemplateValue: + """Resolve scalar values and string sequences against a workflow variable pool. + + A whole scalar reference retains its string or numeric type; + interpolated text and elements of string lists remain strings. Missing + references fail explicitly rather than becoming literal selector text. + """ + + def get_segment(selector: str) -> Segment: + segment = variable_pool.get(selector.split(".")) + if segment is None: + raise ValueError(f"Workflow variable {selector} is not available.") + return segment + + def resolve_template(template: str) -> str | int | float | None: + if match := VARIABLE_PATTERN.fullmatch(template): + resolved = get_segment(match.group(1)).to_object() + if resolved is None or isinstance(resolved, str | float): + return resolved + if isinstance(resolved, int) and not isinstance(resolved, bool): + return resolved + return str(resolved) + return VARIABLE_PATTERN.sub(lambda match: get_segment(match.group(1)).text, template) + + if isinstance(value, str): + return resolve_template(value) + if isinstance(value, Sequence): + return [str(resolve_template(item)) for item in value] + return value diff --git a/api/tests/unit_tests/commands/test_fix_app_site_missing.py b/api/tests/unit_tests/commands/test_fix_app_site_missing.py index c69f4eb6463746..8666429fc9786f 100644 --- a/api/tests/unit_tests/commands/test_fix_app_site_missing.py +++ b/api/tests/unit_tests/commands/test_fix_app_site_missing.py @@ -66,6 +66,13 @@ def _bind_command_database( return command_sessions, command_session +def _run_fix_app_site_missing() -> None: + """Invoke the command callback through a narrowed, non-optional reference.""" + callback = system_commands.fix_app_site_missing.callback + assert callback is not None + callback() + + def test_fix_app_site_missing_passes_loaded_session_to_signal( monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine, @@ -89,7 +96,7 @@ def create_site(sender: App, *, account: Account, session: Session) -> None: monkeypatch.setattr(system_commands.app_was_created, "send", send) try: - system_commands.fix_app_site_missing.callback() + _run_fix_app_site_missing() finally: command_sessions.remove() @@ -123,7 +130,7 @@ def fail_signal(sender: App, **_kwargs: object) -> None: monkeypatch.setattr(system_commands.app_was_created, "send", MagicMock(side_effect=fail_signal)) try: - system_commands.fix_app_site_missing.callback() + _run_fix_app_site_missing() finally: command_sessions.remove() diff --git a/api/tests/unit_tests/commands/test_reset_encrypt_key_pair.py b/api/tests/unit_tests/commands/test_reset_encrypt_key_pair.py index bb3201bbdaafb5..24afa1cb44a16e 100644 --- a/api/tests/unit_tests/commands/test_reset_encrypt_key_pair.py +++ b/api/tests/unit_tests/commands/test_reset_encrypt_key_pair.py @@ -26,8 +26,10 @@ def _invoke_reset() -> int: + callback = commands.reset_encrypt_key_pair.callback + assert callback is not None try: - commands.reset_encrypt_key_pair.callback() + callback() except SystemExit as e: return int(e.code or 0) return 0 @@ -87,7 +89,7 @@ def _bind_command_to_sqlite(monkeypatch: pytest.MonkeyPatch, session: Session) - monkeypatch.setattr(system_commands, "db", SimpleNamespace(engine=session.get_bind())) -def test_reset_aborts_when_not_self_hosted(monkeypatch, capsys): +def test_reset_aborts_when_not_self_hosted(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: apply_config_overrides(monkeypatch, DEPLOYMENT_EDITION=DeploymentEdition.CLOUD) exit_code = _invoke_reset() @@ -128,8 +130,12 @@ def test_reset_purges_provider_and_tool_tables_for_each_tenant( assert TENANT_ID in captured.out sqlite_session.expire_all() - assert sqlite_session.get(Tenant, TENANT_ID).encrypt_public_key == f"new-key-{TENANT_ID}" - assert sqlite_session.get(Tenant, OTHER_TENANT_ID).encrypt_public_key == f"new-key-{OTHER_TENANT_ID}" + tenant_after = sqlite_session.get(Tenant, TENANT_ID) + other_tenant_after = sqlite_session.get(Tenant, OTHER_TENANT_ID) + assert tenant_after is not None + assert other_tenant_after is not None + assert tenant_after.encrypt_public_key == f"new-key-{TENANT_ID}" + assert other_tenant_after.encrypt_public_key == f"new-key-{OTHER_TENANT_ID}" assert sqlite_session.scalars(select(Provider).where(Provider.provider_type == ProviderType.CUSTOM)).all() == [] assert sqlite_session.scalars(select(ProviderModel)).all() == [] assert sqlite_session.scalars(select(BuiltinToolProvider)).all() == [] diff --git a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py index 2678610cb80bf6..76162384b102c8 100644 --- a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py +++ b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py @@ -10,6 +10,7 @@ from dify_agent.layers.prompt import Config as PromptConfig from dify_agent.protocol import DIFY_AGENT_HISTORY_LAYER_ID, DIFY_AGENT_MODEL_LAYER_ID, DIFY_AGENT_OUTPUT_LAYER_ID from dify_agent.protocol.snapshot import SessionSnapshot +from sqlalchemy.orm import Session from clients.agent_backend import ( DIFY_CONFIG_LAYER_ID, @@ -27,9 +28,18 @@ WorkflowAgentRuntimeRequestBuildError, build_shell_layer_config, ) +from core.workflow.workflow_entry import WorkflowEntry from graphon.file import File, FileTransferMethod, FileType +from graphon.runtime.variable_pool import VariablePool from graphon.variables.segments import ArrayFileSegment, FileSegment, StringSegment -from models.agent import Agent, AgentConfigSnapshot, WorkflowAgentNodeBinding +from models.agent import ( + Agent, + AgentConfigSnapshot, + AgentScope, + AgentSource, + WorkflowAgentBindingType, + WorkflowAgentNodeBinding, +) from models.agent_config_entities import ( AgentSoulConfig, AgentSoulModelConfig, @@ -40,6 +50,7 @@ DeclaredOutputType, WorkflowNodeJobConfig, ) +from models.workflow import Workflow from tests.unit_tests.config_override import apply_config_overrides @@ -228,6 +239,192 @@ def _request_layers(result) -> dict[str, dict[str, object]]: return {layer["name"]: layer for layer in dumped["composition"]["layers"]} +def _metadata_filter_context() -> WorkflowAgentRuntimeBuildContext: + context = _context() + soul = context.snapshot.config_snapshot_dict + soul["knowledge"] = { + "sets": [ + { + "id": "exhibits", + "name": "Exhibits", + "datasets": [{"id": "dataset-1"}], + "query": {"mode": "generated_query"}, + "retrieval": {"mode": "multiple", "top_k": 4}, + "metadata_filtering": { + "mode": "manual", + "conditions": { + "logical_operator": "and", + "conditions": [ + {"name": name, "comparison_operator": "=", "value": "{{#start." + name + "#}}"} + for name in ("guide_program_id", "guide_item_id") + ], + }, + }, + } + ], + } + context.snapshot.config_snapshot = soul + return context + + +def test_workflow_metadata_filters_resolve_current_run_values_without_mutating_snapshot(): + context = _metadata_filter_context() + saved_snapshot = context.snapshot.config_snapshot_dict + builder = WorkflowAgentRuntimeRequestBuilder() + + for program_id, item_id in [(123, 456), (789, 0), ("00123", "00456")]: + pool = VariablePool() + pool.add(["start", "guide_program_id"], program_id) + pool.add(["start", "guide_item_id"], item_id) + pool.add(["sys", "query"], "Describe this exhibit") + result = builder.build(replace(context, variable_pool=pool)) + layer = result.request.model_dump(mode="json")["composition"]["layers"] + knowledge = next(item for item in layer if item["name"] == "knowledge") + filtering = knowledge["config"]["sets"][0]["metadata_filtering"] + + assert filtering["conditions"] == { + "logical_operator": "and", + "conditions": [ + {"name": "guide_program_id", "comparison_operator": "=", "value": program_id}, + {"name": "guide_item_id", "comparison_operator": "=", "value": item_id}, + ], + } + assert context.snapshot.config_snapshot_dict == saved_snapshot + + +def test_workflow_metadata_filter_rejects_missing_variable_before_starting_agent(): + context = _metadata_filter_context() + with pytest.raises(WorkflowAgentRuntimeRequestBuildError, match="start.guide_program_id"): + WorkflowAgentRuntimeRequestBuilder().build(replace(context, variable_pool=VariablePool())) + + +def test_single_step_loads_variables_referenced_only_by_knowledge_filters( + sqlite_session: Session, monkeypatch: pytest.MonkeyPatch +): + apply_config_overrides(monkeypatch, AGENT_BACKEND_BASE_URL="http://agent-backend.invalid") + context = _metadata_filter_context() + context.binding.workflow_version = "draft" + context.binding.binding_type = WorkflowAgentBindingType.INLINE_AGENT + context.agent.scope = AgentScope.WORKFLOW_ONLY + context.agent.source = AgentSource.WORKFLOW + sqlite_session.add_all([context.agent, context.snapshot, context.binding]) + sqlite_session.commit() + workflow = Workflow( + id=context.workflow_id, + tenant_id=context.dify_context.tenant_id, + app_id=context.dify_context.app_id, + graph=json.dumps( + { + "nodes": [ + { + "id": context.node_id, + "data": {"type": "agent", "version": "2", "agent_node_kind": "dify_agent", "title": "Agent"}, + } + ], + "edges": [], + } + ), + ) + source_pool = VariablePool() + source_pool.add(["start", "guide_program_id"], "00123") + source_pool.add(["start", "guide_item_id"], 456) + + class DraftVariableLoader: + def load_variables(self, selectors): + return [value for selector in selectors if (value := source_pool.get(selector)) is not None] + + pool = VariablePool() + WorkflowEntry.single_step_run( + workflow=workflow, + node_id=context.node_id, + user_id=context.dify_context.user_id, + user_inputs={}, + variable_pool=pool, + variable_loader=DraftVariableLoader(), + ) + result = WorkflowAgentRuntimeRequestBuilder().build(replace(context, variable_pool=pool)) + conditions = _request_layers(result)["knowledge"]["config"]["sets"][0]["metadata_filtering"]["conditions"][ + "conditions" + ] + assert [condition["value"] for condition in conditions] == ["00123", 456] + + +def test_single_step_legacy_agent_v2_does_not_resolve_new_agent_binding(monkeypatch): + legacy_node = SimpleNamespace(id="legacy-agent") + monkeypatch.setattr(WorkflowEntry, "_run_node_with_layers", lambda *_args, **_kwargs: iter(())) + monkeypatch.setattr( + "core.workflow.workflow_entry.DifyNodeFactory.from_graph_init_context", + lambda **_kwargs: SimpleNamespace(create_node=lambda _config: legacy_node), + ) + + def reject_new_agent_binding(*_args, **_kwargs): + raise AssertionError("Legacy Agent must not resolve an Agent v2 binding") + + monkeypatch.setattr("core.workflow.workflow_entry.WorkflowAgentBindingResolver.resolve", reject_new_agent_binding) + workflow = Workflow( + id="workflow-id", + tenant_id="tenant-id", + app_id="app-id", + graph=json.dumps( + { + "nodes": [{"id": "legacy-agent", "data": {"type": "agent", "version": "2", "title": "Legacy Agent"}}], + "edges": [], + } + ), + ) + node, _ = WorkflowEntry.single_step_run( + workflow=workflow, + node_id="legacy-agent", + user_id="user-id", + user_inputs={}, + variable_pool=VariablePool(), + ) + assert node is legacy_node + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + ("program:{{#start.guide_program_id#}}/{{#start.guide_item_id#}}", "program:123/00456"), + (["{{#start.guide_program_id#}}", "{{#start.guide_item_id#}}", "constant"], ["123", "00456", "constant"]), + (0, 0), + (None, None), + ("", ""), + ], +) +def test_metadata_filters_resolve_templates_and_lists_preserving_constants(value, expected): + context = _metadata_filter_context() + soul = context.snapshot.config_snapshot_dict + condition = soul["knowledge"]["sets"][0]["metadata_filtering"]["conditions"]["conditions"][0] + condition.update(value=value, id="condition-1", metadata_id="program-metadata") + context.snapshot.config_snapshot = soul + pool = VariablePool() + pool.add(["start", "guide_program_id"], 123) + pool.add(["start", "guide_item_id"], "00456") + result = WorkflowAgentRuntimeRequestBuilder().build(replace(context, variable_pool=pool)) + conditions = _request_layers(result)["knowledge"]["config"]["sets"][0]["metadata_filtering"]["conditions"][ + "conditions" + ] + assert conditions[0] == {"name": "guide_program_id", "comparison_operator": "=", "value": expected} + + +@pytest.mark.parametrize("mode", ["disabled", "automatic"]) +def test_inactive_manual_filters_do_not_require_workflow_variables(mode): + context = _metadata_filter_context() + soul = context.snapshot.config_snapshot_dict + soul["knowledge"]["sets"][0]["metadata_filtering"]["mode"] = mode + if mode == "automatic": + soul["knowledge"]["sets"][0]["metadata_filtering"]["model_config"] = { + "provider": "openai", + "name": "gpt-test", + "mode": "chat", + "completion_params": {}, + } + context.snapshot.config_snapshot = soul + result = WorkflowAgentRuntimeRequestBuilder().build(replace(context, variable_pool=VariablePool())) + assert _request_layers(result)["knowledge"]["config"]["sets"][0]["metadata_filtering"]["mode"] == mode + + def _workflow_user_prompt(result) -> str: from clients.agent_backend import WORKFLOW_USER_PROMPT_LAYER_ID diff --git a/api/tests/unit_tests/libs/test_workflow_template_values.py b/api/tests/unit_tests/libs/test_workflow_template_values.py new file mode 100644 index 00000000000000..9ef7025b8bc0bf --- /dev/null +++ b/api/tests/unit_tests/libs/test_workflow_template_values.py @@ -0,0 +1,27 @@ +import pytest + +from graphon.runtime.variable_pool import VariablePool +from libs.workflow_template_values import resolve_template_value, template_value_selectors + + +@pytest.mark.parametrize("value", [0, 1.5, None]) +def test_constant_filter_values_do_not_request_workflow_variables(value: int | float | None) -> None: + assert list(template_value_selectors(value)) == [] + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (True, "True"), + (False, "False"), + (["I10", "I20"], "['I10', 'I20']"), + ({"guide_item_id": "I10"}, "{'guide_item_id': 'I10'}"), + ], +) +def test_boolean_and_collection_filter_references_resolve_to_strings( + value: bool | list[str] | dict[str, str], expected: str +) -> None: + pool = VariablePool() + pool.add(["start", "filter_value"], value) + + assert resolve_template_value("{{#start.filter_value#}}", pool) == expected diff --git a/api/tests/unit_tests/pyrefly.toml b/api/tests/unit_tests/pyrefly.toml index ac608fef71b6a4..62936434dd4f13 100644 --- a/api/tests/unit_tests/pyrefly.toml +++ b/api/tests/unit_tests/pyrefly.toml @@ -18,11 +18,9 @@ project-excludes = [ "commands/test_clean_expired_messages.py", "commands/test_data_migration_commands.py", "commands/test_data_migration_wizard.py", - "commands/test_fix_app_site_missing.py", "commands/test_generate_swagger_markdown_docs.py", "commands/test_generate_swagger_specs.py", "commands/test_lint_response_contracts.py", - "commands/test_reset_encrypt_key_pair.py", "commands/test_upgrade_db.py", "configs/test_dify_config.py", "configs/test_env_consistency.py", diff --git a/packages/dify-ui/docs/selection.md b/packages/dify-ui/docs/selection.md index 167622ceadf36b..5fdf3989e6039b 100644 --- a/packages/dify-ui/docs/selection.md +++ b/packages/dify-ui/docs/selection.md @@ -29,6 +29,13 @@ Autocomplete, Combobox, and Select popups use Base UI's `--anchor-width` and `-- variables to follow their trigger while clamping to the viewport. Do not replace that sizing with a fixed width or an unclamped minimum width. +When a Combobox renders its input inside the popup, do not add a control that clears the typed +query. The query is per-open state that Base UI resets: choosing an item clears it in `multiple` +mode and closes the popup in single mode, and closing the popup (Escape, outside press) clears it. +A clear control there adds a second tab stop to the popup's focus loop and a second x that means +something other than `ComboboxClear`, which clears the selected value. Outside the popup, use +`ComboboxClear` or `AutocompleteClear`. + Use `Radio` for the default radio appearance. Use `RadioItem` when custom content itself is a radio item, and put `RadioControl` inside it for Dify UI's visual indicator. `RadioControl` is a visual part, not a standalone radio. diff --git a/packages/dify-ui/src/autocomplete/index.stories.tsx b/packages/dify-ui/src/autocomplete/index.stories.tsx index b47cd73a9cbc71..7aabbd20741597 100644 --- a/packages/dify-ui/src/autocomplete/index.stories.tsx +++ b/packages/dify-ui/src/autocomplete/index.stories.tsx @@ -300,7 +300,7 @@ function SuggestionItem({ item, dense }: { item: Suggestion; dense?: boolean })